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++が読み書きできる方にはよいかもしれません。