系列 · 迁移学习 · 第 1 篇

迁移学习(一):基础与核心概念

迁移学习入门指南:为什么迁移有效、形式化定义、分类体系、负迁移,以及一个基于 MMD 域适应的完整特征迁移实现。

我花了整整两周时间,用一整柜 GPU 训练出一个 ImageNet 分类器。周一早上,团队负责人突然要求我做一个胸片肺炎识别模型,而手头的标注数据只有 200 张——难道还要再排队两周 GPU 时间,从零开始训练吗?

当然不会。我会直接利用 ImageNet 模型已经学到的边缘、纹理和形状知识,替换掉最后一层,在 X 光片上微调。两小时后,就能得到一个性能远超任何仅靠这少量数据从随机初始化训练出的模型。这就是 迁移学习,也是为什么大多数现实中的深度学习项目能在几天内交付,而不是耗时数月。

本文是整个系列的基础,在深入更专业的主题前,你需要先掌握以下七个关键点:

  1. 为什么从头训练并不总是可行;
  2. 域、任务、源域与目标域的形式化定义;
  3. 迁移学习的分类体系——归纳式、直推式、无监督式;
  4. 深度网络中每一层到底迁移了什么
  5. 负迁移:当借用的知识反而拖后腿时;
  6. Ben-David 界MMD:如何预判迁移是否有效;
  7. 一个可运行的 MMD 特征对齐实现

前置知识:熟悉基本机器学习术语(如损失函数、梯度下降、分类)并能阅读 Python 代码。


为什么需要迁移学习#

从头训练的困境#

教科书式的监督学习流程默认满足三个条件,但在真实工业场景中,这些条件几乎从不成立:

  • 海量标注数据:现代深度网络通常需要数万至数百万条标注样本才能泛化良好,但现实中很少有团队拥有如此丰富的标注资源。
  • 充裕的算力:从随机初始化训练一个 ResNet-50 需要数百 GPU 小时;而从零训练一个 Transformer 可能消耗上万个 GPU 小时。
  • 无法复用已有知识:即便是高度相关的任务(比如胸片 X 光 vs. 胸部 CT),也得从零开始。

相比之下,真实的医疗项目往往只提供几百例罕见病样本,标注必须由持证医生完成,且交付周期以周计。迁移学习正是为了解决这一矛盾:利用在大规模通用数据集上训练好的模型,低成本地适配到数据稀缺的任务上

下图展示了“数据稀缺 + 分布偏移”的典型情形:虽然类别相同,但目标域在特征空间中发生了旋转和平移——这正是从 ImageNet 自然图像迁移到医学影像,或从一家医院的扫描仪迁移到另一家时常见的协变量偏移(covariate shift)的简化示意图。

源域与目标域的分布偏移

直觉#

人类天生擅长知识迁移:

  • 会骑自行车的人,一下午就能学会骑摩托车,而非花上一年;
  • Python 程序员看一眼 Java 语法,就能立刻识别出类、循环和异常;
  • 见过家猫的人,第一次见到狮子也会马上判断为“某种猫科动物”。

深度网络同样具备这种特性。视觉模型的浅层卷积层学到的是近乎通用的基础构件——有方向的边缘、色块、简单纹理,这些对几乎所有视觉任务都有用。深层则学习更专门的概念(如毛皮花纹、眼睛形状),但即便如此,它们在相关领域间仍可复用。迁移学习正是系统性地利用这种知识重叠的工程实践。

一句话核心思想#

给定一个标注丰富的源域和一个标注极少的目标域,迁移学习将源域的知识迁移过来,使目标模型的表现优于单独训练的结果。

唯一前提是源域与目标域之间存在某种相关性。它们不必共享特征空间、标签集,甚至不必属于同一模态——但共享得越多,可迁移的内容就越丰富。


形式化定义#

要精确讨论迁移学习,必须区分两个初学者常混淆的概念:关乎“输入长什么样”,任务关乎“需要预测什么”。Pan 和 Yang 在 2010 年的综述中将这一区分确立为整个领域的基石。

#

