深度学习训练稳定性:从梯度到初始化

从一层线性模型开始,逐步讲清楚梯度、反向传播、梯度消失、梯度爆炸、奇异值、谱半径、Xavier 初始化和 He 初始化。

深度学习训练稳定性:从梯度到初始化

在 机器学习建模 里,我们讨论了线性模型、逻辑回归和泛化。再往前走,就会进入神经网络。

你已有的 浅谈神经网络原理 给出了一个重要直觉:

神经网络的核心计算是矩阵乘法、加权求和和非线性变换。

但神经网络不是把层数堆起来就能工作。层数越深,训练越容易遇到数值问题:梯度消失、梯度爆炸、loss 震荡、nan。这篇文章就从最基础的梯度开始,慢慢引出这些问题。

先从一层模型开始

线性回归可以写成:

y^=wx+b\hat{y}=wx+b

训练时,我们定义损失函数 LL,然后调整 w,bw,b,让损失变小。

梯度下降的更新形式是:

w←w−η∂L∂ww\leftarrow w-\eta\frac{\partial L}{\partial w}

其中:

  • ww 是参数。
  • η\eta 是学习率。
  • ∂L∂w\frac{\partial L}{\partial w} 是梯度。

梯度回答的是:

参数变一点,损失会怎么变?

如果梯度方向告诉我们“往右损失变大”,那我们就往左更新。

多层网络多了什么

一层模型只有一次变换。多层神经网络会把很多层串起来:

h1=f(W1x+b1)h_1=f(W_1x+b_1) h2=f(W2h1+b2)h_2=f(W_2h_1+b_2) y^=W3h2+b3\hat{y}=W_3h_2+b_3

这里 ff 是激活函数,比如 ReLU、sigmoid、tanh。

前向传播时,输入一层层变成输出。反向传播时,梯度从损失函数一层层传回前面的参数。

问题就在这里:梯度传回去时,会经过很多次链式法则。

反向传播为什么会连乘

假设网络里有很多中间变量:

x -> h1 -> h2 -> h3 -> L

如果想知道 xx 对损失 LL 的影响,根据链式法则:

∂L∂x=∂L∂h3∂h3∂h2∂h2∂h1∂h1∂x\frac{\partial L}{\partial x} = \frac{\partial L}{\partial h_3} \frac{\partial h_3}{\partial h_2} \frac{\partial h_2}{\partial h_1} \frac{\partial h_1}{\partial x}

深层网络里,这样的乘法会非常多。

如果每一项大多小于 1,乘很多次后会越来越小;如果每一项大多大于 1,乘很多次后会越来越大。

这就是梯度消失和梯度爆炸的根源。

梯度消失:前面的层学不到

假设每一层反向传播都让梯度乘以 0.5。传 20 层:

0.520≈0.0000010.5^{20}\approx 0.000001

梯度已经几乎没有了。前面的层参数更新非常小,就像没有学习。

这就是梯度消失。

常见表现:

  • loss 下降很慢。
  • 前面层参数几乎不变。
  • 网络加深后效果不升反降。

sigmoid 和 tanh 在饱和区容易造成这个问题。以 sigmoid 为例:

σ(z)=11+e−z\sigma(z)=\frac{1}{1+e^{-z}}

它的导数是:

σ′(z)=σ(z)(1−σ(z))\sigma'(z)=\sigma(z)(1-\sigma(z))

导数最大也只有 0.25。当很多层都乘上小导数时,梯度很容易消失。

梯度爆炸:参数更新失控

反过来,如果每层都让梯度乘以 1.5,传 40 层:

1.540≈110573321.5^{40}\approx 11057332

梯度会变得非常大。

常见表现:

  • loss 突然变得很大。
  • 参数更新过猛。
  • 训练出现 inf 或 nan。
  • 模型直接崩掉。

这就是梯度爆炸。

权重矩阵也会放大或压缩信号

先忽略激活函数,一个多层网络可以粗略看成多个矩阵相乘:

hL=WLWL−1⋯W1xh_L=W_LW_{L-1}\cdots W_1x

矩阵会改变向量的长度和方向。某些方向被放大,某些方向被压缩。

衡量这种放大或压缩,一个重要概念是奇异值:

  • 奇异值大于 1,对应方向会被放大。
  • 奇异值小于 1,对应方向会被压缩。
  • 多层连乘后,这种放大和压缩会累积。

