☰
无约束优化实战:最速下降法与牛顿法对比解析
2026/10/14 1:07:38 网站建设 项目流程

很多人一开始接触“无约束优化”这几个字,脑子里浮现的是数学课上那些拗口的定理,或者论文里密密麻麻的推导。但在工程一线摸爬滚打久了你会发现,这个东西不是摆在纸面上的摆设,而是实打实能拿来吃饭的“手艺”。

我在某智能硬件团队调设备参数时就深有体会。产线上一台设备带了好几个旋钮,对应若干内部参数,组合起来几十种搭配,靠人手一项项试过去,一天都调不出一组能用的标定结果。后来换了思路:把“成品效果偏差”定义成一个数学函数,零配件可调节的参数就是这个函数的自变量,问题就变成了“求这个函数的最小值”——这就是一个标准的无约束优化问题,而它对应的一组函数值就是连续可微的,可以用很多经典方法去解。管用,而且是立刻见效的那种管用。

这篇先讲那类“不需要老老实实算海森矩阵、只依赖一阶信息”的优化方法,重点拆解最速下降法和牛顿法的思路脉络。你看完之后未必能立刻手写一套工业级求解器,但至少能明白它们在干什么、为什么这么干、各自的坑在哪、以及在后端的工程系统里怎么权衡。

1. 内容整体设计与思路拆解

1.1 先弄清楚什么叫“无约束”

大家平时聊天说“无约束优化”的时候,有时候其实没分清楚边界。所谓无约束,严格来讲是指目标函数的自变量可以取整个实空间里的任意值,不存在等式约束,比如“x + y 必须等于 1”;也不存在不等式约束,比如“x 必须在 0 到 100 之间”。

现实里的问题当然常常带着一大堆约束,但我们依然要单独把无约束这块拎出来讲清楚,原因有三:

  • 无约束问题是有约束问题的基础,很多带约束问题的解法都是通过惩罚项、拉格朗日函数等方式,把约束融入目标函数,从而转变成无约束问题去求解。
  • 即便真给你一个带约束的业务场景,在一轮迭代的内部步骤里也经常要解无约束子问题。
  • 无约束方法本身在工程界的应用就非常广,比如模型回归拟合、信号重构、神经网络训练的一部分核心逻辑,本质就是在跑无约束迭代。

所以这篇的目标很明确:搞懂无约束优化里最核心的迭代思路——从某个初始点出发,按什么方向走、走多远,才能尽可能高效地下降到一个让目标函数值“足够小”的点。

1.2 核心方案选型背后的逻辑

如果你去看经典教材,会看到无约束优化方法通常被分成两大类:一类只用目标函数的梯度信息,比如最速下降法;另一类还要用二阶导数信息,比如牛顿法、拟牛顿法。

为什么会有这种分野?直接原因是计算成本。真实世界里很多目标函数压根没有解析表达式,或者即使有,二阶导数也极其难算。比如你在做某个图像处理Demo时,目标函数可能是像素差值的某种范数,计算一阶梯度已经要遍历整张图了,再去算海森矩阵,内存和时间都直接爆炸。

从“只靠梯度”开始讲,也是为了让后续理解“为什么我们要用拟牛顿法去逼近二阶信息”埋下伏笔。搞懂最基础的这个模式——求解方向选完、步长再定——后面所有的花样都是从这两个环节上做文章的。

1.3 这一篇会涉及什么,适合谁看

这篇的内容主要以最速下降法和牛顿法为核心,配合一个经典测试函数的数值对比,聊聊步长选择、收敛行为、参数敏感性这些实操中的取舍。同时会附带有针对性的常见问题排查实录,把那些教科书不会写、实验试过才知道的小坑都摊开讲。

适合这么几类人看:第一,刚开始接触机器学习或科学计算,想知道这些优化算法到底在干嘛的初学者;第二,在工程里连续踩坑、怀疑自己目标函数写得不对的开发者;第三,准备看后续拟牛顿法、共轭梯度法内容,需要一个清晰“地基”的读者。

2. 核心原理:梯度方向、步长与收敛性

2.1 梯度不是一个“向量”那么简单

我们要解的问题是:给定一个可微函数 f(x),自变量 x 可以是一个标量,但更多时候是高维向量,比如 x = (x₁, x₂, ..., xₙ)。求一组 x 使 f(x) 尽可能小。

