迁移学习实战:从MMD理论到DAN代码落地与避坑指南
发布时间:2026/9/30 11:55:03来源:尧图网络
简介这份资源是清华大学龙明盛老师《迁移学习理论与算法》的PDF讲义面向机器学习方向的研究生、算法工程师及希望系统理解域适应理论的读者用于解决源域与目标域分布不一致时的模型泛化问题。压缩包内共1个PDF文件大小约13.68MB内容以理论推导与算法框架为主线涵盖H∆H-Divergence、DAN、DANN、MCD、MDD等代表性方法并延伸至DEV深度嵌入式模型选择以及Accuracy、Precision、Recall、F1-score等评估实现环节。讲义从监督学习的误差界出发逐步引出域间差异度量、偏差-方差-漂移权衡再到算法与性能的桥接结构紧凑、公式与图示并重适合作为迁移学习课程复习或科研入门的参考材料。目前已有268人学习便于读者快速建立从理论到算法的完整认知脉络。1. 迁移学习到底在解决什么问题从龙明盛的理论框架说起你在做一个图像分类任务手头只有几千张标注样本模型训到 70% 准确率就上不去了。但隔壁团队在 ImageNet 上训好的 ResNet 拿来一测直接 85% 起步。这中间的差距就是迁移学习要填的坑。龙明盛教授的《迁移学习理论与算法》系统梳理了这件事的数学本质当源域和目标域的数据分布不一致时如何把源域学到的知识有效地搬到目标域。这不是简单的 fine-tune 就完事背后涉及分布度量、领域自适应、样本权重调整等一整套理论工具。直推式迁移学习、数值最优化算法与理论这些热词之所以反复出现是因为工业界真正卡住的不是“能不能用预训练模型”而是“什么时候该迁移、迁移多少、怎么判断迁移有没有用”。这篇内容面向需要把迁移学习落地到实际任务的算法工程师和研究生从理论框架讲到可复现的实现路径再到参数怎么设、坑在哪。2. 迁移学习的数学基础分布差异怎么度量与优化2.1 从经验风险最小化到领域自适应标准监督学习假设训练数据和测试数据独立同分布经验风险最小化直接优化$$\min_f \frac{1}{n}\sum_{i1}^{n} L(f(x_i), y_i)$$迁移学习打破了这个假设。源域 $D_S {(x_i^S, y_i^S)}{i1}^{n_S}$ 和目标域 $D_T {(x_j^T, y_j^T)}{j1}^{n_T}$ 的联合分布不同$P_S(x,y) \neq P_T(x,y)$。龙明盛在书中把迁移学习按“迁移什么”分为四类基于样本的迁移、基于特征的迁移、基于模型的迁移、基于关系的迁移。工业界最常用的是基于特征和基于模型的两类。核心思路是找到一个变换 $\phi$使得变换后的源域和目标域分布尽可能接近同时保留对任务有用的判别信息。这就引出了两个关键问题——用什么度量分布差异以及怎么把这个度量嵌入优化目标。2.2 最大均值差异MMD及其核方法实现MMD 是迁移学习里最常用的分布度量工具。给定源域样本 ${x_i^S}$ 和目标域样本 ${x_j^T}$MMD 的平方经验估计为$$\text{MMD}^2 \left| \frac{1}{n_S}\sum_{i1}^{n_S} \phi(x_i^S) - \frac{1}{n_T}\sum_{j1}^{n_T} \phi(x_j^T) \right|_{\mathcal{H}}^2$$其中 $\phi$ 是映射到再生核希尔伯特空间RKHS的特征映射。用核技巧展开后可以直接用核矩阵计算不需要显式定义 $\phi$。下面是一个用 Python 计算线性核 MMD 的最小实现import numpy as np def mmd_linear(X_source, X_target): 计算源域和目标域之间的线性 MMD 平方值 X_source: (n_s, d) 源域特征矩阵 X_target: (n_t, d) 目标域特征矩阵 返回: MMD^2 标量 n_s X_source.shape[0] n_t X_target.shape[0] # 源域均值与目标域均值 mean_s X_source.mean(axis0) # (d,) mean_t X_target.mean(axis0) # (d,) # 线性核下 MMD^2 ||mean_s - mean_t||^2 diff mean_s - mean_t mmd2 np.dot(diff, diff) return mmd2 # 使用示例 np.random.seed(42) X_s np.random.randn(500, 128) # 源域500个样本128维 X_t np.random.randn(300, 128) 0.5 # 目标域均值偏移0.5 print(fMMD^2 {mmd_linear(X_s, X_t):.4f})这段代码的逻辑很直接线性核下 MMD 退化为两个分布均值之差的欧氏距离平方。参数说明——X_source和X_target的维度必须一致同一个特征空间样本数可以不同。实际使用中如果分布差异是非线性的需要换成高斯核def mmd_rbf(X_source, X_target, gamma1.0): 高斯核RBFMMD 平方的无偏估计 gamma: 核带宽参数控制对分布差异的敏感度 n_s X_source.shape[0] n_t X_target.shape[0] # 计算核矩阵 K_ss np.exp(-gamma * np.sum((X_source[:, None] - X_source[None, :])**2, axis-1)) K_tt np.exp(-gamma * np.sum((X_target[:, None] - X_target[None, :])**2, axis-1)) K_st np.exp(-gamma * np.sum((X_source[:, None] - X_target[None, :])**2, axis-1)) # 无偏估计去掉对角项 mmd2 (K_ss.sum() - np.trace(K_ss)) / (n_s * (n_s - 1)) \ (K_tt.sum() - np.trace(K_tt)) / (n_t * (n_t - 1)) \ - 2 * K_st.mean() return mmd2gamma是最关键的参数。太小则所有样本对核值都接近1区分度消失太大则只有极近邻有非零核值估计方差爆炸。经验做法是在中位数距离的倒数附近搜索比如gamma 1 / median_pairwise_distance。2.3 把 MMD 嵌入网络训练DAN 的损失设计知道了怎么度量分布差异下一步就是把它作为正则项加入训练目标。龙明盛团队提出的 DANDeep Adaptation Network是经典做法在网络的多个全连接层后计算源域和目标域的 MMD加到分类损失上。总损失函数$$\mathcal{L} \mathcal{L}{cls} \lambda \sum{l \in \mathcal{L}_{adapt}} \text{MMD}^2(\phi_l(X_S), \phi_l(X_T))$$其中 $\lambda$ 控制迁移正则的强度$\mathcal{L}_{adapt}$ 是需要对齐的层集合。下面是一个 PyTorch 实现的核心片段import torch import torch.nn as nn class DANLoss(nn.Module): def __init__(self, lambda_mmd0.5, gamma1.0): super().__init__() self.lambda_mmd lambda_mmd # 迁移损失权重 self.gamma gamma # RBF核带宽 def forward(self, source_features, target_features, source_logits, source_labels): # 分类损失 cls_loss nn.CrossEntropyLoss()(source_logits, source_labels) # 多层 MMD 损失 mmd_loss 0.0 for s_feat, t_feat in zip(source_features, target_features): mmd_loss self._rbf_mmd(s_feat, t_feat) total cls_loss self.lambda_mmd * mmd_loss return total, cls_loss.item(), mmd_loss.item() def _rbf_mmd(self, X_s, X_t): n_s, n_t X_s.size(0), X_t.size(0) # 用pairwise距离计算RBF核 diff_ss torch.cdist(X_s, X_s, p2) ** 2 diff_tt torch.cdist(X_t, X_t, p2) ** 2 diff_st torch.cdist(X_s, X_t, p2) ** 2 K_ss torch.exp(-self.gamma * diff_ss) K_tt torch.exp(-self.gamma * diff_tt) K_st torch.exp(-self.gamma * diff_st) mmd (K_ss.sum() - K_ss.diag().sum()) / (n_s * (n_s - 1)) \ (K_tt.sum() - K_tt.diag().sum()) / (n_t * (n_t - 1)) \ - 2 * K_st.mean() return mmd参数说明lambda_mmd通常从 0.1 到 1.0 之间调太大模型会过度对齐导致源域判别能力下降太小则迁移效果不明显。gamma建议根据特征维度和数值范围做归一化后取 1.0 左右。训练时源域和目标域数据要同一个 batch 里交替送入不能分开训。3. 从理论到代码搭建一个可复现的迁移学习实验3.1 数据准备与域划分策略理论讲完了现在动手搭一个完整的实验。以 Office-31 数据集为例它包含三个域AmazonA、WebcamW、DSLRD每个域 31 个类别。常见的迁移任务有 A→W、D→A 等六组。数据加载的核心是保证源域有标签、目标域在训练时只有无标签数据直推式迁移学习设定测试时用目标域的标签评估。import torch from torch.utils.data import DataLoader, Dataset from torchvision import transforms, datasets class DomainDataset(Dataset): 包装单个域的数据集支持返回索引用于对齐 def __init__(self, root, domain, transformNone, return_labelTrue): self.data datasets.ImageFolder( rootf{root}/{domain}/images, transformtransform ) self.return_label return_label def __len__(self): return len(self.data) def __getitem__(self, idx): img, label self.data[idx] if self.return_label: return img, label return img, -1 # 目标域训练时不提供真实标签 # 图像预处理源域用强增强目标域用弱增强 source_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) target_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 构建 A→W 任务的数据加载器 source_dataset DomainDataset(./Office31, amazon, source_transform) target_dataset_train DomainDataset(./Office31, webcam, target_transform, return_labelFalse) target_dataset_test DomainDataset(./Office31, webcam, target_transform, return_labelTrue) source_loader DataLoader(source_dataset, batch_size32, shuffleTrue, drop_lastTrue) target_loader DataLoader(target_dataset_train, batch_size32, shuffleTrue, drop_lastTrue) test_loader DataLoader(target_dataset_test, batch_size64, shuffleFalse)关键点drop_lastTrue必须加因为 MMD 计算需要源域和目标域 batch 大小一致。目标域训练集不返回标签这是直推式迁移学习的基本设定——训练时看不到目标域标签但可以看到目标域数据本身。3.2 网络结构与迁移层选择用 ResNet-50 做 backbone替换最后的全连接层为任务分类头。迁移层选在layer4的输出和分类头之前。import torch.nn as nn from torchvision.models import resnet50, ResNet50_Weights class TransferNet(nn.Module): def __init__(self, num_classes31, bottleneck_dim256): super().__init__() backbone resnet50(weightsResNet50_Weights.IMAGENET1K_V2) # 去掉原始fc层 self.features nn.Sequential(*list(backbone.children())[:-1]) self.fc1 nn.Linear(2048, bottleneck_dim) self.bn nn.BatchNorm1d(bottleneck_dim) self.relu nn.ReLU() self.fc2 nn.Linear(bottleneck_dim, num_classes) def forward(self, x): feat self.features(x).flatten(1) # (B, 2048) bottleneck self.relu(self.bn(self.fc1(feat))) # (B, 256) logits self.fc2(bottleneck) return bottleneck, logits迁移层选在bottleneck输出256维和fc2之前。为什么不在更浅的层做对齐浅层特征更通用域差异小对齐收益低深层特征域差异大对齐收益高但难度也大。实践中在 bottleneck 层做 MMD 对齐是性价比最高的选择。3.3 训练循环与参数配置def train_dan(model, source_loader, target_loader, test_loader, epochs50): device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) # 优化器backbone用小学习率新加层用大学习率 params [ {params: model.features.parameters(), lr: 1e-4}, {params: model.fc1.parameters(), lr: 1e-3}, {params: model.bn.parameters(), lr: 1e-3}, {params: model.fc2.parameters(), lr: 1e-3}, ] optimizer torch.optim.SGD(params, momentum0.9, weight_decay5e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxepochs) criterion DANLoss(lambda_mmd0.5, gamma1.0) for epoch in range(epochs): model.train() target_iter iter(target_loader) for src_imgs, src_labels in source_loader: # 取目标域一个batch try: tgt_imgs, _ next(target_iter) except StopIteration: target_iter iter(target_loader) tgt_imgs, _ next(target_iter) src_imgs, src_labels src_imgs.to(device), src_labels.to(device) tgt_imgs tgt_imgs.to(device) # 前向 src_bottleneck, src_logits model(src_imgs) tgt_bottleneck, _ model(tgt_imgs) # 损失 loss, cls_l, mmd_l criterion( [src_bottleneck], [tgt_bottleneck], src_logits, src_labels ) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() # 每5个epoch评估一次 if (epoch 1) % 5 0: acc evaluate(model, test_loader, device) print(fEpoch {epoch1}: cls_loss{cls_l:.4f}, fmmd_loss{mmd_l:.4f}, target_acc{acc:.2f}%) def evaluate(model, loader, device): model.eval() correct, total 0, 0 with torch.no_grad(): for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) _, logits model(imgs) preds logits.argmax(dim1) correct (preds labels).sum().item() total labels.size(0) return 100.0 * correct / total参数配置的要点backbone 学习率设小1e-4是因为预训练权重已经很好大学习率会破坏新加的 fc1/fc2 学习率设大1e-3是因为要快速适应新任务。lambda_mmd0.5是 Office-31 上的经验值如果目标域和源域差异更大比如 A→D可以调到 1.0差异小W→D可以降到 0.1。4. 迁移学习落地避坑那些论文里不会写的问题4.1 负迁移迁移之后效果反而变差了现象源域模型在目标域上的准确率比直接在目标域上训一个从零开始的模型还低。原因源域和目标域的分布差异太大强行对齐反而把源域的噪声和无关特征也迁移过来了。比如用 ImageNet 预训练模型去迁移到医学影像纹理和语义差异巨大。解决先做分布差异的定量评估。用 MMD 或 A-distance 算一下源域和目标域的距离如果 MMD 值比源域内部随机划分的 MMD 大一个数量级以上就要谨慎。应对策略包括只迁移浅层特征冻结深层、减小lambda_mmd、或者改用基于样本权重的方法给源域中与目标域更近的样本更高权重。4.2 MMD 损失不收敛或震荡现象训练日志里mmd_loss忽大忽小目标域准确率不升反降。原因gamma参数设置不当。如果特征维度是 256特征值范围在 [-3, 3] 之间gamma1.0时 RBF 核的带宽太窄只有非常近的样本对才有非零核值导致 MMD 估计的方差极大。解决对特征做 L2 归一化后再算 MMD或者用中位数启发式设置gammadef median_heuristic(X_s, X_t): 根据中位数距离设置gamma X torch.cat([X_s, X_t], dim0) dists torch.cdist(X, X, p2) median_dist dists.median() gamma 1.0 / (median_dist ** 2 1e-8) return gamma.item()在训练初期用这个值后期可以适当增大gamma让对齐更精细。4.3 Batch 内类别不平衡导致对齐偏差现象目标域上某些类别的准确率特别低其他类别正常。原因源域和目标域的 batch 是随机采样的如果某个 batch 里源域只有 2 个“键盘”类样本而目标域有 10 个“鼠标”类样本MMD 会强行把这两个不同类别的分布拉近破坏判别结构。解决用类别平衡采样。源域按类别分层采样目标域虽然无标签但可以用聚类伪标签做粗分层。更简单的做法是增大 batch size至少 64让每个 batch 里类别分布更均匀。4.4 目标域测试集泄露到训练中现象实验报告的目标域准确率很高但换一批新数据就崩了。原因在调参过程中反复用目标域测试集评估实际上已经过拟合了测试集。这在直推式迁移学习里尤其容易发生因为目标域数据在训练时就是可见的。解决从目标域数据里划出一部分作为验证集调参只看验证集测试集只在最终报告时用一次。如果目标域数据量本来就少用交叉验证。4.5 预训练权重与任务不匹配现象用 ImageNet 预训练的 ResNet 迁移到遥感图像分类效果不如预期。原因ImageNet 是自然图像遥感图像是俯视图低层纹理差异就很大。直接迁移所有层反而引入噪声。解决只迁移浅层layer1、layer2深层从零训练。或者用领域相关的预训练权重比如遥感领域有专门的预训练模型。判断标准是如果源域和目标域的低层特征可视化后差异明显就不要迁移深层。5. 进阶技巧用数值最优化视角调迁移学习5.1 把 MMD 对齐看作一个正则化优化问题从数值最优化算法与理论的角度看DAN 的训练目标可以写成$$\min_\theta \mathcal{L}{cls}(\theta) \lambda \cdot \mathcal{R}{mmd}(\theta)$$其中 $\mathcal{R}{mmd}$ 是 MMD 正则项。这个问题的难点在于$\mathcal{L}{cls}$ 只在源域上有定义$\mathcal{R}_{mmd}$ 在源域和目标域上都有定义两者的梯度尺度可能差几个数量级。如果直接相加优化过程会被其中一个主导。一个实用的技巧是梯度归一化在每个 batch 里分别计算两个损失的梯度范数然后按比例缩放。def gradient_normalized_step(model, cls_loss, mmd_loss, optimizer, alpha0.5): 对分类损失和MMD损失做梯度归一化后再更新 alpha: MMD损失的相对权重 # 计算分类损失梯度 optimizer.zero_grad() cls_loss.backward(retain_graphTrue) cls_grad_norm sum(p.grad.norm() ** 2 for p in model.parameters() if p.grad is not None) ** 0.5 # 计算MMD损失梯度 optimizer.zero_grad() mmd_loss.backward(retain_graphTrue) mmd_grad_norm sum(p.grad.norm() ** 2 for p in model.parameters() if p.grad is not None) ** 0.5 # 按比例缩放后合并 optimizer.zero_grad() (cls_loss / (cls_grad_norm 1e-8) alpha * mmd_loss / (mmd_grad_norm 1e-8)).backward() optimizer.step()这个做法让两个损失的梯度范数在同一量级alpha就变成了一个更直观的“相对权重”不用再反复调lambda_mmd。5.2 用验证集上的 MMD 值做早停训练过程中监控目标域验证集上的 MMD 值如果连续几个 epoch 不再下降说明对齐已经饱和继续训只会过拟合源域。def compute_val_mmd(model, source_loader, target_val_loader, device, max_batches10): 在验证集上计算MMD用于早停判断 model.eval() src_feats, tgt_feats [], [] with torch.no_grad(): for i, (imgs, _) in enumerate(source_loader): if i max_batches: break feat, _ model(imgs.to(device)) src_feats.append(feat.cpu()) for i, (imgs, _) in enumerate(target_val_loader): if i max_batches: break feat, _ model(imgs.to(device)) tgt_feats.append(feat.cpu()) X_s torch.cat(src_feats, dim0) X_t torch.cat(tgt_feats, dim0) return mmd_rbf(X_s.numpy(), X_t.numpy(), gamma1.0)早停策略如果验证 MMD 在 5 个 epoch 内下降幅度小于 1%就停止训练。这个习惯帮我省了很多无效训练时间也避免了过拟合。5.3 一个具体的调参顺序血泪经验总结出来的调参顺序按优先级从高到低优先级参数推荐范围影响1backbone 学习率1e-5 ~ 1e-4太大破坏预训练权重太小迁移不动2lambda_mmd0.1 ~ 1.0控制对齐强度过大导致负迁移3gamma中位数启发式影响 MMD 估计质量4batch size32 ~ 64太小导致 MMD 估计方差大5迁移层选择bottleneck 层浅层对齐收益低深层难度大先固定lambda_mmd0.5、gamma用中位数启发式调 backbone 学习率直到源域准确率稳定。然后调lambda_mmd每次翻倍或减半观察目标域验证集准确率。最后微调gamma。我自己的习惯是每次实验只改一个参数改完跑三个随机种子取平均。迁移学习的方差比普通监督学习大单次结果不可信。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网