线性规划技巧: Benders Decomposition

2020-03-14  本文已影响0人  胡拉哥

Benders分解由Jacques F. Benders在1962年提出[1]. 它是一种把线性规划问题分解为小规模子问题的技巧. 通过迭代求解主问题和子问题, 从而逼近原问题的最优解. 与列生成(Column Generation)相比, Benders分解是一种行生成(Row Generation)技巧: 主问题的约束来自子问题的解. 本文介绍的内容基于 Georrion和Graves[2].

问题描述

考虑如下的数学规划问题P(x,y):
\begin{aligned} \min ~ & c^{T} x + f(y) \\ & A x + F(y) = b \\ & x \geq 0 \\ & y \in Y \end{aligned}
其中A\in \mathbb{R}^{m \times n}, c\in \mathbb{R}^n, b\in \mathbb{R}^m, y\in\mathbb{R}^p. f(y)F(y)可以是非线性的. Y可以是离散或连续的. 给定y, 上述问题则变成了一个标准的线性规划问题, 记作P(x|y).

假设. \forall y\in Y, 线性规划P(x|y)存在最优解. (否则原问题也不存在最优解.)

Benders分解

等价形式

先把原问题P(x,y)写成如下形式:
\min_{y \in Y} \{ f(y) + \min_x \{c^Tx | Ax = b - F(y), x\geq 0\} \}

根据假设, 问题
P(x|y): \quad \min_x \{c^Tx | Ax=b-F(y), x\geq 0\}
存在最优解. 其对偶问题
D(u|y):\quad \max_u \{ [b-F(y)]^Tu | A^T u \leq c \}
的最优解也存在. (不会写对偶? 这篇文章手把手教你: <线性规划技巧: 如何写对偶问题>) .

因此P(x, y)进而可以写成:
\min_{y \in Y} \{ f(y) + \max_u \{ [b-F(y)]^Tu | A^T u \leq c \} \}.

写成上述形式的好处是D(u|y)中的约束与变量y无关. 因此, 其最优解一定是多面体(Polytope) Q=\{u | A^Tu\leq c\}中的一个顶点. 令U代表多面体Q顶点的集合. 因此原问题等价于
P(u,y): \quad \min_{y \in Y} \{ f(y) + \max_{u\in U} [b-F(y)]^T u \}.

再把P(u, y)写成数学规划的形式P_1(u,y):
\begin{aligned} \min ~ & f(y) + z \\ \text{ s.t. } & [b-F(y)]^Tu \leq z, \quad u \in U \\ & y \in Y. \end{aligned}

问题分解

对原问题P(x, y), 我们先固定y得到P(x|y), 然后考虑其对偶问题D(u|y). 通过这种方式我们把原问题转化成等价问题P_1(u,y). 在新问题P_1(u,y)中, 每个约束条件对应一个对偶问题D(u|y)的基本解, 且目标函数f(y)+zu无关. 这样一来, 使得我们可以通过迭代的方式进行求解: 主问题P_1(u,y)一开始只考虑少量的约束, 然后通过求解对偶问题D(u|y)添加新的约束到P_1(u,y), 直到逼近最优解.

主问题M(y, z)
\begin{aligned} \min ~ & f(y) + z \\ \text{s.t. } & [b - F(y)]^T u \leq z, \quad u \in B \\ & y\in Y. \end{aligned}

我们知道u是多面体\{u| A^T u \leq c \}的顶点. 是否需要把所有顶点加入到主问题中? 回顾主问题P(u,y)的目标函数\min_{y \in Y} \{ f(y) + \max_{u\in U} [b-F(y)]^T u \}. 显然, 我们应该找到u使得[b-F(y)]^Tu最大化.

子问题S(u|y)

\begin{aligned} \max ~ [b-F(y)]^T u \\ \text{s.t. } A^T u \leq c. \end{aligned}

求解过程

思路

  1. 初始化主问题. 令B=\emptyset. 换句话说, 主问题一开始不考虑关于u的约束. 令z=0, 然后求解M(y, z=0), 从而得到子问题的输入y_0.
  2. 令OPT(master)代表最优解的值. 由于主问题只考虑了部分约束, 因此OPT(master)是原问题P_1(u,y)最优解的下界.
  3. 求解子问题S(u|y_0)得到u_1, 然后把约束[b-F(y)]^T u_1 \leq z添加到主问题M(y, z).
  4. 令OPT(sub)代表子问题最优解的值. 回顾原问题P(u,y)的目标函数f(y) + \max_u [b-F(y)]^Tu, 因此f(y)+OPT(sub)是原问题最优解的上界.
  5. 反复求解主问题和子问题直到上下界相等(或非常接近).

