深度学习优化器(Optimization)

模型训练的本质是一场”寻找最低点”的旅程,而优化器就是你的向导。本文从梯度下降出发,系统讲解 SGD、Momentum、AdaGrad、RMSProp、Adam 等主流优化器的原理与适用场景,帮你在实践中做出正确的选择。

📑 目录


1. 为什么需要优化器

训练一个神经网络,本质上是在解一个优化问题:找到一组参数 $\theta$,使损失函数 $\mathcal{L}(\theta)$ 尽可能小。

损失函数的地形通常高度非凸——有山谷、鞍点、陡坡和平原。优化器就像登山者手里的路线图和工具,不同优化器对不同地形的适应能力截然不同。

朴素 SGD 存在三个根本痛点,正是这些痛点推动了现代优化器的演进:

  • 参数一刀切:所有参数使用同一学习率 η,但不同参数的重要性和梯度尺度差异悬殊
  • 稀疏梯度更新慢:词嵌入等稀疏特征偶尔才出现在 batch 中,统一学习率无法让这类参数快速追上
  • 鞍点陷阱:高维空间中鞍点远比局部最优更常见,鞍点附近梯度趋近于零,训练容易停滞

好的优化器需要兼顾三个目标:

  • 收敛速度:尽快找到低点
  • 泛化能力:找到的解在测试集上表现好
  • 稳定性:不在训练过程中发散或剧烈震荡

2. 梯度下降基础

2.1 原始梯度下降

最朴素的想法是:沿着损失函数下降最快的方向走一步。这就是梯度下降(Gradient Descent):

$$
\theta_{t+1} = \theta_t - \eta \cdot \nabla_\theta \mathcal{L}(\theta_t)
$$

其中 $\eta$ 是学习率,控制每步的步长;$\nabla_\theta \mathcal{L}$ 是损失函数对参数的梯度。

2.2 三种梯度下降变体

实际训练中,我们不一定每次都用全量数据计算梯度:

变体 每次使用数据量 梯度噪声 计算代价
批量梯度下降(BGD) 全部训练集 极高(大数据集不可行)
随机梯度下降(SGD) 单个样本 极低
小批量梯度下降(Mini-batch SGD) 一个 batch(如 32/128) 适中 适中

💡 提示:现代深度学习几乎都用 Mini-batch SGD,”SGD” 在框架(如 PyTorch)中通常就指 Mini-batch SGD。

2.3 学习率的重要性

学习率 $\eta$ 是梯度下降中最关键的超参数:

  • $\eta$ 太大 → 参数在最优点附近震荡甚至发散
  • $\eta$ 太小 → 收敛极慢,容易卡在局部最优

⚠️ 注意:学习率没有”通用最优值”,需要结合模型规模、batch size、数据分布调整。常见的起始值范围是 $[10^{-4}, 10^{-1}]$。


3. 动量优化:SGD with Momentum

3.1 SGD 的问题

普通 SGD 在”沟壑型”损失函数上表现很差:在狭窄方向上(梯度大)步子迈得太大,在宽阔方向上(梯度小)步子迈得太小,结果路线在沟里来回震荡,进展缓慢。

类比:想象你在一个狭长的山谷里滚球,球会在两壁之间反弹,而不是沿着谷底直奔出口。

3.2 动量的引入

动量(Momentum)的核心思想:积累历史梯度方向,抑制振荡,加速前进。就像物理学中的动量,物体运动会有惯性,方向不会突变。

引入速度向量 $v$:

$$
v_t = \beta \cdot v_{t-1} + \nabla_\theta \mathcal{L}(\theta_t)
$$

$$
\theta_{t+1} = \theta_t - \eta \cdot v_t
$$

其中 $\beta \in [0, 1)$ 是动量系数,通常取 0.9。$v_t$ 直接累积梯度(而非做归一化加权),使得持续同向的梯度会叠加放大,这正是”动量”加速的来源。

📌 关键点:当历史梯度方向一致时,$v$ 会不断累积,步长加速;当梯度方向反复变化时,$v$ 相互抵消,抑制震荡。

3.3 PyTorch 使用示例