在循环神经网络里,还经常看谱半径:

ρ(W)=max⁡i∣λi∣\rho(W)=\max_i|\lambda_i|

谱半径大致反映矩阵反复作用时信号是容易增长还是衰减。

这也是为什么权重初始化不能随便写一个随机数。

初始化到底想控制什么

好的初始化不是为了让模型“一开始就很准”,而是为了让信号在网络里稳定传播。

它希望做到两件事:

  1. 前向传播时,每层激活的尺度不要越来越大或越来越小。
  2. 反向传播时,每层梯度的尺度不要越来越大或越来越小。

换句话说,初始化是在给深层网络一个可训练的起点。

Xavier 初始化

Xavier 初始化常用于 tanh、sigmoid 这类较对称的激活函数。

它让权重方差大致满足:

Var(W)=2fan_in+fan_out\text{Var}(W)=\frac{2}{\text{fan\_in}+\text{fan\_out}}

其中:

  • fan_in 是输入维度。
  • fan_out 是输出维度。

目标是平衡输入和输出的方差,让信号不要在层间快速放大或缩小。

PyTorch 示例:

import torch
from torch import nn

linear = nn.Linear(128, 64)
nn.init.xavier_uniform_(linear.weight)
nn.init.zeros_(linear.bias)

He 初始化

He 初始化主要用于 ReLU 及其变体。

ReLU 会把负数变成 0:

ReLU(x)=max⁡(0,x)\text{ReLU}(x)=\max(0,x)

这意味着大约一部分激活会被截断。为了补偿这种影响,He 初始化常用:

Var(W)=2fan_in\text{Var}(W)=\frac{2}{\text{fan\_in}}

PyTorch 示例:

import torch
from torch import nn

linear = nn.Linear(128, 64)
nn.init.kaiming_normal_(linear.weight, nonlinearity="relu")
nn.init.zeros_(linear.bias)

如果网络主要使用 ReLU,一般优先考虑 He 初始化。

训练稳定性的其他工具

初始化只是起点。现代深度网络还会配合其他方法。

方法解决的问题直觉
残差连接深层网络梯度难传给梯度一条更短路径
BatchNorm / LayerNorm激活分布不稳定稳定每层输入尺度
梯度裁剪梯度爆炸限制梯度最大范数
合理学习率更新过大或过小控制每次移动幅度
AdamW参数尺度差异大自适应调整更新

梯度裁剪示例:

loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()

如果梯度范数超过 1.0,它会被缩放回来,避免一次更新把模型推崩。

和 Attention 里的缩放有什么关系

在 Attention 机制 里,Scaled Dot-Product Attention 有一项:

QKTdk\frac{QK^T}{\sqrt{d_k}}

为什么要除以 dk\sqrt{d_k}?本质也是控制数值尺度。

如果 QKTQK^T 的分数太大,softmax 会变得非常尖锐,梯度可能变小,训练会不稳定。

所以从初始化到 attention 缩放,它们背后的共同目标都是:

不要让数值在网络里失控。

一个排查顺序

训练深度网络不稳定时,可以按这个顺序看:

  1. 输入数据有没有归一化。
  2. loss 是否出现 nan 或 inf。
  3. 学习率是否过大。
  4. 初始化是否匹配激活函数。
  5. 梯度范数是否长期接近 0 或突然暴涨。
  6. 是否需要梯度裁剪。
  7. 是否缺少归一化层或残差连接。

可以打印梯度范数:

total_norm = 0.0
for p in model.parameters():
    if p.grad is not None:
        total_norm += p.grad.data.norm(2).item() ** 2
total_norm = total_norm ** 0.5
print("grad_norm:", total_norm)

如果它长期接近 0,要警惕梯度消失;如果突然极大,要警惕梯度爆炸。

总结

这篇文章从一层模型走到深层网络:

梯度下降 -> 链式法则 -> 梯度消失/爆炸 -> 权重尺度 -> 初始化 -> 稳定训练技巧

深度学习不是简单堆层数。网络越深,越要关注前向信号和反向梯度能不能稳定穿过整张计算图。

下一步可以继续读 文本预处理 和 Embedding与位置编码,从一般神经网络进入大语言模型的输入层。

尺素

0 封尺素

正在取尺素...