运筹优化博士,只做原创博文。更多关于运筹学,优化理论,数据科学领域的内容

0 介绍

前面介绍的割平面法和分支定界法都是求解整数规划的常用方法,但是面对大规模整数规划/混合整数规划,往往直接采用割平面法和分支定界法求解是不现实的,这时候就需要对大规模整数规划/混合整数规划问题先进行分解和松弛,然后再进一步采用割平面法和分支定界法来帮助求解。目前我个人总结整数规划问题的分解/松弛的主流的方法有如下三种:
1 Benders decomposition (主要思想是行生成+割平面方法)
2 Dantzig-Wolfe decomposition (主要思想其实就是列生成)
3 Lagrangian decomposition (主要思想是 Lagrangian relaxation)
我们今天主要介绍的是 列生成 (Column Generation) 方法,前两种方法我们会在后续笔记中进行更新。
列生成 (Column Generation) 实际上就是Dantzig-Wolfe decomposition 里边最重要的一个环节,因此列生成经常是和DW分解联合在一起使用。本文分为三部分,第一部分是从cutting stock问题出发介绍列生成算法的基本思想,第二部分是在python环境下基于Gurobi对列生成算法进行实现,第三部分是从对偶的角度来分析列生成算法的好坏。

1 从 cutting stock 问题出发介绍列生成算法

1.1 直接建模

cutting stock 中文可翻译为一维下料问题。假设钢材厂有若干根长度为 整数规划python代码 python整数规划求解_机器学习的钢卷,假设现在有整数规划python代码 python整数规划求解_建模_02 个客户需要 整数规划python代码 python整数规划求解_建模_03个长度为 整数规划python代码 python整数规划求解_python_04的零件。现在问怎么样切割钢卷能够在满足所有客户的基础上,让所使用的钢卷数量最少?

举个例子来说就是钢材长生产的都是统一的标准的钢卷长度为 整数规划python代码 python整数规划求解_建模_05,现在有3个客户需要10个长度为6的零件,20个长度为8的零件,12个长度为12的零件。对以上描述直接采用数学优化模型建模可得:

整数规划python代码 python整数规划求解_建模_06

整数规划python代码 python整数规划求解_机器学习_07

目标函数(极小化钢卷被使用的数量):整数规划python代码 python整数规划求解_整数规划python代码_08

约束1(满足所有客户的需求):整数规划python代码 python整数规划求解_整数规划python代码_09

约束2 (切割不能超过钢卷长度):整数规划python代码 python整数规划求解_整数规划python代码_10

整数规划python代码 python整数规划求解_建模_11

以上模型就是非常直观的一个建模,该模型也被成为 Kantorovich(苏联应用数学家康托洛维奇) 模型。该模型从求解的角度来说是一个非常不好的模型。原因是它的线性松弛的界很差,如果我们把决策变量 整数规划python代码 python整数规划求解_整数规划python代码_12都松弛为连续变量,则模型(1.1-1.3)会变成一个线性规划。

易知该线性规划(1.1-1.3)的最优解 整数规划python代码 python整数规划求解_建模_13必然会在约束(1.2)和约束(1.3)等式成立的地方。即最优解满足:

整数规划python代码 python整数规划求解_算法_14

最优的目标函数为整数规划python代码 python整数规划求解_建模_15

也就是说原模型(1.1-1.3)的线性松弛问题的最优解是一个naive的解,由这样得到的线性松弛问题模型的界是非常送的,那么直接调用求解器去求解原模型(1.1-1.3)的效率就会非常低。

1.2 基于列生成的建模

上面我们谈到直接建模得到的(1.1-1.3)整数规划模型不是一个好的模型。那么接下来我们尝试从另外一个角度对 cutting stock 进行建模。首先我们需要定义切割模式,还是从之前的例子来看,一根长度为20的钢卷切成若干长度6,8,12的零件有哪些切法?例如 切1个长度为6的,切1个长度为8的这是一种切法,切2个长度为6的,切1个长度为8的这也是一种切法,切1个长度为6的切一个长度为12 的这也是一种切法。我们把一种切法就叫做一种 切割模式,当然不止我这里列的三种切割模式。那么我们只需要决策出每种切割模型需要多少次就可以得到整个切割方案了。

主问题(Master Problem)
我们将上述采用切割模型的方式用数学模型描述出来就可以得到

