Series · Transfer Learning · Chapter 3

迁移学习(三):域适应

域适应实战指南:协变量偏移、标签偏移、DANN 梯度反转、MMD 对齐、CORAL、自训练、AdaBN,以及一份可运行的 DANN 完整实现。

你的自动驾驶模型在加州的晴天里跑得无懈可击。然后一进西雅图就开始下雨,Top-1 准确率从 95% 跌到 70%。模型本身没有变差——是数据分布变了,而你的训练集从未告诉它"傍晚的湿沥青"长什么样子。

这就是**域适应(domain adaptation)**要解决的日常问题:源域有大量标注数据,目标域只有未标注数据,但模型必须在目标域上跑出业务可接受的性能。本文从理论第一原理一路推到一份可运行的 DANN 实现。

你将学到

  • 三种分布偏移——协变量偏移、标签偏移、概念偏移——以及各自的标准修正方法
  • Ben-David 上界:为什么域适应有理论可能性,以及它确切告诉我们要去最小化哪个量
  • DANN:用梯度反转层在一次反向传播里完成对抗对齐
  • MMD 与 CORAL:两种非对抗的、显式的分布对齐损失
  • 自训练、AdaBN、CycleGAN、ADDA——现代工具箱里的其他常用件
  • 一份完整的 PyTorch DANN 实现
  • 一棵方法选择决策树,以及 Office-31 / DomainNet 上的对比数字

**前置:**本系列第 1、2 章;对 GAN 式对抗训练有基本了解。


1. 分布偏移的三种面孔

**域(domain)**由特征空间 $\mathcal{X}$ 和边缘分布 $P(X)$ 构成;**任务(task)**由标签空间 $\mathcal{Y}$ 和条件分布 $P(Y \mid X)$ 构成。域适应研究的就是源域和目标域在其中某一项上不一致时该怎么办。

设定源域 $\mathcal{D}_S$目标域 $\mathcal{D}_T$目标
数据大量带标注 $(x_i, y_i)$大多无标注 $x_j$学到 $f: \mathcal{X} \to \mathcal{Y}$,在 $\mathcal{D}_T$ 上工作

源域与目标域的特征对齐

整张图就是问题的全部:适应之前,源域学到的判别边界穿过了目标域几乎没有数据的空白区;适应之后,两个域共享同一个特征流形,同一条边界对两边都成立。

1.1 协变量偏移——输入分布变了

$$P_S(X) \neq P_T(X), \qquad P_S(Y \mid X) = P_T(Y \mid X)$$

标注规则没变,只是观测到的东西不一样了:

  • 用 2020 年邮件训练的垃圾邮件分类器在 2026 年部署:主题在变,但"什么是垃圾邮件"的判断标准没变。
  • 西门子 CT 上训练的模型在 GE CT 上推断:成像特性不一样,但放射科医生看片子的诊断标准是一样的。

标准做法——重要性加权。给每个源样本乘上密度比 $w(x) = P_T(x) / P_S(x)$,加权后的源域 ERM 就在估计目标域风险:

$$\mathbb{E}_{P_T}[\ell(f(X), Y)] = \mathbb{E}_{P_S}\!\left[\frac{P_T(X)}{P_S(X)}\,\ell(f(X), Y)\right].$$

高维下直接估密度无望,所以工程上直接估比值——KLIEP、uLSIF,或者训练一个分类器区分源/目标,贝叶斯最优分类器的 logit 等价于这个比值。

1.2 标签偏移——类别先验变了

$$P_S(Y) \neq P_T(Y), \qquad P_S(X \mid Y) = P_T(X \mid Y)$$

类别条件下的样本长得没变,只是类别比例变了:

  • ICU 病人数据训练的模型部署到门诊,疾病流行率天差地别。
  • 在年轻用户里做 A/B 测出来的推荐模型,铺到全年龄段用户。

