这几年我一直在和深度学习模型打交道训练过分类、检测、生成类的各种网络说实话调模型最花时间的往往不是网络结构本身而是那个不起眼的优化器。很多人把优化器当成一个黑盒写个Adam(lr1e-3)就开训等loss不降了才回头找问题。Model-Optimizer这个主题说白了就是搞清楚优化器在做什么、不同优化器之间的差异在哪、什么时候换掉默认配置能带来明显收益。这篇内容我会结合自己的实操经验把优化器的选型逻辑、核心参数含义、调参套路和踩坑记录一次性说透适合正在训练模型的工程师、学生以及对训练效果有追求的调参党参考。1. 优化器选型前的三个核心认知1.1 优化器到底在解决什么问题用最直白的话讲优化器就是决定模型如何更新参数的策略。每次迭代我们会算出梯度也就是loss对每个参数的变化方向优化器负责回答三个问题走多远学习率、朝哪个方向走动量矫正、每一步怎么走得稳自适应调整。没有优化器模型更新参数就只能靠最原始的梯度下降虽然理论可行但实际训练中收敛极慢还容易卡在局部最优或者震荡不收敛。我早期刚入门时犯过一个大错以为只要把网络结构搭得漂亮训练自然会收敛。结果用固定的学习率训练一个ResNetloss前几十轮下降还正常后面直接变成一条横线怎么调都纹丝不动。后来才明白优化器的存在并不是简单地把梯度乘个系数更新参数而是要配合训练阶段的动态变化持续调整每一步的幅度和方向。同一个模型、同一份数据换一个优化器或改一组参数训练曲线的差异能大到让你怀疑自己代码写错了。理解优化器的起点是你必须先清楚一个事实没有任何一个优化器是万能的。不同任务、不同模型规模、不同batch size下同一个优化器的表现可能天差地别。这也是为什么我建议你做任何新项目之前花十分钟搞清楚优化器家族里各个成员的设计初衷和适用边界比盲目追求“最新效果最好”的调包行为重要得多。1.2 主流优化器的技术脉络与演进逻辑如果你去看优化器的发展历史会发现它其实就是一个不断解决上一代问题、又引入新问题的过程。最早的SGD很简单沿着负梯度方向固定步长更新稳定可靠但收敛慢。为了加速Momentum引入了物理中“惯性”的概念让更新方向不仅依赖当前梯度还参考历史梯度累计这样能冲过局部极小值收敛明显加快。接着Nesterov提出了改进版的动量方法先按照动量预估下一步位置再在那个预估位置计算梯度相当于带上了“前瞻”能力处理拐弯的场景更灵敏。与此同时自适应学习率方向也在演进AdaGrad对每个参数进行独立的学习率缩放解决了手动逐参数调学习率的问题但它累积所有历史梯度的平方学习率会单调递减训练后期基本走不动RMSProp用滑动平均替代累积和缓解了这个问题Adam则把RMSProp和动量结合同时维护梯度的一阶矩估计和二阶矩估计成为深度学习实战中使用最广泛的优化器。Adam之后的演进方向则主要集中在修正它的已知问题。AdamW把权重衰减和梯度更新解耦避免L2正则被Adam的自适应机制“吃掉”在NLP预训练和ViT训练中表现很好。LAMB针对大批量训练做了自适应层级学习率的调整让上万batch size的训练也能稳定收敛。这些优化器并非简单堆叠每一个特性都对应着实际训练中的某个痛点理解脉络才能知道该选谁。2. 常用优化器原理解读与适用边界2.1 SGD稳定性最强的“老将”SGD和带Momentum的SGD现在依然是很多视觉任务的首选。它没有花哨的自适应机制只依靠梯度方向和动量历史来更新参数。很多人在调参指南里看到“SGD Momentum收敛效果比Adam好”但不知道为什么。我实验下来核心原因在于SGD的更新方式更“干净”不会因为某个参数的梯度历史累积而大幅改变有效学习率因此最终收敛到的解往往更平滑、泛化性更好。用SGD训练时常见的参数组合是SGD(lr0.1, momentum0.9, weight_decay5e-4)配合cosine或step学习率衰减。0.1这个初始学习率看起来很大但在ImageNet等大数据集、batch size为256的条件下配合weight decay和适当的warmup是多年沉淀下来的经验值。换了更小的batch size比如64学习率通常要相应降到0.010.05否则梯度噪声变大更新步长相对过大训练早期就容易发散。我踩过一个很典型的坑把batch size从256减到64之后忘了调学习率结果模型前30个epoch训练acc一直在10%以内徘徊看起来像是网络结构写错了。后来按线性缩放规则把学习率降到0.025问题直接消失。SGD的参数敏感度很高任何环境变化都要重新审视学习率这是它和自适应优化器最大的区别。2.2 Adam及AdamW训练效率与收敛精度的取舍Adam系列最大的优点是不用怎么调学习率也能在短时间内把loss压到很低。它通过一阶矩估计移动平均梯度和二阶矩估计移动平均梯度平方的计算给每个参数一个动态调整后的有效学习率天然适应不同参数的尺度差异。对于训练Transformer、embedding层这类参数分布范围很大的模型Adam几乎是开箱即用的选择。但Adam也有一个被广泛讨论的问题它更容易收敛到尖锐极小值泛化能力有时不如SGD。直观理解是Adam会频繁放大那些梯度较小参数的有效学习率导致参数在最优解附近反复横跳难以落在平缓的底部。我在训练图像分类模型时对比过同样的epoch数Adam前期acc提升快但最终acc常常比SGD低0.51个百分点。不过这个问题在计算资源有限、只需要快速验证模型思路的场景下并不重要——我通常用Adam快速跑通几十个epoch验证模型能收敛再换SGD做最终精调。AdamW则修正了Adam在权重衰减处理上的缺陷。传统Adam在做L2正则时梯度里包含weight_decay * w这一项Adm会根据梯度大小自适应缩放这一项结果是大参数的衰减被无形放大小参数又衰减不到位。AdamW把权重衰减从梯度计算中剥离出来直接在参数更新时减去weight_decay * lr * w让正则效果可预测。现在用PyTorch训练ViT、BERT系列模型transformers库默认的优化器就是AdamW不是没有原因的。2.3 冷门但关键的优化器RMSProp、LAMB与NAdamRMSProp是Adam的“前身”只维护梯度平方的滑动平均没有动量机制。它在处理非平稳目标、比如RNN训练和强化学习场景中表现依然不差。我偶尔在音频任务里用到它原因是Adam在这种任务上偶尔会出现后期loss突刺RMSProp反而平缓一些。LAMB则是大批量训练场景中的救星。常规batch size下Adam能稳定工作但当batch size提升到数千甚至数万Adam的更新步长和梯度噪声之间的平衡会崩掉收敛极其不稳定。LAMB对每一层计算独立的自适应学习率再乘以层的norm比例相当于给每个层单独做一个缩放这样大批量下的更新不会因为某些层梯度幅值过大而整体失衡。当年用1024的batch size预训练BERT时LAMB能把训练时间缩短数倍且最终perplexity和Google论文中报告的数值一致。NAdam则是在Adam基础上融合了Nesterov动量收敛速度在部分任务上比Adam稍好但提升幅度普遍有限。我的建议是时间有限的话可以直接跳过NAdam把精力放在理解AdamW和SGD这两个主线上。3. 实际调参经验与踩坑记录3.1 学习率策略从warmup到衰减学习率的设定是优化器调参里最核心、也最容易出问题的一环。固定学习率几乎只在玩具模型里有效真实训练中必须配合衰减策略。目前主流选择有两种Step衰减和Cosine衰减。Step衰减就是每隔若干epoch把学习率乘以一个固定系数常见0.1实现简单、效果稳定Cosine则让学习率沿余弦曲线从初始值平滑降到接近0在大模型训练中更常用因为它让训练后期的小学习率阶段足够长有助于收敛到更好的解。warmup预热是另一个经常被忽略的设计。训练一开始就用较大的学习率会让模型参数剧烈震荡尤其是使用Adam时前几步梯度二阶矩估计还没稳定更新步长可能极大。我在训练BERT类模型时如果不加warmup前几个step的loss直接飙升到几十甚至NaN。加了min_lr0到目标学习率的线性warmup、持续总步数的10%之后loss曲线立刻正常了。图像模型训练中warmup虽然不那么必须但如果batch size很大或者用LAMB优化器强烈建议加上。学习率初始值的确定我会先用一个简单的范围测试以小学习率启动训练每个step指数增大学习率观察loss曲线的变化找到loss开始下降最快的那一点对应的学习率作为上界。这个技巧在fastai的lr_finder中实现得很成熟实际操作起来只要跑几十个step就能得到参考值比拍脑袋靠谱得多。3.2 动量、权重衰减与梯度裁剪的搭配动量参数在SGD中通常取0.9这个值本身相对稳定大部分情况下不需要精细调。当模型训练不稳定时我反而会先把momentum调低到0.8试试因为过高的动量会累积历史梯度方向一旦某个阶段的梯度方向整体变化较大更新轨迹容易过冲。Adam中对应的是beta1和beta2默认0.9和0.999除非处理非常稀疏的梯度比如推荐系统embedding一般不用动。权重衰减的选择和优化器类型强相关。SGD下的weight_decay通常设在1e-45e-4Adam下则要小得多我常用1e-65e-5。原因前面提过Adam本身对参数更新有自适应缩放太大的weight_decay会明显干扰正常参数更新。这个区别需要特别注意——很多人从SGD切到Adam时保留原来的5e-4权重衰减训练出来的模型不是欠拟合就是过拟合实际上就是正则强度在两种优化器机制下完全不等价导致的。梯度裁剪是稳定训练的最后一道防线。尤其在NLP任务、多模态模型中偶尔一个step的异常梯度会直接毁掉整个训练进程。我对所有Transformer类模型都会设置梯度裁剪clip_norm设为1.0。这个操作在PyTorch里只用一行代码torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)。加了它之后至少能排除掉90%的“loss突然变NaN”类事故。3.3 什么时候该换掉默认配置我自己的一套判断标准是这样的先用AdamW加一个常用学习率比如1e-4或3e-5跑通流程观察loss是否能正常下降、验证指标是否随着训练稳定提升。如果模型能收敛但最终指标不满意我会先尝试调lr、改scheduler、加长训练epoch而不是急着换优化器。只有当Adam系列无论怎么调都出现泛化差距明显或者训练后期loss震荡严重时才会切回SGDMomentum重建训练流程。还有一个经验蒸馏、迁移学习、微调这些场景AdamW通常是最稳的选择因为在已经收敛的模型参数附近做小更新自适应的步长调节能力优势明显。但从零训练一个深层视觉模型时SGD的优势会在训练后期逐渐体现出来。我建议在项目早期就把这两种路线都试一遍哪怕各跑20个epoch对比loss曲线也能省下后面大量反复试错的时间。4. 典型问题排查与多场景实测对照4.1 损失不下降、震荡与过拟合的处理思路训练中遇到loss不下降时很多人第一反应是加大学习率这往往适得其反。正常情况下loss停在某个高点几天不动多半是学习率过小或者模型梯度消失但loss快速降到某个数值后不再变化那可能是陷入了局部平坦区。我的排查顺序是先确认梯度本身是否为0打印每个step的梯度norm再检查学习率是否被scheduler降得过低最后检查数据管道是否存在重复数据导致模型学到错误模式。loss震荡则通常和batch size、学习率、数据噪声三者相关。小batch size的梯度噪声天然大学习率如果还是按大batch的经验设置loss曲线就会抖动非常厉害。处理方法是调小学习率或者适当增大batch size。另外使用Adam时如果beta2设置得太小梯度平方的滑动平均会跟不上瞬时梯度的变化也会造成后期震荡可以试试把beta2从0.99改到0.999。过拟合和优化器关系不大但要注意一个容易混淆的细节数据量少的时候Adam这类强自适应的优化器更容易迅速拟合训练集但验证集指标掉得也快。想判断是优化器导致的泛化问题还是模型本身过拟合我会用同一个模型分别跑AdamW和SGD各几十个epoch对比训练/验证loss的间距。如果SGD的验证指标显著占优那确实是优化器选择的问题如果两者都过拟合问题在数据增强和正则化上换什么优化器都没用。4.2 优化器在分布式与混合精度训练中的适配细节多卡分布训练时优化器的行为会发生变化。最常见的是学习率需要随卡数调整使用同步梯度时梯度是所有卡的均值等效batch size变大了。按照线性缩放规则学习率应随batch size线性增减。我在8卡训练时单卡batch size为32、总batch size为256SGD的初始学习率会从单卡的0.025提高到0.1这样能保持每step的参数更新幅度一致。如果卡数翻倍而学习率不变模型相当于在更平滑的梯度上做更小的步伐收敛速度会慢不少。混合精度训练AMP中优化器还有一个隐藏问题FP16的梯度下溢。小梯度在FP16下直接变成0导致对应参数根本不更新。常见的做法是loss scalingPyTorch AMP已经自动处理了这个流程但用了自定义优化器或自己实现loss缩放时需要格外注意优化器内部的grad scale是否正确恢复。我遇到过几次明明开了AMP但某些层权重不更新、loss卡着不降的情况排查下来都是梯度下溢造成的后来统一改用PyTorch的torch.cuda.amp标准接口才彻底解决。分布式训练里还应关注优化器的状态同步。Adam维护的一阶、二阶矩估计在多卡环境下要在每step做一次all-reduce如果框架实现或通信库配置有误会出现各卡优化器状态不一致表现出来的现象同样是loss曲线异常。碰到这种问题先跑单卡复现如果单卡正常而多卡异常优先怀疑通信和状态同步环节而不是调优化器参数。5. 优化器状态管理、断点续训与工程化实践5.1 checkpoint中必须保存的优化器信息实际工程中训练到一半中断是家常便饭。恢复训练时如果只加载模型权重而丢掉优化器状态学习率、动量、二阶矩估计全部清零训练效果会明显打折。我自己的经验是保存checkpoint时至少包含模型state_dict、优化器state_dict、scheduler的last_epoch或last_step、当前step、随机数生成器状态。其中scheduler状态最容易被忽略——不恢复scheduler意味着学习率从初始值重新开始前面几十个epoch的衰减白费了。PyTorch里的标准做法是checkpoint { model: model.state_dict(), optimizer: optimizer.state_dict(), scheduler: scheduler.state_dict(), step: step, rng_state: torch.get_rng_state(), cuda_rng_state: torch.cuda.get_rng_state(), } torch.save(checkpoint, checkpoint.pt)加载时按相反顺序恢复并手动把Step数告诉scheduler。Adam的state_dict里保存的是exp_avg和exp_avg_sq两组张量占用的显存和参数数量成正比也就是常说的“Adam会把模型参数占用量翻倍”。在显存紧张的时候这个开销不能忽略所以现在也出现了像AdaFactor那样不保存完整二阶矩的低显存替代方案。5.2 长训练稳定性的保障手段训练超过几十万步后优化器状态本身的数值稳定性也会变成一个风险点。Adam的exp_avg_sq是梯度的指数滑动平均理论上不会归零但在混合精度下存储精度有限长时间的累积误差可能导致某个时刻出现异常的更新步长。我习惯每隔几个epoch把optimizer的state_dict重新载入一次做一次“状态保鲜”顺便验证数值没有异常漂移。长训练中还常见一个和优化器相关的隐蔽问题loss一直缓慢下降但验证指标停滞。此时不少人会加大学习率想“冲一冲”我建议反过来做。把scheduler的min_lr调低一些并在最后10%的训练时间里用极低的学习率做精细磨底往往能拿到零点几个点的提升。这个技巧在fine-tune预训练模型时特别明显值得一试。另一个保障是定期记录优化器当前的有效学习率。PyTorch里每个param_group的lr属性可以直观读取我建议每个epoch打印一次确认scheduler没有把学习率衰减到异常低的数值。我遇到过scheduler设置失误导致lr一路衰减到1e-20的情况训练曲线看着挺正常因为模型已经接近收敛但怎么训练都提不上去查了半天才发现是scheduler的问题。分享几个我自己常留着的经验总结。第一新项目上来先用AdamW跑通流程验证模型构造和数据没问题再切换到SGD精调这个顺序能帮你快速区分“模型有问题”和“优化器没调好”。第二学习率是比优化器品种更关键的超参数任何环境变化都要重新测试学习率别沿用旧值。第三断点续训必查优化器状态不然后面所有对比实验都会因状态不一致而失去参考价值。训练模型这件事优化器不是唯一的决定因素但它是每一个epoch里都在起作用的底层机制把这块吃透了很多看似玄学的“模型训不动”问题都会变得有迹可循。