☰
深度学习优化器全解析:从SGD到Adam的演进与调参实战
2026/10/22 8:39:42 网站建设 项目流程

训练深度学习模型,你大概率有过这样的经历:同一个模型结构,别人跑出来损失曲线平滑下降、指标步步高升,你跑出来却像是在坐过山车,loss震荡到怀疑人生,或者干脆卡在一个平台期怎么都下不去。这时候很多人第一反应是改网络结构、加数据,但往往忽略了最基础也最关键的一个环节——优化器。模型能不能收敛、收敛得快不快、最终效果好不好,优化器起着决定性的作用。SGD、Momentum、RMSprop、Adam这四个名字你肯定都听过,但你真的清楚它们之间的演进关系、每个超参数背后的意义,以及在实际训练中该怎么选、怎么调吗?这篇文章就从原理讲到实操,把这四大优化器彻底讲透,顺便把我这几年踩过的坑和总结出来的调试经验一并分享出来。

1. 优化器到底在解决什么问题

1.1 梯度下降的两大痛点

要理解优化器,得先回到最原始的问题:我们训练神经网络,本质上是在做一个高维空间里的数值优化——找一个参数组合,让损失函数的值尽可能小。最朴素的做法就是梯度下降:算出损失对每个参数的梯度,然后沿着梯度的反方向迈一步。

但真正跑起来就会发现,这条路没这么简单。第一个痛点是梯度本身的噪声。实际训练中我们几乎不会用全量数据算梯度,而是用mini-batch,每个batch的梯度都是对真实梯度的一个有偏估计,带有随机噪声。这就导致参数更新方向忽左忽右,损失曲线呈现出明显的锯齿状震荡。尤其是在损失曲面比较狭长的区域——比如某个方向坡度很陡、另一个方向坡度很缓——梯度下降会沿着陡峭方向来回弹跳,而缓坡方向推进得极其缓慢,整体收敛效率非常低。

第二个痛点是学习率的选择。设置太大,参数会在最优解附近反复横跳甚至发散;设置太小,收敛速度慢到让人失去耐心。更麻烦的是,同一组学习率很难同时适配所有参数——有的参数梯度大,有的参数梯度小,有的参数更新频繁,有的参数偶尔才动一下。固定的全局学习率本质上是在用一种一刀切的方式管理所有参数,这显然不够精细。

优化器家族的演进,其实就是针对这两个痛点不断打补丁的过程。SGD是原版,Momentum解决梯度噪声和震荡问题,RMSprop解决参数自适应步长的问题,Adam把两者合到一起。理解了这条主线,后面所有公式你都不会觉得是死记硬背。

1.2 优化器设计的两条主线

纵观所有主流优化器,你只需要抓住两条设计主线。

第一条是动量。思路很直白:参数更新方向不应该只由当前这一步梯度决定,而应该综合考虑历史梯度的趋势。就像你推一个沉重的购物车,你不会每走一步就完全改变发力方向,而是会顺着已有的速度方向,再叠加当前推力的修正。这样即使某一步的梯度因为噪声出现了瞬时反向,购物车也不会立刻掉头,轨迹自然平滑很多。Momentum优化器就是这条思路的直接实现,而Adam中的一阶矩估计本质上也是动量。

第二条是自适应学习率。不同参数的重要性和梯度尺度不一样,最好每个参数都有自己的步长调控机制。梯度大的参数让步长小一点,防止震荡;梯度小或更新稀疏的参数让步长大一点,加快推进。RMSprop和Adam里的二阶矩估计走的就是这条线。

可以说,你现在用到的几乎所有优化器,都是在动量与自适应学习率这两个维度上做文章。明确了这一点,看论文里那些变体公式就不会一头雾水了。

2. 指数加权平均EWA:一切优化器的地基

2.1 EWA的核心公式与直觉

通常大家口头管它叫EMA,Exponential Moving Average,指数滑动平均,标题中的EWA是同一个东西——指数加权平均。你可以在很多地方看到它的身影:优化器里有,BatchNorm的统计量更新里有,模型参数平滑(比如SWA)里也有。它是理解Momentum、RMSprop和Adam的前置知识,必须单独拎出来讲。

公式非常简洁:

v_t = beta * v_{t-1} + (1 - beta) * theta_t

