Python + PuLP でたくさん輸送問題
あれでもいいのですが、あのコードでは一回解いたきりで終わってしまいます。
ただの趣味ならいいけれど、最適化を外部から呼び出せるものとして実装する必要が(普通は)あるでしょう。そして、ランダムでインスタンス(問題例)が作れるタイプの問題であれば、ガンガンインスタンスを作ってガンガン解くようなテスターを作っておくと色々と便利です。定式化や解法の違いによる計算時間の比較や問題サイズに対する計算量の増加も見やすくなります。
私は、カプセル化のようなことをして、モデルオブジェクトと変数オブジェクトとその他必要なデータとか出力用の関数とかを詰め込んだラッパ (wrapperです、念のため) のクラスをよく作ります。このとき,同じファイルのメイン関数で1個だけインスタンス(問題例)を解くような簡易テスト付き.pyファイルに仕立ててあげるとデバッグも楽になります。ランダムなインスタンスを作って解くテスターは別のpyファイルとかJupyter Notebookに書き、ラッパをimportして使うとすっきりします。
プログラミングの腕も開発経験もない私のやり方なので、他のやり方もあると思います。ひとつの参考程度になさってください。
輸送問題のラッパ
実行すると簡単な問題例を解いて結果を表示します。
""" transportation.py Local Optima 2020. """ import itertools as itrt import pulp EPS = 1e-4 def _main(): capacity = dict(enumerate([6,10])) demand = dict(enumerate([4, 6, 3, 3])) transcost = {(0, 0): 3, (0, 1): 7, (0, 2): 11, (0, 3): 8, (1, 0): 6, (1, 1): 7, (1, 2): 8, (1, 3): 9} tp = Transportation(capacity, demand, transcost) tp.solve() tp.print_solution() class Transportation(): def __init__(self, capacity, demand, transcost): """ Transportation Problem Interface Parameters----- capacity: dict of factory capacity demand: dict of demand qty from customers transcost: dict of transportation cost from i to j whose keys are tuple (i,j) """ self.capacity = capacity self.demand = demand self.transcost = transcost def solve(self): """ Solve transportation problem """ model, x = self._formulate() model.solve() print("Transportation Problem Solved --", pulp.LpStatus[model.status]) def _formulate(self): model = pulp.LpProblem("Transportation", pulp.LpMinimize) # 変数オブジェクト x = {(i,j): pulp.LpVariable("x{}-{}".format(i,j), lowBound=0, upBound=self.demand[j]) for i,j in self.transcost} # 目的関数 model += pulp.lpSum([self.transcost[i,j] * x[i,j] for i,j in self.transcost]), "Objective" # 制約条件 # 工場の出荷上限 for i, Ci in self.capacity.items(): model += pulp.lpSum([x[i,j] for j in self.demand.keys()]) <= Ci, "Capacity{}".format(i) # 顧客の需要 for j, dj in self.demand.items(): model += pulp.lpSum([x[i,j] for i in self.capacity.keys()]) == dj, "demand{}".format(j) self.model = model self.x = x return model, x def print_solution(self): """ Print Solution if possible """ if self.model.status != 1: print("Not Ready to print.") return print("z =", pulp.value(self.model.objective)) for var in self.model.variables(): if var.varValue > EPS: print(var, var.varValue) if __name__ == "__main__": _main()
テスター
問題例をたくさん生成して解きまくるテスターは,たとえば以下のようになります。
さきほどのtransportation.pyをfrom transportation import *
して使うのがミソです。
import math import random import itertools as itrt from transportation import * def distance(pos1, pos2): return sum([(x1-x2)**2 for x1,x2 in zip(pos1, pos2)])**0.5 def random_transportation(factory, store, mapsize=10): """ Returns Transportation problem wrapper Parameters----- factory: number of factories store: number of stores mapsize (optional): """ capacity = dict(enumerate([random.randint(3,15) for _ in range(factory)])) demand = dict(enumerate([random.randint(1,10) for _ in range(store)])) posi = {n:[random.uniform(0,mapsize), random.uniform(0,10)] for n in range(factory)} posj = {n:[random.uniform(0,mapsize), random.uniform(0,10)] for n in range(store)} dist = {} for i,j in itrt.product(posi, posj): dist[i,j] = math.ceil(distance(posi[i], posj[j])) return Transportation(capacity, demand, dist) for _ in range(30): tp = random_transportation(4,6, mapsize=20) tp.solve()
実行結果です。
Transportation Problem Solved -- Optimal Transportation Problem Solved -- Infeasible Transportation Problem Solved -- Optimal Transportation Problem Solved -- Optimal Transportation Problem Solved -- Optimal (以下省略)
capacity
とdemand
をランダムに作っているので,工場の総供給能力が需要の総合計に負けてしまう場合は実行不可能(Infeasible)になります。
補遺
輸送問題は,実は面白い性質をもった線形計画問題です。
各工場の供給能力と,各店舗の需要量を整数値にしておくと,なんと連続変数なのに最適解が整数になるのです。気になる人は上のテスターをいじくって試してみてください。
これはたまたまではないことが知られています。
輸送問題を の形に書いたときの行列 のことを制約行列 といいますが,これが完全単模 (totally-unimodular) であるとき,連続LPを単体法で解いたときの最適解が整数になるそうです。
Python + PuLPでナップサック問題
3回目は整数計画問題ということでナップサック問題を扱います。Knapsack ProblemちぢめてKPというのがツウです。ナップ「ザ」ックと言わないのもツウです。
ナップサック問題
コソドロのあなたは、お店で盗難を計画しています(なんと物騒な)。
闇市場での売値が,重量 (kg)の品物を何個かずつ持って、C(kg)までなら持って逃げられそうです。何を何個ずつ盗むのがよいでしょうか。(制限に収まる限りは何個でも持ってよいのが整数KPです。)
(ちなみに、品物がどれも1点モノで、持ち出すとしても1個ずつの場合が0-1KPです。0-1KPのほうがお好きな方にはすみませんが、今回は整数計画の例題として整数KPを雑に定式化して雑に解きます。)
ナップサック問題の定式化
品物の集合を大文字の,品物iを盗む個数をとすると、KPは以下の最大化問題になります。
※ は0を含む自然数(つまり非負整数)です。
これだけですからPuLPでの定式化も簡単です。
import pulp from math import floor # data value = [1,3,2,1,3,4] weight = [2,3,4,1,4,3] itemsize = 6 capacity = 10 # モデルオブジェクト model = pulp.LpProblem("IntKP", pulp.LpMaximize) # 変数オブジェクト x = [ pulp.LpVariable("item{}".format(i), lowBound=0, upBound=floor(capacity/weight[i]), cat=pulp.LpInteger) for i in range(itemsize) ]
LpVariableの引数で cat=pulp.LpInteger
とすると整数変数に指定できます。
cat=pulp.LpBinary
で0-1変数を指定できます。そうすると0-1KPになります。書き換えて試してみてください。
ここでも,輸送問題のときと同様,自明な上界 があるので一応指定してあげます。(i番目の品物だけを詰めることを考えると,キャパシティを超えない上限個数がある)
# 目的関数 model += pulp.lpSum([value[i] * x[i] for i in range(itemsize)]), "Objective" # 制約条件 # ナップサックの重量制限 weightsum = pulp.lpSum([weight[i] * x[i] for i in range(itemsize)]) model += weightsum <= capacity, "Capacity" # 求解 model.solve() print("Optimal Value =", pulp.value(model.objective)) for i, xi in enumerate(x): print("Item{} (V={} W={})".format(i, value[i], weight[i]), pulp.value(xi)) print("Knapsack holds {} kg".format(pulp.value(weightsum)))
Optimal Value = 13.0 Item0 (V=1 W=2) 0.0 Item1 (V=3 W=3) 0.0 Item2 (V=2 W=4) 0.0 Item3 (V=1 W=1) 1.0 Item4 (V=3 W=4) 0.0 Item5 (V=4 W=3) 3.0 Knapsack holds 10.0 kg
まとめ
LpVariableの引数で cat=pulp.LpInteger
とすると整数変数。
cat=pulp.LpBinary
で0-1変数。
今日はこれだけの話でした。
補遺
Google OR-tools には KPソルバーが入っています。整数計画問題としなくても解くことができます。
https://developers.google.com/optimization/introduction/overview?hl=ja
Python + PuLPで輸送問題
PuLPで最適化シリーズ第2回は、輸送問題を解いてみます。工場から店舗への供給を最適化する、ロジスティクスの基本問題です。
モデリングの復習でもありますが,リストや辞書を使ったPythonらしい書き方でモデリングをしていきます。
輸送問題の定義
m個の工場から,n個の店舗に物資を輸送します。工場の供給上限、店舗jの需要量を満たしつつ、輸送にかかる総費用が最小となるような、工場iから店舗jへの出荷量を求めよ。工場iから店舗jへの単位輸送費用はである。
輸送問題では簡単のため,総輸送費用を
とします。
(Pythonのリストを使ってこのあとの処理をする都合上、添字をゼロスタートで考えてます。)
このとき輸送問題は次の最小化の線形計画問題になります。
目的関数は総費用の最小化。
制約条件の1つ目は工場の出荷量合計が供給上限を超えない制約,2つ目は店舗の入荷量合計が需要と一致する制約です。
例題
Pythonのリストで例題のデータを作ります。の例を見てみましょう。
その前に,Python + PuLPで最適化するときの段取りも思い出してくださいね。
- インポートする (from pulp import *)
- モデルオブジェクトを宣言する
- 変数オブジェクトを宣言する
- 目的関数をセットする
- 制約条件をセットする
- 最適化する
- 結果を見る
from pulp import * # 工場のリスト(中身は供給上限) capacity = [6, 10] # 店舗のリスト(中身は需要量) demand = [4, 6, 3, 3] # 単位費用の行列 c_ij cost = {(0, 0): 3, (0, 1): 7, (0, 2): 11, (0, 3): 8, (1, 0): 6, (1, 1): 7, (1, 2): 8, (1, 3): 9 }
工場と店舗のリストはこういう風にリストで与えるといいです。
単位費用の行列は,二重リストにすることもできますが,私のオススメはタプルをキーとする辞書(dict)にする方法です。添字のたくさんつくものは辞書にせよ。もし今後みたいなものがモデルに現れても,cost[i,j,k]
で参照できるようなdictにするとよいです。
なぜか。Zen of Pythonにも書いてあるとおり,"Flat is better than nested." だからです。でも,どうしても多重リストが好きな人は多重リストにしてください。
次は,モデルオブジェクトと変数オブジェクトの宣言です。変数も2添字の行列の形になりますので,for文の中で回すことになります。
model = LpProblem("Transportation", LpMinimize) # 変数オブジェクト x = {} for i,j in cost: x[i,j] = LpVariable("x{}-{}".format(i,j), lowBound=0, upBound=demand[j])
工場iから店舗jへの出荷量は,にデカくなることはなく,高々,店舗jの需要量までなのは間違いないので,upBound=demand[j]
を入れてあげますが,敢えて入れなくても問題ありません。
リスト内包表記に強い人は,変数のところを
x = {(i,j): LpVariable("x{}-{}".format(i,j), lowBound=0, upBound=demand[j]) for i,j in cost}
と1行にしてもかまいません。
目的関数と制約条件の設定です。
和の形になっているものは,ベタ書きしないでも,LpSum
という便利な関数があります。
lpSum([リスト内包表記])
の形で使ってあげます。
# 目的関数 model += lpSum([cost[i,j] * x[i,j] for i,j in cost]), "Objective" # 制約条件 # 工場の出荷上限 for i, Ci in enumerate(capacity): model += lpSum([x[i,j] for j in range(len(demand))]) <= Ci, "Capacity{}".format(i) # 顧客の需要 for j, dj in enumerate(demand): model += lpSum([x[i,j] for i in range(len(capacity))]) == dj, "demand{}".format(j)
あとは最適化して結果を見てみましょう。
# 求解 model.solve() # 結果の確認 print("Optimal Value =", value(model.objective)) for var in x.values(): # 誤差以上の値を持っている変数だけprint if var.varValue > 1e-4: print(var, var.varValue)
ソルバーの挙動として,浮動小数点に誤差が乗って解となる場合があります。そのため,値1e-4
以上の変数だけ表示してあげています。
Optimal Value = 103.0 x0_0 4.0 x0_3 2.0 x1_1 6.0 x1_2 3.0 x1_3 1.0
こんな結果でした。店舗3には2つの工場から出荷することになっていますね。
データの生成
距離行列は,適当に作るとウソくさいデータセットになるので,
- 10x10のマップにランダムに工場と店舗を配置し
- 2地点間のユークリッド距離を切り上げ
た距離行列にしてみました。以下のコードで作れます。
import math import random import itertools as itrt from pprint import pprint def distance(pos1, pos2): return sum([(x1-x2)**2 for x1,x2 in zip(pos1, pos2)])**0.5 posi = {n:[random.uniform(0,10), random.uniform(0,10)] for n in range(2)} posj = {n:[random.uniform(0,10), random.uniform(0,10)] for n in range(4)} dist = {} for i,j in itrt.product(posi, posj): dist[i,j] = math.ceil(distance(posi[i], posj[j])) pprint(dist)
itertoolsは私はas itrt
ってインポートしてるんですが,みなさんどうしてますか?よかったらコメント欄で教えて下さい。
Python + PuLPで線形計画
しばらく,線形計画でできる話をしていきます。
線形計画問題とは
線形「計画」とはいいますが Linear Programming の訳語です。リニアが線形(一次式)で,プログラミングはこういう文脈では「計算」くらいの意味合いです。どんな計算かといえば,
- 何本かの一次不等式で表される領域のなかで
- 一次式の目的関数の値を最大化 (or 最小化) するような変数の組を特定する
計算であり,要するに最大最小問題の一種です。ツウの人たちはLPっていいますね。
大学入試でも,2変数の線形計画は出題されることがありますね!
大学入試なんかだと,「最大値を求めよ」と出題され,そのときの変数の値はオマケ的だけど必ず答えにつけようねとお作法みたいに思われていますが,実用的には,「最適のときの変数の値がコレです!!」というのをちゃんと求めるのが超重要です。
ちゃんと書きますと,個の変数 に対して,本の制約条件(一次不等式)で表される領域
の中で,目的関数
を最大(または最小)とするのが線形計画問題です。
以上をまとめて,
のように書きます。
最大値と最小値の両方を求めなくていいの?と思う方もいるでしょう。が,実際に問題を組んでみると,最大値がだったり,ただの0だったりすることも多いので,意味のあるほうだけ求めるのが通常です。もちろん,必要があれば両方求めてOKです。
ちなみに,制約条件に等式制約を使うのもOKです。(スラック変数というものを持ち出すと不等式制約をすべて等式制約に変換できます。逆に,等式制約も不等式制約に変換できますが細かい話は割愛。どっちも使えます。)(鉄道方面から来た方,曲線に付けるカントとスラックのスラックと一緒です)
線形計画問題の例題を解く
今回は2変数の簡単な例題を,PuLPで解いてみましょう。
PuLPについて
PuLPは,Pythonで線形計画(と整数計画)に使えるモジュールです。セットアップについては他に詳しいサイトがたくさんありますので今回は割愛。
個人的には,Anacondaを使わずに,VS Code内で完結するようなセットアップが好みです。とりあえずはpipでjupyterとpulpを入れてやればいいと思います。
例題
さきほどの「高校数学の美しい物語」さんのページに載っている例題と一緒です。2変数なので図形的にも解け,最適解はでです。
PuLPの使い方
PuLPを使うときのおおまかな流れは
- インポートする (
from pulp import *
) - モデルオブジェクトを宣言する
- 変数オブジェクトを宣言する
- 目的関数をセットする
- 制約条件をセットする
- 最適化する
- 結果を見る
です。
手順1,2,3は,はじめは おまじない だと思っても結構です。
from pulp import * # モデルオブジェクトの宣言 model = LpProblem("sample", sense=LpMaximize) # 変数オブジェクトの宣言 x = LpVariable("x", lowBound=0) y = LpVariable("y", lowBound=0)
モデルオブジェクトで,sense=LpMaximize
は最大化問題であることを指定しています。デフォルトだと最小化問題になりますが,最大化問題のつもりで最小化問題を解いたりするポカがあるのでいつも書くようにしておくといいです。
変数オブジェクトで,lowBound=0
は変数の下界を0にします。(デフォルトです。)変数の上界がある場合はupBound=
で設定できますがやはりデフォルトはです。
手順4と5で目的関数と制約条件をセットします。同じ書き方をしますが順番が大事です。
# 最初に+=したのが目的関数と解釈される model += 4*x + 5*y, 'objective' # 2番目以降は制約条件と解釈される model += x + y <= 3, 'c1' model += x + 2*y <= 4, 'c2'
objective
, c1
, c2
は式につけた名前です。
ここで一度model
の中身をprint
してみたりなんかすると,中身が見えます。
sample: MAXIMIZE 4*x + 5*y + 0 SUBJECT TO c1: x + y <= 3 c2: x + 2 y <= 4 VARIABLES x Continuous y Continuous
この問題で合っていれば,手順6 最適化してあげます。
model.solve() print("Status:", model.status, LpStatus[model.status])
.solve()
したあと,status
属性を確認してあげて,最適に解けたかどうか確かめます。model.status == 1
なら最適解が見つかったという意味です。LpStatus
は辞書なのでキーにstatusを入れてあげると意味の取れる文字列で見せてくれます。
Status: 1 Optimal
が表示されたのでOKでした。最適値があるはずなのに Infeasible (実行不可能) と言われたり Unbounded (無限解) と言われたら,たぶん定式化(目的関数と制約条件の設定のこと)でミスっています。
手順7 最大値とそのときの各変数の値を確認してあげます。
print("z =", value(model.objective)) for var in model.variables(): print(var, var.varValue)
value()
関数を噛ませたり,varValue
属性を読んだりするのがややこしく感じるかもしれません。変数オブジェクトはいつものfloatオブジェクトとはモノが異なるのでナマで参照しても値のようには振る舞いません。
z = 13.0 x 2.0 y 1.0
と,図形的に解いたのと同じ答えが求められました。
コードの全文をまとめると以下の通り。
from pulp import * # モデルオブジェクトの宣言 model = LpProblem("sample", sense=LpMaximize) # 変数オブジェクトの宣言 x = LpVariable("x", lowBound=0) y = LpVariable("y", lowBound=0) # 最初に+=したのが目的関数と解釈される model += 4*x + 5*y, 'objective' # 2番目以降は制約条件と解釈される model += x + y <= 3, 'c1' model += x + 2*y <= 4, 'c2' # 求解とその前後処理 print(model) model.solve() print("Status:", model.status, LpStatus[model.status]) # 結果の確認 print("z =", value(model.objective)) for var in model.variables(): print(var, var.varValue)
線形計画とは結局なんなのか
線形計画問題 (LP) がもてはやされるのはなぜか。
- 変数のLPでも高速に解けるアルゴリズムが存在する。
- 工学的な応用範囲が広い。
もちろん,「残念ながら,世の中は非線形でできている」のはみんな知っているのですが,線形で表せる工学的(工業的)な営みや,近似して線形だと思えればそれでいいだろうというテーマがたくさんあります。組合せ的な問題も,なかにはLPになったりするものもあるのです。
LPと同じ形で、変数が整数値しか取れない場合のものを整数計画問題といい、これもPuLP (に同梱のCBCソルバー) で解くことができます。 ナップサック問題のような組合せ最適化問題が整数計画に帰着されます。k-means、k-centerのようなクラスタリングや、みんな大好きTSPも整数計画になります。(ところが整数計画シリーズは、「整数計画問題にできても、現実的な時間で最適解を求められない」ことがあります。)
Python + PuLP での最適化は、並木誠先生の本があり、理論の説明から入っていけますがミスプリが多いというAmazonのレビューもあります…。
正誤表とサンプルコードは出版社のHPでゲットできます。
補遺
フリーで使えるソルバーということでPuLP(に同梱のCBCソルバー)を当分使っていきますが,私は実はGurobi使いです(いまのところ)。pythonAPIの出来が良く、問題の記述がPuLPより簡単で柔軟で分かりやすいだけでなく,求解の(速度的な)性能も滅茶苦茶に強いです。アカデミアにいる人はアカデミックライセンスがもらえるので公式サイトを訪問してください。
python API の使い勝手でいうとSCIPもGurobiとかなり似てて使いやすそうです。が,ライセンスがハッキリしません(アカデミアは無料,商用は連絡してね(有料?),なんだけど個人の趣味用途は??)。
CBCはその名を Coin Branch and Cut Solver というだけあって,本当は分枝カット法に基づく高度な解法を実装できるんじゃないかと思うんですが,残念ながらPuLPはそういうところまで踏み込めないみたいです。C++が使える人はゴリゴリにCBCを使い倒してみてもいいんじゃないでしょうか。pythonで比較的深いところまでCBCを叩けるcbcpy
なるモジュールもあるらしいので,このブログでもそのうちやってみたいことの一つです。
Googleのor-toolsも無料にしては優秀なんじゃないかと思っていますが,Python APIがC++のコードから自動生成したもののようでエラく書きにくいので,今回はパス。が,便利そうなツールなのでこれもそのうちやってみたいですね。(ただの混合整数計画を組んでしまうと結局回るのはCBCソルバーだそうです。)こちらもC++が読み書きできる方にはよいかもしれません。
Local Optima はじめました
最適化とかに関わることを書いてみたくなったので,ブログ立ち上げました。
Pythonを使っていきます。大学生・大学院生や最適化を実務に取り入れたい皆さんの役に立てば幸いです。
オペレーションズ・リサーチ(など)を勉強していました。実用を考えたときに日本語ベースのいい読み物がそんなに多くないなーと思っていました。
Local Optimaは,「局所最適解」の意味です。なんかカッコイイからですw
最適化ガチ勢の皆さんには知識でも腕前でも勝てませんので,おかしなところのご指摘、よりよいプラクティスのご指導があればコメントでいただけますと幸いです。