**标准做法。**用 EM(BBSE / RLLS)在无标注目标数据上估 $P_T(Y)$,然后把源域模型的输出概率按 $P_T(y) / P_S(y)$ 重新加权再归一化。

1.3 概念偏移——规则本身变了

$$P_S(Y \mid X) \neq P_T(Y \mid X)$$

最难的一种。“sick” 在音乐评论里是褒义(“这段 beat 太 sick 了”),在产品评论里是贬义——同一个词,不同的标签。如果完全没有目标域标签,没人能解开这个绳结,必须至少有少量目标域标注(半监督 DA)。


2. 理论:Ben-David 上界

为什么"对齐分布"这件事在理论上行得通?经典答案是 Ben-David 等人 (2010) 的上界。对任意假设 $h \in \mathcal{H}$:

$$ \epsilon_T(h) \;\leq\; \epsilon_S(h) \;+\; \tfrac{1}{2}\, d_{\mathcal{H}\Delta\mathcal{H}}(\mathcal{D}_S, \mathcal{D}_T) \;+\; \lambda^{*}. $$
含义你能做什么
$\epsilon_S(h)$源域风险把源域训得更好
$d_{\mathcal{H}\Delta\mathcal{H}}$两个域的对称差散度这一项就是域适应在压缩的东西
$\lambda^{*}$联合最优预测器的误差不可压缩——它若大,无解

两条结论:

  1. **域适应被一个 oracle 上界卡死。**如果源域和目标域的任务在本质上不一样($\lambda^*$ 大),任何方法都救不了你——你需要的是新标注,不是更花的损失函数。
  2. **域散度有一个能跑的代理。**训练一个二分类器区分源域和目标域特征,如果它的准确率接近 50%,说明你的特征已经域不变了。这正是 DANN 自动化的机制。

3. DANN:一次反向传播完成的对抗对齐

域对抗神经网络(Ganin 等,2016)是最有影响力的对抗式方法,也是对"压缩域散度代理"这一思想最干净的实现。

DANN 架构与梯度反转层

3.1 三个子网共享一个主干

子网角色训练数据
特征提取器 $G_f$$x \mapsto f = G_f(x)$源 + 目标
标签预测器 $G_y$$f \mapsto \hat{y}$源域标签
域判别器 $G_d$$f \mapsto$ 源/目标源 + 目标

目标函数是一个 minimax:

$$ \min_{G_f,\, G_y}\; \max_{G_d}\quad \mathcal{L}_y(G_y \circ G_f) \;-\; \lambda\, \mathcal{L}_d(G_d \circ G_f). $$

$G_d$ 想把两个域分开,$G_f$ 想糊弄 $G_d$ 同时让 $G_y$ 把源域分类做好。

3.2 梯度反转层(GRL)

朴素地解 minimax 需要交替优化,又脆弱又难调(早期 GAN 的痛)。DANN 的关键贡献是用梯度反转层把整套系统拆成一次反向传播:

$$ \text{前向:}\; \text{GRL}(x) = x, \qquad \text{反向:}\; \frac{\partial\,\text{GRL}}{\partial x} = -\lambda\, I. $$

GRL 装在"特征 → 域判别器"这条路径上。反传时,判别器的梯度在到达 $G_f$ 之前先翻号,所以同一次 loss.backward() 就能:

  • 用正常梯度更新 $G_y$(分类做得更好);
  • 用正常梯度更新 $G_d$(判别做得更准);
  • 反向后的梯度更新 $G_f$(让 $G_d$ 越来越分不清域),同时仍接收 $G_y$ 传回的正常梯度。

不需要交替训练、不需要两个优化器、不需要手动冻结。

3.3 对抗权重的退火表

DANN 不会一上来就把 $\lambda$ 拉满——那样会摧毁早期学习。它走一条 sigmoid 曲线:

$$\lambda_p = \frac{2}{1 + \exp(-\gamma p)} - 1, \qquad \gamma \approx 10,$$

