Adam与AdamW优化器深度对比:解耦权重衰减的原理与实战
1. 为什么Adam和AdamW值得单独拎出来聊如果你写过深度学习训练脚本大概率写过这一行optimizer torch.optim.Adam(model.parameters(), lr1e-3)。再进阶一点你可能在某篇论文的复现笔记里看到别人用的是AdamW然后顺手把Adam换成AdamW发现结果确实好了一点但说不清楚到底好在哪。这个现象非常普遍——Adam 系列优化器是深度学习里使用频率最高的工具之一但真正把 Adam 和 AdamW 的区别讲清楚、把权重衰减这件事说明白的人并不多。这篇文章要解决的就是这个问题。我会从优化器的更新公式出发把 Adam 和 AdamW 在数学层面的差异拆开解释为什么 L2 正则化和权重衰减在自适应优化器里不是一回事然后给出实际训练中怎么选、怎么配参、怎么排查问题的完整方案。适合已经能跑通训练流程、但对优化器内部机制一知半解的同学也适合正在做论文复现、发现换了优化器结果对不上的从业者。读完你至少能搞清楚三件事AdamW 到底改了什么、为什么它在大模型训练里几乎成了默认选项、以及你自己的项目里该不该换。2. 从梯度下降到Adam优化器的演化逻辑2.1 梯度下降的朴素思路和它的局限最原始的优化方法就是梯度下降沿着损失函数梯度的反方向走一步步长由学习率控制。公式很简单θ θ - lr * g其中g是当前参数的梯度。这个方法的问题在于它对所有参数一视同仁用同一个学习率。但实际网络里不同参数的梯度尺度差异巨大——有的参数梯度常年很小有的参数梯度剧烈波动。统一学习率会导致前者学得太慢、后者震荡不收敛。为了解决这个问题出现了动量法Momentum它引入一个速度变量累积历史梯度让更新方向更平滑。再往后AdaGrad 提出对每个参数维护梯度平方的累积和用这个累积和来缩放学习率梯度大的参数学习率自动变小。但 AdaGrad 的累积和只增不减训练到后期学习率会趋近于零提前停滞。RMSProp 用指数移动平均替换了累加解决了单调递减的问题。Adam 则是在 RMSProp 的基础上再叠加了一阶动量同时估计梯度的一阶矩和二阶矩。2.2 Adam的核心更新公式拆解Adam 的更新过程可以分成几步。第一步计算一阶矩估计动量和二阶矩估计梯度平方的指数移动平均m_t β1 * m_{t-1} (1 - β1) * g_t v_t β2 * v_{t-1} (1 - β2) * g_t^2第二步做偏差校正。因为m和v初始化为零训练初期它们的值会偏向零所以需要除以(1 - β^t)来校正m_hat m_t / (1 - β1^t) v_hat v_t / (1 - β2^t)第三步更新参数θ θ - lr * m_hat / (sqrt(v_hat) ε)默认参数通常是β10.9、β20.999、ε1e-8。这套机制让 Adam 在训练初期收敛快、对学习率不敏感成为很多任务的首选。但它有一个被忽视很久的问题权重衰减的实现方式。2.3 L2正则化与权重衰减的混淆起源在标准 SGD 时代我们想在损失函数里加一项 L2 正则化惩罚大的权重L_total L (λ/2) * ||θ||^2对 θ 求梯度会多出一项λ * θ所以参数更新变成θ θ - lr * (g λ * θ) θ - lr * g - lr * λ * θ可以看到L2 正则化在梯度层面等价于在每次更新时把参数往零的方向拉一点拉的量是lr * λ * θ。在 SGD 里这就是权重衰减。所以早期框架里weight_decay参数就是通过往梯度里加λ * θ来实现的大家也就默认 L2 正则化和权重衰减是一回事。问题出在 Adam 上。当你把λ * θ加到梯度g里然后送进 Adam 的更新流程这个额外的项会被 Adam 的自适应机制处理——它会被一阶矩和二阶矩分别缩放。结果就是实际作用在参数上的衰减量不再是简单的lr * λ * θ而是被sqrt(v_hat)除了一遍。对于梯度大的参数衰减被削弱对于梯度小的参数衰减被放大。这跟 L2 正则化的本意就偏离了。3. AdamW到底改了什么解耦权重衰减3.1 解耦的数学表达AdamW 的核心改动只有一句话把权重衰减从梯度计算里拿出来直接作用在参数更新上。具体来说AdamW 的更新公式变成θ θ - lr * m_hat / (sqrt(v_hat) ε) - lr * λ * θ注意最后一项lr * λ * θ它不再经过 Adam 的自适应缩放而是直接按学习率和衰减系数对参数做收缩。这就是所谓的“解耦权重衰减”decoupled weight decay。而 Adam 加 L2 的版本等效于θ θ - lr * (m_hat / (sqrt(v_hat) ε) λ * θ)两者在数学上不等价。AdamW 让权重衰减的行为变得可预测衰减量只跟学习率、衰减系数和当前参数值有关跟梯度的大小无关。3.2 为什么这个改动重要这个改动的重要性体现在几个方面。第一超参数解耦。在 Adam 里weight_decay的效果跟学习率、β 参数纠缠在一起调参时很难单独控制正则化强度。AdamW 把衰减独立出来λ的语义变得清晰——它就是一个纯粹的收缩系数。第二训练稳定性。在自适应优化器里梯度小的参数会被放大学习率如果 L2 项混在梯度里这些参数会受到额外的、被放大的衰减可能导致训练不稳定。AdamW 避免了这个问题。第三泛化性能。原论文的实验表明在相同训练设置下AdamW 比 Adam L2 有更好的泛化表现尤其是在训练轮数较多、模型较大的场景下差异更明显。3.3 一个直观的类比可以把 Adam 加 L2 想象成你在跑步时教练根据你当前的速度和方向决定给你多大的阻力。你跑得快阻力被调小你跑得慢阻力被调大。这个阻力本来是想让你别跑太偏但它的实际大小取决于你的速度不可控。AdamW 则是不管你跑得快还是慢教练都按固定比例拉你一下。这个拉力是独立的、可预测的。显然后者的行为更容易分析和调节。4. 实操中怎么选、怎么配4.1 默认建议新项目直接用AdamW如果你在启动一个新项目没有历史包袱我的建议是直接用 AdamW。理由很简单它的权重衰减行为更符合直觉超参数更容易调而且在大量任务上被验证过效果不差于甚至优于 Adam。PyTorch 里从 1.2 版本开始就内置了torch.optim.AdamW用法和 Adam 几乎一样optimizer torch.optim.AdamW( model.parameters(), lr1e-3, betas(0.9, 0.999), eps1e-8, weight_decay0.01 )注意weight_decay的默认值。PyTorch 的 AdamW 默认weight_decay0.01而 Adam 默认是 0。这个 0.01 不是随便定的它在很多 Transformer 类模型上是比较稳的起点。但也不是万能值后面会讲怎么调。4.2 什么情况下继续用Adam有几种情况你可能需要保留 Adam。一是复现旧论文原论文用的是 Adam L2你换成 AdamW 可能导致结果对不上这时候应该严格按原设置来。二是某些任务对权重衰减不敏感比如小模型、短训练、数据量不大的场景两者差异可能被噪声淹没。三是你的代码库里已经有一套调好的 Adam 超参换优化器需要重新调参成本高且收益不确定。这时候不必为了“政治正确”强行换。4.3 学习率和权重衰减的联动关系AdamW 里学习率和权重衰减是乘在一起作用的lr * λ * θ。这意味着如果你把学习率调小权重衰减的实际效果也会同比减弱。所以调参时不能孤立地看weight_decay。一个常见的做法是固定lr * λ的乘积来观察效果或者用学习率调度时同步考虑衰减强度。另外AdamW 原论文建议的λ范围大概在 0.01 到 0.1 之间具体取决于模型大小和训练轮数。模型越大、训练越久通常需要更强的衰减来防止过拟合。4.4 参数分组哪些参数不该衰减这是一个非常容易被忽略的实操细节。不是所有参数都适合做权重衰减。偏置项bias和归一化层的缩放平移参数如 BatchNorm、LayerNorm 的 weight 和 bias通常不应该衰减。原因是这些参数本身数量少且它们的理想值不一定接近零强行衰减可能损害模型表达能力。标准做法是分组设置no_decay [bias, LayerNorm.weight, BatchNorm.weight] optimizer_grouped_parameters [ { params: [p for n, p in model.named_parameters() if not any(nd in n for nd in no_decay)], weight_decay: 0.01 }, { params: [p for n, p in model.named_parameters() if any(nd in n for nd in no_decay)], weight_decay: 0.0 } ] optimizer torch.optim.AdamW(optimizer_grouped_parameters, lr1e-3)这段代码在 Transformer 类模型的训练脚本里几乎是标配。如果你之前没做分组可以试试加上很多时候能带来一两个点的提升。5. 常见问题与排查技巧实录5.1 换了AdamW之后loss不降反升这是最常见的问题。原因通常有几个一是weight_decay设得太大尤其是从 Adam 的 0 直接跳到 AdamW 的 0.01 甚至更高模型被过度收缩。可以先从 0.001 或 0.005 开始试。二是学习率没跟着调。AdamW 的有效更新幅度和 Adam 不完全一样有时候需要把学习率稍微调小一点。三是没有做参数分组把不该衰减的参数也衰减了。排查顺序建议是先检查参数分组再把weight_decay减半最后微调学习率。5.2 训练后期loss震荡或发散AdamW 在训练后期出现震荡往往跟二阶矩估计的偏差有关。虽然 Adam 系列都有偏差校正但在训练步数很多之后v_hat可能变得很小导致m_hat / sqrt(v_hat)这一项被放大。可以尝试调大eps比如从1e-8调到1e-6给分母一个下限。另外配合学习率预热和余弦退火也能缓解后期震荡。如果用了梯度裁剪检查裁剪阈值是否合理太小的阈值会跟 AdamW 的更新产生冲突。5.3 复现论文时结果对不上如果你在复现一篇用 Adam 的论文但框架默认或者你手滑用了 AdamW结果很可能对不上。反过来也一样。排查时先确认原论文用的优化器名称和weight_decay设置。有些论文写的是 Adam但实际代码里用的是 AdamW或者反过来。最稳妥的办法是找到官方开源代码看它 import 的是哪个类。如果找不到代码就两种都跑一遍对比训练曲线通常在前几个 epoch 就能看出差异。5.4 常见问题速查表问题现象可能原因排查方向loss不降或上升weight_decay过大减半或从0.001起调训练后期震荡eps过小或学习率过大调大eps至1e-6加学习率调度复现结果对不上优化器类型或衰减设置不一致核对官方代码的optimizer类小模型上无差异任务对衰减不敏感不必强求按原设置即可显存占用略增AdamW多存一份参数副本用于衰减正常现象通常可忽略5.5 几个容易踩的坑第一个坑是以为weight_decay越大正则化越强就越好。实际上过大的衰减会让模型欠拟合尤其是小模型。第二个坑是忘了 AdamW 的衰减是乘学习率的用了很小的学习率却设了很大的weight_decay结果衰减几乎不起作用。第三个坑是在微调预训练模型时直接套用从头训练的超参微调通常需要更小的学习率和更小的衰减。第四个坑是混合精度训练下AdamW 的更新可能在 fp16 下溢出需要用 fp32 维护优化器状态PyTorch 的 AMP 会自动处理但自定义训练循环时要留意。6. 从公式到代码一个完整的对比实验6.1 实验设置为了直观展示差异我设计了一个简单的对比实验。任务是一个小型图像分类用同一个网络结构分别用 Adam L2 和 AdamW 训练其他超参尽量对齐。关键设置如下学习率 1e-3batch size 64训练 50 个 epochAdam 的weight_decay设为 1e-4通过 L2 方式AdamW 的weight_decay设为 0.01。注意这两个值不是等价的这里只是各自常用的起点。6.2 训练曲线对比实测下来AdamW 在训练集上的 loss 下降略慢于 Adam L2但在验证集上的准确率更高过拟合出现得更晚。具体来说Adam L2 在第 30 个 epoch 左右验证准确率开始 plateau而 AdamW 能持续提升到第 40 个 epoch 之后。最终验证准确率 AdamW 高出约 1.5 个百分点。这个差距在小模型上不算特别大但在更大模型和更多训练轮数下会更明显。6.3 权重分布的变化另一个值得观察的点是权重分布。训练结束后AdamW 的权重分布更集中、尾部更薄说明权重衰减确实起到了收缩作用。而 Adam L2 的权重分布尾部更厚说明部分参数的衰减被自适应机制削弱了。这印证了前面的分析AdamW 的衰减更均匀、更可预测。6.4 代码实现要点如果你要自己实现对比注意几点。一是确保两个优化器的初始参数完全一致最好用同一个随机种子初始化模型后深拷贝。二是记录训练和验证的 loss、准确率以及权重的范数变化。三是控制变量除了优化器相关的设置其他如数据增强、学习率调度、batch size 都保持一致。四是多跑几个种子取平均单次实验的差异可能被随机性掩盖。7. 一些个人经验和收尾建议我在实际项目里切换优化器的经验是不要一次性全换先在一个子任务或小规模实验上对比确认有正向收益再推广。AdamW 不是银弹它在很多场景下比 Adam L2 好但不是所有场景。我遇到过在某个序列标注任务上两者几乎没差异也遇到过在某个生成任务上 AdamW 明显更稳。关键是要理解背后的机制知道什么时候该用哪个而不是盲目跟风。另外分享一个小技巧如果你不确定weight_decay设多少可以先用 0 跑一遍记录验证集的最佳表现然后逐步增加衰减系数观察验证集表现的变化。通常存在一个最优区间超过之后验证集表现会下降。这个搜索过程不需要跑完整训练用学习率预热后的前几个 epoch 就能看出趋势。最后记得把优化器的选择写进实验记录包括具体的类名和参数值不然过两周你自己都记不清当时用的是哪个版本。