深度学习优化实战:从梯度下降到Adam的学习率调参指南
2026/9/10 18:45:52 网站建设 项目流程

1. 这篇读书笔记到底在记什么

先说清楚这个编号的来历。我给自己定了一个规矩:读《Deep Learning》(就是那本深度学习领域的“花书”,Ian Goodfellow 等人写的教材)时,每整理一个专题的笔记,就用“书名缩写+章节范围+篇序号”来命名。deeplearningbook_040-2,意思是这本书第四部分相关内容的第二篇专题笔记,核心聚焦在深度学习模型训练环节里最容易翻车的部分——梯度下降、反向传播与优化算法。

你可能会问,深度学习头绪这么多,为什么偏偏先整理这一块?因为我发现一个很普遍的現象:很多初学者能把 CNN、RNN 的结构画得头头是道,PyTorch 的模型类也写得很顺,但一到训练环节就开始玄学调参——loss 不降就换学习率,换完不降就加层,加层不降就换模型,最后实在不行就上早停。整个过程没有任何章法,本质原因是没搞清楚优化这块的底层逻辑。

这篇笔记适合这么几类人看:

  • 正在啃花书,读到第四章和第八章(数值计算与优化)觉得公式太多、不知道怎么落地的读者;
  • 已经能跑通几个经典模型,但训练时一遇到 loss 震荡、收敛慢、梯度爆炸就束手无策的工程师;
  • 准备面试,需要把“从 SGD 到 Adam 再到学习率调度”这条链路讲清楚的求职者。

这篇笔记不打算复述书里所有公式,而是把花书里关于优化的关键结论拎出来,结合我在实际项目里的训练记录,讲清楚“为什么这么做”“不这么做会怎样”“出问题了怎么排查”。后面每一节都会有一个明确的核心论点,你可以把它当成一份带注释的读书卡片来用。

2. 梯度下降的底层逻辑与三个关键细节

2.1 梯度下降不是在“下山”,而是在“反推灯光方向”

花书第四章花了很大篇幅讲数值计算,其中最重要的概念就是梯度。书上给的定义很严谨:梯度是函数对各个自变量偏导数组成的向量,方向指向函数值增长最快的方向。很多教材喜欢用“雾天在山中下山”来类比——你看不清全貌,只能靠脚下坡度判断方向。这个类比没问题,但我想补充一个更工程化的理解方式:

梯度下降实际上是在做一个极其朴素的“反推”过程。模型预测错了,误差会通过损失函数变成一个数值,而这个数值对每一个参数的偏导数,就是在告诉我们:如果把某个权重调大一点点,误差会增加还是减少、增加或减少的幅度是多少。算法做的事情就是朝“减少误差”的方向迈一步,然后重复。

为什么强调这个理解方式?因为它能帮你解释很多实务中的现象。比如 loss 曲线在训练初期下降很快、后来变慢,不是因为“模型快收敛了”,而是因为误差函数在最优解附近通常比较平缓,梯度幅度变小,同样的学习率迈出的实际步长也变小了。你看到的变化背后,是梯度的空间分布规律,不是某个神秘的收敛状态。

从花书里可以提炼出一个实用结论:梯度是局部信息,它只告诉你当前位置最陡的方向,不告诉你全局最优在哪。所有基于梯度的优化算法,包括之后要讲的动量、Adam,本质上都是在用各种技巧缓解“只看局部”带来的问题。

2.2 学习率才是真正需要反复调的“超参数之王”

在花书第八章,作者明确给出了一个观点:学习率是训练深度网络时最重要的超参数之一。这一点我在实际项目中举双手赞成——如果你只能调一个参数,那就调学习率;如果你只能做一件事来改善训练,那就是给学习率设计一个合理的调度策略。

