深度学习优化算法是每个训练过模型的人都要面对的话题。我刚入门那会儿,以为优化器就是选一下Adam然后设个学习率,结果后来发现同样的代码、同样的数据,换个优化器,loss曲线和最终精度能差出一大截。做大规模深度学习后,这个差距会更大。优化算法是整个训练过程真正的“发动机”,发动机调不好,网络结构再漂亮也白搭。
这篇内容我打算从最朴素的SGD开始,一路讲到目前大模型训练里最常见的AdamW,重点不是堆公式,而是讲清楚每个算法到底在解决什么问题、犯了什么错、为什么后来者能弥补这些错误。全程会穿插我自己在实际训练中踩过的坑和总结出来的规律,各位可以根据自己的任务直接拿来参考。
1. 从SGD说起:一个简单到极点的起点
1.1 朴素SGD为什么难用
随机梯度下降(SGD)的更新逻辑非常简单:记参数为 ( \theta ),学习率为 ( \eta ),梯度为 ( g ),那么每次更新就是:
[ \theta \leftarrow \theta - \eta \cdot g ]
这句话翻译过来就是“顺着坡度最大的方向迈一步”。这个思路本身没有任何问题,真正的问题出在“一步一步走”这件事上。实际训练时我们用的是mini-batch,一批样本得到的梯度是有噪声的,这个噪声让参数更新路径变得非常曲折,尤其是遇到损失曲面在某个方向上特别陡、在另一个方向上特别平缓的情况。我印象很深的一次是在某个图像分类任务上,直接跑SGD,loss下降特别慢,损失曲线像锯齿一样上下抖动,看起来训练过程完全没在收敛。后来换了动量SGD,情况立刻好转,训练时间也明显缩短。
朴素SGD另一个让人头疼的地方是它的学习率完全固定。训练初期,参数离最优解很远,我们希望步子迈大一点;到了训练后期,参数已经靠近谷底,我们希望步子缩小,在最优解附近精细搜索。可是SGD不知道你处在哪个阶段,它只会用同一个固定的学习率一直走。所以在实践里,用SGD必须配合学习率退火策略,否则几乎不可能取得理想的收敛精度。
1.2 动量(Momentum)解决的是什么问题
动量SGD是对朴素SGD的第一次有效改造。它借鉴的是物理学里惯性球从山坡滚下的过程:球在下滚的过程中会累积动能,遇到局部小坑或噪声时不容易被卡住,方向的转变也比较平滑。它的更新方式是引入一个速度变量:
[ v \leftarrow \alpha \cdot v + \eta \cdot g \ \theta \leftarrow \theta - v ]
这里的 ( \alpha ) 一般取0.9,表示历史速度的保留比例。如果当前梯度方向和历史速度方向一致,速度会增大,训练加速;如果方向相反,速度会被部分抵消,起到“刹车”作用,避免震荡。这个机制让参数更新在方向上更稳定,在平坦区域也能因为历史积累而快速越过。
动量还有一个进阶版本叫Nesterov动量。它的想法是不要用当前位置的梯度来决定下一步,而是先“猜测”一下走完这一步会到哪,再在这个预估位置上计算梯度。在工程实现上,它常常被证明有更快的收敛速度,尤其在标准的计算机视觉训练流程里,带Nesterov的SGD是非常经典的选择。我在很长一段时间里,凡是做图像分类任务,都默认用Nesterov动量SGD配合余弦退火,效果非常稳定,普适性也很好。
2. 自适应学习率算法:每个参数都该有自己的步长
2.1 AdaGrad与RMSProp的探索
动量虽然平滑了更新方向,但所有参数仍然共享同一个全局学习率。稀疏特征场景下这个问题尤其明显:有些特征出现频率高,梯度频繁,累计更新很多,训练后期其实已经比较“熟”了,不需要大步伐;有些特征出现频率低,梯度稀疏,但偶尔来一个梯度就很有信息量,需要大步更新。显然全局统一的学习率无法应对这种差异。
AdaGrad的做法是,对每一个参数累计其历史梯度的平方和,然后再用这个累计值去除学习率。高频出现的参数累计值大,有效学习率就小;低频参数累计值小,有效学习率就大。这个想法很好,但AdaGrad有一个致命伤:平方和的累计值是单调递增的,越到训练后期,学习率就被压得越小,最后训练几乎停摆。
RMSProp对AdaGrad做了一点关键修改:不再从训练开始一直累计梯度平方,而是用指数移动平均(EMA)来估算近期梯度的大小量级。这样“历史记忆”有了衰减,早期累计的梯度平方不会永久压制后期学习率,每个参数的学习率能根据近期梯度大小动态调整,训练过程就不再那么容易提前僵死。
2.2 Adam的核心机制与两个矩
RMSProp已经具备了自适应学习率的雏形,Adam则在其之上又加入了动量机制,并且做了一套非常好的偏差校正。Adam维护两个状态:一阶矩 ( m ) 和二阶矩 ( v )。一阶矩本质上是梯度的指数移动平均,起到类似动量的作用;二阶矩是梯度平方的指数移动平均,用于估算每个参数梯度的幅度。每次更新时,用一阶矩作为更新方向,再用二阶矩对学习率做归一化:
[ m_t = \beta_1 m_{t-1} + (1 - \beta_1) g_t \ v_t = \beta_2 v_{t-1} + (1 - \beta_2) g_t^2 \ \hat{m}_t = \frac{m_t}{1 - \beta_1^t} \ \hat{v}_t = \frac{v_t}{1 - \beta_2^t} \ \theta \leftarrow \theta - \eta \cdot \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} ]
这里 ( \beta_1 ) 通常取0.9,( \beta_2 ) 取0.999,( \epsilon ) 取1e-8。Adam真正厉害的地方在于:归一化之后,每个参数的有效更新步长和梯度本身的尺度关系不大,只取决于梯度方向的“信噪比”。一个参数就算梯度一直很大,只要这种大是稳定的,有效更新步长也会被控制在一个合理范围。这让Adam对学习率的敏感度远低于SGD。SGD的学习率需要从0.1调到1e-3才能找到合适区间,Adam则经常在3e-4和1e-3之间随便选一个都能正常收敛。
2.3 Adam的隐性缺陷
Adam如此好用,它是不是就没有问题了?在实际使用中,Adam有两个比较令人头疼的问题。
第一个问题是泛化性能。在很多大规模图像分类任务上,不少人发现用SGD(尤其带动量)训练的模型最终验证集精度往往优于Adam,尽管Adam的loss下降得更快。有人猜测这可能跟Adam在后期更新过于“谨慎”有关:Adam会自动压缩每个参数的更新步长,导致最终落点离最优解有一定偏差,等价于一种隐性的正则化效果。正则化本身不是坏事,但如果是未经调好的“固定正则化”,在某些任务上就会伤害精度。
第二个问题是权重衰减的处理。早期PyTorch中的Adam实现里,权重衰减(weight decay)是通过往梯度里加一项 ( wd \cdot \theta ) 来实现的,这本质上是L2正则化。后面我们会专门讨论,这种实现方式和真正的解耦权重衰减在效果上差别非常大。
3. AdamW:一次关键的解耦
3.1 L2正则和权重衰减到底有什么关系
先说数学关系。对SGD来说,在梯度上添加权重项,和在对损失函数添加L2正则项,这两者在数学上几乎是等价的。假设原始损失为 ( L ),权重衰减为 ( \lambda ),真正的L2正则化损失是 ( L + \frac{\lambda}{2}|\theta|^2 ),计算梯度时会多出一个 ( \lambda \theta ) 项。在标准SGD更新时,这一项会直接参与梯度运算,结果等效于每次更新后额外把权重往零方向缩放了一个固定比例。
但请注意,这个等价关系成立的前提是梯度的更新步长没有被归一化。Adam更新时会用二阶矩去除梯度,正则项的梯度同样会被归一化,这就破坏了“等价性”。最终的效果是,权重越大,它的正则化强度反而被缩小得越多,这完全违背了L2正则的初衷。所以Adam里直接加正则项,并不是真正的权重衰减,只是做了个样子。
3.2 AdamW的更新方法
AdamW的做法简单粗暴:把权重衰减从梯度计算里彻底拿出去,每次更新完参数后直接额外执行一步“权重×系数”的衰减:
[ \theta \leftarrow \theta - \eta_t \cdot \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} - \eta_t \cdot wd \cdot \theta ]
注意权重衰减项不再被二阶矩归一化,而是独立作用于参数,且它的系数还会乘以当前学习率,这样衰减的强度会跟随学习率退火自然变化。这就真正保留了“权重衰减”原有的性质:每个参数无论梯度多大,都会被按相同比例往零方向拉回。对于大规模预训练模型来说,这一步的差距直接体现在最终模型的可复现性和下游任务效果上。
最早提出这种解耦思想的工作,本身就同时在多模态预训练、图像分类、机器翻译这些场景做了对比实验,结论非常明确:使用AdamW会让训练曲线和最终效果显著更稳定。从那以后,AdamW逐渐成了大规模深度学习的默认优化器。
3.3 为什么解耦之后效果更好
我自己的理解是,解耦让“梯度下降”和“权重约束”各自回归了本职工作。梯度下降负责寻找目标函数的最优方向,权重衰减负责限制模型复杂度、抑制过拟合。把两者的职责混在一次梯度更新里,在Adam这种带有自适应归一化的算法中会产生严重的耦合效应,导致大权重收到惩罚不足,小权重又容易被过度拉向零,模型的权重分布就会变得很畸形。
另外还有一个与学习率退化相关的观察。AdamW中的权重衰减项乘以了当前学习率,当学习率衰减到很小时,权重衰减的作用也会同步削弱,这样在训练后期,模型结构能够更自由地适应数据分布。我在几个Transformer结构的编码器任务上对比过,训练后期学习率退火阶段,AdamW的验证指标下滑幅度明显低于传统L2型Adam。这说明解耦不仅在中前期帮助稳定训练,在后期也能让模型收敛到更好的平衡点。
4. 超参调度与工程上的选择
4.1 学习率怎么调:warmup、退火、余弦
优化器选定之后,学习率调度策略是决定训练质量的另一个关键变量。Transformer类模型通常需要一个较短的warmup阶段,让学习率从极小的值逐步升到预设值,这是为了防止训练初期参数剧烈变化时产生超大更新。要知道初始阶段参数完全是随机的,梯度方向可能非常不准确,如果一开始就用大学习率,模型很可能一步就跳到某个布满坏点的区域,后期也很难逃出来。warmup本质上就是在训练早期给模型一个“热启动”的适应时间。
warmup之后,主流选择之一是余弦退火:学习率按余弦曲线从初始峰值平滑降到接近零。余弦退火的好处是训练后期学习率下降比较缓慢,在参数接近最优区域时不会因为学习率突变而“跌出”好位置。另一种常用策略是带重启的余弦退火,也就是学习率周期性上升后再下降,这种方法在某些任务上能帮助模型跳出局部极小值,但会引入新的超参数(训练周期数、重启次数),对大多数人来说并不比标准的余弦退火更容易控制。
4.2 AdamW的超参数怎么定
默认超参只是一个起点,不是万能答案。AdamW最常用的默认组合是 ( \beta_1=0.9 )、( \beta_2=0.999 )、( \epsilon=1e-8 ),权重衰减一般设置在0.01到0.1之间。对一般中小规模的模型,weight decay设为0.01通常没什么问题;大类别的分类任务或者多模态对齐任务,稍微增大到0.05或者0.1会让模型更稳定。我曾经在某个多模态对比学习任务上,weight decay从0.01改成0.05后,在下游几个子任务上的结果都有小幅提升,而且训练过程中的震荡也少了不少。
学习率方面,AdamW的峰值学习率我的参考区间是1e-4到3e-4。如果你用的是较大的batch size,比如超过2048,可以按线性缩放规则适当调大一些。还有一点:batch size在训练损失曲线上影响很大,更大的batch size通常允许更高的学习率,但梯度噪声也更低,有时反而需要更精益的warmup设计。这个规律不是绝对的,需要看具体loss曲面,我只是建议不要盲目照搬别人的超参组合。
4.3 大规模训练中如何选
当训练规模大到以“天”为单位计算时,选择优化器的原则会发生变化。大模型训练中常见的AdamW和带动量的SGD都在被使用,它们各有适用场景。预训练任务中,AdamW的稳定性和对超参不那么敏感的特性极其宝贵,因为一次失败的预训练尝试代价极高,谁也不想重跑一遍;而微调阶段、或者数据规模不算特别大、任务定义比较清晰的场景,SGD反而常常能精细打磨出更好的泛化性能。
另外,在大规模分布式训练中,通信效率也很重要。AdamW本身需要维护一阶矩和二阶矩两个额外状态,显存开销比SGD大一倍,这对全参数微调来说是一个很大的负担。很多开源项目在实际训练时用的是分片优化器状态、梯度检查点等技巧来缓解显存问题。这也是为什么近年来的低秩适配微调方法会受欢迎,它大幅降低了需要维护优化器状态的参数量。说到底,优化器选择从来不是单独一件事,它跟显存、通信、训练目标都强相关。
5. 常见问题与排查实录
5.1 训练中期loss突然变成NaN
这个问题我碰到过很多次,多数发生在使用Adam或AdamW训练Transformer结构时。经典排查顺序是先看学习率:排除掉特别大的学习率场景后,检查数据中是否存在异常值、NaN标签或极端的特征。如果数据没有问题,那么很可能是模型计算中的数值稳定性问题。
在混合精度训练里,AdamW的 ( v_t ) 有可能因为指数移动平均累积到极小值,然后除进去产生异常大的更新,导致中间激活溢出。我的习惯是把 ( \epsilon ) 从1e-8调大到1e-6或1e-4,同时开启损失缩放和梯度裁剪。梯度裁剪尤其重要,它能防止偶发的大梯度一竿子打翻训练过程。如果在GAN训练或对比学习里遇到NaN,另外要检查一下相似度计算和softmax的logits范围,必要时用数值更稳定的实现。
5.2 训练loss一直在降,验证集指标却很难提升
这种情况很常见,特别是从SGD切换到Adam或AdamW时。很多人误以为优化器直接换一下就能提升指标,但实际换完后常常看到的是过拟合更早发生,或者后期收敛精度不如预期。这可能有两个原因:一是Adam系列优化器本身的泛化能力相对弱一些,二是权重衰减没有生效或者设置得不当。
遇到这种问题,我会先确认weight decay是否真的生效。到代码里看优化器参数组,确认不是用了旧版“L2正则型Adam”的实现,然后再逐步调大weight decay,观察验证集指标变化。同时检查学习率是否偏高,如果训练集指标比验证集高出一大截,适当降低学习率峰值往往就能缓解过拟合。
5.3 复现时的坑:优化器状态的初始化
复现论文结果时,优化器状态保存和加载之间的匹配问题非常容易踩坑。特别是从检查点再继续训练时,如果加载了模型的参数但没有正确加载优化器状态,训练曲线会从完全不同的状态重新开始,后续结果很难和之前一致。
另一个常见坑是不同框架或不同版本之间优化器超参默认值不一致。比如某些老版本代码里Adam的 ( \epsilon ) 是1e-8,另一些实验里默认是1e-8但实现方式却是在平方根外防止除零,而实现方式不同,在低频稀疏梯度场景下的行为差异会很显著。如果你要复现论文实验,一定要确认优化器默认参数和权重衰减的公式细节,否则看起来一样的模型,实际训练动态会差很多。
5.4 选择困难症的实用解法
经常有人问我,到底该用SGD还是Adam还是AdamW。我给的建议很简单:如果你是刚入门,或者正在做一个全新任务、不太确定哪些超参合适,直接用AdamW,设置权重衰减为0.01,峰值学习率1e-4,配合1000步左右的线性warmup和余弦退火,这个组合几乎不会出大错。如果你的任务已经有很成熟的训练管线,例如标准的图像分类、目标检测或某些推荐模型,那就沿用社区验证过的SGD动量方案,不要随意更换。
还要注意数据集规模的影响。小数据上,SGD和AdamW的差距通常不大,更多是超参调优的功力问题;大数据上,AdamW的稳定性优势会越来越明显。没有绝对的好坏,只有是否匹配当前领域的训练习惯。
6. 多年实践后的一点体会
我早期非常迷信Adam,因为它的收敛速度快、调参省心,几乎不需要太多干预。后来大规模预训练和微调做多了之后才慢慢意识到:AdamW带来的改进不仅仅是“更规范的正则项”,更深层的是它让“优化”与“约束”这两个目标回到了各自的控制通道上。这种设计上的清晰感,最终让模型的收敛性和泛化性能都变得可控。
这里分享一个我们经常使用的小技巧:在训练后期,把二阶矩的 ( \beta_2 ) 调大到0.9999甚至0.99995,可以让AdamW更新步长更平滑,减少后期loss曲线的抖动。对某些任务来说,这一个参数带来的最终指标提升甚至比调整权重衰减更明显。还有一个建议是,记录每次训练中每个参数组的真实更新范数,在训练脚本中定期打印出来,观察它是否出现奇怪的增长趋势,这能提前发现很多数值不稳的隐患。
优化算法这个方向,看起来只是深度学习流程里的一小步,封神了多年,但真正深入之后会发现,从SGD到AdamW,每一个改动都对应着真实训练中的痛点。希望这些记录能让正在选择优化器的你少走一些弯路。