AI-深度学习

从零搭建视觉模型 17:预训练、迁移学习与微调

展开合集目录
  1. 从零搭建视觉模型 01:图片进入模型后发生了什么
  2. 从零搭建视觉模型 02:Python 起步,写给 C/C++ 开发者
  3. 从零搭建视觉模型 03:从像素到张量的基础概念
  4. 从零搭建视觉模型 04:图片如何变成张量
  5. 从零搭建视觉模型 05:MLP 入门,从手算到图片输入
  6. 从零搭建视觉模型 06:亲手搭建一个小型 CNN
  7. 从零搭建视觉模型 07:模型怎样预测类别:分数、概率与决策
  8. 从零搭建视觉模型 08:让模型开始学习,损失、梯度与训练循环
  9. 从零搭建视觉模型 09:数据集、标签与数据划分
  10. 从零搭建视觉模型 10:损失函数与优化器:怎样把错误变成更新
  11. 从零搭建视觉模型 11:批次、训练轮次与学习率
  12. 从零搭建视觉模型 12:训练第一个真实图片分类器
  13. 从零搭建视觉模型 13:过拟合、数据增强与正则化
  14. 从零搭建视觉模型 14:验证、测试与错误分析
  15. 从零搭建视觉模型 15:保存模型、恢复训练与独立推理
  16. 从零搭建视觉模型 · 综合实践:实现 MLP,从损失计算、训练到 ONNX 推理
  17. 从零搭建视觉模型 16:ResNet 与残差连接
  18. 从零搭建视觉模型 17:预训练、迁移学习与微调
  19. 从零搭建视觉模型 18:轻量模型与计算成本
  20. 从零搭建视觉模型 19:目标检测与 YOLO:从类别到目标框
  21. 从零搭建视觉模型 20:图像分割与 U-Net:给每个像素一个判断
  22. 从零搭建视觉模型 21:注意力与视觉 Transformer
  23. 从零搭建视觉模型 22:从训练模型到 ONNX
  24. 从零搭建视觉模型 23:量化与精度对比
  25. 从零搭建视觉模型 24:从摄像头输入到部署验收
  26. 从零搭建视觉模型 25:发展历史、主流分类与当前格局
从零搭建视觉模型 17:预训练、迁移学习与微调的封面

学习初稿 · 第 17 课。 本篇已开放网页阅读,用于先建立完整认识。文中手算值和示意记录不代表真实训练成绩;真实数据训练、预训练权重和板端测试以本篇具体说明为准,本篇已附代码练习,实际验证范围见文末与包内记录。

为什么不每次都从随机参数开始

从零训练需要自己的数据来逐步塑造特征。预训练网络已经在其他任务或数据上学到表示,可以作为新任务起点。迁移学习讨论怎样利用这些已有知识,微调则是继续更新部分或全部参数的一种方式。

目标仍是飞机汽车分类。预训练模型原来的类别头通常不符合我们的两类编号,必须更换并训练;直接把原来的某两个输出取出来,不等同于完成同样定义的新任务。

三种方案应该分别比较

方案 哪些参数更新 主要问题
从零训练 全部 数据是否足够,优化是否稳定
冻结主干 新分类头 原有表示是否适合当前任务
部分或全部微调 头及选定主干层 如何避免过度破坏已有表示

先冻结主干可以降低训练成本,帮助检查类别头和输入流程;若性能受限,再逐步解冻。不是所有领域都与预训练数据接近,医学、工业纹理或特殊传感器图像可能需要不同策略。

权重与预处理是一套约定

以 Torchvision 的预训练 ResNet-18 为例,权重枚举提供相应变换入口。使用显式权重版本比只记“默认权重”更便于复现。调用权重加载可能下载文件,需要提前准备网络或本地缓存。

下面展示配置与一次训练步,依赖 PyTorch、Torchvision 和已缓存的权重。随机输入仅用于说明计算,不是迁移训练数据,也不能用损失作为真实成绩:

import torch
from torch import nn
from torchvision.models import resnet18, ResNet18_Weights

weights = ResNet18_Weights.IMAGENET1K_V1
model = resnet18(weights=weights)
preprocess = weights.transforms()
for parameter in model.parameters():
    parameter.requires_grad = False