其中 $p \in [0, 1]$ 是训练进度。早期 $\lambda \approx 0$,模型先专心学到好的源域特征;后期 $\lambda \to 1$,域对齐才真正发力。省掉这个退火,是 “DANN 跑了但反而比 source-only 还差"的最常见原因,没有之一。


4. MMD:在 RKHS 里对齐均值

对抗式很强,但训练不稳定。非对抗替代方案是:显式定义一个分布距离,然后直接最小化它最大均值差异(Gretton 等,2012)是该方向的标准选择。

最大均值差异:核均值嵌入

4.1 想法

核函数 $k(x, y) = \langle \phi(x), \phi(y) \rangle_{\mathcal{H}}$ 隐式地把样本映到一个(可能无限维的)再生核希尔伯特空间 $\mathcal{H}$。分布 $P$ 的核均值嵌入是其特征均值

$$\mu_P = \mathbb{E}_{X \sim P}[\phi(X)] \;\in\; \mathcal{H}.$$

对于特征核(高斯 RBF 是经典代表),映射 $P \mapsto \mu_P$ 是单射的——两个分布相等 当且仅当 它们的核均值相等。所以分布距离就是核均值在 RKHS 里的距离:

$$\text{MMD}^2(P_S, P_T) = \|\mu_{P_S} - \mu_{P_T}\|_{\mathcal{H}}^2.$$

图示给出了直观感受:哪怕原始直方图有点重叠,核均值嵌入会把"差距"显式画出来,阴影面积正是 $\text{MMD}^2$。

4.2 真正能算的估计量

嵌入是隐式的,把平方范数展开后内积就变成核值评估:

$$ \widehat{\text{MMD}}^2 = \frac{1}{n_s^2}\sum_{i,j} k(x_i^s, x_j^s) + \frac{1}{n_t^2}\sum_{i,j} k(x_i^t, x_j^t) - \frac{2}{n_s n_t}\sum_{i,j} k(x_i^s, x_j^t). $$

它对特征可微,可以直接当一项额外损失加进深度网络:

$$\mathcal{L} = \mathcal{L}_{\text{task}} + \lambda \cdot \widehat{\text{MMD}}^2\!\big(G_f(X_S),\, G_f(X_T)\big).$$

这就是 DAN / DDC(Long 等,2015;Tzeng 等,2014)。

4.3 工程提示

  • **用多核 MMD。**多个不同带宽的高斯核线性组合 $k = \sum_u \beta_u k_{\sigma_u}$,对带宽误设鲁棒。
  • **带宽选择用 median heuristic。**取一个 batch 内成对距离的中位数——便宜、鲁棒,绝大多数情况够用。
  • **对靠后的层做 MMD。**底层带域特异性纹理;要对齐的是顶部的抽象表示。

4.4 MMD 与 DANN 一图对比

MMDDANN
距离核 RKHS 范数Jensen–Shannon(通过判别器)
优化直接最小化对抗 minimax(GRL)
稳定性非常稳定偶尔震荡
表达力受核选择限制更灵活
适合场景中小差距、数据少差距大、数据足

合理的默认工作流:先 MMD 试一把;不够再上 DANN。


5. CORAL:对齐二阶统计量

如果对齐均值是好事,那么同时对齐均值和协方差通常更好。CORAL(Sun & Saenko,2016)就这么做。

CORAL 协方差对齐

设源、目标特征的协方差矩阵分别为 $C_S$、$C_T$,CORAL 损失为

$$\mathcal{L}_{\text{CORAL}} = \frac{1}{4 d^2} \|C_S - C_T\|_F^2.$$

**直觉——白化 + 重新着色。**给源特征左乘 $C_S^{-1/2} C_T^{1/2}$,先把源协方差"擦掉”,再"涂上"目标协方差。Deep CORAL 把上面这个损失加到深度网络里,让梯度隐式完成同样的事。