1
2
3
4
5
6
7
8
import torch.optim as optim

optimizer = optim.SGD(
model.parameters(),
lr=0.01,
momentum=0.9,
weight_decay=1e-4 # L2 正则化
)

4. 自适应学习率:AdaGrad

4.1 核心动机

不同参数的梯度大小差异悬殊——某些参数(如高频特征对应的权重)梯度很大,另一些(如低频特征)梯度极小。用统一的学习率对所有参数一视同仁,显然不公平。

AdaGrad 的思路:给梯度大的参数自动缩小学习率,给梯度小的参数自动放大学习率。

4.2 更新规则

AdaGrad 累积每个参数的历史梯度平方和 $G_t$:

$$
G_t = G_{t-1} + \left(\nabla_\theta \mathcal{L}(\theta_t)\right)^2
$$

$$
\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{G_t + \epsilon}} \cdot \nabla_\theta \mathcal{L}(\theta_t)
$$

其中 $\epsilon$(通常取 $10^{-8}$)是防止除零的数值稳定项。

梯度越大的方向,$G_t$ 积累越多,有效学习率 $\frac{\eta}{\sqrt{G_t}}$ 越小——这正是”自适应”的含义。

4.3 AdaGrad 的缺陷

$G_t$ 只增不减。训练时间一长,$G_t$ 会越积越大,所有参数的学习率趋近于零,模型过早停止学习

这使得 AdaGrad 在深层网络长时间训练中表现很差,但在稀疏数据场景(如 NLP 词向量)中仍有竞争力。


5. RMSProp:修正 AdaGrad 的缺陷

5.1 指数移动平均代替累积求和

RMSProp(Root Mean Square Propagation)对 AdaGrad 的”学习率归零”问题提出了一个简洁的修复:不累积全量历史梯度,改用指数移动平均(EMA)保留近期梯度信息

$$
s_t = \rho \cdot s_{t-1} + (1 - \rho) \cdot \left(\nabla_\theta \mathcal{L}(\theta_t)\right)^2
$$

$$
\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{s_t + \epsilon}} \cdot \nabla_\theta \mathcal{L}(\theta_t)
$$

衰减系数 $\rho$(通常取 0.9)控制”记忆长度”——越接近 1,越看重历史;越小,越注重当前梯度。

📌 关键点:EMA 机制让 $s_t$ 保持稳定,不会无限增大,从而解决了学习率消亡的问题。

5.2 与 AdaGrad 的对比

特性 AdaGrad RMSProp
历史梯度处理 累积求和 指数移动平均
学习率变化 单调递减趋零 动态调整,保持活跃
适用场景 稀疏特征、浅层模型 深层网络、RNN

6. Adam:工程上的首选

6.1 Adam = Momentum + RMSProp

Adam(Adaptive Moment Estimation)可以理解为把 SGD with Momentum 和 RMSProp 的优点合二为一:

  • 一阶矩(动量项)$m_t$:累积梯度方向信息,加速收敛
  • 二阶矩(自适应项)$v_t$:累积梯度幅度信息,自适应调整学习率

6.2 完整公式

Step 1:计算一阶矩和二阶矩的 EMA

$$
m_t = \beta_1 \cdot m_{t-1} + (1 - \beta_1) \cdot \nabla_\theta \mathcal{L}(\theta_t)
$$

$$
v_t = \beta_2 \cdot v_{t-1} + (1 - \beta_2) \cdot \left(\nabla_\theta \mathcal{L}(\theta_t)\right)^2
$$

Step 2:偏差修正

初始时刻 $m_0 = v_0 = 0$,导致早期估计值偏小。偏差修正项将其校正:

$$
\hat{m}_t = \frac{m_t}{1 - \beta_1^t}, \quad \hat{v}_t = \frac{v_t}{1 - \beta_2^t}
$$

Step 3:参数更新

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

默认超参数:$\beta_1 = 0.9$,$\beta_2 = 0.999$,$\epsilon = 10^{-8}$,$\eta = 10^{-3}$。

6.3 为什么需要偏差修正

考虑初始状态:$m_0 = 0$。

