简介:这份MATLAB代码资源围绕增广拉格朗日乘子法(ALM)展开,面向机器学习与数值优化方向的学习者和研究者,用于求解带约束的优化问题,尤其适合低秩矩阵恢复、稀疏表示、推荐系统等场景的入门与复现。压缩包共106个文件,约749KB,以46个m脚本为主,辅以c源码、mex系列编译文件、mat数据、txt与doc说明文档,覆盖算法主框架、变量更新、乘子与惩罚参数调整等模块,便于直接运行与二次修改。资源已有7196人学习下载,热度较高。通过阅读与调试,读者可掌握ALM从拉格朗日函数构造、惩罚参数ρ递增到收敛判定的完整流程,理解参数选择与初始解对求解效率的影响,并借助附带的C与MEX文件观察底层数值实现,为将ALM迁移到更广泛的约束优化任务提供可复用的代码基础与排错参考。
1. 增广拉格朗日乘子法:为什么罚函数越调越崩,它却能稳住
很多人第一次用罚函数法解带约束的优化问题时,都会经历同一个崩溃瞬间:罚因子调小了,约束根本压不住,解直接飘到可行域外面;罚因子调大了,目标函数的 Hessian 条件数爆炸,数值解开始震荡甚至发散。你反复调那个罚参数,像在走钢丝,两边都是悬崖。增广拉格朗日乘子法(Augmented Lagrangian Method,ALM)解决的正是这个两难——它把拉格朗日乘子估计和二次罚项绑在一起迭代,让罚因子不需要趋于无穷大就能收敛到约束最优解。换句话说,ALM 不是"更好的罚函数",而是把对偶变量也当成未知量一起更新,用乘子去"吸收"约束违反量,罚项只负责兜底。搞懂它,你就理解了从罚函数到对偶上升、再到 ADMM 这一整条约束优化算法演进的主线,也能看懂为什么机器学习里的分布式训练、矩阵分解、稀疏编码背后反复出现它的影子。这份 MATLAB 代码资源,正是把这条主线落成了可运行、可调试的实现。
2. 从罚函数到增广拉格朗日:约束到底是怎么被"消化"掉的
2.1 罚函数法的死穴:罚因子与数值稳定性的正面对撞
先看最朴素的等式约束问题:min f(x) s.t. h(x)=0。罚函数法的思路是把约束违反量平方后加到目标上,构造 P(x,σ) = f(x) + (σ/2)·‖h(x)‖²,然后对 x 做无约束极小化。直觉上,σ 越大,违反约束的代价越高,解就越接近可行域。
问题出在"越大越好"这个直觉上。当 σ → ∞ 时,P 的 Hessian 中来自罚项的部分是 σ·∇hᵀ∇h,它的特征值随 σ 线性增长,而来自 f 的部分不变。两者的比值就是条件数,σ 每翻十倍,条件数大致也翻十倍。无约束求解器在病态 Hessian 上要么收敛极慢,要么直接数值崩溃。更麻烦的是,有限 σ 下解永远不在可行域上——你只能无限逼近,永远到不了。
这就是血泪经验里那句"罚因子调到怀疑人生"的来源。它不是一个调参技巧问题,而是罚函数法结构上的缺陷:约束违反量的惩罚和目标函数的曲率被强行耦合在同一个尺度上。
2.2 拉格朗日对偶:把约束"价格"显式建模
拉格朗日函数换了个角度:L(x,λ) = f(x) + λᵀh(x)。这里 λ 是乘子,可以理解成"违反约束的边际价格"。对偶函数定义为 g(λ) = min_x L(x,λ),对偶问题就是 max_λ g(λ)。
对偶上升法(dual ascent)交替做两件事:先固定 λ 对 x 最小化 L,再沿梯度方向更新 λ ← λ + α·h(x)。它的优雅之处在于,当强对偶成立时,对偶问题的最优 λ* 恰好让 ∇_x L = 0 的解落在可行域上。约束不是被"罚"掉的,而是被"定价"消化的。
但纯对偶上升有个硬伤:g(λ) 未必可微,也未必强凹,步长 α 稍大就震荡,稍小就爬不动。而且它要求内层 min_x L 能精确求解,实际中往往做不到。
2.3 增广拉格朗日的核心构造:乘子 + 罚项的双轨制
ALM 把前两者的优点缝在一起,构造增广拉格朗日函数:
L_σ(x,λ) = f(x) + λᵀh(x) + (σ/2)·‖h(x)‖²
注意它同时含有一阶的 λᵀh 和二阶的 (σ/2)‖h‖²。迭代分两步:
- 固定 λ 和 σ,对 x 最小化 L_σ;
- 更新乘子 λ ← λ + σ·h(x)。
关键洞察在这里:乘子更新用的步长正好是 σ,而 σ 又是罚项系数。当内层最小化做到 ∇_x L_σ = 0 时,有 ∇f + (λ + σh)ᵀ∇h = 0。对比最优性条件 ∇f + λᵀ∇h = 0,可以看出 λ + σh 正在逼近真正的 λ。也就是说,罚项产生的约束违反量 σh 被自动折算进了乘子估计里。
这带来一个直接后果:σ 不需要趋于无穷大。只要 λ 迭代收敛到 λ*,即使 σ 是个中等大小的常数,h(x) 也会趋于零。罚因子从"必须无限大"降级成了"够用就行",条件数问题随之缓解。这就是 ALM 能稳住数值的根本原因。
2.4 收敛判据与停止条件的设计逻辑
ALM 的停止条件通常看两个量:约束违反范数 ‖h(x)‖ 和对偶残差 ‖λ_{k+1} - λ_k‖。前者衡量可行性,后者衡量乘子是否稳定。常见的实现会设两个容差 tol_feas 和 tol_dual,两者同时满足才停。
这里有个容易翻车的地方:如果只看 ‖h(x)‖ 小就停,可能 λ 还在剧烈变化,解其实没稳定;如果只看 λ 变化小就停,可能约束根本没满足。两个判据缺一不可。代码里一般还会加最大迭代次数和 σ 的自适应放大策略——当约束违反量下降太慢时,把 σ 乘一个放大因子(比如 2 或 10),但设一个上限,避免重新掉进罚函数法的坑。
3. 源码级拆解:ALM 主循环、乘子更新与子问题求解
3.1 主循环骨架:外层乘子迭代怎么组织
下面这段是 ALM 主循环的典型结构,对应资源里最核心的那部分逻辑。参数命名沿用常见的 MATLAB 习惯,方便对照。
function [x, lambda, history] = alm_solve(f, h, x0, lambda0, sigma0, ... max_iter, tol_feas, tol_dual, sigma_max) % f : 目标函数句柄,返回标量 % h : 约束函数句柄,返回列向量 % x0 : 初始点 % lambda0: 初始乘子估计 % sigma0 : 初始罚因子 % sigma_max: 罚因子上限,防止条件数失控 x = x0; lambda = lambda0; sigma = sigma0; history = struct('x', [], 'lambda', [], 'sigma', [], 'feas', [], 'dual', []); for k = 1:max_iter % 内层:固定 lambda 和 sigma,最小化增广拉格朗日函数 aug_obj = @(x) f(x) + lambda' * h(x) + (sigma/2) * norm(h(x))^2; x_new = unconstrained_min(aug_obj, x); % 乘子更新:步长就是 sigma h_val = h(x_new); lambda_new = lambda + sigma * h_val; % 残差计算 feas_res = norm(h_val); dual_res = norm(lambda_new - lambda); % 记录历史,便于后续画收敛曲线 history.x(:, end+1) = x_new; history.lambda(:, end+1) = lambda_new; history.sigma(end+1) = sigma; history.feas(end+1) = feas_res; history.dual(end+1) = dual_res; % 停止判据:可行性和对偶稳定性同时满足 if feas_res < tol_feas && dual_res < tol_dual x = x_new; lambda = lambda_new; break; end % 自适应放大罚因子:约束下降太慢时加码,但不超过上限 if feas_res > 0.25 * (history.feas(max(1,end-1))) sigma = min(sigma * 2, sigma_max); end x = x_new; lambda = lambda_new; end end这段代码里有三个设计点值得展开。第一,内层调用unconstrained_min是一个黑匣子,实际资源里可能用拟牛顿法(BFGS)、梯度下降或共轭梯度,取决于问题规模。第二,乘子更新lambda + sigma * h_val的步长不是随便选的,它来自对偶上升的梯度步长与罚项系数的等价推导,改成别的值会破坏收敛性保证。第三,罚因子放大条件用的是"约束违反量下降不足上一轮的 1/4",这是一个经验阈值,放大太频繁会让内层子问题变病态,放大太慢则外层迭代次数暴涨。
3.2 内层子问题:无约束求解器的选择与容差控制
内层子问题 min_x L_σ(x,λ) 是 ALM 的计算瓶颈。资源里通常提供两种求解路径:小规模问题用 BFGS,大规模稀疏问题用梯度类方法。这里的关键参数是内层容差——它不能设得比外层收敛要求还松,否则乘子更新基于的是一个没解准的 x,外层会假收敛。
常见的实现思路是让内层容差随外层迭代逐步收紧,比如第 k 次外层迭代的内层容差取 min(0.1, ‖h(x_k)‖) 之类的自适应形式。这样早期外层不需要精确解,省算力;后期约束快满足时,内层精度跟上,保证乘子估计准确。
function x = unconstrained_min(obj, x0) % 用 BFGS 做无约束极小化,返回近似最优解 opts = optimset('GradObj', 'off', 'MaxIter', 200, ... 'TolFun', 1e-8, 'Display', 'off'); x = fminunc(obj, x0, opts); end注意TolFun设成 1e-8 是一个偏紧的值,适合中小规模问题。如果目标函数每次求值都很贵(比如涉及大规模矩阵运算),这个容差要放宽到 1e-4 到 1e-6 之间,否则内层迭代会吃掉绝大部分时间。参数怎么改,取决于你的 f 和 h 的单次求值成本,以及你对最终精度的要求。
3.3 乘子初始值与罚因子初值的选取策略
λ0 和 σ0 的选取直接影响收敛速度,但不影响最终收敛性(凸问题下)。实践中 λ0 常取零向量,σ0 取 1 或者根据问题尺度估计。如果 h 的量纲很大,σ0 要相应调小,否则初始罚项会主导目标函数,内层第一步就跑到约束面上,反而让乘子更新失去信息。
一个稳妥的做法是先跑几步纯对偶上升,用得到的 λ 作为 ALM 的 λ0。资源里如果提供了 warm-start 接口,就是干这个用的。对于序列化求解的场景(比如每次数据更新后重新求解),用上一次的 λ 和 x 做热启动,能把迭代次数压到个位数。
3.4 收敛曲线怎么读:可行性残差与对偶残差的博弈
把 history.feas 和 history.dual 画在同一个对数坐标图上,你能看到一条典型的 ALM 收敛曲线:可行性残差单调下降,对偶残差先降后升再降,中间那个"鼓包"出现在罚因子放大之后。如果对偶残差持续不降,说明 σ 放大太激进或者内层没解准;如果可行性残差卡在某个平台不动,说明 σ 上限设得太低,约束压不下去。
这两条曲线的交叉点附近,通常就是停止条件该触发的位置。读图比读日志快得多,这也是为什么资源里要保留 history 结构——它不只是调试用的,更是判断算法有没有走对路的仪表盘。
4. 避坑与排查:ALM 实现里最容易翻车的五个地方
4.1 现象:外层迭代几十次,约束违反量几乎不动
原因:罚因子 σ 初值太小,且放大条件没触发。如果 h(x) 的尺度本身很小(比如约束是 x1+x2-0.001=0),σ=1 时罚项贡献微乎其微,内层解几乎不受约束影响,乘子更新也推不动。
解决:先算一下初始点处的 ‖h(x0)‖,让 σ0 大致取 1/‖h(x0)‖ 的量级。或者在主循环里加一个判断:如果前三次迭代 feas_res 下降不到 10%,直接强制放大 σ。
4.2 现象:内层求解器报"达到最大迭代次数",解的质量很差
原因:罚因子放大后,增广拉格朗日函数的 Hessian 条件数变差,BFGS 的拟牛顿方向失效。或者内层容差设得太紧,求解器在病态曲面上反复横跳。
解决:给内层设一个迭代上限,同时监控内层退出标志。如果内层没收敛,不要直接用它的输出更新乘子,而是先把 σ 调小一档重试。资源里如果返回了内层的 exitflag,一定要检查。
4.3 现象:乘子 λ 数值爆炸,最终解完全不可用
原因:乘子更新步长用了 σ,而 σ 被放大到了很大的值,同时 h(x) 没有同步缩小,导致 λ 一步跳太远。这在非凸问题或约束不相容时尤其常见。
解决:给 λ 的更新加一个阻尼,比如 λ ← λ + β·σ·h(x),β 取 0.5 到 1 之间。或者设一个 λ 的范数上限,超过就截断。注意这会牺牲一点收敛速度,但能防止数值溢出。
4.4 现象:换了组数据,同样的参数完全不收敛
原因:ALM 的收敛行为对问题尺度敏感。目标函数值域、约束量纲、变量量级变了,σ0 和容差都得跟着调。把一组参数当万能钥匙是常见的误用。
解决:在求解前做归一化——把变量缩放到单位量级,约束除以它的典型尺度。资源里如果有预处理步骤,别跳过。归一化之后,σ0=1、tol=1e-6 这套默认值通常能覆盖大部分问题。
4.5 现象:约束满足了,但目标函数值比预期差很多
原因:ALM 收敛到的是可行域上的驻点,不一定是全局最优。非凸问题下,初始点和乘子初值决定了你落在哪个局部解。另外,如果停止条件只看可行性,可能在一个目标函数还很差的点就停了。
解决:多组初始点跑一遍,取目标值最好的。停止条件里加一项目标函数变化量,确保它也稳定了。对于有理论保证的凸问题,这个现象不该出现,如果出现了,检查 h 的 Jacobian 是否写错。
5. 设计取舍:为什么 ALM 不追求精确解,以及它和 ADMM 的分岔点
ALM 最反直觉的设计,是它允许内层子问题"差不多就行"。传统优化思维里,子问题当然要解到高精度,否则外层基于一个不准的解更新乘子,岂不是在错误的方向上迭代?但 ALM 的收敛理论恰恰证明,内层不精确反而可能是好事——早期外层离最优解远,花大力气精确求解一个错误的子问题纯属浪费;后期接近最优时再收紧内层容差,总算力反而更省。这是一种"粗调快跑、细调慢收"的节奏控制,和罚函数法一根筋地加大 σ 是完全不同的哲学。
这个取舍也解释了 ALM 和 ADMM 的分岔点。ADMM 可以看成 ALM 在变量可分离结构上的特化:当 f 和 h 能拆成两块、且每块单独求解很容易时,ADMM 用交替方向的方式把 ALM 的内层子问题进一步拆解,代价是收敛速度可能从超线性降到线性,但每步计算量大幅下降。如果你的问题没有可分离结构,硬套 ADMM 反而比直接 ALM 慢。选哪个,取决于子问题的可分解性,而不是哪个名字更流行。
验证一个 ALM 实现对不对,我一般会构造一个已知解析解的等式约束二次规划:min ½xᵀQx + cᵀx s.t. Ax=b,它的 KKT 系统能直接解出 x* 和 λ*。跑一遍代码,看输出的 x 和 λ 是否同时逼近解析解。如果 x 对了但 λ 不对,说明乘子更新有问题;如果两个都不对,先查内层求解器。这个验证习惯帮我省过很多次盲调参数的时间。
从那以后我每次拿到一个新的约束优化代码,都强制走一遍"解析解对照 + 残差曲线 + 参数敏感性"这三步,不看到三条曲线都合理,绝不往真实问题上套。希望帮到你。
本文还有配套的精品资源,点击获取