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 知识结构总览
下面这张图把本章的核心逻辑串起来了。你可以把它当作排查梯度问题的路线图。
4.6 实战排查清单
如果你现在正被不收敛的问题卡住,按这个顺序查:
- 看 Loss 曲线:是缓慢下降、震荡、还是突然飞升?
- 打印梯度范数:每层梯度的 L2 范数,看是否逐层递减(消失)或过大(爆炸)。
- 检查激活函数:Sigmoid/Tanh 在深层网络里基本是雷区,换 ReLU 或 GELU。
- 加梯度裁剪:先设 max_norm=1.0,看训练是否稳定。
- 加归一化层:CNN 加 BN,Transformer 加 LN,位置放在激活函数前或残差后。
- 检查初始化:He 初始化配 ReLU,Xavier 初始化配 Tanh。
我曾经踩过的坑:
有一次训练一个 50 层的 ResNet,Loss 死活不降。我查了梯度、调了学习率、换了优化器,折腾了两天。最后发现——BN 层的 momentum 设成了 0.9,但 batch size 只有 4,统计量根本不准。把 momentum 调到 0.99 后,问题立刻解决。嗯,有时候问题不在大方向,而在小参数上。