LoginSignup
0
0

More than 5 years have passed since last update.

Luminus で GCE 上の API を叩いてみる

Last updated at Posted at 2019-02-11

シリーズバックナンバー

  1. 何も考えずに Emacs を使って Clojure の Luminus webframework を使う
  2. Luminus のサーバ側で手っ取り早くAPIを試したいメモ書き
  3. Luminus で re-frame 、ping-pong ボタンを追加してみるメモ書き
  4. 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 からやってみようということです。

{project-name}/src/clj/{project-name}/routes/services.cljに追記
(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-httpclient クラスです。今回は後述するように、このクラスの 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! を押すとサーバから出力を得ることができます。
image.png

image.png

image.png

ちょっとした愚痴

ところでこのAPIの中身、卒業研究で行ったものなんですが、すこぶる受けが悪くて私悲しい...
やっぱり画像認識の方が学術的に受けるんですかね?

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0