2.2 优化与根查找 本节摘要:scipy.optimize 把"找极值"和"找零点"两类问题装进同一个模块:minimize 提供 BFGS、L-BFGS-B、SLSQP 等求解器,覆盖无约束、边界约束与等式不等式约束;curvefit 与 leastsquares 面向数据拟合,输出参数协方差用于不确定度评估;fsolve 求解非线性方程组。本节的核心判断:选对方法、给对初值、读懂收敛标志,比任何调参技巧都重要。 本节地图 阅读完本节,你应当能够: 按"导数信息、边界、约束"三个维度为 minimize 选择求解器; 读懂优化结果的 success、nit、message 等字段,判断收敛是否可信; 用 curvefit 做非线性拟合,并从协方差矩阵计算参数标准差;
本节摘要:scipy.optimize 把"找极值"和"找零点"两类问题装进同一个模块:minimize 提供 BFGS、L-BFGS-B、SLSQP 等求解器,覆盖无约束、边界约束与等式不等式约束;curve_fit 与 least_squares 面向数据拟合,输出参数协方差用于不确定度评估;fsolve 求解非线性方程组。本节的核心判断:选对方法、给对初值、读懂收敛标志,比任何调参技巧都重要。
阅读完本节,你应当能够:
做实验的同事递来一组数据:不同底物浓度下的反应初速率。他想要两个数——最大反应速率和米氏常数。这就是优化问题:模型形式已知,找一组参数让模型曲线离数据点最近。
另一个常见场景是解方程。结构力学里算梁的临界载荷,公式写出来是个关于载荷的非线性方程,没有解析解,只能数值逼近。求根和优化看起来是两件事,实际上互为表里:不少求根算法把方程改写成某个函数,然后去找这个函数的极小值,因为零点的平方必然非负,最小值接近零就找到了根。所以 SciPy 把两者放进同一个模块,不是偷懒,是数学上本来就连着。
我们先把最要紧的结论放在前面:优化器不是魔法。它从你给的初值出发,沿某种方向迭代,只保证找到局部最优。初值给得离谱、变量尺度差异悬殊、方法选错,结果就是报错,或者一个看起来合理的坏答案。这一节把这几类失败挨个讲清楚,然后给出一张可以照着选的决策地图。
minimize 是一个调度器,method 参数决定背后的算法。按问题特征,选型逻辑如下:
先跑一个无约束的经典例子。Rosenbrock 函数是优化界的试金石——它的最小值在参数全为一处,但通往最小值的路是个弯曲的峡谷,不少算法会在谷壁之间来回震荡:
import numpy as np from scipy.optimize import minimize def rosen(x): return sum(100.0 * (x[1:] - x[:-1]**2)**2 + (1 - x[:-1])**2) res = minimize(rosen, np.array([-1.2, 1.0]), method="BFGS") print(res.success, res.nit, res.x)
BFGS 是拟牛顿法,用梯度信息近似二阶导数,不显式计算 Hessian 矩阵,中小规模无约束问题里它是默认首选。收敛速度通常比无导数方法快一个量级,前提是能给出梯度——不给也行,minimize 会用有限差分近似,只是每一步都更贵。
只有边界约束时换 L-BFGS-B。它把 BFGS 的近似 Hessian 限制在少量向量上,内存占用小,还能保证变量始终落在边界内:
def shifted(x): return (x[0] - 2.0)**2 + (x[1] + 1.0)**2 res = minimize(shifted, [0.0, 0.0], method="L-BFGS-B", bounds=[(-5, 5), (-5, 5)]) print(res.success, res.x)
等式或不等式约束交给 SLSQP。比如"两个变量平方和最小,同时要求两者之和等于一",这是典型的带约束问题,SLSQP 能处理的远不止二次函数:
def objective(x): return x[0]**2 + x[1]**2 cons = [{"type": "eq", "fun": lambda x: x[0] + x[1] - 1.0}] res = minimize(objective, [0.0, 0.0], method="SLSQP", constraints=cons) print(res.success, res.x, res.fun)
约束写法的通用格式是列表套字典,type 指定等式或不等式,fun 给出约束函数,等式约束要求函数值为零,不等式约束要求函数值非负。约束越多,可行域越小,初值越要落在可行域附近,否则算法可能在第一步就找不到可行方向。
优化结果对象里,success 只是"算法宣称收敛",不等于"找到全局最优"。建议每次都看三个字段:success、nit 迭代次数、message 里的终止原因。常见的几种 message:梯度范数足够小,正常收敛;步长太小无法继续,可能卡在鞍点或数值噪声里;迭代次数超限,要么加 maxiter,要么换个初值重来。
另一个容易翻车的地方是变量尺度。目标函数里一个变量量级是 1e-6,另一个是 1e6,梯度方向基本被大尺度变量垄断,小变量几乎不被优化。解决办法是把变量归一化到同一量级再优化,或者用 x_scale 参数告诉算法各变量的尺度。这个细节在真实工程里出现频率极高,代价却只是多写一行参数。
还有一个省心的特例:只有一个变量时,别用通用 minimize,改用 minimize_scalar。它走黄金分割或布伦特搜索,不吃梯度、基本不吃初值,对一维曲线找谷底这类问题是最稳的入口。很多人习惯一个函数走天下,结果在一维问题上被初值坑了半天——工具箱里每把工具都有主场,选对主场的收益远大于调参。
拟合本质上是最小化残差平方和。curve_fit 面向"模型表达式已知"的场景,一行调用返回两样东西:最优参数和协方差矩阵:
from scipy.optimize import curve_fit rng = np.random.default_rng(7) t = np.linspace(0, 5, 50) y = 2.5 * np.exp(-0.8 * t) + 0.3 + 0.05 * rng.normal(size=t.size) model = lambda t, a, b, c: a * np.exp(-b * t) + c popt, pcov = curve_fit(model, t, y, p0=[1.0, 1.0, 0.0]) perr = np.sqrt(np.diag(pcov)) print("参数:", popt.round(3), "标准差:", perr.round(4))
协方差矩阵的对角线开方就是各参数的标准差,非对角线反映参数之间的相关性。衰减常数 b 与振幅 a 常常强相关:a 大一点、b 小一点,曲线形状几乎不变,于是两个参数一起涨一起跌,协方差矩阵里对应元素就大。这个信息比单看拟合曲线更值钱——它告诉你哪些参数被数据约束得死,哪些只是"看起来拟合了"。
初值 p0 给得好不好,直接决定成败。我们习惯先画散点图目测数量级再给 p0;对指数这类对初值敏感的函数,把 p0 放在对数尺度上估计往往更稳。curve_fit 默认用 Levenberg-Marquardt,它是信赖域方法,对初值的要求比 BFGS 还苛刻。返回参数是无穷大或协方差矩阵出现负对角线时,先回头检查模型是否参数过多、数据是否覆盖了参数敏感的区域。报告里写参数时也要克制:标准差保留两位有效数字就够,把自助法的四个小数位全抄上去,不是严谨,是过度精确。
当拟合问题需要边界、稳健损失或更细的控制时,用 least_squares 替代 curve_fit。它接受残差函数而不是模型函数,自由度大得多:
from scipy.optimize import least_squares res = least_squares(lambda x: model(t, *x) - y, x0=[1.0, 1.0, 0.0], bounds=([0, 0, 0], [10, 10, 10]), loss="soft_l1") print(res.x, res.cost)
loss 参数换成 soft_l1 或 huber,可以削弱离群点对拟合的拉扯——普通最小二乘里一个离群点就能把整条曲线拽偏,稳健损失让残差大的点权重自动降低。数据里有明显坏点时,先试稳健损失再决定是否剔除,比"直接删点"更诚实,也更不容易误删有用信息。
求根是优化的近亲。fsolve 基于 MINPACK 的混合方法,把方程组写成"向量函数等于零向量"的形式:
from scipy.optimize import fsolve def equations(x): return [x[0]**2 + x[1]**2 - 4.0, x[0] - x[1] - 1.0] sol = fsolve(equations, [1.0, 1.0]) print(sol, equations(sol))
圆与直线的交点有两个,fsolve 找到哪一个完全取决于初值。想要两个解,就分别从两个不同初值出发再解一次——这是求根问题的常态:多解不是 bug,是数学本身。返回值最好代入原方程验算残差,残差数量级小于容差才算真解。root 函数是更现代的接口,支持多种算法,需要处理大规模稀疏方程组时值得一试。
把反复出现的主题收拢一下:初值决定局部解,尺度决定收敛速度,方法决定可行性。给初值的实用套路有三条:一是从物理含义出发估数量级,浓度、速率、温度都有合理范围;二是用粗网格扫描一组初值,各自优化后取目标函数最小的结果;三是对数尺度拟合先得到量级,再回到原尺度精调。三条都试过还失败,问题大概率出在模型本身——先怀疑模型,再怀疑算法。

