ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深度学习优化器全解析:从SGD到AdamW的演进与调参实战

深度学习优化器全解析:从SGD到AdamW的演进与调参实战

模型训练跑不动、收敛慢、效果差,很多人第一反应是调网络结构或者加数据,但说实话,优化器往往才是那个被忽视的隐形推手。同样的模型、同样的数据,换一个优化器,loss曲线可能完全是两个世界。我从SGD一路用到AdamW,中间踩过不少坑,这篇文章就把深度学习优化器的演进脉络、背后的数学直觉、以及实际训练中的调参经验一次性讲清楚,希望能让正在被loss振荡、不收敛、spike折磨的读者少走点弯路。

1. 整体脉络:优化器演进的底层驱动力

要理解优化器为什么从最简单的SGD一路演变到今天的AdamW,得先看清这条线索:每一次优化器的迭代,本质都是在解决前一个版本留下的短板。

最原始的SGD,方向准但步子死板,于是有了Momentum,给它装上惯性;Momentum解决了峡谷震荡,但所有参数共用一个学习率又不合理,于是AdaGrad出现,给每个参数单独配学习率;AdaGrad的累积平方和导致学习率过早衰减到零,RMSProp用指数移动平均来替代,解决了这个问题;Adam把Momentum和RMSProp一结合,既有一阶动量又有二阶动量,成了深度学习的事实标准。再往后,Adam在正则化项上存在数学瑕疵,AdamW把权重衰减和梯度更新真正解耦,在大模型时代全面接管。

优化器核心机制解决的问题代表性适用场景
SGD随机梯度下降,单步参数更新最基本的优化框架简单模型、调试baseline
Momentum SGD历史梯度方向累积振荡、局部极小值CV分类、部分经典网络
AdaGrad按参数历史梯度平方和缩放学习率不同参数频率差异稀疏特征场景(早期NLP)
RMSProp指数移动平均的梯度平方AdaGrad学习率过早归零非平稳目标、RNN训练
Adam一阶动量+二阶动量自适应综合收敛速度与稳定性CNN、RNN、Transformer等主流场景
AdamW解耦权重衰减与梯度更新Adam正则化失效问题预训练大模型、B站类推荐模型

这个演进过程有个明显的趋势:从"一刀切"走向"差异化"。SGD对每个参数一视同仁,学习率像一个全局旋钮;而后续优化器逐渐学会在训练过程中动态判断:"哪些参数走得慢了要推一把,哪些参数震荡太厉害要踩一脚刹车"。

一句话总结这条主线:优化器的进化史,就是一部从"盲目更新"走向"感知与自适应"的历史。理解这条主线后,后面所有细节就顺理成章了。

2. 从SGD到SGD+Momentum:梯度下降的朴素起点与惯性突围

2.1 朴素SGD:方向正确但效率堪忧

SGD的核心思想用一句话说就是:沿着loss下降最快的方向,挪一小步。

公式是 $$ \theta_{t+1} = \theta_t - \eta \nabla L(\theta_t) $$,其中 $$\eta$$ 是学习率,$$\nabla L$$ 是当前点的梯度。

朴素走法在loss曲面平坦区域会极其折磨:梯度很小,每一步都像在平地上挪动蚂蚁腿,训练一万步loss还是纹丝不动。而在陡峭的峡谷区域,梯度方向剧烈变化,参数来回震荡,loss像神经病一样上下乱跳。

实际的SGD收敛过程,可以想象成蒙着眼睛下山:脚下感觉坡度最陡的方向就迈一步,但山脊两侧的坡度常常是反复横跳的。

这里要补充一个初学者容易忽略的点:SGD的"每一笔"是用一个batch的梯度估计全量梯度的。batch有随机性,所以梯度本身带着噪声,这个噪声既是坏事也是好事——坏事是它会让loss曲线不平滑,好事是它有助于跳出局部极小值。

2.2 Momentum:给下山者装上一个惯性轮

解决SGD震荡和平坦区慢速问题的最直接思路,是引入历史梯度方向。这就有了Momentum:

$$ v_t = \gamma v_{t-1} + \eta \nabla L(\theta_t) $$,$$ \theta_{t+1} = \theta_t - v_t $$

$$ \gamma $$ 一般取0.9,意思就是让参数更新方向不仅看当前梯度,还要看前几步积累的"速度"。可以理解为推一个很重的铁球下山:铁球一旦滚起来,不会因为局部坡度变化就立刻转向,而是借着惯性冲过去。好处有两点:在峡谷中抑制震荡,在平坦区域加速前进。