学习这个领域的第一个关键直觉是:梯度 ∇f(x) 在某一点上的方向,指向的是这个函数在这一点上“上升最快”的方向。因此,如果我们要让函数值下降,自然该往梯度的反方向走。

这个直觉本身没有错,但很多人容易忽略一个更重要的点:梯度是一个局部性质,它只能告诉你当前位置附近的变化趋势,并不能推演出“沿着这个方向走到头,是不是全局最小”。这是最速下降法后续一切缺点的根源——它本质上是个贪心策略,每一步都选择当前视线范围内最陡的下坡路,而从不管这条路是不是通往真正的谷底。

2.2 最速下降法的迭代公式拆解

最速下降法的迭代形式可以写成一句话:

从当前点 xₖ 出发,沿负梯度方向 dₖ = -∇f(xₖ) 走一步,走多远由步长 αₖ 决定,得到新的点 xₖ₊₁ = xₖ + αₖ · dₖ。

这里的 αₖ 可以是一个预先设好的常数,也可以是在这一步里临时搜索出来的最优步长。所谓最优步长,指的是“在当前方向上走多少,能使 f 下降最多”,这本身又是一个一维优化问题。

我在实际写代码的时候,一开始总想用最优步长,以为这样每一步都做到最好,整体收敛必然快。测试之后发现完全不是这么回事:精确搜索步长在很多问题里会让算法朝“之”字形路径前进,比如经典的“锯齿现象”。原因在于负梯度方向虽然局部最优,但前后两步的方向往往是正交的,导致行进路径绕来绕去,整体效率低下。

2.3 牛顿法为什么能“直线”逼近

牛顿法走的是另一条路:不只看当前的一阶梯度,还要求解二阶导数矩阵,也就是海森矩阵 ∇²f(xₖ)。它的思路来自泰勒展开:

f(xₖ + s) ≈ f(xₖ) + ∇f(xₖ)ᵀs + ½sᵀ∇²f(xₖ)s

把右侧看作是 s 的二次函数,然后求出这个近似模型的极小点,作为下一步的迭代点。由此得到的迭代方向是:

dₖ = -[∇²f(xₖ)]⁻¹ ∇f(xₖ)

也就是说,牛顿法不只是给一个方向,还通过海森矩阵的逆对方向做了一个“校正”,把空间扭曲的因素考虑进去。如果目标函数是强凸的二次函数,牛顿法理论上一步就能达到最小值点,这也是它“霸道”的地方。

但代价也很明显。第一,每一步都要计算 n×n 的海森矩阵,维度稍微一高就非常昂贵。第二,如果海森矩阵不可逆或者不是正定矩阵,迭代过程可能直接崩坏。第三,当初始点离最优解较远时,牛顿法不一定能保证每一步都让函数值下降,偶尔还会上升,需要增加步长搜索之类的保护机制。

3. 实操过程:从零实现两类方法并对比

3.1 环境准备与测试函数选择

我通常用 MATLAB 或 Python 来做这类实验。Python 环境相对轻量,直接依赖 NumPy 就能跑本书所有代码,不需要额外装机器学习框架。

测试函数我选择 Rosenbrock 函数,业界更常叫它“香蕉函数”:

f(x) = 100(x₂ - x₁²)² + (1 - x₁)²

这个函数节点非常有意思——它在二维平面里画出来就像一个弯曲的山谷,谷底沿着一条弧线延展,极值点在 (1, 1) 处。等高线在山谷两侧特别陡峭,顺着山谷方向却几乎平坦。这意味着传统的只依赖一阶梯度的算法行进到峡谷中心后会非常拧巴:步长稍一大就越过谷壁反复震荡,步长一小则在谷底慢慢蠕动。用这个函数来对比算法优劣,效果非常直观。

选择 Rosenbrock 函数还有个原因:它的全局极小点是我事先知道的,所以能方便地计算每一步迭代后的误差,量化两种方法的收敛速度。做算法实验时,一个“已知答案”的问题能帮助你快速定位:如果迭代半天还没收敛到目标区域,说明算法实现或参数选择出问题了,而不是函数特性本身有歧义。

3.2 最速下降法的代码实现与步长实验

最基础的最速下降法代码很短,核心循环就是:计算梯度、沿负梯度方向走、判断收敛条件。以 Python 为例,核心部分大概长这样:

