专栏 AI 与算法

Day 20|训练技巧:优化器、学习率、正则化、混合精度,从"会训"到"训好"(AI 学习笔记 · 深度学习周 · 第 20 篇)

代码写完只是开始,真正决定模型能不能训好的,是这些工程 trick。LLaMA-7B 训练失败的常见原因 80% 不在模型架构,而在优化器选错、学习率没调、正则化没加、或混合精度没开。本节把 6 大类训练技巧(优化器 / 学习率 / 权重衰减 / Dropout / 混合精度 / 梯度累积)一次讲透,加 ResNet-50 + CIFAR-10 完整训练循环,把”训练 95% 的时间”花在刀刃上。


1. 优化器:SGD / Momentum / Adam / AdamW

1.1 朴素 SGD 的问题

随机梯度下降(SGD)的更新规则:

θ_{t+1} = θ_t - lr · ∇L(θ_t)

问题:每个参数用同一个学习率,但损失 landscape 各方向曲率差异巨大——损失函数对某些参数极其敏感(学习率必须小),对另一些极不敏感(学习率可以大)。一个固定 lr 在所有方向上要么太慢要么震荡。

1.2 Momentum:加惯性

v_{t+1} = β · v_t + ∇L(θ_t)
θ_{t+1} = θ_t - lr · v_{t+1}

直觉:小球滚下坡会累积动量,冲过局部小坑继续往下滚。β 通常 0.9-0.99。

PyTorch:

optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=1e-4)

1.3 Adam:自适应学习率

Adam(Adaptive Moment Estimation)给每个参数单独算学习率——梯度大(频繁更新)的学习率小,梯度小(稀疏更新)的学习率大。

m_t = β_1 · m_{t-1} + (1-β_1) · ∇L(θ_t)      # 一阶矩(均值)
v_t = β_2 · v_{t-1} + (1-β_2) · ∇L(θ_t)^2    # 二阶矩(方差)
m̂_t = m_t / (1-β_1^t)
v̂_t = v_t / (1-β_2^t)
θ_{t+1} = θ_t - lr · m̂_t / (√v̂_t + ε)

默认 β_1=0.9, β_2=0.999, ε=1e-8。

优点:前期收敛快,对学习率不敏感(默认值 lr=1e-3 就能跑)。缺点:训练末期泛化性能可能不如 SGD+Momentum(ResNet 论文里反复提到)。

1.4 AdamW:把权重衰减从梯度里拆出来

Adam 的权重衰减(weight decay)实际上等价于 L2 正则,但因为 Adam 的自适应分母 √v̂_t + ε,L2 项被「自适应缩放」了,效果与真正的 weight decay 不一样。

Loshchilov & Hutter(2019)的 AdamW 修法:把 weight decay 从梯度里拿出来,直接作用在权重上:

θ_{t+1} = θ_t - lr · m̂_t / (√v̂_t + ε) - lr · λ · θ_t

PyTorch:

optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.1)

事实:AdamW 是当前训练 Transformer 的事实标准——LLaMA、GPT、BERT、Qwen 全用 AdamW。

1.5 五大优化器对比表

优化器 公式核心 优点 缺点 适用场景
SGD θ - lr·∇L 简单 / 泛化好 慢 / 需精调 lr ResNet 等视觉模型
SGD+Momentum + 动量 突破局部最优 仍需精调 lr ResNet, 大 batch 训练
RMSProp 平方梯度 EMA RNN 友好 已被 Adam 取代 历史意义
Adam 一阶 + 二阶矩 收敛快 / lr 不敏感 末期泛化差 早期 Transformer
AdamW Adam + 独立 wd 收敛快 + 正则干净 比 Adam 略慢 现代 LLM 标配

2. 学习率调度:Warmup + Cosine Annealing

2.1 为什么不能直接用大学习率

训练 Transformer 用大学习率(比如 1e-3)几乎必崩——Loss 在前几百步飙升,再降不下来。原因:

  1. Adam 的 m̂ / √v̂ 统计量在初始几步还没稳定,被放大后导致更新过大
  2. 权重随机初始化,远离最优,大步更新震荡

2.2 Warmup(预热)

lr(t) = lr_max · min(1, t / T_warmup)        # 线性 warmup

前 T_warmup 步(总步数的 1-10%)学习率从 0 线性升到峰值。LLaMA-7B 训练 1T token 用 2000 步 warmup(占总步数 0.1%),LLaMA-2 进一步用 0.05%。

