优化(scipy.optimize
)#
scipy.optimize
包提供了几种常用的优化算法。提供了详细清单:scipy.optimize
(也可以通过 help(scipy.optimize)
查找)。
多元标量函数的无约束最小化(minimize
)#
scipy.optimize 中的 minimize
函数为多元标量函数的无约束和约束最小化算法提供一个常见的接口。scipy.optimize
。为了说明最小化函数,请考虑最小化 \(N\) 变量的 Rosenbrock 函数:
该函数的最小值为 0,当 \(x_{i}=1.\) 时达到此值。
请注意,Rosenbrock 函数及其导数包含在 scipy.optimize
中。以下部分中显示的实现提供了有关如何定义目标函数及其雅可比函数和海塞函数的示例。scipy.optimize 中的目标函数将其第一参数预期为 numpy 数组,该数组将被优化,并且必须返回一个浮点值。准确的调用签名必须为 f(x, *args)
,其中 x
表示 numpy 数组,args
表示传给目标函数的其他参数的元组。
Nelder-Mead 单纯形算法 (method='Nelder-Mead'
)#
在下面的示例中,minimize
例程用于具有 Nelder-Mead 单纯形算法(通过 method
参数选择)
>>> import numpy as np
>>> from scipy.optimize import minimize
>>> def rosen(x):
... """The Rosenbrock function"""
... return sum(100.0*(x[1:]-x[:-1]**2.0)**2.0 + (1-x[:-1])**2.0)
>>> x0 = np.array([1.3, 0.7, 0.8, 1.9, 1.2])
>>> res = minimize(rosen, x0, method='nelder-mead',
... options={'xatol': 1e-8, 'disp': True})
Optimization terminated successfully.
Current function value: 0.000000
Iterations: 339
Function evaluations: 571
>>> print(res.x)
[1. 1. 1. 1. 1.]
单纯形算法可能是最小化行为良好的函数的最简单方法。它仅需要函数评估,并且是简单最小化问题的良好选择。但是,因为它不使用任何梯度评估,所以可能需要更长的时间才能找到最小值。
另一种仅需通过函数调用即可寻找最小值的优化算法是使用Powell方法,可以通过设置 method='powell'
来使用该方法,在 minimize
中进行设置。
为了演示如何为目标函数提供其他参数,让我们以附加缩放因子 a 和偏移量 b 来最小化 Rosenbrock 函数
同样使用 minimize
例程,可以通过以下代码块针对示例参数 a=0.5
和 b=1
来解决此问题。
>>> def rosen_with_args(x, a, b):
... """The Rosenbrock function with additional arguments"""
... return sum(a*(x[1:]-x[:-1]**2.0)**2.0 + (1-x[:-1])**2.0) + b
>>> x0 = np.array([1.3, 0.7, 0.8, 1.9, 1.2])
>>> res = minimize(rosen_with_args, x0, method='nelder-mead',
... args=(0.5, 1.), options={'xatol': 1e-8, 'disp': True})
Optimization terminated successfully.
Current function value: 1.000000
Iterations: 319 # may vary
Function evaluations: 525 # may vary
>>> print(res.x)
[1. 1. 1. 1. 0.99999999]
除了使用 minimize
的 args
参数外,我们还可以只接受 x
的新函数包装目标函数。当必须将其他参数作为关键字参数传递给目标函数时,此方法也非常有用。
>>> def rosen_with_args(x, a, *, b): # b is a keyword-only argument
... return sum(a*(x[1:]-x[:-1]**2.0)**2.0 + (1-x[:-1])**2.0) + b
>>> def wrapped_rosen_without_args(x):
... return rosen_with_args(x, 0.5, b=1.) # pass in `a` and `b`
>>> x0 = np.array([1.3, 0.7, 0.8, 1.9, 1.2])
>>> res = minimize(wrapped_rosen_without_args, x0, method='nelder-mead',
... options={'xatol': 1e-8,})
>>> print(res.x)
[1. 1. 1. 1. 0.99999999]
另一个替代方法是使用 functools.partial
。
>>> from functools import partial
>>> partial_rosen = partial(rosen_with_args, a=0.5, b=1.)
>>> res = minimize(partial_rosen, x0, method='nelder-mead',
... options={'xatol': 1e-8,})
>>> print(res.x)
[1. 1. 1. 1. 0.99999999]
Broyden-Fletcher-Goldfarb-Shanno 算法 (method='BFGS'
)#
为了更快地对解进行收敛,该例程使用了目标函数的梯度。如果用户没有提供梯度,那么它就会使用一阶差分进行估计。即使必须估计梯度,Broyden-Fletcher-Goldfarb-Shanno (BFGS) 方法通常也比单纯形算法需要更少的函数调用。
为了演示此算法,又一次使用 Rosenbrock 函数。Rosenbrock 函数的梯度是向量
此表达式对内部导数有效。特殊情况如下
通过该代码段构建一个用于计算此梯度的 Python 函数
>>> def rosen_der(x):
... xm = x[1:-1]
... xm_m1 = x[:-2]
... xm_p1 = x[2:]
... der = np.zeros_like(x)
... der[1:-1] = 200*(xm-xm_m1**2) - 400*(xm_p1 - xm**2)*xm - 2*(1-xm)
... der[0] = -400*x[0]*(x[1]-x[0]**2) - 2*(1-x[0])
... der[-1] = 200*(x[-1]-x[-2]**2)
... return der
此梯度信息在 minimize
函数中通过 jac
参数指定,如下所示。
>>> res = minimize(rosen, x0, method='BFGS', jac=rosen_der,
... options={'disp': True})
Optimization terminated successfully.
Current function value: 0.000000
Iterations: 25 # may vary
Function evaluations: 30
Gradient evaluations: 30
>>> res.x
array([1., 1., 1., 1., 1.])
避免冗余计算#
目标函数及其梯度通常会共用某些计算部分。比如,考虑以下问题。
>>> def f(x):
... return -expensive(x[0])**2
>>>
>>> def df(x):
... return -2 * expensive(x[0]) * dexpensive(x[0])
>>>
>>> def expensive(x):
... # this function is computationally expensive!
... expensive.count += 1 # let's keep track of how many times it runs
... return np.sin(x)
>>> expensive.count = 0
>>>
>>> def dexpensive(x):
... return np.cos(x)
>>>
>>> res = minimize(f, 0.5, jac=df)
>>> res.fun
-0.9999999999999174
>>> res.nfev, res.njev
6, 6
>>> expensive.count
12
这里,expensive
被调用 12 次:6 次出现在目标函数中,6 次出现在梯度中。减少冗余计算的一种方法是创建一个同时返回目标函数和梯度的单个函数。
>>> def f_and_df(x):
... expensive_value = expensive(x[0])
... return (-expensive_value**2, # objective function
... -2*expensive_value*dexpensive(x[0])) # gradient
>>>
>>> expensive.count = 0 # reset the counter
>>> res = minimize(f_and_df, 0.5, jac=True)
>>> res.fun
-0.9999999999999174
>>> expensive.count
6
调用 minimize 时,我们指定 jac==True
来表明提供的函数既返回目标函数,又返回其梯度。这种方法很方便,但并非所有 scipy.optimize
函数都支持此特性,而且仅可用来在函数及其梯度之间共享计算,然而在某些问题中我们会希望在 Hessian(目标函数的二阶导数)和约束条件之间共享计算。总体而言,更好地做法是对计算的昂贵部分进行记忆。在简单情况下,可以使用 functools.lru_cache
包装器来实现此目的。
>>> from functools import lru_cache
>>> expensive.count = 0 # reset the counter
>>> expensive = lru_cache(expensive)
>>> res = minimize(f, 0.5, jac=df)
>>> res.fun
-0.9999999999999174
>>> expensive.count
6
牛顿-共轭梯度算法 (method='Newton-CG'
)#
牛顿-共轭梯度算法是对牛顿方法所做的修改,该算法使用共轭梯度算法(近似地)对局部 Hessian [NW] 求逆。牛顿方法基于将函数局部拟合到二次形式
其中 \(\mathbf{H}\left(\mathbf{x}_{0}\right)\) 是二阶导数的矩阵(Hessian)。如果 Hessian 是正定的,则可以通过将此二次形式的梯度设为零来找到此函数的局部最小值,从而得到
使用共轭梯度法评估海森矩阵的逆矩阵。下面给出了使用此方法最小化 Rosenbrock 函数的示例。为了充分利用 Newton-CG 方法,必须提供一个计算海森矩阵的函数。不需要构建海森矩阵本身,最小化例程只需要一个矢量,该矢量是海森矩阵和任意矢量的乘积。因此,用户可以提供一个函数来计算海森矩阵,或一个函数来计算海森矩阵和任意矢量的乘积。
完整海森示例:#
Rosenbrock 函数的海森矩阵为
如果 \(i,j\in\left[1,N-2\right]\),其中 \(i,j\in\left[0,N-1\right]\) 定义 \(N\times N\) 矩阵。矩阵的其他非零项为
例如,当 \(N=5\) 时,海森矩阵为
以下示例中展示了计算此海森矩阵的代码以及使用 Newton-CG 方法最小化函数的代码
>>> def rosen_hess(x):
... x = np.asarray(x)
... H = np.diag(-400*x[:-1],1) - np.diag(400*x[:-1],-1)
... diagonal = np.zeros_like(x)
... diagonal[0] = 1200*x[0]**2-400*x[1]+2
... diagonal[-1] = 200
... diagonal[1:-1] = 202 + 1200*x[1:-1]**2 - 400*x[2:]
... H = H + np.diag(diagonal)
... return H
>>> res = minimize(rosen, x0, method='Newton-CG',
... jac=rosen_der, hess=rosen_hess,
... options={'xtol': 1e-8, 'disp': True})
Optimization terminated successfully.
Current function value: 0.000000
Iterations: 19 # may vary
Function evaluations: 22
Gradient evaluations: 19
Hessian evaluations: 19
>>> res.x
array([1., 1., 1., 1., 1.])
海森积示例:#
对于更大的最小化问题,存储整个海森矩阵会消耗大量时间和内存。Newton-CG 算法只需要海森矩阵和任意矢量的乘积。因此,用户可以提供代码来计算此乘积,而不是提供完整的海森矩阵,方法是提供一个 hess
函数,该函数将最小化矢量作为第一个参数,将任意矢量作为第二个参数(以及传递给要最小化函数的额外参数)。如果可能,对海森积选项使用 Newton-CG 可能大概是最小化此函数的最快速方法。
在这种情况下,Rosenbrock 海森矩阵乘以任意向量的结果计算起来并不困难。如果 \(\mathbf{p}\) 是任意向量,那么 \(\mathbf{H}\left(\mathbf{x}\right)\mathbf{p}\) 具有以下元素
代码使用这个海森矩阵乘积来最小化 Rosenbrock 函数,使用 minimize
如下
>>> def rosen_hess_p(x, p):
... x = np.asarray(x)
... Hp = np.zeros_like(x)
... Hp[0] = (1200*x[0]**2 - 400*x[1] + 2)*p[0] - 400*x[0]*p[1]
... Hp[1:-1] = -400*x[:-2]*p[:-2]+(202+1200*x[1:-1]**2-400*x[2:])*p[1:-1] \
... -400*x[1:-1]*p[2:]
... Hp[-1] = -400*x[-2]*p[-2] + 200*p[-1]
... return Hp
>>> res = minimize(rosen, x0, method='Newton-CG',
... jac=rosen_der, hessp=rosen_hess_p,
... options={'xtol': 1e-8, 'disp': True})
Optimization terminated successfully.
Current function value: 0.000000
Iterations: 20 # may vary
Function evaluations: 23
Gradient evaluations: 20
Hessian evaluations: 44
>>> res.x
array([1., 1., 1., 1., 1.])
依据 [NW] 的第 170 页 Newton-CG
算法在海森矩阵病态时效率低下,因为在这种情况下该方法提供的搜索方向质量不佳。据作者称,方法 trust-ncg
可以更有效地处理这种情况,下面将对此方法进行描述。
置信域 Newton-共轭梯度算法 (method='trust-ncg'
)#
Newton-CG
方法是一种线搜索方法:它找到一个方向,在这个方向上搜索函数的二次近似值,然后使用线搜索算法来找到该方向上的(接近)最佳步长。另一种方法是,首先固定步长限制 \(\Delta\),然后求解以下二次子问题,找到给定置信半径内的最佳步长 \(\mathbf{p}\)
然后更新解 \(\mathbf{x}_{k+1} = \mathbf{x}_{k} + \mathbf{p}\),并根据二次模型与真实函数一致的程度调整置信半径 \(\Delta\)。这种方法系列称为置信域方法。 trust-ncg
算法是一种置信域方法,它使用共轭梯度算法求解置信域子问题 [NW]。
全海森矩阵示例:#
>>> res = minimize(rosen, x0, method='trust-ncg',
... jac=rosen_der, hess=rosen_hess,
... options={'gtol': 1e-8, 'disp': True})
Optimization terminated successfully.
Current function value: 0.000000
Iterations: 20 # may vary
Function evaluations: 21
Gradient evaluations: 20
Hessian evaluations: 19
>>> res.x
array([1., 1., 1., 1., 1.])
海森矩阵乘积示例:#
>>> res = minimize(rosen, x0, method='trust-ncg',
... jac=rosen_der, hessp=rosen_hess_p,
... options={'gtol': 1e-8, 'disp': True})
Optimization terminated successfully.
Current function value: 0.000000
Iterations: 20 # may vary
Function evaluations: 21
Gradient evaluations: 20
Hessian evaluations: 0
>>> res.x
array([1., 1., 1., 1., 1.])
置信域截断广义 Lanczos / 共轭梯度算法 (method='trust-krylov'
)#
类似于 trust-ncg
方法,trust-krylov
方法是一种适用于大规模问题的办法,因为它仅利用矩阵-向量积便将 Hessian 作为线性算子。它比 trust-ncg
方法更准确地求解二次子问题。
此方法封装了 [TRLIB] 的 [GLTR] 方法的实现,专门求解限制于截断 Krylov 子空间的置信域子问题。对于不确定的问题,通常最好使用此方法,因为它会以牺牲每个子问题的解决办法中更多矩阵-向量积作为代价减少非线性迭代次数,与 trust-ncg
方法相比。
完整 Hesse 示例:#
>>> res = minimize(rosen, x0, method='trust-krylov',
... jac=rosen_der, hess=rosen_hess,
... options={'gtol': 1e-8, 'disp': True})
Optimization terminated successfully.
Current function value: 0.000000
Iterations: 19 # may vary
Function evaluations: 20
Gradient evaluations: 20
Hessian evaluations: 18
>>> res.x
array([1., 1., 1., 1., 1.])
Hesse 积示例:#
>>> res = minimize(rosen, x0, method='trust-krylov',
... jac=rosen_der, hessp=rosen_hess_p,
... options={'gtol': 1e-8, 'disp': True})
Optimization terminated successfully.
Current function value: 0.000000
Iterations: 19 # may vary
Function evaluations: 20
Gradient evaluations: 20
Hessian evaluations: 0
>>> res.x
array([1., 1., 1., 1., 1.])
F. Lenders、C. Kirches、A. Potschka:“trlib: 一种 GLTR 方法的无矢量实现,用于迭代求解置信域问题”,arXiv:1611.04718
N. Gould、S. Lucidi、M. Roma、P. Toint: “利用 Lanczos 方法求解置信域子问题”,SIAM J. Optim., 9(2), 504–525, (1999)。 DOI:10.1137/S1052623497322735
近似确切算法 (method='trust-exact'
)#
所有 Newton-CG
、trust-ncg
和 trust-krylov
方法都适合处理大规模问题(具有数千个变量的问题)。原因在于,共轭梯度算法通过迭代(没有明确的 Hessian 分解)近似求解置信域子问题(或者对 Hessian 求逆)。由于只需要 Hessian 与任意向量的乘积,因此该算法特别适合处理稀疏 Hessian,这使得存储需求降低,可为这些稀疏问题节省大量时间。
对于中型问题,其中 Hessian 的存储和分解成本并不关键,通过几乎准确求解信赖域子问题可以在更少的迭代中获得一个解决方案。为此,对每个二次子问题迭代求解一个特定非线性方程 [CGT]。该解通常需要 Hessian 矩阵的 3 或 4 个 Cholesky 分解。结果,该方法以更少的迭代次数收敛并比其他已实现的信赖域方法评估目标函数的次数更少。此算法不支持 Hessian 乘积选项。以下是一个使用 Rosenbrock 函数的示例
>>> res = minimize(rosen, x0, method='trust-exact',
... jac=rosen_der, hess=rosen_hess,
... options={'gtol': 1e-8, 'disp': True})
Optimization terminated successfully.
Current function value: 0.000000
Iterations: 13 # may vary
Function evaluations: 14
Gradient evaluations: 13
Hessian evaluations: 14
>>> res.x
array([1., 1., 1., 1., 1.])
多变量标量函数的约束最小化 (minimize
)#
minimize
函数提供了约束最小化算法,即 'trust-constr'
、 'SLSQP'
、 'COBYLA'
和 'COBYQA'
。它们要求将约束定义为使用略有不同的结构。方法 'trust-constr'
和 'COBYQA'
要求将约束定义为 LinearConstraint
和 NonlinearConstraint
对象序列。另一方面,方法 'SLSQP'
和 'COBYLA'
要求将约束定义为具有键 type
、 fun
和 jac
的字典序列。
作为一个例子,让我们考虑 Rosenbrock 函数的约束最小化
此优化问题具有唯一的解 \([x_0, x_1] = [0.4149,~ 0.1701]\),其中仅第一个和第四个约束条件处于活动状态。
Trust-Region 受约束算法 (method='trust-constr'
)#
Trust-Region 受约束方法处理如下形式的受约束最小化问题
当 \(c^l_j = c^u_j\) 时,此方法将 \(j\) 约束条件读作等式约束条件并相应地处理。此外,可以通过将上限或下限设置为 np.inf
并附加适当的符号来指定单边约束条件。
此实现基于 [EQSQP](用于等式约束条件问题)和 [TRIP](用于不等式约束条件问题)。它们都是适合于大规模问题的 Trust-Region 类型算法。
定义边界约束条件:#
边界约束条件 \(0 \leq x_0 \leq 1\) 和 \(-0.5 \leq x_1 \leq 2.0\) 使用 Bounds
对象定义。
>>> from scipy.optimize import Bounds
>>> bounds = Bounds([0, -0.5], [1.0, 2.0])
定义线性约束条件:#
约束条件 \(x_0 + 2 x_1 \leq 1\) 和 \(2 x_0 + x_1 = 1\) 可以写成线性约束条件标准格式
并使用 LinearConstraint
对象定义。
>>> from scipy.optimize import LinearConstraint
>>> linear_constraint = LinearConstraint([[1, 2], [2, 1]], [-np.inf, 1], [1, 1])
定义非线性约束条件:#
非线性约束条件
带雅可比矩阵
以及黑塞矩阵的线性组合
使用 NonlinearConstraint
对象定义。
>>> def cons_f(x):
... return [x[0]**2 + x[1], x[0]**2 - x[1]]
>>> def cons_J(x):
... return [[2*x[0], 1], [2*x[0], -1]]
>>> def cons_H(x, v):
... return v[0]*np.array([[2, 0], [0, 0]]) + v[1]*np.array([[2, 0], [0, 0]])
>>> from scipy.optimize import NonlinearConstraint
>>> nonlinear_constraint = NonlinearConstraint(cons_f, -np.inf, 1, jac=cons_J, hess=cons_H)
此外,还可以将黑塞矩阵 \(H(x, v)\) 定义为稀疏矩阵,
>>> from scipy.sparse import csc_matrix
>>> def cons_H_sparse(x, v):
... return v[0]*csc_matrix([[2, 0], [0, 0]]) + v[1]*csc_matrix([[2, 0], [0, 0]])
>>> nonlinear_constraint = NonlinearConstraint(cons_f, -np.inf, 1,
... jac=cons_J, hess=cons_H_sparse)
或作为 LinearOperator
对象。
>>> from scipy.sparse.linalg import LinearOperator
>>> def cons_H_linear_operator(x, v):
... def matvec(p):
... return np.array([p[0]*2*(v[0]+v[1]), 0])
... return LinearOperator((2, 2), matvec=matvec)
>>> nonlinear_constraint = NonlinearConstraint(cons_f, -np.inf, 1,
... jac=cons_J, hess=cons_H_linear_operator)
当黑塞矩阵 \(H(x, v)\) 的评估难以实现或在计算上不可行时,可以使用 HessianUpdateStrategy
。目前可用的策略是 BFGS
和 SR1
。
>>> from scipy.optimize import BFGS
>>> nonlinear_constraint = NonlinearConstraint(cons_f, -np.inf, 1, jac=cons_J, hess=BFGS())
此外,可以使用有限差分来近似计算黑塞矩阵。
>>> nonlinear_constraint = NonlinearConstraint(cons_f, -np.inf, 1, jac=cons_J, hess='2-point')
约束条件的雅可比矩阵也可以通过有限差分来近似。但是,在这种情况下,无法使用有限差分来计算黑塞矩阵,并且需要用户提供或使用 HessianUpdateStrategy
定义。
>>> nonlinear_constraint = NonlinearConstraint(cons_f, -np.inf, 1, jac='2-point', hess=BFGS())
求解优化问题:#
使用下述方法求解优化问题
>>> x0 = np.array([0.5, 0])
>>> res = minimize(rosen, x0, method='trust-constr', jac=rosen_der, hess=rosen_hess,
... constraints=[linear_constraint, nonlinear_constraint],
... options={'verbose': 1}, bounds=bounds)
# may vary
`gtol` termination condition is satisfied.
Number of iterations: 12, function evaluations: 8, CG iterations: 7, optimality: 2.99e-09, constraint violation: 1.11e-16, execution time: 0.016 s.
>>> print(res.x)
[0.41494531 0.17010937]
在需要时,可以使用 LinearOperator
对象定义目标函数黑塞矩阵,
>>> def rosen_hess_linop(x):
... def matvec(p):
... return rosen_hess_p(x, p)
... return LinearOperator((2, 2), matvec=matvec)
>>> res = minimize(rosen, x0, method='trust-constr', jac=rosen_der, hess=rosen_hess_linop,
... constraints=[linear_constraint, nonlinear_constraint],
... options={'verbose': 1}, bounds=bounds)
# may vary
`gtol` termination condition is satisfied.
Number of iterations: 12, function evaluations: 8, CG iterations: 7, optimality: 2.99e-09, constraint violation: 1.11e-16, execution time: 0.018 s.
>>> print(res.x)
[0.41494531 0.17010937]
或通过参数 hessp
进行黑塞矩阵向量乘法。
>>> res = minimize(rosen, x0, method='trust-constr', jac=rosen_der, hessp=rosen_hess_p,
... constraints=[linear_constraint, nonlinear_constraint],
... options={'verbose': 1}, bounds=bounds)
# may vary
`gtol` termination condition is satisfied.
Number of iterations: 12, function evaluations: 8, CG iterations: 7, optimality: 2.99e-09, constraint violation: 1.11e-16, execution time: 0.018 s.
>>> print(res.x)
[0.41494531 0.17010937]
此外,可以近似计算目标函数的一阶和二阶导数。例如,可以使用 SR1
拟牛顿逼近法近似计算黑塞矩阵,并使用有限差分近似计算梯度。
>>> from scipy.optimize import SR1
>>> res = minimize(rosen, x0, method='trust-constr', jac="2-point", hess=SR1(),
... constraints=[linear_constraint, nonlinear_constraint],
... options={'verbose': 1}, bounds=bounds)
# may vary
`gtol` termination condition is satisfied.
Number of iterations: 12, function evaluations: 24, CG iterations: 7, optimality: 4.48e-09, constraint violation: 0.00e+00, execution time: 0.016 s.
>>> print(res.x)
[0.41494531 0.17010937]
顺序最小二乘规划 (SLSQP) 算法 (method='SLSQP'
)#
SLSQP 方法处理形式受限的最小化问题
其中 \(\mathcal{E}\) 或 \(\mathcal{I}\) 是包含等值和不等值约束的索引集合。
线性约束和非线性约束均定义为键为 type
、fun
和 jac
的字典。
>>> ineq_cons = {'type': 'ineq',
... 'fun' : lambda x: np.array([1 - x[0] - 2*x[1],
... 1 - x[0]**2 - x[1],
... 1 - x[0]**2 + x[1]]),
... 'jac' : lambda x: np.array([[-1.0, -2.0],
... [-2*x[0], -1.0],
... [-2*x[0], 1.0]])}
>>> eq_cons = {'type': 'eq',
... 'fun' : lambda x: np.array([2*x[0] + x[1] - 1]),
... 'jac' : lambda x: np.array([2.0, 1.0])}
优化问题也通过以下方式解决
>>> x0 = np.array([0.5, 0])
>>> res = minimize(rosen, x0, method='SLSQP', jac=rosen_der,
... constraints=[eq_cons, ineq_cons], options={'ftol': 1e-9, 'disp': True},
... bounds=bounds)
# may vary
Optimization terminated successfully. (Exit mode 0)
Current function value: 0.342717574857755
Iterations: 5
Function evaluations: 6
Gradient evaluations: 5
>>> print(res.x)
[0.41494475 0.1701105 ]
方法 'trust-constr'
可用的多数选项均不可用于 'SLSQP'
。
全局优化#
全局优化旨在找到函数在给定约束范围内的全局最小值,即使可能会存在很多局部最小值。通常,全局最小化器会高效搜索参数空间,同时使用幕后的局部最小化器(例如,minimize
)。SciPy 包含许多优秀的全局优化器。在此,我们将使用那些在同一目标函数(恰如其名地)eggholder
函数
>>> def eggholder(x):
... return (-(x[1] + 47) * np.sin(np.sqrt(abs(x[0]/2 + (x[1] + 47))))
... -x[0] * np.sin(np.sqrt(abs(x[0] - (x[1] + 47)))))
>>> bounds = [(-512, 512), (-512, 512)]
此函数看起来像蛋盒
>>> import matplotlib.pyplot as plt
>>> from mpl_toolkits.mplot3d import Axes3D
>>> x = np.arange(-512, 513)
>>> y = np.arange(-512, 513)
>>> xgrid, ygrid = np.meshgrid(x, y)
>>> xy = np.stack([xgrid, ygrid])
>>> fig = plt.figure()
>>> ax = fig.add_subplot(111, projection='3d')
>>> ax.view_init(45, -45)
>>> ax.plot_surface(xgrid, ygrid, eggholder(xy), cmap='terrain')
>>> ax.set_xlabel('x')
>>> ax.set_ylabel('y')
>>> ax.set_zlabel('eggholder(x, y)')
>>> plt.show()
我们现在使用全局优化器获取最小值和最小值处的函数值。我们将其结果存储在字典中,以便我们稍后比较不同的优化结果。
>>> from scipy import optimize
>>> results = dict()
>>> results['shgo'] = optimize.shgo(eggholder, bounds)
>>> results['shgo']
fun: -935.3379515604197 # may vary
funl: array([-935.33795156])
message: 'Optimization terminated successfully.'
nfev: 42
nit: 2
nlfev: 37
nlhev: 0
nljev: 9
success: True
x: array([439.48096952, 453.97740589])
xl: array([[439.48096952, 453.97740589]])
>>> results['DA'] = optimize.dual_annealing(eggholder, bounds)
>>> results['DA']
fun: -956.9182316237413 # may vary
message: ['Maximum number of iteration reached']
nfev: 4091
nhev: 0
nit: 1000
njev: 0
x: array([482.35324114, 432.87892901])
所有优化器返回一个 OptimizeResult
,除了求解器外,还包含有关函数求解次数、优化是否成功的信息等等。为了简洁起见,我们不会显示其他优化器的全部输出
>>> results['DE'] = optimize.differential_evolution(eggholder, bounds)
shgo
有一个第二个方法,它返回所有局部最小值,而不是只返回它认为是全局最小值
>>> results['shgo_sobol'] = optimize.shgo(eggholder, bounds, n=200, iters=5,
... sampling_method='sobol')
现在,我们将在函数热图上绘制所有找到的最小值
>>> fig = plt.figure()
>>> ax = fig.add_subplot(111)
>>> im = ax.imshow(eggholder(xy), interpolation='bilinear', origin='lower',
... cmap='gray')
>>> ax.set_xlabel('x')
>>> ax.set_ylabel('y')
>>>
>>> def plot_point(res, marker='o', color=None):
... ax.plot(512+res.x[0], 512+res.x[1], marker=marker, color=color, ms=10)
>>> plot_point(results['DE'], color='c') # differential_evolution - cyan
>>> plot_point(results['DA'], color='w') # dual_annealing. - white
>>> # SHGO produces multiple minima, plot them all (with a smaller marker size)
>>> plot_point(results['shgo'], color='r', marker='+')
>>> plot_point(results['shgo_sobol'], color='r', marker='x')
>>> for i in range(results['shgo_sobol'].xl.shape[0]):
... ax.plot(512 + results['shgo_sobol'].xl[i, 0],
... 512 + results['shgo_sobol'].xl[i, 1],
... 'ro', ms=2)
>>> ax.set_xlim([-4, 514*2])
>>> ax.set_ylim([-4, 514*2])
>>> plt.show()
最小二乘法求解 (least_squares
)#
SciPy 能够解出鲁棒的约束非线性最小二乘问题
此处,\(f_i(\mathbf{x})\) 是从 \(\mathbb{R}^n\) 至 \(\mathbb{R}\) 的平滑函数,我们称其为残差。标量值函数 \(\rho(\cdot)\) 的目的是减少离群残差的影响,并提高解的鲁棒性,我们称其为损失函数。线性损失函数给出标准最小二乘问题。此外,还允许对部分 \(x_j\) 采用上下限形式的约束。
所有特定的最小二乘求解方法都利用一个偏导数的 \(m \times n\) 矩阵,称为雅可比矩阵,定义为 \(J_{ij} = \partial f_i / \partial x_j\)。强烈建议计算此矩阵,并将其传递给 least_squares
,否则,系统将会通过有限差分进行估计,这将花费大量额外时间,而且在困难的情况下,可能非常不准确。
函数 least_squares
可用于将函数 \(\varphi(t; \mathbf{x})\) 拟合到经验数据 \(\{(t_i, y_i), i = 0, \ldots, m-1\}\)。为此,只需将残差预先计算成 \(f_i(\mathbf{x}) = w_i (\varphi(t_i; \mathbf{x}) - y_i)\),其中 \(w_i\) 是指派给每个观测值的权重。
拟合问题的解答示例#
在此处,我们考虑酶促反应 [1]。有 11 个残差定义为
其中 \(y_i\) 是测量值,\(u_i\) 是自变量的值。未知参数向量是 \(\mathbf{x} = (x_0, x_1, x_2, x_3)^T\)。如前所述,建议以闭合的形式计算雅可比矩阵
我们将使用 [2] 中定义的“硬”起点。为了找到一个有物理意义的解、避免潜在的零除并确保收敛到全局最小值,我们施加约束 \(0 \leq x_j \leq 100, j = 0, 1, 2, 3\)。
以下代码实现了对 \(\mathbf{x}\) 的最小二乘估计,并最终绘制了原始数据和拟合模型函数
>>> from scipy.optimize import least_squares
>>> def model(x, u):
... return x[0] * (u ** 2 + x[1] * u) / (u ** 2 + x[2] * u + x[3])
>>> def fun(x, u, y):
... return model(x, u) - y
>>> def jac(x, u, y):
... J = np.empty((u.size, x.size))
... den = u ** 2 + x[2] * u + x[3]
... num = u ** 2 + x[1] * u
... J[:, 0] = num / den
... J[:, 1] = x[0] * u / den
... J[:, 2] = -x[0] * num * u / den ** 2
... J[:, 3] = -x[0] * num / den ** 2
... return J
>>> u = np.array([4.0, 2.0, 1.0, 5.0e-1, 2.5e-1, 1.67e-1, 1.25e-1, 1.0e-1,
... 8.33e-2, 7.14e-2, 6.25e-2])
>>> y = np.array([1.957e-1, 1.947e-1, 1.735e-1, 1.6e-1, 8.44e-2, 6.27e-2,
... 4.56e-2, 3.42e-2, 3.23e-2, 2.35e-2, 2.46e-2])
>>> x0 = np.array([2.5, 3.9, 4.15, 3.9])
>>> res = least_squares(fun, x0, jac=jac, bounds=(0, 100), args=(u, y), verbose=1)
# may vary
`ftol` termination condition is satisfied.
Function evaluations 130, initial cost 4.4383e+00, final cost 1.5375e-04, first-order optimality 4.92e-08.
>>> res.x
array([ 0.19280596, 0.19130423, 0.12306063, 0.13607247])
>>> import matplotlib.pyplot as plt
>>> u_test = np.linspace(0, 5)
>>> y_test = model(res.x, u_test)
>>> plt.plot(u, y, 'o', markersize=4, label='data')
>>> plt.plot(u_test, y_test, label='fitted model')
>>> plt.xlabel("u")
>>> plt.ylabel("y")
>>> plt.legend(loc='lower right')
>>> plt.show()
更多示例#
下面三个交互式示例更详细地说明了 least_squares
的用法。
大规模捆绑调整使用 scipy展示了
least_squares
的大规模功能,以及如何有效地计算稀疏雅可比的有限差分逼近。在 scipy 中的稳健非线性回归展示了如何在非线性回归中使用稳健损失函数处理异常值。
在 scipy 中解决离散边界值问题研究了如何解决一个大的方程组,并使用界限来实现解的所需属性。
有关实现背后的数学算法详细信息,请参阅 least_squares
的文档。
单变量函数最小化((minimize_scalar
)#
通常只需要单变量函数(即,将标量作为输入的函数)的最小值。在这些情况下,已经开发了可以更快运行的其他优化技术。这些技术可以通过 minimize_scalar
函数访问,该函数提出了几种算法。
无约束最小化(method='brent'
)#
实际上,有两种最小化一元函数的方法:brent
和 golden
,但是 golden
仅出于学术目的而包含,应很少使用。这些方法分别可以通过 minimize_scalar
中的 method 参数来选择。brent
方法使用 Brent 算法来查找最小值。最优情况下,应给出包含所需最小值的括号(bracket
参数)。括号是一个三元组 \(\left( a, b, c \right)\),其中 \(f \left( a \right) > f \left( b \right) < f \left( c \right)\) 且 \(a < b < c\) 。如果不提供这些信息,则可以选择两个起始点并使用简单的 Marching 算法从这些点找到一个括号。如果不提供这两个起始点,则使用 0 和 1(这可能不是您函数的正确选择,并导致返回意外的最小值)。
下面是一个示例:
>>> from scipy.optimize import minimize_scalar
>>> f = lambda x: (x - 2) * (x + 1)**2
>>> res = minimize_scalar(f, method='brent')
>>> print(res.x)
1.0
有界最小化 (method='bounded'
)#
通常,可以在开始最小化之前对求解空间施加约束。minimize_scalar
中的 bounded 方法就是约束最小化程序的一个示例,它为此函数提供了一个基础区间约束。区间约束允许最小化仅在两个固定的端点之间执行,这两个端点使用强制 bounds 参数指定。
例如,要查找\(J_{1}\left( x \right)\)在\(x=5\)附近的最小值,minimize_scalar
可以使用间隔\(\left[ 4, 7 \right]\)作为约束条件进行调用。结果为\(x_{\textrm{min}}=5.3314\)。
>>> from scipy.special import j1
>>> res = minimize_scalar(j1, bounds=(4, 7), method='bounded')
>>> res.x
5.33144184241
自定义最小值函数#
有时,将自定义方法用作(多变量或单变量)最小值函数可能非常有用,例如,在使用某些minimize
的库包装器(例如,basinhopping
)时。
我们可以通过传递一个可调用的对象(函数或实现__call__方法的对象)作为method参数,而不是传递一个方法名称来实现这一点。
让我们考虑一个(诚然相当虚拟的)需要,即使用一个简单的自定义多元最小值函数,它将仅以固定的步长独立地在各个维度上搜索邻域。
>>> from scipy.optimize import OptimizeResult
>>> def custmin(fun, x0, args=(), maxfev=None, stepsize=0.1,
... maxiter=100, callback=None, **options):
... bestx = x0
... besty = fun(x0)
... funcalls = 1
... niter = 0
... improved = True
... stop = False
...
... while improved and not stop and niter < maxiter:
... improved = False
... niter += 1
... for dim in range(np.size(x0)):
... for s in [bestx[dim] - stepsize, bestx[dim] + stepsize]:
... testx = np.copy(bestx)
... testx[dim] = s
... testy = fun(testx, *args)
... funcalls += 1
... if testy < besty:
... besty = testy
... bestx = testx
... improved = True
... if callback is not None:
... callback(bestx)
... if maxfev is not None and funcalls >= maxfev:
... stop = True
... break
...
... return OptimizeResult(fun=besty, x=bestx, nit=niter,
... nfev=funcalls, success=(niter > 1))
>>> x0 = [1.35, 0.9, 0.8, 1.1, 1.2]
>>> res = minimize(rosen, x0, method=custmin, options=dict(stepsize=0.05))
>>> res.x
array([1., 1., 1., 1., 1.])
这在单变量优化中也能很好地工作。
>>> def custmin(fun, bracket, args=(), maxfev=None, stepsize=0.1,
... maxiter=100, callback=None, **options):
... bestx = (bracket[1] + bracket[0]) / 2.0
... besty = fun(bestx)
... funcalls = 1
... niter = 0
... improved = True
... stop = False
...
... while improved and not stop and niter < maxiter:
... improved = False
... niter += 1
... for testx in [bestx - stepsize, bestx + stepsize]:
... testy = fun(testx, *args)
... funcalls += 1
... if testy < besty:
... besty = testy
... bestx = testx
... improved = True
... if callback is not None:
... callback(bestx)
... if maxfev is not None and funcalls >= maxfev:
... stop = True
... break
...
... return OptimizeResult(fun=besty, x=bestx, nit=niter,
... nfev=funcalls, success=(niter > 1))
>>> def f(x):
... return (x - 2)**2 * (x + 2)**2
>>> res = minimize_scalar(f, bracket=(-3.5, 0), method=custmin,
... options=dict(stepsize = 0.05))
>>> res.x
-2.0
求根#
标量函数#
如果有一个单变量方程,则有许多不同的求根算法可以尝试。其中大多数算法都需要预期根所在的间隔的端点(因为函数会改变符号)。通常,brentq
是最佳选择,但在某些情况下或出于学术目的,其他方法可能更有用。当没有括号但可以使用一个或多个导数时,则newton
(或halley
,secant
)可适用。尤其如此,如果函数是在复平面的子集上定义,而不能使用括号方法的话。
定点求解#
与求函数的零点密切相关的一个问题是求解函数的定点问题。函数的定点是函数计算返回该点的点:\(g\left(x\right)=x.\)显然,\(g\) 的定点是 \(f\left(x\right)=g\left(x\right)-x.\) 的根。等价地,\(f\) 的根是 \(g\left(x\right)=f\left(x\right)+x.\) 的定点。例程 fixed_point
提供了一个简单的迭代方法,使用艾肯序列加速度来估计给定起始点的 \(g\) 的定点。
方程组#
可以使用 root
函数求解一组非线性方程的根。有几种方法可用,其中 hybr
(默认)和 lm
分别使用 Powell 混合方法和 MINPACK 的 Levenberg-Marquardt 方法。
以下示例考虑单变量超越方程
它的根可以按如下方式求得
>>> import numpy as np
>>> from scipy.optimize import root
>>> def func(x):
... return x + 2 * np.cos(x)
>>> sol = root(func, 0.3)
>>> sol.x
array([-1.02986653])
>>> sol.fun
array([ -6.66133815e-16])
现在考虑一组非线性方程
我们定义目标函数以便它也返回雅可比,并通过将 jac
参数设置为 True
来指示这一点。此外,这里使用 Levenberg-Marquardt 求解器。
>>> def func2(x):
... f = [x[0] * np.cos(x[1]) - 4,
... x[1]*x[0] - x[1] - 5]
... df = np.array([[np.cos(x[1]), -x[0] * np.sin(x[1])],
... [x[1], x[0] - 1]])
... return f, df
>>> sol = root(func2, [1, 1], jac=True, method='lm')
>>> sol.x
array([ 6.50409711, 0.90841421])
大规模问题的根求解#
在 root
中的方法 hybr
和 lm
无法处理非常多的变量(N),因为它们需要在每个牛顿步骤中计算和求解一个密集的 N x N 雅可比矩阵。当 N 增大时,这变得相当低效。
例如,考虑以下问题:需要求解正方形 \([0,1]\times[0,1]\) 上的以下积分微分方程
在正方形边界上的上边缘使用边界条件\(P(x,1) = 1\),在其他地方使用\(P=0\)。可以通过网格上的值来近似连续函数P,\(P_{n,m}\approx{}P(n h, m h)\),并使用较小的网格间距h。然后可以近似导数和积分;例如\(\partial_x^2 P(x,y)\approx{}(P(x+h,y) - 2 P(x,y) + P(x-h,y))/h^2\)。该问题然后相当于找到某个函数的根residual(P)
,其中P
是长度为\(N_x N_y\)的向量。
现在,因为\(N_x N_y\)可以很大,因此root
中的hybr
或lm
方法将需要较长时间来解决此问题。但是,可以使用一个大型求解器来寻找解决方案,例如krylov
、broyden2
或anderson
。这些使用所谓的不精确牛顿法,该方法不准确地计算雅可比矩阵,而是对其进行近似。
我们现在可以按以下方式解决问题
import numpy as np
from scipy.optimize import root
from numpy import cosh, zeros_like, mgrid, zeros
# parameters
nx, ny = 75, 75
hx, hy = 1./(nx-1), 1./(ny-1)
P_left, P_right = 0, 0
P_top, P_bottom = 1, 0
def residual(P):
d2x = zeros_like(P)
d2y = zeros_like(P)
d2x[1:-1] = (P[2:] - 2*P[1:-1] + P[:-2]) / hx/hx
d2x[0] = (P[1] - 2*P[0] + P_left)/hx/hx
d2x[-1] = (P_right - 2*P[-1] + P[-2])/hx/hx
d2y[:,1:-1] = (P[:,2:] - 2*P[:,1:-1] + P[:,:-2])/hy/hy
d2y[:,0] = (P[:,1] - 2*P[:,0] + P_bottom)/hy/hy
d2y[:,-1] = (P_top - 2*P[:,-1] + P[:,-2])/hy/hy
return d2x + d2y + 5*cosh(P).mean()**2
# solve
guess = zeros((nx, ny), float)
sol = root(residual, guess, method='krylov', options={'disp': True})
#sol = root(residual, guess, method='broyden2', options={'disp': True, 'max_rank': 50})
#sol = root(residual, guess, method='anderson', options={'disp': True, 'M': 10})
print('Residual: %g' % abs(residual(sol.x)).max())
# visualize
import matplotlib.pyplot as plt
x, y = mgrid[0:1:(nx*1j), 0:1:(ny*1j)]
plt.pcolormesh(x, y, sol.x, shading='gouraud')
plt.colorbar()
plt.show()
仍然太慢?预处理。#
在寻找函数\(f_i({\bf x}) = 0\)的零点时,i = 1, 2, …, N,krylov
求解器花费大部分时间来求雅可比矩阵的逆,
如果你对逆矩阵有近似值\(M\approx{}J^{-1}\),则可以使用它进行线性逆问题的预处理。其思想是,求解\(MJ{\bf s}=M{\bf y}\),而不是求解\(J{\bf s}={\bf y}\):由于矩阵\(MJ\)比矩阵\(J\)“更接近”单位矩阵,因此对于 Krylov 方法而言,更容易处理该方程。
矩阵 M 可传入 root
,方法 krylov
选项 options['jac_options']['inner_M']
。它可以是(稀疏)矩阵或 scipy.sparse.linalg.LinearOperator
实例。
对于上一部分中的问题,我们注意到求解函数由两部分组成:第一部分是应用拉普拉斯算子,\([\partial_x^2 + \partial_y^2] P\),第二部分是积分。我们实际上可以轻松计算对应拉普拉斯算子部分的雅可比矩阵:我们知道在 1-D 中
因此整个 2-D 算子可表示为
对应积分的雅可比矩阵 \(J_2\) 比较难计算,并且由于 所有 条目都是非零的,它将很难倒置。\(J_1\) 相反,是一个相对简单的矩阵,可通过 scipy.sparse.linalg.splu
倒置(或可通过 scipy.sparse.linalg.spilu
逼近倒置)。因此我们很高兴采用 \(M\approx{}J_1^{-1}\) 并期待最佳结果。
在以下示例中,我们使用预处理器 \(M=J_1^{-1}\)。
from scipy.optimize import root
from scipy.sparse import spdiags, kron
from scipy.sparse.linalg import spilu, LinearOperator
from numpy import cosh, zeros_like, mgrid, zeros, eye
# parameters
nx, ny = 75, 75
hx, hy = 1./(nx-1), 1./(ny-1)
P_left, P_right = 0, 0
P_top, P_bottom = 1, 0
def get_preconditioner():
"""Compute the preconditioner M"""
diags_x = zeros((3, nx))
diags_x[0,:] = 1/hx/hx
diags_x[1,:] = -2/hx/hx
diags_x[2,:] = 1/hx/hx
Lx = spdiags(diags_x, [-1,0,1], nx, nx)
diags_y = zeros((3, ny))
diags_y[0,:] = 1/hy/hy
diags_y[1,:] = -2/hy/hy
diags_y[2,:] = 1/hy/hy
Ly = spdiags(diags_y, [-1,0,1], ny, ny)
J1 = kron(Lx, eye(ny)) + kron(eye(nx), Ly)
# Now we have the matrix `J_1`. We need to find its inverse `M` --
# however, since an approximate inverse is enough, we can use
# the *incomplete LU* decomposition
J1_ilu = spilu(J1)
# This returns an object with a method .solve() that evaluates
# the corresponding matrix-vector product. We need to wrap it into
# a LinearOperator before it can be passed to the Krylov methods:
M = LinearOperator(shape=(nx*ny, nx*ny), matvec=J1_ilu.solve)
return M
def solve(preconditioning=True):
"""Compute the solution"""
count = [0]
def residual(P):
count[0] += 1
d2x = zeros_like(P)
d2y = zeros_like(P)
d2x[1:-1] = (P[2:] - 2*P[1:-1] + P[:-2])/hx/hx
d2x[0] = (P[1] - 2*P[0] + P_left)/hx/hx
d2x[-1] = (P_right - 2*P[-1] + P[-2])/hx/hx
d2y[:,1:-1] = (P[:,2:] - 2*P[:,1:-1] + P[:,:-2])/hy/hy
d2y[:,0] = (P[:,1] - 2*P[:,0] + P_bottom)/hy/hy
d2y[:,-1] = (P_top - 2*P[:,-1] + P[:,-2])/hy/hy
return d2x + d2y + 5*cosh(P).mean()**2
# preconditioner
if preconditioning:
M = get_preconditioner()
else:
M = None
# solve
guess = zeros((nx, ny), float)
sol = root(residual, guess, method='krylov',
options={'disp': True,
'jac_options': {'inner_M': M}})
print('Residual', abs(residual(sol.x)).max())
print('Evaluations', count[0])
return sol.x
def main():
sol = solve(preconditioning=True)
# visualize
import matplotlib.pyplot as plt
x, y = mgrid[0:1:(nx*1j), 0:1:(ny*1j)]
plt.clf()
plt.pcolor(x, y, sol)
plt.clim(0, 1)
plt.colorbar()
plt.show()
if __name__ == "__main__":
main()
最初运行结果,未经预处理
0: |F(x)| = 803.614; step 1; tol 0.000257947
1: |F(x)| = 345.912; step 1; tol 0.166755
2: |F(x)| = 139.159; step 1; tol 0.145657
3: |F(x)| = 27.3682; step 1; tol 0.0348109
4: |F(x)| = 1.03303; step 1; tol 0.00128227
5: |F(x)| = 0.0406634; step 1; tol 0.00139451
6: |F(x)| = 0.00344341; step 1; tol 0.00645373
7: |F(x)| = 0.000153671; step 1; tol 0.00179246
8: |F(x)| = 6.7424e-06; step 1; tol 0.00173256
Residual 3.57078908664e-07
Evaluations 317
然后加上预处理
0: |F(x)| = 136.993; step 1; tol 7.49599e-06
1: |F(x)| = 4.80983; step 1; tol 0.00110945
2: |F(x)| = 0.195942; step 1; tol 0.00149362
3: |F(x)| = 0.000563597; step 1; tol 7.44604e-06
4: |F(x)| = 1.00698e-09; step 1; tol 2.87308e-12
Residual 9.29603061195e-11
Evaluations 77
使用预处理器将 残差
函数的评估次数减少了 4 倍。对于计算残差十分耗时的难题,良好的预处理至关重要——它甚至可以决定该难题是否可以在实践中得到解决。
预处理是一门艺术、一门科学,也是一项产业。在这里,我们很幸运地做出一个相对合理的选择,但这项主题的深度远不止此处所展示的。
线性规划 (linprog
)#
此函数 linprog
可以在线性相等和不等式约束下,使线性目标函数最小化。此类问题就是众所周知的线性规划。而线性规划将解决符合下述形式的问题
其中 \(x\) 为决策变量向量;\(c\)、\(b_{ub}\)、\(b_{eq}\)、\(l\) 和 \(u\) 为向量;且 \(A_{ub}\) 和 \(A_{eq}\) 为矩阵。
在本教程中,我们将尝试使用 linprog
来解决一个典型的线性规划问题。
线性规划示例#
考虑以下简单的线性规划问题
我们需要一些数学运算,才能将目标问题转换成 linprog
可接受的形式。
首先,让我们考虑目标函数。我们想最大化目标函数,但 linprog
只允许最小化问题。通过将最大化 \(29x_1 + 45x_2\) 转换为最小化 \(-29x_1 -45x_2\) 来轻松解决该问题。此外,\(x_3, x_4\) 未显示在目标函数中。这意味着对应于 \(x_3, x_4\) 的权重为零。因此,目标函数可转换为
如果我们定义决策变量 \(x = [x_1, x_2, x_3, x_4]^T\) 的向量和目标权重向量 \(c\),由 linprog
在此问题中给出,那么它应该为
接下来,让我们考虑两个不等式约束。第一个是不等式“小于”,因此它已经由 linprog
接受的形式给出。第二个是不等式“大于”,因此我们需要将两侧乘以 \(-1\) 将其转换为不等式“小于”。显式显示零系数,我们有
这些方程可以转换为矩阵形式
其中
接下来,让我们考虑两个等式约束。显式显示零权重,这些约束为
这些方程可以转换为矩阵形式
其中
最后,我们来考虑对各个决策变量的独立不等式约束,这些约束称为“范围约束”或“简单界限”。这些约束可以使用 linprog
的 bounds 参数进行应用。如 linprog
文档所述,bounds 的默认值为 (0, None)
,这意味着每个决策变量的下界为 0,每个决策变量的上界为无穷大:所有决策变量均为非负数。我们的下界不同,因此我们需要将各个决策变量的下界和上界指定为一个元组并对这些元组进行分组并编入列表。
最后,我们可以使用 linprog
来求解转换后的问题。
>>> import numpy as np
>>> from scipy.optimize import linprog
>>> c = np.array([-29.0, -45.0, 0.0, 0.0])
>>> A_ub = np.array([[1.0, -1.0, -3.0, 0.0],
... [-2.0, 3.0, 7.0, -3.0]])
>>> b_ub = np.array([5.0, -10.0])
>>> A_eq = np.array([[2.0, 8.0, 1.0, 0.0],
... [4.0, 4.0, 0.0, 1.0]])
>>> b_eq = np.array([60.0, 60.0])
>>> x0_bounds = (0, None)
>>> x1_bounds = (0, 5.0)
>>> x2_bounds = (-np.inf, 0.5) # +/- np.inf can be used instead of None
>>> x3_bounds = (-3.0, None)
>>> bounds = [x0_bounds, x1_bounds, x2_bounds, x3_bounds]
>>> result = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b_eq, bounds=bounds)
>>> print(result.message)
The problem is infeasible. (HiGHS Status 8: model_status is Infeasible; primal_status is At lower/fixed bound)
结果显示我们的问题不可行,这意味着没有满足所有约束的解向量。这并不一定意味着我们做错了;有些问题确实不可行。但假设我们决定 \(x_1\) 的界限约束太严苛,可以放宽到 \(0 \leq x_1 \leq 6\)。在调整我们的代码 x1_bounds = (0, 6)
以反映更改并再次执行后
>>> x1_bounds = (0, 6)
>>> bounds = [x0_bounds, x1_bounds, x2_bounds, x3_bounds]
>>> result = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b_eq, bounds=bounds)
>>> print(result.message)
Optimization terminated successfully. (HiGHS Status 7: Optimal)
结果显示优化成功了。我们可以检查目标值 (result.fun
) 是否等于 \(c^Tx\)
>>> x = np.array(result.x)
>>> obj = result.fun
>>> print(c @ x)
-505.97435889013434 # may vary
>>> print(obj)
-505.97435889013434 # may vary
我们还可以检查所有约束是否都在合理容差范围内得到满足
>>> print(b_ub - (A_ub @ x).flatten()) # this is equivalent to result.slack
[ 6.52747190e-10, -2.26730279e-09] # may vary
>>> print(b_eq - (A_eq @ x).flatten()) # this is equivalent to result.con
[ 9.78840831e-09, 1.04662945e-08]] # may vary
>>> print([0 <= result.x[0], 0 <= result.x[1] <= 6.0, result.x[2] <= 0.5, -3.0 <= result.x[3]])
[True, True, True, True]
分配问题#
线性总分配问题示例#
考虑一下为游泳混合接力队挑选学生的问题。我们有一张表格显示了五名学生的每种泳姿的成绩
学生 |
仰泳 |
蛙泳 |
蝶泳 |
自由泳 |
---|---|---|---|---|
A |
43.5 |
47.1 |
48.4 |
38.2 |
B |
45.5 |
42.1 |
49.6 |
36.8 |
C |
43.4 |
39.1 |
42.1 |
43.2 |
D |
46.5 |
44.1 |
44.5 |
41.2 |
E |
46.3 |
47.8 |
50.4 |
37.2 |
我们需要为四种游泳方式各自选择一名学生,以使接力总时间最小化。这是一个典型的线性求和分配问题。我们可以使用 linear_sum_assignment
来解决它。
线性求和分配问题是最著名的组合优化问题之一。给定“成本矩阵”\(C\),问题是选择
每行的刚好一个元素
并且不会从任何一列中选择多个元素
使得所选元素之和最小化
换句话说,我们需要将每一行分配给一列,使得对应项之和最小化。
形式上,令 \(X\) 为一个布尔矩阵,其中 \(X[i,j] = 1\) 当且仅当行 \(i\) 被分配给列 \(j\)。那么,最优分配的成本为
第一步是定义成本矩阵。在本例中,我们要将每种游泳方式分配给一名学生。 linear_sum_assignment
能够将成本矩阵的每一行分配给一列。因此,为了生成成本矩阵,需要将上面的表格进行转置,以便行对应于游泳方式,列对应于学生
>>> import numpy as np
>>> cost = np.array([[43.5, 45.5, 43.4, 46.5, 46.3],
... [47.1, 42.1, 39.1, 44.1, 47.8],
... [48.4, 49.6, 42.1, 44.5, 50.4],
... [38.2, 36.8, 43.2, 41.2, 37.2]])
我们可以用 linear_sum_assignment
来求解分配问题
>>> from scipy.optimize import linear_sum_assignment
>>> row_ind, col_ind = linear_sum_assignment(cost)
row_ind
和 col_ind
是成本矩阵最优分配的矩阵索引
>>> row_ind
array([0, 1, 2, 3])
>>> col_ind
array([0, 2, 3, 1])
最优分配为
>>> styles = np.array(["backstroke", "breaststroke", "butterfly", "freestyle"])[row_ind]
>>> students = np.array(["A", "B", "C", "D", "E"])[col_ind]
>>> dict(zip(styles, students))
{'backstroke': 'A', 'breaststroke': 'C', 'butterfly': 'D', 'freestyle': 'B'}
最优总混合泳时间为
>>> cost[row_ind, col_ind].sum()
163.89999999999998
请注意,此结果与每种游泳方式最小时间的总和不同
>>> np.min(cost, axis=1).sum()
161.39999999999998
因为学生“C”是“蛙泳”和“蝶泳”方式的最佳游泳者。我们无法将学生“C”分配给两种方式,因此我们将学生 C 分配给“蛙泳”方式,将学生 D 分配给“蝶泳”方式以最小化总时间。
参考资料
一些进一步的阅读和相关软件,例如 Newton-Krylov [KK]、PETSc [PP] 和 PyAMG [AMG]
D.A. Knoll 和 D.E. Keyes,“Jacobian-free Newton-Krylov 方法”,J. Comp. Phys. 193, 357 (2004)。 DOI:10.1016/j.jcp.2003.08.010
PETSc https://www.mcs.anl.gov/petsc/ 及其 Python 绑定 https://bitbucket.org/petsc/petsc4py/
PyAMG(代数多重网格预处理条件器/解算器) pyamg/pyamg#issues
混合整数线性规划#
背包问题示例#
背包问题是一个众所周知的组合优化问题。给定一组物品,每个物品都有一个大小和一个值,该问题是要选择那些在总大小低于一定阈值条件下最大化总价值的物品。
形式上,令
\(x_i\) 为布尔变量,指示是否将物品 \(i\) 放入背包中,
\(n\) 为物品总数,
\(v_i\) 为物品 \(i\) 的价值,
\(s_i\) 为物品 \(i\) 的大小,以及
\(C\) 为背包的容量。
则该问题为
尽管目标函数和不等式约束在决策变量\(x_i\) 中是线性的,但这与典型的线性规划问题不同,因为决策变量只能取整数值。具体而言,我们的决策变量只能是 \(0\) 或 \(1\),因此这被称为二元整数线性规划(BILP)。此类问题属于更大的混合整数线性规划(MILP)类别,我们可以用 milp
来解决。
在我们的示例中,有 8 项物品可供选择,并且每个物品的大小和价值如下所示。
>>> import numpy as np
>>> from scipy import optimize
>>> sizes = np.array([21, 11, 15, 9, 34, 25, 41, 52])
>>> values = np.array([22, 12, 16, 10, 35, 26, 42, 53])
我们需要将八个决策变量限制为二进制。可以通过添加以下内容来实现:Bounds
:约束条件确保这些变量介于 \(0\) 和 \(1\) 之间,我们应用“整体性”约束条件以确保这些变量要么为 \(0\) 要么为 \(1\)。
>>> bounds = optimize.Bounds(0, 1) # 0 <= x_i <= 1
>>> integrality = np.full_like(values, True) # x_i are integers
背包容量约束条件使用 LinearConstraint
指定。
>>> capacity = 100
>>> constraints = optimize.LinearConstraint(A=sizes, lb=0, ub=capacity)
如果我们遵循线性代数的常规规则,输入 A
应是二维矩阵,下限和上限 lb
和 ub
应是一维向量,但 LinearConstraint
对输入保持宽容,只要这些输入可以广播到一致的形状即可。
使用上面定义的变量,我们可以使用 milp
解背包问题。请注意,milp
最小化目标函数,但我们希望最大化总价值,因此我们将 c 设置为负值。
>>> from scipy.optimize import milp
>>> res = milp(c=-values, constraints=constraints,
... integrality=integrality, bounds=bounds)
让我们检查结果
>>> res.success
True
>>> res.x
array([1., 1., 0., 1., 1., 1., 0., 0.])
这意味着,我们应选择项目 1、2、4、5、6,以针对尺寸约束优化总价值。请注意,这与我们通过求解(没有整体性约束条件的)线性规划松弛并尝试舍入决策变量而获得的结果不同。
>>> from scipy.optimize import milp
>>> res = milp(c=-values, constraints=constraints,
... integrality=False, bounds=bounds)
>>> res.x
array([1. , 1. , 1. , 1. ,
0.55882353, 1. , 0. , 0. ])
如果我们将该解法向上舍入为 array([1., 1., 1., 1., 1., 1., 0., 0.])
,我们的背包就会超过容量约束条件;而如果我们将该解法向下舍入为 array([1., 1., 1., 1., 0., 1., 0., 0.])
,就会得到次优解法。
如需了解其他 MILP 教程,请参阅 SciPy 食谱上的 Jupyter 笔记本