深度学习笔记 03:深度网络训练与优化器

深度神经网络中,光能训练模型还不够,能“稳定的”训练模型才NB。 本篇涉及:梯度消失/爆炸,归一化,dropout,以及优化器。

模型如何稳定训练

问题是什么:梯度消失与梯度爆炸

深层网络里,gradient 要从 loss 一层层传回前面层。

如果每一层的梯度因子都小于 1,多层相乘后会越来越小,这就是 vanishing gradients。

如果每一层的梯度因子都大于 1,多层相乘后会越来越大,这就是 exploding gradients。

一个粗略例子:

\[ 0.5^{20} \approx 9.5 \times 10^{-7} \]

如果反向传播里连续乘很多个 0.5,前面层几乎收不到有效梯度。

反过来:

\[ 1.5^{20} \approx 3325 \]

如果连续乘很多个大于 1 的因子,梯度可能爆炸,训练变得不稳定。

这也是为什么 deep network 不是简单“多堆几层”就行。层数变深后,梯度能不能稳定传过去本身就是问题。

参数初始化(initialization)

初始化的目标不是找一个好模型,而是给训练一个合适起点。

如果权重太小,信号逐层传递后可能越来越弱;如果权重太大,activation 和 gradient 可能变得不稳定。

常见初始化包括 Xavier initialization 和 He initialization。

  • Xavier initialization 适合 tanh / sigmoid 这类 activation,核心想法是让每层输入输出的方差保持相对稳定。
  • He initialization 更适合 ReLU,因为 ReLU 会把一部分负值截断为 0。

直觉上,初始化是在控制每一层信号的尺度。尺度合适,forward 和 backward 都更容易稳定。

经过线性变换后,网络初始化完成后,第一轮前向传播时q和k的实际分布满足均值0,方差1的初始化。不是权重参数本身满足。

归一化(normalization)

Normalization 的作用是让中间表示的分布更稳定。

Batch Normalization 通常对 batch 维度做归一化:

\[ \hat{x} = \frac{x-\mu_B}{\sqrt{\sigma_B^2+\epsilon}} \]

其中,\(\mu_B\)\(\sigma_B^2\) 是当前 batch 的均值和方差。

BatchNorm 在 CNN 里很常见。它能让训练更稳定,也允许使用更大的 learning rate。

Layer Normalization 则通常对单个样本内部的 hidden dimension 做归一化。Transformer 里更常用 LayerNorm,因为序列任务里 batch 统计不一定稳定,而且不同 token 长度和并行方式会让 BatchNorm 不方便。

可以这样粗略理解:

方法 主要归一化维度 常见场景
BatchNorm batch 维度 CNN
LayerNorm feature / hidden 维度 Transformer、RNN

当前的大模型主要适用RMSNorm,在之后的章节会介绍。

随机失活(dropout):训练时故意丢掉一部分连接

Dropout 是一种正则化方法。训练时随机把一部分 hidden units 置为 0:

1
h = dropout(h, p=0.5)

dropout的np实现

1
2
3
4
5
6
7
8
def dropout_forward(x, p=0.5, training=True):
if not training:
return x
# 生成与 x 形状相同的二值掩码,保留概率 = 1 - p
mask = np.random.binomial(1, 1 - p, size=x.shape)
# 将保留下来的神经元放大 1/(1-p),使输出总和的期望不变
out = x * mask / (1 - p)
return out

注意这里将被留下来的神经元的值除以(1-p),比如p=0.5,那就让剩下的值乘以2,保持输出的数值期望不变。并且如果不做缩放,会导致训推激活值不一致的问题。

它的直觉是:不要让模型过度依赖某几个 neuron,而是让不同子网络都能工作。

测试时通常不再随机丢弃,而是使用完整网络。

Dropout 在早期深度网络里很常见。现在大模型里也会用 dropout,但很多大规模预训练模型会把 dropout 设得比较小,甚至某些设置下不用(因为模型不太可能过拟合了,训练不过来)。

