搞深度学习的人,迟早会撞上一组名字:SGD、Momentum、AdaGrad、Adam。刚开始跑模型的时候,我跟很多同学一样,把这些优化器当“黑盒”用——反正框架里设一行optimizer = torch.optim.Adam(...)就能跑,收敛了就是成功,不收敛就换个学习率。但真到项目不好调的时候,才意识到一个问题:优化器不是“选一个就行”的开关,它本质上是在决定训练过程走什么样的“下山路线”,不同路线对应完全不同的收敛行为、泛化能力和调试手感。
这篇文章我打算把这四个优化器从头到尾聊透。会用大白话和物理直觉解释它们的原理,也会把每个算法的更新公式、超参含义、适用场景和坑位都摊开来说。不管你是刚入门深度学习、在“炼模型”过程中头疼过 loss 震荡的新手,还是想搞清楚“Adam 和 SGD 到底该选谁”的从业者,这篇都能给你一个可以照着用的参考。
1. 优化器到底在解决什么问题——不是玄学,是“下山”
先忘掉公式,想一个场景。你站在一座山的某个位置,目标是走到山谷最低点。你看不清全貌,只能靠脚底踩到的坡度来判断方向:坡度往下,就往那边走一步。这个“靠坡度找方向、一步步挪到低点”的过程,就是梯度下降。模型训练里的“山”,是损失函数构成的曲面;脚下踩到的“坡度”,是每个参数对损失的梯度;走的“步子”,就是学习率。
方向知道了,但怎么走大有讲究。步子迈太大,可能会直接跨过谷底、冲到对面山坡上,然后来回震荡;步子迈太小,训练慢到天亮都到不了终点。更麻烦的是,这座山不是标准的“大碗”——有的方向很陡、有的方向很缓,干脆是一片高原,中间还可能有个假洼地(局部最小)或者马鞍形区域(鞍点)。标准梯度下降在这种地形里非常吃亏。
SGD、Momentum、AdaGrad、Adam,本质上就是针对这些“下山难题”一层层打补丁。SGD 提供了最朴素的走法;Momentum 发现老是震荡,就给脚步加“惯性”;AdaGrad 发现不同方向难度不一样,就给每个方向配一个“自动调节的步长”;Adam 干脆把前两家的经验合并,既有惯性,又给每个参数动态配步长,成了这几年最稳的默认选择。
这套演进逻辑非常重要。很多人记不住公式,是因为不知道每个公式在解决什么具体问题。一旦你理解了“补丁在补什么洞”,你不需要背公式也能做出正确的调参决策。
2. 四个选手逐个拆解:原理、公式与直觉
2.1 SGD:最朴素的梯度下降,但别小看它
一般框架里写的SGD,严格说其实是“小批量随机梯度下降”(Mini-batch Gradient Descent)。它不是每看一个样本就更新一次,而是随机抽一小批样本,算出这批样本的平均梯度,然后用这个梯度去更新参数:
θ = θ - α * g_t其中 θ 是参数,α 是学习率,g_t 是当前小批量算出来的梯度。
SGD 最大的优点是简单、稳定、在下坡方向清晰时非常靠谱。它最大的问题也摆在明面上:第一个是震荡。在峡谷类地形里,梯度在陡峭方向上来回摆,但平缓方向却推进很慢,loss 曲线看起来就是一条“波浪式下降”的线,收敛很磨人。第二个是弯曲地形适应差。换到那种“弯弯绕绕”的沟壑地形,它每一步都走得很机械,效率低。第三个是学习率难调。α 太小,训练慢;α 太大,直接在最优解附近转圈,loss 下不去。
但有一个现象很反直觉:SGD 在很多视觉任务里,泛化效果反而比 Adam 好。我个人的理解是,SGD 的“机械和简单”让它落到的解往往在更平坦的区域,而 flat minima 在测试集上通常表现更稳。所以如果你在做一个标准的图像分类任务,不是特别在意收敛速度的话,用带动量的 SGD 仍然是非常好的选择。
2.2 Momentum:给梯度加“惯性”
SGD 在峡谷地形里震荡太严重,解决思路非常直观:加惯性。想象一个保龄球从山坡滚下来,它不会因为某个瞬时坡度变平就立刻停下来,而是会靠着之前的动能继续往前冲一段。动量法(Momentum)就是给参数更新引入一个“速度项” v:
v_t = μ * v_{t-1} + α * g_t θ_t = θ_{t-1} - v_tμ 是动量系数,一般取 0.9。
仔细看这个公式。梯度方向如果连续一致(比如始终向下坡走),v 会越累越大,相当于越走越快;梯度方向来回反弹时,历史的 v 和当前梯度方向相反,会相互抵消一部分,震荡就被削平了。实践里我最直观的感受是:加了动量之后,训练前期 loss 下降更快,中后期曲线也更“顺滑”,不会再像裸 SGD 那样锯齿状乱跳。
这里还有一个变体叫 NAG(Nesterov Accelerated Gradient),它的做法是先顺着当前的动量方向“超前看一步”,再算那个位置上的梯度。效果上比普通 Momentum 收敛更稳,很多框架里的torch.optim.SGD(..., momentum=0.9, nesterov=True)就是这种设置。配合 0.9 的动量,基本是视觉任务里最经典的配置组合之一,值得直接抄作业。
2.3 AdaGrad:给每个参数单独配“步长”
SGD 和 Momentum 都还是“全局一个学习率”,但真实问题里不同参数的处境差太多了。拿推荐系统举例:有的特征(比如热门商品)频繁出现,对应参数的梯度几乎每次都在更新;有的特征(比如长尾商品)一整个 epoch 才遇到几次,参数很少得到有效更新。如果全用同一个学习率,频繁更新的参数容易震荡,稀疏的参数又学不动。
AdaGrad 的思路是:针对每个参数维护一个“历史梯度平方和” s,更新时用学习率除以这个和的平方根:
s_t = s_{t-1} + g_t^2 θ_t = θ_{t-1} - (α / (√s_t + ε)) * g_t直觉是:某个参数历史梯度大、更新频繁,s 就大,等效学习率被压小;某个参数历史梯度小、更新稀少,s 就小,等效学习率被放大。于是稀疏特征也能得到足够的更新幅度,而频繁更新的参数不容易震荡。
但 AdaGrad 有一个致命缺陷:s 只增不减。随着训练进行,s 越积越大,等效学习率会持续衰减,到最后所有参数的学习率都趋近于 0,训练基本就“卡死”了。所以它在训练周期短、特征稀疏的模型里表现不错,但在深度网络的长周期训练里基本没人用。它更大的价值在于提出了“自适应学习率”这个概念,后面 RMSProp 和 Adam 都根植于此。
RMSProp 是 AdaGrad 的直接改良——把“历史梯度平方和”改成“历史梯度平方的滑动平均”,用 e.g. 0.9 的衰减系数让 s 只保留最近一段时间的梯度平方信息,不再单调增长,解决了学习率衰减到零的问题。不过 RMSProp 没有动量机制,单独用的人也不多,它更像是通往 Adam 的中间站。
2.4 Adam:把动量和自适应步长融为一体
Adam(Adaptive Moment Estimation)是 Momentum 和 RMSProp 的合体,由两套滑动平均组成:一阶动量 m 保存梯度的均值方向(相当于带惯性的梯度),二阶动量 v 保存梯度平方的均值(相当于自适应放缩的学习率):
m_t = β1 * m_{t-1} + (1 - β1) * g_t v_t = β2 * v_{t-1} + (1 - β2) * g_t^2 m_hat = m_t / (1 - β1^t) v_hat = v_t / (1 - β2^t) θ_t = θ_{t-1} - α * m_hat / (√v_hat + ε)默认设置是 β1 = 0.9、β2 = 0.999、ε = 1e-8。很多框架内部已经把这些超参配好了,所以 Adam 确实开箱即用。
为什么实践里 Adam 几乎成为“默认王”?因为它一次性解决了前三个算法的痛点:不同参数自适应学习率(继承 AdaGrad/RMSProp)、方向平滑不震荡(继承 Momentum)、对初始学习率不那么敏感——即使学习率设得稍微高一点,二阶动量会自动对梯度过大的方向做抑制,不容易直接发散去。我自己做生成模型、Transformer 类任务、多模态对齐项目时,基本首选 Adam,因为它的收敛速度足够快、曲线稳定,省掉了大量为“分方向调步长”操心的时间。
Adam 也有被反复吐槽的缺点:在某些任务上泛化能力不如带动量的 SGD,对学习率依然敏感(只是容差比 SGD 大)。后者通常通过 AdamW(把权重衰减从 L2 正则里拆出来单独处理)缓解。坦白讲,在一个新项目起步阶段,我会先用 Adam 把模型跑通、找到合理的结构和数据方式,再回头尝试 SGD 调参去刷更好的泛化精度。
下面是这四种优化器的速览对比表:
| 优化器 | 核心机制 | 收敛速度 | 稳定性 | 典型使用场景 |
|---|---|---|---|---|
| SGD | 直接按梯度更新 | 慢 | 中等,容易震荡 | 视觉任务、追求泛化能力时 |
| Momentum | SGD + 惯性 | 中等偏快 | 明显改善震荡 | 视觉任务的标配,常配合 NAG |
| AdaGrad | 自适应学习率 | 受限于学习率衰减 | 早期稳定,后期易停 | 线性模型、稀疏特征训练 |
| Adam | 动量 + 自适应学习率 | 快 | 非常稳定 | 通用默认,Transformer、GAN、多模态等 |
3. 实战中的选择思路与超参调节经验
3.1 优化器怎么选才合理
很多朋友喜欢固定用 Adam 跑一切任务,不能说错,但要分场景。我现在的选择逻辑大概是这样的:如果任务(比如图像分类、语义分割、检测)结构成熟,数据相对规范,我会优先试带动量的 SGD(momentum 0.9,学习率从 0.01 起),因为它泛化更好,最终指标往往能刷得更高;如果任务涉及 Transformer、大规模预训练微调、生成模型,或数据分布异常复杂,直接上 AdamW 更省心,它自适应方向和步长后,不用去猜“哪个方向的梯度尺度差太多”这种细节。
还有一个选型思路依赖你的调试预算:如果项目上线时间紧,优先 Adam,它的默认参数基本能跑,顶多调学习率;如果项目是长期打磨型,比如想刷榜单或者做高精度业务模型,SGD 系更值得花时间调。再补充一点小经验:模型非常深(超过 100 层)时,SGD 系的学习率需要配合更精细的初始化才稳定,Adam 系对深层的尺度变化容忍度更高,所以大模型预训练阶段基本是现代 Adam 家族的天下。
3.2 学习率设置与退火策略
学习率是比优化器本身更关键的开关。我用过的比较靠谱的经验值:SGD/带动量 SGD 初始学习率常见 0.01 到 0.1(视觉分类任务常从 0.1 开始找);Adam 常见 0.001 到 0.0003,Transformer 常用 2e-4 或 1e-4 甚至更低;大规模预训练模型通常 1e-5 到 5e-5,稍大就会 loss 发飘。
学习率不能一尘不变。训练初期需要大一点,让参数快速到达“靠谱区域”;训练后期需要小一点,让参数在最优解附近精细搜索。常见做法有:指数衰减(每 N 个 epoch 学习率乘个 0.9 之类的系数)、余弦退火(Cosine Annealing,学习中后期平滑降到很小,配合 Warm Restart 还能跳出局部小洼地)。我最常用的组合是“Warmup + 余弦衰减”:前 5% 的步数从很小的学习率线性爬升到目标值,防止刚开始大步子踩空;之后按余弦曲线逐渐降低,收敛曲线的尾部非常干净漂亮。
关于 Warmup 的原理,解释一下:训练开始时参数还是随机的,直接上一大批梯度很容易让早期更新过于激进,而且 Adam 的二阶动量估计还没积累起来,前几步的“自适应”是不可靠的。Warmup 相当于给优化器一个适应期。
3.3 Adam 与 SGD 的切换心得
这是我把参数调到一定程度之后非常喜欢的“绝招”:先用 Adam 快速跑一个大体可靠的模型,把学习率、网络结构、数据增强这些大头确定下来;然后切换到带动量 SGD(或 NAG),从头或从当前权重继续训练,往往能把泛化误差再压一截。原因前面说了,Adam 在很多任务上偏向于收敛到尖锐的最小值,SGD 则偏向于平坦的最小值,后者测试误差通常更小。
切换时有两个坑。第一个坑:直接换优化器会导致 loss 短暂飙升,因为两种优化器的参数更新尺度完全不同,好在一般几个 epoch 内能重新稳定,不用被吓到。第二个坑:切换后要重新搜索学习率,不能接着用 Adam 的旧值。经验上 SGD 的合适学习率往往比 Adam 高一个数量级,建议从 0.01 左右重新试,用三五个短训练确认合适的量级再跑长训。
4. 训练调优中的常见问题与避坑技巧
4.1 Loss 震荡不收敛,怎么排查
最典型的症状是 loss 曲线像锯条一样上下抖。第一优先检查学习率是不是太大,试着把学习率缩小 10 倍,如果曲线明显变平稳,就说明先前步子迈太大。第二个常见原因是 batch size 太小,小批量梯度噪声大,sample 出来的方向不稳定,可以适当增大 batch size。第三个容易被忽略的是数据本身:标签噪声大、样本分布异常、脏数据比例高,都会让 loss 无法稳定下降,此时光调优化器没用,需要清理数据或检查 loss 权重的设置。
如果是 Adam 训练过程中 loss 直接跳动到 NaN,多半是梯度爆炸。可以先用torch.nn.utils.clip_grad_norm_设置梯度裁剪(clipping),把梯度范数限制在一个安全范围内(比如 1.0 或 0.5)。这个操作对长序列 Transformer 尤其重要。顺便一提,如果用了混合精度训练,NaN 也可能来自 fp16 下的数值溢出,要配合动态缩放(Grad Scaler)处理。
4.2 AdaGrad 和 Adam 的高阶注意事项
AdaGrad 在长时间训练中的“学习率归零”问题我前面提过,在实际项目中我建议不要直接用原始 AdaGrad 去训练深度模型,否则你会看到训练到一半 loss 基本不动了,不熟悉的人会以为模型“死了”,其实是分母里的累计平方和把步长压没了。如果一定要用自适应类优化器做稀疏数据,优先考虑 Adam 或支持稀疏梯度的 AdamW。
Adam 也有一个比较隐蔽的坑:权重衰减(weight decay)的处理。早年不少实现直接把 weight decay 加进 L2 正则里,但 Adam 的自适应机制会破坏这个正则的语义,导致实际正则效果和预期不符,所以 AdamW 把权重衰减从梯度里拆出来,在参数更新时单独做一次衰减。经验上是:用 Adam 做预训练或大规模模型时,不要简单开启 weight_decay=0.01 了事,建议明确使用 AdamW,并把权重衰减的目标集中在合适的层(比如只对权重、不对偏置和 LayerNorm 的 γ/β 做衰减),能省掉不少泛化下降的烦恼。
4.3 从“练不出来”到“练得动”的排查顺序
训练效果差,很多人第一反应是去调优化器参数,但优化器往往不是罪魁祸首。我的排查顺序是:先看 loss 是否在原子层面正常下降(排除实现 bug、数据 pipeline 问题);再确认初始化方式是否合理(比如残差网络、Transformer 的初始化往往要特殊设计);然后查学习率和 warmup 的设置;最后才考虑优化器类型和超参。这个顺序能帮你省大量时间。
另一个很多人忽视的现象:模型在训练集上 loss 下降正常,测试集表现差,你调 Adam 的 β2 或者 momentum 不会有本质影响。此时应该从正则化、数据增强、dropout、标签平滑、早停策略这几个方向下手。优化器能帮你“到得了低点”,但能不能“低得有价值”,更多取决于正则化与数据策略。把这层关系理清楚,就不会在 optimizer 的超参上白费力气。
我自己这几年跑实验最大的体会是:优化器从来不是一个孤立的选择,它跟学习率、batch size、数据分布、权重衰减和任务类型纠缠在一起。与其花大力气追求“最好的优化器”,不如先把这四类算法的“脾气”摸清楚,再根据项目当前的瓶颈去选。新手期先用 Adam 跑通万事,进阶之后再尝试带动量 SGD 刷精度,是一条非常平滑的成长路线。最后再补一句话:如果你真的只想记一个配置,那就记 Adam 的默认参数加个 cosine 退火学习率,绝大多数情况都不会让你失望。