社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

优化 | Benders Dual Decomposition:理论、案例及实现 (Python+Gurobi)

运筹OR帷幄 • 1 年前 • 206 次点击  

作者刘兴禄,清华大学,清华大学深圳国际研究生院,清华-伯克利深圳学院,博士在读

作者:江波,清华大学,深圳国际研究生院,硕士在读

稿件初次完成时间:2021.09.28



目录

  • 1. Benders Dual Decomposition Method (BDD)

    • 1.1 经典Benders Decomposition Method

    • 1.2 Benders Dual Decomposition Method

    • 1.3 总结

  • 2. 推导Benders Dual Decomposition

    • 补充:关于Lagrangian dual problem

  • 3. Case Study

  • 4. Benders Dual Decomposition Code (Python+Gurobi版本)

  • 参考文献

  • 完整代码获取方式

1. Benders Dual Decomposition Method (BDD)

考虑如下的混合整数线性规划问题(MILP):

其中.

1.1 经典Benders Decomposition Method

我们将其按照Benders 分解的思路,拆成主问题(MP):

其中,的一个下界,是为了防止原问题无界。

主问题MP,也就是模型(2),为模型(1),也就是原模型,提供了一下下界。其中MP可以通过branch and cut算法求解。

在每一个branch-and-bound tree的节点上,该节点的解 被固定,且构成下面的子问题(SP):

我们写出子问题的对偶形式,即对偶子问题Dual subproblem(DSP):

如果问题(3-1)是无界的,我们就可以识别一个无界方向(或者极射线) ,并且构造一个Benders feasibility cut

这个cut就会被加入到MP中,去排除使得子问题无界的的解,也就是排除那些使得的解。

否则,如果(3-1)是存在最优解,则原问题的UB会被更新,且我们需要构造一个Benders optimality cut

并将其加入到MP中。这就是Benders decomposition的过程。

1.2 Benders Dual Decomposition Method

本小节来介绍Benders Dual Decomposition,我们直接给出最终的结论,至于详细的推导过程,我们在下一节进行展开讲解。

在Benders Dual Decomposition方法中,我们引入辅助变量,并令,然后子问题SP就可以被重写为:

注意,在上述重写后的模型中,约束是冗余的,因为我们限定了。但是这么转化是为了后面更方便地使用拉格朗日松弛,所以这一步转化是必要的。

求解问题(3-2),如果得到最优解,则我们就可以得到下面的optimality cut

其中,是约束的对偶变量。(因为我们是把这条约束松弛到目标函数中去了)。

如果问题(3-2)是不可行的,则我们需要求解下面的可行子问题:

求解后,最优解为且产生的feasibility cut

其中,是约束的对偶变量。(因为我们是把这条约束松弛上去了),且是一个长度为的全1的向量。

Cut (4-1) and (4-2)通常被称为generalized Benders cuts (GBC),更多内容见下面的文献.

Geoffrion, A.M., 1972. Generalized Benders decomposition. J Optim Theory Appl 10, 237–260. https://doi.org/10.1007/BF00934810

1.3 总结

经典Benders方法,就是按照Benders子问题的极点和极射线去构造这两类cut,其实是直接求解Benders子问题。

Benders Dual Decomposition方法在second-stage的子问题中,通过首先添加辅助变量和辅助约束,然后利用拉格朗日对偶理论,将拉格朗日对偶的目标函数加回master problem,从而实现更新Benders主问题的目的。这个对偶保证了不cut掉任何的feasible integer solution,最后只需要保证收敛就可以了。这个方法不是直接求subproblem,而是利用了拉格朗日对偶的有关知识。确实非常巧妙,这思路让人很受启发。

2. 推导Benders Dual Decomposition

上面Cut的形式是怎么推导出来的呢,接下来我们推一下。

我们对重写后的子问题(3-2)使用拉格朗日松弛,将约束松弛,并且取拉格朗日对偶,则(3-2)的拉格朗日对偶问题为