一个域是二元组 $\mathcal{D} = \{\mathcal{X}, P(X)\}$ ,其中 $\mathcal{X}$ 是特征空间,$P(X)$ 是其上的边缘分布。

  • 源域:ImageNet 的 RGB 自然图像,$\mathcal{X} = \mathbb{R}^{224 \times 224 \times 3}$$P_S(X)$ 表示自然场景的像素统计分布。
  • 目标域:胸部 CT 切片,维度相同,但 $P_T(X)$ 的灰度直方图、结构先验和噪声特性截然不同。

任务#

一个任务是二元组 $\mathcal{T} = \{\mathcal{Y}, f(\cdot)\}$ :标签空间和待学习的预测函数。在监督学习中,任务隐含地固定了条件分布 $P(Y \mid X)$

  • 任务 1:ImageNet 1000 类分类,$|\mathcal{Y}| = 1000$
  • 任务 2:肺炎二分类,$|\mathcal{Y}| = 2$

源域 vs. 目标域#

源域目标域
$\mathcal{D}_S$ 对应任务 $\mathcal{T}_S$$\mathcal{D}_T$ 对应任务 $\mathcal{T}_T$
标注充足稀缺或缺失
分布$P_S(X), P_S(Y\mid X)$$P_T(X), P_T(Y\mid X)$

迁移学习明确不要求源域与目标域完全一致——这恰恰是它存在的根本原因。两者可在特征空间($\mathcal{X}_S \neq \mathcal{X}_T$ )、边缘分布($P_S(X) \neq P_T(X)$ )、标签空间($\mathcal{Y}_S \neq \mathcal{Y}_T$ )或条件分布($P_S(Y\mid X) \neq P_T(Y\mid X)$ )中的任意组合上存在差异,每种组合对应一个不同的子问题。

形式化目标#

给定源域 $\mathcal{D}_S$ 及其任务 $\mathcal{T}_S$ ,目标域 $\mathcal{D}_T$ 及其任务 $\mathcal{T}_T$ ,迁移学习旨在利用 $\mathcal{D}_S$$\mathcal{T}_S$ 的知识提升目标预测函数 $f_T(\cdot)$ 的性能,其中 $\mathcal{D}_S \neq \mathcal{D}_T$$\mathcal{T}_S \neq \mathcal{T}_T$

$$ \epsilon_T < \epsilon_0 $$

或等价地:用更少的目标标注达到相同精度。后者通常是衡量价值更诚实的标准——迁移学习很少能提升已饱和模型的上限,但它能在饱和点之前大幅降低标注成本。


迁移学习的分类体系#

该领域看似杂乱,但若按“目标侧缺少什么”来组织,就能清晰划分为三类。

迁移学习分类体系:归纳、直推、无监督

归纳迁移#

  • 源任务与目标任务不同$\mathcal{T}_S \neq \mathcal{T}_T$ );
  • 目标域有少量标注——通常很少;
  • 方法:预训练后微调、多任务学习、自训练;
  • 典型例子:将 ImageNet 预训练的 ResNet 主干换上新分类头,用于仅有几百张标注胸片的肺炎分类任务。这是大多数医学影像论文的标准流程。

直推迁移#

  • 任务相同,但域不同$\mathcal{T}_S = \mathcal{T}_T$$\mathcal{D}_S \neq \mathcal{D}_T$ );
  • 目标域无任何标注
  • 方法:域适应(特征对齐、对抗对齐)、样本重加权;
  • 典型例子:在 GTA5 游戏画面训练的语义分割模型,直接部署到 Cityscapes 真实驾驶数据上,全程无需新标注。自动驾驶研究大量依赖此类方法。

无监督迁移#

  • 双方均无标注,迁移的是结构本身——表示、聚类、流形;
  • 方法:自监督预训练(MoCo、SimCLR、MAE)、深度聚类;
  • 典型例子:在通用语料上训练的 Word2Vec 或 BERT,作为特征提取器用于任意下游 NLP 任务。这也是现代基础模型流水线的起点。

实践中这些类别常相互交织:典型的基础模型流程先进行无监督预训练,再通过归纳迁移到下游任务;若部署环境与标注训练集存在差异,还会叠加一层直推域适应。这套分类体系更像一套词汇表,而非严格划分。


