☰
深度学习优化算法全解:从梯度下降到Adam及调参实战
2026/10/18 13:04:09 网站建设 项目流程

相信不少人在训练深度学习模型时都遇到过这种情况:loss曲线像心电图一样上下狂跳,或者收敛到一半死活下不去,又或者一个不小心就冒出NaN,然后整个实验白跑。遇到这些问题,很多人第一反应是模型结构写得不对,或者数据预处理出了问题。但往往检查了半天,发现错在一个更基础、更容易被忽略的环节——优化算法的选择与参数配置。这一篇“动手学深度学习”系列的内容,我打算把深度学习优化算法这件事完整地梳理一遍,从最朴素的梯度下降讲到形形色色的改进版本,再结合我自己调参时踩过的坑,把原理和实战放在一起说清楚。不管你是刚入门的学生,还是已经跑过几个项目的研究人员,这篇内容都能帮你少走不少弯路。

1. 优化算法全景图:先看清我们手里有哪些牌

1.1 为什么训练离不开“优化算法”这个角色

要理解优化算法存在的意义,先得明白训练神经网络到底在做什么。简单来说,我们有一堆参数(权重和偏置),有一个损失函数来量化预测值和真实值差得多远,训练过程就是不断寻找一组参数,让损失函数的值尽可能小。这件事在数学上是一个典型的无约束优化问题,只不过自变量的维度特别高,动辄百万、千万级别,而且函数形状极其复杂,没法用解析方法直接求解。

优化算法的本质,就是回答一个问题:已知当前参数点,下一步该往哪个方向走、走多大步。方向决定了模型能不能找到更好的参数区域,步长决定了收敛速度和最终精度。如果方向选错,模型可能在某个局部区域来回震荡;如果步长太大,可能直接跨过最优区域,甚至导致loss爆炸;如果步长太小,训练会慢到让人怀疑人生。

有人可能会说,为什么不直接用二阶方法,比如牛顿法或拟牛顿法,那样收敛不是更快吗。理论上确实如此,但在深度学习中,参数量通常上千万,二阶方法需要计算海森矩阵或其近似,存储和计算开销完全承受不起。梯度下降类的算法只需要计算损失对每个参数的偏导数(也就是梯度),计算量可控得多,而且配合梯度累积、分布式训练等技术,能很好地扩展到大规模模型。所以业界实际上走的路线是:用一阶梯度信息,加上各种“启发式”的修正策略,让更新过程既稳又快。

这一篇要把“优化算法”当作主题,其实也是在提醒一个问题:训练模型时,大部分人把注意力放在网络结构、损失函数、数据增强上,但优化器本身怎么选、参数怎么配,同样在很大程度上决定了一个模型到底是能跑出漂亮的结果,还是停在原地空转。

1.2 一张直觉地图:从批量梯度到自适应学习率

优化算法家族里,主流演进路线大体是这样的:先从最原始的批量梯度下降出发,然后出现随机梯度下降,再后来有了动量机制,接着又出现了自适应学习率的一派,最终汇流成我们现在最常用的那些优化器。

批量梯度下降(BGD)是最朴素的想法,它用整个训练集算一次损失函数的梯度,然后沿负梯度方向更新一次。这种方法的优点是每次更新方向都比较准确,缺点是计算量太大,数据集稍微大一点就跑不动,而且它只有一个固定的学习率,在复杂损失曲面上表现不佳。

随机梯度下降(SGD)的出现解决了批量梯度下降计算量大的问题。SGD每次只用一个样本的梯度来更新参数,计算开销瞬间降下来,但由于单样本梯度有随机性,更新路线很抖,收敛过程中会来回震荡。后来大家发现,用一个小批量的样本来计算梯度(mini-batch SGD)是个折中方案,既比单样本稳定很多,又比全批量高效很多,于是mini-batch SGD成了最通用的基础方案。

那之后,改进方向大致分两条线。一条线在“方向”上做文章,代表人物就是动量(Momentum)和Nesterov加速梯度(NAG)。它们模仿物理世界里小球滚下坡的惯性,把历史梯度的累积方向叠加到当前更新中,这样能有效压制震荡,帮助参数冲过平坦区域和鞍点。另一条线在“步长”上做文章,代表人物是AdaGrad、RMSProp。它们根据不同参数的历史梯度大小,自动调节每个参数的学习率,让频繁更新的参数步子小一点,让稀疏更新的参数步子大一点。