CORAL 极其便宜(每个 batch 一次矩阵和一次 Frobenius 范数)、完全确定性,在轻度偏移下出奇地能打。在掏出 MMD/DANN 之前,它是非常值得一试的基线。


6. AdaBN:永远第一个尝试的免费午餐

最简单的一招:在目标数据上重新统计 Batch Norm 的均值和方差

标准 BN 在测试时用的是训练期间累计的 running mean/variance。如果目标分布不一样,这些统计量就是错的,而它们坐在每一个卷积层和下一个非线性之间。AdaBN(Li 等,2017):

  1. 正常在源域上训练。
  2. 冻结权重,把无标注目标数据正向跑一遍,重新算每一层 BN 的 $\mu_T, \sigma_T^2$。
  3. 部署时用目标域的统计量替换源域的。

成本:几分钟。代码改动:几个 BatchNorm 的 running stats。效果:在协变量偏移下常能拿回 2–10 个点的准确率。任何更花的方法之前先做这一步。


7. GAN 式与像素级适应

有时候差距是视觉性的——合成 vs. 真实、白天 vs. 黑夜——光对齐特征已经太晚了,你需要直接翻译输入

  • CycleGAN 同时学两个生成器 $G: \mathcal{X}_S \to \mathcal{X}_T$ 和 $F: \mathcal{X}_T \to \mathcal{X}_S$,加上循环一致性 $F(G(x)) \approx x$。把源图像翻译到目标风格,再用原来的源标签训练分类器。注意:循环一致性保证语义不变,配合感知损失或身份损失更安全。
  • ADDA 把源、目标两个编码器解耦。第 1 阶段:正常训练源编码器 + 分类器;第 2 阶段:从源编码器初始化一个目标编码器,对抗一个域判别器适应它,分类器冻结;第 3 阶段:测试时目标输入走目标编码器 + 源分类器。这种不对称给了 ADDA 比 DANN 更大的容量,代价是多一个训练阶段。

8. 自训练:在目标上自举出标签

对抗式和统计式对齐都把目标当成一团没差别的云。自训练(也叫伪标注)更进一步:用当前模型给目标打标签,然后训练它自己。

自训练 / 伪标注

循环很简单:

  1. 在源域上训练 $f$。
  2. 对每个目标样本预测,只保留 $\max_y f(x)_y > \tau$ 的那些(高置信度阈值)。
  3. 把保留下来的 (输入, 预测) 对当成新标注数据,重训。
  4. 反复迭代。

自训练强大但被低估,最臭名昭著的失败模式叫确认偏差:错但自信的预测被反复喂回训练,越来越固化。标准缓解措施:

  • 把阈值 $\tau$ 调高(通常 0.9 以上);
  • 类别平衡选择(每类伪标签数量设上限);
  • 增广下的一致性正则(FixMatch 风格);
  • 每轮重新从源模型出发,而不是从上一轮的自训练模型出发。

9. 决策树:什么时候用什么方法

1. 你有没有任何目标域标注?
   ├─ 有 → 半监督 DA:fine-tune + 重要性加权
   └─ 没有 → 第 2 步

2. 偏移在哪?
   ├─ 只 P(X) 不同         → 第 3 步
   ├─ 只 P(Y) 不同         → 标签偏移修正(BBSE / EM)
   └─ P(Y|X) 不同(概念)   → 必须有少量目标标注

3. 视觉/特征差距多大?
   ├─ 极小   → AdaBN(永远先试)
   ├─ 小     → AdaBN + Deep CORAL
   ├─ 中     → MMD(DAN)或 DANN
   ├─ 大     → DANN / CDAN,或像素级(CycleGAN、ADDA)
   └─ 巨大(仿真→真实)→ CycleGAN + ADDA + 自训练

实战中,强 pipeline 经常组合多种方法:AdaBN 拿走容易的几个点,MMD/DANN 做特征对齐,最后跑一轮自训练拿走最后那几个点。


10. Benchmark:到底有多大用?