每一层迁移的是什么#

深度网络并非单一知识单元,而是由逐层递进的表示堆叠而成。Yosinski 等人(2014)做过一个经典实验:用 ImageNet 的一半数据训练 CNN,冻结前 $k$ 层,在另一半数据上重训剩余部分,并滑动 $k$ 观察准确率变化。结果如下图所示,这是关于迁移学习最实用的经验事实。

CNN 各层特征的可迁移性

该图揭示了三点:

  1. 低层特征(conv1conv3)具有通用性:边缘和纹理在视觉任务中近乎普适。冻结这些层几乎无损性能——事实上,在小目标数据集上迁移它们的效果常优于从头训练。
  2. 高层特征(conv5 及以后)具有特异性:针对 ImageNet 类别优化的滤波器难以匹配目标任务。冻结它们会导致显著性能下降——橙色“冻结”曲线急剧下滑。
  3. 微调可免费恢复特异性:蓝色“微调”曲线保持平稳——只要允许高层适应目标分布,就能同时保留低层通用先验并匹配目标。这正是“冻结低层、微调高层”成为默认策略的原因。

这张图解释了后续大部分实操建议:哪些层该冻结、各模块的学习率如何设置,以及为何 LoRA 等参数高效方法只作用于深层。


负迁移#

$$ \epsilon_T > \epsilon_0 $$

下图清晰展示了这一现象:随着源-目标差异增大,迁移准确率会跌破从头训练的基线。越过该交叉点后,你实际上是在花钱引入错误的归纳偏置。

负迁移:随域散度增长的正/负迁移区域

成因#

  1. 输入分布差异大:照片纹理对手绘草图毫无用处,高频统计特性完全不同;
  2. 任务冲突:源任务最优解可能远离目标任务最优解,梯度下降难以跳出源任务的局部最优;
  3. 源域过拟合:预训练模型可能记住了数据集特有噪声(如 ImageNet 图片角落的固定水印),这类“知识”会误导目标预测。

规避方法#

  • 先测量:在原始或浅层特征上计算 MMD 或训练域分类器。若差异巨大,则减少迁移强度或更换源数据集;
  • 选择性迁移:冻结通用低层,重训特定高层。前述可迁移性曲线即为指南;
  • 正则化微调:添加 $L_2$ 惩罚项将参数拉向预训练值(如 L2-SP),限制漂移幅度;
  • 集成保底:不确定时,将迁移模型与从头训练模型取平均。多数基准下,集成效果不会差于两者中较弱者。

图中的交叉点并非理论假设,而是每位迁移学习实践者实际行走的操作曲线。你的任务是在上线前明确自己处于哪一侧。


量化迁移可行性#

有两个工具可在训练前预判迁移是否有效。

Ben-David 界#

$$ \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^{*}$ :最优假设在双域上的联合误差下限,由问题本身决定。若不存在单个假设能在双域上都表现良好,再精巧的对齐也无济于事。

该界的实践启示很残酷:若散度或 $\lambda^{*}$ 过大,直接更换源域,而非浪费算力尝试注定失败的适应。

最大均值差异(MMD)#

$$ \mathrm{MMD}(\mathcal{D}_S, \mathcal{D}_T) \;=\; \big\lVert \mathbb{E}[\phi(X_S)] - \mathbb{E}[\phi(X_T)] \big\rVert_{\mathcal{H}}. $$ $$ \mathcal{L} \;=\; \mathcal{L}_{\mathrm{task}} \;+\; \lambda \cdot \mathrm{MMD}^{2}. $$

最小化第二项可对齐源-目标特征分布,这正是 Ben-David 界所要求的。

一条实用经验法则:预训练后,嵌入空间中 MMD 低于 0.1 是强正向信号;高于 0.5 则基本处于负迁移区域。


整合起来:标准配方#

预训练主干 + 新任务头#

解决九成生产问题的方案其实极为简单:

预训练主干 + 任务专用新头

取一个在大规模通用数据集上训练好的卷积或 Transformer 主干,丢弃原分类头,接上适配你标签空间的新头,然后微调。逐层决定冻结(省钱、保守)还是解冻(费钱、表达力强)。对大多数预算有限的场景,最佳策略是:“先冻结主干训练若干 epoch,再以极小学习率解冻微调”。

