シリーズバックナンバー
- 何も考えずに Emacs を使って Clojure の Luminus webframework を使う
- Luminus のサーバ側で手っ取り早くAPIを試したいメモ書き
- Luminus で re-frame 、ping-pong ボタンを追加してみるメモ書き
- Luminus で GCE 上の API を叩いてみる
GCEで良い感じにAPIが建った!取り敢えず curl しようぜ!
GCE (Google Cloud Engine) でGPU付きの仮想マシンを借り、cuda 9.0 なんかの設定を(~~~当時手っ取り早く Tensorflow を動かすためにはなんとかして 9.0 をインストールする必要があった。おのれTensorflow~~~)済ませる下処理1、良い感じに Nginx や gunicorn、 supervisor やらを設定する下処理2をし、良い感じにAPIが立ったものとします(中身の公開はもう少し待ってね)。
ちなみに借りた環境はこんな感じの構成になっています。よわよわですが、学習をさせたいわけじゃないので、まあいいかなという感じです。
- マシンタイプ : n1-highmem-8(vCPU x 8、メモリ 52 GB)
- GPU : 1 x NVIDIA Tesla P100
- zone : us-east1-b
- ファイアウォール : http トラフィックを許可(これがないと多分HTTPリクエスト通りません)
- ストレージ : SSD 50GB (外部ストレージは速度的にどうなのか不安だった(~~~あとよくわかんなかった~~~)ので、モデルファイルはここに入れました。)
- 外部IP : 34.73.35.132 (固定していないので再起動するごとに変わります。)
するとAPIはこんな感じで curl でアクセスする事ができるようになります。(jq を通しているのは UTF-8 文字を可視化するためと、JSONを見やすくするためです。)
一つ目のAPI
docomo の雑談対話APIみたいなことをやります。入力文に対応する出力文を作って返してきます。雑に作っただけあって適当な答えが返ってきます。
curl -H "Content-type: application/json" -X GET -d '{"input": "付き合って下さい"}' http://34.73.35.132/translate | jq
# =>
# {
# "Content-Type": "application/json",
# "input": "付き合って下さい",
# "request-date": "None",
# "turn": "どうしようかな。",
# "user-id": "None"
# }
二つ目のAPI
文を良い感じにくだけさせます。特に語尾変化なんかをよく学習しています(多分)。
curl -H "Content-type: application/json" -X GET -d '{"input": "付き合って下さい"}' http://34.73.35.132/transform | jq
# =>
# {
# "Content-Type": "application/json",
# "input": "付き合って下さい",
# "request-date": "None",
# "turn": "付き合って",
# "user-id": "None"
# }
三つ目のAPI
任意の入力文に対してそれがいくつかのイベントトリガーになる文であるかを判定し、その確率を返します。
イベントトリガーとなる文として今回は30文用意し、その類似文を幾つか用意してそれの要約統計を取ることで確率としています。
curl -H "Content-type: application/json" -X GET -d '{"input": "注文お願いします。"}' http://34.73.35.132/detect-class | jq
# {
# "Content-Type": "application/json",
# "input": "注文お願いします。",
# "request-date": "None",
# "turn": {
# "label": 26,
# "percentage": 71.32364147109911,
# "represent": "注文して良いですか?",
# "sort": [
# [
# 20,
# 0.0017914147465489805
# ],
# [
# 7,
# 0.0017973725334741175
# ],
# ...
# [
# 29,
# 0.15609956318179943
# ],
# [
# 26,
# 0.7132364147109911
# ]
# ]
# },
# "user-id": "None"
# }
Web サーバで curl の再現をしよう!
curl で出来たことを Luminus からやってみようということです。
(ns demo-app.routes.services
(:require [reitit.swagger :as swagger]
[reitit.swagger-ui :as swagger-ui]
;; ...
[ring.util.http-response :refer :all]
[clojure.java.io :as io]
;;;
[clj-http.client :as client]
;;;
))
;;;
(def gce-global-ip "http://34.73.35.132")
(def my-api-routes
["/sample-api"
{:swagger {:tags ["sample-api"]}}
["/get-translate"
{:post {:summary "get a translated (1-by-1 communication) sentence"
:parameters {:body {:uttr string?}}
:handler (fn [{{{:keys [uttr]} :body} :parameters}]
(let [response (client/get
(str gce-global-ip "/translate")
{:content-type :json
:as :json
:form-params {:input uttr}
})]
{:status 200
:body (:body response)}))
}}]
["/get-transform"
{:post {:summary "get a transformed sentence"
:parameters {:body {:uttr string?}}
:handler (fn [{{{:keys [uttr]} :body} :parameters}]
(let [response (client/get
(str gce-global-ip "/transform")
{:content-type :json
:as :json
:form-params {:input uttr}})]
{:status 200
:body (:body response)}))}}]
["/get-class"
{:post {:summary "get a transformed sentence"
:parameters {:body {:uttr string?}}
:handler (fn [{{{:keys [uttr]} :body} :parameters}]
(let [response (client/get
(str gce-global-ip "/detect-class")
{:content-type :json
:as :json
:socket-timeout 10000
:form-params {:input uttr}})]
{:status 200
:body (-> response
:body
(update-in [:turn] dissoc :sort))}))}}]
])
;;;
(defn service-routes []
["/api"
{:coercion spec-coercion/coercion
:muuntaja formats/instance
:swagger {:id ::api}
:middleware [;; query-params & form-params
parameters/parameters-middleware
;; ...
;; multipart
multipart/multipart-middleware]}
;; swagger documentation
["" {:no-doc true
:swagger {:info {:title "my-api"
:description "https://cljdoc.org/d/metosin/reitit"}}}
["/swagger.json"
{:get (swagger/create-swagger-handler)}]
["/api-docs/*"
{:get (swagger-ui/create-swagger-ui-handler
{:url "/api/swagger.json"
:config {:validator-url nil}})}]]
;;;
my-api-routes
;;;
["/ping"
{:get (constantly (ok {:message "pong"}))}]
["/math"
{:swagger {:tags ["math"]}}
["/plus"
;; ...
]]
["/files"
{:swagger {:tags ["files"]}}
["/upload"
;; ...
]
["/download"
;; ...
]]])
説明
(ns demo-app.routes.services
(:require [reitit.swagger :as swagger]
[reitit.swagger-ui :as swagger-ui]
;; ...
[ring.util.http-response :refer :all]
[clojure.java.io :as io]
;;;
[clj-http.client :as client]
;;;
))
依存関係に clj-http.client
を追加しています。これは clojure で http 通信を行うためのライブラリ clj-http
の client
クラスです。今回は後述するように、このクラスの get
関数を使っています。
(def gce-global-ip "http://34.73.35.132")
GCE から与えられるグローバルIPアドレスです。静的なものではないので、サーバを再起動するたびに書き換える必要があります。(そして恐らくGCEの運営側はそれなりの料金を払って静的グローバルアドレスを取得してこの部分を書き換えることを期待しています。)
(def my-api-routes
["/sample-api"
{:swagger {:tags ["sample-api"]}}
["/get-translate"
{:post {:summary "get a translated (1-by-1 communication) sentence"
:parameters {:body {:uttr string?}}
:handler (fn [{{{:keys [uttr]} :body} :parameters}]
(let [response (client/get
(str gce-global-ip "/translate")
{:content-type :json
:as :json
:form-params {:input uttr}})]
{:status 200
:body (:body response)}))
}}]
["/get-transform"
{:post {:summary "get a transformed sentence"
:parameters {:body {:uttr string?}}
:handler (fn [{{{:keys [uttr]} :body} :parameters}]
(let [response (client/get
(str gce-global-ip "/transform")
{:content-type :json
:as :json
:form-params {:input uttr}})]
{:status 200
:body (:body response)}))}}]
["/get-class"
{:post {:summary "get a transformed sentence"
:parameters {:body {:uttr string?}}
:handler (fn [{{{:keys [uttr]} :body} :parameters}]
(let [response (client/get
(str gce-global-ip "/detect-class")
{:content-type :json
:as :json
:socket-timeout 10000
:form-params {:input uttr}})]
{:status 200
:body (-> response
:body
(update-in [:turn] dissoc :sort))}))}}]
])
ちょっと長いですね。しかしやっていることは対して難しいものではないです。
[]
のネストを考えると、/sample-api
の下に /get-translate
などが含まれていることがわかると思います。これはそのままURLの階層構造と見なすことができます。つまり、get-translate にアクセスしたいならば (localhost:3000/api)/sample-api/get-translate
を指定すれば良いということです。
さて、まず get-translate
を見てみましょう。
["/get-translate"
{:post {:summary "get a translated (1-by-1 communication) sentence"
:parameters {:body {:uttr string?}}
:handler (fn [{{{:keys [uttr]} :body} :parameters}]
(let [response (client/get
(str gce-global-ip "/translate")
{:content-type :json
:as :json
:form-params {:input uttr}})]
{:status 200
:body (:body response)}))
}}]
:post
とは即ちpost method を示しています。この値としては :summary
:parameters
:handler
のキーを持ったマップ構造になっています。
:summary
はこの method の説明を記述します。
:parameters
は引き取る値のヒントを記述します。ここでは、body 要素の中の uttr に 文字列型の値を期待しています。
:handler
はこの method が呼ばれた時に処理されるハンドラを記述します。ここでは、クライアントから送られてきたデータから :parameter
要素を取り出し、その中の :body
要素にある :uttr
要素を取り出します。
簡単に言うと、{:parameters {:body {:uttr data}}}
の data
を取り出していることになります。
そしてその data
を用いて機械学習サーバに問い合わせを行います。この部分は後述します。
最後に得られたレスポンスの body を取り出し、:status 200
を付けてクライアントへ送るデータとしてまとめます。
(client/get
(str gce-global-ip "/translate")
{:content-type :json
:as :json
:form-params {:input uttr}})
これが curl を書き換えている部分です。ここで注目するのは :as :json
の部分と :form-params
の部分です。
:as :json
は json データがサーバからかえってくる事を補足し、受け取った際にこれを clojure のマップ構造に置換できるようにしています。
:form-params
は :content-type :json
が指定されているとき、値である マップ構造を json 形式にエンコードし、リクエストの body 要素とします。(:content-type :json
が指定されていなければこれは url エンコードされて body 要素になるようです。)
他の2つも同様ですが、最後の get-class
のハンドラがちょっと特殊なのでその部分だけ触れます。
["/get-class"
{:post {:summary "get a transformed sentence"
:parameters {:body {:uttr string?}}
:handler (fn [{{{:keys [uttr]} :body} :parameters}]
(let [response (client/get
(str gce-global-ip "/detect-class")
{:content-type :json
:as :json
:socket-timeout 10000
:form-params {:input uttr}})]
{:status 200
:body (-> response
:body
(update-in [:turn] dissoc :sort))}))}}]
:socket-timeout
を指定しているのはこの機械学習APIが結構時間がかかってしまうためで、10秒経っても結果が返ってこなければその旨を示すエラー出すようにしています。
最後の行周辺の Threading macro は、:turn
要素の :sort
要素を削除する処理を行っています。これは :sort
部がちょっと量が多いのでクライアントに見せる必要がないかな、と思いまして省略する意図で書いています。
Swagger で動いているのを見せれば十分やろ
localhost:3000/swagger-ui
にアクセスすると良い感じの画面が見えます。
適当にぽちぽちと入力して Try it out!
を押すとサーバから出力を得ることができます。
ちょっとした愚痴
ところでこのAPIの中身、卒業研究で行ったものなんですが、すこぶる受けが悪くて私悲しい...
やっぱり画像認識の方が学術的に受けるんですかね?