第一步更新后:$m_1 = (1 - \beta_1) \cdot g_1 = 0.1 \cdot g_1$,这严重低估了实际梯度 $g_1$。

偏差修正后:$\hat{m}_1 = \frac{m_1}{1 - \beta_1^1} = \frac{0.1 \cdot g_1}{0.1} = g_1$,恢复了正确估计。

随着 $t$ 增大,$1 - \beta_1^t \to 1$,偏差修正的影响逐渐消失。

6.4 Adam 直觉:三个组件各司其职

组件 核心作用 超参默认值 直觉
一阶矩 $m_t$ 平滑梯度方向,提供动量 $\beta_1 = 0.9$ 记忆约 10 步的梯度方向,帮助穿越鞍点
二阶矩 $v_t$ 自适应缩放,反映梯度震荡幅度 $\beta_2 = 0.999$ 记忆约 1000 步的历史,频繁更新的参数步长被压缩
偏差修正 $\hat{m}, \hat{v}$ 校正初始为零导致的低估 $t \to \infty$ 时修正项趋近 1,自动消退

理解 Adam 最核心的直觉是:分母 $\sqrt{\hat{v}_t}$ 起到了”缩放器”的作用:

  • 若某参数梯度持续偏大($\hat{v}_t$ 大),步长被压缩 → 防止震荡
  • 若某参数梯度持续偏小($\hat{v}_t$ 小),步长被放大 → 加速稀疏特征的学习

这正是自适应学习率的精髓:不同参数获得各自合适的有效步长,而非一刀切。

6.5 PyTorch 使用示例

1
2
3
4
5
6
7
8
9
import torch.optim as optim

optimizer = optim.Adam(
model.parameters(),
lr=1e-3,
betas=(0.9, 0.999),
eps=1e-8,
weight_decay=0 # 如需 L2 正则化可设置此项
)

💡 提示:大多数情况下,Adam 的默认参数就能工作得很好,无需大量调参——这是它广受工程师喜爱的重要原因。


7. 进阶变体

7.1 AdamW:分离权重衰减

Adam 和 AdamW 的区别,是优化器发展史上最重要的洞见之一。

问题根源:L2 正则化 ≠ 权重衰减

在 SGD 中,L2 正则化与权重衰减是完全等价的。但在 Adam 里,两者行为截然不同:

❌ Adam 的 L2 实现(存在缺陷):将正则项 $\lambda\theta_t$ 加入梯度,再经过自适应缩放:

$$
\tilde{g}_t = g_t + \lambda \cdot \theta_t
$$

$$
\theta_{t+1} = \theta_t - \eta \cdot \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}
\quad \text{(此处 } m, v \text{ 均由 } \tilde{g}_t \text{ 计算)}
$$

✅ AdamW 的权重衰减(正确实现):将衰减直接作用在参数上,绕过自适应机制:

$$
\theta_{t+1} = \theta_t - \eta \cdot \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} - \eta\lambda\theta_t
\quad \text{(此处 } m, v \text{ 由原始梯度 } g_t \text{ 计算)}
$$

为什么 Adam 的 L2 有问题\?

自适应缩放会扭曲正则化效果:梯度小的参数($\hat{v}_t$ 小)本应被正则化更多,但分母小反而导致步长被放大,L2 的惩罚强度被意外增大,行为不可预测。AdamW 将权重衰减与梯度历史解耦,正则化强度不再受梯度幅度干扰。这在 Transformer 类模型(BERT、GPT 等)的预训练中被广泛验证为更有效的正则化手段。

1
2
3
4
5
6
7
8
9
10
11
12
13
import torch

# ⚠️ Adam:weight_decay 被自适应缩放干扰,L2 效果失真
optimizer_adam = torch.optim.Adam(
model.parameters(),
lr=1e-3, betas=(0.9, 0.999), weight_decay=0.01
)

# ✅ AdamW:真正的权重衰减,正则化与梯度更新解耦
optimizer_adamw = torch.optim.AdamW(
model.parameters(),
lr=1e-3, betas=(0.9, 0.999), weight_decay=0.01
)