为何在小数据场景下特别有效#

下图的数据效率曲线是迁移学习最有力的商业论据:仅 10 个标注时,从头训练几乎等同于瞎猜,而迁移已能提供可用模型;100 个标注时,差距巨大;到 10,000 个标注时,两条曲线收敛——这正是迁移学习对无力标注上万样本的团队最有价值的原因。

数据效率:目标域准确率 vs. 标注量

可将此图视为一份契约:水平切片告诉你迁移能省多少标注,垂直切片告诉你能提升多少精度。两种视角等价,且收益常达数量级之差。

无监督场景:域适应#

当目标域完全无标注时,无法常规微调。主流策略(本系列第三篇详述)是学习一个共享编码器,将源域和目标域特征拉入同一子空间,然后在(有标注的)源域上训练分类器,并直接应用于(已对齐的)目标域。

域适应问题设定:共享编码器

训练目标正是前述损失函数:源域分类损失 + MMD 惩罚项以对齐嵌入。下一节将展示,该方法在 PyTorch 中不到 100 行即可实现。


迁移学习 vs. 相关概念#

迁移学习与若干邻近领域边界模糊,术语区分至关重要:

维度迁移学习多任务学习
目标优化目标性能同时优化所有任务
训练方式顺序(先源后目标)并行(联合训练)
数据假设域可不同任务需相关
典型模式预训练-微调共享编码器 + 多头
维度迁移学习元学习
目标迁移特定知识学习“如何快速掌握新任务”
训练数据单/少数源域大量多样化任务
适应方式通常需微调快速小样本更新
维度迁移学习域泛化
测试信息可访问目标数据目标域未知
方法域适应学习域不变特征

这些区别决定了你该查阅哪些论文、使用哪些基准和工具。


完整实现:基于 MMD 的特征迁移#

源域与目标域分布以及 RKHS 中的 MMD 差距。

以下是一个完整示例,演示本文所述工作流。我们模拟 2D 域偏移,并比较三种策略:在目标域从头训练、直接迁移源模型、通过 MMD 对齐进行特征迁移。

  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
"""
特征迁移与域适应示例
方法:特征提取 + MMD 对齐 + 微调
"""

import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
from sklearn.metrics import accuracy_score
from sklearn.svm import SVC

np.random.seed(42)
torch.manual_seed(42)

# --- 数据生成:模拟域偏移 ---------------------------------------------------

