迁移学习与微调
本教程共 60 篇 · 第 26 篇 · 更新于 2026-08-17 · 约 4 分钟阅读
本节目标:搞懂迁移学习为什么有用,学会两种玩法——冻结主干当特征提取器、微调整个网络,并把分类头换成自己的。
站在巨人肩膀上
你手里只有 500 张猫狗照片,想训练一个分类器。从零开始训一个 ResNet?大概率惨不忍睹,这么点数据根本喂不饱几千万个参数。
换个思路:先找一个在 ImageNet 上练好的 ResNet,它已经见过 120 万张图、1000 个类别,学会了边缘、纹理、形状这些通用视觉特征。我们直接把它拿来,换掉最后一层,再用自己的数据稍作训练。这就是迁移学习(Transfer Learning)。
打个比方:请十年驾龄的老司机改开货车,比让新手从零学车快得多、稳得多。
为什么迁移学习有效
深度网络的知识是分层的:
- 浅层学通用特征:边缘、颜色、纹理
- 中层学组合特征:形状、部件
- 深层学任务相关特征:猫脸、狗耳
浅中层知识对几乎所有视觉任务都有用,这正是迁移学习成立的根基。从头训练等于把浅层这些「通用能力」也重新学一遍,数据和算力都浪费了。而迁移学习直接继承它们,只需要集中资源学「你的任务特有的那点差异」。
什么时候用迁移学习
迁移学习也不是万能的,用不用、怎么用,主要看两件事:你的数据量,以及新任务和原任务的相似度。
| 你的数据量 | 新任务与原任务相似度 | 建议策略 |
|---|---|---|
| 少(几百到几千张) | 相似 | 冻结主干,只训分类头 |
| 少 | 不相似 | 解冻高层微调,或谨慎全量微调 |
| 多(几万张以上) | 相似 | 全量微调,效果最稳 |
| 多 | 不相似 | 全量微调,甚至考虑从头训练 |
相似度怎么判断?新任务和预训练任务都属「图像分类」,就是高相似;图像分类迁移到目标检测、图像分割,相似度中等;迁移到语音、文本,就得靠别的预训练模型了。
加载预训练模型
torchvision 提供了一堆现成的预训练模型。加载时用新的 weights 参数:
from torchvision import models
# 推荐写法:指定预训练权重
model = models.resnet18(weights="IMAGENET1K_V1")
# 只要结构、不要权重
model = models.resnet18(weights=None)
旧写法 pretrained=True 从 torchvision 0.13 起已弃用,会弹警告,别再用。第一次调用会自动下载权重(约 45 MB)到本地缓存,之后就不用再下。
Tip更规范的做法是用枚举对象:
from torchvision.models import ResNet18_Weights,再传ResNet18_Weights.IMAGENET1K_V1。这样有代码补全,还能直接拿到配套的预处理函数。
两种玩法:特征提取与微调
迁移学习就两条路。
特征提取:冻结主干所有参数,只训练新换的分类头。训练快、不容易过拟合,但天花板有限。
微调(Fine-tuning):解冻全部或部分主干,用小学习率继续训练。上限更高,但要小心别把预训练权重练坏。
先看特征提取,三步走:
import torch.nn as nn
model = models.resnet18(weights="IMAGENET1K_V1")
# 1. 冻结主干所有参数
for param in model.parameters():
param.requires_grad = False
# 2. 换掉分类头:ResNet 的分类头叫 fc,原来是 1000 类
model.fc = nn.Linear(model.fc.in_features, 2) # 改成自己的 2 类
# 3. 优化器只传新层的参数
optimizer = torch.optim.SGD(model.fc.parameters(), lr=0.001, momentum=0.9)
requires_grad = False 的意思是反向传播时不给这些参数算梯度,参数自然不更新。新换上的 fc 默认 requires_grad=True,只有它会被训练。省下大量算力,CPU 上都能跑。
分类头叫什么?因模型而异
不同架构的分类头名字不一样,这是新手最容易卡住的地方:
| 模型 | 分类头位置 |
|---|---|
| ResNet / RegNet | model.fc |
| VGG / AlexNet | model.classifier[-1] |
| DenseNet | model.classifier |
| MobileNet / EfficientNet / ConvNeXt | model.classifier[-1] |
| ViT | model.heads.head |
不确定时先 print(model) 看结构,输入维度可以用 model.fc.in_features 这类属性直接拿。
微调:解冻后的温柔训练
特征提取只动了分类头,天花板有限。想更进一步,就把主干也放开:
model = models.resnet18(weights="IMAGENET1K_V1")
model.fc = nn.Linear(model.fc.in_features, 2)
# 分层学习率:主干走小步,新头走大步
optimizer = torch.optim.SGD([
{"params": model.fc.parameters(), "lr": 0.001},
{"params": [p for n, p in model.named_parameters()
if not n.startswith("fc")], "lr": 0.0001},
], momentum=0.9)
主干学习率通常是分类头的 1/10 甚至 1/100。道理很直白:预训练权重是宝贝,走大步容易踩坏;新头是随机初始化的,反而需要大步快跑。
还有一种进阶打法叫逐步解冻(Gradual Unfreezing):先只训分类头几个 epoch,模型稳定后再解冻最后一组残差块,再解冻更多。稳是稳,就是训练节奏更繁琐,入门阶段可以先不折腾。
Warning微调最大的坑是学习率太大。预训练权重被大学习率一顿乱更新,几天的心血几轮就毁了。主干 lr 从 1e-5 到 1e-4 起步比较安全。NLP 的 BERT 类模型更敏感,业界推荐 2e-5 左右。
数据预处理要对齐
用预训练权重,喂给模型的图也得按当年 ImageNet 的规矩处理:同样的尺寸、同样的归一化参数。
from torchvision import transforms
train_tf = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225]),
])
这三个均值和标准差是 ImageNet 的统计值,所有 torchvision 预训练模型通用。自己瞎改归一化参数,特征分布对不上,效果掉一大截。偷懒的办法是从权重对象直接拿官方预处理:weights.transforms()。
两个容易忽略的细节
- 冻结主干时顺手冻结 BN。BatchNorm 除了可学习参数,还有滑动统计量。只设
requires_grad=False拦不住统计量更新,预训练信息会被新数据污染。把主干的 BN 层都设成eval()最干净。 - 验证、推理时记得
model.eval()。这个道理上一章讲过,在迁移学习场景里同样关键,忘了它 Dropout 和 BN 都会捣乱。
另外,冻结主干后喂数据可以更省:把图片先过一遍主干、把特征向量存下来,再训练分类头。这样每个 epoch 都不用重复跑主干,速度起飞。等你想解冻微调时,再回到常规流程。
不止于图像
迁移学习的思路不限于视觉。NLP 里用 HuggingFace 加载预训练的 BERT,套一个分类头,同样是「预训练 + 微调」的路子。可以说,当今深度学习的主旋律就是「大模型预训练、小数据微调」,本章学的正是这门手艺的入门姿势。
下一章我们解决训练中的另一个老大难:过拟合。