当然,拉格朗日对偶问题,是一个piece-wise linear函数。求解(3-2-LD)得到最优解,这就可以得到原问题的一个下界,因此,就需要加入optimality cut:

证明 (来自文献2):

同理,如果子问题无解,我们也是构造拉格朗日对偶,将约束松弛,则(3-3)的拉格朗日对偶问题为

求解(3-3-LD)得到最优解,这就可以得到原问题中变量的一个可行割,因此,就需要加入feasibility cut:

证明 (来自文献2):

补充:关于Lagrangian dual problem

以下来自参考文献[4]。

lagrangian dual problem(1)
lagrangian dual problem(2)
lagrangian dual problem(3)

3. Case Study

下面以两阶段鲁棒优化模型为例来讲解Benders dual method的用法。两阶段鲁棒优化可以使用列与约束生成算法进行求解,参见文献[3]。

我们运小筹公众号之前也对文献[3]进行了详细的理论推导,并提供了完整的实现代码。详细内容见往期推文:

鲁棒优化| C&CG算法求解两阶段鲁棒优化:全网最完整、最详细的【入门-完整推导-代码实现】笔记

本小节将补充文献[3]中涉及到的Benders dual method求解两阶段鲁棒优化模型。

两阶段鲁棒优化模型如下:

论文中给出的例子[3]:

不确定集定义如下:

将该问题写成两阶段鲁棒优化模型的形式:

根据Benders decomposition的思想,我们设置, 为第一阶段的决策变量,设置为第二阶段的决策变量。则Master problem

不过,我们需要将上述模型变化一下。其中,主问题(MP)为:

其中,第二条约束是为了保证所有的子问题一定有可行解。

子问题为(注意,其中可以看做输入参数,由上一阶段的模型给出):

我们对其进行对偶,得到

我们设不确定集为:

因此,对偶子问题的目标函数可以写为:

综上,最终形式为

主问题:

子问题:

求解子问题,如果有最优解,则得到极点 ,并且构造Benders Optimality Cut,也就是下面的通用表达式

具体为:

其中,是第次迭代中,子问题的目标函数,是第次迭代中,主问题的解,是第次迭代中,子问题的解。

可以仔细对比Benders dual method下,添加回主问题的 cut 就只有一条,而列与约束生成算法(CCG)则不仅会在MP中创新新的决策变量,而且会添加一系列的约束条件。因此,根据文[3]的对比,CCG算法的表现优于Benders dual method。

4. Benders Dual Decomposition Code (Python+Gurobi版本)

本小节给出Python调用Gurobi实现Benders duam method求解两阶段鲁棒优化模型的完整代码。

为了缩减篇幅,这里仅给出主体代码,完整代码可以通过文末的获取方式获取。

from gurobipy import *
import numpy as np

""" The input parameter """
facility_num = 3
customer_num = 3
fixed_cost = [400414326]
unit_capacity_cost = [182520]
trans_cost = [[ 223324],
              [332330],
              [202527]]
max_capacity = 800

demand_nominal = [206274220]
demand_var = [404040]

def print_sub_sol_benders(model, demand_nominal, demand_var, x, g, pi, theta, v, w, h):
    d_sol = {}
    if(model.status != 2):
        print('The problem is infeasible or unbounded!')
        print('Status: {}'.format(model.status))
    else:
        print('Obj(sub) : {}'.format(model.ObjVal), end='\t| ')
        for key in g.keys():
            # print('demand: {}, perturbation = {}'.format(demand_nominal[key], g[key].x))
            d_sol[key] = demand_nominal[key] + demand_var[key] * g[key].x
    return d_sol


""" build initial master problem """
""" Create variables """
master = Model('master problem')
x_master = {}
z = {}
y = {}
eta = master.addVar(lb=0, vtype=GRB.INTEGER, name='eta')

