☰
从SGD到AdamW:大模型训练优化器的演进与实战指南
2026/10/14 5:13:27 网站建设 项目流程

很多人刚开始接触大模型训练时,最容易被一堆名词淹没:优化器、学习率调度、混合精度、梯度累积、DeepSpeed……我见过不少同学第一反应是找一个现成的训练脚本直接抄,结果换了个数据集就发散,又不知道问题出在哪。这篇我打算把优化器这一块掰开揉碎讲一遍,重点就是SGD、EMA、Momentum、RMSProp、Adam/AdamW这几条主线。为什么要把它们放一起讲?因为Adam/AW并不是凭空冒出来的黑科技,它是前面这些思路一层层堆出来的。搞懂了SGD的局限、Momentum的动机、RMSProp在做什么,你才能在大模型训练里真正理解“为什么选AdamW而不是Adam”、为什么现在不少人又回头研究SGD和LAMB。这篇文章的目标读者是那些已经跑通过一个简单模型,但想进一步深入理解训练底层机制的开发者,内容偏原理和实战结合,不堆公式,但该给的公式和参数含义会尽量说清楚。

1. 从SGD讲起:为什么把梯度下降当基准不是偷懒

1.1 一次参数更新到底在做什么

用一句话概括SGD的核心:参数沿着损失函数下降最快的方向移动一小步。假设我们要最小化损失L(θ),在参数θ处算出梯度g,然后做更新:

θ = θ - lr * g

lr就是学习率,也是整个优化器体系里最需要精心调的超参数。很多刚入门的人觉得SGD太简单、不屑于用它,但我建议所有新项目先强行用SGD跑一个baseline,原因很简单:如果连SGD都收敛不了,大概率不是优化器的问题,而是数据、loss、模型结构或训练流程本身有问题;如果SGD能收敛但Adam反而发散了,那才说明Adam相关的超参数没配好。先用最原始的方法把训练链路调试通,能省下后面排查问题的大量时间。

SGD的另一个名字叫“随机梯度下降”,里头的“随机”不是说着玩的。每一批数据算出来的梯度都是全量梯度的一个噪声估计,所以SGD天然带有一定的随机性。这个随机性在训练中有时反而是优点,它能帮助跳出尖锐的局部极小点。代价就是:Batch size比较小时,梯度噪声太大,训练曲线会抖得厉害;Batch size太大时,梯度又太准,反而可能收敛到较差的平缓区域。这也是为什么业界总结出一个经验:调大Batch size时,通常要同步调大学习率,因为梯度更准了,步子可以迈得更大。

1.2 学习率到底影响的是什么

学习率是SGD里唯一直接控制步长的东西,但它的行为并不是“大一点就更快、小一点就更好”这么简单。

  • 学习率过大:参数会在最优值附近来回震荡,甚至直接发散到NaN。
  • 学习率过小:前期的训练进展极慢,而且模型容易被困在某个局部极小区,因为步长不足以跨过小的势垒。
  • 学习率合适:loss按比较稳定、比较快的速度下降,收敛点通常也好。

实操中,我们不会只用一个固定学习率。一开始用较大的学习率让参数快速进入一个“合理的区间”,后面再通过调度器逐步衰减。这里要提一个重要概念:网络的不同层对学习率的敏感度完全不一样。比如靠近输入的底层,梯度数值通常比较小,需要相对大的学习率;靠近输出的顶层,梯度可能比较大,学习率太猛反而会震荡。SGD本身不管这些,所以后来才会有Layer-wise Adaptive学习率这类变体(比如LARS、LAMB),大模型预训练里用得多。

1.3 小实验感受一下SGD的短板

如果你自己动手做一个二维的Loss曲面可视化,比如一个狭长的椭圆等高线,你会发现SGD走的是“Z字形”路线。沿着梯度方向分解后,一个轴的梯度小、另一个轴的梯度大,SGD就在两个轴之间摇摆,收敛速度非常慢。

这时候你就能直观理解为什么需要动量了。动量解决的正是这种“误差面在不同方向上的尺度差异导致的震荡”问题。它不是把每次梯度都当成瞬时方向,而是累积历史梯度的趋势,让更新方向更平滑。

提示:在做大模型训练调试时,先固定优化器为SGD+momentum或者Adam,然后专心调学习率、batch size、数据质量。频繁更换优化器,会让问题排查变得非常困难,因为你不知道是哪一类改动引起的提升或恶化。

2. EMA与Momentum:同样做滑动,一个在参数域一个在梯度域

2.1 EMA到底是谁的“平均”