伪代码

Init: B = empty; UB = inf; e = -1e-4;
solve master problem M(y, z) by fixing z := 0 and get y;
let LB := f(y) + z;
While UB - LB > e: 
    solve sub problem S(u|y) and get u;
    update UB := min {UB, f(y) + OPT(sub)};
    add constraint [b-F(y)]^T u <= z to the master problem;
    solve master problem M(y, z) and get y;
    update LB := OPT(master);
EndWhile

说明

  1. 迭代过程中LB不会减小, UB不会增大.
  2. 如果子问题的最优解满足唯一性, 这个迭代过程收敛(证明略).

例子: 设施选址问题(A Facility Location Problem)

某个工厂需要开设一些仓库, 用来向它的客户提供仓储和配送服务.

图片来自网络.

(如上图所示, 蓝色方框代表开设的仓库, 其它候选仓库未展示. 红色圆圈代表客户. 黑色的线代表服务关系.)

整数线性规划

Indices

Parameters

Decision Variables

\begin{align} \min ~ & \sum_{i=1}^m \sum_{j=1}^n c_{i,j} x_{i,j} + \sum_{i=1}^m f_i y_i \\ \text{ s.t. } & \sum_{i=1}^m x_{i, j} = 1,\quad \forall j \\ & x_{i, j} \leq y_i, \quad \forall i, j \\ & x_{i, j}, y_i \in \{ 0, 1 \}, \quad \forall i, j \end{align}

如果开设仓库确定, 我们指定离客户最近的仓库来服务这个客户, 从而达到连接成本最低. 因此, 设施选址问题的核心是决定开设哪些仓库, 因而上述规划的决策变量x_{i,j}的取值范围可以写成x_{i,j} \geq 0. 我们得到如下规划P(x,y):

\begin{align} \min ~ & \sum_{i=1}^m \sum_{j=1}^n c_{i,j} x_{i,j} + \sum_{i=1}^m f_i y_i \\ \text{ s.t. } & \sum_{i=1}^m x_{i, j} = 1,\quad \forall j \\ & x_{i, j} \leq y_i, \quad \forall i, j \\ & x_{i, j} \geq 0, ~ y_i \in \{ 0, 1 \}, \quad \forall i, j \end{align}

Benders分解

给定y_i, P(x|y)的最优解等价于如下问题的最优解.
\begin{align} \min ~ & \sum_{ i=1}^{m} \sum_{j=1}^{n} c_{i,j} x_{i,j} \\ \text{ s.t. } & \sum_{i=1}^{m} x_{i, j} = 1, \quad \forall j \\ & x_{i, j} \leq y_i, \quad \forall i, j \\ & x_{i, j} \geq 0, \quad \forall i, j \end{align}
它的对偶问题则是我们要求解的子问题S(\alpha, \beta|y):
\begin{align} \max ~ & \sum_{ j=1}^n \alpha_j - \sum_{i=1}^m \sum_{j=1}^{n} y_i \beta_{i, j} \\ \text{s.t. } & \alpha_j - \beta_{i, j} \leq c_{i,j}, \quad \forall i, j \\ & \beta_{i, j} \geq 0 \end{align}

结合原问题P(x,y)的目标, 我们得到主问题M_0(y, z):
\begin{align} \min ~ & \sum_{i=1}^{m} f_i y_i + z \\ \text{ s.t. } & \sum_{ j=1}^{n } \alpha_j - \sum_{i=1}^{m} \sum_{j=1}^{n} y_i \beta_{i, j} \leq z, \quad \forall (\alpha, \beta) \in B \\ & y_i \in \{ 0,1\}, \quad \forall i \end{align}

注意: 我们需要检查本文开头提到的假设是否成立. 即, 对任意可行的y_i, 子问题S(\alpha, \beta|y)是否存在最优解. 事实上, 当y_i=0, \forall i时, 子问题的目标函数的最大值是+\infty, 因而不存在最优解! 因此我们需要为主问题添加约束来保证假设条件成立. 注意到原问题P(x,y)的最优解至少要保证开设1个仓库, 把它加入主问题的约束从而保证了子问题最优解的存在(想想为什么).