for i in range(facility_num):
    y[i] = master.addVar(vtype=GRB.BINARY, name='y_%s' % (i))
    z[i] = master.addVar(lb=0, vtype=GRB.INTEGER, name='z_%s' % (i))

""" Set objective """
obj = LinExpr()
for i in range(facility_num):
    obj.addTerms(fixed_cost[i], y[i])
    obj.addTerms(unit_capacity_cost[i], z[i])
obj.addTerms(1, eta)

master.setObjective(obj, GRB.MINIMIZE)

""" Add Constraints  """
# cons 1
for i in range(facility_num):
    master.addConstr(z[i] <= max_capacity * y[i])

""" Add initial value Constraints  """
# create new constraints

expr = LinExpr()
for i in range(facility_num):
    expr.addTerms(1, z[i])
master.addConstr(expr >= 772)  # 206 + 274 + 220 + 40 * 1.8
# master.addConstr(-eta - 10 * z[1] - 8 * z[2] <= -20942)     # 206 + 274 + 220 + 40 * 1.8


""" solve the model and output """
master.optimize()
print('Obj = {}'.format(master.ObjVal))
print('-----  location ----- ')
for key in z.keys():
    print('facility : {}, location: {}, capacity: {}'.format(key, y[key].x, z[key].x))

""" Column-and-constraint generation """
LB = -np.inf
UB = np.inf
iter_cnt = 0
max_iter = 100000
cut_pool = {}
eps = 0.001
Gap = np.inf

z_sol = {}
for key in z.keys():
    z_sol[key] = z[key].x
# print(z_sol)
# z_sol[0] = 300
# z_sol[1] = 300
# z_sol[2] = 200


""" solve the master problem and update bound """
master.optimize()

""" 
 Update the Lower bound 
"""

LB = master.ObjVal
print('LB: {}'.format(LB))

''' create the subproblem '''
subProblem = Model('sub problem')
x = {}  # transportation decision variables in subproblem
# d = {}    # true demand
g = {}  # uncertainty part: var part
pi = {}  # dual variable
theta = {}  # dual variable
v = {}  # aux var
w = {}  # aux var
h = {}  # aux var
big_M = 10000
for i in range(facility_num):
    pi[i] = subProblem.addVar(lb=-GRB.INFINITY, ub=0, vtype=GRB.INTEGER, name='pi_%s' % i)
    v[i] = subProblem.addVar(vtype=GRB.BINARY, name='v_%s' % i)
for j in range(customer_num):
    w[j] = subProblem.addVar(vtype=GRB.BINARY, name='w_%s' % j)
    g[j] = subProblem.addVar(lb=0, ub=1, vtype=GRB.CONTINUOUS, name='g_%s' % j)
    theta[j] = subProblem.addVar(lb=-GRB.INFINITY, ub = 0, vtype=GRB.INTEGER, name='theta_%s' % j)
#     d[j] = subProblem.addVar(lb = 0, ub = GRB.INFINITY, vtype = GRB.CONTINUOUS, name = 'd_%s'%j)
for i in range(facility_num):
    for j in range(customer_num):
        h[i, j] = subProblem.addVar(vtype=GRB.BINARY, name='h_%s_%s' % (i, j))
        x[i, j] = subProblem.addVar(lb=0, ub = GRB.INFINITY, vtype=GRB.CONTINUOUS, name='x_%s_%s' % (i, j))

""" set objective """
sub_obj = LinExpr()
for i in range(facility_num):
    for j in range(customer_num):
        sub_obj.addTerms(trans_cost[i][j], x[i, j])
subProblem.setObjective(sub_obj, GRB.MAXIMIZE)

""" add constraints to subproblem """
# cons 1
for i in range(facility_num):
    expr = LinExpr()
    for j in range(customer_num):
        expr.addTerms(1, x[i, j])
    subProblem.addConstr(expr <= z_sol[i], name='sub_capacity_1_z_%s' % i)

