Skip to content

训练配方与调参

本页速览 深度学习训练是一场按配方操作又需要临场判断的烹饪。本文给出数据、初始化、损失、优化器、学习率、正则化、归一化的默认配方表,讲透学习率范围测试、batch size 联动、warmup 与余弦退火、迁移学习与显存管理,最后是复现实验的纪律。

训练配方与调参

一句话定义:训练配方(training recipe)是把一组经过验证的默认选择——数据、初始化、损失、优化器、学习率、正则化——组合成一套能稳定收敛的实验起点;调参则是在这个起点上,用可测量的信号(损失、验证指标)做单变量搜索。

调参不是玄学,而是有默认值、有诊断信号、有搜索方法的工程。理论根基(梯度下降、损失函数、正则化)见优化与梯度下降过拟合与正则化;训练中遇到"loss 不降"这类症状时的排查步骤见调试与诊断

一、默认配方表

不知道用什么时,先照抄这张表,它覆盖了大多数视觉/文本任务的合理起点:

环节默认选择说明
数据归一化均值/标准差标准化(用训练集统计量)输入接近 0 均值、单位方差
数据增强视觉:随机裁剪+翻转;文本:随机 mask/裁剪增强强度随数据量增大而提高
权重初始化PyTorch 默认(Kaiming 初始化 + 正确 fan 模式)手工初始化是大多数新手的错误来源
损失函数分类 CrossEntropyLoss;回归 MSELoss/HuberLoss输出层要匹配,见损失函数与输出层
优化器首选 Adam(lr=1e-3);CNN 可换 SGD+momentum(lr=1e-2weight_decayAdam 对 lr 不敏感、上手稳
学习率3e-4(Adam)~1e-2(SGD),再用范围测试精调见下一节
批大小32–256,尽量取大(2 的幂)与学习率联动,见"batch size 与学习率"
归一化层BatchNorm(CV);LayerNorm(NLP/Transformer)初始化与归一化
正则化weight_decay(Adam 配 1e-4~5e-4);Dropout 按需数据量小/模型大时加重
训练轮数小数据 10–50;大数据 50–200,配早停以验证集早停为最终判据
学习率调度warmup + 余弦退火(或按验证集折半)见"训练周期规划"
随机种子torch.manual_seed + np.random.seed + 环境变量见"复现实验纪律"

一句话原则

先让训练"正常过拟合",再谈正则化。如果一个模型连训练集都拟合不了(训练 loss 不下降),那问题在模型或数据,不在正则化;此时加 weight_decay 只会雪上加霜。

二、学习率怎么定:范围测试法

学习率是最重要的单超参数。过小收敛慢,过大直接发散。与其靠猜,不如做学习率范围测试(learning rate range test):让学习率在一个 epoch 内从极小值线性(或指数)升到较大值,画出"学习率 → 损失"曲线:

python
import torch, torch.nn as nn
from torch.utils.data import DataLoader

def lr_find(model, loader, criterion, optimizer_cls,
            lr_min=1e-6, lr_max=1e0, steps=200):
    """在 steps 个 batch 内线性扫描学习率,返回 (lrs, losses)"""
    optimizer = optimizer_cls(model.parameters(), lr=lr_min)
    scheduler = torch.optim.lr_scheduler.LambdaLR(
        optimizer, lambda t: lr_min * (lr_max / lr_min) ** (t / steps))
    lrs, losses = [], []
    for i, (x, y) in enumerate(loader):
        if i >= steps:
            break
        optimizer.zero_grad()
        loss = criterion(model(x), y)
        loss.backward()
        optimizer.step()
        scheduler.step()
        lrs.append(optimizer.param_groups[0]["lr"])
        losses.append(loss.item())
    return lrs, losses

画出来后看曲线:损失刚开始下降的区域,取学习率为曲线最陡下降段的 1/10 左右,通常是最佳起点。比如损失从 1e-4 开始明显下降、到 1e-2 时开始反弹,那就取 3e-3 附近。

这条曲线的形态本身也是诊断信息:如果没有任何学习率能让 loss 下降,问题大概率不在学习率,而在数据或模型(排查步骤见调试与诊断)。PyTorch 也提供 torch.optim.lr_scheduler 内置方案,另可参考 Leslie Smith 的原始论文(文末资料)。

三、batch size 与学习率联动

批大小不是孤立的超参数,它与学习率、梯度质量耦合:

  • 大 batch 的梯度更准(方差小),可以、也应该用更大的学习率。经验法则:batch size 翻倍,学习率大致乘 √2(线性缩放法则的平方根版本)
  • 小 batch 的梯度噪声大,起隐式正则化作用,但收敛不稳,通常配小学习率。
  • 大 batch 的最终泛化有时略差于小 batch("large-batch generalization gap"),但前提是你把学习率和调度也按比例调好——很多时候差距来自学习率没调,而不是 batch 本身。
python
# 从 32 提到 64 的配套改动示例
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3 * (64 / 32) ** 0.5)

