从SGD到Adam:深度学习优化器原理、演进与调参全解析

发布时间:2026/10/17 18:34:22
从SGD到Adam:深度学习优化器原理、演进与调参全解析
深度学习入门的时候大概没有人会绕过这四个名字SGD、Momentum、AdaGrad、Adam。尤其是后面这两个Adam 几乎成了默认选项PyTorch、TensorFlow 里的训练代码一拉下来多半是optim.Adam(model.parameters(), lr1e-3)。但你有没有想过一个问题为什么是 AdamSGD 真的就一无是处吗Momentum 到底在“动”什么AdaGrad 又是怎么被淘汰的我早年调模型的时候对优化器的理解就是“换一个试试”SGD 不收敛就换 AdamAdam 效果不好就加个学习率衰减至于背后的原理一直等到吃了几次亏、翻了几篇原始论文、把公式手推过一遍之后才真正想通透。这篇东西我就用做项目踩坑的视角把这四个优化器从头到尾拆开说什么、为什么、怎么调、坑在哪。1. 先把问题说清楚优化器到底在优化什么1.1 参数更新公式的数学底座不管是哪种优化器核心做的都是同一件事更新模型参数让损失函数的值不断变小。我们可以把训练过程抽象成一个极简公式w w - α · g其中 w 是模型参数g 是损失函数对参数的梯度α 是学习率。这个公式就是最原始、最朴素的梯度下降。它的几何含义很直白梯度指向的是损失函数上升最快的方向那我们就沿着反方向挪一步参数就会一点点逼近最低点。但直接拿这个公式去训深度学习模型马上会遇到三个问题震荡、收敛慢、难调参。想象你站在一个山坡上想下山你能看到的只有脚下这一小块地形的坡度梯度每迈一步的方向都是“这一步踩下去最陡的方向”。但如果地形是个狭长的沟壑你会发现自己在沟两边来回横跳——这正是 SGD 的典型症状如果地形坑坑洼洼有很多局部极小值你又可能刚走到一个小凹坑就停住不动了。优化器全部的工作本质上就是在回答一个问题在只知道当前梯度的情况下怎么迈出这一步最稳、最快、最不容易被困住1.2 三个主角梯度、学习率、参数状态要理解四种优化器的差异先分清三个要素。第一个是梯度它对每个参数给出一个方向和大小告诉我们损失函数在当前位置对每个参数有多敏感。第二个是学习率控制每一步迈多大。第三个是参数历史状态这是早期优化器几乎没考虑过的东西也是后来所有改进的核心除了当前这一步的梯度我们还能记住之前积累了什么信息吗SGD 是“完全不记”每次更新只看当前梯度Momentum 是“记住了以前的速度”AdaGrad 和 Adam 是“记住了历史梯度的强度”并据此给每个参数分配不同的学习率。这四个优化器的差异本质就是它们对历史的利用方式不同。这里有个新手容易忽略的点学习率不是一个可有可无的经验值它是整个更新策略里最敏感的旋钮。同样的模型、同样的数据学习率差一个数量级训练结果可能一个收敛一个发散。而优化器之所以越做越复杂很大程度上就是为了让模型对学习率不那么“神经质”。2. 四个优化器逐个拆解2.1 SGD最朴素的下山策略先看最基础带动量的公式严格的原始 SGD 不加动量但工程上所说的“SGD”几乎都是带动量的版本v γ · v η · gw w - v这里的 γ 通常取 0.9η 是学习率。原始 SGD 则是 v 直接等于 g也就是 w w - η·g。先说原始 SGD。它的问题是显而易见的收敛速度慢而且在沟壑地形里会剧烈震荡。为什么因为梯度本身有噪声mini-batch 算出来的梯度不等于全量梯度每一步的方向都有随机误差当损失函数在一个方向上很缓、另一个方向上很陡时参数会在陡的方向上反复横跳而在缓的方向上挪不动。学习率设大了震荡更剧烈甚至发散设小了在缓坡上半天走不动。经验就是SGD 对学习率和初始化的要求非常高新手直接用裸 SGD 去训深度模型多数时候是在和 loss 做毫无意义的搏斗。但原始 SGD 有一个不可替代的优点简单、稳定、可预期。它没有额外状态不引入任何自适应机制在某些任务上的泛化性能反而更好这一点后面展开。2.2 Momentum让下山带上惯性Momentum 的原理一句话就能说清让参数更新像小球滚下山一样带有惯性。v γ · v η · gw w - v这里的 v 会积累历史梯度的方向信息。假设某一步梯度突然反向由于噪声或陡坡v 不会立刻跟着反向而是会保留一部分之前的方向——因为惯性。这就天然平滑了震荡同时在损失函数平缓的方向上只要梯度方向一致v 会越滚越大速度越来越快有效加速收敛。你可以把它类比成推车每推一下车就往前动一点要是我的手不停抖动梯度噪声车不会跟着乱晃而方向一致时推了十下和推一下的速度完全不一样车会越跑越快。工程上的“SGD with Momentum”其实就是这个公式。默认 γ0.9 是几十年来实践出来的好参数γ 越大惯性越强收敛越快但太大比如 0.99会导致 overshootloss 会在极小值附近反复折返。0.9 是一个不用动脑子的安全值。还有一个进阶变体叫 Nesterov MomentumNAG公式略有不同先按照当前动量前瞻一步再在那个位置算梯度。效果是在凸优化中有更强的理论保证实际训练中也能带来“刹车更早、转弯更快”的效果但提升幅度没有想象中大所以工程上不常用。2.3 AdaGrad给每个参数配独立学习率Momentum 解决的是“震荡”和“慢”但还有一个问题它没碰不同参数的重要性和梯度尺度是不一样的。一个模型里可能有成千上万个参数有的在稀疏特征上梯度几乎为 0有的在密集特征上梯度大得很。对所有参数用同一个学习率很不公平。AdaGrad 的解决方式是每个参数 i 都维护自己的累计梯度平方和 G_i更新时把学习率除以这个累计值的平方根G_i G_i g_i²w_i w_i - (η / √(G_i ε)) · g_i一句话解释哪个参数的历史梯度大它后面的学习率就被压得更低历史梯度小的参数学习率就能保持较大。这在处理稀疏特征时非常有效频繁出现的特征梯度经常很大加上 AdaGrad 后学习率快速衰减不至于在早期就把参数推到离谱的区域稀疏特征的梯度出现次数少学习率还保持得很大能学到更细的数据模式。但 AdaGrad 有一个致命伤梯度平方和 G_i 只增不减。训练到几十轮之后分母巨大学习率无限趋近于 0参数彻底“冻死”模型再也没有学习的能力。训练一个深度网络用 AdaGrad大概率你看到的是前半段还行、后半段 loss 几乎不动。这个“累积”的设计在凸优化里好使在非凸的深度网络上就成了绊脚石。2.4 Adam一阶矩加二阶矩的集大成者Adam 的思路是既然 Momentum 用历史梯度的一阶矩均值来平滑方向AdaGrad 用历史梯度的二阶矩平方和来自适应调学习率那把两边结合起来不就好了公式是这样理解即可不必背m_t β₁ · m_{t-1} (1 - β₁) · g_tv_t β₂ · v_{t-1} (1 - β₂) · g_t²m̂_t m_t / (1 - β₁^t)v̂_t v_t / (1 - β₂^t)w w - η / (√v̂_t ε) · m̂_tm_t 是梯度的一阶动量梯度的带衰减均值方向大致等于“历史梯度的平滑方向”提供了 Momentum 的惯性v_t 是梯度的二阶动量梯度平方的带衰减均值近似等于“梯度大小的一直以来水平”用来调节每个参数的学习率。最容易被忽略的是那一步偏差校正bias correction也就是除以 (1-β₁^t) 和 (1-β₂^t)。为什么要校正因为 m_t 和 v_t 的初始值是 0在训练的早期阶段前几步的均值会被严重拉低导致最初的更新步长过小。除以 (1-βᵗ) 之后相当于把早期偏差修正回来让更新步幅在第一轮就接近应有的尺度。默认超参数 β₁0.9β₂0.999ε1e-8 是论文经过大量实验验证的配置至今仍是标准答案。这个组合的特点是更新过程既平滑又能自适应每个参数的尺度而且因为二阶动量是滑动平均而不是累计和不会像 AdaGrad 那样冻死参数。Adam 在实践中近乎“开箱即用”换数据集换模型都不用怎么调学习率默认的 1e-3 基本都能跑出像样的结果。这也让 Adam 成了深度学习实际应用中的默认优化器。3. 从朴素到自适应四次迭代背后的博弈3.1 每次升级到底解决了什么问题把四种优化器放在时间线上看每一步都是针对上一代的具体缺陷做的修补而不是什么玄学创新优化器引入机制解决的核心问题代价/副作用SGD无最基础的梯度下降震荡、慢、难调学习率Momentum一阶动量梯度噪声大、震荡严重惯性可能带来 overshootAdaGrad二阶动量累计不同参数尺度差异大、稀疏特征学习率衰减到 0网络“冻死”Adam一阶 二阶滑动平均Momentum 不解决尺度问题 AdaGrad 会死掉超参数多泛化性有争议Nesterov 动量夹在 Momentum 和 AdaGrad 之间但核心思想仍然是一阶的范畴RMSProp 则是 AdaGrad 的改良版把“累计平方和”换成“滑动平均平方”解决了衰减到 0 的问题Adam 正是 RMSProp 的动量加强版。3.2 不同的地形对应不同的优化器各种优化器的差异在平坦地形、陡峭地形、沟壑地形和稀疏特征地形上各有体现平坦区域里SGD 走得像是没有导航的人慢且犹豫Momentum 因为有惯性能借着一路积累的速度快速冲出平台Adam 则表现得稳健又迅速。沟壑地形里比如 RNN 训练里常见的 loss 曲面SGD 会在沟壁之间来回撞墙Momentum 能冲出去但容易冲过头Adam 因为逐参数调整步长整体更加平稳。稀疏数据场景是最能体现 Adam 优势的地方。推荐系统、文本分类这类任务里特征极其稀疏大部分参数的梯度偶尔才有非零值。用 SGD 给所有参数一个固定学习率零梯度的参数永远不动非零梯度的参数又可能因为步长太大而震荡Adam 的自适应机制天然适应这种场景所以 NLP、推荐领域很长一段时间基本是 Adam 天下。3.3 泛化性之争Adam 是不是太“聪明”了近年来一个反复被注意到的现象Adam 在训练集上的收敛速度明显优于 SGD但在测试集上的最终表现有时候反而比 SGD 差这个现象被称为 “generalization gap”。为什么一个较有说服力的解释是Adam 的逐参数学习率会让更新过程过于“迁就”大梯度参数导致模型在损失函数的平坦极小值附近反复游荡而没真正收敛到平坦区域中心SGD 因为不区分参数重要性在大梯度上维持较大的步长反而把参数带到了更“宽”的极小点泛化性更好。另一个解读是从噪声角度SGD 的梯度噪声在训练后期近似于一种隐式正则化而 Adam 把这个随机性削掉了。这个问题到今天都没有最终定论但实践上的对策已经成熟训练前期用 Adam 快速收敛后期切到 SGD或 SGDMomentum微调几轮。这是 CV 领域常见的 trick——先 Adam 冲到接近最优的区域再用 SGD 在这个邻域里精修。我在几个图像分类实验上验证过这种方式比全程 Adam 往往能再涨零点几个到几个点的准确率。代价是切换瞬间学习率需要重新设低不然 loss 会跳一下。4. 实操怎么选、怎么调、怎么避坑4.1 超参数设置建议先说超参数。虽然大多数框架都给了默认值但上线前值得手调一遍优化器关键超参数常用起点经验调整方向SGDlr, momentumlr0.1, γ0.9lr 太大发散太小不动γ 一般不动Momentumγ0.9可以试 0.95/0.99注意 overshootAdaGradlr, εlr0.01, ε1e-8基本不用于深度网络Adamlr, β₁, β₂, εlr1e-3, β₁0.9, β₂0.999, ε1e-8lr 是最敏感参数最好配合 schedule关于 ε有个细节很多人没有在意它虽然名义上是避免除零的数值稳定项但在实际训练中也承担着“限定最小学习率”的角色。ε 设得太小比如 1e-10 以下当二阶动量 v̂_t 接近 0 时有效学习率会非常大训练容易出现 NaN设得稍大一点1e-6 到 1e-4在部分任务上有额外的稳定效果。关于 β₂如果训练中 loss 曲线出现剧烈的周期性波动可以试试把 β₂ 增大到 0.9999。β₂ 控制的是二阶动量对历史信息的记忆长度越大代表估计得越长期、越平滑对梯度的瞬时波动越不敏感。代价是早期的偏差校正需要更长时间发挥作用。4.2 最佳实践学习率调度才是隐藏的大头一个经常被忽视的事实优化器和学习率调度器是配合作战的不是各自为政。在 Adam 上最经典也最简单的策略是warmup decay。warmup 就是最初几百到几千步内把学习率从 0 线性升到目标值目的是让二阶动量统计先积累起来避免一开始就用大学习率去更新还没见过世面的参数之后再用 cosine 或 exponential decay 让学习率逐步降低让参数在临近收敛时走得更稳。手动实现一个简单的 warmup 其实就几行代码# 假设 total_steps 已知前 5% 步做 warmup for step in range(total_steps): if step warmup_steps: lr base_lr * (step 1) / warmup_steps else: # 这里接入 cosine 衰减或线性衰减 progress (step - warmup_steps) / (total_steps - warmup_steps) lr base_lr * 0.5 * (1 cos(pi * progress))为什么 warmup 有效这两个梯度分布还没有稳定Adam 的二阶动量 v_t 还是个很小的值除以一个接近于 ε 的数更新的步长会被严重放大导致参数一步就飞出去甚至触发 NaN。等 v_t 统计上来了更新步长回归到合理区间才是 Adam 真正发挥作用的时候。有一类常见问题几乎都是调度问题loss 前期下降不错训练到一半突然就开始震荡甚至发散。大部分人第一反应是把学习率调小其实更对症的做法是检查是不是 warmup 没做、β₂ 偏小、或者 decay 时学习率降得太快打断了模型的平稳过渡。4.3 常见误区与排查手册踩过的坑一多就会积累出一份自己的“黑名单”列几条最实用的误区一Adam 是万能的。不一定。在需要极致结果的任务里Adam 跑完的最优分数往往不如精心调过的 SGDMomentum。不是 Adam 差是它太“稳”对陷入平坦且不敏感的区域少了一点冲击力。误区二所有优化器都用同一个学习率。大错特错。同一个任务SGD 的可用学习率范围可能只有 0.01 到 0.1Adam 却在 1e-4 到 1e-2 之间都能工作。从 SGD 换到 Adam如果不把学习率从 0.1 降到 0.001loss 直接飞掉。误区三默认超参数永远最佳。默认的 β₂0.999、ε1e-8 对大多数 CNN/MLP 任务很好但换到大规模稀疏模型或强化学习场景常常需要针对性调整。强化学习中 Adam 的 ε 调大到 1e-4 左右往往能让训练更稳。误区四loss 变 NaN 一定是模型写错了。排查顺序应该先是学习率过大→再是梯度爆炸→然后是数据里有异常值→最后才轮得到模型结构问题。用了 Adam 还是 NaN先看一眼第一条。误区五Adam 权重衰减写错了。很多框架里weight_decay实现的方式不一样。早期的错误实现是在 Adam 更新中直接以 L2 正则形式混入梯度结果权重衰减的累积项被二阶动量缩放了效果大打折扣。现代框架普遍采用解耦的 AdamW把权重衰减直接应用到参数上这也是 PyTorch 现在推荐AdamW的原因。如果你在做大规模预训练或 fine-tune优先用 AdamW 而不是 Adam。这个坑真的很隐蔽假设你加了weight_decay1e-4的 Adam看起来损失曲线一切正常但实际权重衰减起到的正则化强度远比你预想的小。切到 AdamW 之后往往会发现学习率需要重新调小一点因为解耦衰减真正开始发力了。4.4 一个完整的调参决策流程假设你拿到一个新任务没有任何历史经验可以参考一个合理的第一波方案是先用 AdamWlr1e-3跑 50 轮配上 warmup 和 cosine decay看 loss 曲线的形状如果前期 loss 不降说明学习率呆在过小的位上——换 lr3e-3 或 1e-2 再试对大规模生成模型要更保守如果训练中途 loss 开始抖动查看是不是 lr 衰减在此时过快调衰减速率或延长 warmup收敛稳定后如果想抠最后那一点指标用当前最佳 checkpoint 作为初始化切到 SGDMomentumlr 对应降到 0.01 量级继续微调几轮拿几百条验证样本做提前停止监控避免微调阶段过拟合遮住收益。这套流程的本质是短期看 Adam 解决“能不能收敛”长期看 SGD 解决“能不能泛化得更好”但多数项目如果对最终精度没有那么苛刻全程 AdamW 也完全够用。5. 多年实战后才明白的道理写到最后分享几条我这几年折腾优化器攒下来的体会不一定写在论文里但确实能省时间。第一优化器选择远不如损失函数定义和数据质量重要。我曾经在某个回归任务上花了一周调 Adam 的 β 和 εvalidation loss 就是纹丝不动。后来发现是损失函数里有一个特征没做归一化它的尺度比其他特征大了三个数量级任何优化器都拉不住。把数据修好之后默认 Adam 直接性能起飞。遇到优化器不收敛第一步永远不是换优化器而是回头查数据、查梯度是否正常、查损失计算是否正确。第二梯度范数监控比 loss 曲线更能说明问题。我在训练循环里加了三行代码打印梯度的全局范数。如果梯度范数突然比历史平均值大十倍以上基本上就是学习率接近爆炸的前兆——这比看到 loss 变为 NaN 再反应要早得多留出了调参数的时间窗口。total_norm 0.0 for p in model.parameters(): if p.grad is not None: param_norm p.grad.data.norm(2) total_norm param_norm.item() ** 2 total_norm total_norm ** 0.5 # 阈值经验上 1 就要警惕 10 基本是爆炸前兆第三训练后期切换优化器时要“轻拿轻放”。从 Adam 切到 SGD我第一次直接用当时的默认学习率 0.1结果 loss 直接冲回训练初期水平。后来习惯是先加载 Adam 训练完的 checkpoint然后把学习率设成原先的 1/10 甚至 1/100用 SGD 只训 5~10 轮就停下。微调阶段的目的是在小邻域内修正不是另一次大规模搜索。第四少即是多。很多人上来就 Ultra-high 调参Adam 的 β₁、β₂、ε 全换了个遍再加上 start/end lr、warm_restart 各种 schedule一堆旋钮转了一通最后还不如默认参数配合一个简单的 cosine decay。我现在的原则是先默认再动 lr然后动 schedule其余超参数没有充分理由绝不动。调参本质上是在有限的实验预算里做搜索应该把资源花在最敏感的那个旋钮上。回头看SGD、Momentum、AdaGrad、Adam 这四个名字代表的其实是一整段深度学习发展的缩影——从简单的迭代到物理直觉的注入再到逐参数自适应每一步都踩在上一代的痛点上前进。理解了它们背后的“为什么”你不会再对着训练曲线的抖动发呆也不会因为听说某个大佬换了优化器就盲目跟风。按照这套思路去调一次模型我相信你对优化的理解会上一个台阶。