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行、とても勉強になりました。ぜひリンク先に飛んでオリジナルの記事で勉強してみてください。