30

Adam——自适应学习率

阅读约 5 分钟第 30/60 课

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 修复了这个问题。