主问题M(y,z)
\begin{align} \min ~ & \sum_{i=1}^{m} f_i y_i + z \\ \text{ s.t. } & \sum_{ j=1}^{n} \alpha_j - \sum_{i=1}^{m} \sum_{j=1}^{n} y_i \beta_{i, j} \leq z, \quad \forall (\alpha, \beta) \in B \\ & \sum_i {y_i} \geq 1 \\ & y_i \in \{ 0,1\}, \quad \forall i \end{align}

说明 主问题M(y,z)的约束条件一开始为空, 即B=\emptyset. 它的约束条件通过求解子问题得到.

Python实现

主问题模型

# master_model.py

from ortools.linear_solver import pywraplp

import numpy as np


class MasterModel(object):

    def __init__(self, f, fix_z=None):
        """
        :param f: 开仓成本(m维向量)
        :param fix_z: 限定z的值. 目的是初始化的时候令z=0,然后求解主问题得到y的值.
        """
        self._solver = pywraplp.Solver('MasterModel',
                                       pywraplp.Solver.GLOP_LINEAR_PROGRAMMING)
        self._f = f
        self._fix_z = fix_z
        self._m = len(self._f)
        self._y = None  # 决策变量
        self._z = None  # 决策变量
        self._solution_y = None  # 计算结果
        self._solution_z = None  # 计算结果
        # 迭代求解之前会调用add_constraint
        # 所以决策变量的初始化要放在前面
        self._init_decision_variables()
        self._init_constraints()
        self._init_objective()

    def _init_decision_variables(self):
        self._y = [self._solver.NumVar(0, 1, 'y[%d]' % i)
                   for i in range(self._m)]
        self._z = self._solver.NumVar(-self._solver.Infinity(), self._solver.Infinity(), 'z')

    def _init_objective(self):
        obj = self._solver.Objective()
        for i in range(self._m):
            obj.SetCoefficient(self._y[i], self._f[i])
        obj.SetCoefficient(self._z, 1)
        obj.SetMinimization()

    def add_constraint(self, alpha, beta):
        """ Benders主流程会调用此方法为主问题增加新的约束.

        :param alpha: 子问题的解
        :param beta: 子问题的解
        """
        ct = self._solver.Constraint(sum(alpha), self._solver.Infinity())
        for i in range(self._m):
            ct.SetCoefficient(self._y[i], sum(beta[i]))
        ct.SetCoefficient(self._z, 1)

    def _init_constraints(self):
        if self._fix_z is not None:
            ct = self._solver.Constraint(self._fix_z, self._fix_z)
            ct.SetCoefficient(self._z, 1)

        ct = self._solver.Constraint(1, self._solver.infinity())
        for i in range(self._m):
            ct.SetCoefficient(self._y[i], 1)

    def solve(self):
        self._solver.Solve()
        self._solution_y = [self._y[i].solution_value() for i in range(self._m)]
        self._solution_z = self._z.solution_value()

    def get_solution(self):
        return self._solution_y, self._solution_z

    def get_objective_value(self):
        return sum(np.array(self._solution_y) * np.array(self._f)) + self._solution_z

子问题模型

# sub_model.py

from ortools.linear_solver import pywraplp
import numpy as np


class SubModel(object):

    def __init__(self, y, C):
        """
        :param y: 主问题的解(代表y_i=1代表开设仓i)
        :param C: 连接成本(m*n维矩阵)
        """
        self._solver = pywraplp.Solver('SubModel',
                                       pywraplp.Solver.GLOP_LINEAR_PROGRAMMING)
        self._y = y
        self._c = C
        self._m = len(self._c)
        self._n = len(self._c[0])
        self._alpha = None  # 决策变量
        self._beta = None  # 决策变量
        self._constraints = []  # 所有约束(后续获取对偶变量, 得到原始问题的解)
        self._solution_alpha = None  # 计算结果
        self._solution_beta = None  # 计算结果

    def _init_decision_variables(self):
        self._alpha = [self._solver.NumVar(-self._solver.Infinity(),
                                           self._solver.Infinity(),
                                           'alpha[%d]' % j)
                       for j in range(self._n)]

        self._beta = [[self._solver.NumVar(0, self._solver.Infinity(), 'beta[%d][%d]' % (i, j))
                      for j in range(self._n)] for i in range(self._m)]

    def _init_constraints(self):
        for i in range(self._m):
            self._constraints.append([])
            for j in range(self._n):
                ct = self._solver.Constraint(-self._solver.Infinity(), self._c[i][j])
                ct.SetCoefficient(self._alpha[j], 1)
                ct.SetCoefficient(self._beta[i][j], -1)
                self._constraints[i].append(ct)

    def _init_objective(self):
        obj = self._solver.Objective()
        for j in range(self._n):
            obj.SetCoefficient(self._alpha[j], 1)
        for i in range(self._m):
            for j in range(self._n):
                obj.SetCoefficient(self._beta[i][j], -self._y[i])

        obj.SetMaximization()

    def solve(self):
        self._init_decision_variables()
        self._init_constraints()
        self._init_objective()
        self._solver.Solve()
        self._solution_alpha = [self._alpha[j].solution_value() for j in range(self._n)]
        self._solution_beta = [[self._beta[i][j].solution_value()
                                for j in range(self._n)]
                               for i in range(self._m)]

    def get_solution(self):
        return self._solution_alpha, self._solution_beta

    def get_objective_value(self):
        return sum(self._solution_alpha) - sum(np.array(self._y) * np.sum(self._solution_beta, axis=1))

    def get_dual_values(self):
        """ 得到原问题的解: x[i][j]
        """
        return [[self._constraints[i][j].dual_value()
                for j in range(self._n)]
                for i in range(self._m)]

