老实说,很多学优化理论的人,前几章都在“看”,到了第四章才真正开始“算”。前面那些凸集、凸函数、对偶理论,更像是在给一张地图绘制疆域边界;到了无约束凸优化这一章,地图上才真正出现了可以走的路。这一章要解决的事情很简单:给定一个可微凸函数 f(x),找它的全局极小点。但实际做起来,步长怎么定、停止条件怎么设、数据要不要归一化、用一阶还是二阶信息,每一个细节都能让收敛结果天差地别。
这篇文章我想从“为什么第四章如此关键”开始,把最优性条件、梯度下降、牛顿法、拟牛顿法、坐标下降与近端梯度这几个核心方法串在一起,再补上我在实际调参和落地时踩过的坑。适合正在读凸优化教材、但又想把理论落到代码里的人,也适合那些已经在用各类优化接口、却不太明白内部在干什么的算法工程师。读完之后你会发现,无约束凸优化不是一堆孤立算法的堆砌,而是一套“按问题特征选工具”的决策框架。
1. 无约束凸优化解决什么问题:先理解这章在整本书里的位置
1.1 为什么几乎所有模型最终都会走到无约束形式
很多同学会有一个疑问:现实中不是到处都有约束吗?比如资源分配要满足总量限制,投资组合要满足比例约束,为什么第四章偏偏要专门研究“无约束”情形?
我的理解是:无约束凸优化是整个优化工具箱里最底层的那把改锥。大量带约束的问题,在求解过程中都会被反复转化为无约束子问题来处理。举个例子,带等式约束的凸优化问题可以用拉格朗日对偶转化,对偶函数通常是内层无约束极小化加外层调整乘子的结构;内点法处理不等式约束时,每一步都要对数障碍函数做无约束极小化。再比如支持向量机带不等式约束,但对偶问题配合正则项之后,实际操作层面人们常常直接去解一个含 L2 正则的合页损失,而这个目标函数就是无约束的。
所以这一章学扎实了,后面讲约束优化、内点法、对偶分解时,你才不会觉得每一步都像新知识。你会意识到:约束问题难的不是“怎么求无约束极小”,而是“怎么把约束消掉,再设计一轮合理的无约束迭代”。
1.2 凸性给我们的特权:一阶条件既是必要也是充分
无约束凸优化在数学上最漂亮的一点,就是最优性条件极其简单。假设 f 是可微凸函数,那么 x* 是全局极小点,当且仅当
∇f(x*) = 0
这个结论在非凸问题里完全不成立。非凸函数梯度为零的点可能是局部极小值、局部极大值,也可能是鞍点。比如单变量函数 f(x) = x³,在 x=0 处导数等于零,但那里既不是极小点也不是极大点。凸性把这个麻烦整体消掉了,因为凸函数的割线始终在函数值上方,任意一点满足切线水平,就意味着它不比任何其他点差。
我建议你在理解这个条件时,不要只记公式,而要把它当作一个“搜索目标”。既然最优点的充要条件是梯度为零,那么所有无约束凸优化算法在做的事情,本质上都是“不断构造一个使梯度范数递减的迭代序列”。梯度下降沿着负梯度走,牛顿法解梯度为零的线性近似,都是这一思路的具体化。想通了这点,后面所有算法对你来说就不再是孤立技巧,而是围绕同一个核心目标展开的不同路径。
1.3 评价算法好坏的两把尺子:收敛率与单轮成本
学这一章时,我强烈建议你脑子里常备两把尺子:收敛率的阶数,以及单次迭代的计算成本。
收敛率刻画的是“误差缩小的速度”。如果迭代到第 k 步得到的点与最优点的距离为 e_k,那么线性收敛意味着 e_{k+1} 与 e_k 的比值趋近于一个小于 1 的常数;超线性收敛是指这个比值本身在趋近于零;二阶收敛则意味着误差从 ε 变成大约 ε²,极小点附近几步就精确到机器精度了。
但收敛率快不代表算法好,因为每步付出的代价完全不一样。梯度下降每步只算一次梯度,复杂度与变量维度 n 成正比;牛顿法每步要算 Hessian 并求解一个 n 阶线性方程组,单轮成本可能高达 O(n³),当 n 很大时几乎不可接受。这一章所有算法的对比,本质上都是在“收敛需要的迭代次数”和“每步多少钱”之间做权衡。你把这个框架建立起来,以后选择算法就像选交通工具:高铁快但站点少,步行慢但哪都能去——关键看你的起点终点和对时间的要求。
2. 梯度下降法的步长策略与收敛速度那些坑
2.1 梯度方向为什么是对的:三种步长选择的定位
梯度下降是这一章第一个登场的算法,公式简单到一句话就能说完:从某个初始点出发,每一步沿负梯度方向走一小步,直到梯度范数足够小。写成迭代就是
x_{k+1} = x_k − α_k ∇f(x_k)
方向选负梯度在直觉上很好理解:函数值下降最快的局部方向就是负梯度。但真正决定梯度下降成败的,是步长 α_k 的选择。这一步选不好,轻则收敛慢,重则直接发散。
常见的步长策略有三种。第一种是固定步长,全程用一个 α 不调整,数学上要求 α 不超过目标函数梯度 Lipschitz 常数的倒数才能保证收敛,但实际操作中这个常数往往只能靠经验估,设大了就震荡甚至爆炸。第二种是精确线搜索,每步都去求一个一维问题的最优步长,这一步本身要花不少计算量,除非目标函数结构特殊,否则并不划算。第三种是回溯线搜索,从一个稍大的步长开始,不断缩小直到满足下降条件。工程上第三种最常用,原因很简单:它在“每步多花几次函数评估”和“避免无效大步长”之间取得了很好的平衡。
2.2 Armijo 回溯线搜索的参数与实现细节
回溯线搜索最常用的判据是 Armijo 条件,也就是要求新的函数值比当前函数值下降一个与“梯度方向上的线性预测”成比例的量。翻译成人话:如果这个步长走完,函数值的实际下降没有达到线性模型预测下降的一小部分,就说明步长太大了,需要缩小。
具体来说,Armijo 条件写作 f(x + αd) ≤ f(x) + c·α·∇f(x)ᵀd,其中 d 是下降方向,c 通常取 1e-4 这样很小的值。实现时从 α=1 开始,每次缩小为原来的 β 倍,β 常取 0.5 到 0.8 之间,直到 Armijo 条件成立。
我实际用下来的体会是:c 别设太大,否则条件太宽松,算法会接受一个“下降得勉强可以接受”的大步长,迭代节奏不对;β 取 0.5 比较稳,取 0.8 收敛速度更快但偶尔要更多次回退。另外要注意,如果目标函数梯度范数本身很大,从 α=1 开始回退会浪费不少函数评估,这种情况下初始步长先除以梯度范数的量级,能省去很多无谓的计算。这个坑我踩过不止一次:数据集没归一化,梯度动辄上千,Armijo 从 1 开始疯一样回退,白白烧掉一堆算力。
2.3 条件数导致的“之字形”问题和补救手段
梯度下降有一个非常经典的反面案例:目标函数是椭圆抛物面,比如 f(x) = 0.5·(x₁² + 100x₂²)。这个函数的等高线是扁长椭圆,梯度方向在椭圆长轴和短轴之间来回摆动,迭代轨迹呈现出明显的“之字形”,每一步都在靠近最优解,但都走得歪歪扭扭。理论上它的收敛速度由最陡与最缓方向的曲率比值决定,也就是条件数 κ。
用最小二乘问题来看更直观。假设目标是最小化 ||AX − b||²,那么问题等价于一个二次型,其最优收敛速度取决于 AᵀA 的最大与最小特征值之比。如果原始特征之间的尺度差异巨大,这个比值可能大到几百甚至上万,梯度下降就会慢到让人怀疑人生。
补救措施有三条路。一是对特征做归一化,让每个维度的尺度差不多,条件数自然降下来;二是换用牛顿法,它天然对条件数不敏感;三是用下一节要讲的拟牛顿法,近似地获得类似效果。我个人的建议是:做任何优化之前,先对数据做标准化,这一步几乎永远能带来免费的收敛速度提升。
3. 牛顿法的本地模型思维:快,但需要条件
3.1 牛顿方向是怎么从二次模型推导出来的
如果说梯度下降是“用一阶信息指路”,那牛顿法就是“用二阶信息画出局部的完整地形图”。出发点非常朴素:在当前位置 x 附近,把目标函数做二阶泰勒展开
f(x+d) ≈ f(x) + ∇f(x)ᵀd + 0.5·dᵀ∇²f(x)d
这个近似模型是变量 d 的二次函数。对它求极小点,令导数等于零,就得到
∇f(x) + ∇²f(x)d = 0,也就是 d = −∇²f(x)⁻¹∇f(x)
这个方向就是牛顿方向。它不是在任意意义上“猜出来的方向”,而是在当前位置的局部二次模型里,理论上的最优下降方向。所以牛顿法每一步都在做一件事:“用二次模型预测最优点在哪里,然后直接朝预测的最优点走过去。”
3.2 局部二阶收敛与仿射不变性
牛顿法最惊艳的特性是局部二阶收敛:当迭代点已经进入最优点的某个邻域时,每一步的误差大约会平方缩小。比如当前误差是 0.01,下一步就可能变成 0.0001,再下一步变成 1e-8。这意味着只要顺利进入收敛域,牛顿法只需要很少几步就能达到很高的精度。
另一个常被忽略但很重要的性质是仿射不变性。如果你对变量做一个可逆线性变换,比如 x = Ty,那么牛顿法的迭代路径经过逆变换后保持不变。梯度下降没有这个性质,因为梯度方向会随着坐标系的选择而改变。这个差异直接解释了为什么牛顿法对条件数不敏感:它本质上是利用曲率信息,在每一步都偷偷地对坐标轴做了一次重新标定。
3.3 阻尼牛顿法与步长修正
理论上的牛顿法默认步长为 1,但在实际中直接这么跑并不稳妥。如果起始点离最优点较远,二阶近似在全局范围内并不准确,一步走 1 可能直接越过好区域,导致函数值不降反升。所以工程上普遍采用“阻尼牛顿法”:方向仍然取牛顿方向,但真实步长由线搜索决定。
这就像把牛顿法当成一个方向更聪明的梯度下降来用。方向由二阶信息决定,步长由一维线搜索兜底。我在调试某类逻辑回归模型时就发现,纯牛顿步长偶尔会让目标函数值反复横跳,加上回溯线搜索之后,整体迭代步数增加了约一成,但稳定性好了很多,最终收敛时间反而更短。
3.4 Hessian 求逆的正确姿势与数值细节
这里必须强调一个很多人会犯的错误:牛顿法的每一步需要的是解线性方程组 ∇²f(x)d = −∇f(x),而不是显式地计算 Hessian 的逆矩阵。直接求逆在数值上既慢又不稳定,还会放大舍入误差。正确的做法是每次迭代对 Hessian 做一次 Cholesky 分解(正定情形)或对称不定分解,然后解两个三角方程组,就能得到方向。
从成本角度看,牛顿法每步的复杂度主要来自两部分:构造 Hessian 需要 O(n²) 的存储和计算,解线性方程组需要 O(n³) 的分解代价。当变量维度上千时,这个成本已经很可观;维度上万甚至百万时,直接牛顿法几乎无法落地。这也是为什么工程界更常用拟牛顿法——它试图用更便宜的手段,靠近牛顿法的收敛性能。
4. 工程最爱的拟牛顿法:不心疼 Hessian,也照样接近牛顿
4.1 从曲率数据里“学习”Hessian
拟牛顿法的核心思想说穿了很简单:不直接计算 Hessian,而是在迭代过程中,用相邻两次迭代点的梯度差和位置差,一步步“逼近”真正的曲率信息。
定义 s_k = x_{k+1} − x_k,y_k = ∇f(x_{k+1}) − ∇f(x_k)。如果函数是二次的,那么 Taylor 展开会告诉我们 y_k 应该等于 Hessian 乘以 s_k,这个关系被称为割线条件。拟牛顿法就是让构造出的近似矩阵满足这一条件,同时保持对称正定。从几何上看,它是在用最近一步的真实曲率观测值,去修正之前对地形曲率的估计。这有点像你闭着眼睛走路,通过脚底的坡度变化,不断修正对前方地形的想象。
4.2 BFGS 更新公式与算法骨架
所有拟牛顿法里,BFGS 是实践中口碑最好的一种。它的更新公式看起来有点吓人:
B_{k+1} = B_k + (y_k y_kᵀ)/(y_kᵀ s_k) − (B_k s_k s_kᵀ B_k)/(s_kᵀ B_k s_k)
但拆开看,第一项是原近似矩阵,第二项在“沿最新方向补上实测的曲率”,第三项在“把旧方向上的错误曲率抹掉”。整体上,BFGS 既吸收了新信息,又保持了矩阵正定性。
算法骨架是一个循环:计算搜索方向 d = −B⁻¹∇f;做线搜索确定步长 α;更新位置;更新 s 和 y;更新 B 的近似。过程中需要维护一个 n×n 的矩阵,复杂度是 O(n²) 每步,比牛顿法的 O(n³) 便宜不少,同时收敛速度在实践中通常远好于梯度下降。
4.3 L-BFGS:把记忆长度压到只需 m 对向量
BFGS 虽然好,n 很大的时候存储一个稠密近似矩阵仍然不可接受。L-BFGS 属于有限记忆版本:它不存储完整的矩阵,只保存最近 m 轮的 (s, y) 向量对,并用一种高效的递归算法去模拟与“近似矩阵之逆”相乘的过程。
m 通常取 5 到 20,默认 10 已经能覆盖绝大多数情况的收敛需求。理论直觉是,在局部收敛域里,真正影响方向的曲率信息高度集中在最近几步;太老的曲率数据反而会干扰当前方向的准确性。我在实践中的体会是:L-BFGS 对步长不敏感,鲁棒性强,几乎不需要太多调参,适合做各种机器学习目标函数的默认优化器。有一点可以留意:当目标函数在迭代中途发生明显跳变(比如学习率调度切换、动态采样切换)时,最好清空记忆重新累积,否则旧曲率信息会“带偏”方向。
5. 坐标下降与近端梯度:把“不可微”也收进来
5.1 坐标下降的循环思路与适用条件
坐标下降延续了一个更朴素的直觉:既然高维问题难,那就每次只处理一维。具体做法是,在一个循环里,依次固定其他所有坐标,只对当前坐标做精确最小化,然后更新这个坐标的值,再换下一个坐标。
这个算法听起来过于简单,但在很多问题上有惊人的效率。前提是“固定其他坐标后,单维极小化”必须足够便宜。比如二次型目标函数对单坐标求导后,直接能得到闭式解;再比如 L1 正则化下的线性模型,单维问题同样有解析结果。矩阵分解、稀疏字典学习这些场景里,坐标下降几乎成了标准解法。
有一点需要特别注意:坐标下降对非光滑问题不保证收敛到全局最优,但如果是光滑的、可分的凸目标,收敛性是有保障的。它和梯度下降的关系有点像“逐个旋钮反复调”和“整体方向一起动”的区别,在结构可分的优化问题中往往表现更好。
5.2 近端梯度:处理不可微正则项的现代套路
实际应用中,很多目标函数会长这样:min g(x) + h(x),其中 g 是光滑可微的,比如平方损失;h 是不可微的,但结构简单,比如 L1 范数。逻辑回归加 L1 正则、LASSO、压缩感知,都是这一类问题的代表。
对这种目标直接算梯度是行不通的,因为 h 在零点不可导。近端梯度法的做法很优雅:先对光滑部分走一步梯度下降,然后施加一个“近端映射”来修正不可微部分的影响。迭代公式可以统一写成
x_{k+1} = prox_{αh}(x_k − α∇g(x_k))
其中 prox 是近端算子。它本身也是一个优化问题,但要求 h 结构足够简单,能快速求解。对于 L1 范数,近端算子恰好就是软阈值操作:把每个分量收缩到零附近,幅度超过阈值的保留,低于阈值的清零。这也解释了为什么很多稀疏优化代码里,核心循环就是“一步梯度更新加一个软阈值函数”,简单到惊人。
5.3 试验中常用的加速策略与停止标准
直接跑近端梯度收敛偏慢,实际使用中几乎都会配上 Nesterov 型的外推加速,原理就是在迭代序列上再加一层动量修正。我自己在求解 Lasso 问题时,就用过形如“先梯度更新,再做软阈值,再按固定系数做外推”的循环,几十行 Python 代码即可完成,却能达到与其收敛复杂度对应的不错效果。
关于停止标准,一个可落地的判据是同时看原始残差和对偶间隙。对偶间隙是原问题目标值与某个可行对偶下界之间的差值,当它降到足够小时,可以保证现有点已经接近最优。只看函数值差容易误判,尤其是当函数在最优附近很平缓时,函数值几乎不动但解还差得很远。我习惯的做法是,设置两个条件:梯度范数小于阈值,或者对偶间隙相对值小于 1e-6。谁先满足谁触发终止,这样既保险又高效。
6. 算法选型决策:面对实际问题时我怎么做
6.1 一张表做选型:从场景到方法
很多初学者拿到一个优化问题,第一反应是“用梯度下降”。但实际工作中,正确的做法是先看问题的结构,再选工具。下面这张表是我平时做决策的基本框架:
| 方法 | 适用条件 | 单轮成本 | 收敛速度 | 主要风险 |
|---|---|---|---|---|
| 梯度下降 + 回溯线搜索 | 任何可微凸问题,维度很高 | O(n) | 线性(依赖条件数) | 病态条件数下收敛极慢 |
| 牛顿法 | 维度中等,Hessian 可求且可解 | O(n²) 存储,O(n³) 分解 | 局部二阶 | 存储和计算成本太高 |
| 阻尼牛顿法 | 维度中等,起点可能离最优远 | O(n³) | 全局稳健,局部二阶 | 仍需完整 Hessian |
| BFGS | 维度几千以内 | O(n²) | 超线性 | 存储 n² 矩阵 |
| L-BFGS | 维度几十万到几百万 | O(n) | 超线性(实践强) | 对非光滑目标不太适用 |
| 坐标下降 | 单坐标优化有闭式解 | O(n) 每轮 | 线性或更快 | 非平滑时可能不收敛 |
| 近端梯度 | g 光滑 + h 不可微可分离 | O(n) | 依赖条件数 | 需要会求 prox 算子 |
我实际选择的顺序通常是:如果目标是光滑的,先试 L-BFGS;如果有明显的 L1 惩罚项,直接进近端梯度框架;如果维度不大且 Hessian 结构清晰,才考虑阻尼牛顿法。梯度下降在现代工具箱里反而是“兜底”的存在,因为 L-BFGS 在绝大多数光滑问题上都做得更好。
6.2 实操经验:先归一化,再谈收敛
有一个几乎不需要成本、但能显著改善所有一阶方法表现的技巧:先对特征做归一化,再跑优化。
原因在前面的条件数讨论里已经很清晰了。假设你要拟合一个带截距的线性模型,其中一个特征取值在 0.001 到 0.01 之间,另一个特征取值在 100 到 10000 之间,那么目标函数在不同方向上的曲率可能相差几个数量级。这种情况下,梯度下降的收敛速度会被最慢的那个方向拖死;L-BFGS 虽然对尺度没那么敏感,但归一化后依然能减少不必要的内部校正。
我在实际调试中见过一些很典型的现象:样本量不大、特征维度也不高的问题,梯度下降跑了几百轮没下降,归一化之后几十轮就收敛了。这类问题不是算法本身错了,而是你把一个条件数极差的问题硬塞给了对条件数敏感的算法。做优化前的数据预处理,既是建模常识,也是优化实践的必须步骤。
6.3 停止条件与常见误区清单
最后梳理几个在工程调试中最常见的误区。第一个误区是只看函数值差而不看梯度范数。函数值在最优值附近变化很小,但解可能还差很远,尤其是在目标函数极其平缓的方向上。用梯度范数作为停止条件,更能反映“是否还能继续下降”。
第二个误区是盲目套默认参数。不同优化器的默认容差、最大迭代次数、线搜索参数,都是针对平均场景调出来的,真遇到条件数很差或者尺度差异很大的问题时,默认值经常不顶用。我的做法是:先用少量迭代跑一条下降曲线,观察函数值的变化形态,再决定是调步长、调容差,还是换算法。
第三个误区是忽略“问题的结构”。同样是解一个带 L1 正则的模型,你可以用次梯度方法硬推,也可以用近端梯度轻松处理;同样是训练一个逻辑回归,你可以手动实现梯度下降,但换 L-BFGS 几行代码就能稳定收得更快。先看清楚目标函数的光滑性、可分性、维度规模,再决定工具箱里取哪把刀,这才是无约束凸优化这一章真正想让读者带走的思维方式。
就我个人体会来说,第四章的价值不在于背下几个算法的迭代公式,而在于建立起“问题结构—收敛特性—计算成本”三者之间的映射能力。我最早也是从梯度下降一路顺手写过来的,后来系统地补了牛顿法和拟牛顿法的原理,才意识到自己以前很多“调不动”的问题,其实换一个算法就立刻顺畅了。如果只能给一条建议:在你决定自己写优化器之前,先确认一下手头的问题到底适合哪类方法,这比调任何超参数都能更快地带来收敛上的质变。