この記事はJubatus Advent Calendar 11日目の記事です。
Jubatusをとあるプロジェクトで使う為に問題になったことを手元で解決するために作った道具の話をします。
問題点1. JubatusにHTTP経由でアクセスしたい
Jubatusとはmsgpack-rpcプロトコルで通信するサーバプロセスである。
msgpack-rpcは高速なバイナリプロトコルで、それをjubatus-proxyで負荷分散することができる。
しかし、JubatusにHTTP経由でアクセスすることができればロードバランサで負荷を散らしたりnginxにhttpsを終端させたりBASIC認証を噛ませたりと様々なHTTPのノウハウを活かす事ができる。
問題点2. Jubatusプロセスを再起動せずにモデルを作り直したい
0.3までのJubatusは実は新しいモデルを set_config
rpcで作ることができたが、一度に1プロセスに付き1モデルしか保持できない事から、実質初期化rpcに過ぎなかったので起動時オプションで与える形に変わった。
昔のJubatusの構想としてはrpc経由で複数のモデルを作ったりその出力を組み合わせたり多段にしたりアンサンブルしたりなどといろんなことができる構想が有ったようだが、そのインタフェースをどうしたら良いか確定しなかったのもあって実装されなかった。
Jubatusのrpcは第一引数にname
という引数を取り、クラスタ内にある複数のモデルを呼び分ける事ができる。しかし1プロセス1モデルの構造の上なので複数のモデルを同時に使うためにはそれぞれ別のモデル名を与えたJubatusプロセスを立ち上げる必要がある。そしてZooKeeperとjubatus_proxyが必要となり管理が面倒になる。
http_jubatus
これら2つの問題を同時に解決するために作ったのが、httpエンドポイントを持ち、複数のJubatusプロセスの死活を行い、そいつらとの通信を中継してくれるhttp_jubatusである。
慣れないGo言語でおっかなびっくり書いたのでイケてないコードはちょいちょいあると思う。
$ go run http_jubatus.go
と立ち上げる事ができる。
モデル追加
新たにclassifierプロセスを立ち上げる際には
POST /classifier/
にjubatusのコンフィグファイルとname
を投げつける。幸いにしてjubatusのコンフィグファイルはjsonなのでhttpと親和性が高い。classifier以外のプロセスを立ちあげたい場合はその名前がpathになっているので POST /recommender/
なり何なりできる。
$ curl localhost:3000/classifier \
-H 'Content-type: application/json' \
-X POST \
-d '{
"name": "sample_classifier",
"parameter": {
"converter" : {
"string_rules" : [
{ "key" : "*", "type" : "str", "sample_weight" : "bin", "global_weight" : "bin" }
],
"num_rules" : [
{ "key" : "*", "type" : "num" }
]
},
"method" : "PA"
}
}'
という感じでcurlでPOSTすると、sample_classifier
というname
を持ったjubatusプロセスが新たにexecされ、コンフィグとしてJSONファイルの中身が渡される。
機械学習実行
name
はそのままPATHの一部となる、そこの下にjubatusのrpc名をつなげた物が機械学習のHTTPエンドポイントになる。また、msgpackはJSONとおおよそ対応関係があるので、JSON形式でjubatusのパラメータを表現する事で機械学習が実行できる。例えばtrain
を行いたい場合は以下のようになる。
curl localhost:3000/classifier/sample_classifier/train \
-H 'Content-type: application/json' \
-X POST \
-d '[
[
[
"bar",
[
[],
[
["fuga", 1.0]
],
[]
]
]
]
]'
反省点
良かったことと悪かった事がある。
良かったこと:モデルをRPCで作れるのは便利
ハイパーパラメータの調整のためなどでモデルを調整するにしてもクライアント側のプログラムを調整してリトライすればよいし、複数のハイパーパラメータで精度を比べたい場合でも全部とりあえず立ち上げるというクライアント側のプログラムがとても書きやすかった。サーバに食わせるJSONファイルを個別に編集してプロセスを立ちあげては殺していた日々は何だったのか。
悪かった事:JSONパラメータをcurlで記述するのは地獄
括弧の対応関係を追いかけるのが非常につらい。
HTTPで与えるオブジェクトを1つにしたかったのもあり、引数全体を1つの配列オブジェクトとして受け取るので配列1層、trainは複数の教師データを一度に与える事ができるようにpaired datum
の配列を受け取る。paired datum
はラベルとdatum
による長さ2の配列としてmsgpackの構造で表現されている。そしてdatum
は長さ3の配列となっていて、string_values
,num_values
,binary_values
でできている。それぞれのvalues
はキーバリューペアの配列でありキーバリューペアはキーとバリューを含む長さ2の配列である。それらがすべて入れ子構造になっているので、それを表したJSON構造のデバッグは地獄だった。
もっとJSONライクに配列ではなくてオブジェクトでデータ構造を詰め込めばよかったのだけれど、RPC毎にJSON⇔msgpackの変換を手作業で書くのは現実的なコストではなく、いっそのことJubatus-idlから自動生成するようjenerator
に改変を加えたほうが良いと思う。
もう少し使ってみて良さそうであればJubatus本流でも同様のインタフェース経由で使えるようにできたらいいなと思う。