# cons 2
for j in range(facility_num):
    expr = LinExpr()
    for i in range(customer_num):
        expr.addTerms(1, x[i, j])
    subProblem.addConstr(expr >= demand_nominal[j] + demand_var[j] * g[j])

# cons 3
for i in range(facility_num):
    for j in range(customer_num):
        subProblem.addConstr(pi[i] - theta[j] <= trans_cost[i][j])

""" demand constraints """
# for j in range(customer_num):
#     subProblem.addConstr(d[j] == demand_nominal[j] + g[j] * demand_var[j])

subProblem.addConstr(g[0] + g[1] + g[2] <= 1.8)
subProblem.addConstr(g[0] + g[1] <= 1.2)

""" logic constraints """
# logic 1
for i in range(facility_num):
    subProblem.addConstr(-pi[i] <= big_M * v[i])
    expr = LinExpr()
    for j in range(customer_num):
        expr.addTerms(1, x[i, j])
    subProblem.addConstr(z_sol[i] - expr <= big_M - big_M * v[i], name='sub_capacity_2_z_%s' % i)

# logic 2
for j in range(customer_num):
    subProblem.addConstr(-theta[j] <= big_M * w[j])
    expr = LinExpr()
    for i in range(facility_num):
        expr.addTerms(1, x[i, j])
    subProblem.addConstr(expr - demand_var[j] * g[j] - demand_nominal[j] <= big_M - big_M * w[j])

# logic 3
for j in range(customer_num):
    for i in range(facility_num):
        subProblem.addConstr(x[i, j] <= big_M * h[i, j])
        subProblem.addConstr(trans_cost[i][j] - pi[i] + theta[j] <= big_M - big_M * h[i, j])
subProblem.write('SP_1_gurobi_new.lp')
subProblem.optimize()
d_sol = {}

d_sol = print_sub_sol_benders(subProblem, demand_nominal, demand_var, x, g, pi, theta, v, w, h)

print('\n\n\n *******            Benders dual method starts        *******  ')
print('\n **                Initial Solution             ** ')

d_sol = print_sub_sol_benders(subProblem, demand_nominal, demand_var, x, g, pi, theta, v, w, h)

iter_cnt = 0
""" 
 Update the initial Upper bound 
"""

UB = min(UB, subProblem.ObjVal + master.ObjVal - eta.x)
# print('UB (iter {}): {}'.format(iter_cnt, UB))

# close the flag
master.setParam('Outputflag'0)
subProblem.setParam('Outputflag'0)
"""
 Main loop of CCG algorithm 
"""

while (UB - LB > eps and iter_cnt <= max_iter):
    iter_cnt += 1
    # print('\n\n --- iter : {} --- \n'.format(iter_cnt))
    print('\n iter : {} '.format(iter_cnt), end='\t | ')

    # if subproblem is frasible and bound, create variables xk+1 and add the new constraints
    if (subProblem.status == 2 and subProblem.ObjVal 1000000000):
        """ Benders Optimality Cut"""
        # create new cuts
        """
         eta >= sum {theta * d} - sum {pi * z}   
        """

        ......
        ......

        # print('constant: ', constant)
        master.addConstr(eta >= constant + expr)
        # print(eta >= constant + expr)
        master.write('master_gurobi_benders.lp')
        master.optimize()
        # update LB
        LB = max(LB, master.ObjVal)
        # print('LB (iter {}): {}'.format(iter_cnt, LB))

        # solve the resulted master problem
        # master.optimize()
        print('Obj(master): {}'.format(master.ObjVal), end='\t | ')
        """ Update the LB """
        LB = master.ObjVal
        print('LB (iter {}): {}'.format(iter_cnt, LB), end='\t | ')

        """ Update the subproblem """
        # first, get z_sol from updated master problem
        for key in z.keys():
            z_sol[key] = z[key].x

            # change the coefficient of subproblem
        for i in range(facility_num):
            constr_name_1 = 'sub_capacity_1_z_' + str(i)
            constr_name_2 = 'sub_capacity_2_z_' + str(i)
            subProblem.remove(subProblem.getConstrByName(constr_name_1))
            subProblem.remove(subProblem.getConstrByName(constr_name_2))
            # add new constraints
        # cons 1

            # logic 1

        """ Update the lower bound """
        subProblem.optimize()
        d_sol = print_sub_sol_benders(subProblem, demand_nominal, demand_var, x, g, pi, theta, v, w, h)

        """ 
         Update the Upper bound 
        """

        if (subProblem.status == 2):
            UB = min(UB, subProblem.ObjVal + master.ObjVal - eta.x)
            # print('eta = {}'.format(eta.x))
        print('UB (iter {}): {}'.format(iter_cnt, UB), end='\t | ')
        Gap = round(100 * (UB - LB) / UB, 2)
        print('eta = {}'.format(eta.x), end='\t | ')
        print(' Gap: {} %  '.format(Gap), end='\t')


