サブロウ丸

Sabrou-mal サブロウ丸

主にプログラミングと数学

Benders DecompositionのGurobipy実装例

Benders Decomposition(ベンダー分解)は問題を二つのmain, sub問題に分割することで問題を解きやすくするテクニックです。次のような性質がある問題に有効です。Kohji Nishimuraさんの「双対変数を直感的に理解したい」から引用しています。

決定変数にyが一部含まれているが、もしこのyがなかったら解くのが簡単な問題なのになぁ、といったシチュエーションは数理最適化にて頻繁に起こる。例えば

  • yがなければ残った問題が複数の小さな問題に分解できる
  • yが離散変数 (バイナリ変数等)であるため、yさえ固定できてしまえば残りの問題が線形計画問題として解ける

双対変数はややこしいイメージがありましたが、上記の記事で分かりやすく説明されており、理解が深まりました。 今回は簡単な問題に対して、Benders DecompositionをGurobipyを用いて実装したという記事です。 Benders Decompositionについては次のsnowberryfieldさんの記事「Benders分解法」が詳しいです。

テスト問題

下記のサイトの問題を用いました。そのまま解くと最適な目的関数値は2。

https://homepages.rpi.edu/~mitchj/matp6620/benders_eg/benders_eg_slides.html

from gurobipy import Model, GRB

# Create a new model
m = Model("mip_problem")

# Create variables
x = m.addVars(4, vtype=GRB.CONTINUOUS, name="x")
y = m.addVars(2, vtype=GRB.BINARY, name="y")

# Set objective
m.setObjective(8*x[0] + 9*x[1] + 5*x[2] + 6*x[3] - 15*y[0] - 10*y[1], GRB.MAXIMIZE)

# Add constraints
m.addConstr(x[0] + x[2] <= 1)
m.addConstr(x[0] + x[3] <= 1)
m.addConstr(x[1] + x[2] <= 1)
m.addConstr(x[1] + x[3] <= 1)
m.addConstr(x[0] - y[0] <= 0)
m.addConstr(x[1] - y[0] <= 0)
m.addConstr(x[2] - y[1] <= 0)
m.addConstr(x[3] - y[1] <= 0)
m.addConstr(-y[0] - y[1] <= -1)

gurobipy実装

上記のブログに記載の手順とは異なりますが、行なっている処理は同じです(本稿の方が多少無駄が含まれます)。 テスト問題のバイナリ変数yがsubから分離する決定変数たちです。 上記のブログではsubで双対問題を解いていますが、下のコードでは対応する主問題を解いてその後双対変数の値を取得することで同様の制約をmain問題に追加しています。

main問題の初期化

import math
from gurobipy import Model, GRB

# Creat main problem
main = Model("main")
main_y = main.addVars(2, vtype=GRB.BINARY, name="y")
main_t = main.addVar(lb=-GRB.INFINITY, vtype=GRB.CONTINUOUS, name="t")
main.addConstr(- main_y[0] - main_y[1] <= -1)
main.setObjective(main_t)
main.Params.OutputFlag = 0 # silent/verbose mode

sub問題の初期化

# Create sub problem
sub = Model("sub")
x = sub.addVars(4, vtype=GRB.CONTINUOUS, name="x")
y = sub.addVars(2, lb=0, ub=1, vtype=GRB.CONTINUOUS, name="y")
sub.addConstr(x[0] + x[2] <= 1)
sub.addConstr(x[0] + x[3] <= 1)
sub.addConstr(x[1] + x[2] <= 1)
sub.addConstr(x[1] + x[3] <= 1)
sub.addConstr(x[0] - y[0] <= 0)
sub.addConstr(x[1] - y[0] <= 0)
sub.addConstr(x[2] - y[1] <= 0)
sub.addConstr(x[3] - y[1] <= 0)
d0 = sub.addConstr(0 <= 0)  # dummy
d1 = sub.addConstr(0 <= 0)  # dummy
sub.setObjective(-8*x[0] - 9*x[1] - 5*x[2] - 6*x[3] + 15*y[0] + 10*y[1])
sub.Params.OutputFlag = 0 # silent/verbose mode

Benders分解ループ

# Set initial solution randomly
opt_y = [0 for i in range(2)]
main_obj = float("-inf")

# Benders decomposition
index = 0
while True:
    # Update sub problem
    sub.remove([d0, d1])
    d0 = sub.addConstr(y[0] == opt_y[0])
    d1 = sub.addConstr(y[1] == opt_y[1])
    sub.update()

    # Solve sub problem
    sub.optimize()
    sub_obj = sub.ObjVal
    l = [d0.Pi, d1.Pi]  # get dual variable values

    # Output logging
    print(f"Round {index}")
    print(f"  opt_y    = {opt_y}")
    print(f"  main_obj = {main_obj}")
    print(f"  sub_obj  = {sub_obj}")
    print(f"  lambda   = {l}")
    
    # Check termination condition
    if math.isclose(main_obj, sub_obj, abs_tol=1e-6):
        break

    # Add constraint to the main problem
    c = main.addConstr(main_t >= sub_obj + quicksum(l[i] * (main_y[i] - opt_y[i]) for i in range(2)))
    main.update()
    print(f"  add {main_t >= sub_obj + quicksum(l[i] * (main_y[i] - opt_y[i]) for i in range(2))}")

    # Solve main problem
    main.optimize()
    main_obj = main.ObjVal
    opt_y = [main_y[i].X for i in range(2)]
    
    index += 1

print()
print("Result")
for v in sub.getVars():
    print(f'  {v.varName} = {v.x}')

以下が求解時のログ。最適化の方向を最小化にしているため符号が逆転していますが、オリジナルの問題を直接解いた場合の目的関数値2と一致していますね。

Round 0
  opt_y    = {0: 0, 1: 0}
  main_obj = -inf
  sub_obj  = 0.0
  lambda   = [-2.0, -1.0]
  add <gurobi.TempConstr: t >= -2.0 y[0] + -1.0 y[1]>
Round 1
  opt_y    = {0: 1.0, 1: 1.0}
  main_obj = -3.0
  sub_obj  = 8.0
  lambda   = [15.0, 10.0]
  add <gurobi.TempConstr: t >= -17.0 + 15.0 y[0] + 10.0 y[1]>
Round 2
  opt_y    = {0: 1.0, 1: 0.0}
  main_obj = -2.0
  sub_obj  = -2.0
  lambda   = [0.0, -1.0]
  add <gurobi.TempConstr: t >= -2.0 + 0.0 y[0] + -1.0 y[1]>

Result
  x[0] = 1.0
  x[1] = 1.0
  x[2] = 0.0
  x[3] = 0.0
  y[0] = 1.0
  y[1] = 0.0

コード

gist.github.com