Adam可以看作是两条线的集大成者,它既引入了一阶动量(类似Momentum的累积方向),又引入二阶动量(类似RMSProp的自适应步长),还加上了偏差校正机制,让训练前期数值更稳定。后来又有AdamW把权重衰减和动量更新解耦,彻底解决了Adam在正则化方面的一些隐性问题。整条演进路线其实是逐步在回答“怎么在复杂的高维损失曲面里更快更稳地求最小值”这个问题。

1.3 选型思路:不同任务配不同的优化器

既然优化器这么多,那具体选哪个合适?我的经验是,不能只看名气,要看任务背景、模型规模和可训练的预算。

对于计算机视觉任务,比如图像分类、目标检测,CNN结构配合SGD加动量(momentum设为0.9)依然是经典且稳健的组合。SGD虽然收敛速度不如Adam这类自适应方法,但它的泛化能力往往更好,最终测试集精度有望更高。很多人初学时觉得Adam丢上去就完事了,但在CV任务里,SGD+动量有时候能多出一个点的精度,这点差距在模型竞赛里可能就是决定性因素。

对于自然语言处理任务,特别是Transformer结构,AdamW基本上是标配。Transformer的训练对学习率波动极其敏感,AdamW的自适应步长能稳住更新过程,而解耦的权重衰减方式参数更可控。Vision Transformer(ViT)这类把Transformer结构搬到视觉领域的模型,通常也沿用AdamW。

对于超大模型的预训练阶段,比如动辄几十亿、上百亿参数的大语言模型,常规的单卡优化器也扛不住了,需要专门的分布式优化策略,比如LAMB、LARS这类把学习率和各层参数规模解耦的优化器。它们做的事情本质上还是“自适应步长”,只不过更进一步,把步长的尺度跟模型层的规模接上了头。

所以选型没有一套万能公式。我的习惯是:先看项目里最主流的框架和文章默认用什么,在这些“被验证过”的配置上开始实验。

2. 核心算法逐层拆解:每个参数更新背后的原因

2.1 从BGD到SGD:用“噪声”换速度

先展开讲讲从BGD到SGD这个步骤,因为这个转变是整个优化算法演进中最关键的一次跳跃。BGD的更新公式是w = w - lr * grad_full,这里grad_full是全体样本梯度的平均值。这样做方向很可靠,但每次迭代都扫一遍全量数据,训练一轮的成本高得惊人。如果你用GPU训练一个现代神经网络,用全批量梯度下降几乎是不现实的。

SGD的思路是:从梯度计算这个最大的开销上动刀子,每次只随机抽一个样本,用这个样本的梯度来近似全体梯度。w = w - lr * grad_sample。这样做更新一次的成本极低,但问题也随之而来——单个样本会引入很大的梯度方差,参数轨迹非常崎岖。这个崎岖倒也不完全是坏事,因为引入的随机噪声在优化过程中有一种类似“随机扰动”的正则化作用,神经网络在纯SGD下训练出来的结果,有时泛化性能反而比全批量更好。这就是所谓“噪声换速度”的一种平衡。

而mini-batch SGD是中间态,每次取一个batch的样本,比如32个或128个,计算batch内的梯度均值。这样梯度方差比单样本低很多,方向更稳定,计算开销又远小于全batch。深度学习中实际说的SGD,默认指的就是mini-batch版。

batch size的大小对这个过程影响非常大。batch比较小时,更新方向噪声大,但每个step的耗时短,迭代次数多,模型可能探索到更平坦、更通用的极值区域;batch比较大时,梯度估计更准确,训练曲线更平滑,但每一步耗时更长,而且容易陷入尖锐极值,泛化能力不一定占优。所以很多大batch训练都伴随学习率等比例的放大或余弦调度,目的就是抵消大batch带来的泛化劣势。

2.2 动量机制:给梯度一个“惯性”

