机器学习优化算法剖析:梯度下降法与牛顿法手推公式及工程应用 机器学习优化梯度下降法与牛顿法手推公式详解做算法这行尤其是刚入门机器学习那阵子绝大多数人第一个被劝退的坎儿就是优化算法。看代码倒还好调个库跑个模型似乎也不难但一旦要认真看论文、看源码或者自己手搓一个模型出来满屏的梯度、海森矩阵、迭代公式扑面而来瞬间就懵了。我当年也是这样前前后后折腾了大半个月才勉强把梯度下降法和牛顿法从“知道名字”变成了“能自己推、能讲清楚为什么”。这篇文章就把我踩过的坑和琢磨透的东西一次讲明白。我会从最朴素的直觉出发把梯度下降法和牛顿法的每一步推导都手写出来不跳步、不藏公式同时把“为什么这么设计”“什么时候用哪个”“实际工程里有哪些大坑”也一并说清楚。不管你是刚接触机器学习的初学者还是在复习数学基础准备面试的开发者这篇文章应该都能帮你把这两个算法真正装进脑子里。1. 从优化问题开始我们到底在做什么先理清一个基本问题机器学习里的“训练”本质上是在解一个优化问题。我们有一个模型模型带一堆参数我们用数据去算一个损失函数——这个函数的值越小说明模型在训练集上表现得越好。训练的过程就是不断调整参数让损失函数的值尽可能小。写成数学语言就是min θ L(θ)其中 θ 是模型的参数向量L(θ) 是损失函数。比如线性回归的损失函数是均方误差逻辑回归是交叉熵神经网络是各种自定义的 loss。不管损失函数长什么样我们手上的工具其实就那么几类一类是只用到一阶导数梯度的方法梯度下降法是代表另一类是用到二阶导数海森矩阵的方法牛顿法是代表。1.1 为什么不能直接求解析解有人可能会问求最小值不就是令导数等于零吗为什么还要搞迭代算法确实如果损失函数是凸函数且形式简单比如线性回归的均方误差我们可以直接通过正规方程求出解析解。但现实中的模型没这么温柔。神经网络的损失函数是一个极度复杂的非线性函数参数动辄百万千万级别解析解根本不存在或者即使存在也根本算不出来。所以我们必须依赖迭代法——从一个初始参数出发一步一步地更新参数让损失函数逐步下降最终逼近最小值。这种迭代优化的思路就像你站在一座山上要摸黑走到山谷的最低点。你看不到整座山的地形图只能通过脚底感受当下的坡度然后朝最陡的下坡方向迈一步再感受、再迈步慢慢接近谷底。梯度下降法就是这么干的而牛顿法更厉害一点它不光感受坡度还能感受到坡度的变化趋势迈步的时候把前方的地形曲率也算进去。1.2 泰勒展开所有优化算法的共同语言无论梯度下降还是牛顿法它们的推导都逃不开一个数学工具——泰勒展开。泰勒展开告诉我们一个光滑的函数在某个点附近可以用它的导数信息来逼近。对于一元函数 f(x)在点 x_k 处做二阶泰勒展开f(x) ≈ f(x_k) f(x_k)(x - x_k) (1/2) f(x_k)(x - x_k)^2对于多元函数 f(x)其中 x 是 n 维向量在点 x_k 处做二阶泰勒展开f(x) ≈ f(x_k) ∇f(x_k)^T (x - x_k) (1/2) (x - x_k)^T H(x_k) (x - x_k)这里 ∇f(x_k) 是梯度向量H(x_k) 是海森矩阵也就是二阶偏导数组成的矩阵。掌握了这个工具接下来的所有推导就顺理成章了。梯度下降法可以看作只保留了泰勒展开的一阶项牛顿法则把二阶项也考虑进来了。差别就在这一项上但这一项带来的效果天差地别。2. 梯度下降法手推全过程梯度下降法是最基础、最常用的优化算法没有之一。理论上讲只要你有一阶导数信息你就能用梯度下降法去优化任何一个可微函数。它的核心思想简单到令人发指既然梯度是函数增长最快的方向那梯度的反方向就是函数下降最快的方向沿着反方向走就能让函数值下降。2.1 朴素版本一元函数的梯度下降推导先从最简单的情况入手。设我们有一个一元函数 f(x)在点 x_k 处做一阶泰勒展开f(x) ≈ f(x_k) f(x_k)(x - x_k)我们希望找到一个新点 x_{k1}使得 f(x_{k1}) f(x_k)。从泰勒展开式来看要让 f(x) 比 f(x_k) 小只需要让 f(x_k)(x - x_k) 这一项尽可能小为负且绝对值大。但这里有个问题如果不对 (x - x_k) 的距离加以限制我们可以让 x 离 x_k 无限远一阶泰勒展开早就失真了。泰勒展开只在 x_k 附近的一个小邻域内才近似成立。所以我们要限制每次迈步的步长——这就是学习率的由来。令 Δx x - x_k那么要最小化的是 f(x_k) · Δx同时 |Δx| 被限制在一个很小的范围 η 内。显然当 Δx 与 f(x_k) 方向相反时乘积最小。也就是Δx -η · f(x_k)于是新的迭代公式x_{k1} x_k - η · f(x_k)这个式子看着简单但整个过程推出每一步都是有说道的。学习率 η 就是步长控制了每次更新走多远。方向是负梯度方向步长是 η两者缺一不可。2.2 多元情况的推导与矩阵形式实际机器学习中参数几乎永远是向量所以必须升级到多元版本。设损失函数 L(θ)参数 θ ∈ R^n。在 θ_k 处做一阶泰勒展开L(θ) ≈ L(θ_k) ∇L(θ_k)^T (θ - θ_k)同样的逻辑我们希望选择一个 Δθ θ - θ_k在步长受限的条件下让 L(θ_k) ∇L(θ_k)^T Δθ 最小。由柯西-施瓦茨不等式∇L(θ_k)^T Δθ ≥ -|∇L(θ_k)| · |Δθ|等式成立的条件是 Δθ 与 ∇L(θ_k) 方向相反。因此Δθ -η · ∇L(θ_k)最终迭代公式θ_{k1} θ_k - η · ∇L(θ_k)这就是我们在所有深度学习框架里反复见到的那个更新公式。无论是 PyTorch 里的optimizer.step()还是手写线性回归时用到的更新语句本质都是这一行。2.3 学习率的影响从数学公式看为什么步长不能太大关于学习率我一直建议大家不要只把它当超参来调而是要从公式层面理解它为什么会带来那些问题。看上面的迭代公式如果 η 太小每一步都走得很谨慎需要迭代很多次才能收敛训练速度非常慢。如果 η 太大会出现什么情况逐项分析迭代公式。假设当前点 x_k 在最小值点 x_min 右侧梯度为正更新后 x_{k1} x_k - η · g_k。如果 η 很大x_{k1} 可能一下子冲到最小值的左侧很远的地方。此时在新的点梯度为负更新后又冲回右侧。结果就是参数在最小值附近来回震荡甚至每一步都越弹越远——这就是“发散”。从数学上看对于一个二次函数 f(x) (1/2) a x^2梯度是 ax迭代公式是 x_{k1} (1 - ηa) x_k。要让这个序列收敛必须满足 |1 - ηa| 1即 0 η 2/a。如果 ηa 2|1 - ηa| 1参数就会每一步放大直接爆炸。所以学习率本质上受限于损失函数在当前位置的曲率或者说二阶导。曲率越大允许的步长就越小。这也自然引出我们为什么要研究牛顿法——它把曲率信息显式地考虑进来了。3. 牛顿法把曲率信息加进来如果说梯度下降法只用了函数的一阶信息那牛顿法就是用了一阶加二阶信息。它的想法很直接既然泰勒展开里有二阶项能更准确地描述函数在局部的形状为什么不让它参与决定步长和方向3.1 从二阶泰勒展开推导牛顿法继续用一元函数开始。对 f(x) 在 x_k 处做二阶泰勒展开f(x) ≈ f(x_k) f(x_k)(x - x_k) (1/2) f(x_k)(x - x_k)^2这是一个关于 (x - x_k) 的二次函数。我们希望找到让这个近似函数最小的点。对右边关于 x 求导并令其等于零f(x_k) f(x_k)(x - x_k) 0解这个方程x - x_k -f(x_k) / f(x_k)于是得到牛顿法的迭代公式x_{k1} x_k - f(x_k) / f(x_k)注意这里的推导过程和梯度下降法有本质区别。梯度下降法是人为设定一个步长 η然后沿着负梯度方向走。牛顿法则没有显式设置学习率它直接通过二阶信息算出了一个“最优步长”—— f(x_k) / f(x_k) 这个比值同时包含了方向和步长信息。对于二次函数来说这个公式一步就能跳到最小值点因为二阶泰勒展开就是原函数本身没有近似误差。3.2 多元牛顿法向量与矩阵形式把牛顿法推广到多元函数。损失函数 L(θ) 在 θ_k 处的二阶泰勒展开L(θ) ≈ L(θ_k) ∇L(θ_k)^T (θ - θ_k) (1/2)(θ - θ_k)^T H_k (θ - θ_k)其中 H_k 是海森矩阵第 i 行第 j 列的元素是 ∂²L/∂θ_i ∂θ_j 在 θ_k 处的取值。对这个近似函数关于 θ 求梯度并令其为零∇L(θ_k) H_k (θ - θ_k) 0移项得到θ - θ_k -H_k^{-1} ∇L(θ_k)最后牛顿法迭代公式θ_{k1} θ_k - H_k^{-1} ∇L(θ_k)每一步都要计算海森矩阵的逆 H_k^{-1}这就是牛顿法在实际工程中最大的痛点。假设模型有 1000 万个参数海森矩阵就是 1000 万 × 1000 万的矩阵光存储就需要 400 TB按 float32 计算这显然不现实。即便参数少一些求逆的复杂度是 O(n³)参数稍微多一点就完全不可接受。3.3 为什么牛顿法收敛更快线性收敛与二次收敛对比这里用一个直观的比喻。梯度下降法就像是一个近视眼只知道眼前几米的路是上坡还是下坡只能凭感觉迈出固定大小的一步。牛顿法则像是一个能看到远方地形的人它知道前方是平缓还是陡峭会根据地形的弯曲程度调整步伐地形平缓就迈大步地形陡峭就迈小步。用数学语言来比较。对梯度下降法在最小值点附近误差 e_k x_k - x_min 的收敛行为近似为e_{k1} ≈ (1 - ηf(x_min))e_k这是线性收敛误差每一次乘一个常数因子。而对牛顿法可以证明e_{k1} ≈ (f(x_min) / (2f(x_min))) e_k^2误差的平方出现在右边这是二次收敛。如果当前误差是 0.1下一次大约变成 0.01再下一次变成 0.0001每步缩小的位数翻倍。而线性收敛的话每次可能只从 0.1 变成 0.05 或 0.08慢得肉眼可见。看完收敛性分析你就明白为什么在某些问题上牛顿法只需要几步就到精度而梯度下降法要跑几百上千轮。4. 从推导到工程两种算法的选型、改进与常见问题公式推完了接下来是更接地气的部分。你在实际训练模型的时候直接用原始牛顿法的场景其实极少倒不是因为原理不行而是计算代价压不住。但理解它的原理去看那些更高级的优化器时——比如 L-BFGS、Adam、RMSProp——你会发现它们都是在牛顿法和梯度下降法之间做折中设计出来的。4.1 工程实现避坑学习率、海森矩阵与数值稳定性先说梯度下降法在工程实现中最常见的几个坑。学习率的选择永远是第一个坑。我之前训练一个简单的逻辑回归模型初始学习率设为 0.5结果损失值在训练日志里跳来跳去完全收敛不了。后来把学习率降到 0.01模型很快就稳定收敛了。当时我就想要是早点从二次函数的收敛条件去理解学习率就不会这样盲目去试了。一个实用的做法是开始训练时用一个较小的学习率比如 0.001观察前几个 epoch 的损失变化如果损失在稳步下降可以尝试逐步调大如果损失震荡就调小一个数量级再试。梯度下降法还有一个需要注意的地方是特征缩放。因为这个方法对每个维度用的是同一个学习率 η如果特征之间的量纲差距悬殊比如一个特征范围是 [0, 1]另一个范围是 [0, 100000]那么梯度在各个维度上的量级会完全不同。此时同一个学习率会导致某些维度更新太慢另一些维度又可能震荡甚至发散。这就是为什么要做标准化或归一化——把各个维度的尺度统一之后梯度下降才能真正高效工作。再看牛顿法。原始牛顿法在工程中的第一个问题是海森矩阵可能不是正定的。在非凸函数上海森矩阵可能是负定或半负定的这时候 H_k^{-1} ∇L(θ_k) 的方向不仅不是下降方向反而可能指向上升方向。实际表现是迭代一步之后损失函数反而变大了。解决方案是加一个正则化项把海森矩阵修正为 H_k λI其中 λ 是一个正数这样保证矩阵正定迭代也就稳定了——这个思想后来也被应用到 Levenberg-Marquardt 算法中。第二个问题是当海森矩阵接近奇异时求逆的结果会非常大导致参数更新一步就飞出去。这个在数值优化里通常可以通过加正则化或使用伪逆来缓解。4.2 变体算法速览从 SGD 到 Adam牛顿的遗产说实话现在深度学习中标准的优化器已经不是朴素的梯度下降法了而是各种变体。但如果你把公式拿过来对比会发现它们都在解决同一个问题——如何让梯度下降既快又稳。随机梯度下降SGD解决了全量梯度计算代价过高的问题。原来每次迭代要用全部训练样本算梯度现在每次只随机抽一个小批量mini-batch去估计梯度。梯度带上了噪声但期望方向仍然是对的而且计算速度快到能迭代更多轮次整体反而更快收敛。带动量的 SGD 则在公式里加了一个“惯性项”v_{k1} βv_k (1-β)∇L(θ_k) θ_{k1} θ_k - ηv_{k1}这等于把历史梯度信息累积起来方向一致时加速方向不一致时减速有效抑制了梯度下降在峡谷地形中的震荡——这个思路也借鉴了牛顿法中“利用更多历史/局部信息来修正方向”的理念。RMSProp 和 Adam 则更进一步它们用梯度的平方滑动平均来给每个参数自适应地调整学习率。核心思想是对梯度大的维度自动放慢步伐对梯度小的维度自动加快步伐。这其实是对牛顿法“用二阶信息调整步长”的一种廉价近似——不是真的计算海森矩阵而是用历史梯度平方来模拟曲率。理解了牛顿法的原理之后你再看 Adam 的公式就不会觉得它是凭空冒出来的了。4.3 实战心得我如何选择优化器并调试收敛根据我的个人经验在传统机器学习模型上比如逻辑回归、线性回归如果特征维度不高几千以内而且训练样本量适中用 L-BFGS 这类拟牛顿法效果非常惊艳。它比梯度下降法收敛快得多而且不需要手动调学习率省心省力。在深度学习中Adam 基本是默认首选因为它在大多数任务上都能以较少的调参成本达到不错的收敛效果。但 Adam 也有它的毛病在训练后期容易在最优解附近来回波动难以精细收敛。这个时候有一个常用的技巧在训练后期切换到带动量的 SGD或者把 Adam 的学习率衰减到很小的值让模型在局部区域精细打磨。这个“先 Adam 后 SGD”的策略在很多比赛中都能有效提升最终精度是业界常見的做法。另外关于牛顿法的经验有一次我需要求解一个规模不算大的非线性最小二乘问题参数大概只有几百个用梯度下降法怎么调都感觉很慢。后来改用牛顿法几下就收敛了。当时海森矩阵的计算是通过数值差分近似得到的虽然每一步计算有点贵但总迭代次数少了两个数量级整体耗时反而大幅下降。所以不要一听牛顿法就说“太贵用不起”——在小规模问题上它依然是一个极其有力的工具。4.4 常见问题速查表为了方便实际操作中快速排查问题我把常见现象、原因和应对方案整理成了下面这张表。现象可能原因排查方向与解决方案损失震荡不下降学习率过大数据未归一化调低学习率对特征做标准化处理损失下降极慢学习率过小梯度消失调大学习率检查模型初始化和激活函数训练后期损失波动无法精调Adam 类优化器步长过粗衰减学习率切换到带动量的 SGD 精调牛顿法迭代时损失上升海森矩阵非正定给海森矩阵加正则项 λI保证正定性牛顿法更新一步就爆炸海森矩阵接近奇异逆矩阵过大加更大的正则化系数改用伪逆或拟牛顿法梯度下降收敛到鞍点停滞一阶信息在鞍点处为零使用带动量的 SGD 打破对称性或改用二阶优化方法这个表中包含的问题都是我在实际工作中真正遇到并解决过的。尤其是牛顿法在海森矩阵非正定时损失上升这个问题最初我甚至怀疑是自己的梯度算错了后来排查了很久才发现是矩阵正定性出了问题。如果你也在做类似的事情不妨对照排查能节省很多时间。5. 写在最后的一点体会从梯度下降到牛顿法本质上是在追问同一个问题我们如何找到一个点让函数值不断下降直到最小梯度下降说“只信脚下的坡度每次小心地走一截”牛顿法说“多看几步把地形弯曲也考虑进去一步就能跨到位”。两种策略各有代价和适用范围而现代优化算法的历史很大程度上就是在这两种思路之间不断折中、改进和融合。我个人在实际操作中的体会是先把这两套公式从零推一遍比读十篇博客都有用。推导的过程中你会自然地明白每一个符号的含义明白为什么学习率不能太大、为什么海森矩阵要正定、为什么二阶收敛快但计算代价高。之后再去看 Adam、RMSProp、L-BFGS 这些进阶算法你会发现自己几乎不需要死记硬背公式的来龙去脉自动就清晰了。如果这篇文章对你的学习有一点帮助那我的目的就达到了。至少下次当你再看到某个优化算法的公式时可以想起——这些看似复杂的符号背后其实就是一个朴素的问题下一步该往哪儿走走多远。