编写一个带有整数部分的用户定义问题(MINLP)#

在这个pygmo教程中,我们展示了如何编写一个具有单一目标、不等式约束(在这种情况下没有等式约束)和整数部分的非平凡用户定义问题(UDP)。我们假设问题的数学公式如下:

\[\begin{split}\begin{array}{rl} \mbox{minimize:} & \sum_{i=1}^3 \left[(x_{2i-1}-3)^2 / 1000 - (x_{2i-1}-x_{2i}) + \exp(20(x_{2i-1}-x_{2i}))\right]\\ \mbox{subject to:} & -5, <= x_i, <= 5\\ & 4(x_1-x_2)^2+x_2-x_3^2+x_3-x_4^2 <= 0 \\ & 8x_2(x_2^2-x_1)-2(1-x_2)+4(x_2-x_3)^2+x_1^2+x_3-x_4^2+x_4-x_5^2 <= 0 \\ & 8x_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_6^2 <= 0 \\ & 8x_4(x_4^2-x_3)-2(1-x_4)+4(x_4-x_5)^2+x_3^2-x_2+x_5-x_6^2+x_2^2+x_6-x_1 <= 0 \\ & 8x_5(x_5^2-x_4)-2(1-x_5)+4(x_5-x_6)^2+x_4^2-x_3+x_6+x_3^2-x_2 <= 0 \\ & 8x_6(x_6^2-x_5)-2(1-x_6) +x_5^2-x_4+x_4^2 >= x_5 \\ & x_5, x_6 \in \mathbb Z \end{array}\end{split}\]

这是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解决方案策略中是多么简单。