单纯靠SGD,还有一个让人头疼的毛病,就是沿着峡谷状的损失函数表面来回震荡。想象一下,一个窄而深的峡谷,水平方向上梯度很小,垂直方向上梯度很大,普通SGD会在峡谷壁上左右弹跳,前进速度却极慢。更麻烦的是,深度学习损失函数里非常多的“鞍点”,在这些地方所有方向上的梯度都趋近于零,普通SGD会像断电的机器人一样僵在原地。

动量机制(Momentum)的解决办法是:维护一个速度变量v,每次更新时,把历史速度乘上一个衰减系数(典型值0.9),再加上当前梯度,然后沿着更新后的速度方向走。对应公式为:v = momentum * v + grad,w = w - lr * v。用生活类比来说,这就好比滑下坡的雪球,就算遇到一段平地,它也会因为惯性继续往前滚,不至于立刻停下来。这个“惯性”可以有效压制梯度方向的频繁反向震荡,同时帮助参数更坚定地越过鞍点。

Nesterov加速梯度(NAG)是在动量基础上再做一步“前瞻”。它先按照当前速度估算出位置更新后的近似位置,在那个位置上计算梯度,再结合当前速度做最终更新。Nesterov动量在凸优化理论上有更好的收敛边界,在实际神经网络训练中,它跟标准动量的差距有时候没那么大,但在某些问题上确实更稳、更快。如果做CV训练时发现标准Momentum收敛偏慢,我会试试NAG,这个对比测试的成本很低。

2.3 自适应学习率:AdaGrad和RMSProp的迭代逻辑

动量是在“速度方向”上做文章,而自适应学习率的一派盯住的是“每个参数的步长”。为什么要分参数处理步长?因为不同参数的梯度量级是不一样的。比如一个稀疏特征对应的权重,它只在很少的样本里被更新,另一个密集特征对应的权重则几乎每个batch都在更新。用一个全局学习率,前者学习得太慢,后者又可能震荡。如果能让稀疏参数获得较大的有效步长、密集参数获得较小的有效步长,那优化效率会高很多。

AdaGrad是第一个里程碑式的自适应方法。它记录每个参数历史梯度的平方和,然后更新时把这个平方和开根号放到分母上:w = w - lr / sqrt(G) * grad。它的逻辑是:如果一个参数的历史梯度很大,说明它更新频繁,有效步长就该自动缩小;梯度稀疏的参数,历史累积量小,有效步长就大。问题也很明显,平方和只会越来越大,分母单调递增,导致有效步长越来越小,训练后期几乎学不动。这相当于一个人走到后半程,越走越没劲,最后僵在原地。

RMSProp对AdaGrad做了一个关键修改,改用“指数滑动平均”来统计历史梯度的平方,而不是简单累加。E[g^2] = rho * E[g^2] + (1 - rho) * g^2。这样过去很远的历史只保留一个很小的权重,不会无限累积,有效步长就不会单调递减到零。这相当于给记忆加了一个滑动窗口,更符合非平稳环境的优化需求。

实际上,RMSProp里的更新公式已经非常接近现代优化器的内核了。只不过RMSProp缺少“动量方向”的累积。Adam的出现,等于把RMSProp的自适应步长和Momentum的惯性方向做了统一结合。

2.4 Adam与AdamW:一阶、二阶矩估计和权重衰减解耦

Adam全称是Adaptive Moment Estimation,它维护了两个指数滑动平均变量:一个是梯度的一阶矩估计m,用于累积方向;一个是梯度的二阶矩估计v,用于调节步长。公式看起来比SGD复杂不少,但拆开看并不恐怖:m = beta1 * m + (1 - beta1) * grad,v = beta2 * v + (1 - beta2) * grad^2。

Adam有一个普通SGD没有的步骤——偏差校正。因为在训练刚开始时,m和v都是从0开始累计的,早期值会比真实值偏小很多,直接除会造成更新步长异常偏大。所以Adam在正式更新前先做一次校正:m_hat = m / (1 - beta1^t),v_hat = v / (1 - beta2^t),其中t是迭代步数。这一步在点击量上看似不起眼,但对训练初期的稳定性影响非常大。

