只要做过几天深度学习你一定对这样一行代码再熟悉不过optimizer optim.adam(model_params, lrcur_lr)它是 PyTorch 教程里出镜率最高的语句之一也是无数论文复现实验的起点。说 Adam 是整个深度学习生态里最“大”的默认项一点不过分——从图像分类到语言模型从 GAN 到强化学习到处都能看到它的身影。但“大”和“美”从来是两回事这些年关于“Adam 是不是被高估了”“自适应学习率到底有没有让泛化变差”的争论从来没停过。这篇东西我就围绕 optimizer 这个话题把 Adam 的来龙去脉、数学机制、实际训练中的坑、以及选型和调参经验一次讲透。适合正在复现论文、训练自己的模型、或者在优化器选型上纠结的读者。看完你至少能回答一个问题下一个项目里我到底该不该继续写optim.adam(...)。1. 为什么满世界都是 optim.adam从 SGD 到自适应学习率的演化逻辑1.1 在 Adam 之前被学习率折磨的日子很多人现在学深度学习一上来就用 Adam根本不知道在它出现之前训练一个模型有多折腾。经典的 SGD 更新极其简单θ - lr * g就是沿着梯度方向走一步。但这个简单更新有个致命问题梯度在不同维度上的尺度差异可能非常夸张。拿一个稀疏特征举例一个出现频率很高的特征它的梯度可能非常稳定而一个只在很少样本里出现的特征梯度要么是 0要么是很大的值。用同一个学习率去更新这两个维度结果一定是一边没吃饱、一边撑得慌。更麻烦的是损失函数的“地形”在不同方向上是弯曲程度不同的——有的方向陡峭有的方向平缓。学习率太小陡峭方向走得慢学习率太大又在峡谷两侧反复横跳。所以经典 SGD 时代调 lr 是训练里最核心也最痛苦的工作要靠经验、靠肉眼观察 loss 曲线甚至靠玄学。Momentum动量的出现解决了方向漂移和局部震荡的问题但它本质上是把历史梯度“平均”了一下仍然没有解决每个参数该用多大步长的问题。1.2 自适应学习率的三次关键演进真正让“每个参数拥有独立学习率”这个想法落地的是三个里程碑式的工作。第一个是 AdaGrad。它统计每个参数历史梯度的平方和更新的时候用它去缩放学习率。历史梯度大的方向学习率自动变小历史梯度小的方向学习率相对变大。这个思路在当时很惊艳但缺陷同样明显平方和一直累加不衰减导致学习率单调下降训练到中后期几乎就学不动了。第二个是 RMSProp。它把 AdaGrad 的“累加全部历史平方梯度”改成“滑动平均”用指数加权移动平均来估计二阶动量。这样学习率不会一路衰减到 0能持续保持自适应能力。RMSProp 已经相当能打了但它没有引入动量方向上的抖动仍然存在。第三个就是 Adam。它把 RMSProp 的二阶动量估计和 Momentum 的一阶动量估计合到了一起同时做了偏差校正。也就是说Adam 既知道“历史梯度的平均方向”也知道“每个方向上梯度的剧烈程度”然后两者结合给每个参数算出一个既平滑又自适应大小的更新量。1.3 Adam 的三个“省心”点决定了它的统治地位Adam 能成为事实默认靠的不是什么高深理论而是三个非常实际的优点。第一它对学习率的敏感度大幅降低。因为每个维度的更新量都被二阶动量做了归一化lr 从 1e-3 变成 3e-3通常不会导致训练直接爆炸这在 SGD 时代是不可想象的。第二收敛速度确实快。尤其对于 NLP、Transformer 这类模型Adam 在前期和中期能把 loss 压得非常快省下的训练时间非常可观——对做实验的人来说时间就是命。第三默认超参数很能打。Kingma 和 Ba 在论文里给出的默认值betas(0.9, 0.999)、eps1e-8配合lr1e-3在大多数任务上都能跑出一个不算差的结果。这个“开箱即用的鲁棒性”在工业界太重要了大家自然愿意选它。所以你去看今天的代码Adam 只花了一行就取代了当年调参工程师大半个工作日的工作量。说它是“大”优化器名副其实。2. Adam 的核心机制拆解动量、二阶动量与偏差校正到底在做什么2.1 先建立一个直觉下山的两种策略理解 Adam 最好用的方法是想象你在雾天里下山。你看不到全貌只能靠脚底的坡度判断方向。经典 SGD 像是一个只会看脚下的人哪里最陡就往哪里迈一步步子大小固定。遇到一个坑他可能一头扎进去出不来了。Momentum 像是给这个人加了“惯性”如果前面一段路一直在向下他会保持一个下山的方向遇到小坑也能借着惯性冲出去。这就是一阶动量——记录“历史梯度的指数平均”抑制方向抖动。RMSProp 的思路完全不同——它像是一个能感受“地形平缓程度”的人。走在一个坡度变化剧烈的乱石坡上时他每步都小心地迈小步走在平缓的草地上时他敢大步流星。这就是二阶动量——记录“历史梯度平方的指数平均”估计每个方向上地形的陡峭程度。Adam 就是给一个同时拥有“惯性”和“地形感知”的人。它用一阶动量定方向用二阶动量定步长。2.2 公式逐项拆解Adam 的完整更新过程可以分成三部分。先把一阶动量服务好m_t β1 * m_{t-1} (1 - β1) * g_t这就是 Momentum 的指数滑动平均。β1 一般取 0.9表示当前梯度在历史动量里只占 10% 的权重大部分来自历史方向。然后估计二阶动量v_t β2 * v_{t-1} (1 - β2) * g_t^2β2 一般取 0.999滑动窗口更长对梯度尺度的估计更稳定。它对元素级梯度做了平方所以能反映每个维度上梯度振幅的大小。接着是偏差校正这一步很多讲解会跳过但它恰恰是 Adam 初始阶段不“翻车”的关键m_hat_t m_t / (1 - β1^t) v_hat_t v_t / (1 - β2^t)为什么要校正因为 m 和 v 的初始值都是 0。在第一步的时候m_1 0.9 * 0 0.1 * g_1 0.1 * g_1比真实梯度小了 10 倍v_1 0.001 * g_1^2更是小得离谱。如果不做校正第一步更新就会被一个严重偏小的二阶动量放大成极大的步长训练直接就炸了。1 - β^t这个分母在 t 很小时远小于 1乘上去相当于把偏小的估计拉回真实量级。随着 t 增大分母逐渐趋近于 1校正的影响就慢慢消失了。最后一步合体更新θ_t θ_{t-1} - lr * m_hat_t / (sqrt(v_hat_t) ε)分子是平滑后的方向分母是梯度的均方根。结果就是梯度历史平均很大的维度步长被压小梯度历史平均很小的维度步长被放大。ε 的作用是防止分母为 0一般取 1e-8。2.3 从公式看行为为什么它对 lr 不敏感从这个更新式你能直观看到一个特性更新量的量级主要由m_hat / sqrt(v_hat)决定而不是由梯度本身的绝对大小决定。这个比值本质上是一个无量纲化的东西大致反映了“梯度的信噪比”——如果一阶动量比二阶动量的平方根大说明梯度方向一致可以放心多走一点如果两者差不多说明梯度方向很不稳定要走谨慎些。这也是为什么 Adam 对学习率的敏感度远低于 SGD它已经自动把每个维度的梯度尺度归一化到了大致相同的范围。你只需要给定一个全局步长剩下的尺度问题让适配机制去解决。但同样因为这种归一化Adam 步长的缩小只能靠外部的学习率调度它自己不会像 SGD 那样自动衰减——这一点是理解它所有“坏毛病”的起点后面讲的很多坑都源于此。3. 美丽的玫瑰有刺Adam 在实际训练里的四个大坑3.1 泛化差与 Sharp Minima 之争Adam 在训练集上收敛快但很多人发现它在验证集上的表现不如 SGD——尤其在图像分类这样的大任务上差距可以相当明显。这个问题的主流解释和“Sharp Minima / Flat Minima”有关。SGD 因为梯度噪声大倾向于在损失曲面里“逛”到比较平坦的极小值区域而平坦极小值的泛化性通常更好因为参数的微小扰动不会让 loss 剧烈变化。Adam 因为做了梯度归一化收敛过程更“顺滑”反而容易陷进尖锐的极小值——在训练集上是极小值但换到验证集一个扰动就崩了。不过这几年也有人在为 Adam 翻案如果给 Adam 配一个足够长的训练时间和精心设计的学习率调度它和 SGD 之间的泛化差距其实会大幅缩小。也就是说差距可能不完全是优化器的问题而是“默认配置下 Adam 只训了很短时间就停下了”的问题。但这个争论侧面说明了一件事Adam 不是免费的午餐它把泛化风险藏在了“快”的背后。3.2 收敛后期的震荡它的步长不会自己缩小我在第二章节末尾特意提了一句Adam 的步长由信噪比决定梯度尺度本身不直接参与。这就带来一个现象——训练后期当模型已经接近最优解时梯度虽然整体变小了但每个维度上的m_hat / sqrt(v_hat)比值不一定跟着变小。噪杂的小梯度只要方向一致依旧会产生不小的更新量于是 loss 在最优值附近来回震荡迟迟不收敛。我见过不少新手拿着 Adam 默认参数一口气训了几百个 epochloss 从中期开始就一直在小范围抖动他们以为是模型容量或数据问题实际上只是学习率压根没衰减。解决办法不是没有核心思路是给外部学习率加调度。StepLR 可以应付简单任务但更推荐用 CosineAnnealingLR它让学习率在训练周期内按照余弦曲线平滑地从初始值下降到接近 0训练后期步长自然变小loss 就稳稳地落在最优区域里。3.3 对 Weight Decay 的处理不正确AdamW 为什么更好这是 Adam 最隐蔽的一个缺陷也是 AdamW 出现的原因。经典 SGD 里L2 正则化在 loss 上加权重的平方和和 weight decay每一步把权重乘一个小于 1 的系数是等价的。但在 Adam 里这个等价性被打破了。因为 Adam 的更新会先算梯度再把 L2 正则产生的梯度放到二阶动量里做归一化——一个大权重方向的 L2 梯度会被自适应缩放正则在“惩罚大权重”这件事上的力度就不稳定了。Ilya Loshchilov 和 Frank Hutter 在 2017 年提出 AdamW主张把 weight decay 从梯度的计算里解耦出来权重更新的时候直接减去一个和梯度无关的固定比例项。这样一个大权重不会被二阶动量的缩放“保护”住正则化效果更稳定实际训练里泛化也更好。现在训练 Transformer 类模型AdamW 基本是绝对主流默认配置已经很少有人在用原版 Adam 了。从 PyTorch 代码的角度说就是下面两行的区别optimizer optim.AdamW(model.parameters(), lr1e-3, weight_decay0.01) # 而不是 optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay0.01)3.4 混合精度训练下的数值地狱eps 太小会出大事最后一个坑来自数值计算。Adam 默认eps1e-8这个值在 fp32 精度下没什么问题但一旦切到混合精度训练fp16/bf16问题就来了。混合精度下梯度会被缩放梯度平方后数值可能小到超出 fp16 的表示范围直接变成 0。如果二阶动量 v 下溢为 0更新公式里的分母就是sqrt(0) epsilon整个更新就变成一个巨大的值——通常表现为 loss 突然变成 NaN 或者模型权重爆炸。我自己在训练一个中小型 BERT 类模型时就踩过这个坑排查到最后不是数据问题、不是学习率问题就是eps太小。把eps从1e-8调到1e-6之后训练立刻稳了下来。这个经验后来成了我的固定配置只要开了 AMPAdam 家族优化器的eps我都至少放到1e-6。4. 优化器选型对比SGD、Adam、AdamW、LAMB 各自的主场4.1 一张表看明白主流优化器的差异优化器核心机制典型场景lr 敏感性显存开销SGD Momentum固定步长 动量方向CV 大规模分类、需要极致泛化高需要精心调度低只多一份动量Adam一阶动量 二阶动量 偏差校正NLP、GAN、RL、快速迭代验证低默认 1e-3 可跑高多两份状态AdamWAdam 的解耦 weight decayTransformer、预训练语言模型低高LAMBLayer-wise 自适应大 batch 训练超大规模预训练、万级 batch中高AdaFactor矩阵分解近似二阶动量显存受限的 Transformer中极低显存开销这一列可以展开说一下。Adam 需要为每个参数维护 m 和 v 两份状态如果模型有 1 亿参数光优化器状态就用掉 2 乘以 1 亿乘以 4 字节等于额外的 800 MB这个开销对很多实验环境来说并不小。SGD 只有一份动量AdaFactor 通过低秩分解把状态压缩到接近 O(n) 量级这两者的显存优势在超大模型上非常明显。4.2 我的选型决策流程把项目拿来我通常会按下面这套流程决定用哪个优化器如果任务在 CV 领域、数据集干净、训练周期充裕而且我非常在意最终泛化精度那我会选 SGD Momentum配上 CosineAnnealing 和合适的长训练周期。这个组合在 ImageNet 这类任务上至今仍是性能标杆之一。如果任务是 NLP、Transformer、GAN、扩散模型或者强化学习选 AdamW。它收敛快、对超参数宽容同时在 weight decay 上比原版 Adam 更合理。很多开源代码里写的“Adam”实际也是指 AdamW这一点看具体实现。如果模型大到了单卡显存被优化器状态吃干净、连一个 batch 都塞不进去我会考虑 AdaFactor。它能省下一大截显存代价是收敛速度稍慢、行为不如 Adam 稳定需要多花点精力调。如果是在超大 batch size例如 4096 甚至更大下做预训练LAMB 的价值就体现出来了。它对每一层单独计算学习率的缩放能在超大 batch 下保持稳定的更新量让大规模并行真正“吃满”。4.3 几个容易被忽略的判断依据除了任务类型还有两个细节会影响选型。第一个是梯度稀疏度。如果模型里有大量 embeddings 或者稀疏特征Adam 系列利用二阶动量对稀疏维度自动放大的能力是压倒性的优势这时不要犹豫。第二个是对训练中断的容忍度。SGD 的训练曲线通常更平滑中途停了拿到的 checkpoint 还能继续用Adam 前期快、中期容易出现看起来像“过拟合”的抖动其实只是调度问题。如果你需要的是一个“挂上去就可以不管”的训练任务Adam 系配合 warmup 衰减调度反而是更省心的选择因为它的 lr 一不小心设大了也不至于像 SGD 那样直接 NAN。5. 我的实战配置手册让 Adam 系列优化器发挥出真正实力5.1 学习率区间与线性缩放规则拿到一个新任务我从来不会直接沿用“经典论文里的 lr”。我的习惯是先在固定步数内做一个小规模 lr 扫描在[1e-4, 3e-4, 1e-3, 3e-3]四个值里各跑几百步看训练 loss 的下降斜率。loss 下降太慢说明 lr 偏小loss 震荡上升说明 lr 偏大总有一两个值能给你一个平稳下降的曲线。有一个值得时刻记住的规则是当你把 batch size 翻倍时lr 也应该相应翻倍。这里的直觉是——batch size 翻倍后每个 step 的梯度噪声更小方向估计更准理应有信心走出更大的步子。但注意lr 放大之后warmup 的步数也要跟着拉长否则初始阶段大 lr 加二阶动量未稳定很容易直接起飞。5.2 Betas、eps、weight decay 的具体调节建议betas参数大多数情况下用默认的(0.9, 0.999)就足够。不过有一个值得掌握的技巧如果你想在长训练后半段更“细致地”微调可以把β2从 0.999 减小到 0.995 或 0.99。β2 决定二阶动量对历史梯度的记忆长度调小它意味着把历史窗口缩短让优化器更快地对最近的梯度尺度变化作出反应。转录类模型的训练中我发现β20.995配合 cosine 调度能稍微改善收敛速度。反过来在梯度信噪比很低的 GAN 训练场景把β2调大到 0.9999 可以让训练更稳。eps的经验值是fp32 下用默认1e-8没什么问题但做了混合精度或者使用 bf16建议直接改成1e-6或1e-7。你的模型规模很大、梯度分布很窄时即使 fp32也可以把 eps 调到1e-6代价是几乎可以忽略的精度损失却能显著提升数值稳定性。weight_decay在 AdamW 里的合理区间大约是[0.001, 0.1]。我一般从小模型直接上 0.01大模型如果遇到 loss 不平滑则往下调到 0.001。我的经验是权重衰减调大了模型会欠拟合调小了过拟合部分会很严重0.01 对大多数 Transformer 结构来说是个不错的中位数起点。5.3 如何判断优化器工作正常两个关键监控指标很多人只看 loss 曲线但优化器的运行状态有几个更深层的信号更值得看。第一个是梯度范数grad_norm。训练前期它应该保持在一个不算小的量级并缓缓下降如果它快速崩到 0说明梯度消失如果它突然冲高说明 lr 太大或数据里有异常样本——不要等到 loss 变成 NaN 才反应过来。第二个是更新范数与参数范数的比值。从训练过程里取一步更新量update optimizer.param_groups[0][lr] * m_hat / (sqrt(v_hat) eps)算它的范数再和对应参数范数比较。一个经验法则是这个比值应该稳定在1e-3到1e-2之间。如果远超这个范围说明更新量太大参数在剧烈跳动如果远小于这个范围说明优化器没有有效推进lr 可能太小或者梯度出了问题。这两个指标怎么实际接入 PyTorch可以在每一步反向传播后加上total_grad_norm 0.0 for p in model.parameters(): if p.grad is not None: total_grad_norm p.grad.detach().norm().item() ** 2 total_grad_norm total_grad_norm ** 0.5 total_update_norm 0.0 for p in model.parameters(): if p.grad is not None: step optimizer.param_groups[0][lr] * p.grad / (p.grad.norm() 1e-8) total_update_norm step.norm().item() ** 2 total_update_norm total_update_norm ** 0.5我推荐把这两行做成一个训练日志的小工具每个 step 都输出一次。训练中途瞄一眼比盯着 playing loss 曲线盲猜靠谱得多。5.4 一个可以直接套用的完整训练配置示例最后给一个我在中小型 GPT 类模型上常用的完整配置作为本文的落地参考import torch from torch.optim import AdamW from torch.optim.lr_scheduler import OneCycleLR optimizer AdamW( model.parameters(), lr2e-4, # 小型数据集建议从 2e-4 起步大语料可以往上走 betas(0.9, 0.995), eps1e-6, # 混合精度训练必须调大 weight_decay0.01, ) total_steps len(train_loader) * epochs scheduler OneCycleLR( optimizer, max_lr2e-4, total_stepstotal_steps, pct_start0.05, # 前 5% 步数做 warmup anneal_strategycos, )这个配置的意图很明确AdamW 保证 weight decay 解耦eps1e-6保证混合精度下的数值安全β20.995让二阶动量对训练中期变化更敏感OneCycleLR 先快速上升到峰值再余弦下降到接近 0——前 5% 的 warmup 让二阶动量从零开始有个缓冲期后半段的余弦衰减省去手动调衰减步数的烦恼。我自己踩过几次坑之后现在的默认工作流基本固定成了这样先确认精度模式和显存预算再选优化器用上述的更新范数指标验证 lr 方向最后在上线大训练任务前跑一个小规模 lr 扫描确认没有“隐藏的爆炸点”。这套流程说不上惊艳但它让我少了很多陪 NaN 到深夜的日子。优化器这个东西从来没有真正“大而美”的银弹。Adam 系列用它的易用性换走了你的调参痛苦但也把泛化、稳定性和数值安全的账单藏在了一个optim.adam(...)下面。真正靠谱的做法是每次写这行代码的时候都多想一层我的模型结构是什么我的精度设置是什么我的训练调度是什么。想清楚这三件事那个写着optim.adam(model_params, lrcur_lr)的“大块头”才真正有可能成为你项目里那个又大又美的存在。