def generate_source_domain(n_samples=1000):
    """源域:两个分得很开的高斯簇。"""
    X0 = np.random.randn(n_samples // 2, 2) * 0.5 + np.array([-2, -2])
    X1 = np.random.randn(n_samples // 2, 2) * 0.5 + np.array([2, 2])
    X = np.vstack([X0, X1])
    y = np.hstack([np.zeros(n_samples // 2), np.ones(n_samples // 2)])
    return X, y

def generate_target_domain(n_samples=200):
    """目标域:旋转 45 度并平移(协变量偏移)。"""
    theta = np.pi / 4
    rotation = np.array([[np.cos(theta), -np.sin(theta)],
                         [np.sin(theta),  np.cos(theta)]])
    X0 = (np.random.randn(n_samples // 2, 2) * 0.6 + np.array([-1, -1])) @ rotation.T
    X1 = (np.random.randn(n_samples // 2, 2) * 0.6 + np.array([ 1,  1])) @ rotation.T
    X = np.vstack([X0, X1])
    y = np.hstack([np.zeros(n_samples // 2), np.ones(n_samples // 2)])
    return X, y

X_source,        y_source        = generate_source_domain(1000)
X_target_train,  y_target_train  = generate_target_domain(50)   # 少量标注数据
X_target_test,   y_target_test   = generate_target_domain(200)  # 测试集

print(f"源域: {X_source.shape}  |  目标训练: {X_target_train.shape}  "
      f"|  目标测试: {X_target_test.shape}")

# --- 方法 1:仅在少量目标样本上从头训练 -----------------------------------

clf_scratch = SVC(kernel="rbf", gamma="auto").fit(X_target_train, y_target_train)
acc_scratch = accuracy_score(y_target_test, clf_scratch.predict(X_target_test))
print(f"[从头训练]   准确率: {acc_scratch:.4f}")

# --- 方法 2:在源域训练,直接套用到目标域(无适应) -----------------------

clf_direct = SVC(kernel="rbf", gamma="auto").fit(X_source, y_source)
acc_direct = accuracy_score(y_target_test, clf_direct.predict(X_target_test))
print(f"[直接迁移]   准确率: {acc_direct:.4f}")

# --- 方法 3:特征迁移 + MMD 对齐 + 微调 -----------------------------------

class FeatureExtractor(nn.Module):
    def __init__(self, input_dim=2, hidden_dim=32, output_dim=16):
        super().__init__()
        self.encoder = nn.Sequential(
            nn.Linear(input_dim, hidden_dim), nn.ReLU(),
            nn.Linear(hidden_dim, output_dim), nn.ReLU(),
        )

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

class Classifier(nn.Module):
    def __init__(self, input_dim=16, num_classes=2):
        super().__init__()
        self.fc = nn.Linear(input_dim, num_classes)

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

def compute_mmd(x_source, x_target, gamma=1.0):
    """计算两个 batch 之间的 RBF 核 MMD(有偏估计,SGD 下足够用)。"""
    xx = torch.sum(x_source ** 2, dim=1, keepdim=True)
    yy = torch.sum(x_target ** 2, dim=1, keepdim=True)
    K_ss = torch.exp(-gamma * (xx + xx.t() - 2 * x_source @ x_source.t()))
    K_tt = torch.exp(-gamma * (yy + yy.t() - 2 * x_target @ x_target.t()))
    K_st = torch.exp(-gamma * (xx + yy.t() - 2 * x_source @ x_target.t()))
    n_s, n_t = x_source.size(0), x_target.size(0)
    return K_ss.sum() / n_s ** 2 + K_tt.sum() / n_t ** 2 - 2 * K_st.sum() / (n_s * n_t)

def train_with_mmd(X_source, y_source, X_target_unlabeled,
                   X_target_labeled, y_target_labeled,
                   epochs=100, lambda_mmd=0.5):
    """两阶段训练:源域分类 + MMD 对齐,然后在目标域上微调。"""
    X_s   = torch.FloatTensor(X_source)
    y_s   = torch.LongTensor(y_source.astype(int))
    X_t_u = torch.FloatTensor(X_target_unlabeled)
    X_t_l = torch.FloatTensor(X_target_labeled)
    y_t_l = torch.LongTensor(y_target_labeled.astype(int))

    feat = FeatureExtractor()
    clf  = Classifier()
    optimizer = optim.Adam(list(feat.parameters()) + list(clf.parameters()), lr=1e-3)
    criterion = nn.CrossEntropyLoss()

    loader = DataLoader(TensorDataset(X_s, y_s), batch_size=32, shuffle=True)

    # 阶段 1:源域分类 + 在无标注目标上做 MMD 对齐
    for _ in range(epochs):
        for X_batch, y_batch in loader:
            optimizer.zero_grad()
            loss_cls = criterion(clf(feat(X_batch)), y_batch)
            loss_mmd = compute_mmd(feat(X_batch), feat(X_t_u))
            (loss_cls + lambda_mmd * loss_mmd).backward()
            optimizer.step()

    # 阶段 2:在少量标注目标样本上做轻量微调
    for _ in range(50):
        optimizer.zero_grad()
        criterion(clf(feat(X_t_l)), y_t_l).backward()
        optimizer.step()

    return feat, clf

feat, clf_mmd = train_with_mmd(
    X_source, y_source,
    X_target_unlabeled=X_target_test,
    X_target_labeled=X_target_train,
    y_target_labeled=y_target_train,
)

feat.eval(); clf_mmd.eval()
with torch.no_grad():
    preds = torch.argmax(clf_mmd(feat(torch.FloatTensor(X_target_test))), dim=1).numpy()

acc_transfer = accuracy_score(y_target_test, preds)
print(f"[特征 + MMD] 准确率: {acc_transfer:.4f}")
print(f"\n相比从头训练提升: "
      f"{(acc_transfer - acc_scratch) / acc_scratch * 100:.1f}%")

各组件作用#

组件功能
generate_source/target_domain制造可控协变量偏移(旋转 + 平移)
compute_mmd计算两个嵌入 batch 间的 RBF 核 MMD
阶段 1 训练在源域分类的同时拉近目标嵌入
阶段 2 微调利用 50 个标注样本进行轻量适配

关键参数lambda_mmd=0.5 控制对齐强度——过小会忽略目标域,过大会破坏源域分类精度。100/50 epoch 划分将大部分预算用于对齐,小部分用于精细化调整。

将合成 2D 数据替换为任意真实数据对(如 Office-31、DomainNet、VisDA),代码结构无需改动——这正是本练习的意义所在。


在训练前检测负迁移#

防范负迁移最便宜的保险是 双基线探针:只需十分钟,就能告诉你是否该继续或更换源数据集。

协议#

你在目标标签上训练两个线性探针:

  1. 冻结源探针:冻结预训练的源骨干网络,仅在其冻结特征上训练线性分类器。记其目标测试准确率为 $a_{\text{frozen}}$
  2. 随机初始化探针:完全丢弃源权重,在展平像素(或同架构随机初始化骨干的特征)上直接训练线性分类器。记其准确率为 $a_{\text{rand}}$

$a_{\text{frozen}}$ 显著高于 $a_{\text{rand}}$ ,说明源表示确实有用,完整微调很可能进一步提升性能。若 $a_{\text{frozen}} \approx a_{\text{rand}}$ ,源表示中性——你可能受益于低层先验,但别期待奇迹。若 $a_{\text{frozen}} < a_{\text{rand}}$ ,源表示会误导分类器,任何微调都可能收敛到比从零开始更差的解。

30 行实现#

 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
import torch, torch.nn as nn, torch.optim as optim

def _train_linear_probe(features, labels, num_classes, n_epochs=5, lr=1e-2):
    probe = nn.Linear(features.size(1), num_classes)
    opt   = optim.Adam(probe.parameters(), lr=lr)
    crit  = nn.CrossEntropyLoss()
    for _ in range(n_epochs):
        opt.zero_grad()
        crit(probe(features), labels).backward()
        opt.step()
    return probe

@torch.no_grad()
def _extract(model, loader):
    feats, labs = [], []
    for x, y in loader:
        feats.append(model(x)); labs.append(y)
    return torch.cat(feats), torch.cat(labs)

def transfer_feasibility_check(source_model, target_train_loader,
                               target_test_loader, num_classes, n_epochs=5):
    """比较源冻结 vs 随机初始化线性探针在目标域上的表现。"""
    source_model.eval()
    rand_model = type(source_model)()      # 同架构,全新初始化
    rand_model.eval()

    f_tr_s, y_tr = _extract(source_model, target_train_loader)
    f_te_s, y_te = _extract(source_model, target_test_loader)
    f_tr_r, _    = _extract(rand_model,   target_train_loader)
    f_te_r, _    = _extract(rand_model,   target_test_loader)

    probe_s = _train_linear_probe(f_tr_s, y_tr, num_classes, n_epochs)
    probe_r = _train_linear_probe(f_tr_r, y_tr, num_classes, n_epochs)

    acc_s = (probe_s(f_te_s).argmax(1) == y_te).float().mean().item()
    acc_r = (probe_r(f_te_r).argmax(1) == y_te).float().mean().item()
    rec   = ("transfer" if acc_s > acc_r + 0.05 else
             "marginal" if acc_s > acc_r - 0.02 else "scratch")
    return {"source_frozen_acc": acc_s, "random_init_acc": acc_r,
            "recommendation": rec}

真实基准中的三种情形#

$\to$ 目标$a_{\text{frozen}}$$a_{\text{rand}}$结论
卫星图像 $\to$ 胸部 X 光片$14\%$$23\%$强负迁移 —— 更换源数据集
ImageNet $\to$ Fashion-MNIST$81\%$$78\%$边缘收益 —— 可迁移,但增益有限
ImageNet $\to$ Food-101$71\%$$32\%$强正迁移 —— 迁移至关重要

卫星案例是警示性的。航拍纹理与医学 X 光片在底层结构上几乎无共性(传感器物理不同、几何先验不同),预训练滤波器会响应错误特征。随机初始化反而胜出。

Fashion-MNIST 行代表多数团队忽略的情形:ImageNet 略有帮助,但若只看迁移准确率,你会把全部 $81\%$ 归功于预训练骨干——实际上其中 $78$ 个百分点本可免费获得。

Food-101 是教科书级成功案例:数千类自然图像与 ImageNet 分布接近,源表示几乎完成了全部工作。

在预订 GPU 时间进行完整微调 之前 运行此探针。若结论是“从零开始”,后续任何操作都无法改变结果。


失败模式分析:何时冻结有害#

标准做法——冻结浅层、微调深层——只是默认策略,而非铁律。存在冻结会损害准确率的情形,识别该情形可在多数基准上提升几个点。

冻结有害的三种情形#

  1. 低层统计量发生分布偏移。若源为 RGB 照片而目标为灰度医学扫描,第一卷积层需 遗忘 其颜色对立滤波器。冻结它会使无效通道永久存在。反之亦然(灰度源到彩色目标),以及扫描仪物理特性、传感器分辨率甚至 JPEG 质量变化时同样适用。
  2. 目标数据集极小,正则化比可迁移性更重要。反直觉但真实:当目标样本少于百例时,冻结骨干迫使线性头通过固定特征映射拟合,而该映射可能无任何轴与目标类别对齐。允许深层轻微调整——即使导致轻度过拟合——有时能找回正确方向,且可通过权重衰减控制。
  3. 源-目标任务差距大,需重组表示。若源为物体分类而目标为纹理表面缺陷定位,仅微调头部过于受限。源学到的特征关注 图中有什么;目标需要 纹理何处异常 的特征。仅靠头部训练无法解决此问题。

扫描冻结深度#

找到最佳操作点的最清晰方法是扫描。固定头部和训练轮数,对 $k \in \{0, 1, 2, 3, 4\}$ 冻结前 $k$ 个模块,选择验证准确率最高的 $k$

 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
import copy, torch, torch.nn as nn, torch.optim as optim

def _freeze_first_k(model, k):
    """就地冻结 model.children() 的前 k 个 '模块'。"""
    for i, child in enumerate(model.children()):
        for p in child.parameters():
            p.requires_grad = (i >= k)

def _train_one(model, loader, n_epochs=10, lr=1e-3):
    params = [p for p in model.parameters() if p.requires_grad]
    opt    = optim.Adam(params, lr=lr)
    crit   = nn.CrossEntropyLoss()
    model.train()
    for _ in range(n_epochs):
        for x, y in loader:
            opt.zero_grad()
            crit(model(x), y).backward()
            opt.step()
    return model

@torch.no_grad()
def _eval(model, loader):
    model.eval(); correct = total = 0
    for x, y in loader:
        correct += (model(x).argmax(1) == y).sum().item(); total += y.size(0)
    return correct / total

def compare_freezing_strategies(model, target_train_loader, target_test_loader,
                                depths=(0, 1, 2, 3, 4), n_epochs=10):
    """对每个冻结深度运行一次微调,返回 {depth: test_acc}。"""
    results = {}
    for k in depths:
        m = copy.deepcopy(model)
        _freeze_first_k(m, k)
        _train_one(m, target_train_loader, n_epochs=n_epochs)
        results[k] = _eval(m, target_test_loader)
    return results

Office-31 上的真实结果(Amazon $\to$ Webcam)#

我在 ImageNet 预训练的 ResNet-18 上扫描冻结深度,源为 Office-31 的 Amazon,目标为 Webcam:

冻结深度 $k$Webcam 测试准确率
$0$ (全微调)$78.2\%$
$1$$78.9\%$
$2$$79.1\%$
$3$$76.5\%$
$4$$71.8\%$

最优值在 $k = 2$ 。冻结过少会浪费目标梯度重新学习通用边缘;冻结过多则锁定与 Webcam 光照和裁剪统计不匹配的特征。过度冻结($k = 4$ )的惩罚超过七个百分点——比多数架构技巧带来的提升更大。

实用启发式#

若源-目标域差距大,则少冻结;若目标数据集小,则多冻结——但务必扫描。扫描仅需五次训练,成本低于你本就要做的超参搜索,且能避免迁移学习中最常见的静默失败:直接使用默认冻结深度,因一行配置损失七个百分点。

下一节将从“是否迁移”转向“如何高效迁移”——一旦可行性与冻结深度确定,其余便是标准流程。

常见问题#

迁移学习一定优于从头训练吗?#

不一定。取决于域相关性、目标数据量和任务相似度。经验法则:当目标数据量低于从头训练所需数据的 10% 时,优先考虑迁移;超过该阈值后,收益递减,工程开销可能得不偿失。

如何选择合适的源域?#

选择规模大、多样性高、模态相同的源域。视觉任务默认 ImageNet;NLP 任务默认强预训练 Transformer(如 BERT、GPT、Llama)。通过 t-SNE 检查浅层特征重叠,并计算 MMD:低于 0.1 为绿灯,高于 0.5 为红灯。

哪些层该冻结?#

初始策略:冻结网络底部 30%–50%,微调其余部分。NLP 任务通常微调全部层,但学习率比预训练时小一个数量级。若数据极少(<100 样本),仅训练最后 1–2 层,否则极易过拟合。

如何检测负迁移?#

务必运行从头训练基线。若迁移模型表现更差、微调时验证损失上升,或预训练后源-目标嵌入 MMD 仍高于 0.5,则存在负迁移。按成本从低到高依次尝试:减少迁移层数、更换源数据集、采用对抗域适应(第三篇详述)。


总结#

我们覆盖了每个迁移学习项目必备的七大要素:

  • 动机:数据稀缺、算力昂贵、知识复用的普适价值;
  • 形式化定义:域 vs. 任务,源 vs. 目标,及其四种差异方式;
  • 分类体系:归纳式、直推式、无监督式——一套术语,三种问题形态;
  • 逐层可迁移性:低层通用、高层专用;冻结低层、微调高层;
  • 负迁移:成因、检测与损害控制;
  • 理论工具:Ben-David 分解与 MMD 作为实用散度估计;
  • 标准配方与代码:预训练主干 + 新头,附带可运行的 MMD 对齐实现。

迁移学习并非魔法,而是对真实数据共享结构的系统化利用。善用之,它是现代深度学习工具箱中杠杆率最高的技术之一;滥用之,模型会悄然变差而你浑然不觉。本系列后续内容,正是教你如何善用它。


参考文献#

  1. Pan, S. J., & Yang, Q. (2010). A survey on transfer learning. IEEE TKDE, 22(10), 1345-1359.
  2. Weiss, K., Khoshgoftaar, T. M., & Wang, D. (2016). A survey of transfer learning. Journal of Big Data, 3(1), 1-40.
  3. Yosinski, J., Clune, J., Bengio, Y., & Lipson, H. (2014). How transferable are features in deep neural networks? NeurIPS.
  4. Rosenstein, M. T. et al. (2005). To transfer or not to transfer. NeurIPS Workshop on Transfer Learning.
  5. Ben-David, S. et al. (2010). A theory of learning from different domains. Machine Learning, 79(1), 151-175.
  6. Gretton, A. et al. (2012). A kernel two-sample test. JMLR, 13, 723-773.
本系列

迁移学习 12 篇

  1. 01 迁移学习(一):基础与核心概念 当前
  2. 02 迁移学习(二):预训练与微调
  3. 03 迁移学习(三):域适应
  4. 04 迁移学习(四):小样本学习
  5. 05 迁移学习(五):知识蒸馏
  6. 06 迁移学习(六):多任务学习
  7. 07 迁移学习(七):零样本学习
  8. 08 迁移学习(八):多模态迁移
  9. 09 迁移学习(九):参数高效微调
  10. 10 迁移学习(十):持续学习
  11. 11 迁移学习(十一):跨语言迁移
  12. 12 迁移学习(十二):工业应用与最佳实践

读有所得?

GitHub 关注我 → 新文周更

GitHub