AdamW则处理了一个深层问题。传统Adam在做权重衰减时,直接在最终的梯度里加上weight_decay * w这一项,然后再做自适应步长缩放。问题在于,Adam里每一步实际有效步长是不一样的,同样的惩罚项在不同参数上被缩放了不同倍数,这让正则化的作用变得不可控。AdamW的思路是把权重衰减直接从梯度里拿出去,不参与自适应步长的缩放,而是在参数更新之后单独执行一个衰减操作:w = w - lr * wd * w。这样正则化和优化器的动态步长就完全解耦了,实际使用中AdamW几乎全面替代了Adam,尤其是在NLP和Transformer类模型中效果更稳。

很多人可能不明白,为什么这样一个看起来不算大的改动,会带来训练结果上的显著差异。我理解的关键在于,解耦让超参数weight_decay有了更清晰、更稳定的语义——它在不同学习率、不同训练阶段下都能保持一致的相对强度。而对于带自适应步长的优化器来说,语义清晰的超参数是调出好模型的前提。

3. 参数配置与调参实战:纸上谈兵之外的门道

3.1 学习率怎么定:范围测试与三层经验法则

学习率可以说是深度学习里最敏感的一个超参数。同一个模型,学习率差一个数量级,结果可能是收敛爆炸和稳步下降的天壤之别。我看到很多人只用默认的1e-3当学习率,跑不动就换优化器,实际上问题可能在学习率上。

想认真定学习率,可以用“学习率范围测试”这个方法。做法很简单,让学习率从一个很小的值(比如1e-5)开始,每步迭代逐渐增大,一直到1e-1左右,记录下每一步的loss值,最后画出一条loss随学习率变化的曲线。你会看到曲线至少有一个阶段loss在明显下降,这个阶段对应的学习率区间,就是模型最敏感也最合适的范围。常见的经验是,把曲线下降最陡峭那个点对应的学习率再除以3到10,作为正式训练用的初始学习率。

关于学习率还有一套“三层经验法则”,我整理如下:

  • 第一层,初始化阶段:用上文范围测试选出的值作为起始学习率,通常在1e-4到3e-3之间。
  • 第二层,中期训练阶段:如果需要更精细控制,可以结合学习率调度器,比如训练到总步数30%或50%时按余弦规律逐步调低。
  • 第三层,后期收尾阶段:当loss下降到平台期,可以手动把学习率再缩小10倍,继续训练几个epoch,通常能榨出最后的几个点精度。

3.2 beta、epsilon、weight_decay:藏在默认值里的门道

配置优化器时,大部分人只关注学习率,其他参数全用默认值。这当然没问题,因为那些默认值经过千锤百炼,覆盖面很广。但如果你想追求更好的效果,这些参数是值得花时间研究的。

先看beta1和beta2。beta1控制一阶动量(速度方向)的衰减,默认0.9,对大多数任务都稳。beta2控制二阶动量(自适应步长)的衰减,默认0.999,但这个默认值在部分场景下反而会导致有效步长过小或者训练后期难以收尾。比如训练NLP模型时,如果想增加权重衰减的作用,很多人会把beta2调到0.95或0.98。这个调整的作用是让二阶矩估计更快地吸收近期梯度变化,而不是被非常久的梯度历史拖住。

再看epsilon,这个数值是为了防止分母除0加的一个极小量。默认值是1e-8,但在使用混合精度训练时,如果梯度出现很小的变化,默认epsilon在低精度下可能失效,导致更新异常,这时经验上可以把epsilon调到1e-6甚至1e-4。别看它小,在数值稳定性上是很关键的一环。

最后是weight_decay。这个参数在SGD和AdamW中的语义完全不同。SGD中的weight_decay直接作用于梯度,通常设置1e-4到5e-4;AdamW中因为解耦了权重衰减,经验设置可以在0.01到0.1的范围。很多从SGD迁移到AdamW的人仍然沿用1e-4的权重衰减系数,结果会发现正则化几乎不起作用,这就是不理解参数语义导致的误用。

3.3 学习率调度器:不只要在代码里引入,还要理解调度节奏

