4. 梯度问题:梯度消失与梯度爆炸、梯度裁剪、Batch Normalization 与 Layer Normalization

训练不收敛,十次里有七八次是梯度出了问题。你想想看,梯度就像给模型指路的向导——向导迷路了,模型自然原地打转。我这些年调过的模型,从几层的 MLP 到上百层的 Transformer,几乎都跟梯度问题打过照面。今天咱们就把这几个“梯度刺客”挨个揪出来。

4.1 梯度消失与梯度爆炸:两个极端

先说现象。梯度消失,就是靠近输入层的参数几乎收不到更新信号,模型学不动。梯度爆炸正好相反,梯度值变得巨大,参数一步就飞出去,Loss 直接变成 NaN。

为什么会这样?

根源在链式法则。深层网络反向传播时,梯度是各层导数的连乘。如果激活函数的导数多数小于 1(比如 Sigmoid 的导数最大才 0.25),连乘几十层后梯度就趋近于 0。反过来,如果权重初始化太大或激活函数导数大于 1,梯度就会指数级增长。

核心判断标准:

  • 梯度消失:靠近输入层的参数更新量比输出层小几个数量级,Loss 下降极慢甚至停滞。
  • 梯度爆炸:训练过程中 Loss 突然变成 NaN 或 Inf,参数值出现异常大值。

我在项目中遇到过最典型的案例:用 Sigmoid 激活函数训练一个 10 层的全连接网络,前 3 层的权重几乎纹丝不动。后来换成 ReLU,问题立刻缓解。说白了,选对激活函数就是第一道防线。

问题类型 典型表现 常见原因 快速诊断方法
梯度消失 Loss 下降缓慢,浅层参数几乎不变 Sigmoid/Tanh 激活、网络过深、初始化不当 打印各层梯度范数,看是否逐层递减
梯度爆炸 Loss 突变为 NaN,参数值异常大 初始化过大、学习率过高、深层网络 监控梯度范数是否超过 1e3

4.2 梯度裁剪:最直接的“刹车”

梯度爆炸的应急处理,我个人习惯先上梯度裁剪。原理很简单:给梯度设个上限,超过就截断。

两种主流方式:

  • 按值裁剪(Clip by Value):把梯度每个元素限制在 [-clip_value, clip_value] 之间。
  • 按范数裁剪(Clip by Norm):如果梯度的 L2 范数超过阈值,等比例缩放。
# PyTorch 示例:梯度裁剪
import torch.nn as nn

model = nn.Sequential(...)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

for batch in dataloader:
    loss = criterion(model(batch), target)
    loss.backward()
    
    # 按范数裁剪,阈值 1.0
    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
    
    optimizer.step()
    optimizer.zero_grad()

我的经验值:

  • RNN/LSTM 训练:max_norm 设为 0.25~1.0 之间,我常用 0.5。
  • Transformer 训练:max_norm 可以放宽到 1.0~5.0。
  • 按范数裁剪比按值裁剪更推荐,因为它保留了梯度方向。

我曾经在训练一个 6 层 LSTM 的语音模型时,不加梯度裁剪,每跑 200 步 Loss 就炸一次。加上 clip_grad_norm_ 之后,训练稳定了,收敛速度反而更快——因为不用频繁回退 checkpoint 了。

4.3 Batch Normalization:让每一层输入“标准化”

Batch Normalization(BN)是解决梯度问题的另一个利器。它的思路很直接:每一层都先把输入拉回均值为 0、方差为 1 的分布,再学两个可缩放参数 γ 和 β。

为什么 BN 能缓解梯度问题?

  • 输入分布稳定了,梯度不会因为输入尺度变化而剧烈波动。
  • 允许使用更大的学习率,加速收敛。
  • 对梯度消失也有缓解作用——因为激活函数工作在非饱和区。
# PyTorch 中 BN 的使用
import torch.nn as nn

model = nn.Sequential(
    nn.Linear(128, 256),
    nn.BatchNorm1d(256),   # 放在激活函数之前
    nn.ReLU(),
    nn.Linear(256, 64),
    nn.BatchNorm1d(64),
    nn.ReLU()
)

