ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

惩罚函数法处理约束优化:外点法、内点法与增广拉格朗日实战

惩罚函数法处理约束优化:外点法、内点法与增广拉格朗日实战 约束优化这四个字在很多做算法、做调度、做参数标定的人眼里是绕不过去的一道坎。你手上永远有一个想让它尽量小或者尽量大的目标比如成本、误差、能耗、时间但同时手上还攥着一堆必须满足的条件——资源不能超、误差不能越界、物理量必须有意义。惩罚函数法就是处理这类带约束的优化方法里最经典、最好上手、也最容易被误用的一类思路。它做的事情说白了特别朴素既然无约束优化那么好解那我就想办法把约束揉进目标函数里把有约束问题改造成一串无约束问题来解。这个思路听起来有点作弊但在工程现场极其好用因为它几乎不挑求解器也不需要你去推导复杂的KKT条件改几行代码就能跑。这篇文章适合三类人看正在学优化课程、被罚函数作业卡住的学生工程里需要快速搭一个带约束求解流程的开发者以及已经用过罚函数但总觉得结果差一点、想搞明白问题出在哪的实践者。下面我按自己踩过坑的顺序,把方法、原理、代码、参数选择和排查经验一次讲透。1. 惩罚函数法到底在解决什么问题1.1 从能解到不能直接解的那道墙我们先把问题摆清楚。标准的约束优化问题长这样min f(x) s.t. g_i(x) ≤ 0, i 1, ..., m h_j(x) 0, j 1, ..., pf(x) 是目标函数g 是不等式约束h 是等式约束。如果把这些约束全部拿掉只留 min f(x)那就是无约束优化梯度下降、牛顿法、拟牛顿法BFGS、L-BFGS随你挑成熟得不能再成熟。麻烦就在于这三个字——约束。约束的存在破坏了很多无约束优化里理所当然的假设。最典型的一条无约束问题的最优解一定满足梯度为零一阶必要条件但带约束问题的最优解往往落在约束边界上那里梯度根本不为零。你沿着负梯度方向走会被边界硬生生挡回来。这就是为什么不能简单地把无约束方法套上去。一个思路是直接用投影法、可行方向法每一步都保证迭代点落在可行域内但这需要每一步都求解一个投影到可行域的子问题对复杂约束来说这个子问题本身就很难。另一个思路就是惩罚函数法我不去管迭代点在不在可行域内我让违反约束这件事付出代价。代价足够大的时候最优解自然会被逼到可行域附近甚至内部。注意惩罚函数法严格来说得到的通常是原问题的近似解只有罚因子趋于无穷外点法或趋于零内点法时极限才等于原最优解。工程中我们取有限参数所以要理解误差来源。1.2 罚函数法的核心直觉乱闯红绿灯就罚钱打个生活中的比方。假设你想从家开车到公司目标路程最短或者时间最短但路上有一堆规则不能闯红灯、不能超速、不能逆行。这些就是约束。无约束最优解是什么无视所有规则直线距离最短逆行、超速、闯红灯全上那是理想最优。但现实不允许。惩罚函数法的做法是允许你这么开但每闯一次红灯罚一万块每超一次速罚五千。于是你的总成本 油费原目标 罚款惩罚项。罚款金额调得越高你就越不敢违规最优驾驶路线就越贴近守规矩的那条。对应到数学上外点惩罚函数构造出来是P(x, σ) f(x) σ · [Σ (max(0, g_i(x)))² Σ (h_j(x))²]这里面 σ 就是罚款单价也叫罚因子。max(0, g_i(x)) 的作用是如果约束满足g_i ≤ 0这项为 0不罚如果违反了g_i 0就按违反的程度平方后惩罚。等式约束 h_j(x) 只要不为零就罚。然后我们固定一个 σ去解这个无约束问题 min P(x, σ)得到一个解 x*(σ)。接着把 σ 增大再解一次得到新的 x*。如此反复σ 从小的值一路增大到很大x*(σ) 就会从不太守规矩逐渐收敛到真正的最优解。1.3 为什么用平方而不是绝对值这是初学者经常问的一个点也是设计时的关键取舍。惩罚项用 (违反量)² 还是 |违反量|差别很大。用平方的好处是罚函数在约束边界处连续可微。g_i(x)0 这个交界点max(0, g_i)² 的导数是连续的两边都是 0而 |max(0,g_i)| 在交界处导数会跳变。可微这个性质很重要因为我们的子问题要用梯度类方法解目标函数不可微会让牛顿法、拟牛顿法直接失效得退回到次梯度法收敛速度大打折扣。用平方的代价是当违反量比较大时惩罚增长得特别猛二次增长容易让罚函数的 Hessian 矩阵条件数急剧恶化导致子问题求解困难。这就是罚因子不能设太大的根源之一。我实测下来对大多数光滑问题平方惩罚是首选只有当约束本身非光滑、或者存在很强的病态风险时才会考虑一次惩罚配合专门的非光滑求解器。2. 外点法、内点法与混合法三种主流形式的取舍2.1 外点法从可行域外一步步逼近外点法也叫 SUMT 外点法序列无约束极小化技术是我用得最多的一种。它的特点是从可行域外部出发迭代点一开始不可行随着罚因子增大逐渐被拉向可行域边界。算法流程大致是选初始罚因子 σ₁ 0初始点 x₀可以任意不要求可行放大系数 c 1常用 2 到 10收敛精度 ε。以 x_{k} 为初值求解无约束问题 min P(x, σ_k)得到 x_{k1}。判断收敛如果罚项 σ_k ·违反量已经小于 ε或者相邻两次解的变化 ‖x_{k1} - x_k‖ ε停止。否则 σ_{k1} c · σ_k转第 2 步。外点法最大的优势是初值随便给不需要可行起点。这对工程问题非常友好因为找一个可行解有时候比解优化本身还难。它的缺点是中间迭代点全部不可行如果你的约束代表物理硬性限制比如压力不能超限那么中间过程在物理上是不合法的不能直接拿去执行。举个能手工验证的例子帮助理解收敛过程min f(x) (x₁ - 2)² (x₂ - 2)² s.t. h(x) x₁ x₂ - 3 0解析解很明显对称性给出 x₁ x₂ 1.5f 0.5。罚函数P (x₁-2)² (x₂-2)² (σ/2)(x₁x₂-3)²对 x₁、x₂ 求偏导并令为 0 2(x₁-2) σ(x₁x₂-3) 0 2(x₂-2) σ(x₁x₂-3) 0两式相减得 x₁ x₂ t代入第一式 2(t-2) σ(2t-3) 0 解得 t (4 3σ) / (2 2σ)代几个值看看σ 1t 7/4 1.75σ 10t 34/22 ≈ 1.545σ 100t 304/202 ≈ 1.505σ 1000t 3004/2002 ≈ 1.5005可以看到 t 单调地从 1.75 逼近 1.5。这个手算过程非常能说明外点法的本质罚因子越大解越贴近真实约束面。同时也能看到σ 从 100 到 1000精度只提高了不到 0.005收益在快速递减——这就是为什么不能无脑把 σ 往死里加。2.2 内点法始终待在可行域内部的障碍内点法障碍函数法的思路正好相反。它要求迭代点始终严格在可行域内部靠一个障碍把点挡在边界里面让你永远靠不出去、也出不去。障碍函数常用两种形式倒数障碍B(x, r) f(x) r · Σ 1/(-g_i(x))对数障碍B(x, r) f(x) - r · Σ ln(-g_i(x))其中 r 是障碍因子内点法里 r 是逐渐减小的不像外点法的 σ 逐渐增大。当 r 趋近 0 时障碍越来越弱解趋近于约束边界上的最优解。用一个极简例子看清原理min f(x) x s.t. x ≥ 1真实解显而易见是 x 1。用对数障碍构造B(x, r) x - r·ln(x-1)定义域 x 1。求导1 - r/(x-1) 0得 x 1 r。r 0.1 时 x 1.1r 0.01 时 x 1.01r → 0 时 x → 1。完美体现了r 越小越贴近边界的规律。内点法的巨大优点是中间每一个迭代点都可行这在需要在线执行、实时控制的场景下非常关键——每一步输出的控制量都是满足物理约束的可以立即下发。它的致命缺点是必须有一个严格可行的初始点而且约束如果是等式约束内点法很难直接处理因为等式约束的可行集没有内部通常要先把等式约束消去或者转成两边的单边不等式。实操心得内点法在约束边界附近障碍项的梯度会急剧增大趋于无穷数值上非常陡峭。如果初始点离边界太近第一步就可能因为梯度爆炸而失败。我的经验是初始点至少要离边界有 5% 到 10% 的余量。2.3 三种形式的对比与选择把外点法、内点法还有工程里更常用的增广拉格朗日法乘子法放一起对比选型就清楚了。维度外点法内点法增广拉格朗日法初始点要求任意必须严格可行任意迭代点可行性全不可行全程可行逐渐可行罚因子走向σ 增大到无穷r 减小到零σ 增大但有限病态风险高σ 大时中近边界时低收敛速度慢线性慢线性快可超线性等式约束直接处理难处理直接处理实现复杂度最简单简单中等选型的核心逻辑是如果你只是要快速搭个原型、对精度要求不高、初始解难找可行点用外点法二十分钟能跑起来。如果你的约束是硬物理限制、每一步输出都要可用用内点法。如果你对精度和收敛速度都有要求、问题规模不小直接上增广拉格朗日法它不需要把罚因子推到无穷就能收敛到精确解这背后的原因是它同时更新拉格朗日乘子把罚项和乘子项配合起来避免了单一罚因子的病态问题。增广拉格朗日的更新形式对等式约束是L(x, λ, σ) f(x) Σ λ_j h_j(x) (σ/2) Σ h_j(x)²每轮解完子问题后更新乘子 λ_j ← λ_j σ · h_j(x)。这个 λ 的更新非常关键——它相当于把约束到底该被罚多少这个信息自动学到了所以 σ 不需要无限增大取一个中等值比如 10、100就够。3. 手把手实操从数学形式到可运行代码3.1 参数选择罚因子、放大系数与终止条件参数选择是惩罚函数法里最考验经验的部分因为理论只告诉你σ 要趋于无穷没告诉你具体取多少、每步放大几倍、什么时候停。先看罚因子初始值 σ₁。太小了前几轮子问题几乎是无约束问题解离可行域很远太大了第一轮子问题就病态。我的做法是先估计目标函数的尺度和约束的尺度。如果 f 的量级是 O(1)约束违反量的量级也是 O(1)σ₁ 取 1 到 10 比较稳妥。有个更靠谱的启发式σ₁ 让初始点处的惩罚项量级和 f 的量级相当即 σ₁ ≈ f(x₀) / (违反量²)。这样第一轮就不会被罚项完全主导。放大系数 c 一般取 2 到 10。c 太小比如 1.5需要很多轮才能把 σ 推上去轮数多、总计算量大c 太大比如 100相邻两轮解跳变剧烈子问题初值离最优太远反而求不准。我一般取 c 10兼顾收敛轮数和稳定性。终止条件我通常同时用三个任何一个满足就停约束违反量 ‖违反‖ ε_con最常见的是约束范数小于 1e-6相邻解变化 ‖x_{k1} - x_k‖ ε_x相对目标变化 |f_{k1} - f_k| / (1 |f_k|) ε_f注意只看解的变化量容易假收敛。因为当 σ 已经很大时每轮解几乎不动但违反量可能还没降下来。所以约束违反量这个判据必须留着。3.2 一个完整的外点法 Python 实现下面这段代码是我常用的一套轻量实现用 scipy 的 minimize 解子问题结构清晰可以直接改。以不等式约束为例import numpy as np from scipy.optimize import minimize def objective(x): # min (x1-2)^2 (x2-1)^2 return (x[0] - 2)**2 (x[1] - 1)**2 def ineq_constraint(x): # g(x) x1 x2 - 2 0 return x[0] x[1] - 2 def penalty(x, sigma): f objective(x) g ineq_constraint(x) # 只惩罚违反约束的部分max(0, g) violation max(0.0, g) return f sigma * violation**2 def outer_penalty_method(x0, sigma01.0, c10.0, max_iter20, eps_con1e-6): x np.array(x0, dtypefloat) sigma sigma0 history [] for k in range(max_iter): # 固定 sigma解无约束子问题 res minimize(penalty, x, args(sigma,), methodBFGS, options{gtol: 1e-8}) x res.x g ineq_constraint(x) violation max(0.0, g) history.append((k, sigma, x.copy(), objective(x), violation)) print(fiter {k:2d} | sigma{sigma:10.2f} | fx({x[0]:.6f}, {x[1]:.6f}) | ff{objective(x):.6f} | viol{violation:.2e}) if violation eps_con: print(收敛约束满足) break sigma * c return x, history # 运行 x_opt, hist outer_penalty_method(x0[0.0, 0.0]) print(最优解:, x_opt)运行结果大致是这样不加约束时最优是 (2, 1)但 x₁x₂ 3 2违反约束。理论最优应该是最小化点到 (2,1) 的距离投影到直线 x₁x₂2 上。点 (2,1) 沿法向 (1,1) 投影设投影点 (2-t, 1-t)代入 2-t1-t 2得 t 0.5所以最优是 (1.5, 0.5)f 0.25 0.25 0.5。代码跑出来会逼近这个值这就是验证我们的实现对不对的标准。3.3 从代码到结果一次完整的收敛追踪把上面代码跑起来你会看到 σ 从 1 一路涨到 1e6 甚至更大而每轮的解缓慢向 (1.5, 0.5) 移动。下面是我实测的一组典型轨迹方便你对照轮次σx₁x₂违反量011.6670.6670.3331101.5240.5240.04821001.5020.5020.0048310001.5000.5004.8e-44100001.5000.5004.8e-551000001.5000.5004.8e-6这张表里有几个信息量很大的点。首先解确实在单调逼近真值收敛性没问题。其次违反量每一轮几乎精确缩小 10 倍——这正好对应 c 10 的放大系数说明外点法的收敛速率和罚因子放大倍数直接挂钩是线性的。第三也是最重要的一点到了后面几轮x 的值几乎不变了稳定在 1.500000只有违反量在缓慢下降。这意味着如果你只盯着解的变化来判断收敛第 3 轮就会误以为收敛了而实际上违反量还差得远。这就是前面强调必须看约束违反量的原因。更深一层的现象是数值精度的天花板。当 σ 涨到 1e8 以上罚函数里 f 的贡献(量级 0.5)相对惩罚项(量级 σ × 违反量²)几乎可以忽略子问题的 Hessian 条件数变成 1e8 量级双精度浮点的有效位数只剩 8 位左右这时候 BFGS 给出的解开始抖动再也降不下去。所以工程上我一般把 max_iter 设在 σ 达到 1e8 到 1e10 就停剩下的精度靠增广拉格朗日法去补。4. 常见问题与排查技巧实录4.1 子问题解不准、BFGS 提前终止这是外点法最高频的问题没有之一。现象是子问题求解器返回成功但你一看结果违反量还是很大或者解明显不合理。根因几乎都是数值病态。当 σ 很大罚函数在约束面附近是一个陡峭的深谷谷底很窄Hessian 矩阵的主方向曲率相差极大条件数爆炸。BFGS 靠梯度信息近似 Hessian这种极端各向异性的情形下近似误差大加上浮点舍入梯度算出来不可靠于是它可能在一个远没到谷底的地方就报告梯度足够小了。排查和解决的手段我总结了几条加大子问题的梯度容差没有用反而更糟。正确做法是给变量做尺度归一化让各维量级接近。换用更好的初值。把上一轮的解作为这一轮的初值而不是重复用同一个初值能明显改善。用数值梯度时把步长调大一点相对差分步长 1e-6 而不是 1e-8避免噪声淹没真实梯度。最根本的办法别让 σ 单独一路狂奔改用增广拉格朗日法让乘子分担约束σ 保持中等量级。实操心得当子问题的解开始跳或者违反量不降反升时基本可以判定是病态触顶了。这时候继续加 σ 是徒劳的应该停下来检查问题构造而不是怀疑代码写错了。4.2 收敛慢、参数敏感性高另一个典型困扰是能收敛但慢得让人抓狂或者换个初始点结果差异很大。慢的原因通常有两个。一是罚因子初始值选得太小需要很多轮才能把 σ 抬到有效水平。判断方法是看前几轮的违反量是否下降得太慢——如果每轮只降一点点而放大系数已经给得不小那就是 σ₁ 太小。二是放大系数 c 太小轮数被拉长。参数敏感的根源在于惩罚函数法的本质它是一个近似方法最终精度和 σ 直接相关而 σ 的选择又和问题尺度相关。换个量纲同一个 σ 的效果就完全不同。我见过同一个模型把力的单位从牛顿换成千牛罚因子就得整体调整三个数量级否则要么收敛慢要么病态。对付参数敏感我的做法是每次换问题先用一个小脚本扫一遍 σ₁ 和 c 的粗网格比如 σ₁ ∈ {0.1, 1, 10, 100}c ∈ {2, 5, 10}看哪组能在 10 轮以内把违反量压到 1e-6 以下。这个扫描成本很低却能省掉后面大量试错时间。4.3 常见问题速查表现象可能原因排查/解决违反量不下降σ 太小或 c 太小增大 σ₁ 或 c观察下降速率子问题解跳动数值病态σ 触顶停加 σ改增广拉格朗日初始点无法进入可行域内点法要求严格可行初值用外点法找近似可行解再交给内点法结果对初值敏感问题非凸或尺度未归一化变量归一化多初值试解收敛后精度不够有限 σ 导致近似误差提高 σ 上限或改用乘子法等式约束总是不满足罚因子不够或被非光滑点卡住检查约束可微性提高 σ中间迭代点不可用外点法特性改用内点法获得全程可行的迭代点5. 工程实践中怎么用得不踩坑5.1 什么时候该用、什么时候别用惩罚函数法不是万能的。我的判断标准很直接如果约束数量不多几十个以内、目标函数光滑、对精度要求是中等1e-6 级别足够、开发时间紧那它非常合适二十分钟能出活。反之如果约束上百个、问题本身高度非线性、需要 1e-10 级别的精度那还是老老实实上成熟的 NLP 求解器或者直接用增广拉格朗日法做内核。一个容易被忽视的坑是约束违反量的量纲不统一。比如你同时有电流不超过 10 安和误差不超过 0.001两个约束的数值尺度差三个数量级用同一个 σ 去罚小尺度那个约束实际上被忽视了。解决办法是给每个约束单独配罚因子或者把所有约束归一化成无量纲的相对违反量。这一步我在几个电机参数标定项目里都吃过亏——一开始约束看起来没起作用排查半天才发现是被大尺度约束压住了。5.2 和拉格朗日乘子法的关系别把它们对立起来很多教材把惩罚函数法和拉格朗日乘子法讲成两条平行的路其实它们在增广拉格朗日法里是合流的。理解这一点对用好它们很关键。纯惩罚法只加 σ·(违反)²靠 σ→∞ 收敛。 纯乘子法只加 λ·h(x)但 λ 本身是要靠对偶问题求解的直接在原空间不好做。 增广拉格朗日两者都加λ 每轮迭代更新σ 不用趋于无穷。这里面的直觉是乘子 λ 编码了约束的正确价格也就是最优解处约束对目标的边际影响。有了这个价格罚项就只需要处理当前偏离价格的残差不需要承担全部压力。所以 σ 可以保持温和病态问题也就消失了。这也是为什么我在需要高精度的场景里最终都会切到增广拉格朗日。如果你的项目里既有等式约束又有不等式约束处理不等式可以引入松弛变量把它变成等式或者用 max(0, g) 的写法配合乘子更新后者更简洁我一般选后者。5.3 一个我反复验证的实操流程最后把我自己常用的完整流程理一遍你可以直接套先把问题写成标准形式目标、等式约束、不等式约束分清楚尤其要检查约束是否归一化。用外点法快速试解σ₁1c10跑 10 到 15 轮看趋势对不对。这一轮的目的不是求精确解而是判断问题设置是否有硬伤。如果外点法收敛平稳但精度不够把最后几轮的解和 σ 作为初值切到增广拉格朗日法做精修。如果外点法一开始就不收敛或者解乱跳先怀疑量纲和病态而不是怀疑算法。最后用 KKT 条件核对一遍如果约束是激活的取等号检查乘子符号是否合理如果约束不激活检查乘子是否接近零。这一步能抓出大部分看起来对但其实错的结果。注意KKT 核对不能省尤其是乘子符号。对于 g(x) ≤ 0 形式的约束激活时对应乘子应非负拉格朗日函数写法的约定要与推导一致。符号错了往往说明约束方向写反了而这在代码里很难靠肉眼发现。这套流程我在做机械臂轨迹优化、供应链成本调度、以及几个传感器标定问题里都用过外点法打前站、乘子法做精修的组合兼顾了上手速度和最终精度。真正让我少走弯路的,不是某个高级技巧而是把先看量纲、再看趋势、最后核 KKT这三步固定成习惯——很多看起来玄乎的算法不收敛追到最后都是这三步里某一步出了问题。
返回列表