学习率设置不当的后果,通常表现为两种:

  • 学习率过大:loss 在训练初期不降反升,或者剧烈震荡,甚至直接变成 NaN。原因是参数更新步长过大,跳过了损失函数的低谷区域,跑到了梯度更大、更不稳定的地方。
  • 学习率过小:loss 下降极慢,训练了几个 epoch 还停留在同一个量级。用 TensorBoard 看梯度分布,会发现参数更新的幅度非常小,模型基本在原地踏步。

我在实际训练中遇到过最典型的一次事故:用 0.1 的学习率训练一个图像分类模型,前三轮 loss 从 2.3 降到 0.8,看起来一切正常,第四轮开始时 loss 突然变成 3.9,然后一路飙升到 7.2,紧接着出现 NaN。当时第一反应是数据有问题,检查了数据加载、标签对齐、归一化,全部正常。最后把学习率降到 0.01,从头训练,问题消失。

那次排查耗费了整整一天,而根因只是一个学习率设置。从那以后我养成一个习惯:新模型第一次训练,一律从 0.001 或 0.0001 起步,跑两三个 epoch 观察 loss 的下降斜率,再决定要不要把学习率调大。花书里建议的“在训练初期做几次学习率扫描测试”,就是这个思路的规范化操作——先用几个相差 10 倍的学习率各跑一小段,看 loss 的下降曲线,选一个下降最快且不震荡的区间,再正式训练。

2.3 损失曲面不是碗,而是“皱巴巴的床单”

读花书第四章时,有一句话让我印象很深:在高维空间中,我们很难直观想象损失函数的形状。很多教程画的损失曲面是一个漂亮的碗,最优点在碗底,这个图对理解梯度下降有帮助,但它会严重误导你对实际问题的判断。

真实的高维损失函数,更接近一张被揉皱的床单——有大量局部低谷、鞍点、平台区域。特别要注意“鞍点”,它是某些方向上是极小值、另一些方向上不是极值的点。在高维空间里,鞍点比局部最优更常见,因为要让一个点在所有方向上都高于邻域,条件太苛刻了,而在部分方向上高于邻域则相对容易。

梯度下降到鞍点附近时,梯度幅度很小,训练看起来像停住了。这时候很多人会误以为“模型已经收敛到最优”,其实只是被困在了一个梯度接近零但并不是最优的位置。这也是为什么后面要引入动量法——它有“冲量”,能从鞍点区域冲出去。

花书给的一个关键建议是:不要试图用一阶方法精确逼近全局最优,深度学习的目标本来就是找到一个“足够好”的解,让训练误差和测试误差都达到可接受的水平。理解这一点,你调参的心态会稳很多。

3. 反向传播:计算图与链式法则,以及一次手推验证

3.1 为什么反向传播是整个深度学习训练的发动机

花书第六章有一节专门讲反向传播,核心思想是:通过计算图把前向传播的每一步拆成基本运算,然后利用链式法则,从输出端开始逐层反推每个参数的梯度。

我最初的困惑是:既然每个参数对误差的偏导可以单独算,为什么非要用反向传播这种看起来“绕路”的方法?后来算了一笔账才明白:如果有 1000 万个参数,正向方式计算梯度需要对每个参数都做一次完整的前向误差计算,效率极低;反向传播只需要一次前向传播和一次反向传播,就能把所有权重的梯度全部算出来。计算量从“参数数量乘以单次前向开销”降到了“一次前向加一次后向”,这个差距在深度网络里是数量级的。

一次反向传播,本质上是一套高效的梯度复用机制——先算出输出层误差,再把这个误差逐层传回去,每一层只需要做一个局部链式求导,就能得到该层所有参数的梯度。这个设计是深度学习能够工程化的基石,也解释了为什么现代框架都要把模型建模成计算图。

3.2 手推一个两层网络的链式求导过程

光看理论容易飘,我建议每个想真正掌握反向传播的人,都动手推一遍最简单的两层网络。下面是我在笔记里存的推导过程,用一个不含偏置项的线性层加 Sigmoid 激活函数做例子。