直觉:让 Adam 的 m̂ / √v̂ 先稳定下来,再加大步子。

2.3 Cosine Annealing(余弦退火)

峰值之后,学习率按余弦曲线平滑衰减:

lr(t) = lr_min + 0.5 · (lr_max - lr_min) · (1 + cos(π · t / T_decay))

比 step decay(每隔 N 步除以 2)更平滑,避免硬拐点导致 loss 抖动。

2.4 完整 Warmup + Cosine 调度

from torch.optim.lr_scheduler import LambdaLR
import math

def warmup_cosine(step):
    if step < warmup_steps:
        return step / warmup_steps
    progress = (step - warmup_steps) / (total_steps - warmup_steps)
    return 0.5 * (1 + math.cos(math.pi * progress))

scheduler = LambdaLR(optimizer, lr_lambda=warmup_cosine)

PyTorch 1.12+ 也有 CosineAnnealingLR 加 warmup 的 SequentialLR 写法。

2.5 例子:LLaMA-7B 训练

  • 1T token, batch=4M token,总步数 = 1T / 4M = 250K 步
  • lr_max = 3e-4
  • warmup = 2000 步(0.8%)
  • cosine decay 到 lr_min = 3e-5(峰值的 10%)
  • weight_decay = 0.1
  • 训练 21 天在 2048× A100 上

3. 权重衰减(Weight Decay)

3.1 定义

权重衰减 = 在损失里加 L2 正则项:

L_total = L + λ · ||w||^2

求导等价于每步权重乘一个小于 1 的系数:

w ← w · (1 - lr · λ)

作用:把权重「拖」向 0,防止单个权重过大,是最便宜也最有效的过拟合防御。

3.2 关键细节:不是所有参数都该衰减

权重衰减应该只对 weight 应用,不对 bias 和 LayerNorm 的 gamma/beta 应用——这些参数控制模型的「位置/尺度」,衰减了反而压制模型能力。

# 标准做法:分两组
no_decay = ['bias', 'LayerNorm.weight', 'bn.weight', 'ln.weight']
params_no_decay = [p for n, p in model.named_parameters() if any(k in n for k in no_decay)]
params_decay = [p for n, p in model.named_parameters() if not any(k in n for k in no_decay)]

optimizer = torch.optim.AdamW([
    {'params': params_decay, 'weight_decay': 0.1},
    {'params': params_no_decay, 'weight_decay': 0.0},
], lr=1e-3)

3.3 Transformer vs CNN 的 weight decay 差异

模型 默认 weight_decay 理由
Transformer (BERT/GPT/LLaMA) 0.1 层多、参数多、易过拟合
CNN (ResNet/EfficientNet) 1e-4 卷积天然有空间归纳偏置
RNN/LSTM 1e-5 ~ 1e-4 梯度已易爆,正则要弱

差了 1000 倍——用 CNN 的 wd 训练 Transformer 等于没正则。


4. Dropout / Label Smoothing / Early Stopping

三大「传统但有效」的正则化招。

4.1 Dropout

训练时随机屏蔽一部分神经元(置 0):

self.dropout = nn.Dropout(p=0.1)  # 屏蔽 10%

推理时关闭:

model.eval()
# Dropout 不再屏蔽,所有神经元参与计算

典型值:

  • Transformer 全连接层:p=0.1
  • Transformer attention:p=0.1(attention dropout)
  • CNN:p=0.5(全连接层) / p=0.2(卷积层)
  • RNN/LSTM:p=0.3-0.5(循环层,需更激进)

4.2 Label Smoothing

把 one-hot 标签「软化」成概率分布,阻止模型把 softmax 推到极端。

# 原始 one-hot:[0, 0, 1, 0, 0] (5 类,真实类是 2)
# Label smoothing(ε=0.1):[0.02, 0.02, 0.92, 0.02, 0.02]

实现:

class LabelSmoothingLoss(nn.Module):
    def __init__(self, classes, smoothing=0.1):
        super().__init__()
        self.confidence = 1.0 - smoothing
        self.smoothing = smoothing
        self.cls = classes

    def forward(self, pred, target):
        pred = pred.log_softmax(dim=-1)
        with torch.no_grad():
            true_dist = torch.zeros_like(pred)
            true_dist.fill_(self.smoothing / (self.cls - 1))
            true_dist.scatter_(1, target.unsqueeze(1), self.confidence)
        return torch.mean(torch.sum(-true_dist * pred, dim=-1))

