编写一个带有整数部分的用户定义问题(MINLP)#
在这个pygmo教程中,我们展示了如何编写一个具有单一目标、不等式约束(在这种情况下没有等式约束)和整数部分的非平凡用户定义问题(UDP)。我们假设问题的数学公式如下:
这是Luksan, L., 和 Jan Vlcek在“无约束和等式约束优化的稀疏和部分可分离测试问题”(1999)中问题5.9的修改实例。修改之处在于约束条件,在本教程中,这些约束被视为不等式而非等式,并且限制决策向量的最后两个变量为整数。在优化问题的分类中,最终问题被归类为混合整数非线性规划(MINLP)问题。
暂时忽略适应度,UDP的基本结构让pygmo理解问题类型将是:
>>> class my_minlp:
... def get_bounds(self):
... return ([-5]*6,[5]*6)
... # Inequality Constraints
... def get_nic(self):
... return 6
... # Integer Dimension
... def get_nix(self):
... return 2
注意我们需要明确指定不等式约束的数量和整数问题的维度(因为pygmo默认情况下假设两者都为0)。还要注意(对于整数部分)边界必须是整数,否则pygmo会报错。在这种情况下,不需要明确指定目标的数量,因为pygmo默认假设为单目标优化。关于UDP可选方法的完整文档可以在pygmo.problem文档中找到。
我们仍然需要编写适应度函数,因为这是所有UDPs的强制方法(与get_bounds()一起)。使用不完整的UDP构建problem将会失败。在pygmo中,适应度函数按照强制顺序[obj,ec,ic]封装了目标和约束。所有不等式约束的形式为\(g(x) <= 0\),如pygmo.problem.fitness()中所述。
>>> import math
>>> class my_minlp:
... def fitness(self, x):
... obj = 0
... for i in range(3):
... obj += (x[2*i-2]-3)**2 / 1000. - (x[2*i-2]-x[2*i-1]) + math.exp(20.*(x[2*i - 2]-x[2*i-1]))
... ce1 = 4*(x[0]-x[1])**2+x[1]-x[2]**2+x[2]-x[3]**2
... ce2 = 8*x[1]*(x[1]**2-x[0])-2*(1-x[1])+4*(x[1]-x[2])**2+x[0]**2+x[2]-x[3]**2+x[3]-x[4]**2
... ce3 = 8*x[2]*(x[2]**2-x[1])-2*(1-x[2])+4*(x[2]-x[3])**2+x[1]**2-x[0]+x[3]-x[4]**2+x[0]**2+x[4]-x[5]**2
... ce4 = 8*x[3]*(x[3]**2-x[2])-2*(1-x[3])+4*(x[3]-x[4])**2+x[2]**2-x[1]+x[4]-x[5]**2+x[1]**2+x[5]-x[0]
... ci1 = 8*x[4]*(x[4]**2-x[3])-2*(1-x[4])+4*(x[4]-x[5])**2+x[3]**2-x[2]+x[5]+x[2]**2-x[1]
... ci2 = -(8*x[5] * (x[5]**2-x[4])-2*(1-x[5]) +x[4]**2-x[3]+x[3]**2 - x[4])
... return [obj, ce1,ce2,ce3,ce4,ci1,ci2]
... def get_bounds(self):
... return ([-5]*6,[5]*6)
... def get_nic(self):
... return 6
... def get_nix(self):
... return 2
为了检查上述UDP是否格式正确,我们尝试从中构造一个pygmo.problem并检查它:
>>> import pygmo as pg
>>> prob = pg.problem(my_minlp())
>>> print(prob)
Problem name: ...
Global dimension: 6
Integer dimension: 2
Fitness dimension: 7
Number of objectives: 1
Equality constraints dimension: 0
Inequality constraints dimension: 6
Tolerances on constraints: [0, 0, 0, 0, 0, ... ]
Lower bounds: [-5, -5, -5, -5, -5, ... ]
Upper bounds: [5, 5, 5, 5, 5, ... ]
Has batch fitness evaluation: false
Has gradient: false
User implemented gradient sparsity: false
Has hessians: false
User implemented hessians sparsity: false
Fitness evaluations: 0
Thread safety: none
一切似乎都井然有序。尺寸符合我们的要求,没有检测到梯度等。
通过松弛解决你的MINLP#
MINLP问题是最难解决的优化问题之一,目前没有太多通用的方法能够有效解决这些问题。在本教程中,我们展示了一种基于松弛技术的可能解决方案。本质上,我们移除了整数约束并在\(\mathbb R^6\)中解决问题。然后,我们取解,将最后两个分量固定到最近的可行整数,并在\(\mathbb R^4\)中再次解决由此产生的简化问题。
为了实现上述策略(这里仅作为示例,并不保证能找到最佳解决方案),我们需要一个良好的NLP求解器来解决我们问题的松弛版本。因此,我们需要目标函数和约束的梯度。所以我们添加它们:
>>> def _gradient(self, x):
... return pg.estimate_gradient_h(lambda x: self.fitness(x), x)
>>> my_minlp.gradient = _gradient
>>> # We need to reconstruct the problem as we changed its definition (adding the gradient)
>>> prob = pg.problem(my_minlp())
>>> prob.c_tol = [1e-8]*6
请注意,在这个UDP中,对决策向量的整数部分求梯度是有意义的,因为它包含相关信息,但这并不总是如此。每当你的UDP的梯度不包含任何信息时,松弛技术并不是一个真正的选择,一些全局启发式方法(例如进化算法)可能是唯一的选择。
Pygmo 对 MINLP 问题的支持是围绕使整数松弛变得非常容易的理念构建的。因此,我们可以在 MINLP 上调用 NLP 求解器(或任何其他合适的算法),并且问题的松弛版本将被解决,返回违反整数约束的决策向量种群。
>>> # We run 20 instances of the optimization in parallel via a default archipelago setup
>>> archi = pg.archipelago(n = 20, algo = pg.ipopt(), prob = my_minlp(), pop_size=1)
>>> archi.evolve(2); archi.wait()
>>> # We get the best of the parallel runs
>>> a = archi.get_champions_f()
>>> a2 = sorted(archi.get_champions_f(), key = lambda x: x[0])[0]
>>> best_isl_idx = [(el == a2).all() for el in a].index(True)
>>> x_best = archi.get_champions_x()[best_isl_idx]
>>> f_best = archi.get_champions_f()[best_isl_idx]
>>> print("Best relaxed solution, x: {}".format(x_best))
Best relaxed solution, x: [...
>>> print("Best relaxed solution, f: {}".format(f_best))
Best relaxed solution, f: [...
问题的宽松版本有一个全局最优解,其中 \(x_5 = 0.75822315\),\(x_6 = 0.91463117\),这表明在寻找解时应考虑 \(x_5 \in [0,1]\),\(x_6 \in [0,1]\) 的值。对于四种可能的情况,我们因此固定最后两个变量的边界。在 \(x_5 = 0\),\(x_6 = 0\) 的情况下,我们得到:
>>> # We fix the box bounds for x5 and x6
>>> def get_bounds_(self):
... return ([-5]*4+[0,0],[5]*4+[0,0])
>>> my_minlp.get_bounds = get_bounds_
>>> # We need to reconstruct the problem as we changed its definition (modified the bounds)
>>> prob = pg.problem(my_minlp())
>>> prob.c_tol = [1e-14]*4 + [0] * 2
>>> # We solve the problem, this time using one only process
>>> pop = pg.population(prob)
>>> x_best[-1] = 0; x_best[-2] = 0
>>> pop.push_back(x_best)
>>> algo = pg.algorithm(pg.ipopt())
>>> pop = algo.evolve(pop)
>>> print("Best objective: ", pop.champion_f[0])
Best objective: 134.065695174
>>> print("Best decision vector: ", pop.champion_x)
Best decision vector: [ 0.4378605 0.33368365 -0.75844494 -1. 0. 0. ]
我们找到了一个可行的解决方案!请注意,在其他三种情况下不存在可行的解决方案。
注意
上述解决方案策略在一般情况下存在缺陷,因为它假设松弛问题的最佳解接近完整的MINLP问题解。更复杂的技术会更为彻底地搜索组合部分。我们在这里使用这种方法只是为了展示在pygmo中定义和解决松弛问题以及将最优决策向量反馈到MINLP解决方案策略中是多么简单。