引言
训练一个视觉模型的难点不在写训练循环,而在「让模型在有限数据上稳定收敛并不再过拟合」。迁移学习是解决这个问题的核心手段:复用大规模预训练权重,再用少量目标域数据微调。但「冻结多少层」「学习率设多大」「增强开多强」这些决策没有标准答案,取决于数据量与域差距。
本文按「训练循环 → 损失 → 优化器 → 增强 → 迁移策略 → 训练工程 → 正则」的顺序拆解,并给出可直接运行的微调代码。
目录
- 训练循环的基本组成
- 损失函数
- 优化器与学习率调度
- 数据增强
- 迁移学习策略
- 训练工程
- 过拟合诊断与正则
- 代码:微调 ResNet18
- 代码:分层学习率与调度
- 实验管理与复现
- 学习率查找与超参搜索
- 从训练到部署的一致性
- 策略对比表
- 权衡取舍
- 常见坑清单
- 小结
1. 训练循环的基本组成
一个训练循环包含:数据加载、前向、损失、反向、参数更新、验证。每个环节都有容易出错的细节:
- 数据加载:用 DataLoader 多进程加载,注意
num_workers与pin_memory。 - 训练模式:训练时
model.train(),验证与推理时model.eval(),影响 BN 与 Dropout。 - 梯度清零:每个 step 前
optimizer.zero_grad(),忘记会累加梯度。 - 验证时机:每个 epoch 后在验证集评估,记录指标用于早停。
2. 损失函数
| 损失 | 适用 | 说明 |
|---|---|---|
| CrossEntropy | 单标签多分类 | 最常用 |
| BCEWithLogits | 多标签 | 每类独立 sigmoid |
| Focal Loss | 类别极度不平衡 | 降低易分样本权重 |
| Label Smoothing | 防过拟合 | 软标签,抑制过度自信 |
| Dice / IoU | 分割 | 处理前景背景不平衡 |
分类任务默认用交叉熵;类别不平衡时加权重或换 Focal Loss;需要抑制过拟合时加标签平滑。
3. 优化器与学习率调度
- SGD 加动量:CV 分类任务的传统首选,泛化好,但需要精调学习率与 warmup。
- AdamW:自适应学习率加解耦权重衰减,收敛快、对学习率不敏感,是 ViT 与微调的默认选择。
- 权重衰减:L2 正则,抑制过拟合,AdamW 里与梯度更新解耦。
调度策略:
- StepLR:每隔若干 epoch 乘一个衰减因子,简单。
- CosineAnnealing:余弦退火,平滑下降,常配合 warmup。
- OneCycle:先升后降,收敛快。
- Warmup:前若干步线性升温,避免早期大梯度破坏预训练权重。
4. 数据增强
增强是「免费的数据」,但强度要匹配数据量:
- 基础:随机裁剪、水平翻转、颜色抖动。
- 进阶:RandAugment(随机选若干增强)、RandomErasing(随机遮挡)。
- 混样本:MixUp(两图线性混合)、CutMix(裁剪粘贴)。
- 注意:验证与测试只做尺寸与归一化,不做随机增强。
5. 迁移学习策略
按数据量从少到多,策略逐步放开:
| 数据量 | 策略 | 说明 |
|---|---|---|
| 极少(小于 1k) | 固定骨干当特征提取器 | 只训分类头 |
| 少(1k 到 10k) | 冻结浅层,微调深层 | 分层学习率 |
| 中(10k 到 100k) | 全量微调 | 小学习率 |
| 多(大于 100k) | 全量微调或从头训 | 增强可减弱 |
域差距也影响策略:目标域与预训练域越远,越需要微调更多层。
6. 训练工程
- 混合精度(AMP):用 float16 前向、float32 主权重,显存减半、速度提升,配 GradScaler 防梯度下溢。
- 梯度累积:显存不够时,累积若干 step 再更新,模拟大 batch。
- EMA:维护参数的指数滑动平均,提升稳定性与最终精度。
- 梯度裁剪:防止梯度爆炸,尤其 RNN 与深层网络。
- 早停:验证指标不再提升时停止,省时间防过拟合。
from torch.amp import autocast, GradScaler
scaler = GradScaler("cuda")
for x, y in loader:
optimizer.zero_grad(set_to_none=True)
with autocast("cuda", dtype=torch.float16):
loss = criterion(model(x), y)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
这段是 AMP 的标准写法,注意 zero_grad(set_to_none=True) 更省内存。
7. 过拟合诊断与正则
诊断信号:
- 训练损失持续下降,验证损失上升:过拟合。
- 训练与验证都高:欠拟合,模型太弱或学习率不对。
- 训练震荡:学习率过大或 batch 太小。
正则手段:权重衰减、Dropout、标签平滑、早停、增强、减小模型容量。数据量是根本,正则只能缓解。
8. 代码:微调 ResNet18
下面是一个完整的微调示例,包含数据加载、模型改造、训练循环与验证。
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms, models
device = "cuda" if torch.cuda.is_available() else "cpu"
train_tf = transforms.Compose([
transforms.RandomResizedCrop(224, scale=(0.7, 1.0)),
transforms.RandomHorizontalFlip(),
transforms.ColorJitter(0.2, 0.2, 0.2),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])
val_tf = transforms.Compose([
transforms.Resize(256), transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])
train_ds = datasets.ImageFolder("data/train", train_tf)
val_ds = datasets.ImageFolder("data/val", val_tf)
train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4, pin_memory=True)
val_loader = DataLoader(val_ds, batch_size=64, num_workers=4)
model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1)
model.fc = nn.Linear(model.fc.in_features, len(train_ds.classes))
model = model.to(device)
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.05)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=20)
for epoch in range(20):
model.train()
for x, y in train_loader:
x, y = x.to(device), y.to(device)
optimizer.zero_grad(set_to_none=True)
loss = criterion(model(x), y)
loss.backward()
optimizer.step()
scheduler.step()
model.eval()
correct = total = 0
with torch.inference_mode():
for x, y in val_loader:
x, y = x.to(device), y.to(device)
pred = model(x).argmax(dim=1)
correct += int((pred == y).sum())
total += y.numel()
print(f"epoch {epoch}: val_acc={correct / total:.4f}")
这段覆盖了迁移学习的标准流程:替换分类头、余弦退火、标签平滑、AdamW、验证集评估。
9. 代码:分层学习率与调度
数据量小时,冻结浅层并对深层用更大学习率往往更稳。
import torch
from torchvision import models
model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2)
# 冻结骨干的前三个 stage,只训 stage4 与分类头
for name, param in model.named_parameters():
if name.startswith(("conv1", "bn1", "layer1", "layer2")):
param.requires_grad = False
head_params = list(model.fc.parameters())
backbone_params = [p for n, p in model.named_parameters()
if p.requires_grad and not n.startswith("fc")]
optimizer = torch.optim.SGD([
{"params": backbone_params, "lr": 1e-4}, # 深层用小学习率
{"params": head_params, "lr": 1e-3}, # 分类头用大学习率
], momentum=0.9, weight_decay=1e-4)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30)
print("trainable tensors:", sum(1 for p in model.parameters() if p.requires_grad))
分层学习率的直觉是:浅层特征通用,改动越小越好;深层与分类头需要适应新任务,学习率可以更大。
10. 实验管理与复现
训练实验必须可复现,否则调参就是碰运气。
- 固定随机种子:Python、NumPy、PyTorch、CUDA 都要设,尽量开启确定性算法。
- 记录超参与环境:学习率、批大小、增强强度、数据版本、代码 commit、依赖版本。
- 实验追踪工具:TensorBoard 看曲线,Weights & Biases 与 MLflow 管理实验对比。
- 权重与配置一起存:只存权重不存配置,几个月后无法复现。
import random
import numpy as np
import torch
def set_seed(seed=42):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
set_seed(42)
print("seed set")
开启确定性会略微降低速度,但换来可复现。生产训练常在「快速但不确定」与「慢但可复现」之间取舍,通常先快速探索、最终确认时开确定性。
11. 学习率查找与超参搜索
学习率是最重要的超参。与其盲猜,不如用学习率查找器先扫一遍。
import torch
def lr_finder(model, loader, criterion, optimizer, device, start=1e-6, end=1.0, steps=100):
lrs, losses = [], []
mult = (end / start) ** (1 / steps)
lr = start
for i, (x, y) in enumerate(loader):
if i >= steps:
break
for g in optimizer.param_groups:
g["lr"] = lr
x, y = x.to(device), y.to(device)
optimizer.zero_grad(set_to_none=True)
loss = criterion(model(x), y)
loss.backward()
optimizer.step()
lrs.append(lr)
losses.append(float(loss))
lr *= mult
# 取损失下降最快处的学习率作为上限参考
return lrs, losses
# lrs, losses = lr_finder(model, train_loader, criterion, optimizer, device)
print("lr finder ready")
经验做法:取损失下降最陡处的学习率除以 10 作为初始学习率。超参搜索上,网格搜索昂贵,随机搜索与贝叶斯优化更高效,先搜学习率与权重衰减这两个最敏感的。
12. 从训练到部署的一致性
训练与部署的一致性是掉点的常见来源:
- 预处理对齐:训练用的 resize 方式、归一化参数、通道顺序,部署必须一致。
- 导出验证:导出 ONNX 或 TensorRT 后,用同一批样本比对输出,确认误差在阈值内。
- 推理模式:导出前
model.eval(),并torch.no_grad()或inference_mode导出。 - 动态轴:批大小或分辨率可变时,导出时声明动态轴。
- 后处理对齐:分类的阈值、检测的 NMS 参数,训练评估与线上必须一致。
import torch
from torchvision import models
model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1).eval()
dummy = torch.randn(1, 3, 224, 224)
with torch.inference_mode():
torch.onnx.export(
model, dummy, "resnet18.onnx",
input_names=["input"], output_names=["logits"],
dynamic_axes={"input": {0: "batch"}, "logits": {0: "batch"}},
opset_version=12,
)
print("exported")
导出后务必用 ONNX Runtime 跑一遍,与 PyTorch 输出比对最大绝对误差,通常应小于 1e-4。
13. 策略对比表
| 策略 | 数据需求 | 训练成本 | 效果 | 适用 |
|---|---|---|---|---|
| 只训分类头 | 极少 | 最低 | 基线 | 快速验证 |
| 冻结浅层微调深层 | 少 | 低 | 好 | 小数据集 |
| 全量微调 | 中 | 中 | 最好 | 常规场景 |
| 从头训练 | 多 | 高 | 需大量数据 | 大规模或域差异极大 |
14. 权衡取舍
- 冻结层数与数据量:数据越少冻结越多,数据越多放开的层越多。
- 学习率与收敛:太大破坏预训练权重,太小收敛慢,用 warmup 与分层缓解。
- 增强强度与数据量:数据少时强增强有益,数据多时过强增强反而拖慢收敛。
- AMP 与数值稳定:加速明显,但个别算子对 float16 敏感,需回退 float32。
- 训练时间与精度:更长训练与更大分辨率提升精度,但要权衡成本。
15. 常见坑清单
- 忘记
model.eval():验证时 BN 与 Dropout 仍在训练模式,指标失真。 - 冻结骨干但 BN 仍更新:冻结时应同时把 BN 设为 eval,否则统计量被污染。
- 学习率过大:直接摧毁预训练特征,损失剧烈震荡。
- 验证集做了增强:随机增强让验证指标不可比,只做确定性变换。
- 类别不平衡未处理:少数类几乎学不会,需加权或重采样。
- 归一化参数不一致:预处理与预训练不匹配,精度悄悄下降。
- 忘记梯度清零:梯度累加导致训练发散。
- 数据泄漏:验证集混入训练样本,指标虚高。
- 只训最后一个 epoch:未用早停与最优权重保存,可能拿到过拟合的模型。
- 忽略随机种子:实验无法复现,团队协作困难。
16. 小结
训练与迁移学习的主线是:用预训练权重起步 → 按数据量决定冻结层数 → 用分层学习率与余弦退火 → 配适度增强与标签平滑 → 用 AMP 省显存 → 靠早停与最优权重保存收尾。记住三个要点:数据量决定策略、学习率决定成败、验证集决定何时停。当数据量足够大时,模型与训练配方的收益才逐渐超过数据的收益。
延伸阅读
- 卷积网络与经典骨干架构 — 骨干选型与迁移起点
- 数据标注与数据集工程 — 数据划分与防泄漏
- 模型压缩与端侧部署 — 训练后的压缩与部署
- 分布式训练与并行策略 — 大规模训练的并行手段
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。