機械学習モデルの予測値を返すAPIをDockerで作ってみた

# はじめに

Yellowbrickで精度を確認しつつ作った機械学習モデル[^1]を予測APIサーバのような形で活用してみたいと思い、Dockerで作ってみた。
Flaskについてはこちら[^2]、Dockerについてはこちら[^3]を参考にして、APIサーバーから応答が返ってくるところまでを目標にする。


# 環境

環境は以下の通り。

```
$sw_vers
ProductName:	Mac OS X
ProductVersion:	10.13.6
BuildVersion:	17G8037
```

Dockerのインストールについては[^4]を参考にした。
ログが結構長かったので、一部省略。

```
$docker version
Client: Docker Engine - Community
 Version:           19.03.4
 API version:       1.40
 Go version:        go1.12.10
（省略）
Server: Docker Engine - Community
 Engine:
  Version:          19.03.4
  API version:      1.40 (minimum version 1.12)
（省略）
```

# 構築

## Dockerイメージの作成

まずDockerイメージをDockerfileから作成する。
エディタで以下の内容が記載されたファイルを作成する。

```Dockerfile
FROM ubuntu:latest

RUN apt-get update
RUN apt-get install python3 python3-pip -y

RUN pip3 install flask
RUN pip3 install scikit-learn
RUN pip3 install numpy
RUN pip3 install scipy
RUN pip3 install lightgbm
RUN pip3 install joblib
RUN pip3 install pandas
```

dockerコマンドでイメージを作成する。

```
docker build . -t myflask/mlapi:1.0
```

しばらくすると、以下のメッセージが表示されて構築完了。

```
Successfully built 4a82ed953436
Successfully tagged myflask/mlapi:1.0
```

dockerイメージが作られていることを``docker images`` コマンドで確認。

```
$docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
myflask/mlapi       1.0                 4a82ed953436        7 minutes ago       782MB
ubuntu              latest              775349758637        2 days ago          64.2MB
```

試しにdockerイメージに入って、ライブラリがインポートできるか確認。
確かに``python3``も``lightgbm``のライブラリも使えることを確認。

```
root@117483d4b9ed:/# python3
Python 3.6.8 (default, Oct  7 2019, 12:59:55) 
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import lightgbm
>>> exit()
```

## APIサーバ用プログラム作成

dockerイメージから抜けて、ローカル環境でAPIサーバ用のプログラムを作成する。
機械学習モデルを作った際に``joblib``でモデルを保存した[^1]ので、同じく``joblib``を使ってモデルをロードしている。
こちら[^2]の記事を参考にしているが、データの項目量が違うので受け取り部分について工夫した。
一度、``features``の中身をpandasのDataFrameに渡して予測するようにしている。

```py

from joblib import load
import flask
import numpy as np
import pandas as pd
import lightgbm as lgb

# initialize our Flask application and pre-trained model
app = flask.Flask(__name__)
model = None

def load_model():
    global model
    model = load("./lightgbm.joblib")

@app.route("/predict", methods=["POST"])
def predict():
    response = {
        "success": False,
        "Content-Type": "application/json"
    }

    if flask.request.method == "POST":
        if flask.request.get_json().get("feature"):

            # read feature from json and convert to dataframe
            features = flask.request.get_json().get("feature")
            df_X = pd.DataFrame.from_dict(features)

            # predict
            response["prediction"] = model.predict(df_X).tolist()

            # indicate that the request was a success
            response["success"] = True
    # return the data dictionary as a JSON response
    return flask.jsonify(response)

if __name__ == "__main__":
    load_model()
    print("Server is running ...")
    app.run(host='0.0.0.0', port=5000)
```

## APIサーバ起動

ローカル環境にディレクトリを作って、上のプログラムと、機械学習モデルを格納しておく。

```
mkdir vol
mv lightgbm.joblib ./vol/
mv api.py ./vol/
```

dockerコマンドで上で作ったvolディレクトリをdockerイメージにmountして、起動する。

```
$docker run -it --rm -p 5000:5000 -v $(pwd)/vol:/home myflask/mlapi:1.0 /bin/bash
```

home以下にあるapi.pyを実行すると、APIサーバが起動する。

```
root@5d1e3cf74246:/# cd home/
root@5d1e3cf74246:/home# python3 api.py 
Server is running ...
 * Serving Flask app "api" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

```

## 応答の確認

上でIPアドレス0.0.0.0、ポート5000にサーバを設定しているので、ここに``curl``コマンドで予測するためのデータを渡してみる。
学習データはYellowbrickのload_bikeshareを使っているので、項目を揃えてfeaturesの中に詰めてサーバに渡している。そのままpandasのDataFrameに変換できるように、データの要素をリストにした。

```
$curl http://0.0.0.0:5000/predict -X POST -H 'Content-Type:application/json' -d '{"feature":{"season":[1], "year":[0], "month":[1], "hour":[0], "holiday":[0], "weekday":[6], "workingday":[0], "weather":[1], "temp":[0.24], "feelslike":[0.3], "humidity":[0.8], "windspeed":[0.0]}}'
```

上記コマンドを打つと、下のように応答が帰ってきた。やったー。

```
{"Content-Type":"application/json","prediction":[34.67747315059312],"success":true}
```

# さいごに

REST APIとか、サーバとか詳しくなくてもDockerやFlaskを使うことで簡単に構築できた。
今度Webページから予測を投げて、帰ってきた値を表示するようなものを作ってみたい。

[^1]: https://qiita.com/dyamaguc/items/ea678dbdbe518f4bf405
[^2]: https://qiita.com/fam_taro/items/1464c42324f15d7b8223
[^3]: https://qiita.com/phorizon20/items/57277fab1fd7aa994502
[^4]: https://qiita.com/kurkuru/items/127fa99ef5b2f0288b81