如果你跟我一样,日常要在CV、NLP、多模态这些任务里来回切换,那一定绕不开一个东西:优化器。很多刚入门的同学觉得优化器不就是“选Adam还是SGD”吗,顶多再调个学习率。但真到了项目里,模型结构改了、数据量涨了、分布式并行上了,最先出问题的地方往往就是优化器配置,而不是网络结构本身。这个“Model-Optimizer”看起来像个通用名字,其实核心解决的就是一件事:从优化器选型、学习率调度到权重更新细节,把模型的训练过程稳定下来,让收敛更快、结果更稳、显存不爆。
这篇内容我打算按照自己在一个实际视觉模型项目中调优的经验来写,覆盖优化器的本质拆解、参数选择逻辑、完整落地步骤,以及常见训练事故的排查思路。适合正在训练深度学习模型、但总在“Loss不降”“训练波动大”“换了个优化器反而不如以前”这些问题上卡壳的同学。
1. 先搞懂优化器的本质:从SGD到Adam再到Lion
1.1 为什么每一步更新都要纠结
优化器做的事情,说穿了就是回答一个问题:知道梯度之后,参数到底应该往哪个方向走、走多远。早期的SGD只用梯度的方向,把参数沿着负梯度方向挪一小步,学习率就是那一步的大小。简单,但有痛点:梯度在陡峭方向上来回震荡,平缓方向又推不动,所以带Momentum的SGD出现了——它相当于把历史梯度做了一个指数滑动平均,让更新方向更平滑,就像推一个沉重的球,能冲过小的凹凸区域。
但SGD系优化器对学习率特别敏感。学习率大了,小球直接飞出去;学习率小了,又可能在鞍点和缓坡上磨蹭半天。这时候Adam的登场几乎是必然的。Adam在Momentum之外,额外用梯度平方的滑动平均来“归一化”每个参数的更新步长,相当于给每个参数配了独立的学习率。稀疏梯度、不平稳梯度、不同量级的参数,在Adam手里都能得到适配更新的幅度,这就是为什么这几年几乎所有深度模型的第一选择都是Adam系。
落到项目里,我的体会是:新手别在SGD和Adam之间来回横跳。先把Adam的beta1、beta2是干嘛的搞清楚,再把weight decay放对位置,你就能解决80%的训练不稳定问题。剩下的20%,才是考虑换AdamW、Lion、Sophia这些新角色的时刻。
1.2 主流优化器的演化路径与“为什么”
这里按我自己的理解,把主流优化器分成三代。
第一代是SGD和SGD with Momentum。它们的优势是泛化性好、显存占用极低、行为可解释。即便到了今天,很多图像分类、检测模型的微调阶段,SGD+Momentum依然能打出不错的结果。缺点也明摆着:对学习率和初始化极其敏感,训练大模型或非凸问题时会卡在局部极小值附近动弹不得。
第二代是以Adam、AdaGrad、RMSProp为代表的自适应学习率优化器。Adam把一阶矩(梯度的均值)和二阶矩(梯度平方的均值)结合起来,每个参数都有动态更新步长。阿达的优缺点都很突出。优点是开箱即用、适应面广;缺点是二阶矩估计会随训练变成有偏估计——早期beta2较小时步长过冲,后期又因为二阶矩累积过大导致有效步长偏小,最终“训练不动”或者收敛到宽泛的极值区,泛化有时不如SGD。
第三代开始做“解耦”和“重构”。AdamW把权重衰减从梯度里拆出来,不再跟梯度混合,这让正则和梯度更新各干各的,训练更干净。Lion则另辟蹊径,只用一阶矩的符号做方向,更新幅度全靠学习率和weight decay控制,省显存、省算力,在不少大模型评测里表现还比AdamW更稳。Sophia这类基于二阶信息的优化器也在兴起,但目前工程落地还不够普遍。
我的建议:别神话任何一代。模型结构不同、任务不同,优化器表现会有不小的差异。你自己跑一组对照实验花不了太多时间,但能帮你省掉之后无数次“调参调到头秃”的时间。
1.3 一个很容易被忽略的事实:Adam真的收敛到极小值了吗
一个残酷的事实是,Adam并不总是收敛到好的极小值。原因在于自适应步长改变了损失的几何结构。有时候你在训练集上看到Adam的Loss降得飞快,验证集表现也不错,但你一旦切回SGD再练几十个epoch,效果反而更好,尤其是CNN这类结构。这个现象在ResNet时代就有不少人提过,到了Transformer时代依然存在,只是大家默认“用AdamW就行,别折腾”。
我个人的理解是:Adam虽然每条梯度方向上都能动,但它本质上更偏向于“每个参数均匀地往前走”,所以在loss landscape比较崎岖的时候,它更容易落在宽而平的局部极小值附近——这种位置泛化未必差,但也不是最优的。SGD更多是顺着loss曲线本身走,最后停在的区域往往更“尖”但也更“深”。这不是数学证明,而是大量实验后给人的直觉。实际操作上,我经常用的一个trick是:先用AdamW快速热起来,等loss降到平台期后,切到SGD+Momentum再精调几十个epoch,两者经常能互补。
2. 优化器选型与初始化拆解:参数、场景与匹配逻辑
2.1 选型总览表:什么时候用什么
踩过这么多坑之后,我给自己做了一张优化器选型表。不是说每个项目都照抄,但选错方向会让后续所有调整都变得事倍功半。
| 场景 | 推荐优化器 | 说明 |
|---|---|---|
| 图像分类/检测/分割,数据量中等 | SGD+Momentum 或 AdamW | SGD偏传统任务,AdamW更适合有复杂增强和多分支结构 |
| Transformer(BERT、GPT系、ViT) | AdamW | 自适应更新对注意力权重的稀疏梯度更友好 |
| 大模型预训练/大规模并行 | AdamW(带FP16/BF16)或 Lion | 注意优化器状态带来的额外显存,Lion状态更省 |
| 小样本、稀疏特征、推荐系统 | Adam / AdamW | 每个参数独立学习率的特性适合Embedding类参数 |
| 强化学习策略网络 | Adam | 奖励信号不稳定,自适应步长能增加鲁棒性 |
| 微调阶段追求更强泛化 | 先用AdamW,再切SGD小学习率 | 两阶段策略在我自己的两个项目里都有正收益 |
这里强调一下:上图不是标准答案,而是帮助你缩小范围。真正的选型还是要跑一个baseline。我通常的做法是先拿AdamW默认参数(lr=1e-3或按batch size缩放)跑短若干个epoch,看loss能不能掉下去;能掉下去,再试SGD或其他优化器;掉不下去,先找数据、预处理和模型结构的问题,不要赖优化器。
2.2 学习率与动量:最常用的两组超参数怎么定
优化器里,学习率永远是最核心的超参数。很多人上来就设个默认值,要么太小磨叽到天荒地老,要么太大loss直接飞到NaN。
对于SGD+Momentum,我常用的初始化区间是0.01到0.1,配合momentum=0.9。momentum越大,历史梯度影响越大,方向越平滑,但也更容易冲过头。m=0.9是最常见的起点,0.99适合batch很小、梯度噪声大的情况,但m太大也容易导致收敛震荡加剧。
对于Adam/AdamW,默认lr=1e-3是个不错的起点,但不同模型差异很大。我的经验是:CV里用3e-4到1e-3,NLP里用1e-5到3e-4,大模型里常常低到1e-4甚至更低。如果你的batch size翻倍了,适当把学习率调高一些,但不要严格线性缩放,尤其是用了自适应优化器之后。
至于beta1、beta2,Adam默认是0.9和0.999。beta1控制一阶动量衰减,偏大则方向稳定,偏小则对梯度变化更敏感。beta2控制梯度平方的滑动窗口,偏小可以让更新更激进,常见调法是在长训练任务里把beta2从0.999提高到0.9999,避免后期二阶累积太大导致几乎推不动参数。
2.3 权重衰减放在哪一步:L2正则与解耦权重衰减
这是我最常看到有人搞混的地方。早期Adam论文里,权重衰减是通过在loss里加L2正则项实现的,梯度计算的时候会额外加一项权重。但AdamW发现,这种混合方式在自适应步长下会削弱正则效果,因为L2的梯度也会被二阶矩归一化,导致实际惩罚被缩小。所以AdamW把权重衰减从梯度的计算过程中拆出来,直接在更新参数时减去一个固定比例,这样正则强度与学习率和自适应步长解耦,效果更可控。
实操中,SGD+Momentum常用weight_decay=1e-4到5e-4;AdamW常见的是0.01到0.1。这两个数值差了好几个数量级,正是因为实现方式完全不同。你要是把SGD的1e-4硬搬到AdamW上,基本等于没做正则。反过来,把AdamW的0.1硬搬到SGD上,模型很快就欠拟合了。这是我最早踩过的一个坑。
另一个细节是:是否解耦的weight decay在混合精度训练下表现也不一样。FP16/BF16混合精度下,AdamW的权重衰减实现如果写成param.mul_(1 - lr * weight_decay),那它会自动跟随学习率缩放;如果你手写L2正则再传给优化器,就可能在低精度下精度丢失,导致正则强度飘掉。
3. 实操环节:Model-Optimizer在训练流程中的完整落地方案
3.1 分阶段切换优化器的策略
一次训练从头到尾只用同一个优化器,不能说是错的,但大多不是最优的。我最近在一个细粒度图像分类项目里,把训练分成了三个阶段,效果比单一AdamW涨了接近两个点。
第一阶段是预热期。这个阶段用AdamW,学习率从非常小的值(比如1e-5)慢慢升到目标学习率(3e-4),配合线性warmup,先让模型在训练初期避免大幅震荡,尤其适合结构较深或者加载了预训练权重的模型。第二阶段是稳定期。保持AdamW,配合cosine decay把学习率从3e-4降到1e-5附近,模型逐渐从“大步探索”进入“小步精修”。第三阶段是收尾期。在训练的最后10~20个epoch,我常会切回SGD+Momentum,学习率固定在一个很小的值(2e-3上下),momentum设为0.9,这会让模型在现有最优区域附近做非常精细的搜索。
这里有个操作细节:切换优化器时,无需完全重新开始。优化器状态(momentum和二阶矩缓存)会失效,但这通常不是问题——切到SGD后本来就不需要Adam的二阶矩。你只需要保存好模型权重,用新的优化器重新初始化即可。我一开始还担心切优化器会导致loss反弹,实际跑下来最多在切换后第一个epoch有小幅波动,后面很快稳定下来。
3.2 学习率预热、余弦退火与重启:三种调度器实测
优化器不是孤立组件,学习率调度器对最终结果的影响一点都不亚于优化器本身。我常用三种调度器,这里逐个说下实测感受。
线性预热加余弦退火是我Transformer任务里的标配。预热几百步,解决的是早期梯度二阶矩估计不准导致的“步长过大”问题;余弦退火则让学习率在后半段平滑降低,帮助模型平稳落入较低loss区域。
带热重启的余弦退火(CosineAnnealingWarmRestarts)则适合那种“一个模型要反复迭代”的场景。每隔若干个epoch把学习率突然调大,相当于让模型从当前位置跳出去重新探索。在知识蒸馏和一些生成模型里,这个调度器反而能避免陷入某个固定的模式。代价是训练时间会变长,且不一定每个任务都有收获,需要自己权衡。
此外,阶梯状衰减(StepLR)依然有它的价值。很多人觉得它老土,但在普通CNN任务里,我实测它和cosine相比差距不大,还更直观。比如训练到第30个epoch把学习率降到1/10,观察loss是否继续下降,再决定下一步,非常适合人工介入调整。
调度器选型的原则就一句话:如果你没时间盯训练过程,用cosine;如果你有时间并且想手动干预,用step;如果你在做需要跳出局部极值的生成类任务,可以试试带重启的cosine。
3.3 梯度裁剪与优化器状态:两个常被忽略的细节
梯度裁剪(gradient clipping)看起来是NLP训练才需要做的事,但很多CV项目里的不稳定loss,根本原因就是某个batch里梯度爆炸了。我现在的习惯是:凡是用AdamW训练超过千万级参数的模型,一律给梯度裁剪。最稳的做法是设置一个max_norm,比如1.0,对梯度向量的整体L2范数做clip。它的效果相当于给每一步更新加了一个“安全上限”,即使个别样本产生超大梯度,也不会让参数一步跳坏。
优化器状态则是另一个常被忽略的显存黑洞。以Adam为例,每个参数要额外保存两个跟参数同尺寸的浮点数组——一阶矩和二阶矩。假设你的模型是1B参数,光AdamW的双精状态就是8GB额外显存(1B * 4B * 2 = 8GB,如果FP32),这还没算梯度和参数本身。所以在大模型训练里,有人改用Lion或者用bitsandbytes提供的8-bit Adam来省显存,就是在优化器状态上做压缩。
操作层面,如果你用PyTorch,一个很简单的事后检查是:在保存checkpoint时,看optimizer.state_dict()是否异常大。我遇到过一次训练到一半OOM,排查后才发现是因为优化器状态没清理,继续从checkpoint加载时占用了大量显存。把优化器状态单独存成独立文件,需要续训时再加载,是个好习惯。
4. 常见问题与排查技巧实录
4.1 训练前期Loss不降或炸掉
我第一次训练Transformer时遇到的问题是:loss在前几百步不降反升,甚至直接NaN。当时第一反应是学习率太大,但降到1e-5之后依然如此。后来才发现问题出在数据预处理和模型初始化上——输入里出现了NaN特征,模型层归一化的gamma初始值也不合适。
所以遇到loss爆炸,我建议的排查顺序是这样的:
- 先检查数据里是否有NaN或无穷值,这一步最快。
- 检查label是否正确,是否有部分样本label错乱导致梯度方向冲突。
- 看训练日志里梯度范数变化,如果某个step出现剧烈尖峰,优先怀疑梯度爆炸,加gradient clipping。
- 再看模型输出是否异常,比如输出logits爆到1e4以上,可能是初始化或某一层的scale不对。
- 最后才去调整学习率和优化器参数。
优化器本身能做的事只是把更新步长拉回来。如果你的模型前向输出就已经是无穷了,再好的优化器也救不回来。
4.2 收敛晚、波动大、过拟合三合一怎么处理
训练到中期,最典型的问题是:验证集loss上下抖动,训练集loss一直下降。很多同学这时喜欢去加更重的weight decay,或者调dropout,但有时候真正的根源只是训练进入平台期,学习率退火不够。
我自己的处理方式是:先看训练集和验证集的gap。gap不大但都在缓慢下降,说明还在正常收敛路径上,坚持训练即可。如果验证集波动明显、训练集平滑下降,通常两种对策——第一种是调低学习率并延长训练,第二种是增强数据增强或提高dropout。
如果训练集loss已经压得很低、验证集却上不去,这时再去加weight decay往往效果有限。我更喜欢从优化器角度做调整:把AdamW的beta2从0.999调到0.9999,让二阶矩估计更平稳,减少后期参数更新方向的抖动。这个操作看似微小,但遇到过两个场景都靠它把验证集的波动压下来了。
4.3 检查点恢复训练后效果变差
这是工程上特别容易遇到的坑:训练到一半,机器重启,从checkpoint恢复训练,结果loss不但没继续降,反而上升了,甚至直接NaN。
排查下来大部分是以下两种原因。一是学习率调度器状态没恢复。很多人保存了模型权重和优化器状态,却忘了保存scheduler的step和上一次的lr值,恢复后学习率又从初始值开始,相当于用大学习率继续训练,自然震荡。二是混合精度训练里,随机数种子没恢复,导致后续数据增强、dropout等操作的随机序列变了,同一轮数据下模型看到的输入分布不一样,训练表现自然不稳定。
我的解决习惯是:保存checkpoint时,把model、optimizer、scheduler、scaler(AMP用的)、epoch、global_step、rng_state全部塞进去。恢复后按顺序load,并且在前几个step把学习率强制覆盖为中断时的值。这些小动作看起来啰嗦,但能让你避免“白训几天”的事故。
5. 关于“Model-Optimizer”的进一步思考:优化器仓库化的价值
5.1 一个可复用优化器配置库应该长什么样
我理解的“Model-Optimizer”,不只是某一个优化算法,更是一套可复用的配置和经验库。在团队协作中,每个人训练模型都各调各的优化器参数,是最低效的事情。我后来把这些配置整理到了统一配置文件里,按任务类型分目录,比如image_classification、semantic_segmentation、language_model、diffusion等,每个目录下记录优化器名称、学习率区间、betas、weight_decay、是否梯度裁剪、调度器类型、warmup步数,甚至记录“翻车日志”。
这样的配置库,最大的价值不是让老手偷懒,而是让新人少踩坑。新同学接到训练任务时,直接复制同类型任务的配置作为起点,比对着论文复现快得多。等后续有调优经验了,再在配置库中更新记录,逐渐形成团队自己的优化器best practice。这条思路,比任何单一优化器论文都实用。
5.2 怎么给别人描述自己的优化器方案
写实验报告、做项目总结时,很多人只会写“使用了AdamW optimizer,learning rate为1e-4”。这远远不够。一份合格的优化器方案描述,至少应该包含以下内容:
- 优化器名称与版本(是Adam还是AdamW,是解耦weight decay还是L2正则)。
- 学习率的具体数值、batch size、warmup步数、训练总步数。
- 调度器类型与关键参数(比如cosine的eta_min,step的gamma和step_size)。
- 是否使用梯度裁剪以及max_norm值。
- weight_decay具体数值和解耦方式。
- 混合精度类型(FP16还是BF16),是否影响优化器更新逻辑。
- 是否中途切换过优化器或学习率,切换时机和效果。
只有把这些全部写清楚,别人才能在你的实验基础上做复现和比较。优化器调优之所以被称为“玄学”,很大程度上就是因为很多人省略了这些关键上下文。
6. 写在最后的实际体会
根据我个人做过的项目,优化器的选择确实重要,但它从来不是单独起作用的。模型结构、数据质量、训练时长、学习率调度、batch size,甚至数据增强策略,都会和优化器产生交互。如果只把注意力放在“换一个优化器”上,往往很难得到理想结果。
我的建议是:每个项目都花一点时间,把优化器和调度器当成一个整体来做实验。哪怕只是在基线里同时记录训练loss、验证loss、梯度范数、每类参数的学习率变化曲线,信息量也比只看最终分数大得多。
最后再分享一个小习惯:每次开始跑长训练之前,我会额外用很小的学习率跑一个“冒烟小实验”,只跑几十个step,目标是确保loss在下降、梯度没有NaN、显存不炸。这能让后续正式训练省下很多麻烦。Model-Optimizer这个主题看似不起眼,但只要你肯在这些细节上较真,就会发现它带来的收益远比想象中大。