model.fc = nn.Linear(model.fc.in_features, 2)
model.eval()
model.fc.train()
optimizer = torch.optim.AdamW(model.fc.parameters(), lr=1e-3)
images = torch.zeros(2, 3, 224, 224)
labels = torch.tensor([0, 1])
optimizer.zero_grad()
loss = nn.functional.cross_entropy(model(images), labels)
loss.backward()
optimizer.step()
print(model.fc.weight.grad is not None)

实际图片应经过 preprocess,并核对其裁剪是否仍包含目标。此处创建的零张量只用于结构演示,没有冒充经过真实预处理的样本。相关训练与冻结模式见 官方迁移学习教程。

冻结梯度不等于固定整个主干行为

如果后面直接调用 model.train(),BatchNorm 等层可能恢复训练行为,即使权重被冻结,其运行统计仍可能变化。完全冻结特征提取器时,可保持主干为评估模式,只训练新头;实际工程应把这个策略写入训练入口,而不是只在初始化时调用一次。

解冻部分层后,需要让优化器包含新解冻参数。可以重新构建优化器或正确添加参数组,并给主干较小学习率、给新头较大学习率。不要把这些数值当通用常数,应观察验证曲线与梯度行为。

公平比较迁移是否有用

保持数据划分、类别定义与指标一致,同时报告输入分辨率、训练时间和参数更新量。如果迁移模型使用更高分辨率和更大计算量,其改善不能全部归因于预训练。

小数据下可以先比较冻结与微调,观察哪类错误被修复。验证集太小时,单次微小提升可能来自随机波动;适当重复,并保留错误清单。如果迁移反而更差,要检查输入约定、类别映射、裁剪和学习率,而不是立即断言预训练无用。

练习

  1. 修改 requires_grad 后,为什么还要检查优化器参数组?
  2. 预训练图片常用 224 尺寸,将 32×32 放大是否新增了细节?
  3. 什么情况下先冻结主干比较合适?
参考答案

优化器可能没有包含后来解冻的参数,参数有梯度也不会被它更新。插值只改变采样网格,不产生真实细节。数据少、想先验证接口或控制训练成本时,可先冻结主干训练新头,再根据验证结果决定是否微调。

本篇未下载权重,也未报告真实迁移训练成绩。后续正式实验要固定权重版本并保存其预处理约定。下一课从计算与存储角度理解轻量化。

配套代码练习

实验:冻结主干与 BatchNorm 状态

查看完整代码:main.py
"""冻结主干与 BatchNorm 状态。运行方式与练习见 README.md。"""
import torch
from torch import nn
torch.manual_seed(7)
torch.set_num_threads(2)
# 随机初始化的小主干,仅验证冻结机制,不宣称迁移学习精度。
backbone = nn.Sequential(nn.Linear(4,8),nn.BatchNorm1d(8),nn.ReLU())
head = nn.Linear(8,2)
for p in backbone.parameters(): p.requires_grad_(False)
x = torch.randn(16,4)+2
y = torch.arange(16)%2
before_mean = backbone[1].running_mean.clone()
backbone.train(); backbone(x)
assert not torch.equal(before_mean,backbone[1].running_mean)
backbone.eval()
before = {k:v.clone() for k,v in backbone.state_dict().items()}
head_before = head.weight.detach().clone()
opt = torch.optim.SGD(head.parameters(),lr=0.1)
opt.zero_grad()
nn.functional.cross_entropy(head(backbone(x)),y).backward()
opt.step()
for k,v in backbone.state_dict().items(): assert torch.equal(before[k],v)
assert not torch.equal(head_before,head.weight)
print('requires_grad=False 仍会更新训练模式 BN;eval 后主干固定,分类头更新')
下载代码

在项目根目录执行;下载包解压后,在解压目录执行 python3 main.py。

cd 'content/posts/AI-深度学习/从零搭建视觉模型/17-预训练迁移学习与微调/code' && python3 main.py

本篇已附独立实验包,在代码卡片中查看源码、下载或复制运行命令。解压后执行 python3 main.py;依赖安装见包内 README.md。

观察目标: 冻结参数不会冻结 BN 统计;显式 eval 后主干全部状态保持一致。此实验无需权重下载。

动手修改: 去掉 backbone.eval(),找出哪些 state_dict 项变化;实际迁移时加载对应预训练权重及其输入处理,再比较验证指标。

所有输入均在本篇代码中构造;运行范围和验证记录见包内 verification.md。


完整阅读目录 · 上一篇 · 下一篇