master.write('final_benders_MP.lp')
print('\n\nOptimal solution found !')
print('Opt_Obj : {}'.format(master.ObjVal))
print(' **  Final Gap: {} %  **  '.format(Gap))
print('\n  **    Solution  **  ')
for i in range(facility_num):
    print(' {}: {},\t  {}: {} '.format(y[i].varName, y[i].x, z[i].varName, z[i].x), end='')
    print('\t actual demand: d_{}: {}'.format(i, demand_nominal[i] + demand_var[i] * g[i].x), end='')
    print('\t perturbation in worst case: {}: {}'.format(g[i].varName, g[i].x))
print('\n  **    Transportation solution  **  ')
for i in range(facility_num):
    for j in range(customer_num):
        if (x[i, j].x > 0):
            print('trans: {}: {}, cost : {} \t '.format(x[i, j].varName, x[i, j].x, trans_cost[i][j] * x[i, j].x),
                  end='')
    print()


 *******            Benders dual method starts        *******  

 **                Initial Solution             ** 
Obj(sub) : 20942.0 | 
 iter : 1   | Obj(master): 14628.0  | LB (iter 1): 14628.0  | Obj(sub) : 20918.0 | UB (iter 1): 35238.0  | eta = -0.0  |  Gap: 58.49 %   
 iter : 2   | Obj(master): 14731.0  | LB (iter 2): 14731.0  | Obj(sub) : 20912.0 | UB (iter 2): 35238.0  | eta = 0.0  |  Gap: 58.2 %   
 iter : 3   | Obj(master): 15063.0  | LB (iter 3): 15063.0  | Obj(sub) : 20888.0 | UB (iter 3): 35238.0  | eta = -0.0  |  Gap: 57.25 %   
 iter : 4   | Obj(master): 30532.0  | LB (iter 4): 30532.0  | Obj(sub) : 18789.999999999996 | UB (iter 4): 34556.0  | eta = 14766.0  |  Gap: 11.64 %   
 iter : 5   | Obj(master): 30938.0  | LB (iter 5): 30938.0  | Obj(sub) : 18787.0 | UB (iter 5): 34556.0  | eta = 14774.0  |  Gap: 10.47 %   
 iter : 6   | Obj(master): 30949.0  | LB (iter 6): 30949.0  | Obj(sub) : 18788.0 | UB (iter 6): 34556.0  | eta = 14764.0  |  Gap: 10.44 %   
 iter : 7   | Obj(master): 31355.0  | LB (iter 7): 31355.0  | Obj(sub) : 18785.0 | UB (iter 7): 34556.0  | eta = 14772.0  |  Gap: 9.26 %   
 iter : 8   | Obj(master): 33128.0  | LB (iter 8): 33128.0  | Obj(sub) : 18246.0 | UB (iter 8): 33680.0  | eta = 17694.0  |  Gap: 1.64 %   
 iter : 9   | Obj(master): 33146.0  | LB (iter 9): 33146.0   | Obj(sub) : 18244.0 | UB (iter 9): 33680.0  | eta = 17692.0  |  Gap: 1.59 %   
 iter : 10   | Obj(master): 33563.0  | LB (iter 10): 33563.0  | Obj(sub) : 18242.0 | UB (iter 10): 33680.0  | eta = 17690.0  |  Gap: 0.35 %   
 iter : 11   | Obj(master): 33680.0  | LB (iter 11): 33680.0  | Obj(sub) : 18026.0 | UB (iter 11): 33680.0  | eta = 18026.0  |  Gap: 0.0 %   

