第四章 训练技巧
一、正则化
1.1 权重衰减
过拟合。模型在训练集上效果远好于验证集,通常是因为参数自由度相对数据量太大,学到了训练集中的「噪声模式」而非「数据规律」。正则化的目的是限制模型有效容量,缓解过拟合。
**权重衰减(Weight Decay)**是最经典且几乎无成本的正则化方法:在损失函数上加一项参数 ℓ2\ell_2ℓ2 范数的惩罚:
Lreg(θ)=L(θ)+λ2∣θ∣22 L_{\text{reg}}(\theta) = L(\theta) + \frac{\lambda}{2} |\theta|_2^2 Lreg(θ)=L(θ)+2λ∣θ∣22
对损失求梯度,多出一项 −λθ-\lambda \theta−λθ。等价地在参数更新时做衰减:
θt+1=θt−η(∇L+λθt)=(1−ηλ)θt−η∇L \theta_{t+1} = \theta_t - \eta(\nabla L + \lambda \theta_t) = (1 - \eta \lambda) \theta_t - \eta \nabla L θt+1=θt−η(∇L+λθt)=(1−ηλ)θt−η∇L
参数每步被乘以 (1−ηλ)(1 - \eta \lambda)(1−ηλ),系数小于 1,故称「衰减」。
为什么有效。直观理解:大权重对应「陡峭」的决策边界,容易对训练集中少数样本过拟合;小权重对应「平滑」边界,泛化更好。统计学习理论中,权重衰减等价于对参数加 Gaussian 先验,促使参数后验接近 0。
与 L2 正则的区别 (实现细节上)。从损失加 ∣w∣2|w|^2∣w∣2 推导的更新,与「参数更新后乘 (1−ηλ)(1-\eta\lambda)(1−ηλ)」数学上等价------前提是优化器不带动量 。在带动量的 SGD 或 Adam 中,「L2 正则」(加到梯度上)与「解耦的权重衰减」(直接衰减参数,AdamW 方式)行为不同。Loshchilov & Hutter(2017,arXiv:1711.05101,ICLR 2019 发表)论文系统比较了这点,结论是 AdamW 的解耦方式在大多数场景下更稳健,这也是现代 Transformer 训练的标配。
PyTorch 实践:
python
# AdamW:weight_decay 直接作为参数传入,默认对所有参数生效
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=0.01)
# SGD:weight_decay 项意义与 AdamW 一致,但更常用 momentum 配合
optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=5e-4)
典型 weight_decay 值:
- AdamW:1e-4 到 1e-1,LLM 预训练常用 0.1。
- SGD(CNN):1e-4 到 1e-3,ResNet 训练常用 5e-4。
- RNN:1e-5 到 1e-4(过大导致梯度消失放大)。
不应对所有参数做衰减 。实践中常见做法是只对权重做衰减,不对 bias、LayerNorm/BatchNorm 的 gamma/beta 做衰减。PyTorch 没有内置这种过滤,需要手动把参数分组:
python
decay, no_decay = [], []
for name, param in model.named_parameters():
if not param.requires_grad:
continue
if len(param.shape) <= 1 or name.endswith('.bias'):
no_decay.append(param) # bias、norm 等 1D 参数不衰减
else:
decay.append(param)
optimizer = torch.optim.AdamW([
{'params': decay, 'weight_decay': 0.01},
{'params': no_decay, 'weight_decay': 0.0},
], lr=1e-4)
这是 BERT、GPT 等模型训练脚本的标配。
1.2 Dropout
Dropout (Srivastava et al., 2014)是另一种高效的正则化手段:训练时随机将一部分神经元的输出置为 0,迫使网络不能过度依赖任何单一神经元,达到「集成多个子网络」的效果。
训练阶段 :对每个神经元,以概率 ppp 保留,以概率 1−p1-p1−p 置零。为保持期望不变,保留下来的输出会被除以 ppp(称为 inverted dropout ,PyTorch 默认实现)。设 p=0.5p=0.5p=0.5,则保留的输出乘以 2,使总期望与不 Dropout 时一致。
推理阶段:不 Dropout,所有神经元都参与计算,输出无需缩放。
PyTorch 用法:
python
drop = nn.Dropout(p=0.5) # 默认 p=0.5,作用于任意维度输入
x = torch.randn(10, 50)
y = drop(x) # 训练时约一半元素被置 0
nn.Dropout 与 nn.Dropout2d、nn.Dropout3d 的区别:
nn.Dropout:逐元素独立置零,作用于任意张量。nn.Dropout2d:对整个 channel(feature map)置零,常用于 CNN。输入 (N, C, H, W) 时,采样形状为 (N, C, 1, 1),整 channel 共享同一 mask。nn.Dropout3d:对整个 volume 置零,用于 3D CNN / 视频。
为什么 Dropout2d/3d 更好用于卷积层。图像相邻像素高度相关,逐元素 Dropout 后网络仍能通过相邻像素推断出信息,正则效果弱。把整个 channel 置零则强制网络不能依赖该 feature map 的任何信息,正则效果更强。
Dropout 放置位置 。典型 CNN 结构:Conv → BN → ReLU → Dropout → ...(Dropout 放在 BN 之后);全连接:Linear → ReLU → Dropout → Linear。不要把 Dropout 直接加在输出层(logits)上------对即将参与损失计算的 logits 做随机置零没有意义;但「最后一个隐藏层之后、输出层之前」加 Dropout 是标准且有效的做法(BERT 的分类头即此),不必担心。
Dropout 与模型深度的关系 。Dropout 率 ppp 通常随网络深度增加而增大,或在多层堆叠时使用恒定 ppp。常见选择:输入层 p=0.1−0.2p=0.1-0.2p=0.1−0.2,中间隐藏层 p=0.3−0.5p=0.3-0.5p=0.3−0.5。注意:p=0.5 不是越大越好,过大会损害模型容量,反而欠拟合。
Dropout 的现代替代:
- DropConnect:不置零神经元,而是置零权重(每个连接独立 mask)。
- SpatialDropout / DropBlock:对卷积 feature map 做结构化 mask(连续区域置零),更适合 CNN。
- Stochastic Depth:ResNet 中随机跳过整个残差块。
- Label Smoothing:替代或补充 Dropout,在输出端做软化。
与 model.eval() 的关系 。Dropout 在 model.train() 时激活,model.eval() 时关闭。验证/测试前必须调用 model.eval(),否则结果带随机性。
二、归一化
2.1 批归一化
批归一化 (Batch Normalization, BN;Ioffe & Szegedy, 2015)对每个 mini-batch 内的样本,在某一层激活前/后做归一化:
x^=x−μbatchσbatch2+ϵ,y=γx^+β \hat{x} = \frac{x - \mu_{\text{batch}}}{\sqrt{\sigma^2_{\text{batch}} + \epsilon}}, \quad y = \gamma \hat{x} + \beta x^=σbatch2+ϵ x−μbatch,y=γx^+β
其中 μbatch\mu_{\text{batch}}μbatch、σbatch2\sigma^2_{\text{batch}}σbatch2 是该 batch 在该通道上的均值与方差;γ\gammaγ、β\betaβ 是可学习的缩放与平移参数(初始为 1 和 0,允许网络在需要时「还原」非归一化)。
为什么有效(原论文动机是「减少 Internal Covariate Shift」,但后续研究(Santurkar et al., 2018)表明这并非其奏效的主因;更被接受的解释是 BN 让损失曲面更平滑,使梯度更稳定、更易选学习率)。
PyTorch 接口:
python
# 1D 输入(N, C)或(N, C, L):如 MLP 输出、时序信号
bn1d = nn.BatchNorm1d(num_features=128) # eps=1e-5, momentum=0.1
# 2D 输入(N, C, H, W):图像
bn2d = nn.BatchNorm2d(num_features=64) # CNN 中最常用
# 3D 输入(N, C, D, H, W):视频或 3D 医学影像
bn3d = nn.BatchNorm3d(num_features=32)
关键参数:
num_features:特征数 / 通道数(对 BN1d/2d/3d)。eps:防止除零,默认 10−510^{-5}10−5。momentum:running mean/var 的更新系数,注意不是优化器动量 。更新规则是 x^new=(1−momentum)x^+momentumxt\hat{x}_{\text{new}} = (1 - \text{momentum}) \hat{x} + \text{momentum} x_tx^new=(1−momentum)x^+momentumxt,默认 0.1 表示 running 统计量更新较慢。affine:是否使用可学习 γ\gammaγ、β\betaβ,默认 True。track_running_stats:训练时是否维护 running 统计量,默认 True。False时,推理阶段也用 batch 统计量(不常见)。
训练与推理差异。这是 BN 最容易出错的地方:
- 训练:用当前 batch 的均值/方差做归一化;同时更新 running 统计量。
- 推理:用训练时累积的 running 统计量做归一化(不重新计算 batch 统计量)。
为什么? 推理时 batch size 可能为 1,或样本分布与训练时差异大,基于 batch 统计量不稳定。PyTorch 通过 model.train() / model.eval() 自动切换,调用方无须手动干预 ------但必须切换模式,否则推理结果会带随机性(因为 batch 统计量每次不同)。
何时不该用 BN:
- batch size 过小时 (如 1-2):batch 统计量噪声大,BN 失效甚至损害性能。此时用 Group Normalization (GN) 或 Layer Normalization (LN)。
- 序列任务(RNN/Transformer) :序列长度方向上不同位置的统计量变化大,BN 在 batch 维归一化不合适;Transformer 中统一用 LayerNorm。
- 对比学习 / 自监督:对 batch 统计量敏感的方法(如某些 contrastive loss)需谨慎使用 BN。
BN 在 CNN 中的放置顺序。主流选择是:
- Conv → BN → ReLU :主流默认,原版 ResNet、VGG 系即此顺序------BN 先把卷积输出拉回稳定分布,再过 ReLU,激活不易进入饱和区。
- Conv → ReLU → BN(post-activation):少数网络采用,BN 作用于激活后的分布。
- BN → ReLU → Conv(pre-activation):ResNet-v2 的变体,让 shortcut 路径「完全干净」,支持极深网络(见第五章)。
经验上,从零构建 CNN 用 Conv → BN → ReLU 即可;三种顺序在多数任务上差异不大,保持全书一致比纠结顺序更重要。
2.2 层归一化
层归一化 (Layer Normalization, LN;Ba et al., 2016)对单个样本的所有特征做归一化,与 batch 维度无关:
x^i=xi−μσ2+ϵ,yi=γx^i+β \hat{x}_i = \frac{x_i - \mu}{\sqrt{\sigma^2 + \epsilon}}, \quad y_i = \gamma \hat{x}_i + \beta x^i=σ2+ϵ xi−μ,yi=γx^i+β
其中 μ\muμ、σ2\sigma^2σ2 是该样本在所有特征维度上的均值/方差。
BN vs LN:
| 维度 | BN | LN |
|---|---|---|
| 归一化范围 | 跨 batch(同一通道) | 跨特征(同一样本) |
| 依赖 batch size | 是 | 否 |
| 训练-推理差异 | 有(running 统计量) | 无 |
| 主要适用 | CNN | Transformer、RNN |
| 序列任务 | 不适合 | 标准选择 |
LN 的核心优势 :不依赖 batch 维度,因此:
- batch size = 1 也能用。
- 训练/推理行为完全一致,无需切换。
- 序列模型中,不同位置归一化统计量一致,有利于处理变长输入。
PyTorch:
python
ln = nn.LayerNorm(normalized_shape=512) # 对最后一维(或指定多维)做 LN
# 在 Transformer 中,通常对 d_model 维做 LN
Group Normalization (GN;Wu & He, 2018)是 BN 与 LN 的折中:把通道分成若干 group,在每个 group 内做归一化。num_groups 控制粒度,num_groups=num_channels 时退化为 Instance Normalization (逐通道、仅在空间维归一化),num_groups=1 时退化为 Layer Normalization(对单样本全部通道归一化)。注意:GN 始终在 batch 维不统计,所以两种极端都不可能是 BN。GN 在 batch size 较小(2-16)时是 BN 的有效替代,在检测/分割任务中表现尤佳。
为什么 Transformer 几乎全用 LN 。Transformer 中的 self-attention、FFN 等子层后都会接 LN(Add & Norm),原因:
- 自注意力是逐 token 运算,batch 维归一化(BN)会破坏序列内不同位置的关系。
- 训练大模型常用大批次 + 梯度累积,实际有效 batch size 较大但 sub-batch 不固定,LN 行为更稳定。
- 工程上 LN 在推理时无需任何状态,简化部署。
Instance Normalization (IN)对每个样本的每个通道独立做归一化,仅用空间维度(H, W)上的统计。IN 是风格迁移任务的关键组件,在图像生成网络中常见。
三、训练策略
3.1 学习率调度
学习率 是训练中最敏感的超参数。学习率调度(Learning Rate Schedule)指在训练过程中按某种策略调整学习率,而非始终恒定。
为什么需要调度。直觉上:
- 训练初期:参数远离最优,大学习率加速收敛。
- 训练中期:接近最优,中等学习率稳定收敛。
- 训练后期:接近最优点,小学习率精细调参。
恒定学习率难以兼顾这三个阶段。
Step Decay(阶梯衰减)。每隔若干 epoch 把学习率乘以一个因子(0.1 常见):
python
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
# 每 30 个 epoch, lr *= 0.1
在 ResNet 等经典 CNN 训练中是标配。
MultiStep Decay(多步衰减)。在指定的 epoch 列表处衰减:
python
scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones=[30, 60, 90], gamma=0.1)
Exponential Decay (指数衰减):lr_t = lr_0 * gamma^t。
Cosine Annealing (余弦退火;Loshchilov & Hutter, 2016)。学习率按余弦曲线从 ηmax\eta_{\max}ηmax 平滑下降到 ηmin\eta_{\min}ηmin:
python
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100, eta_min=1e-6)
无突跳,曲线平滑,在 Transformer 训练中非常常用。注意:T_max 是一个完整周期长度 ,若训练总 epoch 不是 T_max 的整数倍,学习率不会回到 ηmax\eta_{\max}ηmax。
CosineAnnealingWarmRestarts (SGDR)。周期性「热重启」:每隔 T0T_0T0 个 epoch,学习率重新拉高到 ηmax\eta_{\max}ηmax。有助于跳出局部极小。
Warmup(预热)。训练初期用一个小的学习率,逐步升到目标值,避免初期不稳定:
python
# Warmup + Cosine Decay(Transformer 训练标配)
from torch.optim.lr_scheduler import LambdaLR
import math
def lr_lambda(step):
warmup_steps = 1000
total_steps = 100000
if step < warmup_steps:
return step / warmup_steps
progress = (step - warmup_steps) / (total_steps - warmup_steps)
return 0.5 * (1.0 + math.cos(math.pi * progress))
scheduler = LambdaLR(optimizer, lr_lambda)
为什么需要 Warmup。训练初期:
- 参数随机初始化,损失曲面「形状」不稳定。
- Adam 等自适应优化器的二阶矩估计在初期噪声大,偏差校正又会放大更新步长,容易触发过大的更新。
- 大 batch 训练时,初期几步容易因某个 batch 极端值而爆炸。
Warmup 用小学习率跑过这段不稳定期,显著提升稳定性。Transformer 训练几乎必备,典型预热步数占总步数的 1%-10%。
ReduceLROnPlateau(基于指标的自适应)。验证 loss 不再下降时降低学习率:
python
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=5)
# 注意:这种 scheduler 调用方式不同
for epoch in range(num_epochs):
train(...)
val_loss = validate(...)
scheduler.step(val_loss) # 不是 epoch!
适用于「不知道训练多久」「验证集变化不定」的场景。
Linear Decay / Square Root Decay。NLP 早期常用:
- Linear:lr 从 ηmax\eta_{\max}ηmax 线性降到 0。
- Sqrt(简化形式):
lr_t = \eta_{\max} / \sqrt{t}。原始 Transformer(Vaswani et al. 2017)实际用的是更复杂的lr = d_{\text{model}}^{-0.5} \cdot \min(t^{-0.5}, t \cdot \text{warmup}^{-1.5})(含 warmup 项),简化版 ηmax/t\eta_{\max}/\sqrt{t}ηmax/t 是教学化抽象,实际使用多采用前者。
Cyclic LR (循环学习率)。在 ηmin\eta_{\min}ηmin 与 ηmax\eta_{\max}ηmax 之间循环,让学习率反复高低震荡;配合 Snapshot Ensemble(每个 cycle 末保存模型)有奇效。
调度策略选择经验:
| 任务 | 推荐调度 |
|---|---|
| 经典 CNN(ResNet 等) | Step / MultiStep Decay + Warmup(可选) |
| Transformer 预训练 | Linear Warmup + Cosine Decay |
| 微调(Fine-tuning) | 恒定小学习率 或 Cosine |
| GAN | 恒定(调度容易致训练不稳) |
| 不确定 | Linear Warmup + Cosine(最稳健) |
3.2 参数初始化
为什么需要好的初始化。训练开始时参数是「起点」。如果起点太极端(全 0、太大、太小),前向传播信号可能消失或爆炸,反向传播同样受影响。好的初始化要保证:
- 前向:每层激活的方差在各层间大致一致,避免逐层缩小/放大。
- 反向:每层梯度的方差在各层间大致一致。
全零初始化是错的。所有神经元输出相同,反向梯度相同,等价于一个神经元,网络无法学到多样性。
Xavier 初始化 (Glorot & Bengio, 2010)。针对 Sigmoid/Tanh 推导,假设激活近似线性,推导要求前向与反向方差都稳定。权重从均匀分布 U−6/(nin+nout),6/(nin+nout)U-\\sqrt{6/(n_{in}+n_{out})}, \\sqrt{6/(n_{in}+n_{out})}U−6/(nin+nout) ,6/(nin+nout) 采样,或正态分布 N(0,2/(nin+nout))N(0, 2/(n_{in}+n_{out}))N(0,2/(nin+nout))。
python
nn.init.xavier_uniform_(layer.weight)
nn.init.xavier_normal_(layer.weight)
He 初始化 (He et al., 2015)。针对 ReLU 推导(ReLU 让一半神经元输出为 0,等价于 fan_in 减半),权重方差为 2/nin2/n_{in}2/nin。对 ReLU 网络的标配。
python
nn.init.kaiming_uniform_(layer.weight, mode='fan_in', nonlinearity='relu')
nn.init.kaiming_normal_(layer.weight, mode='fan_in', nonlinearity='relu')
注意默认值 :nn.init.kaiming_normal_ 的默认参数为 a=0, mode='fan_in', nonlinearity='leaky_relu';对纯 ReLU 网络建议显式传 nonlinearity='relu' 以匹配方差推导假设。
正交初始化(Orthogonal Initialization)。把权重矩阵初始化为正交矩阵,在 RNN 中特别有效,有助于缓解梯度爆炸/消失:
python
nn.init.orthogonal_(layer.weight)
截断正态 / 均匀 。对 Transformer 等场景,常用 N(0,0.02)\mathcal{N}(0, 0.02)N(0,0.02) 作为默认初始化(GPT-2 论文):
python
nn.init.normal_(layer.weight, mean=0.0, std=0.02)
常数初始化 。bias 通常初始化为 0;某些场景下,BN 的 γ\gammaγ 初始化为 1、β\betaβ 初始化为 0;输出层 bias 初始化为特定值(如分类器偏向均匀分布)有时有助。
PyTorch 的默认初始化。不调用显式初始化时,各层有内置默认:
nn.Linear:权重 U(−1/nin,1/nin)U(-1/\sqrt{n_{in}}, 1/\sqrt{n_{in}})U(−1/nin ,1/nin ),bias 同样从 U(−1/nin,1/nin)U(-1/\sqrt{n_{in}}, 1/\sqrt{n_{in}})U(−1/nin ,1/nin ) 采样(不是 0 ;nn.Conv2d的 bias 同理)。nn.Conv2d:与 Linear 类似。nn.BatchNorm:γ=1\gamma=1γ=1, β=0\beta=0β=0。nn.LayerNorm:γ=1\gamma=1γ=1, β=0\beta=0β=0。
经验:对浅层 MLP(2-3 层)默认初始化能用;对深层 CNN 用 He 初始化更稳健;对 Transformer 显式初始化(标准差 0.02)已成为惯例。
死循环:初始化与激活的匹配。Sigmoid/Tanh + Xavier,ReLU + He,这是 2010 年后总结出的「黄金搭配」,混搭(如 ReLU + Xavier)通常不会崩,但可能损失一些性能。
应用初始化到整个模型 。model.apply(fn) 可递归对每个子模块应用初始化函数:
python
def init_weights(m):
if isinstance(m, nn.Linear):
nn.init.kaiming_normal_(m.weight, nonlinearity='relu')
if m.bias is not None:
nn.init.zeros_(m.bias)
elif isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, (nn.BatchNorm2d, nn.LayerNorm)):
nn.init.ones_(m.weight)
nn.init.zeros_(m.bias)
model.apply(init_weights)
注意:apply 会在每次实例化时执行,不需要担心重复初始化------这是预期行为。
与迁移学习的衔接 。如果加载预训练权重,load_state_dict 后不要再调用 apply(init_weights)(否则会覆盖预训练参数)。通常流程是:先构造模型 → 加载预训练 → 冻结部分层(可选) → 训练。