Momentum 解决了方向不稳的问题,但所有参数仍然共用一个学习率。Adam(Adaptive Moment Estimation)在 Momentum 的基础上,给每个参数自动调整学习率。
核心想法
有些参数的梯度一直很大(loss 曲面很陡),有些参数的梯度一直很小(曲面很平)。直觉上:
- 梯度一直很大的参数:步子应该小一些——因为曲面陡峭,大步容易跳过最优点。
- 梯度一直很小的参数:步子应该大一些——因为曲面平坦,小步会走得极慢。
Adam 通过跟踪每个参数梯度的历史大小来实现这一点。
Adam 维护两个统计量
对每个参数,Adam 维护两个数字(不是一个):
m(一阶动量):梯度的指数移动平均——类似于 Momentum 的 v,跟踪梯度的方向和大小。
m_new = β₁ × m_old + (1 − β₁) × 梯度 (β₁ = 0.9)
v(二阶动量):梯度平方的指数移动平均——跟踪梯度的振幅有多大。
v_new = β₂ × v_old + (1 − β₂) × 梯度² (β₂ = 0.999)
更新参数时,用 m 除以 √v:
参数_new = 参数_old − 学习率 × m / (√v + ε)
ε 是一个极小的数(比如 10⁻⁸),防止除以零。
关键在分母 √v。对于梯度一直很大的参数,v 大,√v 大,更新量被压小——不会在陡峭曲面上跳过头。对于梯度一直很小的参数,v 小,√v 小,更新量被放大——在平坦曲面上也能走起来。
用数字看自适应效果
假设有两个参数 A 和 B,学习率 = 0.001:
| 参数 A | 参数 B | |
|---|---|---|
| 每步梯度 | ≈ 10.0 | ≈ 0.01 |
| v(梯度²的平均) | ≈ 100 | ≈ 0.0001 |
| √v | ≈ 10 | ≈ 0.01 |
| 有效学习率 = lr / √v | 0.001 / 10 = 0.0001 | 0.001 / 0.01 = 0.1 |
| 实际更新量 ≈ lr × m / √v | ≈ 0.001 | ≈ 0.001 |
两个参数的实际更新量被拉到了同一个量级——尽管它们的原始梯度差了 1,000 倍。Adam 自动为每个参数匹配了合适的步长。
这是 SGD 做不到的。SGD 用 0.001 的学习率:参数 A 的更新量是 0.001 × 10 = 0.01(可能太大),参数 B 的更新量是 0.001 × 0.01 = 0.00001(可能太小)。
偏差修正
Adam 还有一个细节:偏差修正(bias correction)。
m 和 v 的初始值都是零。训练刚开始时,它们还没有看到足够的数据来给出准确的估计——m 和 v 的值会偏小(被初始的零拉低了)。
偏差修正在前几步对 m 和 v 做放大,弥补初始值为零的影响:
m̂ = m / (1 − β₁ᵗ) ← t 是当前步数
v̂ = v / (1 − β₂ᵗ)
第 1 步时,1 − β₁¹ = 0.1,m 被放大 10 倍。第 10 步时,1 − β₁¹⁰ ≈ 0.65,放大约 1.5 倍。几十步之后,修正系数接近 1,基本不再有影响。
这个修正让 Adam 在训练最初几步就能给出合理的更新量,而不用等到 m 和 v 积累到稳定值。
内存开销
Adam 为每个参数维护 m 和 v 两个统计量。Sophia 有十亿个参数,m 和 v 各占十亿个数字。加上参数本身,Adam 的内存占用是参数量的三倍。
用 FP32 存储:10 亿参数 × 4 字节 = 4 GB。m 和 v 各 4 GB。总计 12 GB。这还不算梯度本身(再加 4 GB)。优化器状态是单卡训练中显存的最大开销之一——第 40 章节会详细算这笔账。
为什么 Adam 几乎成了默认选择
Adam 结合了 Momentum(稳定方向)和自适应学习率(每个参数自动调速),在大多数深度学习任务上都能开箱即用——不需要精心调整学习率,就能获得不错的训练效果。
从 2014 年被提出以来,Adam 成了深度学习训练的事实标准。几乎所有的语言模型预训练都使用 Adam 或它的变体。
但 Adam 有一个理论上的问题:它把权重衰减(regularization)和梯度更新混在了一起。下一章节的 AdamW 修复了这个问题。