模型训练与迁移学习

本文系统讲解视觉模型训练与迁移学习,回答冻结多少层、学习率怎么设、数据增强用哪些、混合精度怎么开等实战问题。覆盖损失函数、SGD 与 AdamW、余弦退火与 warmup、RandAugment 与 MixUp、特征提取与逐层解冻、AMP 与梯度累积、过拟合诊断与正则,并给出微调 ResNet18 与分层学习率两段可运行 PyTorch 代码,附策略对比表、权衡与常见坑。

引言

训练一个视觉模型的难点不在写训练循环,而在「让模型在有限数据上稳定收敛并不再过拟合」。迁移学习是解决这个问题的核心手段:复用大规模预训练权重,再用少量目标域数据微调。但「冻结多少层」「学习率设多大」「增强开多强」这些决策没有标准答案,取决于数据量与域差距。

本文按「训练循环 → 损失 → 优化器 → 增强 → 迁移策略 → 训练工程 → 正则」的顺序拆解,并给出可直接运行的微调代码。

目录

  1. 训练循环的基本组成
  2. 损失函数
  3. 优化器与学习率调度
  4. 数据增强
  5. 迁移学习策略
  6. 训练工程
  7. 过拟合诊断与正则
  8. 代码:微调 ResNet18
  9. 代码:分层学习率与调度
  10. 实验管理与复现
  11. 学习率查找与超参搜索
  12. 从训练到部署的一致性
  13. 策略对比表
  14. 权衡取舍
  15. 常见坑清单
  16. 小结

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 省显存 → 靠早停与最优权重保存收尾。记住三个要点:数据量决定策略、学习率决定成败、验证集决定何时停。当数据量足够大时,模型与训练配方的收益才逐渐超过数据的收益。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「计算机视觉」更多文章

  1. 检测与分割的评估指标
  2. 多模态视觉语言模型
  3. 3D 视觉:点云与深度估计