简介:外点法是求解约束非线性规划问题的常用优化算法,尤其适合处理可行域复杂、难以直接投影的带约束场景。这份Matlab程序实例面向学习运筹优化、人工智能算法以及需要解决约束建模问题的研究人员与工程师,演示了如何构造惩罚函数将约束并入目标,通过逐步调整惩罚因子逼近最优解。压缩包共9个文件,全部为.m脚本,包括目标函数、等式与不等式约束、梯度与雅可比矩阵计算、newton法辅助模块以及用于统一调度的主程序,各文件职责划分清楚,覆盖从目标函数定义、约束构建、惩罚函数生成到迭代求解与停止判断的关键环节,可对照源码逐行推演外点法的完整计算流程。压缩包整体仅3KB,内容精简易读,几乎没有冗余文件。目前已有674人学习下载。通过学习该实例,读者能够掌握惩罚因子的选取与调整策略、约束违反程度的判断方法,以及调用fmincon等优化工具箱函数实现求解的具体写法;同时还能理解基于梯度的搜索方向更新与收敛停止准则,并可将外点法迁移到其他工程优化问题中,是一份简洁且具有较强参考价值的算法实现代码。
1. 外点法matlab程序实例为什么值得从零手写
手写一个外点法(外点罚函数法)求解器,是理解约束优化最直接的路径。与内点法始终把迭代点锁在可行域内不同,外点法从不可行点出发,靠不断增大的罚因子把解“拉”进可行域。
这个“先违反、后收敛”的思路,让它可以处理那些无法轻易给出可行初始点的工程问题,也很适合与 matlab 优化工具箱配合:主循环自己写,内层无约束求解交给 fminunc,或完全用梯度下降实现。对于正在学优化方法的学生,以及需要自定义约束逻辑的工程师来说,这套“外层增罚、内层无约束优化”的程序实例都能在半小时内改成可调、可验证的最小实现。
什么情况下值得放弃 fmincon 转手写外点法?一是目标函数和约束规模小、但结构特殊,需要精确控制惩罚方式;二是想观察罚因子变化如何影响解的轨迹;三是手头没有优化工具箱,只能靠基础 MATLAB 函数完成求解。下面从罚函数的数学形式切入,给出两版可运行代码,最后落到多约束与乘子法改进。
2. 外点罚函数法的数学模型与收敛性前提
2.1 罚函数怎么写:从不等式到 max 算子
约束优化问题的一般形式是
$$\min_{x\in\mathbb{R}^n} f(x), \quad \text{s.t.}\quad g_i(x)\le 0\ (i=1,\dots,m),\quad h_j(x)=0\ (j=1,\dots,l)$$
外点法把约束“吸收”进目标函数,构造辅助函数
$$P(x,M)=f(x)+M\sum_{i=1}^{m}\left[\max\left(0,\ g_i(x)\right)\right]^{2}+M\sum_{j=1}^{l}\left[h_j(x)\right]^{2}$$
其中 M 为罚因子。对不等式约束,max(0, g_i(x)) 保证了只有违反约束时才产生惩罚:g_i(x)≤0 时该项恒为 0,g_i(x)>0 时按违反程度的平方放大。等式约束则始终以平方形式参与惩罚,无论从哪一侧偏离 h_j(x)=0,都会被罚项拉回。
选择二次而不是一次惩罚,核心原因是可微性。只要 f、g、h 自身足够光滑,平方后的罚函数对 x 连续可微,梯度与 Hessian 都存在,fminunc 或自写梯度法都能直接使用。代价是可行域边界上 max 算子引入一阶不可导点,实际计算中拟牛顿法通常能容忍;如果追求更强光滑性,可以改用三次或更高次惩罚,但问题病态程度会同步上升。
2.2 罚因子序列为什么必须趋于正无穷
固定某个有限 M 时,P(x, M) 的最优点 x*(M) 通常并不满足原始约束。目标函数会尽量把解往无约束最优方向推,惩罚项则把解往可行域拉,双方在约束边界外侧达成平衡。要让 x*(M) 真正回到可行域,就必须让惩罚项的相对权重压倒目标函数,也就是令
$$M_0 < M_1 < M_2 < \dots \to \infty$$
收敛性理论上要求三点:目标函数与约束函数在最优解邻域内连续;罚因子序列严格递增且发散;每一轮内层无约束优化都要解得足够精确,否则误差会跨轮累积。这些条件满足时,x*(M_k) 的任意极限点都是原问题的局部最优解,加上凸性假设后可以升级为全局最优解。
需要注意,“罚因子越大越好”是个常见误解。M 过大会让 P 的 Hessian 变得病态,条件数随 M 线性增长,内层求解器反而更难收敛。实际工程里通常取 M0=1~10,按 5~10 倍递增,而不是一步推到 1e6。
2.3 外点与内点的本质区别:迭代点的可行性
外点法得名于迭代点始终落在可行域之外,靠惩罚逐步逼近边界。与之相对的内点法(屏障法)通过在目标函数里加入屏障项,把迭代点限制在可行域内部。两者在使用体验上的差异非常明显:
| 对比项 | 外点法 | 内点法/屏障法 |
|---|---|---|
| 初始点要求 | 任意点,不需要可行 | 必须是严格可行内点 |
| 迭代点位置 | 从不可行逐渐逼近边界 | 始终在可行域内部 |
| 约束类型适配 | 等式与不等式都很自然 | 不等式方便,等式需额外处理 |
| 中途“解”的可用性 | 不可行,不能直接作为工程解 | 近似可行,可提前停机 |
| 实现成本 | 几十行 MATLAB 代码足够 | 需处理屏障参数与边界有界性 |
因为外点法几乎不挑剔初始点,工程上很多场景都在用它。比如模型预测控制里每一步热启动点来自上一时刻的次优解,不保证可行,外点法可以直接接管。
2.4 一个可以解析验证的标准算例
选一个能用手算验证的例子,后续所有程序都以它为准:
$$\min\ f(x)=x_1^{2}+x_2^{2} \quad \text{s.t.}\quad g(x)=2-x_1-x_2\le 0$$
直观上,最优解是直线 x1+x2=2 上离原点最近的点,即 x*=(1, 1),目标值 f*=2。对应的罚函数为
$$P(x,M)=x_1^{2}+x_2^{2}+M(2-x_1-x_2)^{2}$$
注意第二项只在 x1+x2<2 时非零。对 x1、x2 求偏导并令其为零,得到 x1=x2=2M/(1+2M)。把不同 M 代入,可以得到一条完全确定的收敛轨迹:
| M | x1=x2 | f(x) | 约束 g(x) |
|---|---|---|---|
| 0 | 0.0000 | 0.0000 | 2.0000 |
| 1 | 0.6667 | 0.8889 | 0.6667 |
| 10 | 0.9524 | 1.8141 | 0.0952 |
| 100 | 0.9950 | 1.9801 | 0.0099 |
| 1e4 | 0.99995 | 1.99990 | 0.00010 |
| 1e6 | 0.9999995 | 1.999999 | 0.000001 |
罚因子每增加两个数量级,约束违背大约缩小两个数量级,目标函数从下方逼近 2。这条解析轨迹就是后面程序的“标准答案”,数值结果落在同一趋势上,就说明外循环和内层求解器都没写错。
3. 外点法MATLAB程序实例:调用fminunc的内外层循环
3.1 程序骨架与职责划分
一个完整的外点法程序,建议按三层职责分开写:
- 问题定义层:目标函数、不等式约束、等式约束的函数句柄;
- 外循环层:负责更新罚因子 M、调用内层求解、检查停机条件;
- 内层求解层:把当前 M 下的 P(x, M) 当作无约束优化问题处理。
这种划分的最大好处是换问题时不用动外循环,只改第一层;换内层求解器时不用碰问题定义。在 MATLAB 里用函数句柄加元胞数组,就能把多约束问题描述得足够干净。下面的实例对应 2.4 节标准算例,文件名可命名为 outerpoint_demo.m,与 outerpointmethod 这个主题词的检索习惯保持一致。
3.2 可直接运行的代码
% outerpoint_demo.m % 外点法求解: % min f(x) = x1^2 + x2^2 % s.t. g(x) = 2 - x1 - x2 <= 0 clear; clc; % --- 问题定义 --- f_obj = @(x) x(1)^2 + x(2)^2; g_ineq = @(x) 2 - x(1) - x(2); % g <= 0 % 罚函数: P = f + M * max(0, g)^2 penalty = @(x, M) f_obj(x) + M * max(0, g_ineq(x))^2; % --- 外层参数 --- M0 = 1; M = M0; % 罚因子初值 gamma = 10; % 罚因子增长倍数 tol_v = 1e-6; % 约束违背容差 tol_x = 1e-4; % 相邻代理解距离容差 max_outer = 50; % 外层最大迭代数 x_prev = [0; 0]; % 初始点,故意不可行 viol = inf; for k = 1:max_outer % 内层无约束优化: fminunc 求解 P(x, M) opts = optimoptions('fminunc', ... 'Algorithm', 'quasi-newton', ... 'Display', 'off', ... 'MaxIterations', 500, ... 'MaxFunctionEvaluations', 2000, ... 'StepTolerance', 1e-10, ... 'OptimalityTolerance', 1e-8); [x_opt, ~, exitflag] = fminunc(@(x) penalty(x, M), x_prev, opts); viol = max(0, g_ineq(x_opt)); fprintf('k=%2d M=%7.1e x=(%9.6f, %9.6f) f=%.8f viol=%.2e\n', ... k, M, x_opt(1), x_opt(2), f_obj(x_opt), viol); % 停机: 约束已满足且前后两次解基本不动 if viol < tol_v && norm(x_opt - x_prev) < tol_x break; end x_prev = x_opt; % 热启动 M = M * gamma; % 惩罚加重 end fprintf('结果: x*=(%.8f, %.8f), f*=%.8f, g=%.2e\n', ... x_opt(1), x_opt(2), f_obj(x_opt), viol);这段代码在笔者的环境中跑出的轨迹与 2.4 节理论表一致,大约 12~18 次外循环后满足停机条件,具体次数会随 MATLAB 版本和机器精度略有浮动。exitflag 为 1 表示内层无约束优化正常收敛;若为 0,说明迭代次数或函数评估次数触顶,需要放大对应的 Max 选项,而不要怀疑外点法本身的逻辑。
3.3 罚因子初值、增长倍数、容差三个参数的设置逻辑
外点法参数不多,但每个都直接影响迭代轨迹:
| 参数 | 建议取值 | 设置逻辑 |
|---|---|---|
| M0 | 1 ~ 10 | 太小则前几轮约束几乎不受惩罚,白做无约束优化;太大则初始问题就病态 |
| gamma | 5 ~ 10 | 增长太慢外循环多;太快则相邻两轮问题差异大,热启动失效 |
| tol_v | 1e-5 ~ 1e-7 | 决定最终可行性,应比约束尺度本身小两个数量级以上 |
| tol_x | 1e-4 左右 | 防止 M 已经很大但解还在缓慢移动时无限循环 |
M 的增长倍数 gamma 值得单独说明。若 gamma 过小,比如 1.1,外循环需要几十次才能把 M 从 1 推到 100,而相邻两轮的最优解变化极微小,整体上等于在做大量冗余的无约束优化。若 gamma 过大,比如 100,M 从 1 跳到 100 后惩罚项突然主导,上一轮的最优点作为本轮初值离新问题的最优点太远,fminunc 需要额外迭代去追赶,外循环次数少了但总成本没降。5~10 是实践中最稳健的区间。
3.4 热启动策略:为什么每次内层迭代都用上次的最优解
外点法每一轮对应不同的 M,下一轮的无约束问题与上一轮相比只是惩罚权重变化,最优解只移动一小段。如果把初始点固定不变,每轮都从零开始优化,前几轮还能勉强运行,M 变大后每一步都要重新跨越整个解空间,计算量成倍上涨。
把上一轮的 x_opt 当作本轮初值,就是热启动。它对内层算法的迭代次数影响非常大,尤其 M 变大后,P 的等高面被拉长成狭长形状,从远处冷启动很容易在陡峭的惩罚槽里来回震荡。热启动配合拟牛顿法时,fminunc 还能复用近似的 Hessian 信息,收敛速度会明显优于冷启动。
4. 不依赖工具箱的梯度下降实现与收敛性对比
4.1 自写最速下降配回溯线搜索的梯度框架
如果手头机器没有 matlab 优化工具箱许可证,fminunc 不可用。只要罚函数可微,自己写一个最速下降法配合回溯线搜索,就足够支撑外点法的内层求解。核心函数如下:
function [x, iter] = steepest_descent(fun, grad, x0, max_iter, tol) % fun : 目标函数句柄 % grad : 梯度函数句柄 % x0 : 起始列向量 % max_iter : 最大迭代数 % tol : 梯度无穷范数停机阈值 alpha0 = 1.0; % 初始步长 rho = 0.5; % 步长衰减率 c1 = 1e-4; % Armijo 充分下降系数 x = x0(:); for iter = 1:max_iter g = grad(x); if norm(g, inf) < tol return; end d = -g; % 最速下降方向 alpha = alpha0; f_now = fun(x); % 回溯: 只要不满足 Armijo 条件就缩小步长 while fun(x + alpha * d) > f_now + c1 * alpha * (g' * d) alpha = rho * alpha; end x = x + alpha * d; end end这里使用无穷范数停机,是因为罚函数在边界附近的梯度分量可能一个极大、另一个极小,只用欧氏范数容易误判。Armijo 条件中的 c1 取 1e-4 是数值优化里的经典配置,rho 取 0.5 让步长搜索速度与稳定性保持平衡,alpha0 取 1.0 则是因为最速下降方向配合充分下降条件时,单位步长在罚函数光滑区域通常是可行起点。
4.2 解析梯度与数值梯度的验证
标准算例下罚函数为 P=x1^2+x2^2+M(2-x1-x2)^2,解析梯度为 ∇P=(2x1-2M(2-x1-x2), 2x2-2M(2-x1-x2))。用中心差分验证解析式是否正确:
M = 10; x = [0.3; 0.8]; g_analytic = 2*x - 2*M*(2 - x(1) - x(2))*[1; 1]; % 注意 g>0 时才成立 g_numeric = zeros(2,1); eps_g = 1e-6; for i = 1:2 e = zeros(2,1); e(i) = eps_g; g_numeric(i) = (penalty(x+e,M) - penalty(x-e,M)) / (2*eps_g); end disp([g_analytic g_numeric]);这一小段验证值得每次改完罚函数都跑一次。解析梯度写错时,数值梯度对比能直接指出差在哪个分量。对含 max 的罚函数要特别注意:x 恰在约束边界 g(x)=0 时,max 算子不可导,解析梯度与数值梯度都会有偏差,且偏差比远离边界处略大,属于正常现象。
4.3 不同罚因子增长倍数的收敛行为对比
把上面的最速下降嵌入外点法,在相同容差下改变 gamma,可以得到如下趋势(数值在同一台机器上观察得到,绝对值随版本浮动,相对趋势稳定):
| gamma | 外循环次数 | 总梯度调用(约) | 最终违约束量 |
|---|---|---|---|
| 2 | 40 以上 | 800 以上 | 1e-6 附近 |
| 5 | 20 左右 | 400 左右 | 1e-6 |
| 10 | 15~18 | 350 左右 | 1e-6 |
| 50 | 10 左右 | 1000 以上 | 1e-6 |
gamma 从 2 提到 10,总成本明显下降;gamma 提到 50 以后,每轮无约束优化难度陡增,总梯度调用次数反而上升。这说明参数并非越大越好,也印证了前面章节对 gamma 的分析。真实工程中可以先取 gamma=10 跑一轮,再根据每轮内层迭代是否触顶来微调。
4.4 排错:内层求解不收敛时先查什么
最常遇到的现象有三个:
- fminunc 返回 exitflag=0,或最优性残差长时间不下降:先看 MaxFunctionEvaluations 是否触顶。罚函数含 max 时,边界非光滑点会让拟牛顿法反复试探,函数评估次数消耗很快。
- M 增大后回溯线搜索把步长压到极小:这是最速下降在病态二次函数上的典型表现。换成 BFGS 或共轭梯度,或者把 gamma 调回 5。
- 终解的约束违背比 tol_v 大一个量级:多数情况是外层循环提前被 tol_x 停机。应检查 tol_x 是否设得比 tol_v 更严格,或直接改成仅靠 viol 与 M 上限作为停机条件。
5. 多约束与等式约束的扩展,用乘子法弥补外点法的不足
5.1 把问题定义改成约束元胞数组
实际工程很少只有单个不等式约束。把约束句柄收进元胞数组,罚函数模块不需要改结构:
g_cell = { @(x) -x(1), ... % x1 >= 0 写成 -x1 <= 0 @(x) x(2) - x(1)^2 }; % x2 - x1^2 <= 0 h_cell = { @(x) x(1) + x(2) - 1 }; % 等式约束 function P = penalty_all(x, M, f_obj, g_cell, h_cell) P = f_obj(x); for i = 1:numel(g_cell) P = P + M * max(0, g_cell{i}(x))^2; % 逐项独立惩罚 end for j = 1:numel(h_cell) P = P + M * h_cell{j}(x)^2; end end逐项独立计算惩罚,不要先对所有约束求和再取 max,否则某些约束被满足时会被另一些违反的约束“带伤”。这种写法也方便在日志里单独记录每个约束的违背量。
5.2 乘子法改进的更新式
外点法要把罚因子推到很大才能满足可行性,而乘子法在罚项中加入拉格朗日乘子的线性项,让有限罚因子也能达到高精度。不等式约束的增广拉格朗日函数常用形式为
$$L(x,\lambda,\mu)=f(x)+\frac{1}{2\mu}\sum_{i}\left[\max\left(0,\ \lambda_i+\mu g_i(x)\right)\right]^2-\frac{1}{2\mu}\sum_{i}\lambda_i^2$$
乘子更新式为
$$\lambda_i \leftarrow \max\left(0,\ \lambda_i+\mu g_i(x)\right)$$
实现时只需在外循环末尾更新乘子,罚因子按 2~5 倍缓慢增长,收敛速度通常比纯外点法快一到两个数量级。与第 3 章程序相比,改动量只有几行,非常划算。
5.3 用参考解验证扩展程序
把 5.1 的约束与目标 f=(x1-2)^2+(x2-1)^2 组合,可行域由 x1+x2=1 与 x2≤x1^2 相交而成,该问题没有直观的闭式解。验证方法是先用 fmincon 的 'sqp' 算法跑出参考解,再让外点法程序从同一初始点出发,两步都收敛后比较目标值与约束违背量,差异在 1e-5 以内即视为实现正确。还可以把每轮 x_opt 和 viol 存入数组,绘制 viol 随 M 变化的 semilogy 曲线,观察曲线是否呈线性下降趋势。此时再回头对照 2.4 节的解析轨迹,外点法、增广拉格朗日与 fmincon 三条路径的目标值应一致到 1e-5 以内,构成完整的交叉验证闭环。
本文还有配套的精品资源,点击获取