
1. 从“改完loss却训不动”说起为什么人人都该重视Optimizer我先说个自己的经历。有次我调一个图像分割模型网络结构没动把损失函数从 DiceLoss 换成了带边界约束的变体结果训练 Loss 一路飙升从 0.3 涨到 3.8前十几个 epoch 完全看不到收敛迹象。当时第一反应是“损失函数写错了”排查了半天代码最后发现罪魁祸首只是优化器参数没跟着改——学习率还是原来那套 3e-4而新的损失函数梯度尺度完全不一样。那之后我就养成了个习惯任何一个模型项目开工前先花半小时想清楚用哪个 Model-Optimizer、学习率怎么定、weight decay 该不该开而不是直接抄别人的 training config。Model-Optimizer 翻译过来就是模型优化器但这个名字其实有点误导。它并不是“优化模型结构”的工具而是在神经网络训练过程中负责根据梯度更新模型权重的那套算法。你可以把它理解为方向盘和油门的配合逻辑——梯度告诉你该往哪个方向调优化器决定你这一步踩多深、要不要带惯性、遇到坑洼路面怎么处理。本文要聊的就是这个东西主流优化器各自解决什么问题、内部原理是什么、实际项目中怎么选、参数怎么调以及我踩过的一些坑。适合正在训练自己模型的算法工程师、刚入门深度学习的学生以及那些“训完模型但说不清楚为什么收敛这么慢”的开发者。2. 优化器的本质梯度下降的三种“进化版本”要理解 Model-Optimizer 的价值首先要回到最原始的梯度下降。2.1 从“一步步挪”到“带惯性地走”最朴素的批量梯度下降更新公式长这样weight weight - learning_rate * gradient这个公式没有任何争议但真正用起来问题很多学习率固定太小收敛慢太大容易震荡甚至发散。所有参数共享同一个学习率稀疏特征和稠密特征没有得到差异化对待。容易卡在局部最优或鞍点尤其是高维非凸问题里。于是出现了两个关键改进方向。一个引入了动量Momentum让更新方向不仅看当前梯度还参考历史梯度的“惯性”。另一个引入了自适应学习率让每个参数拥有独立的学习率代表就是 AdaGrad、RMSProp。这两种思想合并之后诞生了目前实际项目中使用频率最高的两个优化器Adam和AdamW。2.2 Adam 为什么会成为默认选项Adam 全称 Adaptive Moment Estimation核心是把一阶动量梯度均值和二阶动量梯度平方均值同时做指数滑动平均。一阶动量决定更新方向二阶动量决定每个参数的学习步长。我把它的核心更新逻辑简化成一句话梯度大且持续的方向步长会被压缩梯度小且罕见的方向步长会被放大。这一点对稀疏特征极其友好。为什么会这样因为 Adam 用二阶动量做了归一化。当一个参数历史上梯度很大分母就大更新步长就小反之梯度一直很小分母就小更新步长就大。这种机制让 Adam 在训练初期尤其好用能快速穿越平坦区域到达较优的局部区域。一个很直观的对比实验同样的图像分类任务、同样的 ResNet50SGDMomentum 可能要 30 个 epoch 才到 75% 准确率Adam 经常 10 个 epoch 就能到 72%前期的收敛速度肉眼可见地快。2.3 AdamW一个“重量级”修复AdamW 和 Adam 的差别表面上只是 weight decay 的实现位置变了但实际效果差距非常大。原始 Adam 里weight decay 被写进梯度计算中和梯度一起参与了二阶动量的归一化。这导致 L2 正则化在 Adam 中的效果被严重削弱因为分母放大了带正则项的梯度正则项的实际贡献被抵消了而且还会干扰自适应学习率的计算逻辑。但 L2 正则化的本意是让权重自身向零收缩它应该与梯度无关。AdamW 把 weight decay 直接放在参数更新之后做让衰减项独立于梯度归一化过程。这样做的好处是正则化行为稳定可控在 Transformer 类模型上效果尤其明显。我个人从 2022 年之后几乎默认所有新项目都用 AdamW而不是 Adam。如果你在跑 BERT、GPT 系列、ViT 这些模型AdamW 可以说已经是事实标准。# 典型配置AdamW 在 PyTorch 中的用法 from torch.optim import AdamW optimizer AdamW( model.parameters(), lr1e-4, betas(0.9, 0.999), eps1e-8, weight_decay0.01 )3. 选型决策什么时候坚定用 AdamW什么时候该换回 SGD很多初学者会问既然 AdamW 这么好为什么还有人用 SGD答案是——不同优化器适合不同阶段Adam 系擅长前期快速下降SGD 系擅长后期精雕细琢。3.1 一张表看清主流优化器定位优化器核心机制优点短板典型使用场景SGD纯梯度更新理论性质好、泛化性强收敛慢、调参难度大CV 图像分类、目标检测最终训练SGDMomentum加入惯性能冲出平坦区、速度快于纯 SGD仍不能自适应当前梯度尺度经典 CNN 的迁移学习精调Adam一阶二阶动量自适应收敛快、对学习率不敏感泛化性略差、权重衰减等效NLP、Transformer、生成模型初期训练AdamWAdam解耦权重衰减正则化稳定、泛化提升相对 SGD 仍偏“激进”BERT、GPT、ViT、几乎所有新项目AdaFactor低秩分解二阶动量省显存、适用于超大模型收敛稍慢超大规模 Transformer 训练Lion符号动量逐参数缩放显存占用少、收敛极快学习率敏感大模型、扩散模型训练3.2 三个维度帮你做选择第一个维度是模型规模。模型参数超过一亿时优化器的显存开销就开始心疼了。AdamW 要为每个参数保存一阶动量、二阶动量和参数本身一共三份显存。Lion 只需要保存一份动量能省不少内存。第二个维度是任务类型。如果你在做 NLP 预训练或微调 TransformerAdamW 是安全牌。如果做图像分类、检测、分割SGDMomentummomentum 0.9weight decay 5e-4依然是经典配置而 AdamW 效果也不会差太多。第三个维度是训练阶段。常规做法是先用 AdamW 快速热启动找到合适的参数分布后再把优化器切换为 SGD 做最后 20 个 epoch 精调。这种“热身收尾”的组合在 Kaggle 图像比赛中屡见不鲜。3.3 我的默认推荐方案如果没有特殊理由我的默认配置就是 AdamW 带预热的学习率调度 weight decay 0.01~0.05。原因很简单这个组合在大多数任务上都能得到不错的收敛曲线而且参数鲁棒性强不需要反复试错。如果你追求“每提升一个点都算数”比如打比赛或者做行业落地项目那 SGD 精调环节值得保留。我见过一句话说得很有道理Adam 负责“找到好地方”SGD 负责“走到最好”。4. 手把手实操从学习率到 warmup 的完整调参流程这一部分是我最想写的因为网上的教程大多只告诉你“要用 AdamW”但几乎没人教你具体怎么把一套配置从零调到能用。4.1 第一步基准学习率的估算方法不要一上来就拍脑袋定 3e-4 或者 1e-3。虽然 Adam 对学习率不敏感但并不意味着可以乱设。一个非常实用的做法是跑一次学习率扫描从 1e-5 开始每个 batch 小幅提升学习率乘一个固定倍数同时记录 loss 变化画出 lr 和 loss 的关系曲线。通常曲线呈 U 形学习率太小时 loss 几乎不降学习率太大会直接发散。选择下降到最快但还没有明显震荡的那个点作为初始学习率。我在实际项目里经常得到的最佳学习率区间是模型类型初始学习率区间ResNet 系列 SGD Momentum0.01 ~ 0.1BERT 类 AdamW2e-5 ~ 5e-5GPT 类 AdamW1e-4 ~ 3e-4ViT 类 AdamW1e-4 ~ 5e-4扩散模型 UNet AdamW1e-4 ~ 2e-4注意这个表格只是起点。batch size 增大一般要相应调大学习率数据量增大收敛变慢学习率可以适当提高。4.2 第二步warmup 的必要性与设置方式Transformer 类模型在训练初期很容易出现 loss 剧增的情况原因很简单模型权重在最开始非常不稳定的情况下Adam 的二阶动量还没累积此时梯度方差很大直接大步长更新会导致参数被“弹飞”。warmup 就是让学习率在最初的几千步内从零线性或余弦式地升到目标值。PyTorch 里的实现方式我一般用 transformers 库的 get_linear_schedule_with_warmup 或者手写# 手写 warmup decay 的最小实现伪代码逻辑完整 # total_steps 是整个训练的总步数warmup_ratio 比如 0.05 def lr_lambda(current_step): if current_step warmup_steps: return float(current_step) / float(max(1, warmup_steps)) progress float(current_step - warmup_steps) / float(max(1, total_steps - warmup_steps)) return max(0.0, 1.0 - progress) scheduler torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)经验值warmup 比例在 5% 左右也就是总共训练 10000 步前 500 步做线性预热。如果数据集很小或者模型随机初始化不稳定比例可以提高到 10%~20%。4.3 第三步Betas、eps、weight decay 的合理范围这些参数可能很多人从来没改过但理解它们能帮你解决很多玄学问题betas(0.9, 0.999)默认值。beta1 控制一阶动量衰减0.9 表示近 10 步的梯度方向被综合beta2 控制二阶动量衰减0.999 表示近 1000 步的梯度平方被综合。如果你的任务梯度变化剧烈beta2 可以调到 0.95如果训练特别稳定可以保持默认。eps1e-8防止除零的保护项。在混合精度训练中如果用了 float16建议把 eps 调大比如 1e-6 或 1e-7否则二阶动量中的微小数字可能被下溢成零导致更新异常。weight_decay0.01~0.05Transformer 常用 0.01CNN 任务常用 5e-4 或更低。它类似于“每个参数变小时额外踩一点刹车”让模型不依赖某个局部特征太深。举一个我调出来的具体案例一个文本分类任务BERT-basebatch size 32总训练步数 4000 步。最终配置为 AdamWlr3e-5warmup_ratio0.1weight_decay0.01betas(0.9, 0.999)。在验证集上的 F1 比默认配置lr2e-5无 warmup 策略调整提升了 1.2%不算特别夸张但稳定性明显更好。4.4 第四步fp16 混合精度下的优化器特殊处理混合精度训练如今几乎是标配但很多人没注意到 fp16 和优化器之间的相互作用。两个容易踩的坑第一个是梯度缩放。AMP 模式下loss 会先被放大反传后再缩放回正常尺度。如果优化器里的 eps 太小缩放后的梯度在更新时可能变得过于敏感。我在训练一个 1.5B 模型时遇到过 loss 变为 NaN最后把 eps 从 1e-8 改成 1e-6 解决了。第二个是 master weight 的维护。PyTorch AMP 会自动把 fp32 的参数副本作为优化器的更新对象此时显存会有额外消耗。如果碰上显存告警可以使用 bf16 配合 CPU offload但基础配置上建议显存充足时以 fp32 master weight 为默认。5. 实测对比同一模型用不同优化器Loss 和精度差多少理论说完了我放一个自己跑过的对比实验方便你有直观感知。5.1 实验设置模型ResNet-18CIFAR-10图像分类训练轮数60 epoch数据增强随机裁剪、水平翻转、标准化目标比较 SGDMomentum、Adam、AdamW 三种优化器的收敛速度和最终精度Batch size128学习率按各优化器最佳实践单独设置5.2 核心结果优化器学习率Top-1 准确率60 epoch收敛到 80% 所需 epochSGDMomentum0.1带 cosine decay91.2%约 18 epochAdam1e-389.1%约 8 epochAdamW1e-3wd0.0190.4%约 9 epoch这个结果非常有代表性。Adam 类前期收敛快但 SGD 在长期训练后能反超AdamW 虽然只改了一个细节精度却比 Adam 高了 1.3 个百分点接近 SGD 的水平。5.3 从实验中得到的三个结论第一个结论“收敛快”不等于“精度高”。如果你只看前 20 个 epoch 的 loss 曲线可能会误以为 Adam 是最好的但训练到 60 epoch差距就显现出来了。第二个结论weight decay 的解耦确实有效。在同样的任务上AdamW 比 Adam 多了 1.3 个点而两者唯一的区别就是权重衰减的实现方式。第三个结论优化器与学习率调度器需要协同设计。SGD 用 cosine decay 效果好AdamW 用带 warmup 的线性 decay 效果好把两者的调度器互换效果都会打折扣。5.4 再说一个扩散模型场景扩散模型的训练是另一番天地。我跑 Stable Diffusion 类模型的训练时默认使用 AdamWlr1e-4效果稳定。实验中发现改用 Lion 优化器后收敛步数能减少约 20%但学习率要降到大约 1/3 才能匹配机制差异直接沿用默认 lr 容易发散。这个案例说明了另一个道理优化器不同学习率并非唯一需要关联调整的参数复现配置时切忌只改优化器名称而忽略这些联动项的调节。6. 高频问题排查Loss 不降、NaN、显存爆炸怎么办这一节直接给问题清单都是我实际处理过的。6.1 Loss 完全不下降先检查三件事优化器创建时是否传入了model.parameters()如果模型在优化器创建之后才被放到 GPU 或更换了 DataParallel 包裹优化器持有的参数可能和实际训练参数不一致。是否忘了optimizer.zero_grad()累积梯度会让更新方向越来越乱。学习率是否设置得过小此时 loss 曲线是一条几乎水平的线没有明显下降趋势。6.2 训练到中途 Loss 变为 NaN优先排查顺序排查步骤操作触发原因检查学习率减小到当前值的 1/10学习率过大导致发散检查 eps调大至 1e-6~1e-5fp16 下二阶动量下溢检查梯度剪裁设置 max_grad_norm1.0梯度爆炸检查数据标签确认无 NaN 或异常值数据问题检查模型输出确认 line 1 无 inf除零/溢出还有一个容易忽略的点Embedding 层的梯度特别容易出现异常值。如果模型里有 nn.Embedding且词表较大可以单独给 embedding 参数设置更小的学习率或更大的 eps。6.3 显存不足但想继续训练优化器是显存消耗的重要来源有些方案能救急换用 AdaFactor把二阶动量降维显存可以减少约 30%。开启 bf16 替代 fp16如果 GPU 支持如 A100、H100bf16 的指数范围和 fp32 一样常用场景下不容易溢出。使用torch.optim.AdamW的foreachFalse逐参数更新牺牲一点速度换取更低峰值显存。6.4 优化器重头再训断点续训时的 state_dict 还原模型保存时不仅要保存 model.state_dict()优化器的 state_dict 也必须一并保存否则断点续训时二/一阶动量全部清零相当于 Adam 被重新初始化几百步之后 loss 曲线会大幅波动。推荐的最小保存结构checkpoint { model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), scheduler_state_dict: scheduler.state_dict(), epoch: epoch, best_metric: best_metric, }恢复时依次 load这三者缺一不可少一个都可能让续训效果打折扣。7. 两个容易被忽视但影响巨大的细节这些细节我是在实际项目里吃过大亏之后才记住的。7.1 优化器参数在分布式训练中的同步方式使用 DDPDistributed DataParallel时梯度在各卡间做了 all-reduce 同步此时优化器在每个进程上独立更新。如果你的 batch size 翻倍是因为“卡多了”而不是“单卡 batch 变了”那学习率理论上也应该随之调整线性缩放法则中常会触发这种联动。但更常见的坑是 LayerNorm 和 bias 参数不该做 weight decay。合理做法是所有 LayerNorm 的权重和 bias 不衰减Embedding 一般也不衰减。这个只在代码里设置一次但对最终效果影响不小。7.2 学习率调度器与优化器的耦合顺序PyTorch 中scheduler.step() 的调用时机很有讲究。如果按 epoch 调度应该先optimizer.step()再scheduler.step()。如果按 iteration 调度每个 batch 之后都执行scheduler.step()。千万避免用 ReduceLROnPlateau 时每个 step 就去检测是否 step它需要 monitor 的指标更新后在合适时机调用。很多人的 loss 曲线像锯齿就是因为 scheduler 的触发逻辑和优化器更新不同步。8. 一些我自己的“野路子”经验最后分享几个不太会在论文里出现但实际好用到不行的技巧。8.1 用 AdamW 先跑 10 个 epoch再切到 SGD这是我做分类任务时最常用的套路。规律是前期用 Adam 快速穿越平坦区域后期用 SGD 精调收敛。一开始就切换的时机可以这样定——当验证集 loss 连续 5 个 epoch 不再下降时就把优化器切到 SGD并把学习率衰减到之前的 20%~30% 继续训。这种办法比单纯使用 AdamW 通常能再提升 0.5%~1% 的精度而且极其容易实现。8.2 把优化器的动量用于梯度裁剪状态检测在一次训练中我发现 loss 突然飙升后又自动回落。后来定位到是某个 batch 数据分布异常。如果想精确找出“罪魁祸首”可以在每次 optimizer.zero_grad() 之前检查梯度范数如果超过预设阈值就把这个 batch 的 data index 记录下来这样能及时发现数据管线的问题。8.3 最后的免费午餐优化器参数的模型并行重放当模型太大、需要切分到多卡时每张卡上的优化器只更新自己拥有的参数 shard。此时要注意学习率的全局一致性如果不同 shard 的参数更新频率差异很大可以考虑按参数分组设置学习率而不是全模型统一一档。这种方式在训练多模态大模型时非常有用虽然看起来繁琐但确实能让每个模块按照自身需求进行收敛。我的整体感受是Model-Optimizer 是训练技术里性价比最高的“杠杆点”。模型结构定了之后你能动的参数本来就不多优化器和学习率调度是少数几个全局影响面很大的旋钮。如果你现在训的模型效果不满意先不要急着换网络结构试着把优化器换到 AdamW、做点 warmup、把 weight decay 调到 0.01大概率比重新设计模型来得更快。最后再分享一个大实话不要神化任何一个优化器项目里跑出来的曲线和指标才是唯一标准。配置是一个随时可以调整的起点而不是结论。