Office-31 / DomainNet 基准

数字是 ResNet-50 主干下的代表性文献平均。两点值得注意:

  • **从"什么都不做"到"做点什么",跨度最大。**哪怕只用 AdaBN,差距也能补回相当一截。做点什么远比挑到完美的方法重要。
  • **DomainNet 比 Office-31 困难得多。**DomainNet 上 40% 的准确率对应的方法相当强——这数据集有 345 类,跨 6 种风格迥异的视觉风格。读 DA 准确率永远要相对于 source-only 基线读,不要只看绝对值。

11. 域适应在哪里真正值钱

  • 医学影像——西门子 vs. GE、1.5T vs. 3T MRI、A 医院 vs. B 医院。
  • 自动驾驶——晴 → 雨、城市 A → 城市 B、仿真 → 真实。
  • 推荐系统——国与国、年与年、网页 → 移动。
  • NLP——电影评论 → 商品评论、新闻 → 社交、正式 → 口语。
  • Sim-to-real——机器人和自动驾驶里把合成数据迁到真实传感器。

共同模式:源域标注便宜、目标域标注昂贵或不可能、模型还非得发版


12. 把效果画出来——t-SNE 前后对比

训完一个 DA 模型后的标准检查:把源、目标特征都过一遍 t-SNE。适应之前样本按聚成簇;适应之后按类别聚成簇。

t-SNE 域适应前后

如果"之后"图里仍然能看到两个域块,说明对齐失败了;如果只有一个块、按类别分开,说明对齐成功了。这一张图比任何单一数字都更能诊断问题。


13. 完整实现:DANN

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, TensorDataset
from torch.autograd import Function
import numpy as np
from sklearn.metrics import accuracy_score


class GradientReversalFunction(Function):
    """前向恒等,反向把梯度取反。"""

    @staticmethod
    def forward(ctx, x, lambda_):
        ctx.lambda_ = lambda_
        return x.clone()

    @staticmethod
    def backward(ctx, grad_output):
        return grad_output.neg() * ctx.lambda_, None


class GradientReversalLayer(nn.Module):
    def __init__(self):
        super().__init__()
        self.lambda_ = 1.0

    def set_lambda(self, val):
        self.lambda_ = val

    def forward(self, x):
        return GradientReversalFunction.apply(x, self.lambda_)


class FeatureExtractor(nn.Module):
    def __init__(self, input_dim=28 * 28, hidden_dim=256):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Dropout(0.5),
            nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Dropout(0.5),
        )

    def forward(self, x):
        return self.net(x.view(x.size(0), -1))


class LabelPredictor(nn.Module):
    def __init__(self, feature_dim=256, num_classes=10):
        super().__init__()
        self.fc = nn.Linear(feature_dim, num_classes)

    def forward(self, x):
        return self.fc(x)


class DomainDiscriminator(nn.Module):
    def __init__(self, feature_dim=256):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(feature_dim, 256), nn.ReLU(), nn.Dropout(0.5),
            nn.Linear(256, 256), nn.ReLU(), nn.Dropout(0.5),
            nn.Linear(256, 1), nn.Sigmoid(),
        )

    def forward(self, x):
        return self.net(x)


class DANN(nn.Module):
    """域对抗神经网络。"""

    def __init__(self, input_dim=28 * 28, hidden_dim=256, num_classes=10):
        super().__init__()
        self.feature_extractor = FeatureExtractor(input_dim, hidden_dim)
        self.label_predictor = LabelPredictor(hidden_dim, num_classes)
        self.domain_discriminator = DomainDiscriminator(hidden_dim)
        self.grl = GradientReversalLayer()

    def forward(self, x, alpha=1.0):
        features = self.feature_extractor(x)
        class_logits = self.label_predictor(features)
        self.grl.set_lambda(alpha)
        domain_logits = self.domain_discriminator(self.grl(features))
        return class_logits, domain_logits


