news 2026/10/22 11:00:00

梯度下降原理与优化器演进:从学习率到Adam的实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
梯度下降原理与优化器演进:从学习率到Adam的实战指南

梯度下降这节,是整个《机器学习》系列里我反复回看最多的一节。和前面几篇讲回归、讲误差来源不同,这一篇把视角拉到训练的最底层——模型到底是怎么一步步逼近最优解的。李宏毅老师的原课里有一句容易被忽略但分量很重的话:深度学习整个训练过程,本质上就是在找一个能让损失函数最小的参数组合。飞桨特训营第三篇对应这段内容时,把重点落在两个地方:一是把梯度下降的数学直觉讲透,二是真的打开代码跑起来,亲眼看着 loss 一圈一圈往下降。这篇文章就按我自己过这节课的顺序,把这个过程完整拆开讲一遍。

这篇东西适合两类人:一类是刚接触深度学习、对梯度下降只有"下山"这个概念、想弄清楚参数更新每一步是怎么回事的新手;另一类是已经会调库训练模型、但遇到 loss 不降或收敛不稳时不知道怎么定位问题的人。我会把课程里涉及的核心公式、优化器演进逻辑、以及用飞桨手写梯度下降的完整代码都放进来,尽量做到看完就能在自己的环境里复现。

1. 梯度下降到底在做一件什么事

1.1 从损失函数到参数更新

先回到底层逻辑。模型训练的所有痛苦,都来自一个核心矛盾:参数空间太大,不可能暴力搜索。一个稍微像样点的神经网络,参数量动不动就是几十万上百万,靠遍历去找最优参数是天方夜谭。所以祖师爷们才想出了"沿着坡度往下滚"的思路——既然没法一下子找到最低点,那就每一步都往下降最快的方向走,总能接近一个不错的落点。

这里的"坡度"就是梯度。数学定义上,损失函数 L(θ) 对参数 θ 的梯度是个向量,指向的是 L 增大最快的方向。我们要最小化损失,就得反着来,让参数朝负梯度方向挪动。于是有了最核心的更新公式:

θ ← θ - η · ∇L(θ)

其中 η 是学习率(learning rate),它决定每一步迈多大。∇L(θ) 就是损失对参数的梯度,在反向传播里通过链式法则一层层算出来。

这个公式看起来简单到不值一提,但整套深度学习的训练技巧几乎都围绕它展开。我在实际跑实验时的感受是:如果你只记住一个东西,就记这个更新公式;如果你只能调一个超参,就调学习率。绝大多数训练失败,根源都在学习率和梯度之间的关系没处理好。

1.2 下山的故事:为什么偏要往反方向走

李宏毅老师在课里用过一个很直观的比喻:一个人站在山腰,蒙着眼要往谷底走,靠的就是脚底感受哪边更陡,然后朝陡的方向迈一步。梯度就是那个"陡的方向",但梯度方向是上坡方向,所以要取负号。

这个比喻很形象,但它有个容易误导人的地方:山是二维地形,而参数空间往往是几千维的。在几百维空间里,"坡度"不再是某个单一方向的感觉,而是一个维度上各自都有分量。梯度下降做的事情,是同时计算每个参数方向上的偏导数,然后对每个参数分别更新。

课程里特意强调了一点:更新参数时,梯度本身也是一个向量,它的方向和大小会随着位置变化。最开始你离最低点远,梯度大,步子自然迈得大;靠近最低点时梯度变小,步子就变小。这个特性天然让梯度下降有了点"自适应"的味道。但问题在于,步子大小还乘以一个固定的学习率 η,如果 η 设得不对,要么永远走不到底,要么一步跨过谷底在两边反复横跳。

2. 学习率:最容易被忽略的生死线

2.1 学习率太小和太大的后果

学到这里就绕不开学习率了。课程里用同一个损失函数,分别设了三种学习率,画出来的 loss 曲线差异极大。我第一次看那个对比图印象很深:学习率设 0.01 的时候,loss 一路平稳下降,最后收敛到很小的值;设 0.001 时下降变得很缓慢,训练了很久也没到最优位置;设 0.1 时 loss 曲线像心电图一样剧烈震荡,甚至直接发散成 NaN。