同一座山谷,两条路径。拟牛顿法带着梯度信息直奔谷底,路径平滑;无导数方法像摸着石头过河,路径曲折但每一步都只需要函数值。代价的差别在函数求值次数上,低维问题无所谓,高维或函数昂贵时,梯度信息省下的时间可能是一个量级。
| 问题特征 | 推荐方法 | 注意事项 |
|---|---|---|
| 无约束,能算梯度 | BFGS | 中小规模默认首选 |
| 无约束,梯度不可得 | Nelder-Mead | 收敛慢,适合低维 |
| 变量多且有边界 | L-BFGS-B | 内存友好,保证边界 |
| 等式或不等式约束 | SLSQP | 约束类型要写对 |
| 显式模型拟合 | curve_fit | 附带协方差矩阵 |
| 需要边界或稳健损失 | least_squares | 残差接口更灵活 |
| 解非线性方程组 | fsolve 或 root | 多解靠初值区分 |
拿到优化结果,先看 success 与 message,再代入原问题验算。对拟合,画残差图:残差随机散布在零附近,拟合可信;残差呈系统性弯曲,说明模型形式不对,参数再漂亮也没用。对求根,把解代回方程看残差范数。对约束问题,检查约束残差是否接近零。这一套"结果出来先验算"的习惯,比记住任何参数都值钱。
还有一个屡试不爽的自检招数:如果知道或猜得到最优解的大致位置,把它附近的一组点当初值喂回去,看算法能不能迅速收敛回原位。收敛不回去,要么梯度写错,要么目标函数有数值噪声,要么模型本身有病。这个测试十秒钟就能跑完,却能在你拿着坏结果出门之前拦住你。
⚠️ 常见坑:协方差矩阵算出负的标准差、参数停在边界上、success 为真但结果明显离谱——这三种情况多半是初值太差或模型参数过多。先简化模型、换初值、加边界,再谈调算法参数。
💡 关键直觉:优化结果的收敛标志只说明"在这个初值的邻域内找到了局部最优",它不回答"全局是不是这里"。多初值扫描是成本最低的全局性检查,十次里九次能救你于局部陷阱。
下一节我们处理"补曲线"的问题:插值与样条。拟合是"找规律",插值是"补空缺",两者常被混用,但适用的数据条件完全不同。