跳转到内容

Adam — 让深度学习自己挑步长的优化器

是什么

Adam 是一种给神经网络挑”每一步走多远”的算法。日常类比:你在大雾里下山,普通办法是每步固定 1 米;Adam 的办法是——走过的方向最近一直一致就大胆迈两米,方向忽左忽右就小心半米。每个参数都有自己的”步长尺”,自动调。

写一行 PyTorch:

optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

就这一行,让 2015 年到 2026 年这十年里90% 以上的深度学习训练有了默认起点。从 BERT 到 GPT、从 ResNet 到 Stable Diffusion,第一版基本都是 Adam 跑出来的。

为什么重要

不理解 Adam,下面这些事都没法解释:

  • 为什么大家训练神经网络几乎不用调学习率,开 lr=1e-3 就能跑——Adam 自己在每个参数上做二次调整
  • 为什么 SGD 训练要精心设计 learning rate schedule,Adam 不太用——它自己有”自适应”机制
  • 为什么 LLM 时代出现了 AdamW、Adafactor、Lion 这些”Adam 改”——大家都在踩 Adam 的肩膀
  • 为什么这一篇 ICLR 2015 论文成了机器学习史上被引最多的论文之一(>17 万次)

核心要点

Adam 把两个老想法揉在一起,再加一个补丁:

  1. 动量(Momentum):记一个”梯度的滑动平均” m_t。类比——下山时记住”最近一直在往东南走”,下一步带点惯性。这个想法 1964 年就有了。

  2. RMSprop(自适应步长):记一个”梯度平方的滑动平均” v_t。类比——某个参数最近梯度一直很大(剧烈晃动),就把它的步长缩小;某个参数梯度一直很小(平地),就放大步长。这个想法 Hinton 2012 课上提的。

  3. 偏差修正(bias correction)m_tv_t 初始化为 0,前几步会”偏向 0”。Adam 用 m̂_t = m_t / (1-β1^t) 把它扳正。这是 Kingma & Ba 的原创补丁,让 Adam 在训练初期也稳。

最终更新公式(看不懂没关系,记结构就行):

m_t = β1 · m_{t-1} + (1-β1) · g_t # 梯度的一阶矩(方向)
v_t = β2 · v_{t-1} + (1-β2) · g_t² # 梯度的二阶矩(震荡幅度)
m̂_t = m_t / (1 - β1^t) # 偏差修正
v̂_t = v_t / (1 - β2^t)
θ_{t+1} = θ_t - α · m̂_t / (√v̂_t + ε) # 用方向除以震荡幅度,再走 α 步

默认超参数:α=0.001, β1=0.9, β2=0.999, ε=1e-8。这组数字几乎不用调——这是 Adam 火爆十年的根本原因。

实践案例

案例 1:训练一个分类网络

import torch
model = MyNet()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
loss_fn = torch.nn.CrossEntropyLoss()
for x, y in dataloader:
optimizer.zero_grad()
loss = loss_fn(model(x), y)
loss.backward()
optimizer.step()

注意:你没设置 momentum、没设置 schedule、没调 weight decay。SGD 能不能跑成?也能。但 SGD 你大概率要花 1 小时调 lr 和 momentum,Adam 跑通就开训。

案例 2:Adam 在不同参数上的”个性化”步长

设想一个网络有两个参数:

  • w1:梯度一直在 [−0.01, 0.01] 抖(小且震荡)
  • w2:梯度一直 ≈ 1.0(大且稳定)

Adam 记录的 v_t(梯度平方):

  • w1 的 v_t ≈ 0.0001√v_t ≈ 0.01 → 实际步长 ≈ α / 0.01 = 100α(放大)
  • w2 的 v_t ≈ 1.0√v_t ≈ 1.0 → 实际步长 ≈ α / 1.0 = α(不变)

同一个 lr,对不同参数实际步长差 100 倍。这就是”自适应”的魔力——稀疏梯度的参数(如 embedding 表)自动得到更大的步长。

案例 3:偏差修正解决了什么

第 1 步时,m_1 = 0.1 · g_1(如果 β1=0.9),是真实梯度的 1/10。如果不修正,第一步走得太短,训练初期慢半拍。

修正:m̂_1 = m_1 / (1 - 0.9^1) = m_1 / 0.1 = g_1回到了正常量级。Kingma & Ba 在论文 Figure 1 的对比图里展示了:去掉 bias correction,前 100 步明显发飘。

踩过的坑

  1. Adam 在某些任务上比 SGD 泛化差:图像分类(CIFAR/ImageNet)用 Adam 训出来的模型在测试集上常常比 SGD+momentum 差 1-2%。Wilson et al. 2017 写过专门论文吐槽。原因仍有争议,工业界做法是:前期 Adam 快收敛,后期切回 SGD 提泛化

  2. Adam 的 weight decay 实现是错的:原版 Adam 把 weight decay 当 L2 正则混进梯度,这会被 √v̂_t 缩放,等于”对大梯度参数惩罚弱、小梯度参数惩罚强”——和直觉相反。Loshchilov & Hutter 2019 提出 AdamW 修正:weight decay 直接从 θ 上扣,不进梯度。今天训 LLM 默认都是 AdamW,不是 Adam

  3. β2 在长训练里会出问题:β2=0.999 意味着 v_t 大约平均最近 1000 步。训 10 万步、100 万步时,早期那个大梯度还在 v_t 里慢慢衰减,可能让某些参数永远走不动。Reddi et al. 2018 的 AMSGrad 提了个修复,但实践用得不多。

  4. 大 batch + Adam ≠ 自动收敛快:很多人以为 Adam 能解决一切。LLM 训练里,batch size 一大,Adam 也得调 warmup、调 lr schedule、调 β2。“无脑用默认参数”只在小规模时成立