⚠️ 注意:训练 LLM/Transformer 时,优先选择 AdamW 而非 Adamweight_decay 常设为 0.01 或 0.1。

7.2 Lion:更节省显存

Lion(EvoLved Sign Momentum)由 Google Brain 2023 年通过程序化搜索发现,只使用符号梯度,更新规则更简洁:

$$
c_t = \beta_1 \cdot m_{t-1} + (1 - \beta_1) \cdot \nabla_\theta \mathcal{L}
$$

$$
\theta_{t+1} = \theta_t - \eta \cdot \text{sign}(c_t) - \lambda \cdot \theta_t
$$

$$
m_t = \beta_2 \cdot m_{t-1} + (1 - \beta_2) \cdot \nabla_\theta \mathcal{L}
$$

由于只维护一阶矩 $m$,**显存占用比 Adam 少约 33%**,在超大模型训练中有实际意义。

7.3 Muon:MLP 层专用

Muon(Momentum + Orthogonalization)是近期 LLM 训练中备受关注的优化器,对线性层的权重矩阵进行梯度正交化处理,在矩阵更新的”流形约束”上做优化,在某些场景下表现优于 AdamW。


8. 实战选择指南

8.1 优化器横向对比

优化器 动量 自适应学习率 显存开销 推荐场景
SGD 极低 CV 微调、需要高泛化性
SGD + Momentum 极低 CV 分类任务主流选择
AdaGrad 稀疏特征、词向量
RMSProp ✅(可选) RNN、非平稳目标
Adam 中(额外 2x,共 3x) 通用默认选择
AdamW 中(额外 2x,共 3x) Transformer、LLM 预训练
Lion 较低(额外 1x,共 2x) 超大模型、显存受限

8.2 决策流程


graph TD
A["需要选择优化器"] --> B{"任务类型\?"}
B --> C["CV 分类(ImageNet 等)"]
B --> D["Transformer / LLM 训练"]
B --> E["RNN / 时间序列"]
B --> F["通用 / 快速实验"]
C --> G["SGD + Momentum\n(搭配 CosineAnnealing)"]
D --> H["AdamW\n(weight_decay=0.01)"]
E --> I["RMSProp 或 Adam"]
F --> J["Adam(默认参数)"]
H --> K{"显存是否紧张\?"}
K --> L["是 → 考虑 Lion"]
K --> M["否 → 坚持 AdamW"]

8.3 学习率调度(不可忽视)

优化器需要配合学习率调度器(Scheduler)才能发挥最佳效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from torch.optim.lr_scheduler import CosineAnnealingLR, LinearLR, SequentialLR

optimizer = optim.AdamW(model.parameters(), lr=1e-4, weight_decay=0.01)

# Warmup + Cosine Decay(LLM 训练标配)
warmup_scheduler = LinearLR(optimizer, start_factor=0.1, end_factor=1.0, total_iters=1000)
cosine_scheduler = CosineAnnealingLR(optimizer, T_max=total_steps - 1000)
scheduler = SequentialLR(optimizer, schedulers=[warmup_scheduler, cosine_scheduler], milestones=[1000])

# 每个 step 后调用
for step, batch in enumerate(dataloader):
loss = model(batch)
optimizer.zero_grad()
loss.backward()
optimizer.step()
scheduler.step()

常用调度策略:

策略 适用场景
CosineAnnealingLR LLM/Transformer 预训练,现在几乎是标配
StepLR CV 分类训练,每隔固定 epoch 衰减
ReduceLROnPlateau 验证集 loss 不下降时自动降低 lr
LinearWarmup + 衰减 微调大模型,先 warmup 再衰减

8.4 超参数设置指南

超参数 默认值 含义 调整建议
$\eta$(学习率) 1e-3 整体步长 最重要的超参,通常搭配学习率调度使用
$\beta_1$ 0.9 一阶矩衰减 一般无需调整;训练不稳定时可降至 0.85
$\beta_2$ 0.999 二阶矩衰减 LLM 训练常用 0.95;稀疏梯度场景保持 0.999
$\epsilon$ 1e-8 数值稳定项 梯度极小时可增大至 1e-6
$\lambda$(AdamW) 0.01~0.1 权重衰减强度 大模型常用 0.1;BERT/GPT 类用 0.01