EMA全称Exponential Moving Average,指数滑动平均。在深度学习讨论中,它经常出现在两个场景里:一个是模型参数的滑动平均,另一个是梯度的滑动平均。这两者经常被混着说,我建议先把它们分开。

参数EMA,往往出现在“指数滑动平均模型”或“EMA model”中。它维护一份参数的影子拷贝,每次更新完主模型后,影子参数也做一次滑动更新:

shadow = decay * shadow + (1 - decay) * main_param

在推理或评估时,用shadow参数往往比直接用当前训练参数更稳定、准确率更高。这是因为训练后期参数会在最优解附近来回晃动,EMA把这些抖动平滑掉了,相当于拿到了“一段时间的平均模型”。

梯度EMA,则是Momentum的雏形。它滑动平均的是累积的梯度方向,而不是参数本身。注意,虽然都叫滑动平均,物理含义截然不同,别把它们混为一谈。

2.2 经典Momentum的计算方式和直觉

经典Momentum更新方式:

v = mu * v + lr * g θ = θ - v

也有另一种写法,把mu乘在历史梯度上再做更新,本质上差别不大,关键是mu这个系数怎么理解。mu通常取0.9、0.99这些值,核心含义是:更新方向主要继承历史速度,当前梯度只提供“修正”。想象你推一个购物车下坡:真正的移动方向不只看脚下这一步,还要看车已经有的速度方向。

好处有两个:

  1. 梯度方向在多个batch之间不一致时,速度向量会起到平滑、平均的作用,减少来回震荡。
  2. 在梯度比较小的平坦区域,历史速度能“带着”参数快速穿过,而不是每一步都从头起步。

这就是为什么加了Momentum之后,SGD的收敛速度通常能快一个量级。官方一点说,动量法把“一阶梯度信息”扩展成了“一阶梯度历史信息”,所以它能部分抵消曲率差异带来的影响,但注意只是部分。

2.3 从Momentum到NAG:往哪里看决定了看向哪里

Nesterov Accelerated Gradient(NAG)算是Momentum的小改版:先沿着速度方向走半步,再计算那个位置的梯度。公式看起来是:

θ_lookahead = θ - mu * v g_lookahead = gradient(θ_lookahead) v = mu * v + lr * g_lookahead θ = θ - v

这样做的直觉是:你既然知道车有惯性,就应该事先往前看一点,而不是站在当前位置才决定方向。NAG在很多凸优化问题上比标准Momentum收敛得更快、更稳,但在深度学习里,使用率远不如Adam系列。原因倒不是它没用,而是Adam系列把计算和使用复杂度均衡得更好,大家习惯了默认Adam而已。不过SGD+NAG仍然是很多研究者做精细调优时的选择,尤其在CV类任务上。

对大模型场景来说,Momentum和NAG都只是基础成分。真正把SGD家族推向大规模训练的是LARS和LAMB,它们会在不同层上分别调整学习率。但那是后话,本篇先把当前这条主线走完。

3. RMSProp:为什么不直接把梯度平方做成全局平均

3.1 从AdaGrad到RMSProp的改进逻辑

在RMSProp之前,AdaGrad先提出了一种自适应学习率思路:每个参数都维护它历史梯度的平方和,然后用“全局学习率 / 历史梯度平方和的开方”作为该参数的实际步长。这样做的效果是:梯度大的参数,它的学习率会被压小;梯度小的参数,学习率会相对放大。

但AdaGrad有个致命问题:历史梯度平方和是一直累加的,越到训练后期,分母就越大,学习率会被压到接近于零,几乎学不动。RMSProp的改动很巧妙:不再累加全部历史平方和,而是用滑动平均的方式计算梯度平方的估计:

v = beta * v + (1 - beta) * g^2 θ = θ - lr * g / sqrt(v + eps)

这里的beta通常取0.9或0.99,v便成了梯度的二阶矩估计(说白了就是梯度平方的滑动平均)。这样既保留了“自适应学习率”的思想,又不会让分母无限增长,能够伴随长期训练持续更新。

3.2 RMSProp解决了什么、没解决什么

RMSProp解决的主要是“各参数梯度尺度不一致”引发的问题。有些参数梯度常常很小,有些参数梯度常常很大,如果统一用同一个学习率,就会导致一部分参数学得太慢,另一部分参数震荡。RMSProp在每个时间步上按梯度的均方根对学习率做归一化,让每个参数的更新步长大致在同一量级,而不是单纯被梯度大小主导。