学习率(learning rate)和 scheduler

Learning rate 控制每次参数更新的步长。

如果太小,训练慢;如果太大,loss 可能震荡甚至发散。

深度学习里常见 scheduler:

  • warmup:训练初期逐渐增大学习率
  • step decay:训练到某些 epoch 后降低学习率
  • cosine decay:按余弦曲线逐渐降低学习率

优化器 Optimizer

SGD:最基础的随机梯度下降

Stochastic Gradient Descent 使用 mini-batch gradient 更新参数:

\[ \theta_{t+1} = \theta_t - \eta g_t \]

其中,\(\eta\) 是 learning rate,\(g_t\) 是当前 mini-batch 上的 gradient。

SGD 的优点是简单、泛化表现常常不错。缺点是更新方向受当前 batch 影响,可能噪声较大,收敛也可能比较慢。

比如 loss surface 像一个狭长山谷,SGD 可能在山谷两侧来回震荡,同时沿着谷底方向前进很慢。

动量法(momentum):加入历史方向

Momentum 的思路:不要只看当前 gradient,也看过去一段时间的更新方向。

常见形式:

\[ v_t = \beta v_{t-1} + g_t \]

\[ \theta_{t+1} = \theta_t - \eta v_t \]

其中,\(v_t\) 是 velocity,\(\beta\) 控制保留多少历史方向。

直觉上,如果多个 step 的 gradient 方向一致,Momentum 会加速;如果某个方向来回震荡,正负更新会互相抵消。

这很像推一个球下山。SGD 每一步都只看当前坡度,Momentum 让球带一点惯性。

RMSProp:给每个参数自适应学习率

RMSProp 关注的是每个参数最近 gradient 的平方平均。

\[ s_t = \rho s_{t-1} + (1-\rho)g_t^2 \]

\[ \theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{s_t}+\epsilon}g_t \]

如果某个参数的 gradient 一直很大,\(s_t\) 会变大,于是它的有效学习率会变小。

如果某个参数的 gradient 一直很小,\(s_t\) 小,有效学习率相对更大。

RMSProp 的直觉是:不同参数不一定应该用同一个步长。频繁大幅变化的方向要走得谨慎,变化小的方向可以走得相对积极。

Adam:动量法(momentum)+ RMSProp

Adam 可以看作把 Momentum 和 RMSProp 结合起来。

它维护一阶矩估计:

\[ m_t = \beta_1 m_{t-1} + (1-\beta_1)g_t \]

也维护二阶矩估计:

\[ v_t = \beta_2 v_{t-1} + (1-\beta_2)g_t^2 \]

然后做 bias correction:

\[ \hat{m}_t = \frac{m_t}{1-\beta_1^t} \]

\[ \hat{v}_t = \frac{v_t}{1-\beta_2^t} \]

最后更新:

\[ \theta_{t+1} = \theta_t - \eta \frac{\hat{m}_t}{\sqrt{\hat{v}_t}+\epsilon} \]

Adam 的直觉:

  • \(m_t\) 像 Momentum,记录梯度方向的滑动平均
  • \(v_t\) 像 RMSProp,记录梯度平方的滑动平均
  • 分母让每个参数有自适应步长

Adam 的默认参数经常是:

1
2
3
beta1 = 0.9
beta2 = 0.999
epsilon = 1e-8

Adam 的优点是上手快、收敛稳定、对 learning rate 没有 SGD 那么敏感。

AdamW:把 weight decay 解耦出来

先看Adam 里常见的 L2 regularization。如果把 L2 penalty 加到 loss 里:

\[ L'(\theta)=L(\theta)+\frac{\lambda}{2}\|\theta\|^2 \]

那么梯度会变成:

\[ g'_t = g_t + \lambda \theta_t \]

在 SGD 里,这和 weight decay 基本等价:

\[ \theta_{t+1} = \theta_t - \eta(g_t + \lambda\theta_t) \]

可以整理成:

\[ \theta_{t+1} = (1-\eta\lambda)\theta_t - \eta g_t \]

也就是每步先把权重衰减一点,再按 gradient 更新。

但在 Adam 里,情况不一样。Adam 会用二阶矩 \(\sqrt{\hat{v}_t}\) 对梯度做自适应缩放。如果把 \(\lambda\theta_t\) 混进 gradient,它也会被 Adam 的自适应项缩放。

这样 weight decay 就不再是“统一把权重变小”,而是和每个参数的梯度历史纠缠在一起。

AdamW 的做法是把 weight decay 从 gradient update 里解耦出来:

\[ \theta_{t+1} = \theta_t - \eta \frac{\hat{m}_t}{\sqrt{\hat{v}_t}+\epsilon} - \eta\lambda\theta_t \]

也可以理解成:

  • Adam update: use gradient statistics to update parameters
  • Weight decay: separately shrink weights

AdamW 的核心变化:weight decay 不再被 Adam 的自适应梯度缩放影响。

Adam 和 AdamW 的区别

一句话:

Adam 里的 L2 regularization 是把 weight decay 混进 gradient;AdamW 是把 weight decay 作为单独的参数衰减步骤。

在 Adam 里:

1
2
gradient = gradient + weight_decay * parameter
adaptive_update(gradient)

在 AdamW 里:

1
2
adaptive_update(gradient)
parameter = parameter - lr * weight_decay * parameter

在训练 Transformer / LLM 时,AdamW 比 Adam 更常见。它让 weight decay 的作用更接近正则化直觉。

下面让四个 optimizer 处理同一串 scalar gradient,可以直接看到它们维护的状态有什么不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import math

gradients = [0.8, 0.4, -0.2]
learning_rate = 0.1

# SGD:只使用当前梯度
w_sgd = 1.0

# Momentum:额外保存 velocity
w_momentum, velocity = 1.0, 0.0

# Adam / AdamW:额外保存一阶矩和二阶矩
w_adam = w_adamw = 1.0
m_adam = v_adam = 0.0
m_adamw = v_adamw = 0.0
beta1, beta2, eps = 0.9, 0.999, 1e-8
weight_decay = 0.1

for t, g in enumerate(gradients, start=1):
w_sgd -= learning_rate * g

velocity = 0.9 * velocity + g
w_momentum -= learning_rate * velocity

m_adam = beta1 * m_adam + (1 - beta1) * g
v_adam = beta2 * v_adam + (1 - beta2) * g**2
m_hat = m_adam / (1 - beta1**t)
v_hat = v_adam / (1 - beta2**t)
w_adam -= learning_rate * m_hat / (math.sqrt(v_hat) + eps)

m_adamw = beta1 * m_adamw + (1 - beta1) * g
v_adamw = beta2 * v_adamw + (1 - beta2) * g**2
m_hat = m_adamw / (1 - beta1**t)
v_hat = v_adamw / (1 - beta2**t)
adam_update = m_hat / (math.sqrt(v_hat) + eps)
w_adamw = (
w_adamw
- learning_rate * adam_update
- learning_rate * weight_decay * w_adamw
)

print(t, w_sgd, w_momentum, w_adam, w_adamw)

这段代码刻意把内部状态展开:Momentum 多了 velocity,Adam/AdamW 多了 mv,AdamW 还在 adaptive update 之外单独衰减参数。

优化器对比

优化器 核心想法 优点 常见问题
SGD 当前 mini-batch gradient 简单,泛化常不错 收敛慢,对 lr 敏感
Momentum 加入历史方向 减少震荡,加速一致方向 多一个 momentum 参数
RMSProp 自适应缩放每个参数 处理不同尺度梯度 只看二阶信息
Adam Momentum + RMSProp 稳定、好用、收敛快 weight decay 处理不理想
AdamW Adam + decoupled weight decay 现代深度学习常用 仍然需要调 lr 和 decay

实际训练里,优化器不是越复杂越好。SGD 在一些视觉任务里仍然很强,AdamW 在 Transformer 训练里非常常见。

参考资料