事实:Transformer 训练几乎默认 label smoothing 0.1(GPT-3、BERT、ViT 都用)。

4.3 Early Stopping

监控验证集 loss,N 轮不下降就停。

best_val_loss = float('inf')
patience = 5
counter = 0

for epoch in range(100):
    train(...)
    val_loss = validate(...)
    if val_loss < best_val_loss:
        best_val_loss = val_loss
        torch.save(model.state_dict(), 'best.pt')
        counter = 0
    else:
        counter += 1
        if counter >= patience:
            print(f'Early stop at epoch {epoch}')
            break

关键:小数据集必备,大数据集可以省(LLaMA 训练 21 天,从不停,靠 cosine 衰减到 0)。


5. 混合精度训练:FP16 / BF16 / FP8

5.1 为什么需要混合精度

FP32(4 字节)训练 7B 模型:

  • 模型参数:28 GB
  • Adam 状态(m, v):56 GB
  • 梯度:28 GB
  • 总:112 GB——单卡根本装不下

FP16/BF16(2 字节)直接砍一半:参数 14 GB,Adam 状态 28 GB,梯度 14 GB = 56 GB——单 80GB A100 装得下。

5.2 FP16 vs BF16 vs FP8 数字表示

格式 总位数 指数位 尾数位 范围 精度
FP32 32 8 23 ±3.4e38 高
FP16 16 5 10 ±65504 低(范围窄)
BF16 16 8 7 ±3.4e38(同 FP32) 低(尾数少)
FP8 (E4M3) 8 4 3 ±448 极低
FP8 (E5M2) 8 5 2 ±57344 极低

关键差异:

  • FP16:范围窄,小梯度(< 6e-5)被舍入到 0,必须配合 GradScaler
  • BF16:范围和 FP32 一样,小梯度不会消失,无需 Scaler,Ampere 之后 GPU 默认
  • FP8:Hopper H100 引入,进一步砍一半,但精度损失大,需特殊处理

5.3 实战:BF16 + GradScaler

PyTorch 的 torch.cuda.amp 自动处理:

scaler = torch.cuda.amp.GradScaler()  # 只 FP16 需要,BF16 可省

for batch in dataloader:
    optimizer.zero_grad()

    # autocast:自动选 FP16 / BF16
    with torch.cuda.amp.autocast(dtype=torch.bfloat16):
        outputs = model(batch.input_ids)
        loss = criterion(outputs, batch.labels)

    # BF16 不需要 scaler,但代码兼容
    if use_fp16:
        scaler.scale(loss).backward()
        scaler.step(optimizer)
        scaler.update()
    else:
        loss.backward()
        optimizer.step()

5.4 速度对比

精度 显存占用 训练速度 精度损失
FP32 100% 1.0x 0%
FP16 + GradScaler 50% 1.5-2.0x <0.1%
BF16 50% 1.5-2.0x <0.1%
FP8 25% 2.5-3.5x 0.5-1%

BF16 是 Ampere 之后的事实标准——LLaMA-3 / Qwen-2.5 / DeepSeek-V3 全部默认 BF16。


6. 梯度累积 + 梯度裁剪

LLM 训练 batch 动辄上百万 token,显存装不下。梯度累积 把大 batch 拆成 N 个 micro-batch:

accumulation_steps = 16  # 等效 batch = micro_batch × 16

optimizer.zero_grad()
for i, batch in enumerate(dataloader):
    with torch.cuda.amp.autocast(dtype=torch.bfloat16):
        loss = model(batch)
    loss = loss / accumulation_steps  # 关键:除以累积步数
    loss.backward()

    if (i + 1) % accumulation_steps == 0:
        optimizer.step()
        optimizer.zero_grad()

梯度裁剪 把梯度范数限制在阈值内,防止 loss spike 时梯度爆炸:

torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

LLaMA-7B 训练标配:

  • micro_batch=8 × accumulation=16 = 等效 batch=128
  • grad_clip=1.0
  • 8×A100(80GB)训练 1T token 21 天

7. PyTorch 实战:ResNet-50 + CIFAR-10 完整训练循环

import torch
import torch.nn as nn
import torchvision
from torch.cuda.amp import autocast, GradScaler

