[Package] Genie.jlを使ってJuMPの計算をAPI化する実験 | Knapsack問題を例にして
最近REST API (RESTful API)がいろんな場所で使われています.この記事ではJuliaのGenie.jlパッケージを使ってREST API (とても簡単なAPI Backendです) を作成し,内部でJuMPを使って実装したKnapsack問題を解いて返すシステムを構築してみます.
Knapsack問題をJuMPで解く
Knapsack問題についてはしっかり学ぶ数理最適化などを読んでください (手抜き).この記事で扱う問題は以下の程度の規模のものです (JuMPに投げるため,適当な制約を作っておきます).とはいえ,この制約を心の中のもので実装では使いませんでした.
- 容量
W \leq 20 - 個数
N \leq 10 - 荷物
の価値i ,重量1\leq p_i\leq 10 1\leq w_i\leq 20
まずは例題をJuMPで解いてみることにします.JuMPについては拙い過去記事を参考にして頂いても良いです: JuMPで数理最適化を楽しむ.
例題
-
,W=15 ,N=5 ,\mathbf{p}=[4, 2, 2, 1, 10] \mathbf{w}=[12, 2, 1, 1, 4] - 実装です
using JuMP
using Cbc
# instance
W = 15;
N = 5;
p = [4, 2, 2, 1, 10];
w = [12, 2, 1, 1, 4];
# model
m = Model(with_optimizer(Cbc.Optimizer))
@variable(m, x[1:N], Bin)
@objective(m, Max, sum(p[i] * x[i] for i in 1:N))
@constraint(m, sum(w[i] * x[i] for i in 1:N) <= W)
# solve
optimize!(m)
println("ans=$(objective_value(m))") # 解は15
println("x=$(value.(x))") # 解は[0, 1, 1, 1, 1],1つ目以外を選び,2+2+1+10=15,重さ2+1+1+4=8
REST APIを作成してJuMPと接続する
Simple API Backend
動作のイメージとしては,上のインスタンスに必要なデータをJSONでAPIに送り込み,結果を得る流れをイメージしています.まずは実装に入る前に,Genie.jlの最も簡単な例チュートリアルからやってみます.実装のソースです.
using Genie
import Genie.Router: route
import Genie.Renderer.Json: json
Genie.config.run_as_server = true
route("/") do
(:message => "Hi there!") |> json
end
Genie.startup()
こちらのソースtest.jlをターミナルで起動した状態で,curl -X GET 127.0.0.1:8000 を実行すると,JSONの形で {"message":"Hi there!"} を受け取ることができます.ターミナルをキャプチャしたものが下の図です.
簡単なAPI Backendと入力データの受け取り
Simple API Backendの例ではGETでクエリを投げていましたが,ここからはPOSTで投げます.まずは curl -X ... の部分ですが,世の中の情報に従ってJuliaのHTTP.jlを使って投げることにします (本当はせっかくAPI化した (つもりな) ので,別の手法で投げればいいのですが).最初の段階では余計なデータが飛んでくることはとりあえず目をつぶり,上で使った例題のデータが飛んでくることとします.
using HTTP
HTTP.request(
"POST",
"http://127.0.0.1:8000/solve",
[("Content-Type", "application/json")],
"""{"N": 5, "W":15, "p": [4, 2, 2, 1, 10], "w": [12, 2, 1, 1, 4]}"""
)
受け取る側は,まずはデータが受け取れることを次のコードで確認しておきます.
using Genie, Genie.Router, Genie.Renderer.Json, Genie.Requests
Genie.config.run_as_server = true
route("/") do
(:message => "please POST knapsack problem instance as it is") |> json
end
route("/solve", method=POST) do
message = jsonpayload()
@show message
println(keys(message))
(:echo => "!!!!") |> json
end
Genie.startup()
ここまで来ると,次のようにデータが辞書型で取れるようになりました (POSTした側には!!!!のメッセージだけが戻ります). ちなみに以下の図のデータですが,jsonを間違えてWをwにタイポしてメチャクチャなものになってしまいました (上のKnapsackの例題に合わせてください).
最後にJuMPの実装も入れて,結果を整形して出力しましょう.解はobjとresという形で返し,JSONにパイプすることにしました.
using Genie, Genie.Router, Genie.Renderer.Json, Genie.Requests
using JuMP, Cbc
function solve_knapsack(N, W, p, w)
# model
m = Model(with_optimizer(Cbc.Optimizer))
@variable(m, x[1:N], Bin)
@objective(m, Max, sum(p[i] * x[i] for i in 1:N))
@constraint(m, sum(w[i] * x[i] for i in 1:N) <= W)
# solve
optimize!(m)
# return
obj = objective_value(m)
res_x = value.(x)
res = [i for i in 1:N if res_x[i] > 0.9]
return (obj, res)
end
Genie.config.run_as_server = true
route("/") do
(:message => "please POST knapsack problem instance as it is") |> json
end
route("/solve", method=POST) do
# data (instance)
message = jsonpayload()
N = message["N"]
W = message["W"]
p = message["p"]
w = message["w"]
obj, res = solve_knapsack(N, W, p, w)
(:obj => obj, :res => res) |> json
end
Genie.startup()
動作確認をします.サーバー側ではCbcソルバーのログとアクセスのログが,HTTP.jlでPOSTを投げた側は解が返ってきました.ちなみにJuMPについては,1回目の計算が遅いことが知られているので,2回目のqueryを投げるときは1回目より高速に解が返ってくる挙動を示します.
サーバ側 (Genie.jl) | クライアント側 (HTTP.jl) |
---|---|
まとめ
動いたのですが,待ち時間 (計算時間) を考えると,REST APIにしてもイマイチな気がしました (書いてから気づきました).POSTで処理を投げると問題を登録できてIDを返す,登録された計算が終わっていて,IDが与えられたら結果を返す,みたいな構成が良かったかもしれません (Genie.jlで書けると思いますけどね).
参考
最後に参考にしたページの一覧です.
-
https://qiita.com/buntafujikawa/items/758425773b2239feb9a7
- 最初はcurlを使おうとしました (jsonpayloadでうまくパースできずに投げました)
-
https://qiita.com/Haaamaaaaa/items/54bdb372d0e58a976a55
- Pythonも同じです
-
https://genieframework.github.io/Genie.jl/dev/tutorials/7--Using_JSON_Payloads.html
- Genie.jlのページです
-
https://qiita.com/NagaokaKenichi/items/0647c30ef596cedf4bf2
- REST APIとRESTful APIについて見ました
-
https://genieframework.github.io/Genie.jl/dev/API/genie.html
- Genie.jlのAPIです,読みました
Discussion