显存不够不是小 batch 的借口

batch size 受显存限制时,优先用梯度累积(见"显存管理")模拟大 batch,而不是直接改小 batch 并保持原学习率——后者会悄悄改变训练动力学。

四、训练周期规划:warmup + 余弦退火

现代训练通常不是固定学习率跑到底,而是三段式:

  1. warmup(几百到几千步):学习率从很小的值线性升到目标值。原因:训练初期梯度统计量还没稳定(尤其有 BatchNorm 时),直接上大学习率容易让优化器(Adam 的动量/方差估计)被早期样本带偏。
  2. 主体阶段:保持或缓慢下降。
  3. 余弦退火/线性衰减:把学习率平滑降到接近 0,让参数在损失面最小值附近"精修"。
python
import math
from torch.optim.lr_scheduler import LambdaLR

def cosine_with_warmup(optimizer, total_steps, warmup_steps=1000):
    def lr_lambda(step):
        if step < warmup_steps:
            return step / warmup_steps                     # 线性升温
        progress = (step - warmup_steps) / max(1, total_steps - warmup_steps)
        return 0.5 * (1 + math.cos(math.pi * progress))    # 余弦退火到 0
    return LambdaLR(optimizer, lr_lambda)

scheduler = cosine_with_warmup(optimizer, total_steps=len(train_loader) * 30)

torch.optim.lr_scheduler 还有更省心的 CosineAnnealingWarmRestarts 与直接看验证集的 ReduceLROnPlateau。学习率调度的选择依赖损失面形状,属于经验领域,但**"warmup + 衰减"几乎总是优于恒定的学习率**。

五、正则化组合强度

正则化是"按需加权",不是"越多越好"。组合拳的一般逻辑:

信号正则化手段强度建议
训练 loss 低、验证 loss 高(过拟合)weight_decay、Dropout、增强、早停从最便宜的 weight_decay 开始逐个加
训练 loss 也下不去(欠拟合)关掉正则化,先检查模型容量/学习率别加正则化
数据很小(<10k)全部常规正则化 + 早停 + 迁移学习强烈建议迁移学习
训练集很大(>1M)正则化作用有限,重点放在调度与数据质量轻量 weight_decay 即可

"最便宜的"顺序:weight_decay 基本零成本;Dropout 只在训练时激活;增强有读数据开销;早停是免费但只防过拟合后半程。每加一项都要量化它的净收益——比如用"有/无"对照看验证集,而不是凭感觉叠加(实验纪律见DL 设计原则)。

六、迁移学习配方

把预训练模型搬到新任务上,有一个成熟的两阶段配方(理论见表征学习与预训练,实例见渐进式教程:三版跑起来):

  1. 阶段一:冻结主干,只训练新分类头。学习率取 1e-3(Adam),跑几个 epoch 让分类头先对齐特征。
  2. 阶段二:解冻全部(或后半部分)参数,小学习率微调。学习率取 1e-4~1e-5,比阶段一小一个量级——预训练权重已很优秀,大学习率会把学好的表征"冲毁"。
  3. 归一化统计量必须与预训练一致(ImageNet 的 (0.485, 0.456, 0.406)),否则特征分布错位。
  4. 分辨率:把输入调整到预训练模型的期望分辨率(如 224×224),或选择专为小图设计的预训练模型。
  5. 监控微调幅度:计算每层权重的相对变化,如果某层变化超过 10% 量级,说明学习率太大。

一个常见的失误是"全量微调 + 从头训练的学习率"——这是常见陷阱与反模式里微调翻车的第一名。

七、显存管理三件套

模型放不下显存时,按性价比依次使用:

1. 梯度累积(gradient accumulation)

模拟更大的 batch,不改变梯度统计性质:

python
accumulation_steps = 4          # 相当于把 batch_size 放大 4 倍
optimizer.zero_grad()
for i, (x, y) in enumerate(loader):
    loss = criterion(model(x), y) / accumulation_steps   # 平均而不是求和
    loss.backward()
    if (i + 1) % accumulation_steps == 0:
        optimizer.step()
        optimizer.zero_grad()

注意:loss 要除以 accumulation_steps 做平均,否则等效学习率被放大了 accumulation_steps 倍;BatchNorm 的统计量仍按单 batch 计算,所以大 batch 下的 BN 行为无法完全等价。

2. 混合精度(AMP)

用 PyTorch 内置 torch.amp,一张卡通常能省 30–50% 显存并提速,且多数任务精度几乎不掉:

python
from torch.amp import GradScaler, autocast

scaler = GradScaler("cuda")
for x, y in loader:
    x, y = x.to(device), y.to(device)
    optimizer.zero_grad()
    with autocast("cuda"):
        loss = criterion(model(x), y)
    scaler.scale(loss).backward()
    scaler.step(optimizer)
    scaler.update()

混合精度的坑(精度溢出、梯度变成 NaN、某些算子不支持半精度)见常见陷阱与反模式。

3. 梯度检查点(gradient checkpointing)

torch.utils.checkpoint 以"反向传播时重算前向"换显存,空间换时间:

python
from torch.utils.checkpoint import checkpoint

# 对每个子模块的 forward 启用
def forward(self, x):
    x = checkpoint(self.layer1, x)   # 前向不保存中间激活,反向时重算
    x = checkpoint(self.layer2, x)
    return x

典型用途是 7B 级大模型微调,配合量化/LoRA 使用;10 层以下的小模型收益不大。

优先级顺序:混合精度(几乎零成本)→ 梯度累积(不改模型)→ 梯度检查点(改模型、变慢)→ 模型并行/量化(重构)。显存问题的系统性解法还涉及 torch.utils.benchmark 与内存分析器。

八、复现实验纪律

"昨天还好好的,今天跑不出来了"几乎都是环境或随机性在作怪。复现纪律是实验的底线:

  1. 固定种子:在导入后立刻设置,覆盖 torch / numpy / random:
python
import random, numpy as np, torch
def set_seed(seed):
    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   # 确定性优先,略牺牲性能
  1. 固定依赖pip freeze > requirements.txt(或 pyproject.toml 锁版本),注明 Python/CUDA/驱动版本。
  2. 固定环境:使用 Docker 或 conda 环境,写明镜像 tag。
  3. 记录一切:每个实验记录 超参、数据版本、代码 commit、随机种子、单次运行结果。建议用配置化实验(见 DL 设计原则),把超参写进 config.yaml 而不是散在代码里。
  4. 两次"黄金复现":代码上线/论文提交前,用同一种子从零跑两次,误差应小于随机种子本身的波动;波动大小本身就是评估指标的误差上界,应写进评估实践的报告里。

延伸阅读

参考资料