import numpy as np def rosenbrock(x): return 100 * (x[1] - x[0]**2)**2 + (1 - x[0])**2 def rosenbrock_grad(x): grad = np.zeros_like(x) grad[0] = -400 * x[0] * (x[1] - x[0]**2) - 2 * (1 - x[0]) grad[1] = 200 * (x[1] - x[0]**2) return grad def gradient_descent(x0, alpha, max_iter=1000, tol=1e-6): x = x0.copy() for i in range(max_iter): g = rosenbrock_grad(x) if np.linalg.norm(g) < tol: break x = x - alpha * g return x, i

这里需要特别提醒新手:步长 alpha 的选取是这门手艺的第一道坎。我试过从 0.001 到 0.01 等多个值。取 alpha = 0.001 时,函数值每一步都在掉,但掉得非常慢,迭代了一千步还停在离最优解很远的位置。取 alpha = 0.01 时,迭代初期下降明显,但进入弯曲山谷后开始震荡,后期需要极多次迭代才能修平误差。取 alpha = 0.1 时,直接发散,函数值越迭代越大,很快溢出。

业内通常用两个策略来替代莽撞的常数步长:一是对每次迭代的步长做“回溯线搜索”,即从一个较大的步长开始,如果函数值没有充分下降就按比例缩小步长;二是采用逐步衰减的步长序列,比如 alphaₖ = eta / k。这两种策略都能缓解收敛慢的问题,但也各有代价——回溯搜索每次要评估多次目标函数,相当于多了一笔计算开销。

3.3 牛顿法的代码实现与数值稳定性处理

继续用同样的 Rosenbrock 函数来写牛顿法。理论上我们需要解析海森矩阵,在这个函数上也能手推出来;但更贴近实际工程的做法,是用 NumPy 提供的数值梯度函数去近似,或者使用自动微分库。为了演示方便,这里我直接手写海森矩阵:

def rosenbrock_hessian(x): h = np.zeros((2, 2)) h[0][0] = 1200 * x[0]**2 - 400 * x[1] + 2 h[0][1] = -400 * x[0] h[1][0] = -400 * x[0] h[1][1] = 200 return h def newton_method(x0, max_iter=100, tol=1e-6): x = x0.copy() for i in range(max_iter): g = rosenbrock_grad(x) if np.linalg.norm(g) < tol: break H = rosenbrock_hessian(x) try: d = np.linalg.solve(H, -g) except np.linalg.LinAlgError: break x = x + d return x, i

注意这里我用了np.linalg.solve而不是直接求逆再乘,数值稳定性会好很多,速度也更快。写代码时很多人习惯用inv(H) @ g,这在维度低时没大问题,但一旦维度上去,直接求逆会放大地舍入误差。求逆是在解方程,不是目的。这是我在多处工程代码里踩过坑之后得出的铁律。

从初始点 (-1.2, 1.0) 出发,牛顿法通常在 6 到 8 步内就能把函数值降到 10⁻¹⁰ 以下。作为对照,最速下降法即便用了回溯线搜索,也需要上千步才能达到类似的精度。所以说牛顿法在处理这类强非线性、弯曲峡谷问题时“降维打击”,并不夸张。

3.4 收敛路径图与参数敏感性

我做实验时习惯把迭代路径画出来:横轴是 x₁,纵轴是 x₂,背景是 f(x) 的等高线。最速下降法的路径几乎无处不在体现“锯齿”:先是垂直降落到峡谷一侧,然后横穿到另一侧,反复横跳。而牛顿法的路径则比较直,方向感明确,基本上几步就插到谷底。

这张图对工程判断极有帮助。有一次,我看某套系统的标定结果总在某个精度上抖动,光看误差曲线并没有定位出原因。后来把迭代路径可视化了,立刻发现算法是在一个狭长山谷两侧来回穿越,每一轮迭代的进展都被进程“打枣式”地浪费掉了。后来换了带二阶信息的求解方式,一次就处理到指定精度。

参数敏感性也值得一提。最速下降法对步长极度敏感,稍微调大一点就是“发散”,调小一点就是“蜗牛速度”。牛顿法对初始点更敏感,但所受影响的方式不太一样。可以从不同初始点测试:如果初始点离最优点比较远,或者落在非凸区域,牛顿法的海森矩阵可能不正定,迭代方向不一定朝下降走,必须配合修正策略,比如加正则项或采用阻尼牛顿法。

4. 工具选型解析:什么场景用什么算法

4.1 一阶方法依然是“大众之选”

