训练深度学习模型这几年,我见过太多人把精力都花在网络结构设计上,却在优化器选择上特别随便。尤其当你从 SGD 一路试到 Adam、再换成 AdamW,会慢慢意识到一件事:优化算法不是流水线上一个无关紧要的旋钮,它几乎决定了你的模型到底能不能收敛、收敛到什么地方、以及泛化能力是及格还是优秀。这篇文章我想用实际训练中的视角,把从 SGD 到 AdamW 这一路上的关键原理、踩坑细节和排查思路讲清楚,适合刚接触深度学习、正在被损失曲线折磨的初学者,也适合那些已经跑通不少模型、但对优化器内部机制仍然模棱两可的同学。
1. 为什么优化算法决定训练成败
1.1 优化器不是“调参玩具”
很多人第一次接触 PyTorch 或 TensorFlow 时,第一行代码大概率是Adam,因为教程里默认给的就是这个。但如果你把优化器当成一个不用思考的“黑盒”,损失函数不降的时候就会非常被动。实际上,优化器的作用远比“把梯度往反方向走一步”更复杂,它要解决的是:在损失曲面这种高维、非凸、充满鞍点和局部极小值的复杂地形上,如何用有限的计算资源找到一个足够好的解。
我经常用一个类比来解释这件事:梯度下降就像是蒙着眼睛在山谷里下山,你能感觉到脚下哪个方向最陡,但不知道整个地形长什么样。如果步长太大,你可能会直接跨过山谷对面的缓坡,甚至跳到悬崖下面;如果步长太小,你在平缓的鞍点区域可能磨上几千轮都没什么进展。而不同优化器的本质差异,就是它们对“步长”和“方向”的估计方式不同。SGD 只看当前梯度方向,动量方法会参考历史速度,自适应方法则对每个参数维度单独调整步长。
从这个角度看,优化器选型就不是“哪个好用用哪个”,而是根据你项目的任务类型、数据规模、模型结构,去选择一个与损失曲面特性匹配的更新规则。理解这一点,后面再调试模型时就不会一头雾水。
1.2 从损失曲面读懂优化目标
模型训练的终极目标并不是让训练损失无限逼近零,而是在验证集上表现稳定。这就意味着优化算法不仅要把损失压低,还要压低得“恰到好处”。在超高维空间中,我们几乎不可能可视化真正的损失曲面,但从实验规律中可以总结出一些共性:平滑但扁平的极小值区域往往对应更好的泛化性能;尖锐的极小值区域很容易过拟合;而鞍点附近的梯度很小,很多非自适应优化器会在这里停滞不前。
这也是为什么 SGD 和 Adam 的表现会出现明显差异。SGD 的更新路径通常更“直”,它更容易穿越尖锐峡谷后落在比较平坦的区域;而自适应优化器因为每个参数更新幅度被归一化,常常能找到更快的下降路径,但也可能收敛到泛化稍差的尖锐区域。当然这不是绝对规律,只是从大量实验里看到的倾向。理解这个背景,再看 AdamW 这类变体,就会明白大家为什么那么执着于“恢复 SGD 的泛化优点,同时保留自适应优化器的训练效率”。
2. SGD:最朴素的梯度下降,也是最容易踩坑的起点
2.1 批量大小与学习率的互相牵制
标准 SGD 的更新公式看起来简单:θ = θ - lr * g,这里g是当前批次算出来的梯度。问题在于,这个更新同时受学习率和批次大小影响。如果你把批次翻倍,梯度的方差通常会变小,但梯度本身的量级不会严格减半,所以最优学习率可能也要跟着调整。很多同学第一次训练 CNN 时,固定用 64 的 batch size,把学习率设为 0.01,模型还能跑;一旦为了加速训练把 batch size 改成 256,如果不调学习率,训练反而开始震荡甚至发散。
我自己的经验是,在换 batch size 之后,可以先粗略用“线性缩放规则”做参考:批次变为原来的 k 倍,学习率初始值也可以往 k 倍方向试探,但上下浮动一两倍都很正常,不能死记公式。更重要的是,SGD 对特征缩放特别敏感。如果输入没有做标准化,或者网络某一层的参数初始化尺度差异很大,不同维度的梯度量级可能差出好几个数量级,这时用同一个学习率更新所有参数,效果一定很差。
所以在用 SGD 之前,一定要确认数据归一化和参数初始化策略是稳妥的。不要一上来就怪优化器不收敛,很多时候是前面的数据管线已经在拖后腿了。
2.2 动量到底解决了什么
纯 SGD 在遇到局部震荡时,更新方向会被当前批次噪声带着到处乱跑。动量(Momentum)的引入就像给下山的人加了一个惯性:每一步不只是看当前脚下的梯度,还要结合之前速度的方向和大小。物理意义很简单,它让更新方向在历史信息中平滑化,能加速沿一致方向的移动,同时抑制方向频繁变化的抖动。
具体实现上,多数框架中的动量项并不复杂:维护一个速度变量v,更新时v = momentum * v + g,然后用θ = θ - lr * v。动量系数通常取 0.9 或 0.99,相当于把过去大约1 / (1 - momentum)步的梯度信息纳入考虑。很多人只把这个参数当固定的“稍微好用一点”的开关,但实际调试时它和学习率是协同作用的。动量越大,有效学习率也会变大,因此如果你把 momentum 从 0.9 改成 0.99,学习率可能需要适当调小。
我在实际项目中用过一次高动量配置,收敛速度确实快,但损失曲线在后期出现了低频振荡。后面把动量降到 0.9,并同步降低了学习率,问题才消失。这说明即使是这么简单的超参数,也需要放到整个训练配置里去看,而不是孤立地调。
3. Adam 为什么流行,又为什么需要改
3.1 自适应学习率的直觉
Adam 的全称是 Adaptive Moment Estimation,它把动量思想和自适应学习率结合到了一起。具体来说,它既维护了梯度的一阶矩估计,也就是类似动量的历史平均梯度,也维护了梯度的二阶矩估计,也就是历史梯度平方的平均。对于梯度比较大的维度,二阶矩会变大,更新步长会被压低;对于梯度比较小或变化缓慢的维度,二阶矩较小,更新步长会相对放大。这样做的效果是:每个参数维度的更新幅度大致被归一化,模型对学习率的敏感度大幅下降。
这解释了一个经典现象:同样的学习率,SGD 可能完全跑飞,Adam 却能稳定训练。尤其是在 Transformer、GAN 这类模型上,不同层、不同参数的梯度尺度差异非常大,手动给每个层配学习率几乎不现实,而 Adam 等于内置了这种“按维度缩放”的能力。所以它在 NLP 和生成模型领域几乎是默认选择,不是没有道理的。
但自适应学习率并不是免费的午餐。它的问题在于,二阶矩估计本质上是基于历史梯度平方的调整,如果历史梯度过大或过小,会导致个别维度的更新步长被过度压缩或放大。尤其是在训练后期,梯度变小、噪声占比升高,Adam 容易出现“来回震荡但就是压不到更低误差”的状态。实际操作中,我见过很多模型的 Adam 训练曲线呈现出“前期飞快、后期原地磨”的形状,这和二阶矩长期累积后的尺度失衡有很大关系。
3.2 Adam 的偏置校正和实际陷阱
很多框架实现 Adam 时都包含偏置校正步骤,因为前几步的一阶矩和二阶矩估计都从零开始,如果直接用来更新,初期步长会明显偏小。偏置校正在训练初期效果尤其明显,它把前几步的矩估计放大到无偏状态。
实际陷阱主要是两个。第一,Adam 对学习率初始值虽然不像 SGD 那么敏感,但也不是完全不敏感。我在项目里试过把学习率从默认的 0.001 调到 0.0001,很多任务训练损失反而更稳定,最终精度也没有明显下降。第二,Adam 对权重衰减的处理方式藏了一个隐藏 bug 级别的细节:传统实现里权重衰减直接加到梯度上,相当于在更新时把 L2 正则项混入了自适应缩放过程。结果会导致权重衰减的效果被削弱,尤其是配合大学习率时更明显。这也是 AdamW 出现的最直接动机。
使用 Adam 时还有一个小经验:如果你看到训练损失前期降得很快,但验证指标一直不动,先不要急着加大模型容量,很可能不是模型不够强,而是优化器把当前区域过早“锁死”在一个容易走但不够好的路径上。这时候把学习率调低、或者切换权重衰减策略,比改网络结构更有效。
3.3 AdamW:解耦权重衰减的实用价值
AdamW 的核心改动看起来很小:把权重衰减从梯度里拿出来,单独在参数更新时直接乘一个小于 1 的系数。但正是这么一点改动,让权重衰减重新变成“真正的 L2 约束”,而不是被自适应学习率扭曲的伪正则。
从公式风格看,传统 Adam 的权重衰减方式是先算出梯度g = 原始梯度 + weight_decay * θ,再对这个合成梯度做一阶矩、二阶矩的更新和缩放。问题在于,梯度里的这一项也会被二阶矩缩放。假设某个参数维度梯度一直很小,它的二阶矩会很小,更新的缩放因子反而很大,最后权重衰减的实际力度会被放大到远超预期。AdamW 直接把权重衰减放在参数更新外层,完全绕过自适应缩放,这个现象就被解除了。
在实践中,AdamW 对很多任务都有正向帮助。我在图像分类、文本分类和对比学习模型上都做过对比,AdamW 的验证精度普遍不低于同条件下 Adam,有些任务还稳定高出一两个点。特别是在需要较长训练轮数的预训练或自监督场景,AdamW 的后期稳定性明显更好。很多人一开始不习惯“只改一点点”的优化器变体,但正是这种细节修正,让 Adam 家族在实践中更可靠。
4. 常见问题与排查技巧实录
4.1 损失不降:先查数据再查优化器
训练开始后损失完全不动,或者只降一小段就停住,是最常见的问题之一。我自己的排查顺序通常是:先看输入数据有没有有效信息,再看标签有没有错位,最后才怀疑优化器。
有一次我用一个自建的图像数据做分类,模型结构很常规,优化器用 Adam,学习率也是常见值。但跑了几个 epoch,训练损失始终在某个数值附近横盘。排查到最后,发现是数据读取时把图像像素值错误地在 [0, 255] 和 [0, 1] 之间来回变换,导致有效特征几乎被噪声淹没。把数据归一化修好之后,同一个优化器、同一个学习率,损失立刻正常下降。
如果数据没问题,再从优化器角度检查:学习率是不是太小?权重初始化是不是把输出层推到饱和区?梯度是不是有 NaN 或者极大值?这些都可以通过打印梯度的统计信息快速定位。不要盲目把损失不降归咎于“优化器不够好”,那样很容易越调越偏。
4.2 训练震荡:学习率预热和调度
训练过程中损失上下剧烈跳动,常见原因有几个:学习率过大、batch size 过小、梯度中有大量异常值、或者是接近收敛时没有降低学习率。对 Transformer 这类模型来说,学习率预热几乎是必需品,因为前几步的梯度方差特别大,直接进入正常学习率容易把早期的参数初始状态彻底搅乱。预热阶段先把学习率从很小线性升到目标值,给参数一个相对稳定的起步过程。
我常用的策略是先跑一个很小的多次实验,观察损失曲线形态。如果前 100 步就开始数值震荡,首先把学习率除以 3 再试;如果震荡仍然明显,就把预热步数从 1% 总步数提到 5% 左右。在这个阶段不要同时改一堆超参数,一次只改一个,否则你根本没法判断是哪个变动把曲线救回来了。学习率调度也一样,等训练进入中段后按步数或按指标衰减,比从头到尾固定学习率更符合大多数任务的收敛规律。
4.3 收敛但泛化差:权重衰减、梯度裁剪和正则
训练损失降得不错,验证集表现却差,这是另一个高频问题。很多人把锅甩给“过拟合”,然后疯狂加数据增强。但有时候问题出在优化器细节上:比如 Adam 的权重衰减被削弱,导致参数范数控制不足;或者模型在训练后期还在用很大的学习率,把权重推向了尖锐的极小值区域。
AdamW 之所以在泛化对比中经常胜出,一个重要原因正是恢复了有效的权重衰减机制。在训练后期配合余弦退火这类调度,让学习率平滑下降,通常能让模型在验证集上的表现再上一层。梯度裁剪则更多用在 RNN、Transformer 或者训练不稳定场景,它不直接提升泛化能力,但能防止某个异常梯度把已经学好的参数破坏掉,间接帮助训练走得更远。
我还试过在 AdamW 上把 weight decay 从 0.01 往 0.05 方向调整,配合较小的数据增强,结果在图像分类任务上的验证精度有一定提升。但这类调整必须跟着验证集来,千万不要用训练集表现做判断,否则你只是在优化一个过拟合指标。
5. 真实项目中的选型建议
5.1 图像模型、语言模型与小批量场景的优化器选择矩阵
不同任务里,优化器选型其实是很有规律的。图像分类、目标检测这类 CNN 任务,很多经典研究都倾向用带动量的 SGD,搭配合理的初始化、学习率调度和权重衰减,泛化效果通常很稳。但如果你训练时间有限,或者模型结构比较复杂,AdamW 也完全能替代,而且调试成本更低。
语言模型、Transformer 类结构则更推荐 AdamW,原因主要有两个:这类模型参数量大,不同位置的梯度尺度差异明显,自适应学习率能让训练起步更顺;同时权重衰减的解耦能有效控制参数范数,对长时间预训练尤其友好。我训练中小规模的文本分类模型时,默认配置就是 AdamW + 0.01 权重衰减,几乎不需要额外调优。
小批量场景是另一个容易被忽略的坑。batch size 很小的时候,每个 batch 的梯度噪声很大,Adam 的优势更明显,因为它通过二阶矩估算给噪声相对大的维度压低了步长。这时候如果硬要用 SGD,通常需要搭配很强的手动调度和更长的训练时间,并不划算。反过来,如果你能用很大的 batch size,SGD 配合动量反而可能更稳定。
下面是我在实际项目里常用的一张参考表,注意它只是一个出发点,不是绝对真理:
| 场景 | 推荐优化器 | 常用学习率 | 权重衰减 | 备注 |
|---|---|---|---|---|
| CNN 图像分类,大 batch | SGD + Momentum | 0.01 到 0.1 | 1e-4 到 5e-4 | 需要较稳的调度 |
| CNN 图像分类,小 batch | AdamW | 1e-3 到 2e-3 | 0.01 到 0.05 | 训练更快,调试更省心 |
| Transformer 类 NLP | AdamW | 5e-5 到 5e-4 | 0.01 | 学习率预热基本必需 |
| 生成模型 / GAN | AdamW | 2e-4 到 1e-3 | 0.01 | 梯度裁剪视情况使用 |
| 大规模预训练 | AdamW | 1e-4 到 3e-4 | 0.01 到 0.1 | 长轮训练下稳定性优先 |
5.2 参数搜索从粗到细的经验
很多初学者一上来就做网格搜索,把所有学习率、权重衰减、动量系数都排列组合跑一遍,这样既烧时间又难看出规律。我的经验是先固定一个“大概率能跑通的配置”,比如模型如果是 Transformer,就先用 AdamW,学习率 3e-4,权重衰减 0.01,预热几千步,跑一个短的训练过程。确认训练损失能下降之后,再开始微调超参数。
调参时要有一个清晰的顺序:先定性看优化器选型对不对,再看学习率数量级,然后看权重衰减,最后才看调度细节。每次只动一个维度,并记录验证集表现,而不是只看训练损失。我有一个习惯:每次实验都存下完整的超参数和损失曲线截图,方便回头判断哪一个改动真正起了作用。
有些时候你会发现,验证集精度对学习率极其敏感,但对权重衰减却不敏感。这并不代表权重衰减没用,而是说明当前训练数据和模型容量之间暂时没有过拟合压力。真正有意义的是在模型容量足够大、训练轮数足够的条件下,观察这些正则手段的效果。不然你只是在浪费时间调一个根本不会被触发的约束。
6. 写在最后的一点个人体会
把优化器从 SGD 理解到 AdamW,不只是为了记住几个公式,更重要的是建立一种直觉:模型训练到底在解决什么形状的问题,不同的更新规则又分别会走向哪里。我自己的习惯是,在新项目里不急着选择最“高级”的优化器,而是先用一个可靠的配置把整个数据管线、损失计算和评估流程跑通,再回头验证不同优化器的差异。因为很多时候,模型不收敛并不是优化器的问题,而是数据、初始化、学习率调度共同作用的结果。
另外我想分享一个实用小技巧:训练时记录每一层梯度的均值和方差,用日志或可视化工具展示出来,比单纯盯一个全局损失要直观得多。梯度消失、梯度爆炸、某个模块完全没有学习信号,这些都能从梯度统计上早期看出来。相比之下,盲目换优化器反而容易掩盖真正的问题。
深度学习优化算法这个方向还在继续演进,但从 SGD 到 AdamW 这条主线,已经足够覆盖绝大多数实际项目需求。把这条主线上的原理和坑都吃透,以后再看任何新的优化器变体,你都不会觉得陌生。