第四章 深度学习 -- 训练技巧

第四章 训练技巧

一、正则化

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)(否则会覆盖预训练参数)。通常流程是:先构造模型 → 加载预训练 → 冻结部分层(可选) → 训练。

相关推荐
HDU-Dade1 小时前
【校招笔试真题】简要介绍 Self-Attention 的前沿算法
人工智能·算法
在所不辞兄2 小时前
【零基础学智能仿真-63】ABACUS作业工具与MCP接口
人工智能·深度学习·神经网络·机器学习·智能仿真
楚山之石2 小时前
【Medical Image Analysis】深度学习医学图像合成综述:伪CT、合成MRI与合成PET的架构、性能与临床边界
图像处理·人工智能·学习·mr·mri
blackappleD2 小时前
百度 Unlimited OCR 2 拆解:SR-SWA 如何让百页文档的 KV 缓存不再涨
人工智能·llm
数字新视界2 小时前
国产动环数据库在智能机房环境监控中的应用与优势分析
大数据·人工智能·数据中心·机房管理·模块化机房
holly就是我2 小时前
AI 编程助手为什么跑偏?用开源 ccglass 看清 Prompt、工具调用和 Token 消耗
人工智能·后端
雨中飘荡的记忆3 小时前
额度又见底了?这个开源项目,给 AI 编程助手接上 61 家"加油站"
人工智能
天远Date Lab3 小时前
零信任架构实战:基于天远企业年报信息核验构建自动化商业保理财务与社保穿透评估网关
人工智能·架构·自动化
EnCi Zheng3 小时前
后端怎么把AI接口稳定安全低成本接进业务:超时重试、密钥隔离与成本熔断清单
人工智能·后端·安全·架构·api