Optimal solution found !
Opt_Obj : 33680.0
 **  Final Gap: 0.0 %  **  

  **    Solution  **  
 y_0: 1.0,   z_0: 256.0   actual demand: d_0: 206.0  perturbation in worst case: g_0: 0.0
 y_1: -0.0,   z_1: -0.0   actual demand: d_1: 314.0  perturbation in worst case: g_1: 1.0
 y_2: 1.0,   z_2: 516.0   actual demand: d_2: 252.0  perturbation in worst case: g_2: 0.7999999999999998

  **    Transportation solution  **  
trans: x_0_0: 4.0, cost : 88.0   trans: x_0_2: 251.99999999999997, cost : 6047.999999999999   

trans: x_2_0: 202.0, cost : 4040.0   trans: x_2_1: 314.0, cost : 7850.0   

Process finished with exit code 0

下面对CCG算法和Benders dual算法进行对比(来自文献[3]):

由于代码实现的不同,本推文的代码得到的迭代结果和文献[3]的迭代中间结果略有不同,但是最终都得到了最优解33680。可以发现,CCG算法仅需2步迭代就收敛到全局最优解,而Benders dual method则需要更多的迭代才能实现收敛,这也说明了CCG算法的优越性。

参考文献

  1. Geoffrion, A.M., 1972. Generalized Benders decomposition. J Optim Theory Appl 10, 237–260. https://doi.org/10.1007/BF00934810

  2. Rahmaniani, R., Ahmed, S., Crainic, T.G., Gendreau, M., Rei, W., 2020. The Benders Dual Decomposition Method. Operations Research 68, 878–895. https://doi.org/10.1287/opre.2019.1892

  3. Zeng, B., Zhao, L., 2013. Solving two-stage robust optimization problems using a column-and-constraint generation method. Operations Research Letters 41, 457–461. https://doi.org/10.1016/j.orl.2013.05.003

  4. 刘兴禄,熊望祺,臧永森,段宏达,曾文佳,陈伟坚. 运筹优化常用模型、算法及案例实战:Python+Java实现.清华大学出版社.

完整代码获取方式

为了保证推文的紧凑性,我们在推文中省去了非核心部分的代码。需要完整代码的小伙伴,可以通过下面的方式获取,谢谢大家的支持。

完整代码可以通过朋友圈集赞获得。转发该推文至朋友圈,集齐30赞,然后将集赞截图发送至『运小筹』公众号后台,并发送姓名+学校+电话+邮箱至公众号后台,即可免费获得完整的代码。


微信公众号后台回复

加群:加入全球华人OR|AI|DS社区硕博微信学术群

资料:免费获得大量运筹学相关学习资料

人才库:加入运筹精英人才库,获得独家职位推荐

电子书:免费获取平台小编独家创作的优化理论、运筹实践和数据科学电子书,持续更新中ing...

加入我们:加入「运筹OR帷幄」,参与内容创作平台运营

知识星球:加入「运筹OR帷幄」数据算法社区,免费参与每周「领读计划」、「行业inTalk」、「OR会客厅」等直播活动,与数百位签约大V进行在线交流



                    


        




文章须知

文章作者:刘兴禄,江波

责任编辑:Road Rash

微信编辑:疑疑

文章转载自『运小筹』公众号,原文链接:优化算法 | Benders Dual Decomposition:理论、案例及实现 (Python+Gurobi)





关注我们 

       FOLLOW US


































Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/170514
 
206 次点击