ARTICLE DETAIL

资讯详情

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

惩罚函数法全解析:外点法、内点法与增广拉格朗日乘子法实战

惩罚函数法全解析:外点法、内点法与增广拉格朗日乘子法实战 凡是做过带约束优化的人大概率都在某个时刻冒出过一个念头要是目标函数没有约束该多省事。约束优化方法里惩罚函数法就是把这个念头直接变成算法的典型代表——它不正面处理约束而是把违反约束折算成一笔罚款塞进目标函数然后放开了让无约束优化器去迭代。这个思路朴素到几乎不需要额外解释但真正上手调过参数的人都清楚罚因子怎么取、何时停机、数值在什么阶段开始崩全是需要拿命去换的细节。这篇文章就围绕惩罚函数法这条主线把外点法、内点法、精确罚函数和后来居上的增广拉格朗日乘子法串起来讲透同时给出一份能直接跑通的实现和几个我在实际项目里踩过的坑。无论你是刚开始学数值优化的学生还是需要把约束优化嵌进工程代码的开发者应该都能从这里捞到能用的东西。1. 约束目标函数的困境惩罚函数法要解决的核心矛盾1.1 无约束下山容易带围栏走路难无约束优化的求解框架已经非常成熟梯度下降、牛顿法、拟牛顿法BFGS、L-BFGS、共轭梯度法这些方法经过几十年打磨收敛理论清晰、实现稳定、代码到处都有。你只要给出目标函数和梯度剩下的交给求解器就行了。可一旦加上约束整套逻辑就变味了搜索方向不能随便走因为走出去可能就掉进不可行区域步长不能随便取因为走远了可能撞上约束边界。更麻烦的是约束优化的困难不在于多写几行判断而在于约束的几何结构和目标函数的几何结构往往互相冲突。目标函数的负梯度方向通常指向下降最快的方向但这个方向经常直接指向可行域外面。于是每一步都要在下降和可行之间做权衡而这个权衡的数学本质就是原始的KKT条件系统——它要求梯度之间满足一种线性组合关系同时还要满足互补松弛条件。这个系统本身是非线性方程组直接解它并不比原问题容易。惩罚函数法的思路是绕开这套复杂结构既然联合求解梯度和约束很难那就干脆把约束拆掉用一个附加项把违反约束的代价编码进目标函数。这样原问题就被改写成一族无约束问题每个都能用现成的求解器处理。代价是这一族问题之间需要外部循环协调而且额外项会污染问题的数值性质。1.2 惩罚项的构造逻辑把违反约束折算成目标代价惩罚项的设计原则其实只有一条在可行域内部或者恰好在边界上惩罚项必须为零一旦越界惩罚项就要迅速变大形成一个虚拟的墙。最直观的形式是把所有约束违反量取平方再求和乘以一个正的罚因子。对于等式约束 h(x) 0违反量就是 |h(x)|平方之后得到 h(x)²对于不等式约束 g(x) ≤ 0违反量是 max(0, g(x))平方后是 max(0, g(x))²。把这两部分加起来乘以 σ再和原目标函数相加就得到了外点法的罚函数。这个构造背后的直觉是如果 σ 足够大那么任何显著违反约束的点都会让罚函数的值变得非常大无约束优化器在下降过程中自然会避开这些区域最终被推到接近可行域的位置。σ 越大推得越狠但同时也把罚函数的曲面拉得越陡越来越接近一个带尖角的、条件数极差的函数。这就是惩罚函数法最根本的张力为了让解更接近可行域必须把 σ 推到很大但 σ 一大求解器就解不动了。理解这个张力是理解整个方法族的关键。后来的精确罚函数和增广拉格朗日乘子法本质上都是在想办法缓解这个张力——要么用不可微但一次到位的形式要么引入乘子让 σ 不必无限增长。1.3 三类惩罚思路的定位差异在正式展开推导之前先把几条常见路线摆在一起做个对照后面的章节会逐个拆开。这样你在实际选型时能先有个整体判断不至于一头扎进某个方法才发现方向不对。方法罚项形式迭代点位置核心参数主要痛点外点法外罚函数σ·[Σmax(0,g)² Σh²]从可行域外部逼近σ 单调增到无穷σ 过大导致病态边界不可达内点法障碍函数−r·Σln(−g) 或 r·Σ1/g始终在可行域内部r 单调减到 0需要严格初始内点等式约束难处理精确罚函数σ·Σmax(0,g) σ·Σh有限 σ 即可精确增广拉格朗日λh (σ/2)h² 形式外部逼近乘子修正λ 和 σ 双更新实现稍复杂需维护乘子序列这张表里最重要的一列是迭代点位置。外点法给出的解永远在可行域外面你拿到的永远是一个近似的、略带违反的解内点法给出的解永远在可行域里面永远不会真正踩到边界。对于工程中约束是硬性的、越界即事故的场景这个区别常常比收敛速度更决定选型。2. 外点法的完整推演从罚函数构造到参数推进节奏2.1 罚函数的两种写法与可微性取舍外点法最常用的罚函数形式是二次型也就是把每个约束的违反量平方后加权求和。以等式约束为例罚函数写成 P(x, σ) f(x) σ·Σ h_j(x)²。这个形式的好处是处处可微只要 h_j 本身光滑整个罚函数的梯度就存在且连续可以直接交给牛顿法或BFGS这类依赖梯度的方法。梯度表达式也很干净∇P ∇f 2σ·Σ h_j·∇h_j只需要额外的约束梯度信息。另一条路是用一次型罚项也就是 σ·Σ|h_j(x)|。这个形式在精确罚函数理论里非常重要因为它能在有限的 σ 下就得到精确解不需要让 σ 趋于无穷。代价是它在 h_j 0 处不可导函数曲面在那里有一个尖角梯度流在靠近约束面时会左右摇摆普通梯度类方法会震荡。想用它就得配合光滑化技巧或者专门处理不可微的求解器实现成本明显更高。所以选型很直白如果你只是想快速得到一个接近可行的解并且手头只有标准的无约束求解器那就用二次型如果你对解的精度有硬性要求、又不能接受 σ 无限增大那才去考虑一次型或精确罚函数并且准备好承受不可微带来的实现麻烦。我在实际项目里的默认选择永远是二次型只有在外点法反复调不动、精度卡死的时候才会转向增广拉格朗日而不是转头去写不可微的求解器。2.2 为什么罚因子必须单调递增到无穷很多人第一次看外点法会疑惑既然 σ 越大越好那为什么不一开始就取一个巨大的值一步到位这个问题问得非常关键答案藏在收敛性和数值稳定性之间那对矛盾里。从收敛性角度看对于固定的 σ求解 min f(x) σ·P(x) 得到的解 x(σ) 是 σ 的函数。σ 越大约束违反量越小x(σ) 越接近原问题的解。理论极限是 σ → ∞ 时 x(σ) 收敛到原约束问题的最优解。但这里有一个前提对每个 σ子问题都要被准确求解。当 σ 非常大时罚函数在约束面附近的曲率变得极其陡峭Hessian 矩阵的条件数大致正比于 σ。用BFGS去解这样一个问题时拟牛顿方向的数值精度会迅速退化迭代可能在错误的搜索方向上来回晃收敛变得极慢甚至停滞。所以正确的节奏是从一个温和的 σ 开始比如 1 或者根据梯度尺度估一个初值求解得到 x_1然后把 σ 放大若干倍用 x_1 作为下一次子问题的初值这叫热启动继续求解重复这个过程让 σ 逐步增大每一步都在前一步的基础上微调。这样每一步子问题都在一个相对良态的区域里求解热启动又大幅减少了迭代次数。这条逐步增压的策略是所有外点法实现的核心节奏跳过它直接上大 σ几乎必崩。放大的倍数通常取 4 到 10 之间。取得太小外层迭代次数太多浪费时间取得太大又退化成一步上大 σ 的问题。我一般用 5 或 10视子问题求解的难度决定。2.3 停机判据的三重校验外点法的外层迭代什么时候停这是实操里最容易含糊的地方。只按 σ 是否足够大来判断是不靠谱的因为 σ 大不等于解准确。可靠的做法是同时盯住几个量。第一个是约束违反度也就是 Σmax(0, g_i(x)) Σ|h_j(x)| 的当前值。这个量直接反映当前解有多不可行必须落到一个你能接受的数量级以下比如 1e-6 或 1e-8取决于你的精度需求。第二个是相邻两次外点迭代的解的变化量也就是 ||x_{k1} − x_k||。如果这个变化量已经很小说明继续增大 σ 也没什么改进了可能已经撞到数值精度的天花板。第三个是目标函数值与罚函数值之间的相对差距。很多时候我们真正关心的是目标函数 f(x) 的近似最优值而罚函数值里还夹着一大块惩罚项。如果惩罚项占总量的比例已经很小比如小于 1e-6说明约束基本满足解可用。这三条里我建议至少同时检查前两条。实际操作中如果只查约束违反度可能因为数值问题误判只查解变化量又可能在约束还没满足时就提前退出。2.4 一个等式约束算例的手算全过程用一个最简单的例子把外点法走一遍比看一百行公式有用。考虑问题最小化 f(x) x₁² x₂²约束是 x₁ x₂ − 1 0。这个问题的真解显然是 x₁ x₂ 0.5此时 f 0.5。构造罚函数 P(x, σ) x₁² x₂² σ(x₁ x₂ − 1)²。对 x₁ 和 x₂ 求偏导并令其为零∂P/∂x₁ 2x₁ 2σ(x₁ x₂ − 1) 0∂P/∂x₂ 2x₂ 2σ(x₁ x₂ − 1) 0两式相减立刻得到 x₁ x₂这一点很直观因为目标函数和约束都对两个变量对称。代入任一式设 x₁ x₂ t则 2t 2σ(2t − 1) 0解得 t σ / (1 2σ)。现在代几个 σ 看看收敛过程σ 1 时 t 1/3 ≈ 0.3333约束违反量为 2t − 1 −1/3σ 10 时 t 10/21 ≈ 0.4762违反量约 −0.0476σ 100 时 t 100/201 ≈ 0.4975违反量约 −0.00498σ 1000 时 t ≈ 0.49975违反量约 −0.000499。可以看到违反量大致按 1/σ 的量级衰减x 逐步逼近 0.5。这就是外点法最典型的收敛形态解从可行域外部这里约束面是 x₁ x₂ 1我们的迭代点在它的目标函数更优那一侧逼近永远贴着但踩不到边界。想达到 1e-6 的违反度σ 得开到 1e6 量级这也解释了为什么纯粹的外点法在高精度需求面前会很吃力。3. 内点法的可行域内部行走规则与初始化难题3.1 倒数障碍与对数障碍的差别内点法障碍函数法的思路和外点法正好相反。它不奖励离开约束而是给靠近约束边界的行为施加惩罚——你在可行域内部越接近边界罚项越大形成一个从内部往外推的软墙让迭代点始终待在可行域的安全内部。障碍项有两种经典形式。一种是倒数障碍用 r·Σ 1/g_i(x)其中约定 g_i(x) ≤ 0。当 g_i 从负值趋向 0 时1/g_i 趋向负无穷加上负号以后变成正的巨大值形成屏障。另一种是对数障碍用 −r·Σ ln(−g_i(x))。当 g_i 趋向 0 时−g_i 趋向 0ln 趋向负无穷再取负号变成巨大正值同样形成屏障。这两种在理论收敛性上都能成立对数障碍在现代内点法尤其是原始对偶内点法里用得更多因为它的二阶导数结构更规整。直观理解这两种障碍的差别倒数障碍的屏障陡得更快一旦靠近边界就猛烈反弹数值上容易出问题对数障碍的屏障相对柔和曲率分布更均匀数值表现通常更好。如果你只是想理解概念两者都行如果要写实现优先考虑对数障碍。需要注意内点法要求初始点在可行域内部而且是严格内部——不能落在边界上因为障碍函数在边界处无定义。这个要求把很多人挡在了门外你怎么知道可行域内部有哪个点如果连一个严格可行点都找不到内点法根本起不来。3.2 初始内点的寻找策略找初始内点是内点法的第一个门槛。常见做法是构造并求解一个辅助的第一阶段问题把原始约束稍微放宽最小化约束的最大违反量如果这个辅助问题的最优值小于零就说明原问题存在严格可行点而辅助问题的解本身就可以作为起点。这个思路和大M法、两阶段单纯形法在精神上是一致的。对于结构简单的约束比如变量有上下界第一阶段几乎是白送的——取上下界的中点就行。对于一般的非线性不等式约束第一阶段问题本身也是一个约束优化问题这就有点循环了。实践中更省事的做法是先用外点法跑几步得到一个违反量很小的点然后在这个点的基础上朝可行域内部方向微调。或者利用问题的物理含义直接构造一个明显可行的点——工程问题里的设计变量往往有物理范围随便取一组保守值通常都是可行的。我遇到过最麻烦的情况是可行域非常薄几乎是一条缝。这种情况下第一阶段的求解会很困难而且就算找到了内点障碍函数的数值也会非常勉强。碰到这种结构与其硬上内点法不如换外点法或者增广拉格朗日。3.3 迭代点的边界保护机制内点法的另一个细节是步长控制。因为障碍函数在边界附近会变得非常陡无约束求解器如果步子迈得太大很容易一步跨过边界进入不可行区域然后因为 g_i(x) 0 导致 1/g_i 或者 ln(−g_i) 无定义程序直接报错。解决办法是在求解子问题时给目标函数加一个保护一旦识别到不可行直接返回一个极大的数值比如无穷大或者一个很大的有限值把求解器的线搜索挡回来。这个技巧在Python里写起来很顺手只要在目标函数开头判断一下 g_i(x) 的符号不满足就 return np.inf。求解器收到这个信号后会自动缩短步长退回可行区域。不过这种返回无穷大的做法会让基于梯度的线搜索失灵因为梯度信息在不可行点上是没有的。更稳妥的做法是对可行集施加显式的最大步长约束或者用投影方法保证每一步都落在可行域内。在简单问题上返回无穷大就够用了复杂问题还是得用专门的内点法求解器比如成熟软件里的原始对偶内点实现它们内部有完整的步长保护和对数障碍处理。3.4 内点法的失效场景内点法不是万能的有几类问题它处理得很差甚至完全失效。第一类是可行域没有内点的情况比如等式约束和不等式约束恰好构成一个低维流形严格内部点是空的。第二类是可行域退化比如约束互相平行、边界重合。第三类是问题本身规模太大内存和计算量撑不住内点法的稠密线性代数操作。还有一个容易被忽视的点内点法处理等式约束比较别扭。纯粹的障碍函数只针对不等式约束等式约束通常需要用消元或者投影的方法处理这又增加了实现复杂度。相比之下外点法对等式约束和不等式约束一视同仁用起来省心得多。所以现实中如果问题里等式约束占主导我基本不会首选内点法。4. 罚因子爆炸与增广拉格朗日乘子法的补救思路4.1 大罚因子导致的Hessian病态量化外点法的数值病态不是模糊的感觉而是可以量化的。回到前面那个 x₁ x₂ − 1 0 的例子罚函数的Hessian是这样的对角上都是 2 2σ非对角都是 2σ。这个矩阵的两个特征值可以算出来一个大致是常数级别的约等于 4另一个是 4σ 加上常数。也就是说随着 σ 增大Hessian的条件数大约按 σ 的量级线性增长。这意味着 σ 每放大 10 倍问题的条件数就恶化 10 倍。当 σ 到 1e8 这个量级时条件数就到了 1e8双精度的有效位数大约 16 位被消耗掉将近一半。梯度计算、Hessian近似、牛顿方向求解都会带上明显的舍入误差。更严重的是此时罚函数的曲面在约束面附近已经接近一个山谷结构沿约束方向曲率很大沿约束面方向曲率很小梯度类方法在这种地形上会走出非常曲折的锯齿路径。这解释了为什么纯外点法在高精度问题上总是力不从心。你想把违反量压到 1e-8就得让 σ 到 1e8然后数值就开始不听话了。4.2 精确罚函数的诱惑与不可微的代价既然 σ 增大是问题根源那有没有办法用有限的 σ 拿到精确解答案是有的这就是精确罚函数理论的出发点。它指出如果罚项改成一次型也就是 σ·Σ|h_j(x)| 或者 σ·Σmax(0, g_i(x))那么当 σ 超过某个阈值这个阈值和最优乘子的最大绝对值有关时罚函数的无约束最优解恰好就是原约束问题的最优解。σ 不用趋于无穷。这个结论非常有吸引力但它把困难从σ 太大转移到了函数不可微。一次型罚项在约束面处出现尖角梯度在尖角两侧方向突变普通梯度法会在尖角附近来回震荡收敛不动。要利用精确罚函数就需要专门处理不可微优化的算法比如基于次梯度的方法或者把尖角光滑化或者用信赖域方法配合特定的子问题求解。光滑化的思路是用一个平滑函数近似 |t| 或者 max(0, t)比如用 sqrt(t² ε²) 近似 |t|其中 ε 是一个小的光滑参数。ε 越小近似越精确但曲面越陡数值越难。这其实又回到了 σ 增大导致病态的老问题。所以精确罚函数在理论优美和工程可用之间有一道不窄的沟。4.3 乘子迭代让罚因子不再被迫无限增大真正在工程里被广泛采用的补救方案是增广拉格朗日乘子法也叫乘子法。它的核心洞察非常漂亮外点法之所以需要 σ 趋于无穷是因为罚项本身不能正确处理约束如果在罚项里额外加一个线性项 λh(x)让乘子 λ 承担一部分力那么即使 σ 保持中等大小解也能被推到约束面上。对于等式约束增广拉格朗日函数写成 L(x, λ, σ) f(x) λh(x) (σ/2)h(x)²。注意 λ 是一个可以在外层迭代中更新的参数而不是固定值。迭代格式是先固定 λ 和 σ求解关于 x 的无约束最小化然后用得到的 x 更新 λ公式是 λ_{k1} λ_k σ·h(x_k)σ 可以保持不变只在必要时比如乘子更新后违反量下降太慢才放大。这个格式的妙处在于理论上只要乘子序列更新得当σ 可以保持一个固定的中等值迭代就能收敛到精确解。数值病态的问题被大幅缓解高精度求解变得可能。这也是为什么几乎所有严肃的约束优化求解器底层都用了增广拉格朗日或者它的变体而不是裸的二次罚函数。4.4 与算例的代际对比还是用最小化 x₁² x₂²、约束 x₁ x₂ − 1 0 这个例子对比一下。取 σ 固定为 1初始 λ 0。第一轮求解 min x₁² x₂² 0·(x₁ x₂ − 1) 0.5·1·(x₁ x₂ − 1)²。用同样的对称性分析设 x₁ x₂ t罚函数对 t 求导得 2t (2t − 1) 0解得 t 0.25。此时约束违反量 h −0.5。更新 λ 0 1·(−0.5) −0.5。第二轮λ −0.5σ 仍是 1。求导得 2t λ (2t − 1) 0代 λ −0.5 得 2t − 0.5 2t − 1 0解得 t 0.375违反量 h −0.25。更新 λ −0.5 − 0.25 −0.75。第三轮λ −0.75得 2t − 0.75 2t − 1 0t 0.4375违反量 −0.125λ 更新为 −0.875。第四轮t 0.46875违反量 −0.0625λ 更新为 −0.9375。第五轮t 0.484375违反量 −0.03125。可以看到违反量每轮减半λ 快速趋向 −1这正是原问题在最优解处的拉格朗日乘子。整个过程 σ 一直是 1完全没有放大。熟练之后你会发现如果一开始就把 σ 设得稍大一些比如 10收敛会更快但仍远不需要那种趋于无穷的极端取值。这就是乘子法用同一个小例子给出的最有力证明。5. 从零写一份可复现的惩罚函数法实现5.1 数据接口与函数签名设计写实现之前先把接口理清楚。一个可复用的惩罚函数法框架至少需要这几样东西目标函数 f(x)等式约束向量 h(x)不等式约束向量 g(x)以及它们各自的梯度。如果梯度手写麻烦可以先用数值差分顶上但生产环境一定要用解析梯度原因后面会细讲。我习惯把所有约束统一成 h_i(x) 0 和 g_j(x) ≤ 0 两种形式这样惩罚项的公式可以写成统一形式代码里加个判断就行。数据结构上x 用numpy数组约束用返回数组的函数表示。下面这段是一个最小可用的外点法骨架。import numpy as np from scipy.optimize import minimize def objective(x): return x[0]**2 x[1]**2 def eq_constraints(x): return np.array([x[0] x[1] - 1.0]) def ineq_constraints(x): return np.array([]) # 本例没有不等式约束 def eq_jac(x): return np.array([[1.0, 1.0]]) def ineq_jac(x): return np.zeros((0, 2)) def penalty(x, sigma): val objective(x) h eq_constraints(x) g ineq_constraints(x) if h.size: val sigma * np.sum(h**2) if g.size: val sigma * np.sum(np.maximum(0.0, g)**2) return val def penalty_grad(x, sigma): grad np.array([2*x[0], 2*x[1]]) h eq_constraints(x) g ineq_constraints(x) if h.size: grad sigma * 2.0 * eq_jac(x).T h if g.size: active np.maximum(0.0, g) grad sigma * 2.0 * ineq_jac(x).T active return grad把罚函数和梯度分开写的好处是能直接把梯度喂给BFGS避免求解器内部去差分速度和精度都上一个台阶。5.2 子问题求解器的选型子问题是无约束最小化用什么求解器直接决定整体效率。小规模稠密问题用BFGS足够它对梯度质量要求不算苛刻迭代稳健。如果问题有几百上千维BFGS的内存开销要存近似的逆Hessian会变得不可接受这时改用L-BFGS-B只保留有限的历史信息内存是常数级别。如果目标函数二阶信息容易得到牛顿法收敛更快但每步成本高对病态问题也更敏感。这里要特别强调一点子问题不需要解到极致精确。外层的外点循环本身会反复修正你在内层花大力气把精度抠到 1e-12 纯属浪费而且当 σ 大时这个精度根本达不到。合理的内层收敛容差是和外层进度联动σ 小的时候宽松一点1e-4σ 大的时候收紧一点1e-8让整个算法的计算量分配更合理。还有一些求解器是给最小二乘结构定制的比如lm方法适合罚项占主导的情形。但在通用的惩罚函数法里还是BFGS或L-BFGS-B最省心。5.3 约束尺度归一化的处理这是最容易被忽略、又最容易坏事的一步。假设你有两个约束一个是长度约束量纲是毫米量级在几百另一个是应力约束量纲是兆帕量级在几十。把它们直接平方相加长度约束的违反量在数值上会压过应力约束几十倍优化器会更努力地去满足长度约束而对应力约束视而不见。解决办法是给每个约束违反量配一个权重权重量级取该约束典型值的倒数。比如长度约束除以1000应力约束除以100让它们的违反量在数值上处于同一量级。这一步做与不做收敛效果能差出好几倍。工程问题里凡是约束量纲不统一的务必先做归一化再送进优化器。我吃过这个亏一次结构优化里因为没归一化算法死命压一个几乎无关紧要的位移约束真正的应力约束一直违反排查了半天才发现是尺度问题。5.4 运行结果与收敛曲线解读把上面那段骨架套上外层循环用 σ 从 1 开始、每轮乘 4 的策略跑 12 轮你会看到解的轨迹大概是这样第 1 轮 x ≈ (0.333, 0.333)约束违反约 0.333第 3 轮左右违反量降到 0.02 量级到第 8 轮附近违反量进入 1e-4最后几轮进入 1e-6 甚至更小同时 x 稳定在 (0.5, 0.5) 附近。这条曲线有几个特征值得记住。第一段σ 小下降很快因为罚函数还比较温和求解器走得顺。中间段下降开始变慢因为每轮的有效增益在减小σ 的增长带来的精度改善是渐近的。尾段曲线趋于平坦这时收敛主要由数值精度决定再增大 σ 收益有限。如果你画出的曲线在中段出现明显的台阶或者反弹那通常意味着内层子问题没有解准或者约束尺度出了问题。看到这种形态先检查内层容差和归一化而不是继续加大 σ。6. 实际调试中反复出现的几个失效模式6.1 罚因子初值一步给太猛新手最常见的一个动作是把 σ 初值设成 1e6想着约束是硬要求从一开始就要严。结果是无约束求解器在第一轮就卡住梯度模长巨大线搜索找不到下降步要么迭代次数爆掉要么返回一个离初值不远的点然后程序假装收敛了你拿到的解约束违反得离谱。正确做法是把 σ 初值设得温和一些。一个实用的经验是先计算初始点处的目标函数梯度模长和约束违反量让 σ 初值大约等于梯度模长除以违反量的平方量级把两项的贡献拉到同一数量级。这个估计不精确没关系反正外层会逐步放大关键是别让第一轮子问题就病态。如果你不确定就从 1 开始宁可多跑几轮外层。6.2 数值梯度与解析梯度打架外点法对梯度的依赖比无约束优化更敏感因为惩罚项会放大梯度的误差。如果你用数值差分提供梯度步长选得不好梯度误差可能到 1e-6 量级而 σ 放大之后这个误差被乘以 σ 的倍数直接淹没了真实的搜索方向。表现就是目标值不怎么下降约束违反量反复横跳迭代在最优解附近画圈。解决办法很直接能用解析梯度就用解析梯度。约束函数的梯度通常比目标函数更简单手写代价不大。如果实在要差分把差分步长和 σ 联动σ 大时缩小差分步长并且优先用中心差分而不是前向差分。还有一个技巧是检查梯度的一致性在几个随机点用数值差分和解析公式各算一遍看相对误差是否在 1e-8 以内超过就说明梯度写错了。这个自检花不了几分钟能省下大量排查时间。6.3 不等式约束平方化后的伪最优解把不等式约束 g(x) ≤ 0 处理成 max(0, g(x))² 有个陷阱在 g(x) 0 处罚项的导数是连续的而且恰好为零。这意味着虽然约束刚好满足但罚项在这里没有梯度推力优化器在约束面附近的步态会很奇怪有时会停在略微违反约束的点上因为继续朝可行区域走的收益看起来很小。更隐蔽的问题是当多个不等式约束同时接近激活时平方化会让罚函数在这些边界的交角处形成一片非常平坦的区域求解器容易停在里面出不来误以为收敛。这种情况下得到的解看着约束违反不大但目标函数值比真最优差不少。绕开的办法有两种。一是改用一次型罚项代价是不可微前面讲过二是干脆切换到增广拉格朗日让乘子来精确定位激活约束。工程上我更倾向后者因为实现复杂度可控、精度有保障。6.4 子问题容差与热启动的配合热启动是外点法效率的关键但用不好也会翻车。所谓热启动就是把上一轮的解作为下一轮子问题的初值。这通常能大幅减少内层迭代次数因为相邻两轮的罚函数形状很接近。但如果上一轮的子问题根本没解准这个解就带着误差把它作为初值反而可能把下一轮带偏。所以内层容差和外层进度的配合要把握一个平衡。我的习惯是σ 小的时候内层容差放到 1e-5允许解带着点误差因为外层还要反复修正σ 大到 1e4 以上时内层容差收紧到 1e-9确保最后几轮的解干净。同时加一个保险如果某一轮外层迭代后解的变化量反而比上一轮大说明可能出现了震荡就不要再热启动了改用上一轮的初值重新解一次。还有一点当 σ 跨过某个量级后子问题可能连一次迭代都走不动求解器立刻返回初值。这不是收敛是卡死。判断方法是看内层返回的迭代次数和梯度模长如果迭代次数是零或者梯度模长还很大就必须调整策略比如降低 σ 的增长倍数或者切换到增广拉格朗日。7. 惩罚函数法的能力边界与替代方案选择7.1 问题维度和稀疏结构的影响惩罚函数法的实现复杂度基本和问题维度无关这是它的优点也是它在小规模问题上特别好用的原因。你不需要维护任何稀疏结构不需要处理KKT系统的线性代数一个无约束求解器加上外层循环就能干活。对于几十维到几百维的问题这条路线又快又省事。但维度上去以后情况就变了。大规模约束优化问题的真正瓶颈往往在约束的雅可比矩阵及其线性求解上。成熟的内点法求解器会利用雅可比和海森的稀疏性把计算量压到可接受的范围。而裸的惩罚函数法把约束抹平进目标函数等于放弃了这些稀疏结构当问题到了几千维以上时性能和内存都会成为硬伤。所以规模是选型的第一道分水岭小规模优先惩罚函数法大规模优先结构化的内点法或SQP。7.2 精度要求决定方法路线如果只要求解的约束违反量在 1e-3 到 1e-4 之间纯外点法就能胜任实现简单、调参直观。如果要求到 1e-6 甚至更高外点法就开始吃力必须让 σ 增长到很大数值风险急剧上升。这时候正确的选择不是硬撑而是转向增广拉格朗日或者换用SQP、内点法这类专门为高精度设计的求解器。精度要求还和约束类型有关。等式约束对精度的要求通常更严因为一点点违反在物理上可能就是能量不守恒或者几何不自洽。不等式约束在边界附近有一定的软容忍度稍微违反一点往往可以接受。理解了这一点你就能有区别地设置停机判据而不是所有约束一个标准。7.3 与成熟求解器的协作策略现实项目里很少有人真的从零写一个完整的约束优化器去替代成熟软件更实用的策略是把惩罚函数法当成脚手架。一种常见用法是做初值生成先用几步外点法快速把解推到一个接近可行的区域然后把结果交给SQP或者内点求解器去精修。这样能显著降低后者的迭代次数也避免它在糟糕的初值上浪费时间。另一种用法是做可行性分析。在正式求解之前用外点法快速判断约束是否互相冲突——如果跑了很多轮约束违反量始终降不下去那就说明这组约束可能根本不可行需要回去检查建模而不是继续调参数。这个判断比直接丢给求解器、然后面对一个含糊的失败信息要清楚得多。还有一个实际考量是维护成本。成熟的求解器经过大量测试边界情况处理得比较完善你自己写的惩罚函数法在遇到退化约束、无穷解、NaN 值传播这些情况时容易出问题。所以我的建议是学习和研究可以用手写实现加深理解生产环境优先用成熟库把手写实现当成辅助工具而不是主力。写到这里我自己这几年在结构优化和参数反演里反复用惩罚函数法的体会是它最大的价值不在于能解多难的问题而在于它把约束优化的抽象概念变得具体可操作——你能清晰地看到约束是怎么一步步被满足的罚因子是怎么影响解的数值病态是怎么积累的。这些直觉在你后来用任何高级求解器时都用得上因为你会知道它内部大致在做什么出错时该往哪个方向查。真要用到高精度硬约束的场景别硬扛裸的外点法早点切到增广拉格朗日把乘子维护起来你会发现原来那些怎么调都下不去的违反量很自然就收敛了。
返回列表