python-3.x scipy.optimize.minimize()约束取决于成本函数

8aqjt8rx  于 2022-12-14  发布在  Python
关注(0)|答案(2)|浏览(140)

我在用scipy.optimize.minimize(method='COBYLA')运行一个约束优化。
为了评估成本函数,我需要运行一个相对昂贵的模拟来计算输入变量的数据集,成本函数是一个(计算成本低)属性。然而,我的两个约束也依赖于昂贵的数据。到目前为止,我发现约束优化的唯一方法是使每个约束函数重新计算成本函数已经计算的相同数据集(简化准代码):

def costfun(x):
    data = expensive_fun(x)
    return(cheap_fun1(data))

def constr1(x):
    data = expensive_fun(x)
    return(cheap_fun2(data))

def constr2(x):
    data = expensive_fun(x)
    return(cheap_fun3(data))

constraints = [{'type':'ineq', 'fun':constr1},
               {'type':'ineq', 'fun':constr2}]

# initial guess
x0 = np.ones((6,))

opt_result = minimize(costfun, x0, method='COBYLA', 
                      constraints=constraints)

这显然效率不高,因为对于每个xexpensive_fun(x)被调用三次。
我可以稍微修改一下,加入一个通用的“evaluate some cost”函数来运行代价高昂的计算,然后对给定的任何标准进行求值,但是,尽管这样可以避免我多次编写“代价高昂”的代码,但它仍然会在优化器的每次迭代中运行三次:

# universal cost function evaluator
def criterion_from_x(x, cfun):
    data = expensive_fun(x)
    return(cfun(data))

def costfun(data):
    return(cheap_fun1(data))

def constr1(data):
    return(cheap_fun2(data))

def constr2(data):
    return(cheap_fun3(data))

constraints = [{'type':'ineq', 'fun':criterion_from_x, 'args':(constr1,)},
               {'type':'ineq', 'fun':criterion_from_x, 'args':(constr2,)}

# initial guess
x0 = np.ones((6,))

opt_result = minimize(criterion_from_x, x0, method='COBYLA',
                      args=(costfun,), constraints=constraints)

我还没有找到任何方法来设置在每次迭代时使用x生成data,然后将data传递给目标函数和约束函数。
这样的东西存在吗?我注意到callback的参数,但是那是一个在每一步之后被调用的函数,我需要一些预处理器,在每一步之前被调用,其结果然后可用于成本函数和约束评估。也许有一种方法以某种方式偷偷地?我希望避免编写自己的优化器。
一种更传统的解决方法是评估成本函数中的约束条件(成本函数中包含所需的所有数据),将违反约束条件的惩罚添加到主成本函数中,并在没有显式约束条件的情况下运行优化器,但我以前尝试过这种方法,发现在违反约束条件的情况下,主成本函数可能会变得有些混乱。因此优化器可能会卡在某个违反约束的地方而无法再次找到。
另一种方法是在成本函数中生成某种全局变量,然后编写约束求值来使用该全局变量,但如果涉及到多线程/-处理,或者我为全局变量选择的名称与代码中其他地方使用的名称冲突,这可能会非常危险:“”“定义成本资金(x):全局数据data =昂贵的函数(x)return(cheap_fun1(data))
定义构造1(x):全局数据返回(cheap_fun2(数据))
定义构造2(x):全局数据返回(cheap_fun3(数据))“”“
我知道有些人使用文件I/O的情况下,成本函数涉及运行一个大型模拟,产生一堆输出文件。之后,约束函数可以访问这些文件-但我的问题不是那么大。
我目前使用的是Python v3.9和scipy 1.9.1。

42fyovps

42fyovps1#

你可以用与scipy的MemoizeJac相同的方式编写一个装饰器类,它缓存每次调用代价高昂的函数时的返回值:

import numpy as np

class MemoizeData:
    def __init__(self, obj_fun, exp_fun, constr_fun):
        self.obj_fun = obj_fun
        self.exp_fun = exp_fun
        self.constr_fun = constr_fun
        self._data = None
        self.x = None

    def _compute_if_needed(self, x, *args):
        if not np.all(x == self.x) or self._data is None:
            self.x = np.asarray(x).copy()
            self._data = self.exp_fun(x)

    def __call__(self, x, *args):
        self._compute_if_needed(x, *args)
        return self.obj_fun(self._data)

    def constraint(self, x, *args):
        self._compute_if_needed(x, *args)
        return self.constr_fun(self._data)

接下来,昂贵函数在每次迭代中只计算一次,然后,在将所有约束写入一个约束函数后,可以像这样使用它:

from scipy.optimize import minimize

def all_constrs(data):
    return np.hstack((cheap_fun2(data), cheap_fun3(data)))

obj = MemoizeData(cheap_fun1, expensive_fun, all_constrs)
constr = {'type': 'ineq', 'fun': obj.constraint}

x0 = np.ones(6)
opt_result = minimize(obj, x0, method="COBYLA", constraints=constr)
fwzugrvs

fwzugrvs2#

当Joni在写他们的答案时,我发现了另一个,无可否认,这个答案更古怪。我更喜欢他们的答案,但为了完整起见,我也想把这个贴出来。
它来源于https://mdobook.github.io/的材料和杨百翰大学流动实验室的随附视频教程,特别是this video
诀窍是使用非局部变量来缓存开销函数的最后一次求值:

import numpy as np

last_x = None
last_data = None

def compute_data(x):
    data = expensive_fun(x)
    return(data)

def get_last_data(x):
    nonlocal last_x, last_data
    if not np.array_equal(x, last_x):
        last_data = compute_data(x)
        last_x = x
    return(last_data)

def costfun(x):
    data = get_last_data(x)
    return(cheap_fun1(data)

def constr1(x):
    data = get_last_data(x)
    return(cheap_fun2(data)

def constr2(x):
    data = get_last_data(x)
    return(cheap_fun3(data)

......然后一切都可以按照问题中的原始代码进行。
我更喜欢Joni基于类的版本的原因:

  • 变量范围比nonlocal更清晰
  • 如果某些函数允许计算其雅可比行列式,或者存在其他值得缓冲的内容,则与使用相比,可以更好地控制增加的复杂性
  • 让一个类示例完成所有的工作还允许你做其他有趣的事情,比如记录所有过去的求值和优化器所采用的路径,而不必使用单独的回调函数。如果优化器不收敛或花费太长时间,这对于调试/调整收敛非常有用,而且对于可视化或以其他方式研究目标函数或类似的东西也非常有用。
  • 同样的能力对于从之前的函数计算结果构建响应面模型等事情来说可能真的很酷,如果昂贵的函数是某个数值方法,它可以从一个好的起点中获益,那么它可以用来建立一个起始猜测。

这两种方法都允许使用“廉价”约束,即不需要计算昂贵的函数,只需将它们作为单独的函数提供即可。但不确定这是否会对计算时间有很大帮助。我认为这将取决于优化器使用的算法。

相关问题