很多刚接触深度学习的朋友问过我一个很现实的问题:模型结构也搭了,数据也准备好了,但训练的时候loss要么不动,要么像过山车一样乱跳,到底问题出在哪?有人说是学习率没调好,有人说是初始化不对,但我想说,一个经常被忽略又极其关键的因素,其实是优化器。选不对优化器,你连损失函数的长相都摸不清,更别提让模型收敛了。这篇内容不聊数学推导的每个细节,重点讲清楚优化器到底是什么、主流优化器之间的区别、以及我在实际训练任务里摸索出来的选型经验,希望对正在调模型但卡在训练环节的人有帮助。
1. 优化器到底在解决什么问题:不只是在“更新参数”
先打个比方。你站在一座山上,周围全是雾,手里只有一张海拔计,目标是摸黑走到山谷最低点。优化器干的事情就是决定你每一步怎么走——走多快、往哪个方向、要不要看看之前走过的路。参数更新其实就一句话:新参数 = 旧参数 - 学习率 × 更新方向,但关键在于“更新方向”怎么算,这才是各种优化器的本质分歧。
1.1 从梯度下降说起:为什么“按梯度反方向走”不够用
最朴素的梯度下降法,每次更新都直接沿着当前梯度反方向走。这在数学上没毛病,在实际训练中就很难受——如果山谷比较狭长,梯度在某个方向上来回震荡,步长大了会冲过头,步长小了又在陡坡上磨蹭,收敛速度慢得让人抓狂。我早期训练一个小网络时就用纯SGD,loss从2.0降到1.5只花了几百个step,但从1.5再往下走,几乎就卡住了,怎么调学习率都别扭。
问题出在哪?纯SGD只用了当前这一时刻的梯度信息,没有“历史记忆”,所以它对梯度的随机波动特别敏感。想象一下你下坡时每次都重新看地形,完全不记得刚才哪条路比较顺——每一步都可能被局部的小颠簸带偏。实际训练里这表现为loss曲线锯齿感特别强,尤其是在batch比较小的时候,噪声会更大。
1.2 优化器的家族谱系:三类核心改进思路
后来所有优化器的演进,基本都是在解决同一个问题:如何让“走下山”这个动作既快又稳。我梳理了三条主线,理解了这三条线,你再看任何新出的优化器都不会慌:
第一类是动量类,代表是SGD with Momentum。思路就是"记住惯性",如果前几步一直朝同一个方向走,那这一步就顺着惯性再加速一点。这解决的是梯度震荡问题,就像你骑自行车转弯时,不会完全打死方向,而是顺着前轮的方向柔和过渡。
第二类是自适应学习率类,代表是RMSProp和Adam。思路是"每个参数单独配一个步长",更新频繁的参数步长自动变小,更新稀疏的参数步长自动变大。这解决的是不同参数尺度差异大的问题。我举一个实际例子,你在训练一个带Embedding的推荐模型时,有些词出现几万次,有些词只出现几次,如果所有参数用同一个学习率,那些稀疏特征要么学不动,要么学过头。
第三类是修正+融合类,代表是AdamW、NAdam、Lion这些。要么是把前两类优点合起来,要么是发现了原有算法在某些场景下的毛病后打的补丁。
理解了这三条线,下面我逐个拆解主流优化器的时候,你就能清晰地感受到每个算法到底在“动哪个螺丝”。
2. 逐个拆解主流优化器:每个改进背后的代价
2.1 SGD with Momentum:惯性的力量,但要注意动量系数
Momentum的更新公式比纯SGD多了个“速度”变量,这个速度是历史梯度的指数衰减平均。核心超参是momentum,一般设0.9。它的效果非常直观:如果梯度方向一直没变,速度会越滚越大,相当于在平缓方向的坡上“加速”;如果梯度方向频繁翻转,速度的正负项会相互抵消,更新幅度被压制,从而抑制震荡。
不过这个“加速”也有副作用。动量积累过大时,模型很容易冲过极小值点然后在另一边来回震荡,这就是为什么一些人用Momentum时发现loss快收敛了却又反弹。我一般会配合学习率衰减一起用,比如每过若干个epoch把学习率乘一个衰减系数,避免速度积累得太过。
2.2 RMSProp:给每个参数单独一把尺子
RMSProp的核心是维护梯度平方的指数移动平均,然后用它来缩放学习率。它的好处非常明显:对于那些梯度一直很大的参数,分母会变大,步长自动缩小;对于那些梯度很小的参数,分母变小,步长自动放大。效果就是整体训练变得非常稳定,尤其是处理非平稳目标时,比SGD稳得多。
但RMSProp也有自己的毛病——它对那个“历史平方梯度”的初始状态很敏感,而且在训练后期,分母可能积累得比较大,导致步长过小,收敛变慢。我见过有人在训练一个生成模型时,用RMSProp跑了几千步loss都降不下去,后来一查是epsilon设得太小,导致分母除出来数值非常大,学习率被压到了几乎为零的量级。
2.3 Adam:自适应学习率+动量的组合,但不是银弹
Adam把Momentum和RMSProp的思路结合到了一起。它同时维护一阶动量(梯度的均值)和二阶动量(梯度平方的均值),分别在更新方向和学习率缩放上做文章。这是目前我用过训练初期收敛最快、调参压力最小的优化器,特别是CV、NLP大模型场景,几乎默认首选就是Adam。
但Adam有两个广为人知的问题。第一,它的二阶动量估计在早期会偏小,导致初始学习率被放大,所以需要bias correction(偏差修正)。不过PyTorch里已经内置了这项工作,你直接用默认参数就行。第二,也是更实际的问题——Adam在训练后期容易在最优解附近徘徊,有时最终的泛化能力反而不如SGD。后来我读到一些分析,说法是Adam的“自适应学习率”在参数的某些方向上被缩放得太极端,相当于给模型引入了另一种形式的噪声。
2.4 AdamW:主流之选,修正权重衰减的“误伤”
AdamW是我现在用得最多的优化器。它解决的问题听起来很简单:原来的Adam做L2正则化时,会把权重衰减混入梯度里,等于变相改变了梯度的大小,和“衰减权重”这件事本身是矛盾的。AdamW把权重衰减从梯度计算里拆出来,直接在参数更新那一步做,这样正则化行为就更纯粹。
实际用起来,AdamW几乎保留了Adam快速收敛的优点,同时最终精度和泛化性能比Adam更稳。我在一个图像分类任务上对比过,同样的模型结构,AdamW跑到后期能比Adam高出大约0.4到1个百分点的验证集准确率。如果你的任务对最终精度有要求,我建议直接默认AdamW,而不是Adam。
2.5 新势力:NAdam、Lion、Sophia这些要不要追
NAdam相当于给Adam再加了一层Nesterov动量,理论上收敛梯度方向更准,但实际提升幅度有限,除非你在跑一些对稳定性的需求特别高的小任务,否则没必要刻意追新。
Lion是最近看到比较多的优化器,思路很直接——它把一阶动量的符号直接当作更新方向,省掉了“梯度具体大小”的计算。这听起来很奇怪,但它确实在有些大规模视觉模型训练上表现出了比AdamW更好的收敛速度和效果。不过我对它的理解还不够深,体验不多。我的态度是,新优化器可以在小规模任务上做对比实验,但核心生产任务还是选验证过多次的AdamW或SGD。
下面放一个我平时做选型参考的速查表,方便你直接对照着自己的任务需求来挑。
| 优化器 | 核心思想 | 优点 | 典型坑点 | 我的推荐场景 |
|---|---|---|---|---|
| SGD | 当前梯度反方向 | 简单、泛化性不错 | 收敛慢、易震荡 | 数据量不大、模型较浅 |
| Momentum | 动量累积 | 收敛快于SGD,缓解震荡 | 动量系数需要调 | 中小型任务调优 |
| RMSProp | 自适应学习率 | 对非平稳目标友好 | 后期可能停步 | 生成模型、在线学习 |
| Adam | 动量+自适应 | 初期收敛极快 | 后期泛化可能不佳 | 大多数深度学习任务 |
| AdamW | 解耦权重衰减 | 收敛快、泛化好 | 需注意betas设置 | 大模型、CV、NLP主选 |
3. 实测对比:同一个任务换优化器,结果差距有多大
3.1 一次图像分类实验的具体对比
说再多理论,不如跑一次实验来得直观。我之前在某公开图像数据集上做过一次优化器对比实验,用的模型是一个中等规模的图像分类网络,训练策略完全一样,只替换优化器。我记录了每个优化器跑到同样的loss值所需的步数,以及最终验证集精度:
- SGD(momentum=0.9):跑到loss=0.5需要约15个epoch,最终验证率约84.2%。前期很慢,后期稳定。
- Adam(学习率0.001):跑到同样loss只用了约8个epoch,但验证率只到83.6%。收敛快,但后面有隐忧。后面验证率甚至会小幅回退。
- AdamW(学习率0.001,weight_decay=0.01):跑到同样loss约8.5个epoch,最终验证率约85.1%。初期速度和Adam持平,后期继续稳步上升。
这个结果其实很典型。Adam和AdamW在初始阶段速度都碾压SGD,因为它们的自适应学习率能快速适应不同参数的尺度,但到了后期,SGD那种“老老实实沿着梯度走”的方式反而可能找到更平坦的极小值点,泛化能力更强。AdamW通过解耦权重衰减,在一定程度上兼顾了两者的优点,所以在这个测试里综合表现最好。
3.2 为什么SGD的泛化性常常比Adam好:平坦极值的直觉解释
关于“Adam泛化不如SGD”这个问题,一个比较通俗的解释是“尖锐极小值 vs 平坦极小值”的传说。SGD配合合适的训练策略,倾向于收敛到参数数值变化都相对温和的区域,也就是平坦的盆地区域;而Adam的自适应步长在某些方向上冲得太快,可能卡在一个很深的窄缝里。简单说,平坦的极小值对测试数据的微小扰动更不敏感,泛化能力自然更强。
我之前一直没把这点当回事,直到一次在模拟项目的验证集上观察到了过拟合信号:Adam训练出来的模型在验证集上的loss出现了轻微回升,而同期用SGD训练的模型虽然收敛更慢,但验证集表现一直非常稳定。那次之后,我开始重视一个策略:先用AdamW快速找到一个大致好的区域,再切换成SGD精调几个epoch。这个办法听起来有点土,但实测效果非常不错——不光是精度,模型的稳定性也更好。
3.3 不同任务对优化器的敏感度差异
需要特别提醒的是,优化器在同一个任务上的差异,不代表在你自己的任务上也一定这么明显。我对几个不同类型的任务做过观察:
- NLP分类任务,对优化器不那么敏感,主要原因是我通常用预训练模型加微调的套路,预训练权重已经把模型放在一个比较好的起点,优化器只负责小幅调整。
- 目标检测或分割这类带坐标回归的任务,AdamW的优势往往更明显,因为特征图的尺度差异很大,自适应学习率能帮上大忙。
- 生成对抗网络或者扩散模型这类训练稳定性要求高的任务,动量类优化器容易出现模式崩塌,RMSProp或者Adam系的稳定性更靠谱。
所以选优化器不是“哪个强就无脑用哪个”,而是要看你的任务属于哪一类。当然,最稳妥的方法还是在小规模数据上跑几个短实验,用验证集上的表现说话。
4. 优化器调参与避坑:真正影响结果的那几个细节
4.1 学习率才是最大的杠杆
我见过太多人在优化器之间反复横跳,却没想过问题可能出在学习率上。不同优化器对学习率的敏感度完全不同。SGD的安全学习率范围通常在0.1以下,一旦设大了直接发散;Adam和AdamW默认0.001一般都能跑起来,但上限和下限差异非常大。
我的建议是:当你换优化器时,首先重新搜索学习率,而不是沿用上一个优化器的设置。一个简单的经验是,在固定batch size的情况下,把学习率从小到大按十倍间隔试一遍,比如1e-5、1e-4、1e-3、1e-2、1e-1,训练几百个step观察loss是降、震荡还是发散,快速锁定量级,再在量级内细调。
4.2 warmup:为什么训练初期要“慢慢来”
warmup的意思是在最初若干步内,把学习率从很小的值线性增长到目标值。这个技巧在Adam系和Transformer类模型里几乎是标配。原因也很简单:刚初始化时,模型参数离最优解很远,梯度方向可能很不准确,如果一上来就大步走,很容易被带到沟里。先把步子放小,等梯度方向稳定了再加速,能够明显提升训练的稳定性。
我个人在训练大规模模型时习惯把warmup步数设在总步数的5%~10%之间。比如总共有2万步,就先用1000到2000步做warmup。如果任务比较小,warmup效果不明显,但也不会带来明显的危害,可以按需求保留。
4.3 几个容易踩的坑:epsilon、betas、权重衰减
先说epsilon。Adam和AdamW里有个epsilon超参,默认通常是1e-8。它的作用很简单——防止分母除零。但如果你用混合精度训练,数值精度下降,epsilon太小可能让更新步长产生异常跳动,这时候把epsilon加到1e-6甚至1e-7左右往往能提高稳定性。我在一次混合精度训练中遇到loss偶尔变成nan,最后排查到就是epsilon设太小导致的。这个坑不常见,但遇到了会让人很头疼。
再说betas。Adam类默认是(0.9, 0.999),这个配置适应大多数场景。但如果你训练的是稀疏特征极其多的模型,可以考虑把一阶动量系数调小到0.8,让模型对当前梯度的反应更灵敏;如果你发现训练后期波动很大,可以试试把二阶动量系数从0.999调到0.99,让学习率缩放的历史窗口变短,更适应当前梯度。
最后是权重衰减。用AdamW时我习惯把weight_decay设置在0.01到0.1之间,具体值跟模型参数量和训练集规模有关。参数量大、数据量小时,权重衰减可以适当加大,防止过拟合。之前试过把weight_decay从0.01调到0.05,在某个分类任务上验证集准确率提升了约0.6个百分点,说明这个参数不能随意不管。
4.4 一个逐步排查的套路:loss不降时按什么顺序检查
当你遇到loss迟迟不降的情况,我的排查顺序是这样的:
- 先确认数据加载和标签没问题,这个最基础也最容易被忽略。
- 改成小规模数据过拟合测试,看模型能不能把一个batch数据完全记住。如果连这个都做不到,问题大概率在模型结构或优化器配置上。
- 再检查学习率:尝试调大一倍看loss是否变化,如果变大后爆炸说明学习率偏高,如果完全没有变化,说明学习率太小。
- 最后再考虑换优化器:很多情况下,用AdamW替掉Adam或者把SGD换成Momentum版本就能解决一部分问题。
这个顺序能帮你避免一上来就怀疑优化器,其实大部分问题都出在前两步。
5. 混合精度、分布式训练下的优化器特殊处理
5.1 混合精度训练:浮点精度对Adam类算法的影响
现在的GPU基本都支持混合精度训练,也就是部分计算用FP16,部分用FP32,用来省显存、提速度。但这里有个隐藏问题:梯度、动量这些状态在FP16下会丢失精度,尤其是梯度的取值范围波动较大时,小梯度可能会被舍入到零,直接废掉了那些更新幅度小的参数。
主流框架的解决办法是维护一份FP32版本的优化器状态,也就是Master Weight和FP32的动量缓存。这也就是为什么你看很多训练代码里,混精模式下checkpoint的体积比纯FP32还大——模型参数虽然是FP16,但优化器的FP32状态全被保存下来了。
5.2 分布式训练时要不要调整优化器参数
分布式训练本身不改变优化器算法,但会影响等效batch size。当你把batch从256扩到1024时,梯度估计的噪声变小了,所以可以适当调大学习率。经验上,batch变大k倍,学习率可以试着调大到sqrt(k)倍,或者直接线性调大,但前提是要配合warmup避免早期不稳定。
我在一个分布式训练项目里,batch从256扩到2048,把AdamW的学习率从0.001调到0.002,并延长了warmup步数,训练效率和最终效果都有提升。但如果你同时换优化器又调大batch,就很难判断最后的收益到底来自哪个变量——所以我习惯一次只改一个变量。
5.3 优化器状态与断点续训:一个经常被忽略的问题
断点续训时,很多人只保存了模型权重,忘了保存优化器状态。结果就是恢复训练后,模型还能加载,但优化器里的动量、二阶动量全被清零了,训练初期会非常不稳,之前的“惯性”和“步长记忆”全没了。我遇到过跑了几十小时的训练中断,恢复后发现loss反而回升了一截,最后定位到就是没恢复优化器状态。
解决办法很简单,保存checkpoint时把optimizer的state_dict一并保存。加载时注意模型和优化器状态要严格对应,不要用某个中间step的模型配另一个step的优化器状态。这对长周期训练任务来说,是保持训练连贯性的一个根本保障。
6. 我的选型经验与一点可复用的建议
如果让我给出一套相对通用、不容易出错的组合策略,我会这样推荐:
- 中小规模任务,对最终精度要求高,优先考虑AdamW,默认
lr=0.001, betas=(0.9, 0.999), weight_decay=0.01。如果训练速度慢,再考虑切换到SGD+Momentum精调末期。 - 大规模模型预训练,比如Transformer类,AdamW几乎是不二选择,配合warmup和cosine学习率调度。
- 对稳定性要求极高的生成类任务,RMSProp或者带warmup的AdamW更稳,SGD容易因为梯度噪声大而震得让人心慌。
- 时间有限、只想快速验证一个想法的效果,直接用默认参数的AdamW,它至少在90%的场景不会让你翻车。
优化器本身没有绝对的好坏,只有适不适合。我对很多同学的建议是:把大部分精力放在数据质量和模型结构上,优化器只是把模型推向最优解的工具,但选对了它,能让你的训练曲线平滑许多。
最后分享一个自己常用的做法:每次开始一个新任务,先花半个小时写一个小实验脚本,把SGD、AdamW、RMSProp放在同一个小验证集上各跑几十步,对比loss曲线和验证集指标。这个“三选一”小实验成本很低,但能帮你少走很多弯路,比盲目迷信某个优化器可靠得多。