假设网络结构是:

  • 输入 x(一个标量)
  • 第一层权重 w1,输出 a1 = w1 * x
  • 经过 Sigmoid 激活,得到 h = sigmoid(a1)
  • 第二层权重 w2,输出 a2 = w2 * h
  • 损失采用均方误差,y 是真实标签,L = (a2 - y)^2

对 w2 求梯度:

∂L/∂w2 = ∂L/∂a2 * ∂a2/∂w2 = 2(a2 - y) * h

对 w1 求梯度:

∂L/∂w1 = ∂L/∂a2 * ∂a2/∂h * ∂h/∂a1 * ∂a1/∂w1

= 2(a2 - y) * w2 * sigmoid'(a1) * x

其中 sigmoid'(a1) = sigmoid(a1) * (1 - sigmoid(a1)) = h * (1 - h)。

观察这个式子,你会发现一个关键信息:w1 的梯度里包含了 w2、包含了激活函数的导数、包含了输入 x,所有误差信息都是从输出层一级一级传回来的。如果你用代码把每一步的值打印出来,和手算结果对比一致,反向传播这块就算真正过关了。

我在笔记里还留了个提醒:计算时务必要注意每一步乘的是“前向传播的中间变量”,不是重新计算的变量。比如上式的 h 必须是前向传播时 sigmoid(w1*x) 的输出,不能在前面把 h 改了再回过头来求梯度。这是初学实现时特别容易踩的坑。

3.3 框架自动求导背后的原理与限制

现在我们写 PyTorch 或 TensorFlow,一行 loss.backward() 就能拿到梯度,很多人会忽略框架在背后做了什么。花书对计算图的一大贡献,是明确区分了“符号形式求导”和“数值形式求导”,现代框架用的是前者——先构建符号计算图,再代入具体数值执行链式法则。

理解这一点,对你排查 bug 很有帮助。如果某个操作本身不可导,或者你在计算图中引入了非法的操作(比如原地修改参与求导的 Tensor),框架通常会报错或给出警告。这时候不要只看报错信息,要回看计算图是从哪里断开的。

另外一个容易被忽视的限制是:反向传播保存的前向传播中间变量会占用大量显存。这也是为什么同样的模型,训练模式和推理模式的内存占用差异巨大。理解了这一层,你就能明白为什么会出现“OOM 只在训练时发生”的现象——不是模型结构错了,是反向传播需要把中间结果留在内存里供求导使用。

4. 优化算法演进:从 SGD 到动量再到 Adam,我为什么最终换掉了纯 Adam

4.1 SGD 的困境:方向抖动与收敛慢

花书第八章详细对比了各种优化算法,起点是随机梯度下降,也就是 SGD。SGD 的问题在训练时非常直观:每一个 batch 算出的梯度都是整体梯度的一个带噪声的估计,所以参数更新方向会在最优方向附近来回摆动,体现在 loss 曲线上就是震荡明显、收敛缓慢。

有一个优化的思路是把 batch 变大,让梯度估计更准确。但 batch 太大会带来新问题:内存不够、泛化性能可能下降。另一条路是设计更好的优化算法。花书将这类算法统一称为“带自适应学习率的优化方法”,核心区别就在于它们如何利用历史梯度信息来修正当前更新方向。

4.2 动量法:给小球一个“初速度”

动量法(Momentum)是我在实际项目里非常喜欢的一个改动,因为它的物理解释太直观了:想象一个小球从山坡上滚下来,它的运动方向是历史速度和新坡度共同决定的。如果持续朝一个方向走,速度会叠加,越过小的坑洼和平台;如果方向反复变化,速度会相互抵消,更新趋于平稳。

用公式表达就是:

速度 = 动量系数 * 上一步速度 + 学习率 * 当前梯度

参数 = 参数 - 速度

动量系数的常见取值是 0.9,也就是保留 90% 的历史速度,再叠加当前梯度方向的影响。