有了合适的初始学习率,不代表整个训练过程都该用同一个学习率。训练初期,模型权重的随机初始化状态离最优还很远,这时需要相对较大的步长快速进入一个有希望的区域;训练后期,参数已经落到一个较优区域,这时还用大步长来回震荡,会错过精细的极小值。所以学习率调度器的本质是:在探索期保持合适的学习率,在精细调整期逐步降低步长。

我最常用的两个调度器,一个是StepLR(分段衰减),一个是CosineAnnealingLR(余弦退火)。StepLR的思路很直观:每隔一定epoch数就把学习率乘以一个衰减因子,比如每30个epoch衰减为原来的0.1倍。它的缺点是衰减节点选择很凭经验,衰减太早会拖慢收敛,衰减太晚模型又可能一直在最优附近震荡。CosineAnnealingLR则采用了一个连续的余弦曲线,让学习率从初始值平滑地衰减到设定的最小值。这种平滑方式不需要硬切衰减节点,训练过程更顺滑,在不少CV和NLP任务中表现都很好。

Transformer类模型还常用warmup + decay的组合。warmup是指在训练最开始的一小段步数内,学习率从0或很小的值逐渐线性增加到预设的最大值。原因在于Transformer初始化状态下的参数分布比较特殊,如果开局就直接用较大学习率,很容易把训练带崩。warmup步数通常设定为总步数的5%到10%,然后接余弦退火衰减。

3.4 常用优化器推荐配置表

下面这张表是我在实际项目中常用的配置起点,不一定是最优解,但作为基线是靠谱的。如果你在写自己的代码,可以考虑直接从这里开始再微调。

任务类型常用优化器初始学习率关键参数备注
CV分类(CNN)SGD + Momentum0.01~0.1momentum=0.9, weight_decay=1e-4配合Cosine退火效果更佳
CV检测/分割SGD + Momentum0.001~0.01momentum=0.9, weight_decay=5e-4学习率通常比分类任务略低
NLP(Transformer)AdamW1e-5~3e-5beta2=0.98, weight_decay=0.01~0.1必须配warmup
ViT类视觉模型AdamW1e-4~3e-4weight_decay=0.05~0.3需要较长的warmup
生成对抗网络(GAN)Adam1e-4~2e-4beta1=0.5~0.9判别器学习率通常比生成器略高

提示:所有推荐数值仅供参考,实际训练时务必把“验证集指标”作为判断标准,而不是只看训练loss降得有多快。

4. 训练现场的常见问题与排查实录

4.1 loss震荡不降:问题可能不在模型,而在“更新节奏”

这是我碰见过最频繁的问题。模型结构照着论文抄、数据加载没问题,但loss曲线像锯齿一样来回弹,而且长时间没有下降趋势。有一次我检查了一天代码,最后发现罪魁祸首只是学习率设成了0.01,对那个模型来说实在太大了。

loss震荡不降,优先考虑几个方向。第一,学习率太高。梯度方向来回反转,自然会出现震荡现象,最简单的做法是把学习率除以10再试一次。第二,batch size太小。当每个batch只装8个或者16个样本时,梯度噪声太大,模型很难沿着稳定方向前进,可以试试把batch size调大。第三,数据本身没有做好标准化。不同特征的数值范围差异太大,会让梯度分布失衡,模型更新时有的参数动得太快、有的动得太慢。最直接的办法是对训练数据做归一化或标准化,让所有特征落在相似的数值尺度上。

还有一个小细节容易被忽略,就是随机种子不固定。同一个模型、同样的学习率,种子不同可能跑出来的loss曲线略有差异,但差异不至于非常夸张。如果跑出来曲线差异大到离谱,那基本可以确定是有数据泄露或者梯度更新逻辑写错了。

4.2 收敛很快但泛化差:警惕“优化成功”与“泛化失败”

另一种常见情况是训练集loss下降得很漂亮,但验证集指标一直上不去,典型的过拟合信号。很多人第一反应是加数据增强、加正则化,这些都对,但优化器这一侧也有值得调整的地方。