决策变量:整数规划python代码 python整数规划求解_算法_16
目标函数:整数规划python代码 python整数规划求解_机器学习_17
约束:整数规划python代码 python整数规划求解_python_18
其中 整数规划python代码 python整数规划求解_机器学习_19是参数 表示 在切割模式 整数规划python代码 python整数规划求解_建模_20中 切割零件整数规划python代码 python整数规划求解_整数规划python代码_21的数量。

在约束(1.8)中 每一列就表示一种切割模式。我们发现切割模式一般是指数级别的(对应到模型中就是 n 的数值),也就是说想要把所有切割模式都列举出来是不现实的。因此我们就会自然想到是不是能只加入一部分切割模式,先得到一个可行解(虽然这个可行解可能不太好),接着再加入一些列来逐渐改进切割模式。从这个想法出发,我们在原始的主问题的基础上只列出一部分切割模式,得到受限的主问题:

受限的主问题(Restricted Master Problem)
整数规划python代码 python整数规划求解_python_22
其中集合 整数规划python代码 python整数规划求解_python_23

上述受限的主问题的对偶问题为:
整数规划python代码 python整数规划求解_python_24

子问题(Subproblem)
我们希望在受限的主问题的对偶问题上添加一列来改进受限的主问题的最优解。从线性规划的reduced cost的公式可以知道,添加第整数规划python代码 python整数规划求解_机器学习_25
整数规划python代码 python整数规划求解_算法_26

一种直观的添加列的方式就是选取能够让受限的主问题目标函数最小的列:
整数规划python代码 python整数规划求解_机器学习_27

进一步将上式等价改写为如下子问题:
整数规划python代码 python整数规划求解_建模_28

其中 整数规划python代码 python整数规划求解_建模_29表示第整数规划python代码 python整数规划求解_建模_20整数规划python代码 python整数规划求解_建模_31

2 列生成 cutting stock 算法流程与代码实现

列生成算法的基本流程如下所示:

整数规划python代码 python整数规划求解_机器学习_32


从上图中可以看出,一开始是初始化一个最简单的切割模式(pattern)。之后就是一个循环迭代过程,先求解受限的主问题获得对偶变量,然后求解背包问题获得可以新添加的列,最后把这个列加入到受限的主问题。如此往复循环直到收敛为止。

整个列生成算法的代码分为三部分:1受限的主问题;2子问题;3主干代码。本代码借助Gurobi来构建优化模型和求解优化模型,所以如果想读懂代码需要先掌握Gurobi的使用方法。

受限的主问题代码:

