使用 scipy 最小化多变量函数。导数未知
Posted
技术标签:
【中文标题】使用 scipy 最小化多变量函数。导数未知【英文标题】:Minimizing a multivariable function with scipy. Derivative not known 【发布时间】:2015-07-03 13:13:58 【问题描述】:我有一个函数实际上是调用另一个程序(一些 Fortran 代码)。当我调用这个函数 (run_moog
) 时,我可以解析 4 个变量,并返回 6 个值。这些值都应该接近 0(为了最小化)。但是,我将它们组合成这样:np.sum(results**2)
。现在我有一个标量函数。我想最小化这个函数,即让np.sum(results**2)
尽可能接近零。注意:当这个函数 (run_moog
) 采用 4 个输入参数时,它会创建一个依赖于这些参数的 Fortran 代码的输入文件。
我从the scipy docs 尝试了几种方法来优化它。但没有一个按预期工作。最小化应该能够限制 4 个变量。这是一个尝试:
from scipy.optimize import minimize # Tried others as well from the docs
x0 = 4435, 3.54, 0.13, 2.4
bounds = [(4000, 6000), (3.00, 4.50), (-0.1, 0.1), (0.0, None)]
a = minimize(fun_mmog, x0, bounds=bounds, method='L-BFGS-B') # I've tried several different methods here
print a
然后这给了我
status: 0
success: True
nfev: 5
fun: 2.3194639999999964
x: array([ 4.43500000e+03, 3.54000000e+00, 1.00000000e-01,
2.40000000e+00])
message: 'CONVERGENCE: NORM_OF_PROJECTED_GRADIENT_<=_PGTOL'
jac: array([ 0., 0., -54090399.99999981, 0.])
nit: 0
第三个参数略有变化,其他参数完全相同。还有 5 个函数调用 (nfev
) 但没有迭代 (nit
)。 output from scipy is shown here.
【问题讨论】:
看起来你甚至在迭代一次之前就被非常相似的函数值困住了。或许,像 Nelder-Mead(不使用导数)这样具有小 ftol 和 xtol 参数的方法可能无法解决问题。恕我直言,网格搜索可能是最好的开始(然后给出初始值以最小化)。你确定你没有得到 NaN 或 Inf 吗? (有时即使为参数设置了理论界限,如果算法在数值上不稳定,它也会返回其中之一) 问题是计算近似梯度的步骤太小了,所以雅可比矩阵中的零点。尝试将options='epsilon': 1e-4
与method='L-BFGS-B'
或更大的值(默认为1e-8
)相加,直到您的雅可比矩阵没有零。
两种解决方案似乎都有助于工作。但是,我想使用三个中的一个,我可以使用约束(BFGS
、L-BFGS-B
、SLSQP
)。所以通过设置eps: 1e0
它运行,但在某些时候我运行在我的约束集之外。从 OP 中添加的唯一内容是 minimize
函数中的 , options='eps': 1e+0)
。
另外,三个变量的步长相同也没有什么意义。第一个变量的顺序为1e4
,其余的顺序为1。epsilon
可以是值列表吗?
【参考方案1】:
几种可能性:
-
试试 COBYLA。它应该是无导数的,并且支持不等式约束。
您不能通过普通界面使用不同的epsilon;所以尝试将你的第一个变量缩放 1e4。 (除以进去,乘回来。)
跳过普通的自动 jacobian 构造函数,自己创建:
假设您正在尝试使用 SLSQP,但您没有提供 jacobian 函数。它为你做一个。它的代码在approx_jacobian
中slsqp.py 中。这是一个精简版:
def approx_jacobian(x,func,epsilon,*args):
x0 = asfarray(x)
f0 = atleast_1d(func(*((x0,)+args)))
jac = zeros([len(x0),len(f0)])
dx = zeros(len(x0))
for i in range(len(x0)):
dx[i] = epsilon
jac[i] = (func(*((x0+dx,)+args)) - f0)/epsilon
dx[i] = 0.0
return jac.transpose()
您可以尝试用以下代码替换该循环:
for (i, e) in zip(range(len(x0)), epsilon):
dx[i] = e
jac[i] = (func(*((x0+dx,)+args)) - f0)/e
dx[i] = 0.0
您不能将它作为 jacobian 提供给 minimize
,但修复它很简单:
def construct_jacobian(func,epsilon):
def jac(x, *args):
x0 = asfarray(x)
f0 = atleast_1d(func(*((x0,)+args)))
jac = zeros([len(x0),len(f0)])
dx = zeros(len(x0))
for i in range(len(x0)):
dx[i] = epsilon
jac[i] = (func(*((x0+dx,)+args)) - f0)/epsilon
dx[i] = 0.0
return jac.transpose()
return jac
然后您可以拨打minimize
喜欢:
minimize(fun_mmog, x0,
jac=construct_jacobian(fun_mmog, [1e0, 1e-4, 1e-4, 1e-4]),
bounds=bounds, method='SLSQP')
【讨论】:
【参考方案2】:听起来您的目标函数没有表现良好的导数。输出jac: array([ 0., 0., -54090399.99999981, 0.])
中的行表示仅更改第三个变量值很重要。因为导数 w.r.t.到这个变量实际上是无限的,函数中可能有问题。这也是第三个变量值达到最大值的原因。
我建议您看一下导数,至少在您的参数空间中的几个点上。使用有限差分和 SciPy 的 fmin_l_bfgs_b
、1e-8
的默认步长计算它们。 Here 是一个如何计算导数的示例。
尝试绘制你的目标函数。例如,保持其中两个参数不变,让其他两个参数变化。如果函数有多个局部最优值,则不应使用 BFGS 等基于梯度的方法。
【讨论】:
“问题”是小步长没有改变其他四个参数。然后突然之间,第三个参数发生了微小的变化,导数巨大。目前的解决方案是将所有四个变量置于相同的数量级。这有点帮助。制作一个情节是个好主意。我什至没有想到这一点。谢谢。【参考方案3】:获得梯度的解析表达式有多难?如果你有,那么你可以使用有限差分来近似 Hessian 的乘积。然后您可以使用其他可用的优化例程。
在 SciPy 中可用的各种优化例程中,一种称为 TNC(带有截断的牛顿共轭梯度)对与问题相关的数值非常稳健。
【讨论】:
在这种情况下实际上不可能得到任何解析表达式。【参考方案4】:众所周知,Nelder-Mead Simplex Method(由上述 cmets 中的 Cristián Antuña 建议)是优化(可能表现不佳)函数的好选择(请参阅Numerical Recipies In C, Chapter 10)。
您的问题有两个具体方面。第一个是对输入的约束,第二个是缩放问题。以下建议解决这些问题,但您可能需要在它们之间手动迭代几次,直到一切正常。
输入约束
假设您的输入约束形成convex region(如您上面的示例所示,但我想概括一下),那么您可以编写一个函数
is_in_bounds(p):
# Return if p is in the bounds
使用此函数,假设算法要从点 from_
移动到点 to
,其中已知 from_
在该区域中。然后下面的函数将有效地找到它可以继续的两点之间的直线上的最远点:
from numpy.linalg import norm
def progress_within_bounds(from_, to, eps):
"""
from_ -- source (in region)
to -- target point
eps -- Eucliedan precision along the line
"""
if norm(from_, to) < eps:
return from_
mid = (from_ + to) / 2
if is_in_bounds(mid):
return progress_within_bounds(mid, to, eps)
return progress_within_bounds(from_, mid, eps)
(请注意,此函数可以针对某些区域进行优化,但几乎不值得费心,因为它甚至不调用您的原始对象函数,这是昂贵的。)
Nelder-Mead 的优点之一是该函数执行一系列非常直观的步骤。其中一些点显然会让你离开这个区域,但很容易修改它。这是一个implementation of Nelder Mead,在##################################################################
形式的两行之间标记了修改:
import copy
'''
Pure Python/Numpy implementation of the Nelder-Mead algorithm.
Reference: https://en.wikipedia.org/wiki/Nelder%E2%80%93Mead_method
'''
def nelder_mead(f, x_start,
step=0.1, no_improve_thr=10e-6, no_improv_break=10, max_iter=0,
alpha = 1., gamma = 2., rho = -0.5, sigma = 0.5):
'''
@param f (function): function to optimize, must return a scalar score
and operate over a numpy array of the same dimensions as x_start
@param x_start (numpy array): initial position
@param step (float): look-around radius in initial step
@no_improv_thr, no_improv_break (float, int): break after no_improv_break iterations with
an improvement lower than no_improv_thr
@max_iter (int): always break after this number of iterations.
Set it to 0 to loop indefinitely.
@alpha, gamma, rho, sigma (floats): parameters of the algorithm
(see Wikipedia page for reference)
'''
# init
dim = len(x_start)
prev_best = f(x_start)
no_improv = 0
res = [[x_start, prev_best]]
for i in range(dim):
x = copy.copy(x_start)
x[i] = x[i] + step
score = f(x)
res.append([x, score])
# simplex iter
iters = 0
while 1:
# order
res.sort(key = lambda x: x[1])
best = res[0][1]
# break after max_iter
if max_iter and iters >= max_iter:
return res[0]
iters += 1
# break after no_improv_break iterations with no improvement
print '...best so far:', best
if best < prev_best - no_improve_thr:
no_improv = 0
prev_best = best
else:
no_improv += 1
if no_improv >= no_improv_break:
return res[0]
# centroid
x0 = [0.] * dim
for tup in res[:-1]:
for i, c in enumerate(tup[0]):
x0[i] += c / (len(res)-1)
# reflection
xr = x0 + alpha*(x0 - res[-1][0])
##################################################################
##################################################################
xr = progress_within_bounds(x0, x0 + alpha*(x0 - res[-1][0]), prog_eps)
##################################################################
##################################################################
rscore = f(xr)
if res[0][1] <= rscore < res[-2][1]:
del res[-1]
res.append([xr, rscore])
continue
# expansion
if rscore < res[0][1]:
xe = x0 + gamma*(x0 - res[-1][0])
##################################################################
##################################################################
xe = progress_within_bounds(x0, x0 + gamma*(x0 - res[-1][0]), prog_eps)
##################################################################
##################################################################
escore = f(xe)
if escore < rscore:
del res[-1]
res.append([xe, escore])
continue
else:
del res[-1]
res.append([xr, rscore])
continue
# contraction
xc = x0 + rho*(x0 - res[-1][0])
##################################################################
##################################################################
xc = progress_within_bounds(x0, x0 + rho*(x0 - res[-1][0]), prog_eps)
##################################################################
##################################################################
cscore = f(xc)
if cscore < res[-1][1]:
del res[-1]
res.append([xc, cscore])
continue
# reduction
x1 = res[0][0]
nres = []
for tup in res:
redx = x1 + sigma*(tup[0] - x1)
score = f(redx)
nres.append([redx, score])
res = nres
注意这个实现是GPL,这对你来说是否合适。不过,从任何伪代码修改 NM 都非常容易,无论如何您都可能想输入simulated annealing。
缩放
这是一个更棘手的问题,但jasaarim 对此提出了一个有趣的观点。一旦修改后的 NM 算法找到了一个点,您可能希望在修复几个维度的同时运行 matplotlib.contour
,以查看函数的行为方式。此时,您可能需要重新调整一个或多个维度,并重新运行修改后的 NM。
——
【讨论】:
以上是关于使用 scipy 最小化多变量函数。导数未知的主要内容,如果未能解决你的问题,请参考以下文章
scipy中的最小化,找到N维标量函数的所有局部最小值的算法