我之前在一个图像分类项目里,SGD与普通Adam对比,SGD收敛略慢,但最终测试集精度高了一个多点。这里面有一个大家经常讨论的现象:自适应优化器虽然能快速降低训练loss,但它倾向于收敛到参数空间里那些更尖锐的极值点,这些点对输入扰动和参数扰动都非常敏感,泛化性自然就差。而SGD加动量,轨迹更随机,反而更容易“冲”到比较平缓的极值区域。平缓极值在测试集上的表现通常更稳定。

所以,如果发现训练集收敛极快、但验证集一般,不妨先换掉自适应优化器,用带动量的SGD跑同样的epoch数,很多时候会有惊喜。如果确实需要保留AdamW带来的快速收敛,那可以通过更强的数据增强、更大的weight_decay来抑制过拟合,并配合EMA(指数移动平均)来平滑参数,也能在验证集上捞回一些精度。

4.3 训练中途NAN:一个老问题的几个新诱因

训练到某个epoch突然loss变成NaN,是让所有人都很崩溃的问题。但这类问题排查起来,方向通常比较明确。首先看学习率是否设置过大,这在零基础时最容易遇到。损失值一下子变成NaN,十有八九是数值溢出了,把学习率调小,问题往往立刻消失。

其次要看模型中是否出现了除零或者根号下负数之类的运算。Adam、RMSProp这类算法在分母上会加上一个epsilon,如果epsilon设置得太小,或者梯度平方和长时间为0,就可能出现数值问题。混合精度训练下,epsilon更敏感,调大到1e-6通常能解决。

还有就是输入数据本身带了NaN或无穷大。这个问题挺隐蔽的,尤其在处理文本或视频数据时,某个样本里漏了一个异常值,模型一碰到那个样本,梯度就爆炸。排查办法很简单,在数据加载器里加一个断言,检查mini-batch里有没有非有限值。要是发现“训练到固定epoch就NaN”,就很可能是某一条特定数据引起的。

4.4 一张排查速查表

整理一个排查清单,下次训练出异常可以先过一遍。

现象首要检查项次要检查项参考调整方向
loss完全不下降学习率是否过小输出层是否有nan调大学习率,检查梯度是否消失
loss震荡剧烈学习率是否过大batch size是否过小调小学习率,增大batch size
训练集收敛但验证集差优化器是否自适应过强是否缺少数据增强换SGD+Momentum,加大weight_decay
中途loss变成NaN学习率是否爆炸batch内是否有异常数据调小学习率,调大epsilon,清理数据
训练后期loss停住学习率是否过大是否缺少调度器接入Cosine退火,或手动调小学习率

5. 我的几点实操体会与建议

聊到这儿,关于深度学习优化算法的核心内容差不多讲完了,最后说几点我自己的主观体会。

第一点,优化器没有绝对意义上的“好坏”,只有“合不合适”。我在项目中见过太多人无脑上Adam,也见过有人因为某篇文章用了SGD就迷信SGD。更合理的做法是,针对你的数据规模和模型结构,至少对比一到两种优化器,用同样的训练轮数在验证集上做对比。这个对比实验的时间成本并不高,却能避免你在错误的优化器上浪费时间。

第二点,学习率和调度策略永远值得优先调。就算你选对了优化器,学习率不对依然是白搭。我个人的调参顺序是:先用范围测试确定合理学习率区间,再选定一个优化器配warmup+衰减策略,训练几个epoch看趋势,最后再根据验证集表现微调weight_decay和其他参数。这套流程下来,大部分模型都能在一个可控的时间内达到不错的水平。

第三点,也是我最想强调的一点,记录每一次实验的超参数配置。哪怕只是在本地起一个训练脚本,也建议顺手把优化器类型、学习率、调度器、batch size等写进实验日志里。很多时候模型效果好了,你却说不清是哪个参数起了作用,这种“不可复现的成功”对后续工作毫无价值。

我个人在实作中,几乎所有复杂模型最后都会回到“一个合理的基线设置 + 一个明确想验证的变量”这种实验思路。优化算法是很底层的配置,但它和后处理、损失函数、模型结构一样,都是左右最终结果的环节。切忌把优化器看作一个“填上默认值就不管”的选项,它是你调试模型的工具箱里最常用、也最值得了解的一把扳手。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询