我在训练一个文本分类模型时做过对比:同样的学习率、同样的初始化,SGD 训练了 15 个 epoch 收敛到 87.2% 的准确率,加了动量后 10 个 epoch 就达到了 87.5%。虽然只是零点几个百分点的提升,但收敛速度明显加快。动量法最大的价值不只是提点,而是让你的训练过程更稳。

4.3 Adam 的“自适应”是怎么来的

Adam(Adaptive Moment Estimation)可以说是目前使用最广泛的优化器,花书也给了较高评价。它的思路是把动量和 RMSProp 的自适应学习率结合起来:既维护历史梯度的指数加权平均(作为动量),又维护历史梯度平方的指数加权平均(用于缩放每个参数的学习率)。

这样一来,每个参数都有自己“独立”的更新步长。频繁更新的参数,因为历史梯度平方大,学习率会被缩小;很少更新的参数,学习率会相对放大。这在处理稀疏特征时非常有用,也是 Adam 在 NLP、推荐系统等领域表现优异的重要原因。

但 Adam 并非万能。花书和我在实践中都注意到,Adam 的泛化性能在部分任务上不如调好学习率的 SGD 加动量。一个常见解释是,Adam 的自适应机制让参数更新步子太“聪明”了,反而在损失曲面比较平滑、需要精细收敛的时候,不如 SGD 靠“惯性”能冲到更平缓的最优点。

我现在的经验法则是:

  • 数据量大、特征稠密、任务比较标准(比如图像分类):先用 Adam 快速跑通,再换 SGD+动量调优;
  • 稀疏特征、推荐系统、NLP 里的 embedding 层:优先 Adam 或 AdamW;
  • 模型训练后期想逼近更优解:可以切到 SGD,或者用带余弦退火的学习率调度器搭配 Adam。

4.4 学习率调度不是一个可有可无的附属品

花书里有一句被我划了重点的话:学习率调度是训练现代深度网络的关键组成部分。这句话翻译成工程语言就是:不要在一个训练流程里写死学习率。

我常用的三种调度思路,按启动成本从低到高排列:

  • 步长衰减(Step Decay):每训练 N 个 epoch,学习率乘以一个小于 1 的系数(比如 0.1)。简单粗暴,但需要人工确认衰减点和衰减幅度。
  • 余弦退火(Cosine Annealing):学习率按照余弦曲线从初始值逐步降到接近 0。不需要人工设置衰减点,PyTorch 的 CosineAnnealingLR 直接支持。
  • 带热身的调度(Warmup):训练初期先用很小的学习率逐步升温到目标值,再开始正常衰减。这在 transformer 类模型和超大 batch 训练中几乎是标配。

为什么 warmup 在 transformer 里那么重要?原因是注意力机制在初始阶段梯度非常不稳定,如果一开始就用大学习率,模型参数会被推到不好的区域,后面很难回来。用小学习率先让模型“稳住”,再逐步加速,可以避免这个风险。花书没有直接用 warmup 这个词,但它讨论的“在训练初期使用较小学习率有助于稳定训练”的思路是完全一致的。

5. 实操记录:一次模型训练从翻车到收敛的完整复盘

5.1 问题现象:loss 连续三轮不降反升

几个月前我在训练一个多标签文本分类模型,模型本身不复杂:一个预训练的 embedding 层接到两层 BiLSTM 上,最后接全连接层输出。优化器选的 Adam,学习率 0.001,batch size 32。

前两个 epoch,loss 从 1.8 下降到 1.2,还算正常。第三个 epoch 开始,loss 不降反升,到第五个 epoch 变成了 2.4,比起点还高。用 TensorBoard 看了一下训练集和验证集的曲线,训练集也在涨,所以不是过拟合,是训练过程本身出了问题。

我按顺序排查了三类可能:数据问题、梯度问题、学习率问题。

5.2 排查过程与最终定位

第一步检查数据。我随机抽取了一部分训练样本,打印标签和文本,确认没有 label 错位;检查了损失函数里的 mask 逻辑,确认 padding 位置没有参与 loss 计算。数据层面没有明显问题。

