LoginSignup
10
4

More than 5 years have passed since last update.

flaskで実装された機械学習APIにrequestsでPython objectを渡す方法

Last updated at Posted at 2019-04-02

はじめに

ご了承

  • この記事はわかりにくい(何をしたいか分からない,日本語がバグってる)かもしれません.
  • この記事のプログラムは,僕が書いたスクリプトから一部抜き取ったものなので,動くかどうかはわかりません.
  • Flaskを使った機械学習APIの記事 (たとえば これとか,これとか,これ) はAPIのFlaskでの実装を主としていますが,この記事はクライアント側もPython実装する場合を考えるため,Webサービスを作りたい方には参考にならないかもしれません.

シチュエーション

機械学習モデルにデータをたくさん予測させるようなシチュエーションを想定しています.この記事の内容はすべてCLIで実行することを前提として書いてあります.どちらかと言うと研究用.
APIも,client側のプログラムも自分で実装するものとします.

 _________                       ___________
| client  | ---- x: object ---> | api server|
| data:x  |                     | y = f(x)  |
|_________| <--- y: object ---- |___________| 

メリット

たとえば,機械学習のモデルをサーバ側にAPI実装して,クライアントから,予測したいデータをPython Objectで投げて,それをそのままサーバ側で使えたらデータの形に依存しない通信が可能となる.データ通信の部分を考えずに,スクリプトを実装できるようになる.

Client側の実装

requestsライブラリを使います.requests.post("URL")でURLにPOSTリクエストを送れます.
こんなクラスを実装します.Modelインスタンスを通して,APIにQueryを投げます.

model.py
import requests
import pickle

class Model(object):
   def __init__(self, base_url:str):
        """base_url is api url
        for example: base_url=http://abc.xyz/
        following must be accessable:        
        - http://abc.xyz/evaluate
        response must include 'evaluation' and 'class' fields.
        """
        self.base_url = base_url


    def evaluate(self, x:object) -> dict:
        """ :X -> dict
        return the api result        
        x must be able to convert to a pickle object
        """
        # make payload
        payload = {"instance": x}

        resp = requests.post(self.base_url+"evaluate",
                             data=pickle.dumps(payload),
                             headers={"Content-Type": "application/octet-stream"})
        resp.raise_for_status()
        # if status is 200, raise_for_status returns None

        return pickle.loads(resp.content)
script.py
from model import Model
if __name__ == "__main__":
    model = Model("your_api_url")
    #
    # Scratch here :)
    # 

ポイント

  • Objectをpickle.dumps(object)でPickle化 (わざわざファイルに保存する必要はない) して通信することで,引数xの型に依存しなくなる.これで,使うモデル,データが変わっても,このクラスを使えるようになる.クライアント側はscript.pyだけの変更で良い.
  • レスポンスもPickle化されているため,pickle.loads(resp)でAPI出力を受け取る.Pickle化の恩恵で,APIの出力に依存しない.結果の解析はscript.pyでやればいい.
  • headers={"Content-Type": "application/octet-stream"}が一番大事.これがないと,flaskで読み取れない.一般にdataがjsonの場合はapplication/jsonとか書くでしょ?知らんけど.
  • raise_for_status()で例外処理.400,500系のエラーが発生したらプログラムを終了する.
  • モデルはサーバ側にあるためクライアント側では気にしなくていい.とりあえず,model=Model("your_api_url")ってやっておいて,実験スクリプトをscript.pyに書けばいい.
  • データセットはクライアント側にあっても,なくてもよい.
    • あるとき:x = cv2.imread(img_path)とかで,データを読み込んで,model.evaluate(x)でおk.
    • ないとき:大規模なデータセットがサーバ上にあって,そのテストデータのいくつかを予測をしたい場合,model.evaluate(x=("test", 5)とかにしておいて,API側で,データをロードすればいい.この場合,テストデータの5番目のデータがロードされるようにAPIを実装する.

Server側の実装

サーバ側はFlaskをつかって実装します.Flaskを使ったAPI実装方法は上で示した記事などにも書いてあるため,詳細は割愛します.
以下の例では,学習済みのscikit-learnのmlpを使う例を示します.
このAPIの出力はxの予測されたクラスと,その確率の辞書,すなわち{"prob": prob, "class", cls}となります.

api.py
import pickle
import flask

app = flask.Flask(__name__)
model = None

class Model(object):
    def __init__(self):
        # load model
        with open("pre-trained-mlp.pkl", "rb") as f:
            self.model = pickle.load(f)

    def evaluate(self, x, cls):
        return self.model.predict_proba(x)[0][cls]

    def predict(self, x):
        return self.model.predict(x)[0]


@app.route("/evaluate", methods=["POST"])
def evaluate():
    if flask.request.method == "POST":
        data = pickle.loads(flask.request.data)
        x = data["instance"]
        cls  = model.predict(x)
        prob = model.evaluate(x, cls)
        return pickle.dumps({
            "prob": prob,
            "class": cls
        })

if __name__ == "__main__":
    print(" * Starting server ...")
    print(" * initializing ...")
    model = Model()
    print(" * done")
    app.run()

ポイント

  • サーバ側にもモデルを制御するためのクラス (この場合はModel) を作る.ここは要実装.わざわざクラスにしなくても,グローバル変数で十分な場合もあるかも. データの前処理が多い場合などはクラスにしたほうがいいかも.
  • flask.request.method=="POST"でリクエストの種類の判定
  • flask.request.dataにPickle化されたxの生データが入っている.requests.postheaders={"Content-Type": "application/octet-stream"}を指定することで,これで受け取れる.
  • pickle.loads(flask.request.data)でPython Objectに戻す.あとは,こいつをモデルに入力して結果を得る.
  • 結果はPickle化してreturnする.これで,どんな結果でも返信できる.

おわりに

とりあえず,意図するものができました.分かっていれば多分簡単かもしれませんが,Flaskを触ったのが初めてだったので色々戸惑いました.
機械学習の実験のために書いたものですが,他のタスクにも応用できそうですね.
改善点や変なところがあったらコメントいただけると嬉しいです.

10
4
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
10
4