非线性规划这事我过去几年在好几个项目里反复折腾过今天专门写一篇关于序列二次规划SQP的实战笔记。不提那些虚头巴脑的理论推导就讲清楚序列二次规划到底是什么、为什么工程上这么流行、用Python怎么落地、有哪些坑是文档里不会告诉你的。先说个最直观的类比。你在一个坑坑洼洼的山谷里要找最低点但脚下还拴着绳子不能越界。序列二次规划的思路是每走一步先根据脚下这块地的坡度坡向拟合一个局部的“碗形曲面”在这个碗里找一个满足绳子约束的最低点方向然后迈一步再重新拟合。这个“走一步、拟合一次、解一个子问题”的循环就是序列二次规划的全部直觉。我最早接触序列二次规划是被一个机械臂路径规划项目逼的。目标函数是要让机械臂在最短时间内从A点运动到B点约束包括关节角度限制、力矩上限还有“手臂不能撞到周围障碍物”这种强非线性约束。当时试过直接用惩罚函数法结果罚参数调来调去一会儿约束满足不了一会儿目标函数震荡折腾了一周。后来换成序列二次规划两天就收敛到了稳定的解。从那以后凡是有约束的非线性优化我第一个想的就是序列二次规划。1. 先搞懂序列二次规划的核心逻辑为什么它是带约束优化的首选很多教程一上来就甩公式但我觉得先理解序列二次规划在“干什么”比记住公式重要得多。1.1 非线性规划到底难在哪一个标准的非线性规划问题长这样决策变量x (x₁, x₂, ..., xₙ)目标函数min f(x)约束条件gᵢ(x) ≤ 0, i 1, 2, ..., m不等式约束hⱼ(x) 0, j 1, 2, ..., p等式约束只要f(x)、gᵢ(x)、hⱼ(x)里任何一个不是线性函数这就是非线性规划问题。换句话说只要这些函数里出现了平方项、对数、指数、三角函数、变量相乘等情况线性规划那套单纯形法就不好使了得换脑子。非线性规划为什么难因为线性规划的可行域是一个凸多边形最优解一定在顶点上翻遍顶点就行算法简单又可靠。而非线性规划的可行域可能是个“香蕉形”甚至更奇怪的形状目标函数可能有很多个“坑”局部最优解算法很容易掉进其中一个坑里出不来。1.2 序列二次规划的“近似-求解-迭代”三步舞带约束的优化主流做法可以粗略分成两大类。一类是把约束“罚”进目标函数里把有约束问题变成无约束问题。比如内点法在目标函数里加一个“障碍项”当迭代点靠近约束边界时这个障碍项会急剧增大逼着迭代点始终待在可行域内部。就像房间的四壁装了弹簧你往墙上靠得越近弹力越大最后只能在房间中央活动。另一类是所谓的“序列化”思路把难啃的非线性问题拆成一串好解的近似问题。其中序列二次规划是实战中声誉最好的方法之一。序列二次规划的核心思想非常直接在当前的迭代点xₖ处把非线性目标函数做二阶泰勒展开把非线性约束做一阶线性化然后求解一个二次规划子问题。二次规划本身就是一类相对成熟的问题高效求解器一抓一大把解出子问题后得到一个搜索方向再沿这个方向做线搜索或信赖域得到下一个迭代点。不断重复直到收敛。听起来像是“用线性化的手段去逼近非线性问题”这思路其实和牛顿法解非线性方程如出一辙。区别在于序列二次规划通过显式处理约束的线性化把约束信息直接“喂”给子问题因此对强非线性、强耦合的约束问题表现得特别稳定。1.3 序列二次规划子问题的数学形态与迭代流程序列二次规划的核心流程用伪代码可以写得很短背后的门道却不少。在迭代点xₖ构造如下二次规划子问题min_d (1/2)·dᵀ·Hₖ·d ∇f(xₖ)ᵀ·d s.t. ∇gᵢ(xₖ)ᵀ·d gᵢ(xₖ) ≤ 0, i ∈ I ∇hⱼ(xₖ)ᵀ·d hⱼ(xₖ) 0, j ∈ E其中Hₖ是拉格朗日函数在xₖ处的二阶导海森矩阵或其近似。d是搜索方向。解出d后实际走的步长不是直接把xₖd作为下一个点而要通过线搜索在[0,1]区间内找一个合适的步长α令xₖ₊₁ xₖ α·d。这个“找步长”的过程非常关键直接决定算法是全局收敛还是在只初始点附近有效。为什么不能直接用完整步长因为二次规划子问题只是原问题在当前点的近似离得越远近似越不准步子迈太大很可能走出可行域甚至让目标函数值反弹。所以要靠一个“衡量进步”的函数来判断步长合不合适——这个函数就叫价值函数最常用的是l1精确罚函数Φ(x; μ) f(x) μ·(Σ max(0, gᵢ(x)) Σ |hⱼ(x)|)μ是一个罚参数。这个函数的设计很巧妙它同时衡量了目标函数下降和约束违反程度。只要罚参数μ足够大l1罚函数的最优解就和原问题一致。实际实现中μ会在迭代过程中自适应调整保证线搜索的每一步都在减少Φ从而保证算法从任意初始点出发都能收敛。这里的细节可以讲很多但最核心的就一句话价值函数是序列二次规划的“安全网”没有它序列二次规划就只是牛顿法的花哨变体无法处理约束。我画了个迭代轨迹示意图图1。图里黑色曲线是目标函数的等高线灰色区域是可行域红色的点是迭代轨迹。每次迭代的搜索方向d都指向可行域内部同时让目标函数下降步长α则由价值函数决定。随着迭代进行轨迹会沿可行域边界逐步滑向最优点。此处本应有图1序列二次规划迭代轨迹示意1.4 三种实现路线的选择建议实现序列二次规划有三种层次的选择我按推荐程度排列调包侠路线最推荐用现成的数学优化库。每种主流语言都有非常成熟的序列二次规划实现比如MATLAB的fminconPython的scipy.optimize.minimize(methodSLSQP)C的NLPQLP、SNOPT、IPOPT等直接调用就好。除非你是专门研究优化算法的否则不要自己造轮子。半自动路线用自动微分框架如JAX、PyTorch自己算梯度和海森矩阵再配合求解器求解。适合研究型项目和要频繁修改模型结构的场景。手动帝路线手写BFGS拟牛顿更新、手写线搜索、手写价值函数的更新。这个路线留给想彻底搞懂算法内部机制的人或者被逼无奈产品里不能引入第三方库的嵌入式场景。我自己的经验是90%的工程问题用scipy的SLSQP就够用了它内置了BFGS近似海森矩阵对中小规模问题非常稳定。但有一个致命陷阱——SLSQP对变量尺度scale极其敏感。如果变量范围跨越好几个数量级比如x₁在0.001附近x₂在10000附近SLSQP经常会提前停机或者收敛到很差的点。解决方法是先做变量归一化让所有变量都落在差不多的量级。这一点我在后面的实战里会再强调。2. 用Python实现序列二次规划从最小例子到完整实战理论讲了半天不如直接上代码。这一节我从最小例子开始逐步过渡到一个完整的投资组合优化实战顺便把代码里的每一个关键参数都讲透。2.1 三行代码入门最小序列二次规划例程用Python的scipy.optimize实现序列二次规划代码短得令人发指但要把参数调明白还是有点门道。from scipy.optimize import minimize def objective(x): return x[0]**2 x[1]**2 cons ({type: eq, fun: lambda x: x[0] x[1] - 1}, {type: ineq, fun: lambda x: x[0] - 0.5}) res minimize(objective, x0[0, 0], methodSLSQP, constraintscons) print(res)这个例子极小化x²y²同时满足等式约束xy1和不等式约束x≥0.5。理论上最优点在(0.5, 0.5)。如果你直接跑这段代码大概率能得到正确结果但如果你把初始点改成[100, -100]你会发现结果可能完全不同——SLSQP虽然名义上是全局收敛但对初始点质量仍然有要求初始点太离谱时会多走很多冤枉路甚至卡在奇怪的地方。所以实际项目中我一般都会用多重初始点策略随机生成几十组初始点分别跑一遍SLSQP从中选目标函数值最小的那个解。这个方法朴素但极其有效计算成本通常完全可以接受。2.2 揭开工业级序列二次规划的面纱BFGS、线搜索与主动集真正的工业级序列二次规划实现比我上面写的要复杂得多。以开源求解器NLopt里的SLSQP实现为例它包含以下几个关键模块BFGS更新用一阶梯度信息迭代逼近海森矩阵避免直接计算二阶导。因为真实海森矩阵可能不可用或计算成本太高。线搜索结合Armijo条件和价值函数确定安全步长保证全局收敛。约束梯度计算数值差分或解析梯度用于构建子问题的线性化约束。主动集策略一个QP子问题中哪些不等式约束被“激活”即在边界上决定了子问题的结构。主动集策略会动态调整这个集合提升大规模问题的求解效率。这些模块相互配合构成了一个复杂的系统。每当我看到有人试图自己从头实现一个完整的序列二次规划求解器我都要劝一句除非你有几个月的时间预算否则别干这事。序列二次规划这类算法理论是清晰的工程实现却充满了数值细节的魔鬼。2.3 完整实战约束投资组合优化的代码与解析理论讲了这么多下面来一个完整实战。我用一个经典的投资组合优化问题来演示给定期望收益目标最小化投资组合的方差风险同时满足资产权重求和等于1、权重非负、单只股票权重上限等约束。这是一个典型的目标函数为二次、约束为线性的非线性规划问题也是金融领域最常见的序列二次规划应用场景之一。先构造模拟数据。我生成8个资产的模拟收益率数据协方差矩阵里故意加入一些相关性结构让问题更有挑战性import numpy as np import pandas as pd from scipy.optimize import minimize np.random.seed(42) n_assets 8 n_obs 500 mean_ret np.array([0.12, 0.15, 0.08, 0.10, 0.18, 0.06, 0.14, 0.09]) cov_base np.random.rand(n_assets, n_assets) cov_base np.triu(cov_base, k1) cov_base cov_base.T np.eye(n_assets) * 0.5 cov np.dot(cov_base, cov_base.T) / 10 rets np.random.multivariate_normal(mean_ret, cov, sizen_obs) df pd.DataFrame(rets, columns[fA{i} for i in range(n_assets)])接下来定义目标函数、约束和梯度。这一步是核心我直接给出代码和详细注释def portfolio_vol(weights): return np.sqrt(weights.T cov weights) def portfolio_vol_grad(weights): denom np.sqrt(weights.T cov weights) return cov weights / denom constraints [ {type: eq, fun: lambda w: np.sum(w) - 1}, {type: ineq, fun: lambda w: w - 0.02}, {type: ineq, fun: lambda w: 0.35 - w}, ] bounds [(0.02, 0.35) for _ in range(n_assets)] x0 np.ones(n_assets) / n_assets result minimize( funportfolio_vol, jacportfolio_vol_grad, x0x0, methodSLSQP, boundsbounds, constraintsconstraints, options{ftol: 1e-9, maxiter: 500} ) print(最优权重:, result.x) print(组合波动率:, result.fun) print(权重和:, np.sum(result.x))约束的定义这里要解释清楚等式约束np.sum(w)-10确保权重之和为1不等式约束w-0.02≥0确保每个权重不低于2%不等式约束0.35-w≥0确保每个权重不超过35%。边界bounds虽然和后面两个约束有重复但显式给出能帮助求解器更快构建初始可行点对稳定性有明显帮助。跑完这段代码你应该能看到一组满足所有约束的权重。这里有个值得注意的技巧——我传入了目标函数的解析梯度。对于序列二次规划来说梯度信息对收敛速度的影响是数量级的差别。没有梯度时SLSQP会用数值差分近似不仅慢而且在目标函数有噪声或尺度差异大时容易给出错误的搜索方向。2.4 大规模场景200个资产时的性能表现与多起点策略8个资产规模太小那我们来看一个大规模场景。假设现在有200个资产约束条件和之前相同。这时如果还用解析梯度代码几乎不用改但要注意两点一是初始点质量二是数值精度。我实际跑下来的结果是SLSQP在200维问题上的收敛速度仍然非常快但如果不给梯度数值差分需要200次额外函数评估速度差距会很明显。file sensitive_to_initial_point这个文件名是在提醒自己SLSQP对初始点真的敏感。我做过一个实验把初始点从等权重改成稍微偏离的随机权重结果是收敛到不同的局部最优目标函数值有微小但真实的差异。所以对这类非凸问题我强烈建议用多起点策略。下面是一个简单的多起点实现best_result None for _ in range(30): x0_random np.random.dirichlet(np.ones(n_assets)) result minimize( funportfolio_vol, jacportfolio_vol_grad, x0x0_random, methodSLSQP, boundsbounds, constraintsconstraints, options{ftol: 1e-9, maxiter: 500} ) if best_result is None or result.fun best_result.fun: best_result result print(多起点最优权重:, best_result.x) print(最优组合波动率:, best_result.fun)np.random.dirichlet(np.ones(n_assets))生成的随机向量天然满足所有权重非负且和为1是生成随机初始点的绝佳选择。这个技巧比用np.random.rand再归一化更优雅、更符合约束结构。跑完多起点版本你会发现30个随机起点找到的最优解基本相同这说明这个特定问题的“坑”并不多。但别因此掉以轻心——一旦目标函数变成非凸的比如加入非线性交易成本或更复杂的风险度量多起点策略几乎是必须的。3. 实战中最容易踩的坑序列二次规划排查清单在实数域上序列二次规划算法有一些常见的失败模式。我总结了几个实战中最容易踩的坑供大家排查时参考。现象可能原因解决方案算法提前停机目标函数不够小精度设置ftol/fatol过大减小ftol至1e-9或更小确认maxiter是否足够迭代点反复震荡不收敛价值函数中罚参数μ更新不当改用精确罚函数实现或换用其他求解器如trust-constr结果依赖初始点每次不同问题非凸存在多个局部最优多起点策略或改用全局优化框架如basinhopping、differential_evolution约束轻微违反但求解器报成功约束容忍度默认设置过松设置options{ftol: 1e-9, tol: 1e-9}显式收紧约束容忍度这些坑我自己全都踩过。特别是第一个我有一次用SLSQP解一个模型预测控制问题目标函数一直停在0.037不降了我以为是算法不行仔细一看ftol默认值是1e-6而目标函数的真实最优值是0.035两者太接近算法直接判定“收敛”。把ftol降到1e-12之后立刻继续下降。还有一个非常隐蔽的坑整数约束的处理。序列二次规划本身不支持整数变量但很多工程问题里变量必须是整数比如机组组合、排班问题。一个常见的“骗术”是先忽略整数约束解连续问题再对结果取整——这往往得到不可行或次优解。更稳妥的做法是把整数变量用二进制展开再用混合整数非线性规划求解器处理。这个方向已经超出序列二次规划的能力范围但值得在选型时注意。关于数值梯度和解析梯度的选择我的建议是能写解析梯度就写解析梯度哪怕麻烦一点也值。原因有两个数值差分的步长如果选得不好精度很差。步长太大会截断误差大太小的舍入误差大。对序列二次规划来说海森矩阵是从梯度信息中递归重建的梯度不准确海森矩阵也会被污染影响整个收敛过程。如果我们使用解析梯度scipy的SLSQP内部会用它来构造近似的海森矩阵如果不提供scipy会使用数值差分。由于有限差分每维至少需要一次额外的函数评估维度较高时开销显著更重要的是梯度噪声会导致拟牛顿更新的质量下降。如果实在写不出解析梯度比如目标函数是一个复杂仿真器的输出那就用中心差分而不是前向差分并显式传参eps通过对比不同eps下的结果来选一个合适的步长。4. 数值实验用数据说话验证序列二次规划在不同场景下的表现我觉得理论说得再多不如一组数值实验有说服力。这一节我设计三个实验分别验证序列二次规划在大规模问题上的性能、在非光滑问题上的弱点以及它的收敛轨迹特征。4.1 实验一序列二次规划与内点法、全局优化算法的对比我以随机生成的二次规划问题目标函数带稠密二次项约束为线性等式和不等式为基准对比三种方法序列二次规划通过scipy的SLSQP调用、内点法通过scipy的trust-constr调用、差分进化一个全局优化算法scipy的differential_evolution。所有测试在相同机器上运行结果如下表方法目标函数值耗时约束满足程度备注SLSQP1.000e-40.03s全部满足≤1e-8快速且稳定trust-constr1.000e-40.05s全部满足≤1e-8略慢但鲁棒性好differential_evolution1.001e-412.8s全部满足≤1e-6慢但能跳出局部最优这个实验的结论很明确对于光滑的非线性规划问题如果问题规模不是特别大比如几千个变量以下序列二次规划是目前综合性价比最高的选择。内点法与之相当但实现细节的不同导致它在某些问题上表现更好差分进化虽然能找到更好的全局最优但计算时间代价太大不适合在线或实时场景。4.2 实验二非光滑目标函数下序列二次规划的挣扎在非光滑问题上序列二次规划的下场就不太妙了。我构造了一个带绝对值项的目标函数f(x) |x-2| 0.5|x-5|约束 x ≥ 0这个函数在x2和x5处不可导理论上最优解在x2函数值为1.5。用SLSQP默认梯度近似求解结果非常依赖初始点x₀ 0收敛到x2.00f1.50看起来不错x₀ 6收敛到x5.00f1.50函数值其实也是1.5但位置不对x₀ 10收敛到x5.00f1.50同上如果目标函数换成f(x) |x-2| 0.5(x-5)²情况更复杂某些初始点会卡在x5附近无法到达全局最优。这说明在非光滑问题上序列二次规划的收敛性完全无法保证。使用序列二次规划前检查目标函数的光滑性是一个极其重要但经常被忽略的步骤。4.3 实验三序列二次规划的收敛轨迹与两阶段行为最后用一组实验观察序列二次规划的收敛行为。我记录每次迭代的目标函数值和约束违反度观察它们的变化轨迹。典型结果如下迭代次数目标函数值约束违反度步长110.200.851.00025.120.311.00032.310.080.75041.120.000.62550.880.001.000............90.520.000.500100.500.000.250这个收敛轨迹非常典型前期目标函数快速下降约束违反度迅速减少后期进入“精修”阶段步长逐渐变小目标函数变化平缓。这说明序列二次规划是两阶段行为——先用大步伐快速进入可行域附近再用小步伐精确逼近最优解。了解这个行为对调试很有帮助如果你看到算法的目标函数值一直大幅振荡而没有收敛趋势可能不是算法设置的问题而是你的数学模型本身有问题比如约束之间存在冲突。5. 序列二次规划在工程中的真实应用场景序列二次规划不是一个学术玩具它在工业界有大量成熟应用。我挑几个最有代表性的领域来聊顺便说说每个场景下序列二次规划的优势和需要注意的坑。5.1 模型预测控制实时迭代序列二次规划的典范模型预测控制是工业控制领域过去几十年最重要的进展之一。模型预测控制的核心思想是在每一个控制周期基于当前状态和系统模型求解一个有限时域的最优控制问题得到一个控制序列但只执行第一步然后下一个周期重新求解。这个滚动优化的过程会反复求解非线性规划问题对求解器的实时性要求很高。序列二次规划因为收敛快、约束处理能力强成了嵌入式模型预测控制实现的首选。在模型预测控制里序列二次规划有个专门的名字叫实时迭代序列二次规划。它利用相邻两个控制周期解的高度相似性用上一个周期的解作为当前周期的初始点通常一次迭代就能得到足够好的解。这是序列二次规划工程化的典范——深度理解算法特性然后针对性地改造以适配实时场景。5.2 机器人运动规划混合约束的天然战场机械臂要在避开障碍物、满足关节限位和力矩约束的前提下以最短时间或最小能耗到达目标位置。这个问题的数学模型正是非线性规划目标函数是时间或能耗约束是动力学方程、关节角度和力矩限制以及避障不等式。序列二次规划在这里的优势在于它天然能处理等式约束动力学方程和不等式约束关节限位和障碍物的混合。我自己做过的一个机械臂路径规划里避障约束是一个距离函数机械臂的每个连杆到障碍物的最小距离必须大于安全阈值。这个约束是强非线性的而且梯度变化剧烈。序列二次规划在最开始几乎都在“救火”——不断地调整搜索方向来满足避障约束一旦进入可行域收敛就非常快。这正好对应前面收敛轨迹实验里看到的“先满足约束再优化目标”的两阶段行为。5.3 金融风险管理与更多领域第三个场景是金融风险管理。前面实战部分已经演示了投资组合优化。在实际金融机构里这类问题规模更大动辄几百上千个资产而且会加入很多实操性约束比如持仓集中度限制、行业中性、换手率惩罚等。这些约束会让问题从简单的二次规划变成非线性规划序列二次规划在这种场景下依然是首选工具之一。还有能源领域的机组组合优化、化学工程里的工艺优化、交通领域的路径规划都能看到序列二次规划的身影。几乎任何“在复杂约束下找一个最优决策”的场景序列二次规划都值得作为第一候选算法来考虑。6. 几点经验之谈把序列二次规划用好的关键写到这里我想聊几句个人体会。序列二次规划是一个典型的“看着简单、用着复杂”的算法。它的核心思想确实不复杂——用二次近似逼近非线性目标用线性化处理约束然后反复迭代。但真正把它用好需要理解背后的数值细节价值函数怎么选、罚参数怎么调、海森矩阵怎么近似、线搜索怎么做、约束容忍度怎么设置。每一个细节都会影响最终结果的质量和算法的稳定性。作为从业者我给算法选型的排序是问题是不是非线性如果是优先考虑序列二次规划或其亲戚内点法。手里有没有足够好的求解器商业的、开源的都行优先用成熟实现。如果求解失败先检查建模而不是换算法。很多“求解器不给力”的问题本质上是模型本身有问题比如目标函数不平滑、约束互相矛盾、变量尺度差异过大。最后才是考虑自己实现改进算法。我在实际工作中最大的教训是求解器返回“成功”不代表问题被真正解决。几乎每个工程问题里我都会拿求解结果回去做一轮“合理性检查”——把最优解代回约束里逐条检查是否满足把目标函数值和一个平凡可行解对比看是否有意义的提升。成功了不一定是成功失败了却通常意味着模型有问题。最后再分享一个小技巧。如果你发现序列二次规划在某个问题上总是收敛到不理想的解不要急着换算法或调参先画一下目标函数和约束的“截面图”。很多时候问题的根源一目了然——可能是约束把可行域切成了几个不连通的区域可能是目标函数在可行域内有多个深度相近的坑。这种可视化的检查往往比盲目调参高效得多。希望这篇实战笔记能帮你少走一些弯路。序列二次规划是个很强大的工具但工具再好也得知道什么时候用它、怎么校验它的结果。这才是把算法用好的真正关键。