注意 BN 的坑:

  • BN 依赖 batch 维度,batch size 太小(比如 1 或 2)时效果很差。
  • 训练和推理阶段行为不同:训练用 batch 统计量,推理用全局移动平均。
  • RNN 中慎用 BN——时间步之间统计量不稳定。

4.4 Layer Normalization:BN 的替代方案

Layer Normalization(LN)跟 BN 的思路不同。BN 是对每个特征维度跨样本做归一化,LN 是对每个样本跨特征维度做归一化。

LN 的优势很明显:

  • 不依赖 batch size,单样本也能算。
  • 在 RNN/Transformer 中表现稳定,因为每个时间步独立归一化。
  • 训练和推理行为一致,没有 BN 那种“训练-推理不一致”的问题。
对比维度 Batch Normalization Layer Normalization
归一化方向 跨样本,对每个特征维度 跨特征,对每个样本
batch size 依赖 强依赖,小 batch 效果差 不依赖,单样本可用
适用场景 CNN、固定尺寸的全连接网络 RNN、Transformer、变长序列
训练/推理一致性 不一致,需维护全局统计量 完全一致
# PyTorch 中 LN 的使用
import torch.nn as nn

# 对特征维度做 LN
ln = nn.LayerNorm(normalized_shape=256)  # 假设特征维度是 256

# 在 Transformer 中,LN 通常放在残差连接之后
class TransformerBlock(nn.Module):
    def __init__(self, d_model):
        super().__init__()
        self.attention = nn.MultiheadAttention(d_model, num_heads=8)
        self.norm1 = nn.LayerNorm(d_model)
        self.ffn = nn.Sequential(
            nn.Linear(d_model, d_model * 4),
            nn.ReLU(),
            nn.Linear(d_model * 4, d_model)
        )
        self.norm2 = nn.LayerNorm(d_model)
    
    def forward(self, x):
        # 残差 + LN
        x = x + self.attention(x, x, x)[0]
        x = self.norm1(x)
        x = x + self.ffn(x)
        x = self.norm2(x)
        return x

我的选择建议:

  • CNN 或固定尺寸图像任务:优先用 BN。
  • NLP、序列模型、Transformer:无脑用 LN。
  • batch size 很小(≤8)且必须用 BN 时,考虑 Group Normalization 作为折中。

4.5 知识结构总览

下面这张图把本章的核心逻辑串起来了。你可以把它当作排查梯度问题的路线图。

梯度问题排查与解决方案总览 梯度问题 梯度消失 梯度爆炸 更换激活函数(ReLU等) Batch Normalization 残差连接(ResNet) 梯度裁剪(Gradient Clipping) 降低学习率 / 学习率预热 权重初始化(Xavier/He) Layer Normalization(通用) 提示:梯度消失和爆炸可能同时存在,建议从激活函数和初始化开始排查,再叠加归一化层

4.6 实战排查清单

如果你现在正被不收敛的问题卡住,按这个顺序查:

  1. 看 Loss 曲线:是缓慢下降、震荡、还是突然飞升?
  2. 打印梯度范数:每层梯度的 L2 范数,看是否逐层递减(消失)或过大(爆炸)。
  3. 检查激活函数:Sigmoid/Tanh 在深层网络里基本是雷区,换 ReLU 或 GELU。
  4. 加梯度裁剪:先设 max_norm=1.0,看训练是否稳定。
  5. 加归一化层:CNN 加 BN,Transformer 加 LN,位置放在激活函数前或残差后。
  6. 检查初始化:He 初始化配 ReLU,Xavier 初始化配 Tanh。

我曾经踩过的坑:

有一次训练一个 50 层的 ResNet,Loss 死活不降。我查了梯度、调了学习率、换了优化器,折腾了两天。最后发现——BN 层的 momentum 设成了 0.9,但 batch size 只有 4,统计量根本不准。把 momentum 调到 0.99 后,问题立刻解决。嗯,有时候问题不在大方向,而在小参数上。