但它没有解决另一个问题:更新方向仍然带有很多噪声。RMSProp虽然调整了步长,却没有像Momentum那样累积历史方向。所以实际使用中,我们很少单独用RMSProp,更多是在Adam里看到它的影子——Adam把Momentum和RMSProp的思路合二为一。你可以这么理解:

  • Momentum管方向:让更新方向更平滑。
  • RMSProp管步长:让每个参数的更新步长更合理。

如果把RMSProp当作炒菜的“火候”调节器,它只知道根据食材厚度控制火力大小,却不知道颠勺方向怎么变。

提示:RMSProp里的epsilon不是给分母防除零那么简单的。它实际也在调节“更新步长”的上限:如果eps设得偏大,sqrt(v + eps)整体变大,步长会被整体缩小。大模型训练中ercoef的常用默认值在1e-8到1e-6之间,别为了省事随便调大。

3.3 为什么说RMSProp是AdamW的直接父亲

Adam的计算公式里,归一化项就是“梯度的RMSProp分子”,只是在此基础上增加了一步偏差修正。所以如果你想真正理解Adam,必须有RMSProp这个前序知识。很多人直接跳学Adam,然后看到公式里一堆beta1、beta2和带帽号的一二阶矩估计,会觉得非常抽象。但如果你先懂了RMSProp的v = beta * v + (1-beta) * g^2,再看Adam就会发现:Adam不过是在这个基础上,把分子换成了Momentum的m,并做了一层层偏差校正。

4. Adam与AdamW:两个动量配合下的稳定更新

4.1 Adam是Momentum和RMSProp的合并

把之前两条主线合起来,我们就能手绘出Adam的结构。它维护两个状态量:

m = beta1 * m + (1 - beta1) * g v = beta2 * v + (1 - beta2) * g^2

其中m是梯度的一阶矩估计(带方向),v是梯度的二阶矩估计(带强度)。Adam更新时还会做偏差修正,因为m和v在初始化时都为零,前几步会严重偏小,直接使用会低估真实矩。修正方式如下:

m_hat = m / (1 - beta1^t) v_hat = v / (1 - beta2^t)

其中t是训练步数。之后更新参数:

θ = θ - lr * m_hat / (sqrt(v_hat) + eps)

默认超参数一般是beta1=0.9、beta2=0.999、eps=1e-8。这些默认值在多数CV、NLP任务上表现都不错,所以Adam成了“开箱即用”的典型代表。

我自己的体验是:Adam对学习率的敏感度远低于SGD。即便初始学习率差三倍,Adam往往都能训练到一个可用的水平,而SGD可能会因为学习率大一点直接炸掉。这个特点让它特别适合做新任务的首次训练,你先用Adam跑通,再考虑用别的优化器精调。

4.2 为什么原版Adam在大模型场景里表现不佳

这里得聊一下原版Adam身上的一个隐藏问题:L2正则化与自适应学习率之间的耦合。

很多人在PyTorch里通过weight_decay参数给Adam加上L2正则,也就是在loss里加一个λ||θ||²项,等价于每次更新时把参数往原点拉一点点。如果是在普通SGD里,这就是标准的权重衰减。但Adam会自动调整每个参数的学习率:对于梯度大、二阶矩大的参数,全局学习率被缩小;参数本身偏离也大,那么梯度里权重的二次项部分也会大,自适应学习率把它的更新压得更小了。

结果就是:L2正则项在Adam下被“自适应”削弱了,惩罚的实际效果远不如你在SGD里加weight_decay那么直接。倒过来说就是,Adam加L2正则的方式相当于隐含了“对权重衰减做了某种逐参数缩放”,这在正则化语义上是不干净的。

AdamW的做法是把weight decay从梯度里拿出来,直接在参数更新后再做一步衰减:

θ = θ - lr * m_hat / (sqrt(v_hat) + eps) - lr * weight_decay * θ

这样weight decay就不参与自适应学习率的分子、分母计算,语义清晰:无论这个参数的梯度多大、更新步长缩放成什么样,最后都统一按照相同比例把参数往原点拉。

4.3 AdamW在大模型训练里的相对优势

大模型参数量动辄上亿,正则化的作用不能忽略;而Adam的自适应机制又非常有用,所以AdamW自然成了预训练的主流选择。它保留了Adam快速、稳定、对学习率不太敏感的优点,同时把权重衰减还原成一个独立可控的正则项,超参数也更可解释。

