ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

深度学习优化算法详解:Momentum、RMSProp与Adam原理及PyTorch实践

2026/9/16 8:13:33 拓冰建站 浏览量
深度学习优化算法详解:Momentum、RMSProp与Adam原理及PyTorch实践 做深度学习训练最让人头疼的往往不是模型结构设计而是训练了半天loss纹丝不动、来回震荡、甚至直接爆掉。而这一切的幕后推手就是优化算法。不少同学刚开始用PyTorch时基本就是直接Adam一把梭跑起来挺爽但真到了要调优模型、改结构甚至复现论文时才发现根本不知道Adam到底在干什么Momentum和RMSProp跟它是什么关系什么时候该用哪个。这篇文章就专门把深度学习里的优化算法讲透重点是Momentum、RMSProp、Adam这三大金刚的原理、联系和实际使用经验适合正在入门深度学习、准备动手搭模型或者被loss曲线折磨到抓狂的同学。我最早开始看优化算法时也翻了大量资料但多数不是照着公式念一遍就是直接贴一大段ndarray代码看完依然不知道为什么这么设计。后来在项目里真正调试过几个模型被各种坑教做人之后才慢慢把这些算法的来龙去脉理顺了。这篇东西不打算写太学术而是把原理、公式、代码、调参经验尽量浓缩在一起该有的细节一个不少帮助大家少走弯路。1. 优化算法到底在干什么从SGD说起1.1 优化算法解决的核心问题深度学习的流程本质上是定义一个带参数的函数神经网络然后通过训练数据不断调整这些参数让模型输出和真实标签之间的误差越来越小。这个“调整参数”的过程由优化算法完成。我们训练时经常听到的“梯度下降”就是最基础的思路算出损失函数对每个参数的梯度也就是导数然后沿着梯度的反方向迈一小步让损失逐渐降低。如果只用一句大白话总结优化算法的作用那就是在参数空间中找到一条又快又稳的下山路让损失函数达到尽可能低的位置。模型结构决定的是“山”长什么样而优化算法决定的是“怎么走下山”。这两者缺一不可前者管能力上限后者管能不能真正到达。最朴素的随机梯度下降SGD更新公式大家应该都见过θ θ - η * g其中θ是参数η是学习率g是当前梯度。整个深度学习训练的“所有秘密”基本上都浓缩在这个公式及其变体里。这个简单的过程存在什么问题为什么后来又出现了Momentum、RMSProp、Adam这些增强版本原因主要有三个梯度方向在小批量样本上波动剧烈导致更新路径摇摆不定。对每个参数使用同一个学习率但实际不同参数的变化幅度、梯度尺度往往差异巨大。在峡谷地形、鞍点、平坦区域等情况下SGD的更新步伐缺乏自适应调节能力。大家可以把梯度下降想象成蒙着眼在山里走路每次只根据脚下那一小片区域的坡度决定方向很容易走出锯齿状的路线明明终点不远却在吭哧吭哧绕弯子。优化算法后来做的所有改进本质都是在解决“怎么走得更稳、更快、更聪明”。1.2 损失地形与优化困难为什么SGD会卡住深度神经网络的损失函数表面不是光滑的碗状而是高维空间中极其复杂的“地形图”。这里面有一些在训练中经常碰到的典型结构峡谷Ravine某个方向的坡度极大另一个方向坡度极小梯度来回垂直震荡前进速度慢。鞍点Saddle Point各个方向梯度接近于零但既不是极小值也不是极大值SGD到这里基本就“迷路”了。平坦区域Plateau损失变化很平缓梯度很小参数几乎停滞。SGD在这些地形下的表现非常吃力。峡谷场景中梯度在一个方向上的分量很大、另一个方向上很小导致参数在峡谷壁上来回反弹但沿着峡谷底部前进的速度却非常慢。鞍点附近SGD看各个方向都“没什么坡”就会像无头苍蝇一样停滞不前。这些实实在在的困难催生了后续一系列算法的改进思路既然直观上解决不了就引入历史梯度信息、梯度平方信息让优化器拥有“记忆”和“自适应”能力。2. Momentum原理给梯度下降装上惯性2.1 动量机制的物理直觉Momentum动量算法的思想非常朴素在更新参数时不再只看当前这一步的梯度而是把“历史梯度”也叠加上来相当于给参数更新增加了一个“惯性”或者说“速度累积”的过程。想象一下推一个很重的购物车。如果你每推一下都立刻松手购物车会停下来下一推又要重新从零加速整个过程非常费劲。但如果连续朝同一个方向推购物车会越滚越快即使中间偶尔推歪一下车的运动方向也不会立刻大幅改变只是慢慢偏转。Momentum干的就是这件事建立速度变量v每轮更新时把当前梯度与历史速度进行加权融合。用公式表示就是v β * v η * g θ θ - v这里的β通常取值0.9左右代表“对历史速度的保持程度”。展开之后可以看到v其实累积了所有历史梯度的加权和只是距离越远的梯度权重越小指数衰减。正因如此Momentum能在峡谷地形中大幅抑制垂直方向的震荡同时加速水平方向的推进。2.2 动量如何解决震荡与加速收敛在峡谷场景中SGD的更新轨迹往往在峡谷两侧来回震荡每一步都走成“之”字形。Momentum引入历史速度后垂直方向上过去的梯度和当前梯度方向相反会相互抵消迈出的步长变小水平方向上历史梯度和当前梯度同向则会叠加让有效速度越来越大。一增一减之间更新路径被逐渐拉直收敛速度自然就上来了。但Momentum也有过冲overshoot问题。因为惯性太大模型可能在接近极小值点时刹不住车冲过头再反复折返。这就是为什么有些项目里会加学习率衰减或者动量衰减learning rate schedule / momentum schedule在训练后期把动量系数调低一些让参数更平稳地落到极小值附近。实际调参时我自己的经验是动量β0.9是绝大多数场景的默认起点不需要一上来就改动它如果发现训练后期loss曲线明显“来回甩”可以尝试把β降到0.8甚至0.7往往能得到更平滑的收敛曲线。3. RMSProp原理让每个参数都有自己的步长3.1 自适应学习率的基本思路Momentum解决了“方向震荡”的问题但依然沿用同一个学习率作用于所有参数。实际神经网络中不同参数对损失的影响程度差别极大有些参数梯度大而陡峭有些参数梯度小而平缓。如果所有参数用统一学习率梯度大的参数迭代步长可能偏大导致震荡梯度小的参数迭代步长又可能偏小导致收敛过慢。RMSProp的做法是根据每个参数的历史梯度大小动态缩放学习率。它的核心思想是给每个参数维护一个梯度平方的指数滑动平均值记为E[g²]更新参数时用这个值来“归一化”学习率E[g²]_t β * E[g²]_{t-1} (1-β) * g_t² θ_t θ_{t-1} - η / (√(E[g²]_t) ε) * g_t来看这个公式的含义。当某个参数的梯度一直很大时E[g²]很大分母就大实际步长会被压缩当某个参数梯度很小时E[g²]很小分母变小实际步长会被放大。于是每个参数都有了“自适应”的学习率大梯度参数走得小心小梯度参数走得大胆。这个思路跟归一化有点类似不是用绝对值来衡量梯度大小而是用相对值决定步长。公式中的ε是一个非常小的常数通常取1e-8防止除以零。3.2 RMSProp与Adagrad的关系说起RMSProp就绕不开它的前身Adagrad。Adagrad同样按参数自适应调整学习率但它累积的是全部历史梯度的平方和没有做指数衰减。这带来一个致命问题随着训练推进累积的梯度平方和不断增大分母越来越大学习率被压缩得越来越小最后模型基本“动不了”。RMSProp的改进正在于此用指数滑动平均代替历史平方和让旧梯度的影响逐渐衰减而不是无限累积。这样一来RMSProp就能在训练过程中持续保持自适应调节能力不会像Adagrad那样过早“枯竭”。这也是RMSProp在深度学习中更加实用的根本原因。实际用一个比较形象的类比RMSProp相当于给每个参数装了一个“自动挡”坡度大的时候自动减油坡度小的时候自动给油。配合Momentum的“惯性”机制基本可以应付绝大多数地形变化。4. Adam目前最常用的默认优化器4.1 Adam如何整合两种机制的优点Adam全称是Adaptive Moment Estimation本质上是Momentum和RMSProp的结合体既像Momentum一样维护一阶梯度的指数滑动平均记住方向又像RMSProp一样维护二阶梯度的指数滑动平均调整步长。它的更新过程分为几步m_t β1 * m_{t-1} (1-β1) * g_t v_t β2 * v_{t-1} (1-β2) * g_t²其中m是一阶动量梯度的均值估计v是二阶动量梯度的未中心化方差估计。由于训练初期m和v都是初始化为0前几步估计值会严重偏小所以Adam引入偏差修正m_hat m_t / (1-β1^t) v_hat v_t / (1-β2^t) θ_t θ_{t-1} - η * m_hat / (√(v_hat) ε)仔细观察最终更新公式分母√(v_hat)ε体现了RMSProp的自适应步长调节分子m_hat体现了Momentum的历史方向累积。Adam在这两方面做了“双保险”再加上偏差修正机制让它在训练初期也能稳定起步。默认参数β10.9、β20.999、ε1e-8在绝大多数任务上都表现不错这也是Adam“开箱即用”口碑的由来。对比PyTorch中文教程和实际项目里的用法可以发现很少有任务需要通过大改这些默认参数来获得显著提升更多时候需要调的是学习率本身。4.2 Adam的局限性与AdamW的出现Adam这么好用是不是就万能了并不是。实际使用中Adam有两个比较明显的短板在某些泛化任务上Adam的收敛结果不如精细调参的SGDmomentum理想可能找到的极小值点平坦度不好导致测试集表现略差。Adam的权重衰减实现方式与SGD不同如果直接在Adam优化器上设置weight_decay参数实际上是把L2正则项直接加到了梯度里而并非真正的解耦权重衰减。针对第二个问题产生了AdamW。AdamW的核心改进是将权重衰减weight decay与梯度更新解耦在更新参数时额外单独执行一步“参数直接按比例缩小”而不是把衰减项混入梯度。公式层面可以理解成θ_t θ_{t-1} - η * (m_hat / (√(v_hat) ε) λ * θ_{t-1})这个改动看似细微但实际效果在很多实验中被证明更稳定、泛化性更好。PyTorch的torch.optim.AdamW就是基于此思路实现的。如果你的模型加了大量正则化或者训练BERT类模型建议优先选择AdamW而不是Adam。5. 算法对比与实操选型建议5.1 三种算法在典型训练现象上的差异为了让大家有一个比较直观的认知我整理了一个简单的对比表涵盖训练中比较关注的几个维度维度MomentumRMSPropAdam / AdamW历史梯度利用一阶动量方向累积二阶动量梯度平方均值两者都用学习率是否自适应否是是初始阶段稳定性较好一般偏差修正后稳定对噪声梯度的抑制较强较强非常强默认参数友好度较好较好非常高泛化性经验强中等中等AdamW改善典型适用场景CV迁移学习/精细调参在线学习/RNN类大多数NLP/CV任务的默认选择这个表不是“谁一定比谁好”的结论而是告诉大家在不同场景下算法的表现倾向。比如复现论文时如果用SGD baseline建议别轻易换成Adam否则实验结果可能出现明显偏差。5.2 什么时候用Momentum什么时候用Adam根据自己的实践经验我给一个比较朴素的选型建议如果你在复现一篇论文作者用的是SGDMomentum那就先别换成Adam保持baseline一致性。如果是在做探索性实验、快速验证模型结构直接用Adam或AdamW把更多时间花在数据和模型结构上。如果发现Adam训练后期测试集指标上不去可以尝试切回SGDMomentum配合余弦学习率衰减或warmup经常能带来惊喜。在线学习、强化学习、RNN这类训练信号波动大的场景RMSProp和Adam表现通常更稳。不少同学会问那我能不能用“Adam训前期SGD训后期”这种策略确实有人用叫“优化器切换”或“Adam收敛后微调SGD”但不是万能的切换时学习率需要重新调整否则可能直接把参数震飞。5.3 训练中的学习率与epoch配合优化器选对了学习率调不对照样白瞎。结合深度学习训练中常见的epoch概念来说学习率策略一般分这样几档学习率过大0.1甚至更高loss直接爆炸或者疯狂震荡。学习率适中0.01左右前期loss下降很快后期可能无法很好收敛。学习率较小0.001以下收敛非常慢可能训练很多epoch后loss依然居高不下。在实际项目中我给新手一个比较稳妥的配方先用Adam学习率设1e-3batch size设64或128跑20个epoch观察loss曲线。如果loss下降太快且后期测试指标停滞尝试在第50%和75%的epoch处把学习率降到当前值的1/10如果模型过大或者任务复杂还可以引入warmup阶段让学习率在前几个epoch线性上升防止初期参数剧烈波动。这个配方不敢说最优但对大多数图像分类、文本分类任务都能快速摸到“可行区”。5.4 PyTorch落地代码参考纸上谈兵终觉浅最后给一段在PyTorch里使用三种优化器的示例代码方便直接拿去跑实验import torch import torch.nn as nn from torch.utils.data import DataLoader model nn.Sequential( nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 10), ) # Momentum SGD动量系数0.9weight_decay可加可不加 optimizer_sgd torch.optim.SGD(model.parameters(), lr0.01, momentum0.9, weight_decay1e-4) # RMSProp学习率通常设小一点 optimizer_rmsprop torch.optim.RMSprop(model.parameters(), lr0.001, alpha0.99, eps1e-8) # Adam默认参数直接使用 optimizer_adam torch.optim.Adam(model.parameters(), lr1e-3, betas(0.9, 0.999), eps1e-8) # AdamW适合带权重衰减的Transformer类模型 optimizer_adamw torch.optim.AdamW(model.parameters(), lr1e-3, betas(0.9, 0.999), weight_decay0.01) # 训练循环示例 criterion nn.CrossEntropyLoss() for epoch in range(10): for x, y in DataLoader(train_dataset, batch_size64, shuffleTrue): optimizer_adam.zero_grad() out model(x) loss criterion(out, y) loss.backward() optimizer_adam.step()这里要注意一个细节zero_grad()一定要在backward()之前调用否则梯度会在多个batch之间累积。另外PyTorch中torch.optim.Adam默认的eps是1e-8对绝大多数float32训练都够用但如果用到混合精度训练建议把eps调大一些比如1e-6、1e-7防止梯度下溢导致更新失效。6. 常见问题与排查技巧实录6.1 训练震荡不收敛表现loss上下剧烈跳动甚至越训练越高。最可能的原因是学习率过大。优先把学习率降一个数量级试试比如从1e-3降到1e-4如果震荡明显缓解说明方向对了。其次可以检查batch size是否太小导致梯度估计噪声过大适当调大batch size后梯度更平稳训练曲线也会更平滑。还有一个容易被忽视的原因是数据没做好归一化或标准化。深度学习框架对输入数据的尺度非常敏感不同特征数值范围差异过大会直接导致梯度不稳定。如果你的特征范围横跨几万到几百万再好的优化算法也救不回来。解决办法是先用均值方差标准化或者min-max归一化处理数据。6.2 损失下降很快但验证集指标不涨这种情况在训练里也非常典型。loss下降说明模型在训练集上拟合得很好但验证集提升有限甚至下降明显是过拟合的迹象。优化器层面能做的调整包括增大weight_decayL2正则、切换到SGDMomentum配合学习率衰减、早停early stopping等。此外还需要检查数据增强、dropout等正则化手段是否到位。有些同学此时会执着于换更复杂的优化算法其实方向偏了。优化算法解决的是“怎么更快更好地到达低loss位置”过拟合是“到达的位置泛化能力差”这两件事要分开对待。先诊断清楚是欠拟合、过拟合还是超参数不匹配再针对性出手。6.3 损失值中途突然变成NaN训练到一半loss变成NaN一般有三种可能一是学习率过大导致参数更新一步跨太大数值溢出了把学习率调小就能解决二是数据里有异常值或NaN输入backward之后梯度非法三是模型本身存在数值不稳定问题比如未加epsilon的log操作、除零、梯度爆炸等。排查时可以先把随机种子固定住保证每次复现同样的问题然后逐步简化模型和数据二分定位问题出现在哪里。也可以给梯度裁剪gradient clipping加上PyTorch里用torch.nn.utils.clip_grad_norm_对RNN、Transformer这类模型非常常用torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)这个操作能在梯度爆炸时把梯度模长限制在某个范围内不让数值失控。当然梯度裁剪也不是银弹如果模型设计本身有bug照样会出NaN它只是把风险兜住而已。6.4 收敛速度很慢但最终效果还行这种“慢而能出结果”的情况在复现论文时最让人纠结。先不用急着换优化器可以从两个方向优化一是调整学习率策略比如加入warmup和余弦退火很多情况下比换优化器效果更明显二是检查数据加载是否存在瓶颈GPU利用率太低、数据预处理占了大量时间都会让整个训练“体感”很慢。如果已经排除了上述因素再考虑算法层面的替换。比如从Adam切到SGDMomentum时需要把学习率调整到Adam的10倍甚至更大经验值是0.1左右开始试否则SGD步子太小基本等于没训。这个“学习率切换”是最容易踩的坑换优化器不换学习率然后得出“SGD就是不行的”错误结论。7. 关于优化器组合使用的一些个人心得优化算法的选择没有绝对的“最强者”更多是跟任务特点、模型结构、训练资源高度耦合。我自己在项目里用得最多的组合是探索阶段用AdamW快速出结果稳定复现阶段切到SGDMomentum配合warmup和余弦衰减压榨最终指标。偶尔也会用“Adam先跑几百步再用其状态初始化SGD”的迁移策略但这种情况非常少因为两个优化器内部维护的状态口径不同直接切换需要重新配置学习率操作起来很麻烦。对于还在入门阶段的朋友我建议不要在这上面纠结过多时间先把Adam吃透理解它每一步在干什么然后再去对比SGD和RMSProp这样学起来最省力。等你在实际训练中发现loss曲线的各种“疑难杂症”时自然会回过头来重新审视这些算法的设计精妙之处。最后再分享一个实用小技巧每次跑实验前把优化器类型、初始学习率、学习率调整策略、随机种子统一记在一个配置文件里这样回头排查问题时能快速锁定变量少做很多无效实验。