theta_t是当前时刻的观测值,v_t是平滑后的估计值,beta是衰减系数,通常取0.9、0.99、0.999这类接近1的数。把公式展开你会发现,v_t实际上是历史所有观测值的加权求和,权重按指数衰减——越久远的数据权重越小。beta越接近1,平滑作用越强,v_t对近期变化的响应越迟钝;beta越小,v_t越紧跟最新观测值。

举一个具体的例子帮助建立直觉。假设你在记录一个月的日均气温,每天的温度噪声很大,你想看整体趋势。直接画原始曲线会非常毛糙,而用EWA处理后的曲线就平滑得多。beta=0.9时,前一天的权重约为0.1,前两天的权重约为0.09,前三天的权重约为0.081,权重衰减较快,曲线比较灵敏;beta=0.99时,权重衰减极慢,曲线非常平滑,但对突变完全无感。

在优化器语境里,我们平滑的对象不是气温,而是梯度。后面要讲的Momentum就是对历史梯度做指数加权平均,用平滑后的梯度去更新参数。

2.2 偏差修正:初始阶段不能忽视的细节

EWA虽然好用,但有一个小陷阱:初始阶段。v_0如果初始化为0,那么前几步的v_t被严重拉向0。比如beta=0.9时,第一步v_1 = 0.9 * 0 + 0.1 * theta_1,这只是一个打了0.1折扣的\theta_1,明显偏小。在优化器里,这意味着训练刚开始时参数更新步长被无端压缩,梯队没跑起来就被摁住了。

解决方案是除以一个偏差修正系数:

v_hat_t = v_t / (1 - beta^t)

t是当前时间步。t=1时,分母是1-beta,恰好把第一步被压缩的量恢复回来;t逐渐增大,分母趋近于1,修正项的影响逐渐消失。这个细节在原始Momentum里不常用到,但Adam的标准实现里是明确写出来的——这也是Adam初始收敛表现优于普通Momentum的原因之一。如果你自己手写优化器,千万记得补上这一步。

2.3 EW A与滑动平均:实操中的超参数调和

EWA里唯一需要调的超参数就是beta。beta越大,历史信息保留得越长,平滑力度越强,但也会带来严重的"延迟"——例如beta=0.999时,v_t对观测值变化的响应滞后非常明显,适合极慢变化、噪声极大的场景;在优化器中,像RMSprop的二阶矩估计就需要beta接近0.999甚至0.9999,因为梯度平方值的尺度差异可以极大,平滑时间长一点才能给出稳定的估计。

一个小技巧是:当你在代码里看到类似momentum=0.9或beta2=0.999的参数时,不要只把它当成一个抽象数字,可以换一种理解方式——它约等于"最近多少步的滑动平均"。简单估算一下,beta=0.9约合最近10步的有效窗口,beta=0.99约合100步,beta=0.999约合1000步。这个换算在调试时很管用,比如发现RMSprop的均方根值不稳定,你可以想想是不是beta2设得太大,导致窗口过长、跟不上梯度变化。

3. Momentum:借力打力,冲过局部最小值

3.1 动量机制的直观理解

Momentum在SGD基础上增加了一个速度变量v,更新方式变成:

v_t = mu * v_{t-1} + lr * g_t

theta = theta - v_t

其中mu是动量系数,通常取0.9,默认就是SGD+momentum。直观上,v_t累积了过去所有更新方向的信息,而当前梯度只对速度方向做一次修正。这样有两个直接效果:一是噪声被平均掉了,参数轨迹更平滑;二是在损失曲面的缓坡方向,即使单步梯度很小,只要方向一致,速度会不断累积,前进的步子会越来越大,有效突破了固定学习率带来的缓坡推进困难。

做一个生活化类比:想象你在一个碗里弹弹珠。普通SGD相当于每一步都根据当前受力重新调整方向,弹珠走得磕磕绊绊;Momentum相当于弹珠有了一定的惯性,它在碗壁上冲撞后会带着速度继续往前滑,不仅更平滑,而且更容易冲过一些很浅的局部小坑——只要动能足够,它不会在那个小坑里停下。

值得注意的是,这里的l r乘在梯度上,而有些实现会把lr乘在整个更新项上,数值上等价但初始行为略有差异。实际操作里我习惯把lr单独提出来,这样调整学习率时不会影响v的历史累积方向。

3.2 动量系数mu与学习率的配合

动量系数mu的常规取值是0.9,偶尔用0.99,极少超过0.999。mu越大,历史速度的惯性越强,参数更新的方向越"固执",对当前梯度的响应越弱。这在训练初期可能是个问题,因为初始梯度方向往往不够可靠,过大的惯性会让模型在错误的道路上走很远。