进入工程实战后你会发现,最速下降法尽管收敛慢,但在很多大规模问题里仍然是首选。原因很简单:每一步的成本极低。只要你写得出目标函数的梯度,就能迭代。如果函数本身维度达到百万级甚至更高,海森矩阵根本存不下来,更别提求逆了。哪怕用拟牛顿法,也受不了超高维时的存储开销。因此在深度学习的训练里,使用的绝大多数优化策略仍然是一阶的,比如带动量或自适应学习率的变体。你会发现它们的骨架里依然有着“沿梯度反方向移动”的影子,只是加了历史信息和自适应缩放。

如果说这里有值得记住的一句话,那就是:一阶方法不“笨”,它只是把自己的全部筹码押在了每一步成本足够低上。

4.2 二阶方法在中小维度上的降维打击

牛顿法的适用场景,典型特征是维度低、函数形状复杂、对精度要求高。比如机器人运动规划里的局部轨迹优化、光学系统设计里的镜片参数校正、控制系统的参数辨识等场景,自变量通常几十到几百维,这个规模下求海森矩阵及其逆一点也不心疼。而这类目标函数往往有严重的高原、峡谷和非对称结构,一阶方法会跑得极其痛苦,二阶方法却能借助曲率信息优雅地穿越。

实际工程中很少直接用标准牛顿法,更多是用它的改良版:阻尼牛顿法(迭代步长不是 1,而是通过线搜索确定)、高斯-牛顿法(适用于最小二乘类目标)、列文伯格-马夸尔特法(在高斯-牛顿和梯度下降之间插值过渡)。后者在传感器标定、三维重建这些领域几乎是默认标配,原因就在于它既能保留牛顿法的快速收敛特性,又能在海森矩阵病态时平滑退化为谨慎的一阶步进。

4.3 代码库选择建议

Python 生态里,SciPy 无疑是标配。它的optimize.minimize接口可以一行式切换不同的算法:method='BFGS'、method='Newton-CG'、method='L-BFGS-B'等。做原型验证时,直接丢给 SciPy 跑非常省心,但千万别因此忽略底层原理——当你需要定制目标函数或自己在生产代码里嵌入优化器时,没有底层认知寸步难行。

C++ 工程里常用 Eigen 做矩阵运算,配合一些开源优化库。再往下层走,嵌入式环境里则需要手工控制每一步的迭代,这时更需要理解方向与步长的微观行为。工具可以换,但“方向 + 步长”这个两段式框架永远是核心,就像炒菜可以换锅换灶,但火候和调味的基本功跑不掉。

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

5.1 步长太大导致发散,步长太小导致龟速

这是最频繁遇到的问题,没有之一。我最早做数值实验的时候,以为选个“看起来差不多”的步长就行,结果同一个步长在某个初始点好使,换个初始点直接数值爆炸。

排查思路比较固定:第一,在迭代初期打印每一步的函数值,如果出现连续上升,那基本是步长过大;第二,如果函数值持续下降但下降幅度非常小,迭代几百步后离最优解还很远,那就是步长过小;第三,直接把目标函数形态打印出来,观察极值点附近是不是特别狭窄或特别平坦。

推荐的做法是直接用回溯线搜索,而不是手工试步长。核心伪代码如下:

给定 alpha = 1.0,rho = 0.8,c = 0.1 while f(x + alpha * d) > f(x) + c * alpha * g * d: alpha = alpha * rho x = x + alpha * d

这个策略能保证每一步都有“足够下降”,付出的额外代价是几次目标函数评估,但换来的是稳定性,非常划算。

5.2 梯度计算误差引起的方向错乱

真实工程里很少有能手动写出干净解析梯度的机会。很多时候目标函数是仿真器封装出来的,或者数值上需要差分近似。用有限差分近似梯度时,步长选得太大会引入截断误差,选得太小会引入舍入误差。这是一个典型的“两头堵”问题。

我在某个跨平台系统调试中就吃过这个亏:目标函数是一个十几层嵌套的仿真,差分解算出梯度花了大半天,收敛行为仍然诡异。后来用符号微分工具重写了梯度计算,收敛状态立竿见影地正常了。这里给我的教训是:如果能用自动微分,就别用手推解析梯度;如果连自动微分都没有,至少用一个中心差分公式,并做多组步长测试。

5.3 海森矩阵不正定时的处理方式

