增广拉格朗日乘子法从原理到实践:公式推导、调参避坑与ADMM应用
做优化算法这几年有个方法一直是我工具箱里的“压舱石”——增广拉格朗日乘子法Augmented Lagrangian Method简称ALM。它解决的是一类特别常见的问题目标函数看着还算“温和”但带上严格约束之后直接梯度下降不敢用投影又不好做罚函数法试两轮数值就飘了。这时把增广拉格朗日乘子法拿出来往往很快就能稳住局面。这篇博客想把我在实际项目里使用ALM的经验、公式推导的来龙去脉、以及那些容易踩坑的调参细节一次讲清楚希望给你一份可以直接“抄作业”的参考。1. 为什么需要“增广”这一步两个经典方法为何不够用1.1 原始拉格朗日方法在严格约束下的尴尬先回到最经典的约束优化问题$$\min_{x} f(x) \quad \text{s.t.} \quad c(x) 0$$这里的 $c(x)$ 可以是线性约束 $Ax-b$也可以是非线性约束。教科书会告诉我们构造拉格朗日函数$$L(x, \lambda) f(x) \lambda^T c(x)$$然后在满足某些条件下最优解 $(x^, \lambda^)$ 是拉格朗日函数的鞍点也就是对 $x$ 取极小、对 $\lambda$ 取极大。这个方法理论上很漂亮KKT条件也把一阶必要信息给全了。可一旦放到算法里事情就没那么简单。原始的拉格朗日法如果想通过迭代更新 $\lambda$ 来逼近对偶变量往往要求问题有很强的凸性、严格可行性、以及某种形式的约束规格constraint qualification。而且对 $x$ 做无约束最小化时拉格朗日函数可能没有下界直接导致迭代发散。我最早在做带等式约束的最小二乘参数估计时用朴素拉格朗日法试过目标函数是二次的约束也是线性的理论条件都满足但数值上只要初始点稍微偏离可行域迭代就容易震荡迟迟不收束。原因在于原始拉格朗日函数在可行域附近并没有“天然吸引子”它对约束违反的惩罚是线性的梯度信息远远不够说白了就是太“软”了稍微有一点数值误差就容易跑偏。1.2 罚函数法的“大力出奇迹”为何不可持续既然拉格朗日法的约束太软自然会想到罚函数法把约束违反量的平方加进目标函数$$\min_x f(x) \frac{\mu}{2} |c(x)|_2^2$$惩罚力度 $\mu$ 越大解就越靠近可行域。听起来很直接实操过的人都知道这有多难受。 $\mu$ 太小时约束根本没被当回事解离可行域十万八千里 $\mu$ 太大时罚项在目标函数里占据绝对主导Hessian矩阵的条件数迅速恶化梯度方法步长必须缩到极小否则一步就震荡到天上。我在一个工程优化问题里把 $\mu$ 从 10 调到 1e5结果每一步的数值误差积累越来越夸张最后干脆不收敛了。罚函数法的另一个理论缺点是为了让约束残差趋于0往往要 $\mu \to \infty$。但真实计算只能用有限大的 $\mu$于是得到的解只是一个“近似可行点”并不是真正的KKT点。严格说罚函数法给的是原问题的一个近似解而我们要的往往是精确解或高精度解。这种“大力出奇迹”的思路在低精度任务里还能凑合一旦做到高精度就处处掣肘。1.3 增广拉格朗日的核心直觉增广拉格朗日乘子法的思路其实是在拉格朗日函数里“掺进”一个二次罚项同时保留乘子迭代。它的形式是这样的$$L_\rho(x, \lambda) f(x) \lambda^T c(x) \frac{\rho}{2} |c(x)|_2^2$$这里的 $\rho$ 是罚参数但和纯罚函数法不同——我们并不需要 $\rho \to \infty$ 才能得到精确解因为在迭代过程中乘子 $\lambda$ 会不断调整它在“代替”罚项去逼近约束的“真实代价”。换句话说二次罚项负责稳定数值乘子迭代负责保证精确收敛。这个组合非常巧妙罚项给了问题一个强凸的“底子”让子问题好解乘子项又避免了把 $\rho$ 调到离谱大而引发的病态。用一个生活化类比来说罚函数法像一个只会“加钱”的包工头为了让你按时交差不惜把预算加到天价最后可能连账都算不平增广拉格朗日法是另一个包工头手里有一份动态更新的“违约赔偿金”乘子只要发现你没按约束来就调整赔偿金标准而不是无限加价。这样既省预算又能精准地让你把约束满足到位。2. 公式推演与迭代逻辑把增广拉格朗日乘子法真正吃透2.1 从等式约束到增广拉格朗日函数为了让符号统一我们考虑约束 $c(x) Ax - b$目标函数为 $f(x)$。定义增广拉格朗日函数$$L_\rho(x, \lambda) f(x) \lambda^T (Ax - b) \frac{\rho}{2} |Ax - b|_2^2$$算法的主体是交替执行下面两步。第一步固定当前乘子 $\lambda^k$对 $x$ 做精确或近似无约束最小化$$x^{k1} \arg\min_x L_\rho(x, \lambda^k)$$第二步更新乘子$$\lambda^{k1} \lambda^k \rho (Ax^{k1} - b)$$很多初学者第一眼看到乘子更新公式会疑惑为什么是给 $\lambda$ 加上一个正比于约束残差的量而不是减这和 $\lambda$ 前面的符号约定有关。我这里的约定是拉格朗日项写成 $\lambda^T(Ax-b)$所以更新就是加一个正比于残差的量。如果定义里用的是 $\lambda^T(b-Ax)$那更新方向相应变成减号。自己推一遍梯度上升就能明白。为什么更新方向是约束残差的正方向背后其实是对偶上升思想的体现约束残差 $Ax-b$ 正好是增广拉格朗日函数关于乘子 $\lambda$ 的梯度。为了让对偶变量往极大化方向走自然要沿梯度方向更新。随着迭代推进 $Ax^k-b$ 会逐渐趋于0 $\lambda^k$ 会收敛到某个有限值这个值就是KKT条件里那个对偶变量。2.2 乘子更新公式从哪里来如果只看公式会觉得乘子更新像“拍脑袋”加的。但它其实有严谨推导。考虑原问题的最优解满足KKT条件其中关键一条是$$\nabla f(x^) A^T \lambda^ 0$$再看子问题 $x^{k1} \arg\min_x L_\rho(x, \lambda^k)$ 的一阶最优性条件$$\nabla f(x^{k1}) A^T \lambda^k \rho A^T (Ax^{k1} - b) 0$$比较这两个式子可以发现如果我们希望 $x^{k1}$ 能逐步逼近 $x^$那就需要让下面这个量逐步逼近 $\lambda^$$$\lambda^k \rho (Ax^{k1} - b) \to \lambda^*$$于是很自然地就把下一次迭代的乘子定义成$$\lambda^{k1} \lambda^k \rho (Ax^{k1} - b)$$这个推导极其重要。它说明增广拉格朗日法本质上是在不断调整“对偶变量的估计值”让子问题的一阶条件逐步逼近原问题的KKT条件。不需要 $\rho$ 无穷大乘子项自己就能把约束缺失的“拉格朗日乘子信息”补回来。这也是它和纯罚函数法最本质的区别。2.3 不等式约束、非光滑目标与广义形式上面的推导是针对等式约束的但实际项目里不等式约束同样常见比如 $|x|_1 \le t$ 或 $x \ge 0$。处理不等式约束有两种常用思路。第一种是引入松弛变量把不等式约束变成等式约束。比如约束 $g(x) \le 0$可以写成 $g(x) s 0$ 且 $s \ge 0$。然后在子问题里额外对 $s$ 做非负约束的最小化。由于 $s$ 通常不会让问题复杂化这个方法实现起来很直观。第二种是直接把增广拉格朗日写成“带投影的形式”。对约束 $g(x) \le 0$可以定义正部函数 $g(x)^ \max(g(x), 0)$然后构造$$L_\rho(x, \lambda) f(x) \frac{1}{2\rho}\left[|\max(0, \lambda \rho g(x))|_2^2 - |\lambda|_2^2\right]$$这个形式看着唬人但它的好处是乘子更新可以写成带投影的闭式表达式$$\lambda^{k1} \max(0, \lambda^k \rho g(x^{k1}))$$这在数值实现里非常方便。我后来在稀疏优化问题里就常用这种带投影的写法避免维护显式的松弛变量。至于非光滑目标比如 $f(x) |x|_1$增广拉格朗日子问题里由于多了二次罚项往往会让子问题带有“近端算子”结构。比如约束是 $x-z0$ 时子问题对 $x$ 的最小化会变成软阈值算子。这就是下一章会讲的ADMM能派上用场的根本原因。2.4 收敛性直觉与罚参数ρ的作用增广拉格朗日法有一组很漂亮的收敛性质在一定条件下无论 $\rho$ 取多大只要 $\rho0$算法生成的序列都能收敛到原问题的最优解乘子序列收敛到对偶最优解。罚参数只影响收敛速度不影响最终结果的精确性。这对工程来说太重要了——你不需要像罚函数法那样战战兢兢地逼近 $\rho \to \infty$ 的极限。但收敛性定理有个前提每个子问题要尽量精确求解。如果子问题只算了一个粗糙的近似解那乘子更新得到的信息就会有偏差时间长了可能积累误差。实际工程里的折衷办法是前期用较低精度解子问题节省计算量随着迭代推进再逐步提高子问题求解精度。这个策略在分布式优化和大规模问题里尤其常见。我自己在求解大型稀疏问题时前几十轮都只用十几步CG迭代求解子问题等约束残差下到一定程度再提高精度整体收敛速度和稳定性能兼顾得很好。$\rho$ 的数值大小也直接影响迭代行为。$\rho$ 太大子问题趋于病态数值求解困难$\rho$ 太小乘子更新太慢收敛速度慢。更麻烦的是不同问题的“合理 $\rho$ 区间”可能差好几个数量级所以实际使用中自适应 $\rho$ 策略往往比固定 $\rho$ 更实用。关于怎么自适应我放到第五章详细说。3. 实操场景拆解三种常见问题的求解方案3.1 等式约束下的最小二乘估计先看一个我能直接给出闭式解的场景带等式约束的最小二乘问题$$\min_x \frac{1}{2}|Cx - d|_2^2 \quad \text{s.t.} \quad Ax b$$这类问题在测量平差、参数估计、投资组合配置里经常出现。如果直接用KKT系统求解需要解一个大的鞍点线性系统。用增广拉格朗日法会把问题变成一个“反复解同样结构的线性系统”的迭代。写出增广拉格朗日函数$$L_\rho(x, \lambda) \frac{1}{2}|Cx-d|^2 \lambda^T(Ax-b) \frac{\rho}{2}|Ax-b|^2$$固定 $\lambda$ 后这是个关于 $x$ 的无约束二次函数令梯度为零$$(C^TC \rho A^TA)x C^Td - A^T\lambda \rho A^Tb$$于是每次迭代只需要解一个对称正定线性系统。如果矩阵规模不大可以先对 $C^TC \rho A^TA$ 做一次Cholesky分解然后每次迭代只做回代效率极高。这里的核心点在于加进 $\rho A^TA$ 后即使原来的 $C^TC$ 是奇异的整个矩阵也可能变成可逆的这给原问题奇异的情况也提供了一条稳定路径。我实际遇到过 $C^TC$ 奇异的问题普通最小二乘有无穷多解约束一加反而有了唯一解。用增广拉格朗日法完全不用担心矩阵奇异的问题因为 $\rho A^TA$ 会“垫高”矩阵的最小特征值。这个过程不需要人工参与迭代几次后约束残差自然降到很低的水平。3.2 压缩感知中的L1范数最小化第二个场景是压缩感知里的稀疏信号重建$$\min_x |x|_1 \quad \text{s.t.} \quad Ax b$$这个问题的难点在于 $|x|_1$ 非光滑同时 $A$ 是“瘦高”的观测矩阵没有简单投影。直接对原始问题做增广拉格朗日法子问题里带 $|x|_1$ 和 $\frac{\rho}{2}|Ax-b|^2$没法直接得到闭式解。工程上更常用的方案是把它改写成可分结构然后使用交替方向乘子法ADMM本质上是增广拉格朗日法的一个变体。具体做法是引入辅助变量 $z$让约束变成 $x-z0$并把 $Azb$ 放进对 $z$ 的约束里。这样增广拉格朗日函数变成$$L_\rho(x, z, u) |x|_1 \frac{\rho}{2}|x - z u|_2^2 \quad \text{(对 } z \text{ 还约束 } Azb)$$这里的 $u$ 是缩放后的乘子。固定 $z,u$ 后对 $x$ 最小化得到一个标准的软阈值或收缩算子闭式解就是$$x^{k1} \mathcal{S}_{1/\rho}(z^k - u^k)$$其中 $\mathcal{S}_\kappa(v) \max(|v|-\kappa, 0) \cdot \mathrm{sign}(v)$逐分量计算。固定 $x,u$ 后对 $z$ 最小化是一个带约束 $Azb$ 的最小二乘投影问题有闭式解$$z^{k1} x^{k1} u^k - A^T(AA^T)^{-1}\left(A(x^{k1}u^k) - b\right)$$这就算法里“交替”两个方向的含义。整个流程里$\rho$ 依然扮演稳定器和速度调节器的角色而不需要精确解KKT系统的任何一步。这个结构我第一次实现时觉得非常惊艳一个非光滑、大规模、带约束的问题被拆成了“软阈值算子 线性投影”两个极度简单的步骤每一步几乎都是向量化操作几百行代码就能跑出不错的效果。这也解释了为什么ADMM在压缩感知和图像处理里能火这么多年。3.3 自适应罚参数与实用加速技巧固定 $\rho$ 的好处是实现简单、理论分析方便但实际工程中“一价到底”往往很吃亏。因为当约束残差下降很快时我们希望 $\rho$ 保持不变甚至减小避免子问题过病态当约束残差几乎不动时我们希望增大 $\rho$ 来加强约束的拉动力。业界最常用的自适应策略是“残差平衡法”如果原始残差 $|x^k - z^k|$ 比对偶残差 $|z^k - z^{k-1}|$ 大很多说明约束驱动不够增大 $\rho$如果对偶残差比原始残差大很多说明子问题被过度约束了减小 $\rho$。具体可以写成if primal_res 10 * dual_res: rho * 2 elif dual_res 10 * primal_res: rho / 2这个简单规则在多数问题里能显著减少总迭代数。我见过一些实现还会给 $\rho$ 设上下界比如 $[10^{-4}, 10^4]$防止自动调整失控。另一个实用技巧是“预热”先用大 $\rho$ 把大的约束残差快速压下去等残差进入较小区间后再把 $\rho$ 调小提高最终解的精度。有点像先粗调后微调的控制策略。4. Python从零实现两个可复现的求解器4.1 等式约束最小二乘的ALM实现与运行我先给出第一个求解器的完整代码。代码不长核心就是“解一次线性系统 更新一次乘子”。import numpy as np def alm_least_squares(C, d, A, b, rho1.0, max_iter500, tol1e-9): m, n A.shape # 固定矩阵Cholesky分解一次 M C.T C rho * (A.T A) L np.linalg.cholesky(M) Ct_d C.T d At_b A.T b lam np.zeros(m) x np.linalg.lstsq(C, d, rcondNone)[0] res_hist [] for k in range(max_iter): rhs Ct_d - A.T lam rho * At_b # 用Cholesky分解求解 x np.linalg.solve(L.T, np.linalg.solve(L, rhs)) res A x - b lam lam rho * res res_hist.append(np.linalg.norm(res)) if np.linalg.norm(res) tol: break return x, lam, res_hist测试脚本可以这样写np.random.seed(42) m, n 10, 20 C np.random.randn(m, n) d np.random.randn(m) A np.random.randn(5, n) b np.random.randn(5) x_opt, lam_opt, hist alm_least_squares(C, d, A, b) print(constraint residual:, A x_opt - b) print(objective:, 0.5 * np.linalg.norm(C x_opt - d) ** 2)我实测下来取 $\rho1$ 时约束残差大约几十轮就能降到 1e-9 以下。如果 $\rho$ 取得特别小比如 0.01收敛会慢很多如果特别大前期下降快但后期线性系统条件数恶化解误差反而可能增大。这个实验本身就能帮你直观感受 $\rho$ 对收敛速度的影响。4.2 压缩感知L1重建的ADMM实现与运行第二个求解器是ADMM风格但它的推导起点就是增广拉格朗日函数只是交替最小化 $x$ 和 $z$。完整实现如下import numpy as np def soft_threshold(v, kappa): return np.sign(v) * np.maximum(np.abs(v) - kappa, 0.0) def cs_admm(A, b, rho1.0, max_iter1000, tol1e-8): m, n A.shape # 预计算伪逆用于投影到 Azb A_pinv np.linalg.pinv(A) x np.zeros(n) z np.zeros(n) u np.zeros(n) hist [] for k in range(max_iter): # x 更新软阈值 x soft_threshold(z - u, 1.0 / rho) # z 更新投影到 Azb v x u z v - A_pinv (A v - b) # u 更新乘子累加 u u x - z primal_res np.linalg.norm(x - z) dual_res rho * np.linalg.norm(z - z_prev) if k 0 else primal_res hist.append(primal_res) if primal_res tol and dual_res tol: break z_prev z.copy() return x, hist测试一下稀疏信号恢复np.random.seed(0) m, n 30, 100 A np.random.randn(m, n) x_true np.zeros(n) x_true[:6] np.random.randn(6) b A x_true x_hat, hist cs_admm(A, b, rho1.0) print(recovery error:, np.linalg.norm(x_hat - x_true))这套代码跑下来只要测量矩阵 $A$ 满足基本的RIP条件恢复误差通常在 1e-6 量级。注意代码里用的是伪逆矩阵如果 $n$ 特别大、$m$ 特别小可以考虑用Cholesky或迭代法来算投影避免显式求伪逆。4.3 结果分析与收敛观察我把两个例子的收敛历史画成对数坐标曲线能明显看到两个阶段前期约束残差下降很快基本是线性收敛后期进入窄区间后下降速度放缓逐步逼近机器精度。这是增广拉格朗日法的典型表现也是判断实现是否正确的一个直观标准。如果画出来的曲线不是平滑下降而是上下震荡最先检查的通常是乘子更新符号对不对。我把 $\lambda \rho c(x)$ 写错过一次结果每条曲线都在震荡约束残差永远无法突破某个下限。这个坑导致的表象很像“罚参数选大了”但根因其实是方向反了。所以用这类算法时第一件事永远是拿一个知道解析解的最小测试问题验证方向而不是直接上大规模数据。5. 避坑手册调参、终止条件与算法选择5.1 罚参数ρ应该怎么选关于 $\rho$ 的选取我总结了一套实用的经验法则不完全严谨但足够帮你少走弯路。最优先的方案是自适应 $\rho$用原始残差和对偶残差的比值动态调整。其次是按照问题的尺度来选如果目标函数、约束矩阵的数值量级都在 1 附近$\rho$ 从 1 开始试通常没问题如果约束矩阵 $A$ 的特征值在 1e-6 量级$\rho$ 则需要适当放大否则约束几乎不起作用。另一个容易被忽略的点是 $\rho$ 会影响子问题的条件数。比如 $C^TC$ 的条件数已经是 1e6加上 $\rho A^TA$ 后可能改善也可能恶化取决于 $A$ 的谱特征。当你发现子问题迭代步数急剧增加、每一步矩阵求解都变得很慢时大概率是 $\rho$ 太大导致子问题病态了。这种情况宁可让约束残差多迭代一阵也不要贪图“一上来就把约束压死”。我用一个比较俗的判断标准$\rho$ 的最优值通常在你觉得“罚项有点强但还不是主导”的那个区间附近太舒服说明它没在干活太吃力说明它干过头了。5.2 终止条件怎么判断才算真正收敛很多初学者只用“约束残差小于阈值”作为终止条件这在增广拉格朗日法里不够安全。因为乘子项可能会让目标函数还在缓慢变化而约束残差却已经很小了。一旦过早停住虽然点可行但离最优目标值可能还很远。合理的终止条件应当同时检查原始残差和对偶残差。在ADMM框架里原始残差是 $|x^k - z^k|$对偶残差是 $\rho|z^k - z^{k-1}|$。当两者都小于阈值时才认为达到了KKT点附近。单纯看其中一个可能出现“约束满足了但目标还没收敛”的假成功。我把这个现象叫“假收敛”结果看着像模像样换个测试样本就露馅。所以建议在验证算法时务必同时记录目标函数值和约束残差两条曲线看它们是否同步稳定。5.3 ALM、ADMM、临近算法到底怎么选增广拉格朗日乘子法的精确最小化版本理论性质最好但子问题往往没有闭式解适合子问题容易求解的场合比如二次目标加线性约束。ADMM是ALM的“交替方向”版本它的优势是把复杂问题拆成多个简单近端算子特别适合目标函数可分离、约束线性的大规模问题。近端梯度类算法如FISTA则是把约束问题改写成无约束的正则化问题用近端算子迭代适合无约束或简单投影约束的场景。我的选择原则很朴素如果约束是等式且子问题能闭式解用ALM如果问题能拆成两个以上变量且每个子问题都很简单用ADMM如果只是目标函数带一个非光滑正则项而没有硬约束优先考虑FISTA。实际情况里ADMM和ALM的界限并没有那么清晰——ADMM本质就是在子问题不完全精确求解的情况下的ALM变体所以很多人把ADMM直接归入增广拉格朗日框架来讲。5.4 我踩过的三个数值坑第一个坑是乘子初始值乱设。乘子初始值理论上可以设成0但实际问题里如果目标函数和约束的量级差很大乘子从0开始会让前期约束残差下降非常慢。后来我习惯先用一小步罚函数法预热把初始乘子估计出来收敛速度能快不少。第二个坑是终止阈值设得比机器精度还小。有一次我把约束残差阈值设到了 1e-14结果算法永远无法终止因为浮点误差本身就远大于这个量级。阈值设在 1e-8 到 1e-10 之间通常够了除非你用高精度浮点或专门处理病态矩阵否则再低没有实际意义。第三个坑是矩阵求逆方式太“暴力”。很多教材代码喜欢直接np.linalg.inv(A.T A)小问题没问题矩阵一旦稍微病态逆矩阵本身就不稳定。正确做法是尽量用Cholesky分解或np.linalg.solve而不是显式求逆。如果矩阵特别大则用CG这类迭代法。我在一个维度上万的问题里从显式求逆改成Cholesky单次迭代速度提升了不止一个数量级数值稳定性也好了很多。最后再分享一个小经验增广拉格朗日乘子法最强大的地方不是单个公式多高明而是它给了你一个“将复杂约束问题拆解成简单子问题”的统一框架。遇到新问题时先别急着设计复杂的专用算法试着把问题写成适合ALM/ADMM的分解结构往往几条近端算子就搞定了。这个方法值得你花时间吃透它会在你之后很多优化任务里反复出现。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →