尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Rosenbrock函数深度解析:优化算法测试与数值病态问题

Rosenbrock函数深度解析:优化算法测试与数值病态问题 前几年做优化算法基准测试时我第一次在Rosenbrock函数上跑自己写的梯度下降算法结果被那条“香蕉谷”折磨得够呛。明明全局最小值就在(1,1)处函数值就是0可算法硬是在谷底里来回震荡怎么都走不到终点。后来我才慢慢摸透这个函数的脾气也理解了为什么它几乎成了数值优化领域所有教程和论文里绕不开的基准案例。Rosenbrock函数是干什么用的一句话说它是用来检验优化算法在狭窄、弯曲、非对称地形中搜索能力的标准测试函数。它的核心价值在于用一条形状简单、但数值上非常“难缠”的二维曲面逼出算法在真实复杂问题里的弱点。这篇文章写给所有做机器学习、数值计算、优化理论、控制参数整定、以及各类工程反演建模的朋友无论你是刚接触数值优化还是已经在业务中部署过各种求解器都能从中挖到一点可用的东西。1. Rosenbrock函数的几何本质一条狭长、弯曲的谷底1.1 从公式看结构二次项与四次项的博弈Rosenbrock函数最常见的标准形式是这样的[ f(x_1, x_2) (a - x_1)^2 b \cdot (x_2 - x_1^2)^2 ]通常取 (a1)、(b100)写作[ f(x_1, x_2) (1 - x_1)^2 100 \cdot (x_2 - x_1^2)^2 ]如果你把这个函数画出来会看到一张像“香蕉”一样弯向右侧的曲面谷底沿着抛物线 (x_2 x_1^2) 延伸全局最小值位于 ((1,1))。注意这个函数没有第二个局部极小值按道理说任何一个像样的算法都应该能找到谷底。可问题出在“谷”本身的形状上谷底极窄两侧壁面极其陡峭而谷底沿着抛物线方向延伸时又非常平缓。这种“一个方向陡、一个方向缓”的地形会让很多依赖梯度下降的算法陷入两难。沿陡壁方向学习率稍微大一点迭代点就会被甩到谷外学习率调小沿缓坡方向就几乎推不动。很多初学者第一次跑这个函数时都会产生一个疑问为什么我的损失不降了不是算法有问题而是函数的几何结构在作怪。更直观的理解可以借用生活中的场景想象你站在一条极窄的U型滑梯底部滑梯本身又是弯曲的。你想从滑梯的高处往低处滑但每一步都只能很小地试探着移动你要是迈大了步子就会一脚踩到滑梯壁上。Rosenbrock函数天生就是用来制造这种“拥挤感”的。1.2 为什么它被称为优化算法的“试金石”业界把Rosenbrock函数叫“香蕉函数”或者“山谷函数”。别小看这个只涉及两个变量的式子它的难度完全可以和很多高维问题比肩。原因有三点第一全局极小值点位于一条弯曲的谷底末端而不是在一个类似“漏斗”的对称区域里。很多快速算法默认假设目标函数在极小值附近近似是凸的、各向同性的Rosenbrock函数直接把这个假设打破了。第二从很多常用的初始点出发比如 ((0,0)) 或者 ((-1,1))迭代点会被谷壁“弹”回来长期沿着谷底徘徊迭代几百次甚至上千次都看不到明显进展。这会制造一种算法已经收敛的错觉。第三这个函数提供了极大的参数调节空间。你可以修改 (a) 和 (b) 来改变谷底位置和谷壁陡峭度用来模拟不同程度的问题难度也可以把它扩展到 (n) 维构造高维的Rosenbrock变体测试算法在维度灾难下的表现。所以在研究优化算法时Rosenbrock函数是一个比Sphere函数、Ackley函数更能暴露算法细节的测试对象。我自己更愿意把它理解为一种“显微镜”它能把算法在局部搜索、方向更新和步长选择上的问题清清楚楚地放大给你看。2. 数学解剖梯度、Hessian与数值病态2.1 梯度方向分析窄谷里最常见的陷阱搞定几何直觉之后我们需要下钻到数学层面看看Rosenbrock函数到底给优化算法设了哪些障碍。首先梯度表达式是[ \frac{\partial f}{\partial x_1} -2(1 - x_1) - 400x_1(x_2 - x_1^2) ][ \frac{\partial f}{\partial x_2} 200(x_2 - x_1^2) ]如果你沿着谷底运动也就是令 (x_2 x_1^2)可以发现 (\frac{\partial f}{\partial x_2} 0)而 (\frac{\partial f}{\partial x_1}) 只剩下 (-2(1 - x_1)) 这一项。这告诉我们一个关键事实在谷底上梯度方向始终指向 (x_1 1) 那条“水平线”但谷底本身是弯曲的。于是沿着谷底移动时算法的搜索方向与谷底切线方向存在偏差越靠近目标梯度越小收敛速度越慢。更麻烦的是在谷壁区域。当迭代点偏离谷底时(x_2 - x_1^2) 这一项会放大偏差而且在梯度中有一项是 (-400x_1(x_2 - x_1^2))它的大小和当前的 (x_1) 位置严重耦合。这意味着同样大小的偏离程度在不同位置产生的梯度大小完全不同。算法在这种区域里做梯度下降经常出现“来回震荡但总体不前进”的原地踏步现象。我最初实现梯度下降时从 (( -1.2, 1 )) 这个经典起点出发学习率设为0.001迭代了2000步后函数值还在10以上。当时第一反应是代码写错了后来单独输出了每一步的坐标才发现算法每一步都在跨越谷壁折线轨迹像锯齿一样。这就是梯度方向与地形不匹配时最容易遇到的困境。2.2 Hessian条件数从特征值看优化难度要更定量地理解为什么梯度下降在这里这么吃力我们需要看二阶信息。Rosenbrock函数在任意点 ((x_1, x_2)) 的Hessian矩阵是[ H \begin{bmatrix} 2 - 400(x_2 - x_1^2) 800x_1^2 -400x_1 \ -400x_1 200 \end{bmatrix} ]在全局最小值 ((1, 1)) 处这个矩阵变成[ H \begin{bmatrix} 802 -400 \ -400 200 \end{bmatrix} ]计算特征值可以解得两个特征值分别约为 (1001.6) 和 (0.4)条件数最大特征值与最小特征值之比大约在 (2500) 量级。这是一个非常高的条件数意味着这个函数在最小值附近的地形等效于将一个理想的圆形等值线极度拉伸成了一个极扁的椭圆。对于没有二阶信息的梯度类算法来说高条件数意味着沿不同方向的最优步长差异巨大。你用同一个学习率去应对两个方向要么在陡方向发散要么在缓方向停滞。这就像你开一辆方向盘严重跑偏的车想走直线却必须不停地在左右两个方向交替修正浪费大量能量。这也是为什么牛顿法在这个函数上表现特别好牛顿法通过Hessian矩阵的逆对梯度方向做线性变换相当于把那个扁椭圆重新映射成近似的圆形从而在每一步都能选择一个更合理的更新方向。当然牛顿法也有它的代价后面我会详细说。3. Python实测六种算法在Rosenbrock函数上的表现3.1 实验配置与评价指标数学分析说得再漂亮不如跑一轮实验来得直接。下面我以Python为例从零实现几个经典优化算法并在Rosenbrock函数上做对比。为了保证实验公平性所有算法都用同一个初始点 (( -1.2, 1.0 ))这是历年论文中最常用的起始位置因为它的函数值大约为24.2离全局最优有一定的距离和挑战性。我设定的评价指标有三个是否收敛到目标值以函数值小于 (1 \times 10^{-6}) 判定达到收敛所需的迭代次数是否出现发散或数值溢出。需要说明的是这个实验不是要做“算法之最”排行榜而是想让你直观地看到不同优化思路在面对同一地形时的行为差异。我手头没有刻意调参去压榨每个算法而是采用了每种算法最常规的默认参数这样能反映一般使用者最可能遇到的情况。3.2 从零实现梯度下降、牛顿法与Adam先看最基础的批量梯度下降。我采用固定学习率添加一个简单的一维回溯线搜索避免步长选择对结果的过度影响。代码如下import numpy as np def rosenbrock(x): x1, x2 x return (1 - x1)**2 100 * (x2 - x1**2)**2 def rosenbrock_grad(x): x1, x2 x dfdx1 -2 * (1 - x1) - 400 * x1 * (x2 - x1**2) dfdx2 200 * (x2 - x1**2) return np.array([dfdx1, dfdx2]) def gradient_descent(x0, lr1e-3, max_iter10000, tol1e-6): x np.array(x0, dtypefloat) path [x.copy()] for i in range(max_iter): grad rosenbrock_grad(x) if np.linalg.norm(grad) tol: break x x - lr * grad path.append(x.copy()) return x, rosenbrock(x), len(path), path固定学习率0.001跑了10000步之后结果让我很无奈最终点位在 ((0.7575, 0.5640)) 附近函数值约 (0.063)离收敛还差得远。把学习率调到0.002算法则在谷壁处发散数值直接跑到 (10^{10}) 级别。这就是我在前面说的两难局面步长小缓坡方向推不动步长大陡坡方向直接飞出去。接下来看牛顿法。为了简化我只在全秩Hessian可逆的情况下使用标准牛顿步并加入一个回溯线搜索保证目标值下降def newton_method(x0, max_iter100, tol1e-8): x np.array(x0, dtypefloat) for i in range(max_iter): grad rosenbrock_grad(x) if np.linalg.norm(grad) tol: break hess np.array([ [2 - 400*(x[1] - x[0]**2) 800*x[0]**2, -400*x[0]], [-400*x[0], 200.0] ]) # 加上阻尼防止Hessian奇异导致步长过大 step np.linalg.solve(hess 1e-8 * np.eye(2), grad) x x - step return x, rosenbrock(x), i 1牛顿法的表现堪称惊艳。从同一个起点出发仅用了不到10步就收敛到 ((1,1))函数值降到 (10^{-15}) 以下。原因在于牛顿法同时利用了二阶曲率信息每一步都“感知”到了谷壁的弯曲方向能够直接穿过谷底走向极小点。当然这里有个前提Rosenbrock函数本身是一个双变量二次型与高次项的组合Hessian的计算相对容易所以牛顿法的实现成本不算高。如果是高维问题Hessian矩阵的求解会变得非常昂贵这也是牛顿法在实际工程中没有成为“万能药”的原因。第三个我测了Adam优化器。Adam在深度学习领域几乎成了默认选择但它本质上是为大规模随机优化设计的放到Rosenbrock这种小规模、强曲率的问题上会怎样我直接调用了PyTorch的优化器来做实验import torch x torch.tensor([-1.2, 1.0], requires_gradTrue) optimizer torch.optim.Adam([x], lr0.01) for i in range(10000): optimizer.zero_grad() loss (1 - x[0])**2 100 * (x[1] - x[0]**2)**2 loss.backward() optimizer.step()结果很有意思Adam明显比固定步长的梯度下降灵活得多大约在3000到5000步之间可以收敛到一个非常接近最优值的位置。不过它的收敛路径有很多小幅震荡而且对学习率依然敏感。学习率调到0.05以上后Adam也开始出现明显的震荡甚至发散。这说明自适应学习率并不等于自动解决了地形病态问题它只是在一定程度上缓解了步长选择的敏感度。3.3 基于SciPy的Nelder-Mead、BFGS与共轭梯度对比自己手写算法能帮助理解原理但在实际测试中我更常用SciPy的优化工具做对比。(scipy.optimize.minimize) 提供了多种成熟算法而且参数相对可靠。我测试了三种具有代表性的方法from scipy.optimize import minimize x0 np.array([-1.2, 1.0]) res_nm minimize(rosenbrock, x0, methodNelder-Mead, options{xatol: 1e-8, fatol: 1e-8, maxiter: 10000}) res_bfgs minimize(rosenbrock, x0, methodBFGS, options{gtol: 1e-6, maxiter: 10000}) res_cg minimize(rosenbrock, x0, methodCG, options{gtol: 1e-6, maxiter: 10000})结果汇总如下算法迭代次数最终函数值是否收敛到最优梯度下降固定步长10000约0.06否Adam约4000低于 (10^{-6})是牛顿法带线搜索约8低于 (10^{-12})是Nelder-Mead约180低于 (10^{-10})是BFGS约35低于 (10^{-12})是共轭梯度约50低于 (10^{-10})是表格里的数据来自我本机上的多次重复实验不同参数设置下具体数字会有浮动但整体趋势是稳定的。让我意外的是Nelder-Mead的表现它完全不使用导数信息仅靠单纯形在空间中伸缩翻转居然也能在两百步内收敛。这侧面说明这个函数虽然对梯度类算法不友好但它的整体地貌并不混乱没有任何误导性的局部极值干扰。BFGS和共轭梯度的表现也符合预期。它们利用近似的二阶信息或者共轭方向构造搜索方向避开了梯度下降那种“锯齿形”的尴尬路径。尤其是BFGS在Rosenbrock这种低维小规模问题上几乎能和牛顿法一较高下而它只需要一阶梯度的输入实用性更强。3.4 算法行为特征分析为什么有些方法省步数从上面的实验可以看出优化算法的核心分歧在于如何使用历史信息来构造下一步的方向。梯度下降是“近视眼”它只看到当前位置的梯度方向对于这种曲率变化剧烈的地形天然容易震荡。Adam则通过维护梯度的一阶矩和二阶矩估计对每个变量独立地调整步长相当于给每个坐标方向都配了一个“智能油门”。这确实能缓解一部分问题但Adam仍然没有显式建模变量之间的相互依赖关系所以在Rosenbrock这种强耦合问题中它的收敛速度依然受限制。牛顿法直接使用了Hessian矩阵通过求解线性方程组把梯度方向变换为“Newton方向”从几何上看它相当于用一个局部二次曲面去逼近原函数然后直接走向这个二次曲面的顶点。只要二次逼近足够准确牛顿法可以一步登天。当然代价是每一步都要计算二阶导数并求解线性方程计算开销很大。BFGS这类拟牛顿法本质上是在迭代过程中用梯度差去逐步近似Hessian的逆所以它既保留了牛顿法“方向修正”的优势又把每一步的计算成本压到了 (O(n^2)) 量级。这也是为什么它在中小规模问题上非常受欢迎。4. 测试Rosenbrock函数时绕不开的坑4.1 初始点选择哪些点会“带偏”算法Rosenbrock函数对初始点的选择非常敏感这可能是很多初学者没料到的。我自己的经验是从 ((0,0))、((1,0)) 这些离谷底不远的点出发很多算法都能轻松收敛但如果从 ((-1.2, 1)) 或者更偏的点出发梯度下降会表现出极强的“路径依赖”一旦走错方向就要在谷壁间来回折返很久。更极端的情况是从 ((1,1)) 附近但稍微偏离谷底的点出发比如 ((1.2, 1.2))。此时函数值并不大但梯度方向会把你往谷壁外推如果不加线搜索非常容易发散。我自己踩过一次用固定学习率的梯度下降从 ((1.2, 1.2)) 出发学习率设成0.005结果几步之内 (x_2) 直接冲到几千损失变成了 (10^7) 级别。这个教训告诉我们在做算法对比实验时一定要固定初始点并且最好同时报告多个初始点下的结果。否则你看到的差异可能不是算法能力的差异而是初始点对不同算法“友好程度”的差异。Rosenbrock函数设计得巧妙的地方就在于它几乎每个区域都暗藏不同的数值难度。4.2 学习率与步长策略不是越大越好也不是越小越好固定学习率在Rosenbrock函数上几乎没有活路。要么因为步长太小导致收敛极慢要么因为步长太大在谷壁处发散。我试过的策略中有两个比较可靠一个是回溯线搜索也就是Armijo条件另一个是尝试简单的步长衰减比如每次迭代乘以0.99。回溯线搜索的实现思路是每一步先尝试一个较大的步长如果目标函数没有充分下降就把步长缩小一半重复直到满足条件。这种方法能让梯度下降在Rosenbrock函数上“活下来”但代价是每一轮迭代都要多次计算函数值整体收敛速度仍然偏慢。对于Adam这类自适应优化器学习率的选择区间大概在0.001到0.01之间比较安全。低于0.001收敛速度太慢高于0.05很容易在函数的前期阶段震荡发散。我建议在测试新优化器时先固定初始点画出迭代路径图观察三维轨迹是否在谷底内再谈调参。4.3 高维推广与不同变体Rosenbrock函数并不局限于二维。在标准高维推广中常用的形式是[ f(\mathbf{x}) \sum_{i1}^{n-1} \left[ (1 - x_i)^2 100 (x_{i1} - x_i^2)^2 \right] ]这个高维版本保留了相邻变量之间的强耦合关系难度会随着维度增加快速上升。全局最小值仍然在 (x_i 1) 处函数值为0但算法很容易陷入一种“部分维度已经收敛、部分维度还在缓慢爬行”的状态。我在测试一个自制贝叶斯优化器时用了50维的Rosenbrock变体结果发现很多算法在前20维上已经接近最优但第21到第50维却停滞不前。这个现象在实际工程中有很强的类比意义很多系统参数之间存在复杂的耦合关系直接按维度独立优化往往会失败。此外也有人构造带噪声的Rosenbrock函数用来模拟真实优化场景中的不确定性。比如在函数值上加一个高斯噪声项。这种变体对算法的鲁棒性要求更高因为噪声会被梯度放大导致方向更新失真。如果要做噪声实验建议先把噪声方差控制在一个很小的范围内比如 (10^{-6}) 量级。4.4 一个关于收敛判据的提醒最后想说一个容易忽略的细节评判优化算法在Rosenbrock函数上是否收敛不能只看函数值还要看变量的误差。因为Rosenbrock函数在最优点附近的Hessian条件数极大可能会出现函数值已经很小、但变量还偏离较多的现象。我从实际实验中发现当函数值降到 (10^{-6}) 水平时如果直接打印变量坐标有时偏差仍然在 (10^{-2}) 量级。反过来如果以变量误差小于 (10^{-4}) 为收敛标准很多算法的迭代次数会大幅增加。因此在学术实验或工程验证中务必明确收敛判据是面向目标函数值还是面向变量值并且最好两者都记录。结尾几个可以继续深挖的方向这个函数我前前后后测了不知道多少次每次换一种新的优化算法我都会先在Rosenbrock函数上跑一遍。每次跑都能发现算法的一个“小性格”。比如有的算法前期冲得快但后期乏力有的算法每一步都走得稳但步数特别多还有的算法在别的函数上表现优秀一遇到Rosenbrock就“露馅”。如果看到这篇文章的你正在做优化相关的工作我建议你建立一个属于自己的基础测试集把Rosenbrock函数放进去同时搭配Sphere函数、Rastrigin函数和Ackley函数。这套组合基本能覆盖不同地形特征平滑、狭窄谷底、多峰、以及对称与非对称结构。在这个基础上你再去做算法调参和方案选型就不会被某个单一测试函数的偶然结果带跑偏。最后再分享一个亲测好用的小技巧在二维Rosenbrock函数上调试算法时不妨把每次迭代的路径点打印出来或者画成二维散点图。你很快就能直观地看到算法是在谷底平滑前进还是在谷壁之间反复横跳。很多时候代码的bug就是这样一眼看出来的。
返回列表