学习初稿 · 第 17 课。 本篇已开放网页阅读,用于先建立完整认识。文中手算值和示意记录不代表真实训练成绩;真实数据训练、预训练权重和板端测试以本篇具体说明为准,本篇已附代码练习,实际验证范围见文末与包内记录。
为什么不每次都从随机参数开始
从零训练需要自己的数据来逐步塑造特征。预训练网络已经在其他任务或数据上学到表示,可以作为新任务起点。迁移学习讨论怎样利用这些已有知识,微调则是继续更新部分或全部参数的一种方式。
目标仍是飞机汽车分类。预训练模型原来的类别头通常不符合我们的两类编号,必须更换并训练;直接把原来的某两个输出取出来,不等同于完成同样定义的新任务。
三种方案应该分别比较
| 方案 | 哪些参数更新 | 主要问题 |
|---|---|---|
| 从零训练 | 全部 | 数据是否足够,优化是否稳定 |
| 冻结主干 | 新分类头 | 原有表示是否适合当前任务 |
| 部分或全部微调 | 头及选定主干层 | 如何避免过度破坏已有表示 |
先冻结主干可以降低训练成本,帮助检查类别头和输入流程;若性能受限,再逐步解冻。不是所有领域都与预训练数据接近,医学、工业纹理或特殊传感器图像可能需要不同策略。
权重与预处理是一套约定
以 Torchvision 的预训练 ResNet-18 为例,权重枚举提供相应变换入口。使用显式权重版本比只记“默认权重”更便于复现。调用权重加载可能下载文件,需要提前准备网络或本地缓存。
下面展示配置与一次训练步,依赖 PyTorch、Torchvision 和已缓存的权重。随机输入仅用于说明计算,不是迁移训练数据,也不能用损失作为真实成绩:
|
实际图片应经过 preprocess,并核对其裁剪是否仍包含目标。此处创建的零张量只用于结构演示,没有冒充经过真实预处理的样本。相关训练与冻结模式见 官方迁移学习教程。
冻结梯度不等于固定整个主干行为
如果后面直接调用 model.train(),BatchNorm 等层可能恢复训练行为,即使权重被冻结,其运行统计仍可能变化。完全冻结特征提取器时,可保持主干为评估模式,只训练新头;实际工程应把这个策略写入训练入口,而不是只在初始化时调用一次。
解冻部分层后,需要让优化器包含新解冻参数。可以重新构建优化器或正确添加参数组,并给主干较小学习率、给新头较大学习率。不要把这些数值当通用常数,应观察验证曲线与梯度行为。
公平比较迁移是否有用
保持数据划分、类别定义与指标一致,同时报告输入分辨率、训练时间和参数更新量。如果迁移模型使用更高分辨率和更大计算量,其改善不能全部归因于预训练。
小数据下可以先比较冻结与微调,观察哪类错误被修复。验证集太小时,单次微小提升可能来自随机波动;适当重复,并保留错误清单。如果迁移反而更差,要检查输入约定、类别映射、裁剪和学习率,而不是立即断言预训练无用。
练习
- 修改
requires_grad后,为什么还要检查优化器参数组? - 预训练图片常用 224 尺寸,将 32×32 放大是否新增了细节?
- 什么情况下先冻结主干比较合适?
参考答案
优化器可能没有包含后来解冻的参数,参数有梯度也不会被它更新。插值只改变采样网格,不产生真实细节。数据少、想先验证接口或控制训练成本时,可先冻结主干训练新头,再根据验证结果决定是否微调。
本篇未下载权重,也未报告真实迁移训练成绩。后续正式实验要固定权重版本并保存其预处理约定。下一课从计算与存储角度理解轻量化。
配套代码练习
实验:冻结主干与 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。