所以实操中有两个常用做法。一个是小步快跑:把学习率调低,配合较大的动量。因为惯性大容易冲过头,用更小的lr每步修正一点。另一个是做warmup,训练前几百步让lr从很小数值逐渐升到目标值,这可以有效避免大动量在初始阶段被噪声梯度带偏。如果你发现带Momentum的SGD训练初期loss在飙升,先别急着调结构,把momentum从0.9降到0.5跑几百步看趋势,通常就能稳住。

3.3 与SGD对比:什么时候还是得用Momentum

现在很多论文和框架默认用Adam,但Momentum SGD在不少场景下依然不可替代。尤其是图像分类、目标检测这类CV任务,大量实践表明SGD+momentum配合适当的lr schedule,最终泛化性能往往优于Adam。原因学界有各种解释,一个比较广泛接受的说法是Adam的自适应步长策略在某些情况下导致参数落入了泛化较差的局部极小区域,而SGD的恒定步长则更容易找到平坦的极小值。

所以如果你做的是ImageNet级别的分类或检测任务,迁移学习时别人提供的默认配置大概率就是SGD momentum,lr从0.01到0.1不等。这时候不要自作主张换Adam,先沿袭原配置跑通再优化。做NLP、时间序列、生成模型这类任务,Adam系列才是默认选择。后面第5节我会详细展开这个选型问题。

4. RMSprop:按参数维度自适应步长

4.1 均方根意味着什么

RMSprop全称Root Mean Square Propagation,核心是对梯度平方做指数加权平均,然后让每个参数除以自己历史梯度均方根的平方根。更新规则:

s_t = beta * s_{t-1} + (1 - beta) * g_t^2

theta = theta - lr * g_t / (sqrt(s_t) + epsilon)

这里s_t就是对"梯度平方"的EWA。简单理解,s_t度量了该参数历史梯度的平均大小。如果某个参数在过去几轮梯度很大,s_t就大,那么当前更新步长就缩小,防止速度过快;如果某个参数梯度一直很小,s_t就小,更新步长相对放大,让参数不会因为梯度太小而停滞不前。这就实现了参数级别的自适应学习率。

用生活语言讲,RMSprop是一个会"看人下菜碟"的优化器——每个参数都有自己的步长,不再吃大锅饭。

一个容易误解的地方是:为什么用梯度平方的均方根,而不是直接用梯度的绝对值?原因主要有二。平方操作天然消除了正负方向信息,只保留大小,这正是我们需要的——让更新方向完全交给动量或当前梯度,自适应机制只管调节步长幅度。另外,平方放大了大梯度和小梯度之间的差距,对异常大梯度的压制效果更强,这在处理梯度爆炸场景时尤其有效。

4.2 epsilon那么小,却极其关键

公式末尾加那个epsilon,很多人认为是防止除零,给它一个小值1e-8就完事了。但它在实际训练里的作用远不止于此。

当s_t非常小——比如某参数梯度长期非常小,平方后更加接近0——那么sqrt(s_t)趋近于0,如果没有epsilon,g_t / sqrt(s_t)会变成一个天文数字,参数瞬间被更新到天边去。epsilon的存在相当于给这个除法设置了一个下限,限制了步长的最大幅度。你可以把它理解成在除法器后面串了一个保险丝。epsilon越小,自适应能力越强,但也越容易在某些极端情况下不稳定;epsilon越大,步长上限越低,训练越稳,但自适应调节能力被削弱。

在混合精度训练或低精度推理的场景下,epsilon的选择尤其重要。FP16下的下溢出问题会让梯度平方变为0,继而导致除零放大,此时往往需要把epsilon调大一些,比如1e-6甚至1e-4。不少人在使用AMP时发现训练不稳,第一反应是调学习率,其实先检查一下优化器epsilon是否匹配精度范围更有效。

4.3 与AdaGrad、AdaDelta的关系

理解RMSprop绕不开AdaGrad。AdaGrad的做法是把历史全部梯度平方求和再开根号,作为分母缩放。它的缺陷随时间暴露,梯度平方和不断累积越来越大,分母单调增大导致更新步长越来越小,后期参数基本被"冻住",这也是很多用AdaGrad训练的模型后期收敛非常慢的原因。

RMSprop用EWA替代了累加求和,历史梯度平方的贡献随时间指数衰减,分母不会无限增大,从而解决了AdaGrad的"学习率死亡"问题。AdaDelta则是在RMSprop基础上进一步改进了更新量纲,把更新步长也用动量平滑,减少了对学习率设置的敏感度。

