0%

SciPy数值优化

SciPy 数值优化:从最小二乘到非线性求解,遇到什么问题该用什么

写代码做数据处理,很多问题最终都能归结成一个核心需求:找到一组最优的参数

  • 拟合一条曲线 → 找参数使误差最小
  • 控制一个系统 → 找参数使性能最好
  • 解一个方程组 → 找一组值使所有等式成立

SciPy 的 optimize 模块就是干这个的。但问题是:算法太多了——leastsqcurve_fitfminfmin_bfgsfsolve……新手看了直接懵。


最小二乘拟合:数据点 + 模型 = 找参数

90% 的优化需求都能归到这一类:你有数据点,你有一个带参数的模型,你想让模型尽可能贴合数据。

标准写法(老派):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from scipy.optimize import leastsq
import numpy as np

x = np.array([1, 2, 3, 4, 5])
y = 2.5 * x + 1.3 + np.random.randn(5) * 0.5

def residuals(p, x, y):
k, b = p
return y - (k * x + b)

p0 = [1.0, 0.0]
result = leastsq(residuals, p0, args=(x, y))
k, b = result[0]
print(f"k={k:.3f}, b={b:.3f}")

更友好的写法(推荐):

1
2
3
4
5
6
7
from scipy.optimize import curve_fit

def model(x, k, b):
return k * x + b

popt, pcov = curve_fit(model, x, y, p0=[1.0, 0.0])
k, b = popt

curve_fitleastsq 好用得多——不用自己写残差函数,不用操心 args 传参方式,直接定义模型函数就行。新代码优先用 curve_fit

拟合正弦波(复杂模型示例):

1
2
3
4
def model(x, A, freq, phase, offset):
return A * np.sin(2 * np.pi * freq * x + phase) + offset

popt, pcov = curve_fit(model, x, y, p0=[0.8, 4.5, 0.0, 0.0])

曲线拟合的核心原则:初始值要给对。 给错了,结果可能完全不收敛。如果不知道初始值怎么给,先画图看一眼数据的大致形态,再反推参数范围。

函数最小值:没有数据点,只有函数

有些问题没有”数据点”,只有一个目标函数——你找一组参数,让这个函数的值最小。

典型例子:Rosenbrock 函数——优化算法的”试金石”。

f(x,y)=(1−x)2+100(y−x2)2f(x,y)=(1−x)2+100(yx2)2

它在 (1,1) 处有最小值 0,但有一条狭窄的”山谷”,算法很容易在里面打转。

不用导数(Nelder-Mead,简单问题用):

1
2
3
4
5
6
7
from scipy.optimize import fmin

def rosenbrock(p):
x, y = p
return (1 - x)**2 + 100 * (y - x**2)**2

result = fmin(rosenbrock, [-2, 2])

用导数(BFGS,收敛快):

1
2
3
4
5
6
7
8
9
10
from scipy.optimize import fmin_bfgs

def rosenbrock_prime(p):
x, y = p
return np.array([
-2*(1-x) - 400*x*(y - x**2),
200*(y - x**2)
])

result = fmin_bfgs(rosenbrock, [-2, 2], fprime=rosenbrock_prime)

关键判断:如果你的目标函数能求导,一定要给导数。 BFGS 比 Nelder-Mead 快得多,变量越多差距越大。

算法选择的决策逻辑:

1
2
3
4
5
6
7
8
9
问题是什么类型?

├── 有数据点 + 有模型 → curve_fit(最小二乘拟合)

├── 有目标函数(最小值问题)
│ ├── 能求导 → fmin_bfgs(变量 < 100)/ fmin_cg(变量 > 100)
│ └── 不能求导 → fmin(变量 < 20)/ fmin_powell(变量 > 20)

└── 要求解方程组 → fsolve

非线性方程组:找一组值让多个等式同时成立

你需要找一组 (x, y, z),让一堆方程同时等于 0。

1
2
3
4
5
6
7
8
9
10
11
12
from scipy.optimize import fsolve

def equations(p):
x, y, z = p
return [
x**2 + y**2 - 1, # 单位圆
x - z, # x = z
y - np.sin(z) # y = sin(z)
]

solution = fsolve(equations, [0.5, 0.5, 0.5])
# 约等于 [0.786, 0.618, 0.786]

fsolve 的两个核心问题:

1. 初始值很关键。 给的不好,收敛到奇怪的结果或者干脆不收敛。

2. 方程组越大,越要提供雅可比矩阵。 雅可比矩阵就是”每个方程对每个变量的偏导数”。

1
2
3
4
5
6
7
8
9
def jacobian(p):
x, y, z = p
return [
[2*x, 2*y, 0],
[1, 0, -1],
[0, 1, -np.cos(z)]
]

solution = fsolve(equations, [0.5, 0.5, 0.5], fprime=jacobian)

实际经验: 50 个未知数、每个方程只跟 6 个变量相关的稀疏系统,提供雅可比矩阵能让求解速度提高 4-5 倍。

欢迎关注我的其它发布渠道