我在实际项目里观察到的效果是,Momentum把ResNet这类模型在ImageNet类任务上的收敛epoch数大概缩短了四分之一到三分之一,尤其在训练初期,loss下降的速度会明显更快。

Nesterov Momentum是变体,$$ v_t = \gamma v_{t-1} + \eta \nabla L(\theta_t - \gamma v_{t-1}) $$,它会在计算梯度前先"预跳一步",相当于一个带前视的惯性轮。直观感受是它比标准Momentum更敏锐,在梯度转变方向的场景下能提前减速,实际使用中收敛略微稳定一些,但并没有质的飞跃。

2.3 SGD家族的致命短板:一马平川的学习率

SGD和Momentum最大的问题不是方向,而是学习率对所有参数一视同仁。

深度学习模型的参数量动辄百万千万,不同参数出现频率、梯度尺度千差万别。用一个全局学习率去适配所有参数,就好比一个老师对全班学生布置一模一样难度的作业——基础好的吃不饱,基础差的跟不上。

稀疏场景下的问题更突出。比如推荐系统的Embedding层,有些特征出现频率极高,梯度信息丰富,需要更小的学习率防止参数剧烈波动;有些长尾特征出现频率极低,梯度稀疏,需要更大的学习率尽快收敛。如果统一用一个学习率,Embedding参数的更新就会顾此失彼,最终导致部分特征的表示学习不充分。

正是这个"一刀切"的痛点,催生了自适应学习率优化器。

3. 自适应学习率的崛起:AdaGrad、RMSProp与Adam

3.1 AdaGrad:给每个参数按历史梯度配学习率

AdaGrad的思想很直接:对于每个参数,用它的历史梯度平方和来缩放当前学习率。更新公式为:

$$ \theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{G_t + \epsilon}} \odot g_t $$

其中 $$G_t$$ 是历史梯度平方的累积和。梯度大的参数累积项大,学习率自然被压低;梯度小的参数累积项小,学习率相对放得更大。这在稀疏特征场景下效果显著:频繁出现的特征学习率自动变小,罕见特征则保持较大的更新步长。

但AdaGrad的问题也出在这个累积上。平方和从训练开始就在单调累积,分母越滚越大,学习率最终趋近于零。训练到后期,模型基本陷入"锁死"状态,想继续精调也动不了。这在凸优化问题里也许勉强能忍,但在非平稳的深度网络训练中,这几乎等于判了死刑。

3.2 RMSProp:用滑动平均修复梯度消失问题

RMSProp的改进非常巧妙:既然累积整个历史的平方和会爆炸,那就只累积最近一段时间的梯度信息。

$$ E[g^2]_t = \rho E[g^2]_{t-1} + (1-\rho) g_t^2 $$,其中 $$\rho$$ 通常取0.9或0.99,然后参数更新为 $$ \theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{E[g^2]_t + \epsilon}} \odot g_t $$。

用指数移动平均替代全量累积,RMSProp的学习率不会单调递减到零,而是能根据近期梯度的大小动态调整。这背后的直觉是:让最近一段时间的梯度统计量来决定当前参数的更新步长,而不是让远古时期的梯度来绑架现在的决策。

RMSProp在RNN训练里表现非常出色,要知道RNN的梯度在时间维度上极其不稳定,经常要么爆炸要么消失,RMSProp的自适应缩放能力正好能顶住这种剧烈波动。

3.3 Adam:一阶动量与二阶动量的黄金组合

Adam在优化器演进中的地位,相当于集大成者。它把Momentum的一阶动量(速度)和RMSProp的二阶动量(自适应缩放)融合在一起,同时解决方向稳定性和参数差异性问题。公式为:

$$ m_t = \beta_1 m_{t-1} + (1-\beta_1)g_t $$

$$ v_t = \beta_2 v_{t-1} + (1-\beta_2)g_t^2 $$

再对 $$m_t$$ 和 $$v_t$$ 做偏差校正,因为它们在训练初期偏向于0。最后更新为:

$$ \theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{\hat{v}_t}+\epsilon}\hat{m}_t $$

默认经验值 $$\beta_1=0.9$$、$$\beta_2=0.999$$、$$\epsilon=10^{-8}$$。

Adam的工程意义在于它几乎是"开箱即用"的:它天然适应不同尺度的参数,能够处理稀疏梯度,收敛速度比SGD快得多。尤其在Transformer架构出现后,Adam基本成了标配,自注意力机制对学习率极其敏感,SGD在那上面几乎无法有效训练。