class DANNTrainer:
    def __init__(self, model, source_loader, target_loader, test_loader,
                 num_epochs=100, lr=1e-3, device="cpu", gamma=10.0):
        self.model = model.to(device)
        self.source_loader = source_loader
        self.target_loader = target_loader
        self.test_loader = test_loader
        self.num_epochs = num_epochs
        self.device = device
        self.gamma = gamma
        self.optimizer = torch.optim.Adam(model.parameters(), lr=lr)
        self.class_criterion = nn.CrossEntropyLoss()
        self.domain_criterion = nn.BCELoss()

    def _adaptive_lambda(self, epoch):
        # sigmoid 退火:训练越后期,对抗权重越接近 1
        p = epoch / self.num_epochs
        return 2.0 / (1.0 + np.exp(-self.gamma * p)) - 1.0

    def train_epoch(self, epoch):
        self.model.train()
        source_iter = iter(self.source_loader)
        target_iter = iter(self.target_loader)
        n_batches = min(len(self.source_loader), len(self.target_loader))
        total_loss = 0.0
        lambda_p = self._adaptive_lambda(epoch)

        for _ in range(n_batches):
            try:
                src_x, src_y = next(source_iter)
            except StopIteration:
                source_iter = iter(self.source_loader)
                src_x, src_y = next(source_iter)
            try:
                tgt_x, _ = next(target_iter)
            except StopIteration:
                target_iter = iter(self.target_loader)
                tgt_x, _ = next(target_iter)

            src_x = src_x.to(self.device)
            src_y = src_y.to(self.device)
            tgt_x = tgt_x.to(self.device)

            # 前向:两个头、两个域
            src_class_logits, src_dom_logits = self.model(src_x, lambda_p)
            _, tgt_dom_logits = self.model(tgt_x, lambda_p)

            # 源域分类损失
            class_loss = self.class_criterion(src_class_logits, src_y)
            # 域判别损失(源 = 1,目标 = 0)
            d_loss_s = self.domain_criterion(
                src_dom_logits, torch.ones_like(src_dom_logits))
            d_loss_t = self.domain_criterion(
                tgt_dom_logits, torch.zeros_like(tgt_dom_logits))
            domain_loss = d_loss_s + d_loss_t

            loss = class_loss + domain_loss
            self.optimizer.zero_grad()
            loss.backward()
            self.optimizer.step()
            total_loss += loss.item()

        return total_loss / n_batches

    @torch.no_grad()
    def evaluate(self):
        self.model.eval()
        preds, labels = [], []
        for x, y in self.test_loader:
            x = x.to(self.device)
            logits, _ = self.model(x, alpha=0.0)
            preds.extend(logits.argmax(dim=1).cpu().numpy())
            labels.extend(y.numpy())
        return accuracy_score(labels, preds)

    def train(self):
        best = 0.0
        for epoch in range(self.num_epochs):
            loss = self.train_epoch(epoch)
            acc = self.evaluate()
            if (epoch + 1) % 10 == 0:
                lam = self._adaptive_lambda(epoch)
                print(f"epoch {epoch + 1:3d}  loss={loss:.4f}  "
                      f"target_acc={acc:.4f}  lambda={lam:.3f}")
            best = max(best, acc)
        print(f"best target accuracy: {best:.4f}")


def main():
    N, D, C = 10000, 28 * 28, 10
    # 模拟源、目标分布偏移
    src_x = torch.randn(N, 1, 28, 28)
    src_y = torch.randint(0, C, (N,))
    tgt_x = torch.randn(N, 1, 28, 28) + 0.5     # 偏移
    tgt_y = torch.randint(0, C, (N,))           # 训练时不用
    test_x = torch.randn(2000, 1, 28, 28) + 0.5
    test_y = torch.randint(0, C, (2000,))

    BS = 128
    src_loader = DataLoader(TensorDataset(src_x, src_y), BS, shuffle=True)
    tgt_loader = DataLoader(TensorDataset(tgt_x, tgt_y), BS, shuffle=True)
    test_loader = DataLoader(TensorDataset(test_x, test_y), BS)

    model = DANN(D, 256, C)
    trainer = DANNTrainer(model, src_loader, tgt_loader, test_loader,
                          num_epochs=100, lr=1e-3)
    trainer.train()


