Python PuLP 数理最適化

これまで、pulpによる最適化計算をしたことがなかったので、基本的な使い方を参考記事に沿って実行してみます。

以下が参考にさせていただいた記事になります。とてもわかりやすいです。

github

  • githubのjupyter notebook形式のファイルはこちら

google colaboratory

  • google colaboratory で実行する場合はこちら

筆者の環境

!sw_vers
ProductName:	Mac OS X
ProductVersion:	10.14.6
BuildVersion:	18G6020
!python -V
Python 3.7.3

1. 線形最適化問題(線形計画問題)

高校数学でやった記憶があります。

最大化

$$ x + y $$

制約条件

$$ 2x + y \leq 2 \\ x + 2y \leq 2 \\ x \geq 0 \\ y \geq 0 $$

import pulp
import sys

# 最適化(最大化)を宣言
prob = pulp.LpProblem('test', pulp.LpMaximize)

# 変数の宣言
x = pulp.LpVariable("xx", 0, sys.maxsize, pulp.LpContinuous)
y = pulp.LpVariable("yy", 0, sys.maxsize, pulp.LpContinuous)
# 目的関数を宣言
prob += ( x + y, "Objective" )

# 制約条件を宣言
prob += ( 2 * x + y <= 2 , "Constraint_1" )
prob += ( x + 2 * y <= 2 , "Constraint_2" )
prob
test:
MAXIMIZE
1*xx + 1*yy + 0
SUBJECT TO
Constraint_1: 2 xx + yy <= 2

Constraint_2: xx + 2 yy <= 2

VARIABLES
xx <= 9.22337203685e+18 Continuous
yy <= 9.22337203685e+18 Continuous
result = prob.solve()
print("計算結果")
print("*" * 8)
print(f"最適性 = {pulp.LpStatus[result]}")
print(f"目的関数値 = {pulp.value(prob.objective)}")
print(f"解 x = {pulp.value(x)}")
print(f"  y = {pulp.value(y)}")
print("*" * 8)
計算結果
********
最適性 = Optimal
目的関数値 = 1.33333334
解 x = 0.66666667
  y = 0.66666667
********

2. 整数最適化問題(整数計画問題)

最小化

$$ \sum_{i \in I} \sum_{j \in J} c_{ij}x_{ij} $$

制約条件

$$ \sum_{j\in J}x_{ij} \leq 1 \\ \sum_{i\in I}x_{ij} = 1 \\ x_{ij} \in {0,1} $$

import pulp
import time

# 作業員の集合(便宜上、リストを用いる)
I = ["Aさん", "Bさん", "Cさん"]

print(f"作業員の集合 I = {I}")

# タスクの集合(便宜上、リストを用いる)
J = ["仕事イ", "仕事ロ", "仕事ハ"]

print(f"タスクの集合 J = {J}")

# 作業員 i を タスク j に割り当てたときのコストの集合(一時的なリスト)
cc = [
    [ 1,  2,  3],
    [ 4,  6,  8],
    [10, 13, 16],
   ]

# cc はリストであり、添え字が数値なので、
# 辞書 c を定義し、例えばcc[0][0] は c["Aさん","仕事イ"] でアクセスできるようにする
c = {} # 空の辞書
for i in I:
  for j in J:
    c[i,j] = cc[I.index(i)][J.index(j)]

print("コスト c[i,j]: ")
for i in I:
  for j in J:
    print(f"c[{i},{j}] = {c[i,j]:2d},  ", end = "")
  print("")
print("")
作業員の集合 I = ['Aさん', 'Bさん', 'Cさん']
タスクの集合 J = ['仕事イ', '仕事ロ', '仕事ハ']
コスト c[i,j]:
c[Aさん,仕事イ] =  1,  c[Aさん,仕事ロ] =  2,  c[Aさん,仕事ハ] =  3,
c[Bさん,仕事イ] =  4,  c[Bさん,仕事ロ] =  6,  c[Bさん,仕事ハ] =  8,
c[Cさん,仕事イ] = 10,  c[Cさん,仕事ロ] = 13,  c[Cさん,仕事ハ] = 16,
# 数理最適化を宣言
# pulp.LpMinimize => 最小化
# pulp.LpMaximize => 最大化

prob = pulp.LpProblem('prob', pulp.LpMinimize)
# 変数集合を表す辞書
x = {} # 空の辞書
     # x[i,j] または x[(i,j)] で、(i,j) というタプルをキーにしてバリューを読み書き

# 0-1変数を宣言
for i in I:
  for j in J:
    x[i,j] = pulp.LpVariable(f"x({i},{j})", 0, 1, pulp.LpInteger)
    # 変数ラベルに '[' や ']' や '-' を入れても、なぜか '_' に変わる…?
# lowBound, upBound を指定しないと、それぞれ -無限大, +無限大 になる

# 内包表記も使える
# x_suffixes = [(i,j) for i in I for j in J]
# x = pulp.LpVariable.dicts("x", x_suffixes, cat = pulp.LpBinary)

