🕸️

[Package] Genie.jlを使ってJuMPの計算をAPI化する実験 | Knapsack問題を例にして

2021/03/10に公開

最近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=15N=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の最も簡単な例チュートリアルからやってみます.実装のソースです.

test.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の実装も入れて,結果を整形して出力しましょう.解はobjresという形で返し,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で書けると思いますけどね).

参考

最後に参考にしたページの一覧です.

Discussion