首页 / PyTorch 入门教程 / 迁移学习与微调

PyTorch 入门教程

迁移学习与微调

本教程共 60 篇 · 第 26 篇 · 更新于 2026-08-17 · 约 4 分钟阅读

PyTorch迁移学习微调预训练模型深度学习入门

本节目标:搞懂迁移学习为什么有用,学会两种玩法——冻结主干当特征提取器、微调整个网络,并把分类头换成自己的。

站在巨人肩膀上

你手里只有 500 张猫狗照片,想训练一个分类器。从零开始训一个 ResNet?大概率惨不忍睹,这么点数据根本喂不饱几千万个参数。

换个思路:先找一个在 ImageNet 上练好的 ResNet,它已经见过 120 万张图、1000 个类别,学会了边缘、纹理、形状这些通用视觉特征。我们直接把它拿来,换掉最后一层,再用自己的数据稍作训练。这就是迁移学习(Transfer Learning)。

打个比方:请十年驾龄的老司机改开货车,比让新手从零学车快得多、稳得多。

为什么迁移学习有效

深度网络的知识是分层的:

  1. 浅层学通用特征:边缘、颜色、纹理
  2. 中层学组合特征:形状、部件
  3. 深层学任务相关特征:猫脸、狗耳

浅中层知识对几乎所有视觉任务都有用,这正是迁移学习成立的根基。从头训练等于把浅层这些「通用能力」也重新学一遍,数据和算力都浪费了。而迁移学习直接继承它们,只需要集中资源学「你的任务特有的那点差异」。

什么时候用迁移学习

迁移学习也不是万能的,用不用、怎么用,主要看两件事:你的数据量,以及新任务和原任务的相似度。

你的数据量新任务与原任务相似度建议策略
少(几百到几千张)相似冻结主干,只训分类头
不相似解冻高层微调,或谨慎全量微调
多(几万张以上)相似全量微调,效果最稳
不相似全量微调,甚至考虑从头训练

相似度怎么判断?新任务和预训练任务都属「图像分类」,就是高相似;图像分类迁移到目标检测、图像分割,相似度中等;迁移到语音、文本,就得靠别的预训练模型了。

加载预训练模型

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 / RegNetmodel.fc
VGG / AlexNetmodel.classifier[-1]
DenseNetmodel.classifier
MobileNet / EfficientNet / ConvNeXtmodel.classifier[-1]
ViTmodel.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()

两个容易忽略的细节

  1. 冻结主干时顺手冻结 BN。BatchNorm 除了可学习参数,还有滑动统计量。只设 requires_grad=False 拦不住统计量更新,预训练信息会被新数据污染。把主干的 BN 层都设成 eval() 最干净。
  2. 验证、推理时记得 model.eval()。这个道理上一章讲过,在迁移学习场景里同样关键,忘了它 Dropout 和 BN 都会捣乱。

另外,冻结主干后喂数据可以更省:把图片先过一遍主干、把特征向量存下来,再训练分类头。这样每个 epoch 都不用重复跑主干,速度起飞。等你想解冻微调时,再回到常规流程。

不止于图像

迁移学习的思路不限于视觉。NLP 里用 HuggingFace 加载预训练的 BERT,套一个分类头,同样是「预训练 + 微调」的路子。可以说,当今深度学习的主旋律就是「大模型预训练、小数据微调」,本章学的正是这门手艺的入门姿势。

下一章我们解决训练中的另一个老大难:过拟合。