# pulp.LpContinuous : 連続変数
# pulp.LpInteger  : 整数変数
# pulp.LpBinary   : 0-1変数
# 目的関数を宣言
prob += pulp.lpSum(c[i,j] * x[i,j] for i in I for j in J), "TotalCost"
# problem += sum(c[i,j] * x[i,j] for i in I for j in J)
# 制約条件の宣言
for i in I:
  prob += sum(x[i,j] for j in J) <= 1, f"Constraint_leq_{i}"
  # 制約条件ラベルに '[' や ']' や '-' を入れても、なぜか '_' に変わる…?

# 各タスク j について、割り当てられる作業員数はちょうど1人
for j in J:
  prob += sum(x[i,j] for i in I) == 1, f"Constraint_eq_{j}"
# 問題の式全部を表示
print("問題の式")
print(f"-" * 8)
print(prob)
print(f"-" * 8)
print("")
問題の式
--------
prob:
MINIMIZE
1*x(Aさん,仕事イ) + 3*x(Aさん,仕事ハ) + 2*x(Aさん,仕事ロ) + 4*x(Bさん,仕事イ) + 8*x(Bさん,仕事ハ) + 6*x(Bさん,仕事ロ) + 10*x(Cさん,仕事イ) + 16*x(Cさん,仕事ハ) + 13*x(Cさん,仕事ロ) + 0
SUBJECT TO
Constraint_leq_Aさん: x(Aさん,仕事イ) + x(Aさん,仕事ハ) + x(Aさん,仕事ロ) <= 1

Constraint_leq_Bさん: x(Bさん,仕事イ) + x(Bさん,仕事ハ) + x(Bさん,仕事ロ) <= 1

Constraint_leq_Cさん: x(Cさん,仕事イ) + x(Cさん,仕事ハ) + x(Cさん,仕事ロ) <= 1

Constraint_eq_仕事イ: x(Aさん,仕事イ) + x(Bさん,仕事イ) + x(Cさん,仕事イ) = 1

Constraint_eq_仕事ロ: x(Aさん,仕事ロ) + x(Bさん,仕事ロ) + x(Cさん,仕事ロ) = 1

Constraint_eq_仕事ハ: x(Aさん,仕事ハ) + x(Bさん,仕事ハ) + x(Cさん,仕事ハ) = 1

VARIABLES
0 <= x(Aさん,仕事イ) <= 1 Integer
0 <= x(Aさん,仕事ハ) <= 1 Integer
0 <= x(Aさん,仕事ロ) <= 1 Integer
0 <= x(Bさん,仕事イ) <= 1 Integer
0 <= x(Bさん,仕事ハ) <= 1 Integer
0 <= x(Bさん,仕事ロ) <= 1 Integer
0 <= x(Cさん,仕事イ) <= 1 Integer
0 <= x(Cさん,仕事ハ) <= 1 Integer
0 <= x(Cさん,仕事ロ) <= 1 Integer

--------
# 計算
# ソルバー指定
solver = pulp.PULP_CBC_CMD()
# pulp.PULP_CBC_CMD() : PuLP付属のCoin-CBC
# pulp.GUROBI_CMD()   : Gurobiをコマンドラインから起動 (.lpファイルを一時生成)
# pulp.GUROBI()   : Gurobiをライブラリーから起動 (ライブラリーの場所指定が必要)

# 時間計測開始
time_start = time.perf_counter()

result_status = prob.solve(solver)
# solve()の()内でソルバーを指定できる
# 何も指定しない場合は pulp.PULP_CBC_CMD()

# 時間計測終了
time_stop = time.perf_counter()
# (解が得られていれば)目的関数値や解を表示
print("計算結果")
print(f"*" * 8)
print(f"最適性 = {pulp.LpStatus[result_status]}, ", end="")
print(f"目的関数値 = {pulp.value(prob.objective)}, ", end="")
print(f"計算時間 = {time_stop - time_start:.3f} (秒)")
print("解 x[i,j]: ")
for i in I:
  for j in J:
    print(f"{x[i,j].name} = {x[i,j].value()},  ", end="")
  print("")
print(f"*" * 8)
計算結果
********
最適性 = Optimal, 目的関数値 = 19.0, 計算時間 = 0.028 (秒)
解 x[i,j]:
x(Aさん,仕事イ) = 0.0,  x(Aさん,仕事ロ) = 0.0,  x(Aさん,仕事ハ) = 1.0,
x(Bさん,仕事イ) = 0.0,  x(Bさん,仕事ロ) = 1.0,  x(Bさん,仕事ハ) = 0.0,
x(Cさん,仕事イ) = 1.0,  x(Cさん,仕事ロ) = 0.0,  x(Cさん,仕事ハ) = 0.0,
********

1行1行、とても勉強になりました。ぜひリンク先に飛んでオリジナルの記事で勉強してみてください。