ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Adam优化器原理详解:从动量法到AdamW的进化之路

Adam优化器原理详解:从动量法到AdamW的进化之路 1. 先建立直觉优化器到底在干什么1.1 训练的底层逻辑就是下山训练任何神经网络本质上都在做同一件事把一个损失函数的值降下来。模型有一堆参数每个参数都对应当前位置的一个坐标损失函数的值就是这块地形的海拔。训练过程就是拿着梯度这张等高线地图每次往海拔下降最快的方向挪一步。Adam 优化器就是决定这一步怎么挪的那个算法它属于动量法和 Adam 等现代优化器里最出名的一个。最简单的下山方式是梯度下降θ θ - lr * g。lr 是学习率g 是梯度。这个公式看起来合理但放到真实训练里问题一大堆。过去十年优化器层出不穷本质都是在修补这个朴素方法的漏洞Adam 就是其中把补丁打得很漂亮的一个。这篇我会把 Adam 从里到外讲透公式每个符号在算什么、默认参数为什么是那几个数、和 AdamW 差在哪、以及实际训练里最常踩的坑。1.2 朴素梯度下降的三个软肋第一个软肋是学习率不好选。lr 太大loss 震荡甚至爆炸lr 太小训练慢到怀疑人生。而且同一个 lr 对不同的参数、同一个模型的不同训练阶段合适值完全不一样。一个模型里可能有几百层参数有卷积、有归一化、有 embedding梯度尺度横跨好几个数量级一个全局 lr 根本照顾不过来。第二个软肋是它不区分方向。梯度是向量某个参数方向的梯度可能只有 0.001另一个方向可能上千直接用同一个 lr 乘下去小梯度方向更新太少大梯度方向一步就飞。更麻烦的是很多模型的损失面是狭长峡谷形状一个方向极陡、垂直方向极平缓。普通梯度下降会在峡谷壁上反复横跳明明想往谷底走实际却一直在侧壁打转。第三个软肋是随机性。用 mini-batch 采样得到的梯度是有噪声的它只是全量梯度的一个估计。每次按当前 batch 的噪声梯度走步子会抖收敛也慢。后来大家一步步打补丁动量法解决噪声和峡谷震荡AdaGrad 和 RMSProp 解决不同参数的不同尺度Adam 把这两类思路合在一起又加了一个常被忽略但至关重要的偏差修正。1.3 动量法和 RMSProp 各干了什么动量法Momentum受物理启发想象一个小球滚下山坡它不只会被当前位置的梯度推着走还带着之前累积的速度。更新公式是v βv gθ - lr * vβ 通常取 0.9。加了动量以后梯度方向频繁变化的分量会相互抵消稳定一致的分量会累积加速所以能有效压制 batch 采样带来的噪声也能更快冲过鞍点。你大概可以理解成它给下山过程加了一个惯性缓冲。RMSProp 走的是另一条路它维护一个梯度平方的指数滑动平均 v每个参数的学习率都除以sqrt(v)。梯度经常很大的方向v 大步子自动变小梯度很小或很稳的方向v 小步子自动变大。这就是自适应学习率的雏形相当于它对每个方向的路况分别做了测量然后按路况调整步幅。Adam 的贡献在于把两者结合用梯度的一阶矩滑动平均值估计该往哪个方向走用二阶矩平方滑动平均值估计每个方向的梯度有多大、多不稳再对这两个估计做偏差修正。理解 Adam 的关键就是搞清楚这两个矩分别起到什么作用。2. Adam 的原理拆解两个动量加一个修正2.1 一阶动量 m梯度的滑动平均不是物理动量Adam 第一步是维护一个叫一阶动量的量记作 mm_t β1 * m_{t-1} (1 - β1) * g_t这个式子看着简单但有三个细节值得琢磨。第一个细节为什么系数用(1-β1)而不是直接写成m β1 * m g如果采用后一种写法在梯度平稳时 m 的稳态大小会约等于g / (1-β1) 10g相当于把梯度放大了 10 倍学习率也得跟着缩 10 倍才能稳住。用了(1-β1)之后m 的量级和梯度 g 基本一致默认学习率在不同任务之间才有可迁移性。很多初学者试图化简这个公式结果把优化器行为彻底搞歪这个系数不能乱动。第二个细节指数滑动平均本质上是一个指数衰减窗口。β10.9 时半衰期大约 7 步有效窗口差不多最近 10 步这一步的梯度占 10% 权重上一步占 9%再往前衰减到 8.1%依此类推。换句话说m 大致代表最近 10 步梯度的加权平均方向它同时做了两件事平滑噪声保留方向上的惯性。第三个细节m 在这里和经典物理动量并不完全等价。经典动量更新时速度 v 的尺度和梯度量级直接相关而 Adam 在后面会把 m 除以sqrt(v)梯度的绝对量级在最终更新里基本被消掉。所以如果只记一句话m 是方向和趋势的估计它本身不需要关心梯度量级到底有多大。这也是 Adam 对梯度爆炸更稳的深层原因。2.2 二阶动量 v每个参数方向的路况记录第二个量是二阶动量记作 vv_t β2 * v_{t-1} (1 - β2) * g_t²v 累积的是梯度平方的滑动平均永远是正数大致刻画每个参数方向上梯度的典型大小。这个设计直接继承自 RMSProp相当于给每个参数方向装了一个路况记录仪。为什么用平方而不是绝对值一个是可导、推导方便更关键的是平方能放大离群梯度的影响某个方向偶尔出现一个超大的梯度v 会被立刻抬高接下来一段时间这个方向的步长自动变小等于给训练加了一道安全阀。同时 β20.999 的用意也在这里得到了体现v 的有效窗口约为1/(1-0.999)1000步它统计的是最近近千步的平均路况对短时间的异常波动不太敏感状态更稳定。接下来是 Adam 的更新式θ_t θ_{t-1} - lr * m̂_t / (sqrt(v̂_t) ε)你可以把m/sqrt(v)整体理解成一个标准化后的梯度方向。m 的量级和梯度相近sqrt(v) 的量级也和梯度相近两者相除之后结果基本不再依赖梯度的绝对大小只反映方向信息。因此 Adam 每一步的实际步长量级上大约就是 lr。它把梯度大小和步长彻底解耦了梯度万年很小的参数也能得到像样的更新梯度动不动就爆炸的参数不会一步失控。这就是 Adam 名字里Adaptive自适应的含义。2.3 偏差修正Adam 里最容易被人跳过的一步公式里的m̂_t m_t / (1-β1^t)、v̂_t v_t / (1-β2^t)就是偏差修正。很多人抄代码时把它当成无关紧要的细节但它决定性地影响 Adam 前几百步的行为没有它 Adam 可能在训练刚起步时就跑歪。问题出在初始化上。m_0 和 v_0 都被初始化为 0指数滑动平均是从零开始的。第一步时m_1 (1-β1)*g_1 0.1*g_1v_1 (1-β2)*g_1² 0.001*g_1²。如果不做修正直接用m_1 / sqrt(v_1)得到0.1*g / (0.0316*|g|)量级约为 3.16也就是说第一步的实际更新量是 lr 的 3 倍以上而且这个倍率完全不依赖梯度大小。这还算能忍真正可怕的是 v 的偏差持续时间极长β20.999 意味着到第 100 步时v 仍然只有真实水平的约 9.5%步长依旧被系统性放大约 3 倍。也就是说如果不修正Adam 会在训练初期跑出明显偏大的步子对学习率的上限造成很大限制。修正的原理很直接指数滑动平均经过了 t 步以后累积的权重和是1-β^t把它除回去得到的 m̂ 和 v̂ 在统计上就是无偏估计。对 m 来说β1^t 衰减极快几十步后修正因子就接近 1对 v 来说需要上千步才完全收敛但反正在每个时间步都有一个闭式解不增加任何额外计算。这是 Adam 用极小成本避免了一个极大隐患的典型设计。2.4 完整公式汇总与默认参数解读把 Adam 的完整迭代流程列出来每一步都对应上面讲的某个动机取当前 batch 的梯度 g_t更新一阶矩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 θ_{t-1} - lr * m̂_t / (sqrt(v̂_t) ε)默认参数是 lr1e-3、β10.9、β20.999、ε1e-8。这四个数如果不想动在大多数任务上都能直接跑起来这是 Adam 能横扫深度学习圈的硬道理。每个默认值背后都有原因β10.9 对应约 10 步的方向信息窗口β20.999 对应约 1000 步的梯度尺度窗口ε 是防止除零的数值稳定项。不过 ε 并不只是加个极小值那么简单——当 sqrt(v) 很小时ε 会直接影响步长上限混合精度训练时尤其要注意这部分我在后面实操部分细讲。3. 实操经验默认参数怎么用、什么时候要改3.1 学习率从 1e-3 出发用 warmup 和 schedule 保命对于 CNN、MLP 这类常规模型lr1e-3 是绝大多数情况下的安全起点。但对于 Transformer 这类结构经验值要降到 1e-4 到 3e-4并且几乎强制要求学习率预热warmup。为什么Transformer 的深层结构对初始阶段的参数扰动非常敏感而 Adam 在训练初期虽然做了偏差修正二阶矩 v 的估计方差依然很大如果一上来就用大学习率前几步可能直接把模型推到损失面的死角。warmup 的本质就是让优化器先把统计信息积累起来再切换到完整学习率一般取总步数的 1% 到 3%比如一万步的训练warmup 一千步左右。训练中后期我建议给 lr 加一个衰减 schedule余弦退火或者线性衰减都行。有些人觉得 Adam自带自适应就不需要 schedule这是误区。Adam 的自适应只解决每个参数该用多大步不解决全局该在什么时候收敛。如果 lr 一直保持在初始值训练后期 loss 会在一个较高水平来回震荡很难压缩到极小值。实际项目里我看到过一个很常见的现象同样一个模型Adam 不加调度跑到第 30 个 epoch 还有一个点的差距上不去加个 cosine 调度再跑最后两个 epoch 就补上了。还有一个实用技巧当 loss 开始发散或者剧烈震荡先别急着换优化器直接把 lr 砍到十分之一试跑一百步。我复盘过很多次训练崩了的情况八成以上是 lr 问题而不是优化器选错。3.2 β1、β2、ε 什么时候调β1 默认 0.9很少需要动。如果你发现 batch 很小、梯度噪声很大可以适当提高到 0.95 加强平滑反过来如果目标函数变化很快比如某些在线学习场景降到 0.8 能让 m 更快追上最新的梯度方向。但无论怎么调建议保持在 0.8 到 0.95 这个区间超出范围要么步子太抖要么动量太迟钝。β2 默认 0.999在常规监督学习里很稳但有两个场景建议调低。一个是强化学习和 GAN这类任务的梯度分布会随训练动态骤变用 0.999 意味着近千步的路况统计更新太慢旧信息拖后腿把 β2 降到 0.99 甚至 0.95v 才能快速跟上变化训练明显更稳。另一个是梯度非常稀疏的任务比如某些 embedding 训练过大的 β2 会让非稀疏参数的 v 统计滞后调低一点能加快对尺度变化的反应用。ε 默认 1e-8通常不用改但混合精度训练时要注意。FP16 下 1e-8 已经接近可表示的最小正数范围很容易发生下溢导致数值不稳定。很多框架在混精模式下会把 eps 提到 1e-6 或 1e-7TensorFlow 的 Adam 默认就是 1e-7而 PyTorch 是 1e-8。跨框架复现时这个差异虽然小但它会让曲线有细微差别排查为什么我的曲线和论文对不上时不要忽略。3.3 别把 Adam 当万能药什么时候换成 SGDAdam 收敛快、省心但有两个场景我会主动换成 SGD 加动量。第一个是 ImageNet 量级的大规模图像分类。经典 pipeline 至今大量使用 SGDMomentumlr0.1、batch size 256、配合线性缩放规则和 cosine 衰减。原因是 SGD 在训练后期更容易落到平坦极小值泛化性更好Adam 的逐参数自适应步长虽然前期快后期却容易在尖锐极小值附近打转测试精度可能差零点几个点。对于要把精度扣到小数点后两位的比赛场景这个差异很致命。第二个是超参数的精细调节。SGDMomentum 的 lr、momentum、weight decay 三个参数之间耦合小调参行为很线性可预测性强Adam 的 lr、β1、β2、ε、weight decay 相互纠缠做小范围精细调参反而棘手。不过这并不意味着 Adam 只能用来做 prototype。我的判断标准很简单新任务、新结构、想快速看到效果无脑 AdamW 出 baseline当模型结构稳定、数据配方定型以后再花半天试一次 SGDMomentum 在精心调度下的表现哪个指标高用哪个。4. Adam 和 AdamW 的区别一字之差差之千里4.1 权重衰减不等于 L2 正则化一旦自适应就出问题很多人习惯说weight decay 就是 L2 正则化这句话在 SGD 下成立在 Adam 下不成立这正是 Adam 和 AdamW 的关键分歧。先看 SGD。往损失里加λ/2 * ||θ||²等价于在梯度里加λθ效果就是每次更新时把 θ 朝零方向额外拉一点权重衰减和 L2 正则化完全等价。但在 Adam 里梯度会被sqrt(v̂)除掉L2 带来的那部分梯度λθ也被除掉了。最终每个参数的有效衰减量变成λθ / sqrt(v̂)梯度历史很大的参数v 大衰减被严重削弱梯度历史很小的参数v 小衰减反而被放大。同一个 λ 在不同参数上效果千差万别这违背了权重衰减对所有参数一视同仁地往零拖的本意。结果是在原始 Adam 论文里如果你用weight_decay实现 L2实际上做的是梯度幅值越大的参数越不容易被正则化这显然不是设计者想要的语义。模型规模越大、训练越久这种偏差累积得越明显。4.2 AdamW 的具体改动AdamWLoshchilov Hutter, 2019的改法非常朴素把权重衰减从梯度里拆出来在 Adam 更新完之后单独做一步θ_t θ_{t-1} - lr * m̂ / (sqrt(v̂) ε) - lr * λ * θ_{t-1}注意这里的衰减项lr * λ * θ不受 v̂ 影响因此每个参数都以完全相同的比例被拉向零这才是真正的权重衰减。论文还给出一个对实践极其重要的结论用了 AdamW 之后最优 weight decay 和最优学习率基本可以独立调节而原始 Adam 里两者是缠在一起的调参非常痛苦。这一点在大量现代模型上反复被验证——你会发现扫描 weight decay 时 AdamW 的损失面更平滑结果更可控。这也是为什么从 BERT、GPT 那一代模型开始几乎所有 Transformer 训练代码都默认 AdamW。不是 AdamW 在迭代速度上碾压 Adam而是它在正则化语义上纠正了一个深层的设计错误并让超参数搜索变得可分离、可重复。4.3 你在代码里踩过的坑多半是假 AdamW这个坑我见过太多次值得单独拿出来写。PyTorch 里optim.Adam的参数weight_decay默认实现的是往梯度里加wd * θ也就是 L2 正则化而不是解耦的权重衰减。很多人图省事在 Adam 里设weight_decay0.01然后对外宣称我用了 AdamW这其实是带 L2 的 Adam。真正的解耦版本必须用optim.AdamW。复现论文时这个坑更隐蔽。很多论文正文只写we use Adam但代码仓库里实际用的是 AdamW这就导致你在自己的项目里复现时要么曲线对不上要么调出来的超参数和论文完全不匹配。我的排查习惯是拿到一个开源模型第一件事就是打开优化器初始化代码确认三件事——用的是 Adam 还是 AdamWweight_decay 是多少lr 配合的是什么 schedule。这三样对齐了训练曲线才有可比性。顺带提醒TensorFlow 的 AdamW 和 PyTorch 的 AdamW 在实现顺序上也有细微差别大部分任务可以忽略但如果你在做极端精确的复现最好以原仓库的框架为准。5. 现代优化器全景图谱Adam 的亲戚们该认识一下5.1 Adam 的亲族Nadam、AMSGrad、RAdamAdam 大红大紫以后出现了一堆变体思路大多是在它的某个环节上做修补。Nadam 是把 Nesterov 动量换进 Adam在计算梯度之前先用当前估计的动量方向前视一步理论上能更快响应损失面变化。实际用下来提升有限多数任务感受不到明显差异。AMSGrad 针对的是一个特定问题Adam 在某些非凸问题上v̂ 的滑动平均值可能不升反降导致步长重新变大收敛稳定性受损。AMSGrad 的做法是让 v̂ 取历史最大值、只增不减。这个改法听起来合理但它破坏了 v 对梯度尺度变化的自适应能力实践中并不是全面优于 Adam。RAdam 则是给 Adam 加了一个整流项用来缓解训练初期二阶矩估计方差过大的问题目标是降低对 warmup 的依赖。在部分任务上确实能稳定训练但也没能在整体上取代 AdamW。这些变体的共同特点是论文里通常证明在某些问题上有收益但工程里很少被当作默认选项。我的建议是先把 AdamW 吃透遇到具体问题再按需试一个变体不要为了追逐新鲜感去换优化器。5.2 近年新秀Lion、Adafactor、Sophia、LAMB最近几年每隔一阵就有新优化器打败 Adam的新闻这里简单盘一下几个真正在工程里留下名字的。Adafactor 把二阶矩做了因式分解显存占用大幅下降非常适合超长序列、超大 embedding 的训练场景很多大规模 NLP 框架里直接把它当默认优化器。代价是实现复杂度高更新行为也略有差异。LAMB 把 Adam 的更新做了逐层归一化目的是让大批量分布式训练比如上万 batch size时模型质量不垮。如果你在做大规模预训练它几乎是标配但单卡小批量场景下没有明显收益。Lion 是 2023 年 Google 提出的符号优化器核心是用 sign 函数压缩更新量显存更省在图像分类和部分生成任务上收敛速度可观。需要注意的是Lion 通常要把 lr 调大到 Adam 的 3 到 10 倍而且超参数敏感在部分 NLP 任务上并不如 AdamW 稳。Sophia 引入了二阶信息用 Hessian 对角项的估计来归一化更新在大语言模型预训练上能以更少的轮数收敛但单步计算开销更大工程实现也更复杂。我的总体心得是新优化器的快往往是轮数上的快而不是墙钟时间上的快而且它们几乎都对超参数更敏感。对绝大多数业务项目来说AdamW 加一套靠谱的 lr schedule依然是最省时间、最不容易翻车的选择。5.3 优化器选择的速查表把上面这些整理成一张表方便你按场景直接对号入座场景推荐优化器默认学习率备注快速原型、中小模型、通用任务AdamW1e-3Transformer 用 1e-4~3e-4最省心先出 baseline大规模图像分类SGD Momentum0.1batch256 线性缩放后期泛化好精度上限高Transformer / BERT / GPT 类AdamW1e-4~3e-4 warmup标配配合余弦衰减超大模型 / 超长序列Adafactor视任务调整省显存分布式友好大批量分布式训练LAMB1e-3~1e-2按 batch 缩放保持大批量下模型质量强化学习 / GAN / 在线学习Adamβ2 降到 0.99 附近1e-3快速适配非平稳梯度分布这张表只代表通用经验不代表绝对铁律。你的数据规模、模型结构、训练时长都会影响最终选择最稳妥的做法是在项目里把表里的两三个候选都快速跑 1000 步看 loss 曲线再定。6. 常见问题排查与一个 5 分钟小实验6.1 loss 发散或 NaN 的最全排查清单训练时 loss 突然变 NaN第一反应别骂优化器。我按概率排序给你一份检查清单。第一lr 太高是最常见原因。把 lr 降到十分之一试跑如果曲线稳定下来说明之前就是在悬崖边试探。第二检查数据里有没有 NaN 或 Inf。数据管道里的脏数据经常被误判成优化器问题用torch.isnan(x).any()扫一遍输入就能定位。第三检查是否梯度爆炸。RNN、Transformer 这类结构对梯度爆炸尤其敏感建议加 gradient clipping从max_norm1.0开始调。第四确认混合精度下的 eps。前面说过FP16 下 1e-8 容易下溢把它提到 1e-6 或 1e-7 再试。第五断点续训时有没有正确加载 optimizer 的 state_dict。很多人只加载了模型权重没加载优化器的 m、v 状态导致 momentum 从零重新起步表现为一段时间后曲线诡异波动。我自己遇到最多的是第一条和第四条。尤其是 fp16 训练时loss 突然变 NaN十次有八次是 eps 太小的锅这一点在文档里非常不起眼但只要踩过一次就能记住一辈子。6.2 复现论文时的优化器玄学复现别人的模型跑出来的曲线和论文对不上大多数人先去调模型结构但我建议先调优化器配置。论文正文里写我们使用 Adam实际代码可能是 AdamW论文说 lr1e-3但只有在配合特定 warmup 和衰减策略时这个数才成立论文说 weight decay0.01但没有说明是在 Adam 里做的 L2 还是在 AdamW 里做的解耦衰减——这两个效果差很多。还有一个容易被忽略的点有些实现会对不同参数组设置不同的 lr 和 weight decay比如 bias 和 LayerNorm 参数不做衰减embedding 用相对较小的 lr。这些细节写在配置文件里不显眼但全都影响最终指标。所以我的复现顺序永远是先逐行对齐优化器配置再对齐数据预处理顺序最后才怀疑模型结构本身。绝大多数复现不出来的问题都出在前两步。6.3 亲手看看 Adam 的效果一个极简对比实验理论讲再多不如亲手跑一遍。这里给一个极简的 PyTorch 实验在 MNIST 上对比 SGD、SGDMomentum、Adam 的训练曲线跑几分钟就能看到明显的阶段性差异。import torch import torch.nn as nn from torchvision import datasets, transforms from torch.utils.data import DataLoader class MLP(nn.Module): def __init__(self): super().__init__() self.net nn.Sequential( nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 128), nn.ReLU(), nn.Linear(128, 10), ) def forward(self, x): return self.net(x.view(x.size(0), -1)) transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)), ]) train_loader DataLoader( datasets.MNIST(./data, trainTrue, transformtransform, downloadTrue), batch_size128, shuffleTrue, ) def run(optimizer_name, lr): model MLP() if optimizer_name SGD: optim torch.optim.SGD(model.parameters(), lrlr) elif optimizer_name Momentum: optim torch.optim.SGD(model.parameters(), lrlr, momentum0.9) else: optim torch.optim.Adam(model.parameters(), lrlr) loss_fn nn.CrossEntropyLoss() losses [] for epoch in range(5): for x, y in train_loader: optim.zero_grad() loss loss_fn(model(x), y) loss.backward() optim.step() losses.append(loss.item()) return losses # 依次运行下面三个把 loss 曲线画出来对比 sgd_loss run(SGD, 0.1) mom_loss run(Momentum, 0.1) adam_loss run(Adam, 1e-3)观察点有三个。第一个是前几百步Adam 的 loss 会掉得最快可能 SGD 还在 0.4 附近挣扎Adam 已经到 0.1 以下这就是自适应步长的威力。第二个是训练中期Momentum 会逐渐追上来因为它在平坦方向上持续加速而 Adam 的步长被 v 约束得比较稳。第三个是最终表现如果只训 5 个 epochAdam 通常领先如果把训练拉长并给 SGD 配一个余弦衰减 schedule两者的最终测试精度会非常接近甚至 SGD 更好。这个实验虽然简单但它能把文章前面一大半理论落在地上Adam 的快是真实存在的但快不等于最终更好不同优化器的优势窗口完全不同。6.4 我的选择习惯和一点私货做了这些年训练我的习惯越来越朴素新项目一律 AdamW 起步lr 用一个安全值快速把 baseline 跑出来等到模型结构、数据配方都稳定了再花半天试一下 SGDMomentum 在精心调好 schedule 下的效果哪个指标高用哪个。最后分享一个我踩过好几次坑才记住的小调整如果你在强化学习、GAN 这类梯度分布剧烈变化的任务里用 Adam先把 β2 从 0.999 降到 0.99很多时候训练稳定性立竿见影。这个参数太不起眼论文里几乎不会重点讲但实际效果比你去换一个更高级的优化器来得更直接。优化器这玩意思路就那几条主线把 Adam 的机制想透了后面再出现什么新名字你也能一眼看穿它的底牌。
返回列表