第二步看梯度。我把模型每一层的梯度范数打印出来,发现 embedding 层的梯度范数在训练开始后迅速增大,其他层相对稳定。之前提到过,梯度范数暴涨往往是学习率过大的信号。我用 torch.nn.utils.clip_grad_norm_ 把全局梯度裁剪到 5.0,训练能跑下去,但 loss 依然不降。

第三步回到学习率。我把初始学习率从 0.001 降到 0.0003,同时加了线性 warmup,前 500 步从 0.00003 逐步升到 0.0003。结果从第一个 epoch 开始,loss 就稳步下降,最终收敛到 0.62,比之前任何一次实验都要好。

回看原因,应该是这批数据的标签分布很不均衡,模型在初始阶段就产生了较大的梯度,0.001 的学习率在这个场景下显得太大了。学习率降低后模型没有进入“震荡区”,训练自然就稳了。

5.3 有效排查梯度问题的三个实用技巧

基于这次经历,我总结了一套排查梯度相关问题的流程,分享出来供你参考:

  • 技巧一:每次新任务第一次训练,先跑 5 到 10 个 batch,打印 loss 和每个参数层的梯度范数。如果某个层的梯度范数在几个 batch 内暴涨,优先考虑降低学习率或者给该层单独设更小的学习率。
  • 技巧二:在模型里加梯度裁剪(gradient clipping)作为兜底手段。不仅在 RNN 里需要,长序列 transformer、GAN 训练、多任务模型里同样有用。常见的裁剪阈值在 1.0 到 5.0 之间,具体数值需要试。
  • 技巧三:把 loss 曲线和梯度范数曲线放在同一个画布里观察。Loss 不降不一定有问题,梯度范数正常下降说明优化器在正常推进;Loss 不降且梯度范数一直在高值徘徊,才是需要干预的信号。

我建议所有人在正式训练前,都花 30 分钟把这三条流程跑一遍。因为“训练翻车”的成本永远比“预防翻车”的成本高得多。

5.4 关于 Adam 与 SGD 的实测数据对比

顺手把我之前在同一组实验里记录的数据放出来,数据集是公开的 AG News 文本分类子集,模型统一用两层 BiLSTM+Attention,超参数完全一致,只改优化器和学习率调度。

优化器配置学习率收敛到 90% 准确率所需 epoch最终准确率
SGD + Momentum(0.9)0.019.591.2%
SGD + Momentum(0.9)0.001未收敛到 90%88.6%
Adam0.0017.290.4%
Adam + CosineAnnealing0.0016.891.0%

从这个测试里能看到两个结论,和我前面讲的理论完全对应:

  • Adam 在同样学习率下收敛更快,但最终精度不一定高于 SGD;
  • 给 Adam 配上余弦退火调度后,收敛速度和最终精度都有改善。这说明“好的优化器也要配好的调度策略”,两者不是替代关系,是互补关系。

这个对比实验我建议你也做一次,选一个自己熟悉的数据集和模型。因为只有用自己手里的数据去验证,你才会真正理解花书里那些曲线图到底在说什么。

6. 常见问题实录:训练 loss 无法下降的六种典型场景

6.1 从“loss 不降”到“loss 变 NaN”的完整现象表

训练 deep learning 模型,最怕的不是模型复杂、训练慢,而是 loss 表现异常却找不到原因。我把近几年遇到过的问题整理成了下面这个速查表,放在读书笔记的最后,方便随时翻:

现象特征可能原因验证方法解决方向
loss 一直在高位不下降学习率太小或特征未归一化打印梯度范数,看是否过小调大学习率,检查输入数据归一化
loss 前几轮下降后不降了陷入鞍点或局部低谷看梯度范数是否接近零用动量法或 Adam,或重启训练
loss 震荡剧烈学习率过大或 batch 太小缩小学习率看是否缓解调低学习率,或增大 batch size
loss 突然变成 NaN梯度爆炸或数据含 NaN打印各层梯度范数梯度裁剪,降低学习率,检查数据
训练集 loss 正常,验证集 loss 升高过拟合比对训练集和验证集曲线加正则化、Dropout,早停
loss 一直下降但准确率不升标签错位或损失函数写错抽样检查标签和预测输出检查数据处理、损失函数逻辑

这张表是我压箱底的经验汇总,每次新项目遇到问题,我都会先对照一遍,能省下很多盲目调整的时间。

6.2 典型场景一:loss 打印出来是负数

损失函数是负数的情况,通常不是因为“训练得很好”,而是损失函数本身写错了。一个常见原因是目标函数里带了对数,而输入给对数的值是经过某些变换后的概率,当概率非法或极小,对数输出会变成负的无穷大。另一个常见原因是自定义损失时用了错误的符号,比如把“最大化相似度”写成了“最小化相似度”,loss 就会一路下探到负值。

遇到这种情况,我的建议是先打印 loss 的每一项分解,逐项检查符号和取值范围。千万别觉得“loss 是负的,那我的模型一定学到了很厉害的东西”——这多半是 bug,不是惊喜。

6.3 典型场景二:增加模型深度后 loss 反而更高

加了更多层之后,训练效果不升反降,这是深度学习里一个经典现象。花书里解释了退化问题:深层网络在优化上并不天然优于浅层网络。细节原因包括梯度消失、残差连接设计不合理、初始化不匹配等。

我遇到过的真实案例:把一个两层的 MLP 扩展成十层的 MLP,结果在同一个数据集上准确率反而掉了 3 个百分点。排查后发现问题出在初始化——深层网络对初始化范围更敏感,如果初始权重过大,训练初期梯度就不稳定。改成 He 初始化之后,十层 MLP 的准确率才超过两层版本。

这类现象特别容易让初学者产生“深度学习靠玄学”的错觉。其实不是玄学,每一步都有据可循:加层之后,要么加残差连接,要么调整初始化方案,要么加 Batch Normalization,总有一项能解决问题。

6.4 典型场景三:加了正则化后 loss 不降

Dropout、权重衰减这些正则化手段,在训练初期会让 loss 比不加正则化时更高,这是一件正常的事情,没必要慌。正则化的本质是牺牲一部分训练集拟合能力,换取泛化能力提升。只要训练的曲线整体趋势是下降的,最终验证集指标比不加正则化时更好,就说明正则化在工作。

需要担心的是“加了正则化后,训练 loss 在某一步骤突然不再下降,验证集指标也不见好”,这种情况通常不是正则化的锅,而是学习率调度出了问题。比如学习率被调度得太快降到零,模型根本没有机会继续学习。检查一下学习率曲线的形状,多半能找到原因。

7. 写在笔记末尾的一点体会

花书第八章节的标题是“Optimization for Training Deep Models”,整章读完,最大的收获不是记住了多少个优化算法的公式,而是建立了“训练过程是可以被拆解和诊断的”这个信念。

现在每当我遇到训练不收敛的问题,第一反应不是换模型,而是踏踏实实检查三件事:数据有没有问题、梯度有没有异常、学习率设置是否合理。这三步能解决我遇到过的大概八成问题。

如果你也想真正吃透深度学习优化这部分内容,我的建议是:不要只看书,一定要亲手在代码里做几组对比实验。拿同一个模型,分别用 SGD、Momentum、Adam 跑一遍,记录 loss 曲线;再固定优化器,调几次学习率,看曲线的变化规律。不用多,每个配置跑三五个 epoch,你就能获得直观看感。这个过程比抱着书啃十遍公式更有用。

等这些基础实验做熟了,再回去翻花书的第八章,你会发现那些公式不再是抽象符号,而是对“你亲手观察到的现象”的精确描述。理论与实践,就这么对上了。

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

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

立即咨询