if __name__ == "__main__":
    main()

这段代码到底在做什么

组件作用
GradientReversalLayer前向恒等、反向取反——把 minimax 折叠成一次反向传播。
_adaptive_lambdasigmoid 退火 $\frac{2}{1+e^{-\gamma p}} - 1$:先学特征,再加对抗。
class_loss标准交叉熵,用源域标签(目标域无标注)。
domain_lossBCE,源 = 1、目标 = 0,训练域判别器。
GRL + 域分支反向传播时梯度翻号回到 $G_f$,让特征"藏起"域信息。
evaluate(alpha=0)测试时把 $\lambda$ 设 0,GRL 失活,只用分类头。

总结

域适应面对的是迁移学习里最实战的问题:训练数据和上线数据来自不同分布。工具箱按"动手成本递增"大致排序:

  • AdaBN——在目标域上重算 BN 统计量;零成本、零重训、永远先试。
  • CORAL——对齐源和目标的协方差矩阵;便宜、确定性。
  • MMD(DAN)——对齐核均值嵌入;稳定、有理论支撑、默认用多核。
  • DANN——通过梯度反转层做对抗对齐;一次反向传播搞定。
  • CDAN / ADDA——更灵活的变种,应对更大差距。
  • CycleGAN——特征对齐不够时,去做像素级翻译。
  • 自训练——置信度阈值守门的伪标签;榨出最后那几个点。

Ben-David 上界告诉你哪些事可能:把源域误差和域散度压小,目标误差就会跟着压小——前提是联合最优误差本身就小。如果它本身就大,再多对齐也救不了你;那时你需要的是新标注。

下一章 第 4 篇——Few-Shot Learning ,我们把"源域数据充足"这个假设也丢掉,研究每类只有 1–5 个样本时该怎么学。


参考文献

  1. Ganin et al. (2016). Domain-Adversarial Training of Neural Networks. JMLR. arXiv:1505.07818
  2. Long et al. (2015). Learning Transferable Features with Deep Adaptation Networks. ICML. arXiv:1502.02791
  3. Sun & Saenko (2016). Deep CORAL: Correlation Alignment for Deep Domain Adaptation. ECCV. arXiv:1607.01719
  4. Zhu et al. (2017). Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks (CycleGAN). ICCV. arXiv:1703.10593
  5. Tzeng et al. (2017). Adversarial Discriminative Domain Adaptation (ADDA). CVPR. arXiv:1702.05464
  6. Long et al. (2018). Conditional Adversarial Domain Adaptation (CDAN). NeurIPS. arXiv:1705.10667
  7. Ben-David et al. (2010). A Theory of Learning from Different Domains. Machine Learning.
  8. Li et al. (2017). Revisiting Batch Normalization for Practical Domain Adaptation (AdaBN). arXiv:1603.04779
  9. Gretton et al. (2012). A Kernel Two-Sample Test (MMD). JMLR. paper
  10. Lipton et al. (2018). Detecting and Correcting for Label Shift with Black Box Predictors. ICML. arXiv:1802.03916
  11. Sohn et al. (2020). FixMatch: Simplifying Semi-Supervised Learning with Consistency and Confidence. NeurIPS. arXiv:2001.07685

系列导航

部分主题
1基础与核心概念
2预训练与微调
3域适应(本文)
4小样本学习
5知识蒸馏
6多任务学习
7零样本学习
8多模态迁移
9参数高效微调
10持续学习
11跨语言迁移
12工业应用与最佳实践

Liked this piece?

Follow on GitHub for the next one — usually one a week.

GitHub