现在的实际项目中,学习率设太大会怎样?不只是 loss 震荡这么简单。在深层网络里,学习率太大可能让梯度更新超出正常范围,引发的连锁反应包括:中间层的激活值饱和、梯度消失或爆炸、某些权重变成 NaN。我踩过最痛的坑是在一个图像分类模型里,learning rate 设成 0.1 跑了几百个 step,loss 变成 NaN 还不自知,白等了半小时。后来才明白,batch size 和数据分布都会影响合适的学习率范围,最好的做法是先用小学习率(比如 0.001)跑通,再逐步调大。

学习率表现排查建议
过小收敛缓慢,loss 下降像蜗牛看曲线斜率,斜率平缓时可尝试增大 10 倍
合适loss 平滑下降并稳定收敛保持,或用余弦退火逐步降低
过大loss 震荡、发散、出现 NaN减小 10 倍或 100 倍,重新观察

2.2 课程里那组经典对比实验

我记得课程里的做法是挑了一个比较简单的损失函数,人工设几个不同的学习率,然后把训练过程和收敛点画在同一张图上。几种学习率跑出来的路径完全是天壤之别:有的平滑抵达最低点,有的走之字形绕来绕去,有的干脆冲出了图的范围。

这种"亲眼看到"的冲击力,比干讲公式大得多。我在自己复现时也照做了一次,发现两个细节是课程里不太会专门提的:第一,同一学习率在不同初始化下效果差别很大,和随机种子强相关;第二,走之字形的那条路径,其实是在某些特征方向上更新过度,另一些方向上更新不足。后者正是后续自适应学习率算法要解决的核心问题。

所以看完这个实验,不要只是感慨"学习率好重要",更重要的是理解:一个全局固定的学习率,很难适配所有参数的更新需求。特征尺度的差异、梯度量级的差异,都要求学习率能随参数自适应变化。

2.3 为什么AdaGrad能缓解学习率难题

AdaGrad 的思路很朴素:每个参数拥有自己的学习率,而这个"学习率"会除以该参数历史梯度的均方根。也就是说,梯度大的参数,历史累加值大,学习率自动变小;梯度小的参数,学习率相对更大。它实现了"陡峭的地方小步走,平缓的地方大步走"的效果。

课程里还给了一个生动例子:将参数更新看作一个人在峡谷中行走,跨一个方向很陡,另一个方向很缓,固定学习率会导致人反复撞墙。AdaGrad 相当于给那个陡峭方向穿上模具,限制它乱冲,效果立竿见影。但 AdaGrad 有个后续问题:梯度平方累加后,学习率单调递减,训练后期步长变得极小,几乎动不了。所以后来才有 RMSProp 加入指数衰减平均,取代了这种无脑累加。不过你只要记住,这一整个系列的自适应方法,出发点都是"不同参数该有不同的步长"。

3. 从SGD到Momentum再到Adam

3.1 小批量随机梯度下降的代价

原始梯度下降每次更新都要在全部样本上计算梯度,数据集万级还好,百万级就根本算不动。于是工程上普遍使用小批量随机梯度下降(Mini-batch SGD),每次随机抽一小批样本算梯度,近似看作全局梯度。这种方法让每次更新的计算量大幅下降,还带了一些随机性。

但随机性有好也有坏。课程里专门讲过:小批量估算的梯度会有噪声,导致更新方向不精准,loss 曲线在下降过程中会出现抖动。尤其在局部极小值附近,噪声可能让参数眼前"看似在下坡",但长远看是被困住了。另外一个实际问题是,SGD 更新路径对学习率非常敏感,在病态曲率(比如峡谷地形)下收敛极慢。我跑训练的时候经常遇到一种情况:loss 从 2.3 降到 1.5 很顺利,再往下就像进入粘滞区,怎么都不动了。后来一查,就是SGD在平坦区域梯度接近零,走不动。

3.2 动量如何走出局部极小

动量(Momentum)是一个值得反复琢磨的改动。它的更新不再只看当前梯度,而是维护一个速度项:

v ← β·v + η·∇L(θ) θ ← θ - v