但我必须指出Adam的一个隐蔽缺陷:它在训练后期容易出现loss在平台期附近反复振荡。原因在于二阶动量 $$v_t$$ 的滑动平均估计了历史梯度的方差,这实际给更新量引入了一个随机波动因子。到了训练后期,梯度接近零,这个波动相对就显得大了,参数就在最优点附近来回抖动。

另一个更深层的问题是正则化。主流框架在Adam上实现L2正则通常是在loss里直接加权重惩罚项,但这在数学上会与Adam的一阶动量累积产生耦合,导致正则化强度被扭曲。这就是接下来AdamW登场的核心原因。

4. AdamW:解耦权重衰减与大模型时代的优化器新常态

4.1 权重衰减与L2正则化,看起来一样但本质上不同

要理解AdamW,必须先分清两个概念:权重衰减和L2正则化。

权重衰减的形式是在参数更新后直接乘一个衰减系数:

$$ \theta_{t+1} = (1 - \lambda\eta)\theta_t - \eta m_t $$

L2正则化的形式是在loss上加上权重二范数惩罚,$$\tilde{L} = L(\theta) + \frac{\lambda}{2}|\theta|^2$$,再求梯度更新。

在SGD下,这两种操作是等价的:$$\theta_{t+1} = \theta_t - \eta(\nabla L + \lambda\theta_t) = (1 - \lambda\eta)\theta_t - \eta\nabla L$$。这就是很多框架用L2正则等价替代weight decay的原因。

但在Adam里,这个等价性被打破了。Adam里L2正则项的梯度 $$ \lambda\theta_t $$ 会进入动量 $$m_t$$ 的滑动平均,导致正则项的影响被放大了约 $$1/(1-\beta_1)$$ 倍(默认约10倍)。同时,自适应学习率会把正则项梯度除以二阶动量 $$v_t$$ 的平方根,进一步扭曲正则强度。结果是使用L2正则的Adam实际正则效果不可控,一阶和二阶动量的累积让正则项作用在参数上的比例变得莫名其妙。

4.2 AdamW的核心:让权重衰减回归纯粹

AdamW的修正极其简单:把权重衰减从梯度计算中彻底剥离,直接作用在更新后的参数上。更新公式变为:

$$ v_t = \beta_2 v_{t-1} + (1-\beta_2)g_t^2 $$

$$ \hat{v}_t = \frac{v_t}{1-\beta_2^t} $$

$$ \theta_{t+1} = \theta_t - \eta\left(\frac{m_t}{\sqrt{\hat{v}_t}+\epsilon} + \lambda\theta_t\right) $$

注意这里的区别:$$ \lambda\theta_t $$ 不再参与动量和二阶动量的计算,而是作为一个独立的衰减项叠加在自适应更新量之外。这样保持了权重衰减的原意:无论梯度状态如何,参数每步都会按固定比例向零收缩,起到稳定的正则化作用。

这个改变带来一系列正面效果:参数更新与正则项互不干扰,权重衰减系数 $$\lambda$$ 可以直接解释为每步参数实际被缩小到原来的 $$1-\eta\lambda$$ 倍,超参含义更清晰;训练更加稳定,尤其在预训练大模型和长训练日程中,不会出现后期loss飘忽不定甚至发散的情况;泛化性能普遍更好,因为正则项的作用回归了本意。

实际效果上,AdamW并不总是比Adam大幅提高精度,但在大模型和Transformer类架构中,它的稳定性和可控性优势是压倒性的。我个人的体会是,从Adam切换AdamW之后最明显的变化不是指标暴涨,而是训练过程情绪稳定,不再动不动就loss spike。

4.3 优化器演进的最新方向:大模型时代的变体研究

AdamW并不是终点。大模型训练中,优化器的内存开销成了瓶颈:AdamW需要保存一阶动量、二阶动量和参数本身,每个参数就要占3份显存。对于7B甚至更大的模型,优化器状态占用的显存可能超过模型权重本身。

于是出现了两个主流方向的变体:

一是低秩近似方向。通过对二阶动量做低秩分解来压缩存储,这在减少内存的同时保持训练质量,但实现复杂度高,需要精细的数值稳定性处理。

二是分块更新方向。借鉴稀疏更新的思想,每步只更新一部分参数对应的优化器状态,从而大幅降低显存需求。代价是收敛速度有一定的损失。