顺着这条脉络你会发现,RMSprop其实不是一个独立的发明,更像是在AdaGrad的框架上做了一次"遗忘机制"的修正。了解这些历史渊源,你在看框架源码里的各种参数名时就能举一反三——比如PyTorch里RMSprop的alpha参数对应beta,momentum参数则是另外加的加速项。

5. Adam:集大成者,并非灵丹妙药

5.1 Adam的算法流程与直觉

Adam把Momentum和RMSprop做了缝合,同时维护一阶矩估计m_t和二阶矩估计v_t:

m_t = beta1 * m_{t-1} + (1 - beta1) * g_t

v_t = beta2 * v_{t-1} + (1 - beta2) * g_t^2

m_hat_t = m_t / (1 - beta1^t)

v_hat_t = v_t / (1 - beta2^t)

theta = theta - lr * m_hat_t / (sqrt(v_hat_t) + epsilon)

简单讲,m_t是带动量的梯度平滑,决定更新方向;v_t是梯度平方的平滑,决定每个参数的步长缩放。除以偏差修正项解决前几步估计偏小的问题。最终更新方向是m_hat,更新的步长幅度由sqrt(v_hat)决定——梯度历史较大的方向会自动放慢,梯度历史较小的方向会自动加快。

这套组合在实际使用中相当皮实:既能对抗梯度噪声,又能自适应调整学习率,对超参数的敏感性远低于SGD,在训练初期尤其容易上手。默认参数在大量任务上都表现不错,这也是它成为"默认选择"的原因。

5.2 关键超参数的实操经验值

通俗默认配置是lr=0.001、beta1=0.9、beta2=0.999、epsilon=1e-8。这个配置在多数NLP任务里开箱即用。但你要是做过几轮实际调参,就会知道这套默认值未必是最优的。

beta1控制动量,0.9在大多数任务里够用。如果你发现训练震荡明显,稍微提高beta1到0.95,相当于给方向估计做了更强的平滑;如果训练初始loss下降太慢,可能时因为beta1太大,参数对当前梯度响应迟钝,可以适当降到0.8看看。

beta2控制梯度平方的滑动窗口,0.999意味着约1000步的窗口,这对大部分场景合理。但训练目标变化剧烈或梯度尺度随训练进程有明显趋势变化时,0.999会让v_t更新滞后,自适应力减弱。遇到这种情况,把beta2调到0.99会让分母估计更灵敏,训练往往会更稳定。反过来对特别稀疏的梯度(比如大部分参数只在少量样本上更新),0.999甚至0.9999更靠谱。

epsilon的取值在三板斧里最容易被忽略。PyTorch和TensorFlow的默认值不一样,分别约等于1e-8和1e-7。混合精度训练中如果发现loss曲线出现毛刺,先试着把epsilon提高到1e-6,这一步很多老手都不知道。原理我在RMSprop那节已经说明,Adam同样适用。

5.3 Adam的泛化问题与AdamW

Adam被诟病最多的一个问题是泛化能力不如SGD+Momentum。很多实验表明,在CV分类任务上,Adam训练出的模型测试集指标通常比SGD momentum略低,差距大概在1到3个百分点。原因解释各有不同,比较公认的一个是Adam的权重衰减正则被"自适应步长"破坏掉了。

传统的L2正则化是在梯度上加上权重衰减项,然后一并进入动量更新和自适应缩放过程。对Adam来说,梯度平方估计v_t把权重衰减项也纳入测量,每个参数的衰减强度实际上被自适应学习率"扭曲"了——梯度大的位置正则变弱,梯度小的位置正则反而变强。这是合理的物理直觉:Adam里的L2正则没有起到均匀衰减权重的功能。

AdamW的改进是把权重衰减从自适应机制中解耦,直接在参数更新后做一次确定的收缩:

theta = theta - lr * m_hat / (sqrt(v_hat) + epsilon) - lr * lambda * theta

这样正则强度就不受二阶矩的影响,每个参数的衰减比例一致,更接近SGD+L2的行为。现在的主流框架已经普遍默认实现AdamW,即使你没有显式设置weight_decay,配置里给的decay值也会按解耦方式生效。如果你还在用老式Adam加weight_decay做预训练,建议换成AdamW,收敛稳定性通常更好。

5.4 学习率调度与Adam的配合

