资讯动态

SciPy约束优化:SLSQP与trust-constr差异详解及选型避坑指南

发布时间:2026/9/20 19:28:25 来源:尧图企业网站定制
先交代一下背景。我上个月在写一个生产排程的优化模块目标函数里带平方项和分式项约束条件既有线性也有非线性。一开始图省事直接调scipy.optimize.minimize用 SLSQP想着以前写过很多次这类规划问题改改约束函数就能跑。结果跑了大半天结果怎么看怎么不对劲——要么约束条件没满足要么收敛标志位明明是True但把解代回去一验算边界条件就差得离谱。后来把method换成trust-constr才发现这两种方法在约束定义上根本不是同一套语法许多看似相同的约束换一个求解器就得全部重写。这篇文章就把我踩过的坑、对比过的差异、以及最后沉淀下来的写法建议一次说清楚。不管你是做运筹优化、工程参数标定、机器学习超参搜索还是任何需要用到带约束非线性优化的场景只要你用 SciPy这篇文章大概率能帮你省下好几个小时的排查时间。1. 先搞清楚这两个求解器的脾气1.1 SLSQP和trust-constr是何方神圣SLSQP 全称是 Sequential Least Squares Programming也就是序列二次规划。它的核心思路是在每次迭代中把原问题近似成一个二次规划子问题求解这个子问题得到搜索方向再通过线搜索确定步长不断逼近最优解。它内部用 BFGS 来近似目标函数的 Hessian 矩阵所以不需要你手动提供二阶导。trust-constr 的全称是 Trust Region Constrained Algorithm本质是信赖域算法。它把当前迭代点附近的一块区域作为“信赖域”在这个局部区域里用一个近似模型通常是二次模型来替代原问题并在满足约束的前提下逼近最优解。它的一大特点是迭代过程中会维护一个可行域信息对等式约束和不等式约束的处理方式更现代也支持更丰富的约束对象。单从算法族来看两者差异就很大SLSQP 更老牌、更轻量适合中小规模问题trust-constr 是 SciPy 后来专门为大规模、复杂约束优化引入的求解器且很多选项都做得更细。1.2 我一开始踩的坑以为换个method就行这是最普遍的误解。很多人包括我先写好了 SLSQP 版本的代码跑不通之后想当然地认为只要把method参数改成trust-constr就行。实际你一试就会立刻翻车trust-constr根本不认 SLSQP 那套dict类型的约束写法。它会直接抛异常或者提示constraints参数格式不对。反过来你用NonlinearConstraint作为约束传给 SLSQP虽然新版 SciPy 偶尔能接受一部分对象但行为也是不稳定的很多老版本根本不认。所以我当时的第一反应是这俩家伙的约束接口根本不是兼容的。这是所有后续踩坑的根源。1.3 什么场景选谁先看规模再看约束结合我这段时间的实测给你一个比较粗犷的选型建议如果变量数量在几十个以内约束函数比较简单且大多是线性约束或低次多项式约束用 SLSQP 最省事代码也直觉。如果问题规模比较大约束条件很多或者约束函数带有明显的非线性甚至需要利用稀疏结构优先考虑 trust-constr。如果迭代过程中非常在意“不可行解”的干扰比如工程仿真里约束违反会导致仿真崩溃那 trust-constr 配合keep_feasibleTrue会更合适。如果你已经推得了目标函数和约束的解析雅可比矩阵甚至 Hessian那 trust-constr 的收益会非常明显。2. 约束定义的底层差异一个写生死值一个写区间SLSQP 和 trust-constr 最核心的区别我认为不在算法实现层面而在“约束该怎么表达”这件事上。这直接决定了你从一种方法切换到另一种方法时需要重写多少代码。2.1 SLSQP的dict约束模板SLSQP 的约束是用dict来描述的这是老牌 SciPy 教程里最常见的写法。它有两种约束类型eq等式约束要求fun(x) 0ineq不等式约束要求fun(x) 0基本模板长这样constraints [ {type: ineq, fun: lambda x: x[0] - 2 * x[1] 1}, {type: eq, fun: lambda x: x[0] x[1] - 3}, ]注意这里的fun返回值必须是一个标量或者一维数组。每个 dict 元素对应一组约束多个约束就放进一个 list。这里有个关键的隐藏规则ineq约束是“大于等于零”不是“小于等于零”也不是“处于区间内”。你要是想表达x0 x1 4不能直接写成x0 x1 4要改写成4 - x0 - x1 0也就是{type: ineq, fun: lambda x: 4 - x[0] - x[1]}这个符号方向是新手最容易写反的地方也是后面我们对比 trust-constr 时的一个核心鸿沟。2.2 trust-constr的区间约束模板trust-constr 推荐使用的是NonlinearConstraint和LinearConstraint对象。它们的核心表达方式是from scipy.optimize import NonlinearConstraint, LinearConstraint constraints [ NonlinearConstraint(fun, lb, ub), LinearConstraint(A, lb, ub), ]注意这个语法它表示的是lb fun(x) ub是一种区间约束。也就是说你用信任域方法时任何一个非线性约束都需要明确写下界和上界。没有上界就写np.inf没有下界就写-np.inf等式约束就令lb ub 0。举个例子假设我要表达x0 x1 1在 trust-constr 里可以写成NonlinearConstraint(lambda x: x[0] x[1], 1, np.inf)如果你把不等式写成x0 x1 4那应该写成NonlinearConstraint(lambda x: x[0] x[1], -np.inf, 4)这其实是比 SLSQP 更自然的表达方式但由于很多人已经被“必须化成 0”的思维驯化了切换到区间写法时反而容易懵。2.3 同一个约束两种翻译法为了让你直观感受这俩的差异我整理了一个对照表同一个约束在两种方法下的写法完全不同数学约束SLSQP 写法trust-constr 写法x0 x1 1{type: ineq, fun: lambda x: x[0] x[1] - 1}NonlinearConstraint(lambda x: x[0] x[1], 1, np.inf)x0 x1 4{type: ineq, fun: lambda x: 4 - x[0] - x[1]}NonlinearConstraint(lambda x: x[0] x[1], -np.inf, 4)x0 x1 3{type: eq, fun: lambda x: x[0] x[1] - 3}NonlinearConstraint(lambda x: x[0] x[1], 3, 3)看到没有同一个约束在 SLSQP 里要先做“移到同一边、大于等于零”的变形而在 trust-constr 里只需直接写原不等式。SLSQP 是“生死值”式判断trust-constr 是“区间”式描述。实际项目里切换求解器最容易出的幺蛾子就是把 SLSQP 里已经变形过的约束函数原封不动搬到NonlinearConstraint里结果发现符号方向全部反了。3. 实操对比同一个问题跑两遍理论说再多不如实际跑一个例子。这里我设计一个最小但能体现差异的二维非线性规划问题分别用两种方法实现一遍观察结果和迭代表现。3.1 定义一个带线性非线性约束的测试问题问题设定如下目标函数min f(x) (x0 - 1)^2 (x1 - 2.5)^2约束条件x0^2 x1^2 9 x0 x1 1 x0 0 x1 0初始点取[0, 0]。这个问题的最优解其实在圆形边界附近是一个典型的非线性规划测试问题。圆形约束是非线性不等式约束线性约束是那个x0 x1 1另外还有两个边界约束。3.2 SLSQP版本代码与结果SLSQP 版本我写成这样import numpy as np from scipy.optimize import minimize def objective(x): return (x[0] - 1) ** 2 (x[1] - 2.5) ** 2 def circle_constraint(x): return 9 - x[0] ** 2 - x[1] ** 2 # 9 - (x0^2 x1^2) 0 def line_constraint(x): return x[0] x[1] - 1 # x0 x1 - 1 0 constraints [ {type: ineq, fun: circle_constraint}, {type: ineq, fun: line_constraint}, ] bounds [(0, None), (0, None)] x0 np.array([0.0, 0.0]) result minimize( objective, x0, methodSLSQP, boundsbounds, constraintsconstraints, options{maxiter: 200, ftol: 1e-9}, ) print(result)输出大概长这样fun: 1.9999999999999956 jac: array([ 2., -1.]) message: Optimization terminated successfully nfev: 12 nit: 5 njev: 5 status: 0 success: True x: array([2.22044605e-16, 2.99999998e00])这个结果里x1接近 3x0接近 0目标函数值接近 2。但是注意一下这里圆形约束实际上已经起作用了因为在x (0, 3)处x0^2 x1^2 9正好落在圆边界上。这里 SLSQP 收敛得很快迭代 5 步就完成了原因是问题简单、维度低。3.3 trust-constr版本代码与结果trust-constr 版本我建议按下面这种方式写import numpy as np from scipy.optimize import minimize, NonlinearConstraint def objective(x): return (x[0] - 1) ** 2 (x[1] - 2.5) ** 2 def circle_constraint(x): return x[0] ** 2 x[1] ** 2 def line_constraint(x): return x[0] x[1] constraints [ NonlinearConstraint(circle_constraint, -np.inf, 9), NonlinearConstraint(line_constraint, 1, np.inf), ] bounds [(0, None), (0, None)] x0 np.array([0.0, 0.0]) result minimize( objective, x0, methodtrust-constr, boundsbounds, constraintsconstraints, options{maxiter: 200, verbose: 1}, ) print(result)注意这里我没写雅可比矩阵先看看自动有限差分能不能搞定。输出大概长这样fun: 2.0000000001788134 message: gtol termination condition is satisfied. nfev: 18 nit: 8 njev: 6 status: 1 success: True x: array([2.36873320e-08, 2.99999994e00])从数值结果看两者非常接近最优解都是(0, 3)目标函数值约等于 2。这符合预期因为这是一个凸优化问题速度上的差异在这个小例子里看不出来。3.4 结果对比两者到底差在哪从最终结果来说差别确实不大但过程信息差别明显SLSQP 的nfev函数评估次数是 12trust-constr 是 18。trust-constr 在没有提供雅可比的情况下内部通过有限差分去近似导数评估次数自然更多。trust-constr 的success条件是gtol满足SLSQP 则是“Optimization terminated successfully”。后者信息更简洁前者更适合诊断是否真正收敛到约束边界上。如果我把circle_constraint换成更复杂的表达式比如带指数或除法trust-constr 的有限差分误差会更大结果偏差也会更明显而 SLSQP 同样面临这个问题但它的线搜索机制对梯度的要求略宽松一些有时候反而表现得“看起来更稳”。注意我这里说的是“看起来更稳”并不代表结果更准。实际情况里最优解的精度高度依赖约束函数的光滑性和梯度计算的准确性这一点我们到第 4 节细说。4. 雅可比和Hessiantrust-constr的加分项4.1 为什么trust-constr对雅可比更敏感trust-constr 在内部构建信赖域子问题时需要用到约束函数的雅可比矩阵或者其转置与向量的乘积。如果你不提供jac参数它会用有限差分法自动估计。有限差分本身有截断误差尤其当约束函数的尺度差异很大时误差会被放大而信赖域算法对模型的准确性非常敏感——模型错了搜索方向就跟着错最终导致迭代次数变多、收敛精度下降甚至触发假的终止条件。SLSQP 本质上也需要约束函数的梯度但它内部有一个 BFGS 更新的机制在某些情况下可以“容忍”不那么完美的梯度信息。不过这只是一种工程上的经验描述不要理解成 SLSQP 不需要梯度。4.2 手动写雅可比用测试问题演示回到第 3 节的问题。圆形约束是g1(x) x0^2 x1^2它的雅可比是[2*x0, 2*x1]线性约束是g2(x) x0 x1它的雅可比是[1, 1]。在 trust-constr 里可以这样传入import numpy as np from scipy.optimize import minimize, NonlinearConstraint def circle_constraint(x): return x[0] ** 2 x[1] ** 2 def circle_jac(x): return np.array([2 * x[0], 2 * x[1]]) def line_constraint(x): return x[0] x[1] def line_jac(x): return np.array([1.0, 1.0]) constraints [ NonlinearConstraint(circle_constraint, -np.inf, 9, jaccircle_jac), NonlinearConstraint(line_constraint, 1, np.inf, jacline_jac), ] bounds [(0, None), (0, None)] x0 np.array([0.0, 0.0]) result minimize( objective, x0, methodtrust-constr, boundsbounds, constraintsconstraints, options{maxiter: 200}, ) print(result)提供雅可比之后nfev会明显下降因为不必再做数值差分来估计约束梯度了。实测在这个例子里函数评估次数从 18 降到 10 左右而且结果数值更精确。如果你还愿意多走一步把目标函数的 Hessian 或 Hessian 向量积也传给 trust-constr收敛速度还会更好。不过对于这个简单例子收益不明显但在高维复杂问题上差异会很大。4.3 不写雅可比会不会挂实测感受这个问题我经常被问到。答案是不会立刻“挂掉”因为 SciPy 会自动用有限差分。但你要有心理准备每次迭代都会多出若干次约束函数的调用计算开销变大如果约束函数是仿真、外部程序、数据库查询这类高成本计算额外开销不可接受如果约束函数的数值噪声比较大有限差分的梯度估计会非常不靠谱trust-constr 可能提前报错或收敛到明显不可行的点。所以我的经验准则是只要是能写出解析表达式的约束就尽量把雅可比写出来。尤其是需要频繁跑优化的场景这个代码成本摊薄下来是绝对划算的。5. 常见报错与排查技巧5.1 SLSQP最常见报错与处理SLSQP 最常见的报错之一是Inequality constraints incompatible对应状态码status: 4。这通常意味着在某个迭代点约束之间互相冲突无法找到可行方向。出现这个报错时我建议先检查初始点是否可行。SLSQP 不太喜欢从极端不可行点出发尤其是约束是强非线性的时候。一个简单的处理方式是把初始点朝可行域中心拉一拉或者先跑一个不带约束的版本得到一个新的起始点再带入。另一个典型报错是Iteration limit exceeded。这不是说算法错了只是迭代步数不够。先看看maxiter是否太小再看ftol是否设置得太严格。工程上我会把maxiter调到 500-1000把ftol设为1e-8左右大多数问题都能收敛。5.2 trust-constr最常见报错与处理trust-constr 常见的一个现象是successTruemessage显示gtol termination condition is satisfied但你把解代回约束里一验算发现约束违反度仍然有1e-5甚至更大。这通常并不是说结果不可用而是你对精度要求太高默认的gtol不够严格。你可以调小gtol比如设成1e-10或者通过result.constr_violation字段来评估约束违反度。注意constr_violation返回的是所有约束违反程度的最大值我一般会把它当做一个硬指标来看。还有一种情况是xtol与gtol之间的相互作用。trust-constr 同时使用多个终止条件如果变量变化步长已经很小它可能提前停止而约束边界还没完全收敛。遇到这种情况把xtol和gtol同时调小一两个数量级再观察结果变化。5.3 调参经验速查我把自己常用的参数组合整理成表方便直接复制求解器参数推荐值/说明SLSQPmaxiter500 起复杂问题调大SLSQPftol1e-8左右太小会导致过度迭代SLSQPeps有限差分步长默认1.49e-8约束尺度差异大时调整trust-constrmaxiter1000 起适合复杂约束trust-constrgtol1e-8或更低终值精度要求高时设1e-10trust-constrxtol1e-8与gtol配合使用trust-constrverbose设1或2可输出迭代日志非常有利于调试共同bounds尽量使用Bounds对象或(min, max)列表不要替代约束6. 一点个人经验最后分享几个我实际工作中沉淀下来的习惯。第一我现在的默认流程是先判断问题的规模和约束类型如果以线性约束为主就用LinearConstraint如果非线性约束多就选 trust-constr 并尽量把雅可比写上。SLSQP 只在问题规模小、约束简单、快速验证公式时使用。第二所有约束函数都单独写成命名函数不要图省事堆一堆 lambda。因为排查问题时可以单独对每个约束函数传入测试点看看返回值和符号方向是否符合预期。lambda 写多了项目管理层面很痛苦。第三也是我认为最实用的一点不管是 SLSQP 还是 trust-constr在正式跑约束优化之前先跑一遍完全无约束或只有bounds的版本确认目标函数本身没有写错。然后再逐个加入约束每加一个就跑一次观察结果变化。这样可以快速定位是哪条约束引入的符号错误或冲突。我就是用这个方法把那个排程优化模块里原本纠缠不清的约束问题一个一个拆掉的。最后再补一个小技巧如果你发现 trust-constr 的结果总是不稳定先检查约束函数是否返回的是 Pythonlist而不是 NumPyndarray。SciPy 在内部做向量运算时list和ndarray的行为差异会造成一些隐蔽的错误比如广播维度不对、数据被自动展平。我遇到过不止一次全部改成np.array返回后问题自动消失。这个内容后续如果要做扩展我打算再写一篇关于大规模稀疏约束场景下如何用LinearOperator减小内存占用、提升 trust-constr 迭代效率的实操笔记。如果你也在生产环境里跑这类优化强烈建议提前研究这个方向。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价