这里 β 通常取 0.9,含义是"保留 90% 的历史速度,只并入 10% 的新梯度信息"。我理解它就像滚雪球:物体有惯性,即使当前梯度为零,它也会因为之前的动量继续前冲,不会当场停下。这种惯性让它能穿过某些局部极小的"浅坑",比普通 SGD 有更好的逃逸能力。

实际用下来,动量还有一个容易被忽视的好处:它有效平滑了随机梯度的方向噪声,收敛路径稳定不少。早期我在训练小模型时,用普通 SGD 的 loss 曲线高低起伏像心电图,换成带动量的 SGD 后整个曲线立刻平滑了一个量级。那时候我才真正意识到,优化器带来的改进不是玄学,而是数学结构上的必然。

3.3 Adam为什么成了现在的默认选择

Adam 全称 Adaptive Moment Estimation,核心是同时维护一阶动量(类似 Momentum 的速度)和二阶动量(类似 RMSProp 的梯度平方平均)。前者管方向惯性,后者管每维步长放缩。两者结合后,Adam 对学习率和初始化不敏感,适合在大多数场景下"开箱即用"。

李宏毅老师的课里没有把 Adam 当作重点大讲特讲,但飞桨特训营在实操演示里是用 Adam 当默认优化器用的。我的个人使用体验是:Adam 确实省心,尤其在复现论文或者快速验证想法时,先用 Adam 把模型跑通,再换成 SGD 调优,是很多团队惯用的套路。注意有个细节:Adam 对学习率的推荐值通常是 0.001,但如果你同时在用 warmup,学习率可以从 0 逐渐涨到 0.001,训练会更稳。另外,如果要追求极限精度,一些实证研究反而认为SGD with Momentum 在后期比 Adam 容易泛化得更好,这点在课程里虽然没展开,但是值得留意。

4. 用飞桨手写梯度下降与优化器对比

4.1 准备数据和损失函数

光讲原理不写代码,总觉得隔了一层。飞桨(PaddlePaddle)的好处是既有封装好的优化器,也不阻止你手写梯度计算过程,非常适合用来验证刚刚讲的那些思想。我做了一个最小的实验:用一条带噪声的直线数据 y = 2x + 1 + noise,训练一个单层线性模型去拟合它。

首先定义数据和模型:

import paddle import paddle.nn as nn # 构造数据:100 个点,符合 y = 2x + 1 并加一点噪声 x = paddle.linspace(-1., 1., 100).reshape([-1, 1]) true_w = paddle.to_tensor([[2.0]]) true_b = paddle.to_tensor([[1.0]]) noise = paddle.normal(0, 0.1, shape=[100, 1]) y = x @ true_w + true_b + noise # 定义单层线性模型(等价于 y = w*x + b) model = nn.Linear(1, 1)

损失函数选择均方误差(MSE)。它的表达式是 1/n Σ (y_pred - y_true)²,对参数的梯度推导很干净,适合拿来做手工梯度验证。

4.2 手写梯度更新循环

下一步就不用飞桨的优化器,而是自己写梯度下降循环,完整走一遍前面公式里的流程:

# 手写梯度下降 lr = 0.01 epochs = 200 # 取出模型参数并创建 requires_grad=True 的副本(实际情况直接使用模型参数) for epoch in range(epochs): # 前向计算 y_pred = model(x) loss = nn.functional.mse_loss(y_pred, y) # 反向传播:梯度会被写入每个参数的 .grad 中 loss.backward() # 手动更新参数 with paddle.no_grad(): for param in model.parameters(): param -= lr * param.grad param.clear_grad() if epoch % 20 == 0: print(f"epoch {epoch}, loss: {loss.item():.6f}")

这段代码最值得看的是loss.backward()后面的内容。飞桨会把每个参数的梯度存到param.grad里,你手动做param -= lr * param.grad就是教科书里那个更新公式。跑完后把 200 个 epoch 的 loss 打出来,曲线会一路向下,最后稳定在接近噪声方差的水平。我尝试把 lr 改成 0.002,loss 下降明显变慢;改成 0.05,loss 开始震荡——亲手验证之后,才算真正吃了一颗定心丸。

