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 在前几百步飙升,再降不下来。原因:
- Adam 的 m̂ / √v̂ 统计量在初始几步还没稳定,被放大后导致更新过大
- 权重随机初始化,远离最优,大步更新震荡
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 条压缩结论)
- AdamW + Warmup + Cosine + 权重衰减 0.1 + Dropout 0.1 + Label Smoothing 0.1 是现代 Transformer 训练的「六件套」,缺一不可
- 优化器选 AdamW(Transformer)/ SGD+Momentum(CNN),不要用 Adam(被 AdamW 严格更优)
- 学习率峰值:Transformer 1e-4 到 3e-4,CNN 0.1 量级;Warmup 占总步数 5-10%
- 权重衰减只对 weight 应用,不对 bias / LayerNorm / BN 应用,分两组
- BF16 是 Ampere 之后的事实标准,无需 GradScaler;FP16 已过时
- 梯度累积 + 梯度裁剪(1.0)是 LLM 预训练标配,LLaMA-7B 训练 21 天跑这两套 trick
- 数据增强(随机裁剪 / 翻转)对 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 的毕业作业。