# ===== 1. 数据 =====
transform = torchvision.transforms.Compose([
    torchvision.transforms.RandomCrop(32, padding=4),
    torchvision.transforms.RandomHorizontalFlip(),
    torchvision.transforms.ToTensor(),
    torchvision.transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)),
])
train_set = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_set, batch_size=128, shuffle=True, num_workers=4, pin_memory=True)

# ===== 2. 模型 =====
device = 'cuda'
model = torchvision.models.resnet50(weights=None, num_classes=10).to(device)

# ===== 3. 参数分组(weight decay 只对 conv/linear) =====
decay, no_decay = [], []
for n, p in model.named_parameters():
    if not p.requires_grad: continue
    if p.ndim <= 1 or n.endswith('.bias'):
        no_decay.append(p)
    else:
        decay.append(p]

optimizer = torch.optim.AdamW([
    {'params': decay, 'weight_decay': 5e-4},
    {'params': no_decay, 'weight_decay': 0.0},
], lr=1e-3, betas=(0.9, 0.999))

# ===== 4. Warmup + Cosine 调度 =====
total_steps = len(train_loader) * 30  # 30 epochs
warmup_steps = int(total_steps * 0.1)
def warmup_cosine(step):
    if step < warmup_steps: return step / warmup_steps
    progress = (step - warmup_steps) / (total_steps - warmup_steps)
    return 0.5 * (1 + math.cos(math.pi * progress))
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=warmup_cosine)

# ===== 5. 训练循环 =====
scaler = GradScaler()  # 即使 BF16 也要带,scaler 自动 no-op
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)

step = 0
for epoch in range(30):
    model.train()
    for batch in train_loader:
        x, y = batch[0].to(device), batch[1].to(device)
        optimizer.zero_grad()

        with autocast(dtype=torch.bfloat16):
            logits = model(x)
            loss = criterion(logits, y)

        # BF16 不需 scaler,但接口统一
        if scaler._enabled:  # bf16 时自动 False
            scaler.scale(loss).backward()
            scaler.unscale_(optimizer)
            torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
            scaler.step(optimizer)
            scaler.update()
        else:
            loss.backward()
            torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
            optimizer.step()

        scheduler.step()
        step += 1
        if step % 100 == 0:
            print(f'Step {step}/{total_steps} loss={loss.item():.4f} lr={scheduler.get_last_lr()[0]:.6f}')

预期结果:30 epoch 内 test acc > 92%(ResNet-50 + CIFAR-10 + 这套训练 trick 的标准水平)。


8. 常见坑(7 条)

症状 → 原因 → 修法

  • 症状:Loss 训练初期飙升,降不下来 → 原因:Adam 的 m̂/√v̂ 还没稳定就大 lr 更新 → 修法:加 warmup(总步数的 5-10%),峰值 lr 减小 10x
  • 症状:验证集 loss 训练 5 epoch 后开始上升 → 原因:过拟合,正则不够 → 修法:加 weight decay(Transformer 0.1 / CNN 1e-4)+ dropout(0.1)+ label smoothing(0.1)
  • 症状:训练崩出 NaN Loss → 原因:梯度爆炸 + FP16 数值下溢 → 修法:加 grad_clip=1.0 + 切 BF16 + 检查 loss 是否除以 accumulation_steps
  • 症状:FP16 训练时 loss 出现 spike 然后恢复 → 原因:GradScaler 动态缩放 loss,但缩放因子溢出 → 修法:换 BF16,或把 scaler init_scale 从 65536 调到 256
  • 症状:训练准确率 99%,测试准确率 60% → 原因:严重过拟合 / 数据泄露 → 修法:检查 validation split 是否与 train 重叠 + 数据增强 + 减小模型或加正则
  • 症状:AdamW 训练 loss 正常,但 validation loss 比 train 高 20%+ → 原因:weight decay 应用到所有参数(包括 bias/LN) → 修法:分参数组,no_decay 列表含 bias / LayerNorm / BN
  • 症状:多 GPU 训练时 BN 统计量不准 → 原因:每张卡各自算 BN mean/var,小 batch 不稳定 → 修法:换 SyncBN,或增大 micro batch,或切 GroupNorm / LayerNorm

9. 自检三问(含答案要点)