注意:手动更新时要加paddle.no_grad(),否则参数更新操作也会被记录进计算图,导致梯度污染和显存泄露。这是手写训练循环时最容易踩的坑。

4.3 改用飞桨优化器对比效果

飞桨已经内置了常用优化器,下面是同一模型在不同优化器下的表现对比:

import paddle def train_with_optimizer(opt_name, lr=0.01, epochs=200): model = nn.Linear(1, 1) if opt_name == "sgd": optimizer = paddle.optimizer.SGD(learning_rate=lr, parameters=model.parameters()) elif opt_name == "momentum": optimizer = paddle.optimizer.Momentum(learning_rate=lr, momentum=0.9, parameters=model.parameters()) elif opt_name == "adam": optimizer = paddle.optimizer.Adam(learning_rate=lr, parameters=model.parameters()) for epoch in range(epochs): y_pred = model(x) loss = nn.functional.mse_loss(y_pred, y) loss.backward() optimizer.step() optimizer.clear_grad() return loss.item() for name in ["sgd", "momentum", "adam"]: final_loss = train_with_optimizer(name) print(f"{name} final loss: {final_loss:.6f}")

实测下来,学习率设为 0.01 时,三种优化器都能收敛;把学习率调到 0.1,SGD 开始不稳,Momentum 勉强可以,Adam 仍然稳定。如果把学习率调到 0.01 但把数据改成更崎岖的误差面,SGD 和 Momentum 的差距会更明显。这就是课程里那些公式的直观验证。

更值得做的一个实验是:记录每个 epoch 的 loss 并画在同一张图上,观察差异。我的经验是,SGD 的曲线像"一条颤抖的线滑向低处",Momentum 是"平滑许多的滑降",Adam 则是"快速落到底后基本不再有大动作"。这种视觉差异,比任何文字描述都更有说服力。

5. 训练中常见的收敛问题与排查思路

5.1 损失不降怎么办

真到了实际项目里,最让人头大的是:模型训练了很多个 epoch,loss 就是停在原地不动。按我排查的经验,应该按顺序检查四件事:学习率是否太小(先试着增大 10 倍);数据是否归一化(特征尺度差异过大会让梯度失衡);梯度是否消失了(打印每个参数的 grad norm);模型是否太浅或太小(容量不足时 loss 会降到一定值后无法继续下探)。

课程里专门有个片段提到:loss 不下降不一定是优化器的问题,也可能是模型一开始就没把特征表达出来,梯度反传回去已经衰减到感觉不到,这就是"梯度消失"。排查最简单的方式是在训练循环里加一行打印:

grad_norm = sum([p.grad.norm().item() ** 2 for p in model.parameters()]) ** 0.5 if epoch % 20 == 0: print(f"grad norm: {grad_norm:.6f}")

如果 grad norm 已经小到 1e-8 级别,可以考虑换初始化方式、加深网络但加残差连接,或改用 Adam 这种能为小梯度放大步长的算法。

5.2 梯度消失和梯度爆炸的预警

梯度爆炸在大模型里更常见,尤其在序列模型或者深层网络中。它的表现是 loss 突然变成 NaN,或者参数更新出现离谱的跳变。针对这个问题,有两个实用技巧相当推荐:梯度裁剪(gradient clipping)和更温和的初始化。飞桨里梯度裁剪可以这样写:

clip = paddle.nn.ClipGradByNorm(clip_norm=1.0) optimizer = paddle.optimizer.Adam( learning_rate=0.001, parameters=model.parameters(), grad_clip=clip, )

clip_norm 该怎么定?我的一般做法是先跑 100 个 step,打印未经裁剪的 grad norm 分布,取最大值的 0.1 倍到 1 倍之间作为裁剪阈值。如果裁剪阈值设太大就起不到保护作用,设太小则会把有效梯度也给削掉。

5.3 局部最小值与鞍点的辨认

课程里还重点讲过一个概念:在高维空间中,找到真正的局部极小值其实比想象中难,大多数情况下你遇到的不是低谷,而是鞍点。鞍点在某些方向上梯度为零但不上升,在另一些方向上还可能下降,所以参数会停留在那里很久,表现为 loss 长时间卡住不动。