Adam自带自适应步长后,很多人误以为不需要学习率调度了,实际上调度仍然很关键。前已述及,Adam的绝对更新步长仍然正比于lr,只是对每个参数乘了一个自适应缩放因子。如果不做调度,训练后期在一个相对平坦的极小值附近,lr恒定会导致参数一直在附近来回跳动,难以精确收敛到更优位置。

在Bert、GPT这类Transformer模型的训练中,warmup + 线性衰减或cosine退火几乎是标配。warmup阶段(通常占总数5%-10%)让学习率从很小的值涨到峰值,作用是让一阶矩和二阶矩的估计先稳定下来,避免初始的噪声梯度和极不稳定的v_t把模型带偏。cosine退火则是让学习率按cosine曲线平滑降到接近0,给训练尾期一个"精细打磨"的过程。如果你发现Adam训练loss在尾部出现周期性震荡,大概率是lr退火不足——试着把cosine退火的最终学习率降到峰值的1/10甚至1/100,通常能改善。

6. 常见问题速查与调试经验

6.1 训练事故排查表

把这几年的调试经验整理成一张表,遇到问题可以直接对号入座。

现象常见原因优先处理建议
loss震荡剧烈,曲线像锯齿学习率太大 / batch size过小 / beta1太小先降低lr至当前值的1/2或1/5,观察是否平滑
训练初期loss陡增/发散warmup不足 / 初始lr过大 / beta过大加warmup,或暂时调低beta1到0.5验证
训练停滞,loss平台期lr过小 / 模型结构瓶颈 / 数据问题先把lr调回峰值附近,用几十步快速定位
混合精度时训练不稳定epsilon过小,梯度平方下溢出尝试epsilon=1e-4、1e-3逐步上调
验证集指标上不去,训练集很好过拟合 / 权重衰减太弱 / 泛化问题考虑从Adam切AdamW并加大weight_decay

6.2 何时选择哪种优化器

虽然优先记住"任务决定优化器",但大多数人真正需要的是简洁可行的选型策略。

  • NLP、语音、强化学习、生成类任务:默认AdamW,lr从1e-5到3e-4之间起步,根据模型大小调。
  • CV分类、检测:SGD momentum + cosine退火,lr从0.01到0.1之间,配合weight decay,通常比Adam系列效果更好。
  • 训练时间极短、想快速看效果:Adam,默认参数基本不用怎么调,省心。
  • 模型特别大、显存紧张:Adam需要额外保存m和v两份动量状态,显存开销翻倍。此时可以考虑Adafactor等简化版类Adam优化器。
  • 追求极致性能和更长训练周期:AdamW + warmup + cosine退火是最稳妥的组合,多场景通吃。

6.3 学习率与优化器的联动调参法

新手最容易犯的错误是把学习率和优化器割裂开调。事实上优化器的每个超参都会影响最优学习率的取值。

比如Momentum的mu增大了,最优学习率通常会相应调小——惯性大了,单位步长不能再大,否则容易冲过头。二阶矩估计的beta2增大,RMSprop和Adam的最优学习率一般也可以适当提高,因为分母缩放更平滑、单步步长波动更小。

一个经验性流程是:先把优化器超参定到一个稳妥值(比如Adam默认),然后只调lr,观察初始500步的loss下降速度,找到能让loss稳定下降的最大lr,再回头微调优化器超参。不要同时动两个以上变量,否则很快会陷入调参地狱。

6.4 关于"优化器的期末考试":做一次吃透

如果你还是学生或者刚入行,我强烈建议手动实现一次四大优化器。哪怕只是在简单的多项式拟合或一个两层网络上跑通,也需要把梯度、动量缓冲、二阶矩估计、偏差修正这些概念真正落地。很多框架里一行optimizer.Adam(...)就把所有细节藏起来了,但你写一遍之后对每个超参的理解会完全不同。

补一个我在实际项目中的体会:优化器没有绝对的"最好",只有最适合当前任务和训练策略的组合。做预训练大模型和做小规模微调,最优配置可以差很远。先掌握算法原理,再通过系统实验找到适合自己的配置,比单纯背几个默认参数有效得多。

最后分享一个小技巧:每次开新任务,我都会在日志里记录一组基线——优化器类型、lr、weight_decay、batch size、warmup步数,然后跑50到100步看loss曲线形状。这个习惯帮我省下了大量无效调参时间。你也可以试试,它比任何优化器理论知识都更快地帮你摸清当前任务的脾气。

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

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

立即咨询