1. 为什么需要增广拉格朗日函数法——从两种经典方法的缺陷说起
先聊个实际场景。假设你要解决一个带等式约束的优化问题,比如在重建算法、压缩感知或者机器学习模型训练里经常碰到的:
[ \min_{x} f(x) \quad \text{s.t.} \quad Ax = b ]
这类问题乍看不复杂,但真正上手去解的时候,很多人会先在“罚函数法”和“拉格朗日乘子法”之间纠结。我早年做图像复原时也犯过糊涂,试了一圈才明白,这两种方法各有各的毛病,而增广拉格朗日函数法恰好把两者的优点捏在了一起。
1.1 拉格朗日乘子法的天然缺陷
拉格朗日乘子法的思路很直接:把约束塞进目标函数,定义
[ L(x, \lambda) = f(x) + \lambda^T(Ax - b) ]
然后通过求解对偶问题来逼近原问题的最优解。理论很漂亮,但落地的时候问题就来了。算法需要交替更新 (x) 和乘子 (\lambda),而更新 (\lambda) 的过程本质上是一个梯度上升的过程。如果原函数 (f(x)) 不是严格凸的,或者约束矩阵 (A) 的性质不好,这个对偶上升过程就会像没吃饭的驴子一样,走得极其缓慢,甚至直接原地打转。
我记得早期做实验时,用一个简单线性约束去测拉格朗日乘子法,迭代了几百步,残差还在那里晃悠。原因在于普通拉格朗日函数对偶问题的可微性太差,梯度信息不够强,收敛性在非凸场景下几乎没有保障。
1.2 罚函数法的两难困境
后来大家又想到罚函数法:把违反约束的代价以平方项的形式加进目标函数
[ \min_x f(x) + \frac{\rho}{2}|Ax - b|_2^2 ]
这个方法直观、好实现,只要把 (\rho) 取足够大,求出来的解就能很好地满足约束。听起来挺美,但只要你动手试一次就会明白问题在哪——(\rho) 一大,整个目标函数的海森矩阵条件数会直线飙升,达到 (O(\rho)) 的程度。这意味着你用梯度下降也好,拟牛顿法也好,迭代步长都得压到非常小才能保证稳定,收敛速度慢到怀疑人生。
不去大 (\rho) 呢?那你得到的解约束误差又太大,根本不满足原问题的要求。这就是典型的“顾头不顾腚”:罚函数法只能在“满足约束”和“数值稳定”之间二选一,没法兼得。
1.3 增广拉格朗日函数法如何“缝合”两者
增广拉格朗日函数法的优雅之处在于,它把两个方法揉成了一个:
[ \mathcal{L}_\rho(x, \lambda) = f(x) + \lambda^T(Ax - b) + \frac{\rho}{2}|Ax - b|_2^2 ]
看到没,它既保留了拉格朗日乘子项 (\lambda^T(Ax - b)),又加了罚项 (\frac{\rho}{2}|Ax - b|_2^2)。双管齐下之后,原来两个方法的问题居然都被解决了。
为什么?关键在于乘子项的引入让罚参数 (\rho) 不再需要趋向无穷大。即使 (\rho) 维持在一个温和的数值(比如 1 或者 10),只要随着迭代不断更新乘子 (\lambda),罚项的偏移量就会自动调整,最终也能精确满足约束。 (\rho) 不大的话,海森矩阵的条件数就不会爆炸,数值稳定性自然就有了保障。
用个不太严谨但容易理解的说法:罚项负责“把解拉向可行域”,乘子项负责“记住还差多少没拉到位”,两者配合,用有限的罚强度达到了无限罚的效果。这就是增广拉格朗日法的核心直觉——乘子项会在迭代中持续积累约束误差的信息,等效于一个会自动增加偏移量的动态罚函数。
我记得第一次亲手实现这个算法时,看到它在同样的 (\rho) 下比纯罚函数法的收敛速度快了一个数量级,那种感觉确实有点震撼。这方法最早可以追溯到 Hestenes、Powell 等人上世纪六十年代的工作,几十年过去,到了今天依然是约束优化领域不可绕过的基石。
2. 增广拉格朗日函数法的核心原理与数学推导
上一节说了这方法“大概怎么回事”,但真要用它解决问题,光有上面的直觉还不够。这一节我把数学细节掰开揉碎,把增广拉格朗日函数法从推导到算法流程完整过一遍,保证你理解了之后再也不怕这类问题。
2.1 从对偶函数角度看增广拉格朗日的优势
先回到普通拉格朗日函数 (L(x, \lambda))。它的对偶函数是:
[ g(\lambda) = \inf_x L(x, \lambda) ]
很多问题里 (g(\lambda)) 的梯度不好求,或者根本没有显式表达式,导致乘子更新受阻。增广拉格朗日函数对应的对偶函数则是:
[ g_\rho(\lambda) = \inf_x \mathcal{L}_\rho(x, \lambda) ]
关键在于,这个 (g_\rho(\lambda)) 在很一般的条件下就是可微的,而且它的梯度有非常干净的表达式。根据 Danskin 定理:
[ \nabla g_\rho(\lambda) = Ax^* - b ]
其中 (x^* = \arg\min_x \mathcal{L}_\rho(x, \lambda))。这意味着什么?意味着你想更新乘子的话,梯度信息直接写脸上了——在每一步算出当前最优的 (x) 之后,乘子沿约束残差的方向更新就行。
这就是为什么增广拉格朗日函数法比普通拉格朗日乘子法好用得多。普通拉格朗日乘子法的对偶问题存在光滑性不足,无法保证简单迭代能收敛;增广拉格朗日函数法则相当于给对偶函数做了一次“正则化”,让它的性质变得友好很多。
2.2 乘子更新公式的推导过程
推导乘子更新公式,最直接的办法是借助“对偶上升法”。对偶上升的思路是沿着对偶函数的梯度方向上升:
[ \lambda_{k+1} = \lambda_k + \alpha_k \nabla g_\rho(\lambda_k) ]
把梯度表达式代进去,再取步长 (\alpha_k = \rho),就得到了经典的乘子更新公式:
[ \lambda_{k+1} = \lambda_k + \rho(Ax_{k+1} - b) ]
这个 (\rho) 作为步长的选择不是随便拍的,它有一个很精妙的自适应效应。我讲个直觉:假设当前点离开约束面还有偏差 (r = Ax - b),那么这个偏差会在下一轮迭代中通过罚项反馈到 (x) 的求解中,而乘子更新后又会把新的偏差信息继续传递下去,形成“反馈回路”。步长恰好等于罚参数时,这个反馈回路等效于让罚函数曲线的“零点”自动往可行方向移动,这就是精确收敛的根源。
可以从另一个角度验证这个步长选择的合理性。假设在某一步我们恰好得到了约束问题的最优解 (x^) 和最优乘子 (\lambda^),那么 (\lambda_{k+1} = \lambda_k + \rho(Ax_{k+1} - b) = \lambda^* + \rho \cdot 0 = \lambda^*),乘子不动了,系统保持稳定。这说明 (\rho) 作为步长时,任何“固定点”必定对应原问题的最优 KKT 点,不存在多余的偏差。
2.3 算法主循环与流程框架
整合前面的讨论,增广拉格朗日函数法处理等式约束问题的完整算法流程是标准的三步循环:
固定当前乘子 (\lambda_k),求解增广拉格朗日函数的最小化问题: [ x_{k+1} = \arg\min_x \left( f(x) + \lambda_k^T(Ax - b) + \frac{\rho}{2}|Ax - b|_2^2 \right) ]
用约束残差更新乘子: [ \lambda_{k+1} = \lambda_k + \rho(Ax_{k+1} - b) ]
检查收敛条件,若不满足则回到第 1 步。
注意,这个流程写起来简单,但第 1 步里那个 (x)-最小化问题本身往往并不平凡。它是个无约束优化问题没错,甚至可以是非光滑的、高度非线性的,你需要根据 (f(x)) 的特点选择合适的子问题求解器。这部分的实际处理技巧,我放到第 3 节细讲。
对不等式约束 (g(x) \leq 0) 的情况,处理手法稍有不同。常见做法是引入非负乘子并在更新时做投影:
[ \mu_{k+1} = \max\left(0, \mu_k + \rho g(x_{k+1})\right) ]
实测下来,这种“投影步”实现简单,收敛效果也不错。我做稀疏优化时处理非负约束经常用这个套路,省去了很多额外的麻烦。
2.4 收敛性结论与直觉解释
增广拉格朗日函数法的收敛性不必像我早年那样用最笨的办法做几百次实验去“实测”。它对凸问题的收敛性是理论上有保证的:只要原问题满足 Slater 条件,(f) 是闭凸函数,增广拉格朗日函数法产生的迭代序列就收敛到原问题的最优解和最优乘子。
收敛速度方面,在强凸加线性的条件组合下可以达到线性收敛,而且收敛速率常数可以精确刻画。有一点实操经验可以分享:收敛速度与 (\rho) 的选取直接相关,越大的 (\rho) 通常收敛越快,但稳定性会下降。这种“鱼与熊掌”关系后面第 3 节有专门讨论,如何动态调整 (\rho) 是算法使用中最考验功力的部分。
另外值得特别强调的一点是:增广拉格朗日函数法对 (f(x)) 非凸的情况,实际表现也远比理论预期好。我见过不少深度学习和低秩矩阵恢复的文献都直接在非凸目标上用这个方法,实验效果相当不错。理论分析最近几年才开始严格证明这些场景的收敛性,但实践早就跑在前面了。如果你在非凸问题里碰到它,不用太慌,一般试试无妨。
3. 增广拉格朗日函数法的关键实操细节
理论搞清楚了,接下来才是真正的“杀手级经验”部分——怎么把算法调得又快又稳。这些细节绝大多数教科书不写,但恰恰是你在实际项目里绕不开的坎。我按自己踩坑的顺序一个个说。
3.1 罚参数的选择与动态调整策略
罚参数 (\rho) 的选择有多重要?打个比方,它就像赛车悬挂系统的阻尼——太软,车身颠簸(数值震荡甚至发散);太硬,转弯笨拙(收敛太慢)。初值的经验选择是 (\rho_0 \in [1, 10]),但对于不同尺度的问题,需要根据实际残差动态调整。
更实用的策略是“自适应调节”。具体做法是:在每次乘子更新后计算原始残差和对偶残差:
[ r_k = Ax_k - b, \quad s_k = \rho A^T(x_{k+1} - x_k) ]
然后按下面的规则动态调整 (\rho):
- 如果 (|r_k|_2 > \mu |s_k|_2)(原始残差过大),则增大 (\rho),比如 (\rho \leftarrow \rho \times 2);
- 如果 (|s_k|_2 > \mu |r_k|_2)(对偶残差过大),则减小 (\rho),比如 (\rho \leftarrow \rho / 2);
- 否则保持不变。
这里的 (\mu) 一般取 10 或者 5。这个策略的思想是让原始残差和对偶残差尽可能保持同一水平,避免一方收敛太快而另一方拖后腿。我实际测试里,用了自适应调节之后,很多问题能比定值 (\rho) 快两三倍。
注意,(\rho) 调整步伐不要太大,翻倍或减半就够了。贪心地把 (\rho) 从 1 直接调到 1000 往往适得其反,造成子问题求解器需要大量迭代才能适应新参数,反倒拖慢整体收敛。
3.2 终止条件的正确设置方式
终止条件看起来是小事,但设置不对,要么算法早停导致结果不准,要么迟迟停不下来浪费时间。标准做法是同时检查原始残差和对偶残差两个指标:
[ |r_k|2 \le \varepsilon{\text{prim}}, \quad |s_k|2 \le \varepsilon{\text{dual}} ]
阈值一般取绝对容差和相对容差的结合:
[ \varepsilon_{\text{prim}} = \sqrt{p} \varepsilon_{\text{abs}} + \varepsilon_{\text{rel}} \max{|Ax_k|_2, |b|_2, |r_0|_2} ]
[ \varepsilon_{\text{dual}} = \sqrt{n} \varepsilon_{\text{abs}} + \varepsilon_{\text{rel}} |A^T \lambda_k|_2 ]
其中 (p) 是约束个数,(n) 是变量维数,(\varepsilon_{\text{abs}}) 常取 (10^{-6}) 到 (10^{-4}),(\varepsilon_{\text{rel}}) 常取 (10^{-4}) 到 (10^{-2})。当然,如果你的需求对精度要求不高,放大相对容差会换来更快的迭代终止。
我自己常用的经验法则是把终止条件设置成“两个指标按比例同时收紧”。只盯一个指标很容易被骗——比如原始残差收敛了,但乘子还在慢慢飘,此时停止的结果虽然满足约束,却不一定是最优点。
3.3 子问题求解的实用技巧
增广拉格朗日法的每次外层迭代都要解一个不带约束的优化子问题,这一步往往占据整个算法 90% 以上的计算量。要提高整体效率,关键就在子问题求解上。
这个子问题的目标函数是:
[ f(x) + \frac{\rho}{2}\left|Ax - b + \frac{\lambda_k}{\rho}\right|_2^2 + \text{const} ]
其中常数项不影响最优解。于是你会发现一个特别好的性质:子问题的目标函数只是原目标加上一个二次正则项。很多情况下这个结构能帮你大幅简化解法。
具体举几个例子:
- 如果 (f(x)) 是二次的,那么整个子问题就是一个线性最小二乘问题,可以直接用共轭梯度法求解,甚至直接求闭式解(如果矩阵尺寸允许);
- 如果 (f(x)) 是 (\ell_1) 范数,子问题就变成了一个“最小二乘 + (\ell_1)”的经典 Lasso 形式,可以用近端梯度法(ISTA/FISTA)高效求解;
- 如果 (f(x)) 是指示函数(即硬约束),子问题就退化成一个投影问题。
子问题的求解精度也有讲究。我见过很多人一上来就要求子问题精确解到 (10^{-12}),这其实没有必要——外层迭代初期,乘子离最优值还很远,子问题解得太准纯属浪费算力。一种工程上常用的做法是:前几次外层迭代用比较粗糙的子问题求解精度,比如残差降到 (10^{-3}) 就够了;等乘子接近最优时再逐步收紧精度到 (10^{-6}) 或更高。这种方法叫“不精确求解”,听着不上台面,实际用起来效率提升非常明显。
一个非常值得记的教训:如果子问题求解器本身是迭代式的(比如梯度下降),那么初始化时要用上一次外层迭代得到的 (x_k) 作为热启动点。这比从零开始快得多,尤其是当 (x) 的维度很高时,热启动几乎可以省掉 70% 以上的子问题迭代次数。
3.4 两个常见的变体:乘子更新步长与缩放形式
前面说的乘子更新公式是 (\lambda_{k+1} = \lambda_k + \rho(Ax_{k+1} - b)) 的标准形式。但实际使用中,有时我们会把增广拉格朗日函数整理成“缩放形式”。定义缩放乘子 (u = \lambda / \rho),则增广拉格朗日函数可以写成:
[ \mathcal{L}_\rho(x, u) = f(x) + \frac{\rho}{2}\left|Ax - b + u\right|_2^2 - \frac{\rho}{2}|u|_2^2 ]
乘子更新变为:
[ u_{k+1} = u_k + (Ax_{k+1} - b) ]
这个形式简洁很多,少了一个 (\rho) 的乘法,实现起来更干净。我在写 ADMM 相关代码时基本都用这种缩放形式,代码可读性更好,调试也更方便。
乘子更新步长还有另一种更一般的变化:(\lambda_{k+1} = \lambda_k + \alpha \rho(Ax_{k+1} - b)),其中 (\alpha > 1) 被称为过松弛技术,(\alpha \in [1, 2]) 在一些凸问题上能明显加速收敛。不过这招不是万能的,(\alpha) 太大会导致稳定性问题。
4. 实际应用场景与典型案例拆解
理论讲完了,实操经验也给了,那这方法到底能在哪些地方派上用场?这一节我用几个实际的应用场景来展示增广拉格朗日函数法的威力。每个例子我都会交代清楚问题的结构、为什么选这个方法,以及最终的效果。
4.1 从增广拉格朗日到 ADMM 的演进
提到增广拉格朗日函数法,就绕不开 ADMM(交替方向乘子法)。实际上,ADMM 就是增广拉格朗日函数法处理“可分离目标函数”问题时的一个自然变体。
考虑如下目标可分离的问题:
[ \min_{x, z} f(x) + g(z) \quad \text{s.t.} \quad Ax + Bz = c ]
如果用标准增广拉格朗日函数法,子问题需要同时优化 (x) 和 (z),两个变量耦合在一起,可能并不比原问题好解。ADMM 的聪明之处在于——它不联合优化,而是交替更新 (x) 和 (z):
[ x_{k+1} = \arg\min_x \mathcal{L}_\rho(x, z_k, \lambda_k) ]
[ z_{k+1} = \arg\min_z \mathcal{L}\rho(x{k+1}, z, \lambda_k) ]
[ \lambda_{k+1} = \lambda_k + \rho(Ax_{k+1} + Bz_{k+1} - c) ]
这样拆开之后,每一步的子问题往往都有高效的专用解法,甚至闭式解。这是我非常推荐的一种模式——每个子问题都有各自的“物理结构”,相应地可以用最合适的手段求解。
ADMM 这些年火得一塌糊涂,压缩感知、图像去噪、矩阵分解、分布式优化等大量领域都能看到它的身影。而它的根基就是增广拉格朗日函数法,理解了源头,后面这些变体学起来会轻松得多。
4.2 分布式优化与共识问题
分布式优化是增广拉格朗日函数法最重要的现实落地方向之一。先说说共识问题是什么:你有一批计算节点,每个节点手握一部分数据和一个局部目标函数 (f_i(x)),大家的目标是共同求出一个全局变量 (x),使得总和 (\sum_{i=1}^N f_i(x)) 最小。约束是每个节点各自持有的局部变量必须相等:
[ \min_{x_1, \dots, x_N} \sum_{i=1}^N f_i(x_i) \quad \text{s.t.} \quad x_1 = x_2 = \cdots = x_N ]
这个问题直接用全局增广拉格朗日函数法,每次子问题迭代都要用到所有节点的数据,那就成“集中式”了,失去了分布式的意义。更聪明的做法是引入全局变量 (z),把问题重新写成:
[ \min_{x_1, \dots, x_N, z} \sum_{i=1}^N f_i(x_i) \quad \text{s.t.} \quad x_i = z, ; i = 1, \dots, N ]
然后用 ADMM 来处理。这时候,每个节点的子问题只依赖自己的局部数据:
[ x_i^{k+1} = \arg\min_{x_i} \left( f_i(x_i) + \frac{\rho}{2}|x_i - z^k + u_i^k|_2^2 \right) ]
而全局变量 (z) 的更新只需要做一个简单的平均:
[ z^{k+1} = \frac{1}{N} \sum_{i=1}^N (x_i^{k+1} + u_i^k) ]
这里面的“通信”只发生在收集局部结果和分发全局平均这两步。每个节点根本不需要把自己的数据传给别人,在现代联邦学习等隐私敏感场景下,这个性质的吸引力是压倒性的。我搭过几个小规模的分布式训练原型,用这个方法做参数同步,效果比传统的同步梯度下降稳定很多,也更容易处理通信延迟。
4.3 机器学习与信号处理中的典型应用
下面挑几个我实际碰过、且特别适合用增广拉格朗日函数法处理的经典问题来说。
压缩感知 / 稀疏重构。这类问题的核心是:
[ \min_x |x|_1 \quad \text{s.t.} \quad Ax = b ]
增广拉格朗日函数法的子问题就是 Lasso 形式,可以用软阈值算子一步搞定:
[ x^{k+1} = \mathcal{S}_{\frac{1}{\rho}}\left(x^k - \frac{1}{\rho}A^T(Ax^k - b + u^k)\right) ]
这里的 (\mathcal{S}\kappa(\cdot)) 是软阈值算子(soft-thresholding),定义是 (\mathcal{S}\kappa(v) = \text{sign}(v) \max(|v| - \kappa, 0))。整个算法迭代非常快,几步就能出不错的重建结果。我自己当年在稀疏信号恢复实验里用过这个流程,几百个变量的规模,几十轮迭代就收敛到了不错的精度,比单纯用通用优化包快很多。
图像去模糊 / 去噪。经典的图像复原问题形如:
[ \min_x \frac{1}{2}|Kx - y|_2^2 + \lambda |\nabla x|_1 ]
其中 (K) 是模糊核,(\nabla) 是梯度算子,第二项是总变差正则项。用增广拉格朗日函数法或 ADMM 处理这类问题几乎成了标准做法。子问题里含有 (K^TK) 的矩阵求逆,在循环边界条件下可以用 FFT 快速完成,整体效率非常高。我在测试图像上跑过,一张 512×512 的灰度图,几十次迭代就能达到视觉效果很好的复原结果。
低秩矩阵恢复。我最近一段时间接触比较多的场景是低秩矩阵补全:
[ \min_X |X|* \quad \text{s.t.} \quad X\Omega = M_\Omega ]
其中 (|X|_*) 是核范数(奇异值之和),(\Omega) 是已知元素的集合。增广拉格朗日函数法的子问题需要对矩阵做奇异值分解然后做软阈值收缩,即奇异值阈值算子(SVT)。虽然每次 SVD 都有不小的计算量,但整体收敛所需的迭代次数并不多,实际用起来比直接解原问题高效得多。
这些例子有一个共同规律:原问题的棘手之处往往在于“目标函数不光滑”或“约束复杂”,而增广拉格朗日函数法把难题拆解成若干步,每一步都有快速可靠的算子可用。这就是为什么它在工程界如此受欢迎。
5. 常见问题与排查经验实录
这一节全是干货。下面这些问题,几乎每一个都是我在实际项目中真金白银踩出来的,有的甚至困扰了我好几天。你提前看一遍,能避开的坑就没必要再踩一次。
5.1 数值不稳定与发散问题
表现:算法跑着跑着残差突然飙升,乘子数值变得特别大,甚至出现 NaN。
这种问题的罪魁祸首通常有三个,按出现频率排序:
第一,(\rho) 设置过大。尤其是初始阶段,如果 (\rho) 太大,子问题的解会被罚项强力拉扯,导致乘子更新步长也过大,形成正反馈震荡。解决办法很简单:把 (\rho) 调回较小的值,或者使用自适应调整,先让它小一点,再根据情况逐步变大。
第二,子问题求解不精确且没有热启动。每次外层迭代都从零开始解子问题,意味着子问题的初始点离真实解太远,迭代次数不够时就带着很大的误差进入乘子更新,误差逐层累积,最终炸掉。解决办法是引入热启动机制,并且对子问题的精度要求做一个“由粗到细”的规划。
第三,问题本身的条件数太差。比如矩阵 (A) 是病态的,或者 (f(x)) 含有特别陡峭的项。这种情况下,可以考虑对变量做预处理,比如用对角缩放。我在信号处理里遇到过 (A) 的条件数达到 (10^8) 级别的问题,做了简单的对角缩放之后,数值稳定性明显提升。
5.2 收敛极其缓慢,半天挪不动一步
表现:迭代次数已经很多了,但原始残差或目标函数值的变化幅度非常微小,算法像被钉住了一样。
最常见的原因是 (\rho) 太小,导致约束满足得非常慢。你可以观察一下乘子更新的幅度:如果每次 (\lambda) 的变化特别小,说明罚的力度不够,模型正在慢慢“蹭”向可行域。此时直接把 (\rho) 往上调一个数量级,往往立竿见影。
另一个容易忽视的原因是子问题精度设置得太低。如果子问题只求到很低精度就交给外层,那乘子更新携带的有效信息其实很少,整个算法就处于一种“假迭代”状态。我建议在残差下降到接近目标容差时,把子问题的精度也同步收紧,一般问题一两个数量级的收紧就能明显感受到收敛速度变化。
还有一种是“目标函数过于平坦”的问题,比如某些退化情况下的低秩矩阵恢复。这种情况下,不管你怎么调参数,收敛速度都很难快起来。我遇到过几次,最终还是回到模型本身——换一种等价形式或者增加正则项,效果反而更好。有时候问题不是算法不行,而是模型本身就缺乏足够的曲率信息。
5.3 参数调节的独家经验
这里我分享几个多年来沉淀下来的调参经验,不一定在教科书里出现,但实操中非常有用:
让 (\rho) 和问题尺度匹配。具体做法是,先把问题中的 (A) 和 (b) 做归一化处理,让 (|A|) 的尺度在 1 附近,然后再取 (\rho) 在 1 到 10 之间。尺度不匹配是新手最容易掉进去的坑。
用“残差曲线”而不是“目标函数值”来判断收敛。目标函数值在迭代后期变化本来就不大,区分度不高;反而是残差的对数坐标图,能让你一眼看出算法是线性收敛还是停滞了。我在调试时基本都画 log-log 或者 semilog 图,信息量完全不一样。
自适应调 (\rho) 的频率不要太勤。每轮都调 (\rho) 会让算法行为变得很“毛躁”,我一般每 5 到 10 轮才检查一次并做出调整,整体更稳定,实际收敛也更快。
乘子初始值用 0 就够了,但别在中间手动改乘子值。你可能觉得残差一直不减,于是手动重置乘子清零,这个操作会丢掉之前积累的信息,让算法从头再来,得不偿失。
5.4 实战问题排查速查表
我整理了一张排查表,方便你遇到问题时快速定位:
| 症状 | 可能原因 | 推荐操作 |
|---|---|---|
| 残差震荡上升,甚至 NaN | (\rho) 太大或子问题不精确 | 降低 (\rho),提高子问题精度 |
| 收敛极慢,乘子变化小 | (\rho) 太小 | 增大 (\rho) 一个数量级 |
| 原始残差已达标但对偶残差不降 | 子问题精度不够 | 收紧子问题终止阈值 |
| 前期快后期突然“卡住” | 问题本身非凸或病态 | 尝试预处理,或将终止容差适度放宽 |
| 不同初始值结果差异大 | 目标函数非凸,算法陷入局部最优 | 多起点运行或增加正则项 |
| 分布式场景通信开销大 | 每轮迭代间通信频繁 | 增加本地子问题迭代次数再通信 |
这张表基本覆盖了我这些年使用增广拉格朗日函数法和 ADMM 过程中遇到的大部分问题。
最后再分享一个小技巧:在实际编码时,把增广拉格朗日函数、乘子更新、残差计算三个模块严格分开写。这样一方面方便调试,另一方面你可以在不改变算法主体的情况下,轻松替换不同的子问题求解器。我自己的优化算法库里,外层框架基本很少动,内层的子问题求解器换来换去,适应性极好。这套思维也推荐给你——通用框架 + 专用内核,是工程实践里最稳的打法。