还有一个实际工程上的原因:原版Adam如果你直接加weight_decay,为了达到同样的正则效果,可能要专门调大weight_decay系数,而调大之后又会因为自适应缩放导致某些层的参数变化不均衡。AdamW则把“权重衰减系数”变得非常直观:0.01就是0.01,不会因为某个参数梯度过大而悄悄失效。所以我现在的建议是:新项目优化器首选AdamW,尤其当模型规模超过千万级参数时,几乎可以把它当成默认配置。

4.4 一张表看明白几类优化器差异

优化器维护的关键状态自适应步长方向平滑权重衰减实现
SGD无 / 仅momentum否可选中直接L2正则
Momentum一阶矩m否是直接L2正则
RMSProp二阶矩v是否直接L2正则
Adam一阶矩m+二阶矩v是是耦合的L2正则
AdamW一阶矩m+二阶矩v是是解耦的权重衰减

这张表的价值在于,遇到训练问题时你能快速判断问题出在哪一类:如果更新方向震荡严重,第一反应是查动量相关的beta1;如果参数更新步长忽大忽小、loss跳得很异常,查二阶矩相关beta2和eps;如果是正则化效果不明显,先看weight_decay是不是被Adam的自适应机制削弱了。

5. 大模型场景里的真实选择:参数量、显存和混合精度如何处理优化器

5.1 优化器的显存开销比你想的更大

很多人只盯着模型参数量算显存,结果一跑就OOM,原因往往忽略了优化器状态。Adam/AdamW每个参数需要保存两个额外状态:m和v,都是和参数同尺寸的浮点张量。如果是FP32训练,每个参数等于增加8字节的优化器状态,再加上模型权重4字节、梯度4字节,总共就要16字节。换句话说:

一个70亿参数的模型,训练时仅模型权重加梯度加Adam状态就占用大约112GB显存。这也解释了为什么大模型单卡训练必须引入各种省显存手段,比如混合精度、ZeRO、梯度检查点。

5.2 混合精度下优化器件态怎么处理

混合精度训练(AMP)里,模型权重通常有FP32的master copy,前向计算用FP16/BF16,反向得到的梯度也处于低精度,然后再更新FP32参数。Adam的两份状态m和v一般保留在FP32里,以免累积误差。

但有的框架会尝试压缩优化器状态,比如用BF16来保存Adam状态。这是可行的,因为BF16有和FP32一样的动态范围,只是精度低一些;靠少量精度损失换来几乎四分之一的显存节省。在大规模训练中,这被称为“瓦解法”式优化,是微软Zero++和部分工业框架的常用思路之一。

实操上,如果你的显存很紧张,几个步骤可以按顺序做:

  1. 使用AdamW但把optimizer_state设置为低精度保存。
  2. 打开梯度累积,但注意不是把所有batch拼成一个,而是多次小batch累加梯度后统一更新。
  3. 开启ZeRO Stage1,把优化器状态切分到多卡上。
  4. 实在不行再考虑换用AdaFactor这类省内存优化器。

5.3 大模型为什么总体偏爱AdamW而不是纯SGD

这里有个反直觉的地方:论文里很多理论结果用SGD就能证明,业界大规模训练却普遍用AdamW。原因是理论场景和大规模实操场景不一样。大模型训练中,我们经常只训练一个epoch(或很有限的step),数据量巨大且noisy,模型结构和数据分布都很复杂,很难用手调SGD的精细超参数来适配。AdamW的自适应步长、很好的默认超参数配置,让它以最小的工程代价换来了稳定的训练曲线。

另外,大模型里大量的稀疏激活特征(比如某些token、某些attention head产生的梯度很不均匀),Adam的自适应机制对这种不均匀梯度的容忍度远超SGD。所以如果你追求稳定、省心、显存足够,在外面跑预训练用AdamW是稳妥的。

6. 学习率、weight decay与参数组:训练稳定性最后三块拼图

6.1 warmup为什么和AdamW是黄金搭档

如果你用AdamW直接从头开始训练一个语言模型,前期loss很可能起飞或者直接NaN。原因之一就是Adam的v是从0开始累积的,前几步的更新会被偏差修正放大,导致实际学习率远大于你设置的值。warmup的作用是把学习率从接近0逐步升到目标值,让网络先稳定地适应一下数据分布和梯度尺度,再进入正式快跑阶段。

常见的warmup实现有线性warmup、指数warmup和基于步数的余弦warmup。我建议至少对大模型做1000步以上的warmup。比如总训练步数10万步,warmup设为2000步,cosine anneal到最小学习率为最大学习率的0.1倍,这是一个很稳的起点。

6.2 weight_decay该给多少