Q1. 为什么 Transformer 默认 weight_decay=0.1,CNN 默认 1e-4?差 1000 倍合理吗? A1. 合理。Transformer 层数多(12-96 层)、参数多(几亿到几千亿),单参数过拟合风险大,需要强正则;CNN 卷积核有空间归纳偏置,天然抗过拟合,正则可弱。差 1000 倍不是错——AdamW 论文(Loshchilov & Hutter 2019)实测这两个值都在 CIFAR/ImageNet 上最优。

Q2. AdamW 与 Adam+L2 正则在数学上等价吗?为什么 AdamW 实际效果更好? A2. 在 SGD 上等价,在 Adam 上不等价。Adam 的 L2 正则项梯度 λw 被除以 √v̂ + ε 缩放:参数越稀疏(梯度小),L2 反而越强,导致不常见的 token embedding 被过度惩罚。AdamW 把 λw 直接从梯度里拿出来,正则强度对所有参数一致,训练更稳定。Llama 团队 2023 复现实验:同超参下 AdamW val loss 比 Adam 低 0.05-0.1。

Q3. BF16 什么时候取代 FP16?为什么 FP8 还没大规模铺开? A3. BF16 在 2020 年 Ampere(A100)开始硬件支持,2022 年起 LLaMA / BLOOM 等大模型训练全面切换 BF16,2024 年已是绝对主流。FP8(Hopper H100 2022 引入)目前仅在 NVIDIA自家 Megatron / DeepSpeed 框架试点,主要障碍:(1) FP8 量化需要精细的 per-tensor scaling,工程复杂;(2) 精度损失在小模型上明显(0.5-1% 准确率下降);(3) 仅 H100 支持,部署门槛高。预计 2026-2027 年随 Blackwell 铺开 + 框架成熟才大规模应用。


10. 推荐资源(5 类)

  • 🎬 视频:李宏毅《机器学习》2021/2022 — Optimization 章节对 Adam / SGD / 学习率调度讲得最直觉化,中文友好
  • 📖 教科书:《动手学深度学习》(d2l.ai)第 7-8 章 — 「优化算法」和「计算性能」两章是本节最佳教材,含完整 PyTorch 代码
  • 📄 论文:Loshchilov & Hutter《Decoupled Weight Decay Regularization》(ICLR 2019) — AdamW 原始论文 + Dettmers et al.《QLoRA》(NeurIPS 2023,Day 25 详细讲)
  • 📝 博客:Andrej Karpathy《A Recipe for Training Neural Networks》— 实战派总结的「训练技巧清单」,比很多论文都管用
  • 💻 代码:PyTorch 官方 tutorial「Automatic Mixed Precision」(torch.cuda.amp 文档)+ DeepSpeed ZeRO-3(分布式训练标配)

11. 本节要点(7 条压缩结论)

  1. AdamW + Warmup + Cosine + 权重衰减 0.1 + Dropout 0.1 + Label Smoothing 0.1 是现代 Transformer 训练的「六件套」,缺一不可
  2. 优化器选 AdamW(Transformer)/ SGD+Momentum(CNN),不要用 Adam(被 AdamW 严格更优)
  3. 学习率峰值:Transformer 1e-4 到 3e-4,CNN 0.1 量级;Warmup 占总步数 5-10%
  4. 权重衰减只对 weight 应用,不对 bias / LayerNorm / BN 应用,分两组
  5. BF16 是 Ampere 之后的事实标准,无需 GradScaler;FP16 已过时
  6. 梯度累积 + 梯度裁剪(1.0)是 LLM 预训练标配,LLaMA-7B 训练 21 天跑这两套 trick
  7. 数据增强(随机裁剪 / 翻转)对 CNN 几乎免费;Transformer 训练靠正则而不是数据增强

12. 下一节:Day 21 · 第 3 周复盘与图像分类项目

Week 3 5 篇笔记(神经网络 / PyTorch / CNN / RNN / Transformer)+ 训练技巧,已经构成深度学习的完整工具包。Day 21 把这些工具整合成一个真实项目——CIFAR-10 图像分类,含数据探索 → 数据增强 → ResNet-18 → AdamW + Cosine + BF16 → Early Stopping → 测试集评估 → 模型导出。完整体现「从代码到可部署模型」的工程闭环,作为 Week 3 的毕业作业。

说明 · 本站内容均为学习笔记与经验总结,所有菜谱与技法请结合实际食材、季节与个人口味灵活调整。涉及生食、营养与健康的内容仅供参考,特殊体质或疾病请咨询专业营养师/医生。