1. 从"标注贵如油"说起:为什么需要无监督域自适应
做过深度学习落地的人都有一个共同体会:标注数据是奢侈品,不是日用品。在一个特定数据集上训练一个模型,比如用某个公开的街景数据集训练语义分割网络,模型在该数据集测试集上跑出很高的精度并不难。但一旦把模型搬到真实业务场景——换个城市的街景、换个季节的光照、换个摄像头型号——精度往往断崖式下跌。这个现象在学术上叫域偏移(Domain Shift),而解决它的核心思路就是域自适应(Domain Adaptation)。
域自适应的基本设定是这样的:我们有一个源域(Source Domain),上面有大量带标注的数据;还有一个目标域(Target Domain),上面只有数据、没有标注,或者只有极少量标注。目标就是让在源域上训练的模型,在目标域上也能表现良好。根据目标域标注量的多少,域自适应又分为无监督、半监督和少样本等不同设定。其中**无监督域自适应(Unsupervised Domain Adaptation, UDA)**是最难也最有价值的一档——目标域一个标签都没有。
今天要聊的这篇工作,标题是"Unsupervised Domain Adaptation by Backpropagation",它提出了一种极其优雅的思路:不靠对抗训练里的独立判别器,而是直接在特征提取器和分类器之间插一层"梯度反转层"(Gradient Reversal Layer, GRL),让整个网络用标准的反向传播就能完成域对抗训练。这个想法后来被广泛称为DANN(Domain-Adversarial Neural Network),是域自适应领域被引用最多、影响最深远的基线方法之一。
这篇文章适合谁看?如果你正在做模型跨域部署、跨设备迁移、跨场景泛化,或者你只是好奇"梯度反转"这个听起来很魔法的操作到底怎么实现,那这篇内容会从原理、结构、代码实现、训练技巧到踩坑经验,给你一套可以直接复现的完整方案。我会尽量用从业者之间交流的口吻,把论文里没写清楚、但实操中一定会遇到的细节讲透。
2. 域自适应的核心矛盾:特征要"通用"还是要"判别"
2.1 一个生活化的类比:方言翻译的两难
要理解 DANN 的设计动机,先想一个场景。假设你要训练一个"普通话语音识别"系统,但实际部署时用户说的是各地方言。源域是大量普通话录音加转写文本,目标域是方言录音但没有转写。你希望模型学到的是与口音无关的语音内容表征,而不是记住普通话特有的声调模式。
这里有一个根本矛盾:特征提取器既要让分类器能准确分类(判别性),又要让源域和目标域的特征分布无法区分(域不变性)。这两个目标在优化方向上经常是冲突的。如果特征太"通用",分类器可能学不到足够的判别信息;如果特征太"判别",它就会把源域和目标域的差异也编码进去,导致域偏移。
DANN 的解决思路非常直接:让特征提取器和域判别器玩一个对抗游戏。域判别器努力区分特征是来自源域还是目标域,特征提取器则努力生成让域判别器分不出来的特征。最终达到纳什均衡时,特征就具备了域不变性。
2.2 和传统对抗域自适应的关键区别
在 DANN 之前,很多域自适应方法也用了对抗思想,但通常需要单独训练一个域判别器,然后交替优化。这种做法的工程复杂度很高:你要维护两套优化器、两套学习率、两套更新节奏,还要小心判别器太强导致梯度消失、太弱导致域不变性学不好。
DANN 的巧妙之处在于:它把域判别器和分类器共享同一个特征提取器,并且通过一个梯度反转层把域判别器的梯度"取反"后再传回特征提取器。这样一来,整个网络可以用一个损失函数、一次反向传播完成端到端训练。你不需要写复杂的交替训练循环,只需要在标准分类网络里加一个分支和一个自定义层。
提示:梯度反转层在正向传播时是恒等映射,在反向传播时把梯度乘以一个负数(通常是 -λ)。这个 λ 不是固定的,而是从 0 逐渐增加到 1 的调度参数,目的是让训练初期特征提取器先学好判别特征,后期再逐渐施加域不变性约束。
2.3 为什么这个设计能 work:从博弈论视角看
从博弈论角度看,DANN 是一个两人零和博弈的近似。特征提取器 θ_f 试图最小化分类损失,同时最大化域判别损失(即让域判别器分不清);域判别器 θ_d 试图最小化域判别损失。分类器 θ_y 试图最小化分类损失。
用数学表达就是:
- 分类器目标:min L_y(θ_f, θ_y)
- 域判别器目标:min L_d(θ_f, θ_d)
- 特征提取器目标:min L_y - λ·L_d
注意特征提取器的目标里有一个负号,这就是"最大化域判别损失"的体现。梯度反转层在实现上等价于:反向传播时,来自域判别器的梯度经过 GRL 后被乘以 -λ,然后加到特征提取器的梯度上。这样特征提取器在更新时,就会朝着"让域判别器犯错"的方向走。
这个博弈的均衡点是:域判别器对源域和目标域的输出概率都接近 0.5,即完全无法区分。此时特征分布已经对齐,分类器在目标域上的泛化能力显著提升。
3. 网络结构拆解:三个模块如何协同工作
3.1 特征提取器:共享主干的选择与设计
特征提取器是整个 DANN 的基石。它的输出既要送给分类器做类别预测,又要送给域判别器做域分类。因此,特征提取器的容量和结构直接决定了域不变性的上限。
在实际操作中,特征提取器通常是一个卷积神经网络(处理图像)或循环神经网络/Transformer(处理序列)。对于图像任务,常见选择是 ResNet-50、VGG-16 或更轻量的 MobileNet。对于文本任务,可以用 BiLSTM 或 BERT 的编码器部分。
这里有一个容易被忽略的细节:特征提取器的输出维度不能太小。如果特征维度太低,分类器和域判别器共享的信息空间过于狭窄,两个任务会互相干扰,导致分类精度下降。我的经验是,特征维度至少保持在 256 以上,对于复杂图像任务建议 512 或 1024。
另一个关键点是是否共享底层参数。DANN 原文采用的是完全共享的特征提取器,即分类器和域判别器看到的是同一份特征。但后续一些改进工作发现,在特征提取器的最后几层做部分解耦(比如分类分支和域分支各接一个小的全连接头)效果更好。这相当于给两个任务留了一点"私人空间",减少冲突。
3.2 分类器:源域监督信号的唯一来源
分类器接收特征提取器的输出,输出类别概率分布。它的训练信号完全来自源域的标注数据,因为目标域没有标签。分类器通常就是一个或几个全连接层,最后接 softmax。
分类器的设计要注意两点。第一,分类器的容量不宜过大。如果分类器太强,它可能会记住源域特有的模式,导致特征提取器被迫编码更多源域特有信息,反而加剧域偏移。第二,分类器的输出维度要和任务匹配。对于二分类任务,输出 2 维;对于 10 类任务,输出 10 维。不要为了"统一"而随意扩大输出维度,那会引入不必要的参数和过拟合风险。
分类损失通常用交叉熵损失。对于类别不平衡的源域数据,可以考虑加类别权重或使用 focal loss。但要注意,分类损失的权重和域判别损失的权重需要平衡,这个后面会详细讲。
3.3 域判别器:二分类的"域警察"
域判别器是一个二分类器,输入是特征提取器的输出,输出是"该特征来自源域"的概率。它的结构通常比分类器更简单,因为域分类任务本身比类别分类容易——只需要区分两个域,而不是几十个类别。
域判别器的设计有一个经典陷阱:如果域判别器太强,它会迅速把源域和目标域完全分开,导致梯度饱和,特征提取器学不到有用的对齐信号。如果域判别器太弱,它又无法提供足够的对抗压力。实践中,域判别器通常用 2-3 层全连接网络,隐藏层维度可以比特征维度小一些,比如 256 或 512。
域判别损失用二分类交叉熵。注意,域判别器的输入特征在训练时应该做梯度反转处理,但在推理时不需要。也就是说,GRL 只在反向传播时起作用,正向传播时它就是一个恒等变换。
3.4 梯度反转层:一行代码背后的数学
梯度反转层的实现极其简单,但理解它的数学含义很重要。假设 GRL 的输入是 x,输出是 y:
- 正向传播:y = x
- 反向传播:∂L/∂x = -λ · ∂L/∂y
其中 λ 是一个动态调整的超参数。在 PyTorch 中,可以通过自定义 autograd.Function 实现:
import torch import torch.nn as nn from torch.autograd import Function class GradientReversalFunction(Function): @staticmethod def forward(ctx, x, lambda_): ctx.lambda_ = lambda_ return x.view_as(x) @staticmethod def backward(ctx, grad_output): return -ctx.lambda_ * grad_output, None class GradientReversalLayer(nn.Module): def __init__(self, lambda_=1.0): super().__init__() self.lambda_ = lambda_ def forward(self, x): return GradientReversalFunction.apply(x, self.lambda_)这段代码的关键在于backward函数返回了-ctx.lambda_ * grad_output。负号就是"反转"的来源,λ 控制反转的强度。
λ 的调度策略通常是:λ = 2/(1+exp(-10·p)) - 1,其中 p 是训练进度(从 0 到 1)。这个公式让 λ 从 0 平滑上升到 1。训练初期 λ 接近 0,域判别器的梯度几乎不影响特征提取器,特征提取器专心学分类;训练后期 λ 接近 1,域不变性约束逐渐加强。
注意:λ 的调度曲线不要设得太陡。如果 λ 上升太快,特征提取器还没学好判别特征就被强行拉向域不变,会导致分类精度崩掉。我试过把上升周期设为总训练步数的 30%-50%,效果比较稳。
4. 训练流程与损失函数:一次反向传播搞定对抗
4.1 总损失函数的构成与权重平衡
DANN 的总损失是分类损失和域判别损失的加权和:
L_total = L_y + α · L_d
其中 L_y 是源域分类损失,L_d 是域判别损失,α 是平衡系数。注意,由于梯度反转层的存在,特征提取器在反向传播时收到的域判别梯度是 -λ·∂L_d/∂θ_f,所以实际优化的是 L_y - λ·L_d。
α 和 λ 的关系容易混淆。简单说,α 是损失函数层面的权重,λ 是梯度反转层面的强度。在实践中,很多人直接把 α 设为 1,只调 λ 的调度。这样更简单,也更容易复现。
但如果你发现分类精度和域对齐效果难以兼顾,可以尝试把 α 设小一点,比如 0.1 或 0.5。这相当于降低域判别任务在总损失中的话语权,让分类任务主导训练。
4.2 训练循环的伪代码与关键细节
下面是一个标准的 DANN 训练循环伪代码:
for epoch in range(num_epochs): for batch_src, batch_tgt in zip(source_loader, target_loader): # 合并源域和目标域数据 x = torch.cat([batch_src.x, batch_tgt.x], dim=0) y_src = batch_src.y # 域标签:源域为0,目标域为1 domain_labels = torch.cat([ torch.zeros(batch_src.size(0)), torch.ones(batch_tgt.size(0)) ]) # 前向传播 features = feature_extractor(x) class_pred = classifier(features[:batch_src.size(0)]) domain_pred = domain_classifier(gradient_reversal(features)) # 计算损失 loss_y = cross_entropy(class_pred, y_src) loss_d = binary_cross_entropy(domain_pred, domain_labels) loss_total = loss_y + alpha * loss_d # 反向传播与优化 optimizer.zero_grad() loss_total.backward() optimizer.step() # 更新lambda update_lambda_schedule()这里有几个实操细节值得展开。第一,源域和目标域的 batch 大小要匹配。如果源域 batch 是 32,目标域 batch 也应该是 32,这样域判别器看到的正负样本是平衡的。如果目标域数据量远大于源域,可以在每个 epoch 里对源域做 oversampling,或者对目标域做 undersampling。
第二,域标签的分配要一致。我习惯把源域标为 0,目标域标为 1,但反过来也可以,只要保持一致。有些实现会把域判别器的输出维度设为 2,用交叉熵损失,这也行,但二分类 BCE 更简洁。
第三,优化器的选择。DANN 原文用的是带 momentum 的 SGD,但实践中 Adam 往往收敛更快。如果数据量很大,SGD + momentum 的泛化性可能更好。学习率方面,特征提取器可以用较小的学习率(比如 1e-4),分类器和域判别器可以用稍大的学习率(比如 1e-3),因为后两者是随机初始化的,需要更快学习。
4.3 λ 调度策略的对比与选择
λ 的调度是 DANN 训练中最关键的超参数之一。常见策略有三种:
| 调度策略 | 公式 | 特点 | 适用场景 |
|---|---|---|---|
| 固定 λ | λ = 常数 | 简单,但难以平衡早期和后期 | 源域和目标域差异较小 |
| 线性上升 | λ = min(1, p) | 平稳,但后期可能不够强 | 通用场景 |
| 指数上升 | λ = 2/(1+exp(-10p)) - 1 | 早期慢、后期快,符合"先学判别再对齐" | 域差异较大的场景 |
我个人的经验是:如果源域和目标域差异很大(比如合成数据到真实数据),用指数上升;如果差异较小(比如两个不同摄像头的数据),用线性上升就够了。固定 λ 只在快速实验时用,正式训练不建议。
另外,λ 的最大值不一定要到 1。有些任务里 λ 到 0.5 左右效果最好,再大反而损害分类精度。这个需要根据验证集(如果有目标域验证集)或源域测试集来调。
5. 实操中的坑与调参经验:论文不会告诉你的细节
5.1 域判别器太强导致的梯度消失
这是 DANN 训练中最常见的失败模式。表现是:训练初期分类损失正常下降,但域判别损失迅速降到接近 0,然后分类精度开始下降,目标域精度更是一塌糊涂。
原因在于:域判别器太强,把源域和目标域完全分开了,此时域判别器的输出接近 0 或 1,梯度饱和,特征提取器收到的对抗信号几乎为零。更糟糕的是,特征提取器为了"骗过"域判别器,可能会把特征空间压缩到一个很小的区域,导致分类信息丢失。
解决方案有几种。第一,降低域判别器的学习率,比如设为特征提取器的 0.1 倍。第二,给域判别器加 dropout 或权重衰减,限制它的拟合能力。第三,减小 λ 的最大值,比如从 1 降到 0.3。第四,使用更平滑的域判别损失,比如 label smoothing,把域标签从 0/1 改成 0.1/0.9。
我试过最有效的一招是:在域判别器的输出上做梯度裁剪,把梯度范数限制在一个较小值(比如 1.0)。这样即使域判别器很强,传回特征提取器的梯度也不会爆炸。
5.2 目标域伪标签的引入时机
纯 DANN 只用了域对齐,没有利用目标域的任何标签信息。但在实际项目中,我们往往可以在训练后期给目标域生成伪标签,进一步提升性能。这就是DANN + 自训练(Self-training)的思路。
具体做法是:先用 DANN 训练一个模型,然后在目标域上推理,取置信度高的样本作为伪标签,再用这些伪标签微调模型。伪标签的阈值通常设 0.9 或 0.95,太低会引入噪声,太高则样本太少。
这里有一个时机问题:伪标签不要一开始就加。如果 DANN 还没训练好,目标域预测全是错的,伪标签只会强化错误。我的经验是,等 DANN 的域判别损失稳定在 0.6-0.7 左右(接近随机猜测的 0.693),再开始加伪标签。
5.3 特征归一化与域对齐的相互影响
特征提取器的输出是否需要做归一化(比如 BatchNorm 或 L2 归一化)?这个问题在论文里没有明确答案,但实操中影响很大。
如果特征提取器最后有 BatchNorm 层,注意 BatchNorm 在训练时用的是当前 batch 的统计量,而源域和目标域的 batch 是混合的。这会导致统计量被两个域的分布"拉扯",反而可能损害域对齐。一种做法是在特征提取器输出后、送入域判别器前,加一个 L2 归一化,把特征投影到单位球面上。这样域判别器看到的是方向信息,而不是幅度信息,对齐更稳定。
另一种做法是使用 AdaBN 或 TransNorm,在目标域上重新估计 BatchNorm 的统计量。这相当于在推理时做一次轻量的域自适应,和 DANN 互补。
5.4 类别不平衡与域不平衡的叠加
如果源域本身类别不平衡,同时目标域的类别分布又和源域不同,DANN 会面临双重挑战。域判别器可能把"类别差异"误判为"域差异",导致对齐方向错误。
一个实用的技巧是在域判别器中加入类别条件。也就是说,域判别器不仅看特征,还看分类器预测的类别(或真实类别,如果源域有标签)。这样域判别器学的是"在给定类别下,这个特征来自哪个域",而不是"这个特征来自哪个域"。这被称为Conditional DANN或CDAN,是 DANN 的重要改进方向。
实现上,可以把分类器的 softmax 输出和特征做外积,或者直接拼接,然后送入域判别器。这样域判别器就能感知类别信息,避免把类别差异当成域差异。
6. 从 DANN 出发:后续改进与适用边界
6.1 DANN 的局限性:什么时候它不够用
DANN 虽然优雅,但并不是万能药。它的核心假设是:存在一个共享的特征空间,使得源域和目标域在这个空间里的分布可以对齐,且类别条件分布一致。如果这个假设不成立,DANN 的效果会大打折扣。
具体来说,以下几种情况 DANN 可能不够用:
- 标签空间不一致:源域有 10 类,目标域只有 5 类,或者类别定义不同。这时强行对齐会导致负迁移。
- 域差异极大:比如源域是合成图像,目标域是真实照片,两者的底层特征分布差异太大,简单的对抗对齐难以弥合。
- 目标域内部存在多个子域:比如目标域包含白天和夜晚两种场景,它们的分布差异很大,单一域判别器无法同时对齐。
针对这些问题,后续工作提出了很多改进,比如多源域自适应、多目标域自适应、类条件对齐、最优传输等。但 DANN 作为基线,仍然是理解这些改进的起点。
6.2 和 MMD、CORAL 等方法的对比
除了对抗训练,域自适应还有另一大类方法:基于统计距离的方法,比如 MMD(最大均值差异)和 CORAL(相关对齐)。它们不训练域判别器,而是直接最小化源域和目标域特征分布之间的统计距离。
| 方法 | 核心思想 | 优点 | 缺点 |
|---|---|---|---|
| DANN | 对抗训练,域判别器 | 端到端,理论优雅 | 训练不稳定,超参敏感 |
| MMD | 最小化核均值差异 | 简单,无需对抗 | 核函数选择敏感,计算量大 |
| CORAL | 对齐二阶统计量 | 无需对抗,计算高效 | 只对齐均值和协方差,表达能力有限 |
| CDAN | 条件对抗 | 考虑类别信息 | 实现复杂,计算量增加 |
实操中,我通常会先用 CORAL 或 MMD 快速跑一个基线,因为它们训练稳定、代码简单。如果效果不够,再上 DANN。如果 DANN 也不够,再考虑 CDAN 或更复杂的方案。这个顺序可以节省大量调参时间。
6.3 在真实项目中的落地建议
最后分享几条在真实项目中落地 DANN 的经验。
第一,不要指望 DANN 能解决所有域偏移问题。如果源域和目标域的差异是本质性的(比如模态不同),DANN 只能缓解,不能根治。这时候需要考虑数据层面的方案,比如在目标域采集少量标注数据做微调。
第二,验证集的设计很关键。如果目标域完全没有标签,你无法直接评估模型在目标域上的性能。一种做法是:在目标域上留出一小部分数据,人工标注后作为验证集。如果连这个都做不到,可以用源域留出集 + 目标域无监督指标(如熵、置信度)来间接评估。
第三,训练时间要留足。DANN 的对抗训练通常比普通分类训练慢 1.5-2 倍,因为要同时优化两个任务。而且 λ 调度需要足够的训练步数才能完成。如果训练步数不够,λ 还没上升到有效值,域对齐就没效果。
第四,保存多个检查点。DANN 的训练曲线可能波动较大,最佳模型不一定在最后一个 epoch。建议每隔几个 epoch 保存一次,最后用验证集选最优。
提示:如果你在目标域上有少量标注数据,哪怕只有几十个样本,也可以用来做模型选择。这比完全无监督的模型选择可靠得多。
7. 一个可复现的最小实现框架
7.1 数据准备与域划分
假设我们有一个图像分类任务,源域是某个公开数据集,目标域是另一个风格不同的数据集。数据准备的关键是保证源域和目标域的类别空间一致,即两个域包含相同的类别,只是分布不同。
from torch.utils.data import DataLoader, ConcatDataset source_dataset = SourceDataset(root='path/to/source') target_dataset = TargetDataset(root='path/to/target') source_loader = DataLoader(source_dataset, batch_size=32, shuffle=True, drop_last=True) target_loader = DataLoader(target_dataset, batch_size=32, shuffle=True, drop_last=True) # 注意:目标域没有标签,TargetDataset只返回图像这里drop_last=True很重要,因为要保证每个 batch 的源域和目标域样本数一致。如果目标域数据量少于源域,可以在每个 epoch 里重复采样目标域。
7.2 模型定义与初始化
class DANN(nn.Module): def __init__(self, num_classes=10, feature_dim=512): super().__init__() self.feature_extractor = nn.Sequential( nn.Conv2d(3, 64, 5, padding=2), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 5, padding=2), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2), nn.Flatten(), nn.Linear(128*8*8, feature_dim), nn.BatchNorm1d(feature_dim), nn.ReLU() ) self.classifier = nn.Sequential( nn.Linear(feature_dim, 256), nn.ReLU(), nn.Dropout(0.5), nn.Linear(256, num_classes) ) self.domain_classifier = nn.Sequential( nn.Linear(feature_dim, 256), nn.ReLU(), nn.Dropout(0.5), nn.Linear(256, 1) ) self.grl = GradientReversalLayer() def forward(self, x, lambda_=1.0): features = self.feature_extractor(x) class_out = self.classifier(features) reversed_features = self.grl(features, lambda_) domain_out = self.domain_classifier(reversed_features) return class_out, domain_out注意GradientReversalLayer的forward需要接收lambda_参数,并在每次迭代时更新。分类器和域判别器都加了 dropout,这是为了防止过拟合,尤其是域判别器。
7.3 训练与评估的完整流程
model = DANN(num_classes=10).cuda() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) ce_loss = nn.CrossEntropyLoss() bce_loss = nn.BCEWithLogitsLoss() total_steps = len(source_loader) * num_epochs step = 0 for epoch in range(num_epochs): model.train() for (src_x, src_y), (tgt_x, _) in zip(source_loader, target_loader): src_x, src_y, tgt_x = src_x.cuda(), src_y.cuda(), tgt_x.cuda() # 计算lambda p = step / total_steps lambda_ = 2 / (1 + math.exp(-10 * p)) - 1 # 合并数据 x = torch.cat([src_x, tgt_x], dim=0) domain_labels = torch.cat([ torch.zeros(src_x.size(0)), torch.ones(tgt_x.size(0)) ]).cuda() class_out, domain_out = model(x, lambda_) # 分类损失只用源域 loss_y = ce_loss(class_out[:src_x.size(0)], src_y) # 域损失用全部 loss_d = bce_loss(domain_out.squeeze(), domain_labels) loss = loss_y + loss_d optimizer.zero_grad() loss.backward() optimizer.step() step += 1 # 评估 model.eval() # 在源域测试集和目标域测试集上分别评估评估时要注意:域判别器的输出在推理时不需要梯度反转,所以直接调用model.feature_extractor和model.classifier即可,不经过 GRL。
7.4 结果分析与常见曲线形态
训练完成后,你通常会看到这样的曲线:
- 分类损失:持续下降,最后趋于平稳。
- 域判别损失:先下降,然后回升到 0.6-0.7 左右并震荡。这是正常的,说明域判别器在对抗中逐渐失去优势。
- 源域精度:持续上升,最后接近 100%(如果源域训练充分)。
- 目标域精度:先上升,然后可能略有下降。最佳目标域精度通常出现在域判别损失稳定在 0.6-0.7 的时候。
如果域判别损失一直很低(比如 0.1 以下),说明域判别器太强,需要降低它的学习率或增加 dropout。如果域判别损失一直很高(比如 0.9 以上),说明域判别器太弱,需要提高它的学习率或减少正则化。
如果目标域精度始终不上升,可能的原因包括:源域和目标域差异太大、λ 调度不合适、特征维度太低、或者类别空间不一致。这时候需要逐一排查,而不是盲目调参。
8. 一些个人体会与后续可扩展的方向
DANN 这个工作最让我佩服的地方,是它把复杂的对抗训练压缩成了一个自定义层和一次反向传播。这种"用工程技巧简化理论框架"的思路,在深度学习落地中非常值得学习。很多时候,我们不需要最复杂的模型,而是需要最稳定、最容易复现的方案。
在实际项目中,我通常会把 DANN 作为域自适应的第一选择,但不是唯一选择。如果数据量允许,我会优先考虑在目标域采集少量标注数据做微调,因为那是最直接、最可靠的方法。DANN 更适合那些目标域标注完全不可得的场景,比如隐私敏感数据、罕见设备数据、或者标注成本极高的专业领域。
后续可以扩展的方向包括:把 DANN 和自训练结合,用伪标签迭代提升;把 DANN 扩展到多源域或多目标域;把 DANN 用到目标检测、语义分割等结构化预测任务上。这些方向都有不少公开工作可以参考,但核心思想仍然是那个简单的梯度反转层。
最后分享一个小技巧:如果你在复现 DANN 时发现效果不稳定,可以先关掉域判别分支,只训练分类器,确认分类器在源域上能正常收敛。然后再打开域判别分支,把 λ 的最大值设小一点(比如 0.1),观察目标域精度是否提升。如果提升,再逐步增大 λ。这个"从简到繁"的调试顺序,能帮你快速定位问题出在分类器还是域对齐上。