域适应( Domain
Adaptation)是迁移学习中最具挑战性的问题之一。在实际应用中,训练数据(源域)和测试数据(目标域)往往来自不同分布:医疗影像从一家医院迁移到另一家医院、推荐系统从一个国家迁移到另一个国家、自动驾驶从晴天迁移到雨天。这种分布偏移 (
distribution shift)会导致模型性能大幅下降。
域适应的核心目标是:在源域有标注数据、目标域无标注(或少量标注)数据的情况下,学习一个在目标域表现良好的模型 。这需要对齐源域和目标域的特征分布,同时保持判别性。本文将从分布偏移的数学刻画出发,推导无监督域适应的理论基础,详解
DANN 、 MMD 等经典方法,并提供完整的 DANN 实现。
域偏移问题:协变量偏移与标签偏移
域的形式化定义
域( Domain)由两部分组成: - 特征空间 :输入变量的取值空间 -
边缘分布 :输入变量的概率分布
任务( Task)也由两部分组成: - 标签空间 :输出变量的取值空间 -
条件分布 :给定输入时输出的概率分布
域适应的设定是:
目标是学习一个模型 ,使其在目标域上表现良好。
协变量偏移( Covariate Shift)
定义 :特征分布不同但条件分布相同,即:
$$
P_S(X) P_T(X), P_S(Y|X) = P_T(Y|X) $$
直觉例子 :
垃圾邮件分类 :训练数据来自 2020 年,测试数据来自
2026 年。邮件主题分布变化(
变化),但给定邮件内容判断是否垃圾的规则不变( 不变)
医疗影像 :训练数据来自西门子 CT,测试数据来自 GE CT
。不同设备的成像特性不同(
变化),但病灶的诊断标准不变( 不变)
重要性加权( Importance
Weighting)
协变量偏移可以通过重要性加权 校正。经验风险最小化(
ERM)在源域上优化:
但我们真正关心的是目标域风险:
$$
R_T() = _{(X,Y) P_T} [(f_(X), Y)] $$
利用重要性采样,可以将目标域风险改写为:
其中
是重要性权重 。实际训练时,优化加权损失:
密度比估计
重要性权重 需要估计两个密度的比值。直接估计密度很困难( curse of
dimensionality),但可以直接估计密度比。
Kullback-Leibler Importance Estimation Procedure
(KLIEP) :
最小化 KL 散度:
约束 。实际优化:
通常参数化 ,其中
是基函数(如高斯核)。
标签偏移( Label Shift)
定义 :标签分布不同但类条件分布相同,即:
$$
P_S(Y) P_T(Y), P_S(X|Y) = P_T(X|Y) $$
直觉例子 : -
医疗诊断 :训练数据来自住院患者(病人比例高),测试数据来自门诊患者(健康人比例高)。疾病流行率不同(
变化),但给定疾病,症状分布相同( 不变) -
推荐系统 :训练数据来自活跃用户(年轻用户多),测试数据来自全体用户(老年用户多)。用户年龄分布不同(
变化),但给定用户类型,行为模式相同( 不变)
标签偏移校正
利用贝叶斯定理,目标域的后验为:
$$
P_T(Y|X) = = $$
进一步改写:
$$
P_T(Y|X) = = = P_S(Y|X) $$
因此,只需用 重新加权源域模型的输出:
标签分布估计 :
可以从源域标注数据直接估计。 需要从目标域无标注数据估计。一种方法是期望最大化(
EM) :
E 步:用当前模型预测目标域标签 $$
q(y|x) = P_S(y|x) $$
M 步:更新标签分布 $$
P_T(y) = _{j=1}^{n_t} q(y|x_j^t) $$
迭代直到收敛。
概念偏移( Concept Shift)
定义 :条件分布不同,即:
$$
P_S(Y|X) P_T(Y|X) $$
这是最困难的情况,因为即使特征相同,决策边界也可能不同。
例子 : -
情感分类 :训练数据来自电影评论,测试数据来自商品评论。相同的词语在不同领域可能有不同的情感倾向
-
自动驾驶 :训练数据来自美国(右侧通行),测试数据来自英国(左侧通行)。驾驶规则完全不同
概念偏移通常需要目标域的少量标注数据才能适配。
无监督域适应:对齐特征分布
无监督域适应( Unsupervised Domain Adaptation,
UDA)假设源域有标注数据,目标域完全无标注 。核心思想是:学习一个特征提取器,使得源域和目标域的特征分布对齐 。
理论基础: Ben-David 理论
Ben-David 等人给出了域适应的理论上界。设 是假设空间,
是假设,则目标域风险可以分解为:
$$
R_T(h) R_S(h) + d_{ } (P_S, P_T) + $$
其中: - :源域风险 - :源域和目标域的 -距离 -
:理想联合假设的风险
直觉解释 : 1. :在源域上分类好 2. :源域和目标域分布接近 3. :源域和目标域任务相似(小的理想联合风险)
这三项的平衡是域适应的核心。
-距离
-距离定义为:
$$
d_{ } (P_S, P_T) = 2 {h, h' } | {X P_S}[h(X) h'(X)] - _{X
P_T}[h(X) h'(X)] | $$
直观理解:如果源域和目标域很接近,那么任意两个假设
在两个域上的分歧应该相似。
实践中,可以用域判别器 ( domain
discriminator)的损失近似这个距离:训练一个分类器区分源域和目标域样本,如果无法区分(分类器准确率接近
50%),说明两个域分布接近。
DANN:域对抗神经网络
Domain-Adversarial Neural Network (DANN)
是最经典的域适应方法,通过对抗训练对齐源域和目标域的特征分布。
DANN 架构
DANN 包含三个组件: 1. 特征提取器 :将输入映射到特征空间 2.
标签预测器 :根据特征预测标签(分类器) 3.
域判别器 :根据特征判断来自哪个域
损失函数包含三项:
源域分类损失 : $$
L_y = _{(x,y) _S} [_y(G_y(G_f(x)), y)] 域 判 别 损 失 :
L_d = -_{x S} [G_d(G_f(x))] - {x _T} [(1 - G_d(G_f(x)))]
总 损 失 ( 对 抗 ) :
L = L_y - L_d $$
对抗训练 : - 域判别器 最大化 (区分源域和目标域) -
特征提取器
最小化 同时最大化 的混淆(让 无法区分)
这等价于:
梯度反转层( Gradient
Reversal Layer)
DANN 的巧妙之处在于梯度反转层(
GRL) ,它在前向传播时是恒等映射,在反向传播时翻转梯度:
这样,域判别器的梯度乘以
后传回特征提取器,实现了对抗训练。
自适应
DANN 使用自适应的对抗权重
,随训练进度增大:
其中 是训练进度(当前步数/总步数), 是超参数。
直觉 :训练初期先学好源域分类,后期逐渐增强域对齐。
DANN 的理论解释
DANN 最小化的是源域风险
和域差异 的加权和。域判别器损失是域差异的代理(
proxy):如果域判别器无法区分源域和目标域( ),说明特征分布对齐。
更严格地, DANN 等价于最小化 Jensen-Shannon
散度 :
其中
是混合分布。
MMD:最大均值差异
Maximum Mean Discrepancy (MMD)
是另一种衡量分布差异的方法,通过比较两个分布在再生核希尔伯特空间(
RKHS)中的均值来度量距离。
MMD 定义
设 是
RKHS,对应核函数 ,则 MMD 定义为:
其中 是核映射。
展开平方:
经验估计 :
深度域适应中的 MMD
Deep Domain Confusion (DDC) 和 Deep Adaptation Network (DAN) 将 MMD
嵌入深度网络:
其中 是特征提取器, 分别是源域和目标域样本。
多核 MMD :
单个核可能不足以捕捉分布差异,可以使用多个核的线性组合:
其中 , 。
MMD 的核选择
常用核函数:
高斯核( RBF 核) : $$
k(x, x') = (-) 多 项 式 核 :
k(x, x') = (x^x' + c)^d 拉 普 拉 斯 核 :
k(x, x') = (-) $$
实践中,通常使用多个不同带宽 的高斯核。
CORAL:相关对齐
Correlation Alignment (CORAL)
通过对齐源域和目标域的二阶统计量(协方差)来减小分布差异。
CORAL 损失
设源域特征为 ,目标域特征为 ,则 CORAL 损失为:
$$
L_{} = | C_S - C_T |_F^2 $$
其中
分别是源域和目标域特征的协方差矩阵:
$$
C_S = (F_S - {F}_S)^(F_S - {F}_S) $$ 是均值向量。
深度 CORAL :
Deep CORAL 将 CORAL 损失加入深度网络训练:
CORAL 的直觉
协方差矩阵捕捉了特征之间的相关性。对齐协方差矩阵可以消除域之间的线性变换差异。CORAL
可以看作是白化(whitening)+
重新着色(recoloring) :
白化源域特征:
重新着色到目标域:
这样 的协方差矩阵就等于 。
对抗域适应: GAN-based 方法
生成对抗网络(
GAN)的思想也可以用于域适应:通过生成器将源域样本转换为目标域风格,同时保持语义不变。
CycleGAN:循环一致对抗网络
CycleGAN 通过循环一致性损失 实现无配对的域转换。
CycleGAN 架构
包含两个生成器和两个判别器: - (源域到目标域) - (目标域到源域) - :判别目标域真假 - :判别源域真假
损失函数
对抗损失 : 2. 循环一致性损失 : $$
L_{} = {x_s P_S}[|F(G(x_s)) - x_s|_1] + {x_t P_T}[|G(F(x_t))
- x_t|_1] 总 损 失 :
L = L_{} (G, D_T) + L_{} (F, D_S) + {} L {} $$
直觉 : - 对抗损失让生成的样本看起来像目标域 -
循环一致性损失保证语义不变( )
域适应应用
CycleGAN
可以将源域图像转换为目标域风格,然后用转换后的图像训练分类器:
用 CycleGAN 学习 $G: _S T生 成 伪 目 标 域 数 据 : {(G(x_i^s), y_i^s)} {i=1}^{n_s}$3.
在伪目标域数据上训练分类器
Pixel-level 域适应
对于视觉任务,可以在像素级别进行域适应。
ADDA:对抗判别域适应
Adversarial Discriminative Domain Adaptation (ADDA)
分三步进行:
预训练 :在源域上训练分类器
对抗适配 :固定分类器 ,学习目标域特征提取器 ,对抗域判别器
测试 :在目标域上用 预测
ADDA
的优势是源域和目标域的特征提取器可以不同 ,更灵活。
自适应 Batch Normalization
Batch Normalization (BN) 在训练和测试时行为不同:训练时用 batch
统计量,测试时用全局统计量。这在域适应中会导致问题。
BN 的域偏移问题
BN 层计算:
训练时, 是当前 batch
的均值和标准差;测试时,使用训练集的全局均值和标准差( running
mean/std)。
问题 :如果测试数据(目标域)的分布与训练数据(源域)不同,全局统计量会不准确。
自适应 BN( AdaBN)
AdaBN 的思想很简单:在目标域上重新计算 BN
的统计量 。
AdaBN 算法
在源域上正常训练模型(包含 BN 层)
在目标域数据上运行模型,计算每个 BN 层的均值和方差: 3. 测试时用 替换原来的 为什么有效 ?
BN
的统计量捕捉了数据的低阶统计特性。对齐这些统计量可以部分消除域偏移。实验表明,
AdaBN 对协变量偏移特别有效。
TransNorm:可转移归一化
TransNorm 进一步将 BN
分解为任务相关部分 和域相关部分 :
- ${} , {} : 域 相 关 统 计 量 , 在 目 标 域 上 重 新 计 算 {} , {} $:任务相关参数,保持不变
这样既适应了目标域的分布,又保留了源域学到的任务知识。
完整实现: DANN 域适应
下面提供一个完整的 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 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 import torchimport torch.nn as nnimport torch.nn.functional as Ffrom torch.utils.data import DataLoader, TensorDatasetfrom torch.autograd import Functionimport numpy as npfrom tqdm import tqdmfrom sklearn.metrics import accuracy_scoreclass GradientReversalFunction (Function ): """梯度反转函数""" @staticmethod def forward (ctx, x, lambda_ ): ctx.lambda_ = lambda_ return x.view_as(x) @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, lambda_ ): self.lambda_ = lambda_ def forward (self, x ): return GradientReversalFunction.apply(x, self.lambda_) class FeatureExtractor (nn.Module): """特征提取器( CNN)""" def __init__ (self, input_dim=28 *28 , hidden_dim=256 ): super ().__init__() self.fc1 = nn.Linear(input_dim, hidden_dim) self.fc2 = nn.Linear(hidden_dim, hidden_dim) self.dropout = nn.Dropout(0.5 ) def forward (self, x ): x = x.view(x.size(0 ), -1 ) x = F.relu(self.fc1(x)) x = self.dropout(x) x = F.relu(self.fc2(x)) x = self.dropout(x) return x 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 , hidden_dim=256 ): super ().__init__() self.fc1 = nn.Linear(feature_dim, hidden_dim) self.fc2 = nn.Linear(hidden_dim, hidden_dim) self.fc3 = nn.Linear(hidden_dim, 1 ) self.dropout = nn.Dropout(0.5 ) def forward (self, x ): x = F.relu(self.fc1(x)) x = self.dropout(x) x = F.relu(self.fc2(x)) x = self.dropout(x) x = torch.sigmoid(self.fc3(x)) return 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, 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) reversed_features = self.grl(features) domain_logits = self.domain_discriminator(reversed_features) return class_logits, domain_logits class DANNTrainer : """DANN 训练器""" def __init__ ( self, model, source_loader, target_loader, test_loader, num_epochs=100 , learning_rate=1e-3 , device='cuda' , 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=learning_rate ) self.class_criterion = nn.CrossEntropyLoss() self.domain_criterion = nn.BCELoss() self.train_losses = [] self.test_accuracies = [] def compute_lambda (self, epoch, total_epochs ): """计算自适应对抗权重""" p = epoch / total_epochs lambda_p = 2.0 / (1.0 + np.exp(-self.gamma * p)) - 1.0 return lambda_p def train_epoch (self, epoch ): """训练一个 epoch""" self.model.train() source_iter = iter (self.source_loader) target_iter = iter (self.target_loader) num_batches = min (len (self.source_loader), len (self.target_loader)) total_loss = 0 total_class_loss = 0 total_domain_loss = 0 progress_bar = tqdm(range (num_batches), desc=f'Epoch {epoch+1 } /{self.num_epochs} ' ) for _ in progress_bar: try : source_data, source_labels = next (source_iter) except StopIteration: source_iter = iter (self.source_loader) source_data, source_labels = next (source_iter) try : target_data, _ = next (target_iter) except StopIteration: target_iter = iter (self.target_loader) target_data, _ = next (target_iter) source_data = source_data.to(self.device) source_labels = source_labels.to(self.device) target_data = target_data.to(self.device) batch_size = source_data.size(0 ) lambda_p = self.compute_lambda(epoch, self.num_epochs) source_class_logits, source_domain_logits = self.model(source_data, lambda_p) _, target_domain_logits = self.model(target_data, lambda_p) class_loss = self.class_criterion(source_class_logits, source_labels) source_domain_labels = torch.ones(batch_size, 1 ).to(self.device) target_domain_labels = torch.zeros(target_data.size(0 ), 1 ).to(self.device) source_domain_loss = self.domain_criterion(source_domain_logits, source_domain_labels) target_domain_loss = self.domain_criterion(target_domain_logits, target_domain_labels) domain_loss = source_domain_loss + target_domain_loss loss = class_loss + domain_loss self.optimizer.zero_grad() loss.backward() self.optimizer.step() total_loss += loss.item() total_class_loss += class_loss.item() total_domain_loss += domain_loss.item() progress_bar.set_postfix({ 'loss' : loss.item(), 'class' : class_loss.item(), 'domain' : domain_loss.item(), 'lambda' : lambda_p }) avg_loss = total_loss / num_batches avg_class_loss = total_class_loss / num_batches avg_domain_loss = total_domain_loss / num_batches return avg_loss, avg_class_loss, avg_domain_loss def evaluate (self ): """评估模型""" self.model.eval () all_preds = [] all_labels = [] with torch.no_grad(): for data, labels in self.test_loader: data = data.to(self.device) class_logits, _ = self.model(data, alpha=0.0 ) preds = torch.argmax(class_logits, dim=1 ).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) accuracy = accuracy_score(all_labels, all_preds) return accuracy def train (self ): """完整训练流程""" print (f"Starting DANN training for {self.num_epochs} epochs" ) best_acc = 0.0 for epoch in range (self.num_epochs): loss, class_loss, domain_loss = self.train_epoch(epoch) self.train_losses.append(loss) acc = self.evaluate() self.test_accuracies.append(acc) print (f"Epoch {epoch+1 } /{self.num_epochs} " ) print (f" Loss: {loss:.4 f} (Class: {class_loss:.4 f} , Domain: {domain_loss:.4 f} )" ) print (f" Test Accuracy: {acc:.4 f} " ) if acc > best_acc: best_acc = acc torch.save(self.model.state_dict(), 'best_dann_model.pt' ) print (f" Saved best model with accuracy {best_acc:.4 f} " ) return self.train_losses, self.test_accuracies def main (): INPUT_DIM = 28 * 28 HIDDEN_DIM = 256 NUM_CLASSES = 10 BATCH_SIZE = 128 NUM_EPOCHS = 100 LEARNING_RATE = 1e-3 source_data = torch.randn(10000 , 1 , 28 , 28 ) source_labels = torch.randint(0 , NUM_CLASSES, (10000 ,)) target_data = torch.randn(10000 , 1 , 28 , 28 ) + 0.5 target_labels = torch.randint(0 , NUM_CLASSES, (10000 ,)) test_data = torch.randn(2000 , 1 , 28 , 28 ) + 0.5 test_labels = torch.randint(0 , NUM_CLASSES, (2000 ,)) source_dataset = TensorDataset(source_data, source_labels) target_dataset = TensorDataset(target_data, target_labels) test_dataset = TensorDataset(test_data, test_labels) source_loader = DataLoader(source_dataset, batch_size=BATCH_SIZE, shuffle=True ) target_loader = DataLoader(target_dataset, batch_size=BATCH_SIZE, shuffle=True ) test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE) model = DANN(INPUT_DIM, HIDDEN_DIM, NUM_CLASSES) trainer = DANNTrainer( model=model, source_loader=source_loader, target_loader=target_loader, test_loader=test_loader, num_epochs=NUM_EPOCHS, learning_rate=LEARNING_RATE, gamma=10.0 ) train_losses, test_accuracies = trainer.train() print (f"\nFinal Test Accuracy: {test_accuracies[-1 ]:.4 f} " ) if __name__ == '__main__' : main()
代码详解
梯度反转层
GradientReversalFunction 继承自
torch.autograd.Function,自定义前向和反向传播:
1 2 3 4 5 6 7 8 @staticmethod def forward (ctx, x, lambda_ ): ctx.lambda_ = lambda_ return x.view_as(x) @staticmethod def backward (ctx, grad_output ): return grad_output.neg() * ctx.lambda_, None
自适应对抗权重
compute_lambda 方法根据训练进度动态调整对抗权重:
初期 接近
0(先学好分类),后期接近 1(增强域对齐)。
对抗训练
训练时同时优化分类损失和域判别损失:
1 2 3 4 5 6 7 8 9 class_loss = self.class_criterion(source_class_logits, source_labels) domain_loss = source_domain_loss + target_domain_loss loss = class_loss + domain_loss loss.backward()
梯度反转层确保域判别器的梯度以相反方向更新特征提取器。
深度 Q&A
Q1:
为什么要对齐特征分布而不是直接在源域训练?
在源域训练的模型在目标域可能失效,原因是决策边界在目标域数据密度低的区域 。
数学解释 :设决策边界为 ,源域数据
在边界附近密度高,模型学到了精确的边界。但如果目标域数据
在边界附近密度低,边界就不可靠。
直觉例子 :训练数据是白天的图像,测试数据是夜晚的图像。即使是相同的物体,夜晚图像的特征分布差异很大,决策边界需要重新学习。
对齐特征分布可以让源域和目标域的数据在特征空间中混合,边界在两个域中都可靠。
Q2: DANN
为什么要用梯度反转而不是直接最大化域判别损失?
直接最大化域判别损失需要交替优化:先固定特征提取器优化域判别器,再固定域判别器优化特征提取器。这等价于
GAN 的训练,容易不稳定。
梯度反转层允许单步联合优化 :一次反向传播同时更新所有参数。域判别器的梯度自动翻转后传给特征提取器,实现对抗。
数学上 ,梯度反转等价于最小化:
$$
L = L_{} - L_{} $$
其中
对特征提取器是负号(对抗),对域判别器是正号(判别)。
Q3: MMD 和 DANN
有什么区别?各适用什么场景?
方法
距离度量
优化方式
优势
劣势
MMD
核函数距离
直接最小化
理论保证强、稳定
需要选择核函数、计算复杂度高
DANN
Jensen-Shannon 散度
对抗训练
表达能力强、适应性好
训练不稳定、需要调节超参数
适用场景 : -
MMD :域差异较小、数据量较少、需要稳定性 -
DANN :域差异较大、数据量充足、追求最优性能
实践中,可以先尝试 MMD(更稳定),如果效果不够好再用 DANN 。
Q4: 为什么自适应
BN( AdaBN)有效?它解决了什么问题?
BN
层的统计量(均值、方差)捕捉了数据的低阶统计特性 。源域和目标域即使语义相同,低阶统计量也可能不同。
例子 : -
图像 :源域图像偏亮(均值高),目标域图像偏暗(均值低)
- 传感器 :源域来自设备 A(方差小),目标域来自设备
B(方差大)
AdaBN
用目标域的统计量替换源域的统计量,消除低阶统计差异 ,让模型专注于高层语义。
理论解释 : AdaBN 等价于白化(
whitening)目标域数据到源域的统计分布 ,消除协变量偏移。
Q5:
如何选择域适应方法?有决策树吗?
是的,可以根据以下流程选择:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 1. 有目标域标注数据吗? ├─ 是 → 监督域适应( fine-tuning 、重要性加权) └─ 否 → 2 2. 源域和目标域的差异主要在哪里? ├─ 特征分布($P(X)$)→ 3 ├─ 标签分布($P(Y)$)→ 标签偏移校正 └─ 条件分布($P(Y|X)$)→ 需要少量目标域标注 3. 差异大小? ├─ 小(协变量偏移)→ AdaBN 、 CORAL ├─ 中 → MMD 、 DANN └─ 大(跨模态)→ CycleGAN 、 ADDA 4. 数据量和计算资源? ├─ 数据少、资源有限 → AdaBN 、 CORAL └─ 数据多、资源充足 → DANN 、 ADDA
Q6: 域适应会不会损害源域性能?
会,这称为负迁移 ( negative
transfer)。原因是域对齐可能损害判别性:为了让源域和目标域特征接近,模型可能丢失对分类有用的信息。
Ben-David 理论 告诉我们,目标域风险受三项影响:
$$
R_T(h) {} + {} + _{} $$
过度对齐可能降低 但增大 和 。
解决方法 : 1.
添加源域验证集 :监控源域性能,如果下降则停止域适应 2.
调节对抗权重 :不要让对抗权重太大 3.
类别对齐 :只对齐相同类别的样本( conditional domain
adaptation)
Q7:
如何评估域适应的效果?除了目标域准确率还有什么指标?
除了准确率,还可以评估:
-distance :度量两个域的差异
$$
d_{} = 2(1 - 2) $$
其中
是域分类器的错误率。 越小,域越对齐。
MMD :直接计算特征空间的 MMD 3. t-SNE 可视化 :将源域和目标域特征降维到
2D,观察是否混合
类内/类间距离比 : 类 间 距 离 类 内 距 离
越大越好(类别分离但域混合)
每个类别的准确率 :检查是否所有类别都提升(避免某些类别退化)
Q8: DANN
的对抗权重
如何调节?有自动调节的方法吗?
DANN 使用自适应权重:
其中 是训练进度,
控制增长速度。
超参数
的选择 : - 太小(如
1):对抗权重增长太慢,域对齐不足 - 太大(如
100):对抗权重增长太快,破坏分类学习 - 推荐值:
自动调节 :可以用验证集上的性能动态调整 :
Q9:
如何处理源域和目标域的类别不一致问题( partial/open-set 域适应)?
标准域适应假设源域和目标域类别相同 。但实际中可能出现:
Partial DA :目标域类别是源域类别的子集( )
Open-set DA :目标域有源域没有的类别( )
解决方法 :
Partial DA : - 只对齐源域中在目标域也存在的类别 -
使用类别权重 :给目标域中不存在的源域类别小权重
Open-set DA : - 添加"未知类",检测目标域中的新类别 -
使用开放世界分类器 :当预测置信度低时拒绝分类
Q10: CycleGAN
的循环一致性损失为什么能保证语义不变?
循环一致性损失:
$$
L_{} = {x_s}[|F(G(x_s)) - x_s|_1] + {x_t}[|G(F(x_t)) -
x_t|_1] $$
直觉 :如果
将源域转换为目标域、
将目标域转换为源域,那么
应该接近恒等映射。
数学上 ,循环一致性等价于要求 和 是(近似)逆映射 :
$$
F G _{S}, G F {_T} $$
这保证了语义信息不丢失: 经过 和 后能恢复。
但注意 :循环一致性不能完全保证语义不变。例如,如果
将所有源域图像映射到同一个目标域图像,
再将其映射回原图像,循环一致性仍然满足,但语义显然丢失了。因此通常还需要其他约束(如感知损失、身份损失)。
Q11: 如何选择 MMD
的核函数和带宽?
核函数选择 :
高斯核(最常用) :
多核组合 :使用多个不同带宽的高斯核
带宽 选择 :
经验法则(median heuristic ):
即所有样本对距离的中位数。直觉:这样的 让核函数既不太局部( 太小)也不太全局( 太大)。
多核 MMD :使用 ,其中 是 median heuristic。
Q12:
域适应在哪些实际应用中最有价值?
医疗影像 :不同医院、不同设备的数据分布不同
CT 从西门子迁移到 GE
MRI 从 1.5T 迁移到 3T
自动驾驶 :不同天气、光照、城市的数据分布不同
推荐系统 :不同国家、不同时间段的用户行为不同
情感分析 :不同领域的情感表达方式不同
目标检测/分割 :合成数据迁移到真实数据
域适应特别适合标注困难但源域数据丰富 的场景。
相关论文
Domain-Adversarial Training of Neural Networks
(DANN)
Ganin et al., JMLR 2016
https://arxiv.org/abs/1505.07818
Learning Transferable Features with Deep Adaptation
Networks (DAN)
Long et al., ICML 2015
https://arxiv.org/abs/1502.02791
Deep CORAL: Correlation Alignment for Deep Domain
Adaptation
Sun and Saenko, ECCV 2016
https://arxiv.org/abs/1607.01719
Unpaired Image-to-Image Translation using
Cycle-Consistent Adversarial Networks (CycleGAN)
Zhu et al., ICCV 2017
https://arxiv.org/abs/1703.10593
Adversarial Discriminative Domain Adaptation
(ADDA)
Tzeng et al., CVPR 2017
https://arxiv.org/abs/1702.05464
A Theory of Learning from Different Domains (Ben-David
Theory)
Ben-David et al., Machine Learning 2010
https://link.springer.com/article/10.1007/s10994-009-5152-4
Revisiting Batch Normalization For Practical Domain
Adaptation (AdaBN)
Li et al., ICLR Workshop 2017
https://arxiv.org/abs/1603.04779
Maximum Mean Discrepancy
Gretton et al., JMLR 2012
https://jmlr.org/papers/v13/gretton12a.html
Conditional Adversarial Domain Adaptation
(CDAN)
Long et al., NeurIPS 2018
https://arxiv.org/abs/1705.10667
Universal Domain Adaptation
You et al., CVPR 2019
https://arxiv.org/abs/1902.06906
Covariate Shift Adaptation by Importance Weighted
Cross-Validation
Sugiyama et al., JMLR 2007
http://www.jmlr.org/papers/v8/sugiyama07a.html
Detecting and Correcting for Label Shift with Black Box
Predictors
Lipton et al., ICML 2018
https://arxiv.org/abs/1802.03916
总结
域适应是迁移学习中最具挑战性但也最实用的问题。本文从分布偏移的数学刻画(协变量偏移、标签偏移、概念偏移)出发,推导了无监督域适应的理论基础(
Ben-David 理论),详细解析了 DANN 、 MMD 、 CORAL
等经典方法的原理和适用场景。
我们看到,域适应的核心是在对齐特征分布 和保持判别性 之间找到平衡。
DANN 通过对抗训练隐式最小化域差异, MMD 通过核函数显式度量分布距离,
AdaBN
通过调整统计量消除低阶差异。每种方法都有其优势和局限性,需要根据具体应用场景选择。
完整的 DANN
实现展示了梯度反转层、域判别器、自适应对抗权重等关键技术。下一章我们将探讨Few-Shot
Learning ,研究如何在极少样本下学习新类别。