怎么判断是不是卡在鞍点?一个简单办法是:看当前梯度范数是不是接近 0,但 Hessian 对角元有正有负。手算 Hessian 不现实,实践中我更喜欢换优化器跑一跑。比如用 Adam 时如果还卡住,就切到带动量的 SGD,或者反过来,通常能打破僵局。另一种暴力但有效的方式:把学习率先临时调大跑几十个 step,再调回原值,这相当于用"助推"跳出鞍点和浅谷。

这些经验,课程里不一定每条都写明,但它们都是基于这个梯度下降原理延伸出来的通用手段。理解了优化过程本身,你就有了诊断训练问题的底气。

最后再分享一个我自己的习惯。每开一个新模型实验,我第一件事不是去调什么结构、加什么正则,而是先打一个小模型配上学习率 0.001 和 Adam,跑几十个 step,检查 loss 能不能往下降、grad norm 是不是在合理范围(通常 1e-6 到 1 之间)。这个"冒烟测试"成本极低,却能筛掉一大批初始化、数据预处理和网络实现上的低级错误。等这套机制确认没问题了,再回头去研究要不要换优化器、要不要调学习率策略。

用飞桨做这些实验有个额外好处:它的动态图机制和 Python 原生语法很贴近,打印梯度、观察中间张量都很顺手,非常适合把课程里的原理逐行验证一遍。如果你也在学这一节的梯度下降,建议别只看视频,打开 Jupyter 亲手跑一遍,那种"哦,原来如此"的感觉,是看多少遍讲义都换不来的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 10:58:59

网络安全入门:Windows用户权限与提权基础详解

没绕弯子,直接说结论:网络安全入门阶段,Windows用户权限这块是你绕不过去的基础课。很多人刚开始学安全,第一反应就是去啃漏洞、挖弱口令,结果遇到一个提权场景,连当前用户在系统里是什么角色、为什么某些目…

作者头像 李华
网站建设 2026/10/22 10:58:56

SHAP实战指南:统一分类与回归模型的模型解释性分析

做多模型项目久了,你会越来越在意一个指标以外的维度——模型怎么解释自己。尤其是当你同时面对类别预测(像判断用户会不会违约)和数值预测(像预测一套房子挂牌多少钱)时,模型解释性分析就成了上线前躲不开…

作者头像 李华
网站建设 2026/10/22 10:57:58

前缀后缀分解:LeetCode 238除自身以外数组的乘积全解析

手写一个“前后缀数组”,其实没那么难——我先把这道经典题的思路拆透,再把两种写法和边界条件全部摊开。 如果你刷过LeetCode Hot100,大概率会遇到这道“238. 除自身以外数组的乘积”。它表面是数组题,实际上是考“前缀/后缀分解…

作者头像 李华
网站建设 2026/10/22 10:57:31

L曲线选参可视化:MATLAB regu工具箱解决病态问题调参

简介:这份MATLAB正则化工具箱围绕L曲线方法实现正则化参数的自适应选择,面向进行回归、反问题求解和统计建模的开发者。压缩包共68个文件,包含67个.m函数脚本和1个说明文档,整体仅76KB,轻量高效。目前已有920人学习下载…

作者头像 李华
网站建设 2026/10/22 10:56:32

HCIA/HCIP综合实验:从VLAN到NAT的完整网络搭建指南

从零开始搭一个HCIA/HCIP综合实验,很多人第一反应是“单个知识点我都会,凑到一起就崩”。这话我听过无数遍。单独练VLAN没问题,单独练OSPF也没问题,但当你面前同时摆着Trunk、STP、DHCP、PPP认证、缺省路由、NAT、ACL,…

作者头像 李华
网站建设 2026/10/22 10:54:55

C++模拟操作系统进程调度:FCFS与SJF算法实现与避坑指南

简介:面向操作系统课程设计与期末大作业的C课程设计资源,完整覆盖进程调度FCFS先来先服务与SJF短作业优先两种经典算法的实现。资源包含cpp源码文件、说明文档与可直接运行的exe程序,共3个文件,压缩包仅36KB,轻量便携&…

作者头像 李华