AdamW默认的weight_decay一般在0.01到0.1之间。在视觉模型里0.05很常见,在语言模型里0.01到0.1都有人用。实际中,如果你发现训练loss能降,但泛化不行,可以尝试增大weight_decay;如果你发现loss降不下来,且模型偏欠拟合,可以降低或者设为0。

要注意weight_decay太大对大模型是一个典型灾难:它会持续抑制参数模长,导致有效表达能力下降,loss居高不下。如果训练很久loss还不降,先排查这个超参数,别一上来就改模型结构。

6.3 参数分组你真的用了吗

这里分享一个我自己踩过的坑。用AdamW时,我把所有参数混在一起优化,embedding层和attention层共享同一个学习率和weight_decay。结果模型在部分任务上表现平平,后来把embedding的学习率调低一点、weight_decay设高一点,attention层保持稍高学习率,效果提升了不少。

参数分组的底层逻辑是:embedding矩阵往往会变得稀疏,权重模长大,梯度相对密集;norm层和bias项则通常不需要weight_decay。一个比较通用的分组策略是:

  • 对bias和LayerNorm/batch norm的gamma、beta不施加weight_decay;
  • embedding层可降低学习率,防止梯度冲太大导致embedding向量退化;
  • 分类头或者输出层学习率可以适当放大,因为它随机初始化、需要学得更快;
  • 主干网络保持默认。

这个策略在很多开源库中都能看到,例如HuggingFace Trainer的参数分组逻辑就是类似思路。自己写训练循环时不要偷懒,先把参数分成需要weight_decay和不需要weight_decay两组,再设置不同的学习倍率。

7. 发散、loss不降和NaN:我一个一个排查过的问题

7.1 loss直接变NaN的排查顺序

大模型训练最吓人的就是跑到某个step,loss突然变成NaN。不要慌,按顺序查:

  1. 先看学习率是不是太高。如果loss在前期就很快起飞,先把学习率降10倍。
  2. 看是否有inf的梯度。在loss.backward之后,检查梯度里是否有inf或NaN,可以在每个step里加一个gradient clamp或assert。
  3. 检查模型里是否有数值不稳定的操作,比如没有加epsilon的log、除0、softmax温度过低、attention里没有scale。
  4. 检查数据是否存在异常值,比如某些样本给模型带来了错误标签或特别大的输入。
  5. 如果用了FP16混合精度,检查loss scaling是否合理。scale太小梯度会消失,scale太大梯度溢出变inf。
  6. 如果你用了AdamW但beta2设置过小(比如0.9),二阶矩估计跟不上梯度变化,也会偶发震荡到NaN,可以试着把beta2调回0.999。

7.2 loss不是NaN却怎么都不下降

这种情况通常和优化器的状态很小没关系,反而更应该反查数据流和网络结构。我给一个实用的诊断流程:

  • 先用很小的模型过拟合一个batch,确认训练loss能降到接近0。如果过拟合不了,问题一定在网络本身或数据读取。
  • 确认梯度确实传到了每一层。打印各层参数的梯度norm,看底层是否有梯度弥散。
  • 检查学习率是否过小,导致loss长时间缓慢下降,曲线看起来像是平的。
  • 如果用了warmup,看看学习率现在是不是还在很低的阶段。
  • 检查weight_decay是否过大。前面我提到过,如果weight_decay设成0.5甚至1,loss很可能被压制到无法下降。

7.3 优化器换了一百种也没用,问题根本不在优化器

这是我最想强调的经验:自己在任务里试了一轮SGD、Momentum、Adam、AdamW,loss都表现糟糕,后来发现是数据样本顺序不对,或者embedding层初始化过小。优化器只能在给定损失面形状下做局部搜索,它没法解决“损失面本身有问题”的情况。

所以,我一般把优化器的调试放在整个训练流程的中后段:先保证模型能拟合、数据没毛病、loss数值稳定,再回头改优化器和学习率。否则很容易陷入“今天换一个优化器,明天调一个参数”的无穷循环里。

在我自己看来,优化器是训练系统里最容易被误解的部件。很多人误以为“越高级的优化器越好”,可实际的工程经验是:高级优化器的默认值确实省心,但只有在数据和模型都健康的情况下,它的优势才能体现。SGD那看似原始的更新方式,反而能帮你最快暴露训练链路底层的问题。如果你把SGD、Momentum、RMSProp、Adam/AdamW这条演进线吃透了,以后遇到新的优化器变体,比如LAMB、LION、AdaFactor,你会很容易判断出它们改的是方向、步长、内存占用还是正则化语义,而不是带着一种“到底哪个更高级”的茫然去调参。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询