另一个趋势是自适应学习率的修正。经典Adam的二阶动量在过去一段时间内可能不稳定,导致学习率缩放异常。近年出现的一些修正思路是引入更平滑的二阶矩估计,或者在训练后期切换到SGD风格更新,以改善收敛阶段的行为。

说白了大模型时代优化器的核心矛盾已经从"如何快速收敛"转向"如何在有限显存下稳定完成超长训练"。这也是为什么现代大模型训练基本清一色启用AdamW配合显存优化方案,而不是追求新的花哨算法。就目前看,还没有哪个变体能全面替代AdamW的主流地位。

5. 动手实践:优化器选型、调参与代码实现

5.1 怎么选:不同任务场景的优化器配置

很多初学者喜欢无脑上Adam或AdamW,觉得效果好省事。但真实项目中,选型要结合任务特点来。

纯CV分类任务,尤其是数据量不太大的情况下,效果稳定的SGD加Momentum(用0.9)配合Cosine退火,往往能和Adam系列打平甚至更优,而且超参更鲁棒。原因在于CNN的梯度分布相对均匀,自适应学习率带来的收益有限,而SGD的泛化性优势却显得实实在在。

NLP、Transformer类架构、大规模预训练、推荐系统等场景,无脑用AdamW就好。尤其预训练阶段因为训练步数极其漫长,AdamW的稳定性和可控性优势在长日程中体现得淋漓exhausted尽致。推荐系统里Embedding特征大多是稀疏的,自适应优化器对梯度的动态缩放几乎是刚需。

如果显存紧张或推理部署对内存敏感,可以考虑做分块更新的优化器,相当于用少量精度损失换取一倍以上的显存节省。

5.2 学习率与关键超参:从理论到实操的取值策略

学习率是优化器里最重要也最容易翻车的参数。基本原则:学习率设太小,模型慢慢折磨你;学习率设太大,训练一步就爆。

对于SGD,常用初始学习率通常在0.01到0.1之间,配合Momentum取0.9。对于AdamW,初始学习率通常在1e-4到3e-4之间,大模型预训练往往更保守,取1e-4甚至更低。

warmup几乎成了Transformer训练的标配。原理是训练初期二阶动量 $$v_t$$ 的统计量还不可靠,直接用较大学会率容易使梯度方向偏离,先从一个很小的学习率起步,逐步增长到目标学习率,等动量和方差统计量稳定了再全力冲刺。实践中常听到的"前5%步数做warmup"就是这个意思。

关于 $$\beta_2$$,如果任务中梯度波动很大(比如GAN训练、RNN长序列),把 $$\beta_2$$ 调大到0.99甚至0.995,可以进一步平滑二阶动量。代价是收敛略慢,但稳定性显著提升。我在训练GAN时曾遇到过震荡很严重的情况,调整 $$\beta_2$$ 之后明显稳定很多。

$$\epsilon$$ 这个参数经常被忽视,但它直接保护了数值稳定性,防止二阶动量接近零时除出天文数字。现代框架的默认值大多为1e-8或1e-7,如果遇到loss出现NaN,可以试着把它调到1e-6甚至1e-5看看情况。

5.3 从零手写关键优化器:理解内部机制的另一种方式

只靠调包很难真正理解优化器的内部运作。我自己曾经用NumPy实现过SGD、RMSProp和Adam,对每个细节都有了更深的体感。给出一个简明的Adam参考实现:

import numpy as np def adam_update(params, grads, m, v, t, lr=1e-3, beta1=0.9, beta2=0.999, eps=1e-8): """单步Adam更新""" updated = [] for p, g in zip(params, grads): m[p] = beta1 * m[p] + (1 - beta1) * g v[p] = beta2 * v[p] + (1 - beta2) * (g ** 2) m_hat = m[p] / (1 - beta1 ** t) v_hat = v[p] / (1 - beta2 ** t) updated.append(p - lr * m_hat / (np.sqrt(v_hat) + eps)) return updated

AdamW在手写时只需要把权重衰减从梯度计算里挪出来,放在最后独立执行:

def adamw_update(params, grads, m, v, t, lr=1e-3, beta1=0.9, beta2=0.999, eps=1e-8, wd=0.01): """单步AdamW更新:解耦权重衰减""" updated = [] for p, g in zip(params, grads): m[p] = beta1 * m[p] + (1 - beta1) * g v[p] = beta2 * v[p] + (1 - beta2) * (g ** 2) m_hat = m[p] / (1 - beta1 ** t) v_hat = v[p] / (1 - beta2 ** t) # 注意:权重衰减独立于梯度自适应项,直接作用于参数 updated.append(p - lr * (m_hat / (np.sqrt(v_hat) + eps) + wd * p)) return updated