牛顿法在非凸区域会表现出“判断力失准”,因为海森矩阵可能含有负特征值,导致搜索方向不是下降方向。常见处理手法有几种:一是加正则项,把海森矩阵改成 H + λI,λ 为正数,这样就强制让特征值往正数方向移动;二是当搜索方向不下降时,直接切换到最速下降方向保底;三是采用截断牛顿法,每次迭代只在共轭梯度法内部求解牛顿方向,不显式存海森矩阵。

这几种方式各有适用面,但共同的底线是要在代码里加一个“方向合法性检查”:计算 dᵀ ∇f,如果为正,说明这不是下降方向,必须修正。这类防御式编程在运行期能省下大量排错时间。

5.4 收敛标准设得太严或太松

很多人写终止条件只看“梯度模长小于某个值”。但不同问题的梯度量级差异巨大,同一阈值可能在 A 问题上过于苛刻,在 B 问题上却太宽松了。更稳健的收敛判据是多条件组合:目标函数相邻两轮的变化量小于某阈值、自变量相邻两轮的变化量小于某阈值、梯度范数小于某阈值,三者中满足若干项才停止迭代。

另外还要注意“目标函数变化很小”并不等于“到达极值点”,在有些问题上函数在某个狭长区域里变化极小,但距离极值点还有很长一段距离。最好把迭代轮次上限也设上一个硬约束,防止死循环或观感上的不收敛。

常见问题可能原因排查思路
迭代发散步长过大 / 目标函数不光滑降低步长,使用回溯线搜索
收敛极慢步长过小 / 问题存在狭长峡谷换二阶方法,或采用自适应步长
牛顿法方向错误海森矩阵不正定加正则项 / 做方向合法性检查
梯度方向不准差分步长不当 / 函数噪声大改用自动微分 / 中心差分 / 滤波

5.5 实测下来觉得“算法不行”的时候,先怀疑问题本身

这是最容易被忽略的一点。有一次我调一个工业标定模型,用了最强的求解器也收不到理想效果。折腾很久后发现,目标函数本身对某些参数完全不敏感,也就是说那个参数在最优点附近的梯度几乎为 0,无论用什么算法都需要极多次迭代才能修正到位。后来修正了问题建模,换了一组更具物理意义的参数表示,优化器立刻恢复“智力”。

算法只是工具,目标函数和质量度量才是你要花最多心思的地方。如果你发现一个公认可靠的算法在你的问题上表现差劲,第一反应不该是“这个算法不行”,而是“我的问题建模是不是有问题”。拿无约束优化的框架套进一个问题,至少得保证目标函数是连续且可微的,如果目标是带噪声的离散函数,你需要的可能是别的思路。

6. 深度经验:从一次实际调参复盘看方法选型

前段时间给某公司的一套图像标定系统做优化,自变量是镜头的十余个畸变参数,目标是让重投影误差的函数值最小。一开始我用 L-BFGS(一种拟牛顿法)跑,收敛速度在几十步左右就能到不错精度,但最终残差异常稳定地停在某个阈值附近,怎么都降不下去。

后来仔细分析发现,问题出在目标函数本身存在比较浅的局部极小点,而初始值是从默认零参数开始的,算法很快滑入附近的局部极小区域,却无法越过中间的隆起。于是改成了多起点策略:在参数空间内取多个随机初始点,分别跑一轮 L-BFGS,选取目标函数值最低的结果作为最终解。效果立竿见影,最终的标定精度直接提升了一个量级。

这件事给我最大的启发是:有效优化器加持下,很多问题的瓶颈早已不在迭代算法本身,而在于你给了它什么样的初始点、什么样的目标函数、什么样的约束处理方式。对号入座看这篇内容,你的场景里到底更需要一阶方法的温和稳健,还是二阶方法的雷厉风行,往往取决于五个方面:变量维度、问题结构、精度要求、计算预算、初始点位置。把它们想清楚,方案基本就出来了。

个人经验再补一句:学无约束优化最好的方式不是啃理论推导,而是拿一个已知极值点的函数反复“折腾”,改步长、换初始点、加噪声,亲手感受不同算法在压力和挑战面前的表现。这种手感,看多少篇文章都换不来的。后续我们再继续往后讲拟牛顿法和共轭梯度法,你可以先拿这篇里面的代码框架搭底,到时候把方向计算模块替换掉,就又是一套新工具了。

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

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

立即咨询