适用 vs 不适用场景

适用

  • 默认起点——任何深度学习任务,先用 Adam (或 AdamW) 跑通
  • 稀疏梯度场景(NLP embedding、推荐系统)——自适应步长帮大忙
  • 调参预算少、想快速试模型架构的研究/实习场景
  • 大模型训练前期(再切 SGD 或继续 AdamW)

不适用

  • 计算机视觉的最终模型——SGD+momentum+cosine schedule 至今 SOTA
  • 强化学习——梯度噪声特性不同,常用 RMSprop 或专门优化器
  • 简单凸优化——SGD 收敛保证更好,Adam 反而绕路
  • 内存极受限场景——Adam 要存 m 和 v 两个 state,是参数量的 2 倍。Adafactor / 8-bit Adam 是替代

历史小故事(可跳过)

  • 2011 年:John Duchi 等人提出 AdaGrad——第一个自适应步长方法,但 v_t 一直累加,越走越慢直至停下。
  • 2012 年:Hinton 在 Coursera 课上口头讲 RMSprop——用滑动平均替代累加,解决 AdaGrad 慢死的问题。没发论文
  • 2014 年 12 月:Kingma(当时博士生)和 Ba 把 RMSprop + Momentum + 偏差修正拼起来,投稿 ICLR 2015。论文 9 页,算法只占半页伪代码
  • 2015-2017:从 Word2Vec 到 ResNet,从 GAN 到 Transformer,几乎所有里程碑模型的论文里都有”我们用 Adam 训练”。
  • 2019 年:AdamW 被 PyTorch 默认收录,逐步取代 Adam 成为新基线。

Kingma 后来还共同发明了 VAE(变分自编码器),是 OpenAI 早期员工。

学到什么

  1. 简单的工程组合能产生巨大影响——Adam 没什么深奥数学,就是把已有想法(动量、自适应、修正)拼一起。但拼对了,影响整个领域 10 年。
  2. 默认值的力量——lr=1e-3, β1=0.9, β2=0.999 这组数字让 Adam 几乎免调参。一个算法被采用的速度,常常取决于它对小白多友好。
  3. “自适应”不是魔法——Adam 自适应的是”每个参数的步长尺”,不是”学习率本身”。lr 还是要调,schedule 还是要做,只是宽容度比 SGD 大。
  4. 早期顶会论文短而清晰——Adam 9 页里把动机、算法、收敛证明、实验全讲完了。今天动辄 30 页 supplementary 的论文应该反思。

延伸阅读

关联

  • pytorch —— PyTorch 默认优化器之一
  • jax —— JAX/optax 也内置 Adam
  • keras —— Keras 默认 optimizer='adam'
  • tensorflow-osdi-2016 —— TF 早期就把 Adam 作为一等公民

反向链接

  • adafactor-2018 —— Adafactor — 把 Adam 的优化器内存从 O(d) 压到 O(√d)
  • adamw-2017 —— AdamW — 把 weight decay 从梯度里拆出来
  • batchnorm-2015 —— Batch Normalization — 把每层激活值规整到 0 均值 1 方差,深网训练时间砍成 1/14
  • double-descent-2019 —— Double Descent — 模型越大越准,过参数化时代的反常识曲线
  • dropout-2014 —— Dropout — 训练时随机关掉一半神经元,反而学得更好
  • gcn-2017 —— GCN 2017 — 把卷积搬到图结构上的最简版本
  • goodfellow-fgsm-2014 —— FGSM — 用一行梯度让神经网络看错图片
  • graphsage-2017 —— GraphSAGE 2017 — 给没见过的节点也能算嵌入
  • grokking-2022 —— Grokking — 训练 loss 早归零,几千步后才突然学会
  • jax —— JAX — Google 函数式数值计算
  • label-smoothing-2016 —— Label Smoothing — 别让模型对正确答案过度自信
  • layernorm-2016 —— Layer Normalization — 把归一化方向从 batch 转到 feature,让 RNN/Transformer 也能稳定训
  • lion-2023 —— Lion — 让程序自己搜出来的优化器,比 AdamW 内存少一半
  • lottery-ticket-2019 —— 彩票假设 — 大网里藏着一张能独立训出来的小网
  • maml-2017 —— MAML — 学一个”好起点”,几步就能学会新任务
  • mixup-2018 —— mixup — 把两张图按比例叠成一张,标签也一起叠
  • mode-connectivity-2018 —— Mode Connectivity — 神经网络的两个最优解之间有低洼走廊
  • ntk-2018 —— NTK — 把无限宽的神经网络变成一个可解的核方法
  • pytorch —— PyTorch — 深度学习主流框架
  • sophia-2023 —— Sophia — 让二阶优化器第一次在 LLM 预训练里跑得动
  • szegedy-adversarial-2013 —— Szegedy 对抗样本 2013 — 一张图片骗过神经网络的开山之作
  • td3-2018 —— TD3 — 给 DDPG 装两副刹车,连续控制终于稳了
  • tensorflow-osdi-2016 —— TensorFlow — 把神经网络拆成数据流图再跑到任何机器上