概述
从零训练一个图像分类模型通常需要大量数据和计算资源。如果你的数据集只有几百到几千张图片,从随机初始化开始训练,很容易过拟合,也很难学到通用视觉特征。
迁移学习的思路是:
text
先使用大数据集上训练好的模型
|
保留通用特征提取能力
|
替换最后分类层
|
在自己的数据集上微调
预训练模型已经学到边缘、纹理、形状、局部模式等通用特征。你只需要让它适配自己的类别。
读完本文,你应该能用 Paddle 加载预训练模型,替换分类头,并完成冻结特征层和全量微调两种迁移学习流程。
迁移学习的两种常见方式
| 方式 | 做法 | 适合场景 |
|---|---|---|
| 特征提取 | 冻结 backbone,只训练分类头 | 数据很少、训练资源有限 |
| 微调 | 加载预训练权重,训练部分或全部参数 | 数据较多、想提升精度 |
可以这样选择:
text
数据很少:先冻结 backbone
数据中等:先冻结训练分类头,再解冻全量微调
数据很多:直接全量微调
迁移学习不是固定公式,需要根据数据量、任务相似度、算力和验证集结果调整。
使用 paddle.vision.models 加载预训练模型
Paddle 提供了一些视觉模型接口。例如 ResNet:
python
import paddle
import paddle.nn as nn
from paddle.vision.models import resnet18
model = resnet18(pretrained=True, num_classes=1000)
pretrained=True 表示加载官方预训练权重。num_classes=1000 对应 ImageNet 预训练分类头。
如果你的任务有 5 类,需要替换最后分类层。
不同模型最后分类层名字可能不同,使用前应打印结构:
python
print(model)
对 resnet18,常见分类层是 model.fc。
替换分类头
假设自定义任务有 5 类:
python
num_classes = 5
in_features = model.fc.weight.shape[0]
model.fc = nn.Linear(in_features, num_classes)
有些版本或模型中,weight.shape 的维度含义可能需要确认。更稳的方法是先打印:
python
print(model.fc)
print(model.fc.weight.shape)
如果 model.fc.weight.shape 是 [in_features, out_features],则输入维度是第 0 维。
替换后检查:
python
x = paddle.randn([2, 3, 224, 224])
logits = model(x)
print(logits.shape)
期望:
text
[2, 5]
冻结 backbone:只训练分类头
冻结除分类头外的参数:
python
for name, param in model.named_parameters():
param.stop_gradient = True
for name, param in model.fc.named_parameters():
param.stop_gradient = False
检查可训练参数:
python
for name, param in model.named_parameters():
if not param.stop_gradient:
print("trainable:", name, param.shape)
创建优化器时,只传需要训练的参数:
python
trainable_params = [p for p in model.parameters() if not p.stop_gradient]
optimizer = paddle.optimizer.Adam(
learning_rate=0.001,
parameters=trainable_params,
)
这种方式训练快、过拟合风险低,适合作为迁移学习 baseline。
全量微调:让所有参数参与训练
解冻全部参数:
python
for param in model.parameters():
param.stop_gradient = False
优化器:
python
optimizer = paddle.optimizer.Adam(
learning_rate=1e-4,
parameters=model.parameters(),
weight_decay=1e-4,
)
全量微调通常使用更小学习率,例如:
text
1e-4
3e-5
1e-5
原因是预训练权重已经比较好,太大学习率可能破坏已有特征。
分阶段训练:先头部,后全量
推荐流程:
text
阶段 1:冻结 backbone,只训练分类头
阶段 2:解冻全部或部分层,小学习率微调
示例:
python
for name, param in model.named_parameters():
param.stop_gradient = True
for param in model.fc.parameters():
param.stop_gradient = False
optimizer = paddle.optimizer.Adam(learning_rate=0.001, parameters=model.fc.parameters())
训练若干 epoch 后:
python
for param in model.parameters():
param.stop_gradient = False
optimizer = paddle.optimizer.Adam(
learning_rate=1e-4,
parameters=model.parameters(),
weight_decay=1e-4,
)
这种策略通常比直接全量大步训练更稳。
数据预处理:预训练模型更依赖一致输入
使用 ImageNet 预训练模型时,常见输入:
text
[N, 3, 224, 224]
Transform:
python
from paddle.vision import transforms
train_transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.RandomHorizontalFlip(prob=0.5),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225],
),
])
val_transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225],
),
])
如果预处理和预训练模型习惯差异很大,迁移效果可能下降。
完整示例:ResNet18 迁移学习骨架
python
import paddle
import paddle.nn as nn
import paddle.nn.functional as F
from paddle.vision.models import resnet18
def build_model(num_classes, freeze_backbone=True):
model = resnet18(pretrained=True, num_classes=1000)
in_features = model.fc.weight.shape[0]
model.fc = nn.Linear(in_features, num_classes)
if freeze_backbone:
for param in model.parameters():
param.stop_gradient = True
for param in model.fc.parameters():
param.stop_gradient = False
return model
def accuracy(logits, labels):
pred = paddle.argmax(logits, axis=1)
return paddle.mean((pred == labels).astype("float32"))
def train_one_epoch(model, loader, optimizer):
model.train()
total_loss = 0.0
total_acc = 0.0
count = 0
for images, labels in loader:
logits = model(images)
loss = F.cross_entropy(logits, labels)
acc = accuracy(logits, labels)
loss.backward()
optimizer.step()
optimizer.clear_grad()
total_loss += float(loss.numpy())
total_acc += float(acc.numpy())
count += 1
return total_loss / count, total_acc / count
训练分类头:
python
model = build_model(num_classes=5, freeze_backbone=True)
trainable_params = [p for p in model.parameters() if not p.stop_gradient]
optimizer = paddle.optimizer.Adam(
learning_rate=0.001,
parameters=trainable_params,
)
全量微调:
python
for param in model.parameters():
param.stop_gradient = False
optimizer = paddle.optimizer.Adam(
learning_rate=1e-4,
parameters=model.parameters(),
weight_decay=1e-4,
)
PaddleClas 中的迁移学习
如果使用 PaddleClas,迁移学习通常更简单:
yaml
Arch:
name: MobileNetV3_large_x1_0
class_num: 5
pretrained: True
或者通过命令行覆盖:
bash
python tools/train.py \
-c config.yaml \
-o Arch.class_num=5 \
-o Arch.pretrained=True
如果已有训练权重:
bash
python tools/train.py \
-c config.yaml \
-o Global.pretrained_model=./pretrained/best_model
迁移学习时常同时调整:
Arch.class_numArch.pretrainedGlobal.pretrained_modelOptimizer.lr.learning_rate- 数据集路径
- transform 策略
常见错误
错误一:忘记替换分类头
ImageNet 预训练模型通常输出 1000 类。你的数据集如果是 5 类,输出必须改成 5。
错误二:学习率太大
微调预训练模型时,学习率通常要比从头训练小。
错误三:冻结后优化器仍传全部参数
虽然 stop_gradient=True 会阻断梯度,但更清晰的做法是只把可训练参数传给优化器。
错误四:预处理不匹配
预训练模型通常更依赖标准化输入。检查 Resize、ToTensor、Normalize 是否合理。
错误五:类别映射不稳定
训练、验证、推理必须使用同一类别 ID 映射。
迁移学习调参建议
推荐顺序:
- 使用预训练模型。
- 替换分类头。
- 冻结 backbone,只训练分类头。
- 验证集稳定后,解冻全量小学习率微调。
- 加入
weight_decay。 - 根据验证集保存最佳模型。
- 推理前确认类别映射。
如果数据很少,直接全量微调可能过拟合;如果数据较多,只训练分类头可能欠拟合。
建议练习:从分类头微调开始
- 加载
resnet18(pretrained=True)。 - 打印模型结构,找到分类头。
- 替换分类头为自己的类别数。
- 冻结 backbone,只训练分类头。
- 解冻全部参数,用小学习率微调。
- 对比冻结和全量微调的验证准确率。
- 用 PaddleClas 配置完成同样迁移学习流程。
总结
这一篇讲了迁移学习基础:
- 预训练模型能提供通用视觉特征。
- 小数据集优先考虑迁移学习。
- 常见方式包括冻结特征提取器和全量微调。
- 替换分类头是必需步骤。
- 微调通常使用较小学习率。
- 预处理和类别映射必须稳定一致。
- PaddleClas 可以用配置快速完成迁移学习。
如果只能记住一句话,那就是:
迁移学习不是重新训练一个模型,而是在已有通用视觉能力上,让模型适配你的新类别。