这两段代码看似差别不大,实际行为差异显著。手写之后你会真正意识到,AdamW的简练形式背后是正则化语义的深刻修正。

5.4 常用框架下的优化器配置速查

  • PyTorch Lightning / HuggingFace Trainer:直接引用AdamW优化器类,设置好学习率和权重衰减系数即可,框架会自动配合warmup调度器。
  • 分布式训练:注意初始化和保存优化器状态时的随机种子一致性,否则断点续训会出现梯度和动量统计量不匹配的问题。
  • 混合精度训练:优化器参数通常需要以FP32精度维护,要和模型权重精度分开管理。

下面给一个适配多数场景的初始配置参考:

超参数推荐初始值说明
学习率1e-4(AdamW) / 0.01-0.1(SGD)不收敛则降一半,发散则降一个量级
Beta10.9一般不用动
Beta20.999梯度波动大时调到0.99
Epsilon1e-8出现NaN时调到1e-6或更大
权重衰减系数0.001-0.1(推荐0.01起步)过小正则不起作用,过大会损害模型容量
Batch Size尽量大增大batch时记得同步调整学习率

6. 常见问题与排查技巧实录

6.1 Loss不下降,卡在一个奇怪的高位

这是最常被小白的锅甩给模型结构的问题,但优化器的嫌疑往往更大。

先检查学习率:如果学习率太大会导致参数在能量较高的震荡区反复横跳,loss降不下去;如果太小则训练速度慢到看起来像没在学。我的排查方法是用一个极小的子集(比如32个样本)先做过拟合实验,如果loss能降到很低,说明优化器工作正常,问题在全量数据的学习率或batch size设置上。

再检查权重衰减系数。曾经有一次我把权重衰减设成1.0,模型loss直接卡住不动,因为每一步参数都被强衰减扯向零,模型基本失去了表达能力。权重衰减从0.01起步,把它当成普通超参去调就行。

6.2 Loss出现NaN,训练瞬间崩溃

NaN问题的原因有很多,但优化器相关的排查顺序是:先看学习率是不是过大,造成梯度爆炸;再看 $$\epsilon$$ 是否过小,二阶动量接近零时数值不稳定;再看混合精度下的梯度溢出。

一次实际经历中,我遇到的NaN是FP16混合精度下的梯度下溢造成的,损失函数在输出层产生极小梯度,被量化成0后,权重衰减又能继续更新,最终导致loss爆掉。解决办法是开启loss scaling并确保二阶动量以FP32保存。

6.3 训练后期Loss反复振荡,下不了平台期

这个现象在Adam里非常典型。处理思路有三种:一是逐步降低学习率,配合余弦退火或线性衰减;二是提高 $$\beta_2$$,让二阶动量估计更平滑;三是切换到带权重衰减的AdamW,并配合小学习率。

我自己的经验是,训练后期把学习率按步数线性衰减到初始值的1/10,比一直保持恒定要好得多。平台期的loss振荡大部分时候是学习率没退火造成的,不一定是优化器选错了。

6.4 训练初期损失下降很快但验证集完全不涨

优化器导致的训练-验证gap大,往往与正则化设置有关。检查一下权重衰减系数是否过小、是否有数据增强、以及模型的归一化层是否正常工作。从优化器角度说,所有权重衰减系数从0.01开始调,如果验证集也没有变化,那问题大多不在优化器上。

7. 一点个人总结与选型建议

这两年做模型训练,最深刻的一个体会是:优化器不是拿来即用的魔法,而是一套需要理解并调校的系统。SGD系列像传统的手工锻造,每个细节都有讲究,调好了很出彩;AdamW则是工业流水线的标准件,稳妥可靠,但如果不理解权重衰减和动量的关系就随手开用,出了问题很难排查。

如果你正在带一个新项目,我的建议是:默认方案选AdamW,初始配置按1e-4的学习率和0.01的权重衰减开始训练,配合warmup和cosine退火调度。如果你发现模型收敛了但效果还不够好,再回溯检查SGD+Momentum是否更适合当前任务。

优化器领域当然还在继续演进,新的内存优化方案、更稳定的自适应机制不断出现,但万变不离其宗:所有设计都是在梯度方向、步长估计与正则控制之间寻求更好的平衡。理解经典系列的演进逻辑,再去看任何新论文,都会快很多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进