9. 常见陷阱与最佳实践

9.1 训练不稳定时的排查顺序

当训练出现 loss 震荡、NaN 或收敛停滞时,按以下优先级排查:

  1. 降低学习率:过大的 lr 是最常见原因,先尝试缩小 10 倍
  2. 加梯度裁剪:防止梯度爆炸,clip_grad_norm_max_norm 通常设 1.0
  3. **降低 $\beta_2$**:从 0.999 降至 0.95,让优化器对近期梯度变化更敏感

LLM / Transformer 推荐配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import torch

optimizer = torch.optim.AdamW(
model.parameters(),
lr=3e-4, # 初始 lr,配合 warmup 使用
betas=(0.9, 0.95), # β₂ 取 0.95,对近期梯度更敏感
eps=1e-8,
weight_decay=0.1 # 较强的正则化
)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=total_steps)

for batch in dataloader:
optimizer.zero_grad()
loss = criterion(model(batch.x), batch.y)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # 梯度裁剪
optimizer.step()
scheduler.step()

9.2 何时不选 Adam

Adam 的自适应步长有时会找到较尖锐的局部最优,使泛化性能不如 SGD + 动量。

  • 追求极致泛化(如 ImageNet 刷 Top-1):SGD + Momentum 搭配 StepLR 仍有竞争力
  • 超大规模模型、显存受限:考虑 Adafactor(不存储完整 $v_t$,显存大幅降低)或 8-bit Adam(对优化器状态量化,显存减少约 75%,性能几乎无损)

📌 关键点:Adam 的自适应性是双刃剑——它让训练更容易,但有时会以牺牲泛化为代价。追求极致性能时,值得对比 SGD + Momentum。


📝 总结

优化器的发展脉络可以用一句话概括:从统一步长到自适应步长,从纯梯度方向到动量加速,每一代改进都针对上一代的具体缺陷

关键要点回顾:

  • SGD:最简单,但需要精心调学习率,在 CV 任务中搭配 Momentum 和 Scheduler 仍是竞争力很强的选择
  • AdaGrad → RMSProp:引入自适应学习率,RMSProp 用 EMA 解决了 AdaGrad 学习率归零的问题
  • Adam:动量 + 自适应学习率,加上偏差修正,是工程上最常用的默认优化器
  • AdamW:解耦权重衰减(L2 ≠ 权重衰减),Transformer 训练的最优选择
  • Lion:只用符号梯度,显存更省,适合超大模型

实践建议:

  • 快速实验用 Adam(lr=1e-3)
  • Transformer 训练用 AdamW(lr=3e-4,betas=(0.9, 0.95),weight_decay=0.1)+ Warmup + Cosine Decay
  • CV 分类刷指标用 SGD + Momentum(lr=0.1)+ StepLR
  • 训练不稳定 → 先降 lr,再加梯度裁剪,详见第 9 节

🎯 自我检验清单

  • 能推导梯度下降的参数更新公式,解释学习率过大/过小的后果
  • 能解释 SGD Momentum 如何抑制震荡、加速收敛,并写出对应的更新公式
  • 能说明 AdaGrad 为什么会导致学习率归零,RMSProp 如何用 EMA 修复这一问题
  • 能写出 Adam 的完整更新步骤(包括偏差修正),并解释为什么需要偏差修正
  • 能解释 Adam 中三个组件($m_t$、$v_t$、偏差修正)各自的直觉作用
  • 能说明 L2 正则化在 Adam 中为何与真正的权重衰减不等价,以及 AdamW 如何修复这一问题
  • 能在 PyTorch 中配置 AdamW + CosineAnnealingLR + 梯度裁剪,并在训练循环中正确调用
  • 能根据任务类型(CV / LLM / RNN)给出合理的优化器和超参数选择建议
  • 能列举 Adam 不适合的场景,并说明 Adafactor 或 8-bit Adam 的使用动机

📚 参考资料