Benders分解流程

# benders_proc.py

import numpy as np

from master_model import MasterModel
from sub_model import SubModel


class BendersProc(object):
    """ Benders分解流程
    """

    def __init__(self, f, C, max_iter=10000):
        """
        :param f: 开仓成本(m维向量)
        :param C: 连接成本(m*n矩阵), 其中m是候选设施的数量, n是客户的数量
        :param max_iter: 最大循环次数
        """
        self._f = f
        self._c = C
        self._iter_times = 0
        self._max_iter = max_iter
        self._status = -1  # -1:执行错误; 0:最优解; 1: 达到最大循环次数
        self._ub = np.inf
        self._lb = -np.inf
        self._solution_x = None  # 计算结果
        self._solution_y = None  # 计算结果

    def _stop_criteria_is_satisfied(self):
        """ 根据上下界判断是否停止迭代
        """
        if self._ub - self._lb < 0.0001:
            self._status = 0
            return True
        if self._iter_times >= self._max_iter:
            if self._status == -1:
                self._status = 1
            return True
        return False

    def solve(self):
        # 初始令z=0. 求解主问题得到y
        master0 = MasterModel(self._f, fix_z=0)
        master0.solve()
        y, z = master0.get_solution()
        # 下面的迭代需要重新生成master对象
        # 因为master0中z=0是约束条件
        master = MasterModel(self._f)
        sub = None
        # 迭代过程
        while not self._stop_criteria_is_satisfied():
            # 求解子问题
            sub = SubModel(y, self._c)
            sub.solve()
            # 更新上界
            fy = sum(np.array(self._f) * np.array(y))
            self._ub = min(self._ub, fy + sub.get_objective_value())
            # 生成主问题的约束
            alpha, beta = sub.get_solution()
            master.add_constraint(alpha, beta)
            master.solve()
            # 更新y和下界
            y, z = master.get_solution()
            self._lb = master.get_objective_value()

            print(">>> iter %d: lb = %.4f, ub = %.4f" % (self._iter_times, self._lb, self._ub))
            self._iter_times += 1

        # 保存结果
        self._solution_x = sub.get_dual_values()
        self._solution_y = y

        status_str = {-1: "error", 0: "optimal", 1: "attain max iteration"}
        print(">>> Terminated. Status:", status_str[self._status])

    def print_info(self):
        print("---- Solution ----")
        res = {}
        for i in range(len(self._f)):
            if self._solution_y[i] == 0:
                continue
            connected_cities = [str(j) for j in range(len(self._c[0]))
                                if self._solution_x[i][j] > 0]
            res[i] = ', '.join(connected_cities)

        for f, c in res.items():
            print("open facility: %d, connected cities: %s" % (f, c))

主函数

# main.py

from benders_proc import BendersProc
from data import f, C  # data.py包含了一个实例


if __name__ == '__main__':
    bp = BendersProc(f, C)
    bp.solve()
    bp.print_info()

完整代码

参考文献


  1. J.F. Benders. Partitioning Procedures for Solving Mixed-Variables Programming Problems. Numerische Mathematik, Vol. 4, 1962.

  2. A.M. Geoffrion and G.W. Graves. Multicommodity distribution system design by benders decomposition. Management Science 20, no. 5, 822–844, 22, 1974.

上一篇下一篇

猜你喜欢

热点阅读