class Master:
    def __init__(self, lengths, demands, W) -> None:
        self.M, self.lengths, self.demands, self.W = len(lengths), lengths, demands, W
        self.n_col, self.n_dim =  0, 0
    
    def create_model(self):
        self.x = []
        self.model = gp.Model("Master")
        self.__set_vars()
        self.__set_contrs()
    
    def solve(self, flag = 0):
        self.model.Params.OutputFlag = flag
        self.model.optimize()
    
    def get_dual_vars(self):
        return [self.constrs[i].getAttr(GRB.Attr.Pi) for i in range(len(self.constrs))]
    
    def __set_contrs(self) -> None:
        self.constrs = self.model.addConstrs((self.x[i]*(self.W // self.lengths[i]) >= self.demands[i]) for i in range(self.M))
    
    def __set_vars(self) -> None:
        for i in range(self.M):
            self.x.append(self.model.addVar(obj = 1, lb = 0, ub = GRB.INFINITY, vtype = GRB.CONTINUOUS, name = 'x'+ str(i)))
        self.n_col = 1
        self.n_dim = self.M
    
    def update_contrs(self, column_coeff):
        self.column = gp.Column(column_coeff, self.model.getConstrs())
        self.model.addVar(vtype = GRB.CONTINUOUS, lb = 0, obj = 1, name = 'x' + str(self.n_dim), column = self.column)
        self.n_dim += 1
        self.n_col += 1

子问题代码

class SubProblem:
    def __init__(self, lengths, W) -> None:
        self.lengths, self.M, self.W = lengths, len(lengths), W
    
    def create_model(self):
        self.model = gp.Model("sub model")
        self.y = self.model.addVars(self.M, lb = 0, ub = GRB.INFINITY, vtype = GRB.INTEGER, name = 'y')
        self.model.addConstr((gp.quicksum(self.lengths[i]*self.y[i] for i in range(self.M)) <= self.W))
    
    def set_objective(self, pi):
        self.model.setObjective(gp.quicksum(pi[i]*self.y[i] for i in range(self.M)), sense = GRB.MAXIMIZE)
    
    def solve(self, flag = 0):
        self.model.Params.OutputFlag = flag
        self.model.optimize()
    
    def get_solution(self):
        return [self.model.getVars()[i].x for i in range(self.M)]

    def get_reduced_cost(self):
        return self.model.ObjVal

主干代码:

W = 20 # width of large roll
lengths = [3, 7, 9, 16] 
demands = [25, 30, 14, 8]
M = len(lengths) # number of items
MAX_ITER_TIMES = 10 # 最大迭代次数

cutting_stock = Master(lengths, demands, W) #初始化主问题
cutting_stock.create_model()    #建立主问题模型

sub_prob = SubProblem(lengths, W) #初始化子问题
sub_prob.create_model()  #建立子问题模型

for k in range(MAX_ITER_TIMES): 
    cutting_stock.solve()  # 求解主问题
    pi = cutting_stock.get_dual_vars() #得到主问题的对偶变量 
    cutting_stock.write()
 
    sub_prob.set_objective(pi)  # 重新给子问题目标函数赋值
    sub_prob.solve() # 求解子问题
    y = sub_prob.get_solution() #获得子问题的最优解
    reduced_cost = sub_prob.get_reduced_cost() #获得子问题的 reduced cost
    sub_prob.write()
    cutting_stock.update_contrs(column_coeff=y) #更新主问题的约束(添加新的列进去)
    if reduced_cost <= 1:  # 判定收敛条件
        break

cutting_stock.to_int() # 将主问题的模型改回整数规划问题求解
cutting_stock.solve(flag=1)

想下载完整版代码可见如下链接:
EasyIntegerProgramming/ColumnGeneration at master · WenYuZhi/EasyIntegerProgramming

3 从对偶角度分析列生成算法的好坏

如果说你仅仅是想使用列生成算法的话,到前面第二部分结束就已经足够了。那我们作为一个优化理论的研究者,仅仅停留在建模+算法实现的层次还是不够的。我们需要回答一个问题就是采用列生成算法得到的解到底质量如何?是不是能足够接近最优解呢?

考虑最初的 cutting stock 的模型(1.1-1.3),我们将其整理如下所示:

整数规划python代码 python整数规划求解_python_33

通过观察以上模型可知,如果我们采用拉格朗日松弛将约束(3.2)松弛掉,那么该问题就会被分解为 整数规划python代码 python整数规划求解_建模_11个背包问题。可得松弛问题为:
整数规划python代码 python整数规划求解_建模_35
其中 整数规划python代码 python整数规划求解_算法_36是拉格朗日乘子。

我们进一步可以将上述问题分解为 整数规划python代码 python整数规划求解_建模_11 个背包问题,如下所示:
整数规划python代码 python整数规划求解_算法_38
其中
整数规划python代码 python整数规划求解_python_39
进一步将 整数规划python代码 python整数规划求解_算法_40等价改写为:若整数规划python代码 python整数规划求解_整数规划python代码_41整数规划python代码 python整数规划求解_机器学习_42,若整数规划python代码 python整数规划求解_整数规划python代码_43整数规划python代码 python整数规划求解_整数规划python代码_44。因此 整数规划python代码 python整数规划求解_算法_45,其中
整数规划python代码 python整数规划求解_整数规划python代码_46

上述优化问题(3.12-3.14)是一个背包问题,并且这个背包问题和整数规划python代码 python整数规划求解_算法_47无关。因此进一步可以将式(3.8)改写为如下式:
整数规划python代码 python整数规划求解_算法_48

至此,我们给出定理3.1:对偶问题 整数规划python代码 python整数规划求解_建模_49=主问题(1.7-1.9)最优解。

参考文献:

【1】孙小玲,李端,整数规划,科学出版社,2010
【2】Laurence A. Wolsey, Integer Programming, Wily, 2021
【3】运筹OR帷幄:优化 | 从下料问题看整数规划中的列生成方法(附Gurobi求解器源代码)
【4】Column Generation求解Cutting Stock Problem