scikit-learnとFlaskによる機械学習モデルのサービング 

著者: 伊藤 雅博, 株式会社日立製作所

# はじめに

データサイエンティストが作成した機械学習モデルを本番環境へ導入するためには、機械学習モデルを用いた予測機能を提供（サービング）するシステムの開発が必要となります。本投稿では、実際にscikit-learn pipelinesとFlaskを使用して機械学習モデルをサービングする例を紹介します。

**投稿一覧:**
1. [MLOpsの概要と機械学習モデルのサービングシステム](https://qiita.com/mshr-ito/items/f456f60fc5c557a19cf5)
2. scikit-learnとFlaskによる機械学習モデルのサービング ...本投稿

# 構築するリアルタイム推論システムの全体像

Web APIを用いたリアルタイム推論システムの構築手順を紹介します。[前回の投稿](https://qiita.com/mshr-ito/items/f456f60fc5c557a19cf5#oss%E3%81%AE%E4%B8%BB%E8%A6%81%E3%81%AA%E3%82%B5%E3%83%BC%E3%83%93%E3%83%B3%E3%82%B0%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AF%E3%83%BC%E3%82%AF)で紹介したようなサービングFWを活用する方法もありますが、今回は個別のOSSを組み合わせてシンプルな推論システムを構築してみます。

この推論システムでは、Python製の機械学習フレームワークであるscikit-learnと、Python製のWebサーバであるFlaskを使用します。システム構築手順の全体像を以下の図に示します。データセットの準備から処理のパイプライン化、デプロイ、推論システムの作成、推論実行まで一通り実施してみます。

![リアルタイム推論システムの構築手順](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/937307/3938fa77-6484-145e-0c20-e80da52fb9e6.png)

以下ではPythonのソースコードを掲載しますが、1.から4.は学習サーバ上のJupyterノートブックで実行しても問題ありません。5.と6.は各サーバ上にソースファイル（.py）を作成して実行することを推奨します。

# システム環境

上記の図に示したシステムを構築するため、学習サーバ、推論サーバ、AI利用サーバの3台のマシンを用意します。各マシンにインストールしたソフトウェアを以下に示します。

|マシン|ソフトウェア|
|:---|:---|
|学習サーバ|Python、NumPy、Pandas、scikit-learn、Jupyter|
|推論サーバ|Python、NumPy、Pandas、scikit-learn、Flask|
|AI利用サーバ|Python、urllib|

# 構築手順

## 1. データセットの準備

データセットには以下のKaggleサイトからダウンロードできる「タイタニック号の生存者データ」を使用します。

* [Titanic: Machine Learning from Disaster](https://www.kaggle.com/c/titanic/data)

データセット`train.csv`の一部を整形して表示したものを以下に示します。

```
     PassengerId  Survived  Pclass             Name     Sex   Age  SibSp  Parch            Ticket     Fare Cabin Embarked
0              1         0       3  Braund, Mr. Owe    male  22.0      1      0         A/5 21171   7.2500   NaN        S
1              2         1       1  Cumings, Mrs. J  female  38.0      1      0          PC 17599  71.2833   C85        C
2              3         1       3  Heikkinen, Miss  female  26.0      0      0  STON/O2. 3101282   7.9250   NaN        S
3              4         1       1  Futrelle, Mrs.   female  35.0      1      0            113803  53.1000  C123        S
4              5         0       3  Allen, Mr. Will    male  35.0      0      0            373450   8.0500   NaN        S
..           ...       ...     ...              ...     ...   ...    ...    ...               ...      ...   ...      ...
886          887         0       2  Montvila, Rev.     male  27.0      0      0            211536  13.0000   NaN        S
887          888         1       1  Graham, Miss. M  female  19.0      0      0            112053  30.0000   B42        S
888          889         0       3  Johnston, Miss.  female   NaN      1      2        W./C. 6607  23.4500   NaN        S
889          890         1       1  Behr, Mr. Karl     male  26.0      0      0            111369  30.0000  C148        C
890          891         0       3  Dooley, Mr. Pat    male  32.0      0      0            370376   7.7500   NaN        Q
```

データセットの各列の説明を以下に示します。

|#|列名|説明|
|---:|:---|:---|
|1.|PassengerId|乗客の識別ID|
|2.|Survived|生存結果（0: 死亡、1: 生存）|
|3.|Pclass|チケットのクラス（1st、2nd、3rd）|
|4.|Name|乗客の名前|
|5.|Sex|性別（male: 男性、female: 女性）|
|6.|Age|年齢|
|7.|SibSp|同乗している兄弟/配偶者の数|
|8.|Parch|同乗している両親/子供の数|
|9.|Ticket|チケット番号|
|10.|Fare|乗船料金|
|11.|Cabin|客室番号|
|12.|Embarked|乗船した港（C: Cherbourg、Q: Queenstown、S: Southampton）|

まず、以下のようにデータセットを訓練用、検証用、評価用に3分割します。

```python
import pandas as pd
from sklearn.model_selection import train_test_split

# データを読み込み
df = pd.read_csv('titanic/train.csv')

# データセットを、訓練用データと検証・評価用データに分割
(train, valid_test) = train_test_split(df, test_size = 0.3)

# 検証・評価用データを、検証用データと評価用データに分割
(valid, test) = train_test_split(valid_test, test_size = 0.5)

# 件数を確認
print("train: " + str(len(train)))
print("valid: " + str(len(valid)))
print("test : " + str(len(test)))
```

上記ソースコードの実行結果（データセット分割後の件数）を以下に示します。

```
train: 623
valid: 134
test : 134
```

## 2. 探索的モデル分析（実験）

よい学習モデルを作成するために、特徴量の作成、モデルの選択、モデルのハイパーパラメータ探索といった実験をJupyter Notebook上で繰り返します。実験の結果、以下が最適だと分かったことにします。

- 特徴量作成処理として、不要な列の削除、欠損地処理、カテゴリ変数の数値変換を実施
- モデルにランダムフォレストを選択
- ランダムフォレストのハイパーパラメータは、シード値（random_state）が0、決定木の個数（n_estimators）は10に決定

実験後の最終的なソースコードを以下に示します。

```python
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (roc_curve, auc, accuracy_score)
import matplotlib.pyplot as plt

train = train.copy()
valid = valid.copy()

# 説明変数を抽出
X_train = train.drop('Survived', axis=1)
X_valid = valid.drop('Survived', axis=1)

# 目的変数を抽出
y_train = train.Survived
y_valid = valid.Survived

# 特徴量作成処理を定義
def preprocess(X):
    # 不要な列を削除
    X = X.drop(['Cabin','Name','PassengerId','Ticket'],axis=1)

    # 欠損値処理
    X['Fare'] = X['Fare'].fillna(X['Fare'].median())
    X['Age'] = X['Age'].fillna(X['Age'].median())
    X['Embarked'] = X['Embarked'].fillna('S')

    # カテゴリ変数の変換
    X['Sex'] = X['Sex'].apply(lambda x: 1 if x == 'male' else 0)
    X['Embarked'] = X['Embarked'].map({'S': 0, 'C': 1, 'Q': 2}).astype(int)
    return X

# 特徴量作成
X_train = preprocess(X_train)
X_valid = preprocess(X_valid)

# モデルにランダムフォレストを使用
clf = RandomForestClassifier(random_state=0, n_estimators=10)

# モデルを訓練
clf = clf.fit(X_train, y_train)

# 検証データで予測
pred = clf.predict(X_valid)

# 予測結果の評価指標を確認
from sklearn.metrics import accuracy_score
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score
from sklearn.metrics import f1_score

# 正解率 (Accuracy)
print("Accuracy : ", accuracy_score(y_valid, pred))
# 精度 (Precision)
print("Precision: ", precision_score(y_valid, pred))
# 検出率 (Recall)
print("Recall   : ", recall_score(y_valid, pred))
# F値 (F-measure) 
print("F-measure: ", f1_score(y_valid, pred))
```

上記ソースコードの実行結果（検証データによる評価指標）を以下に示します。

```
Accuracy :  0.7835820895522388
Precision:  0.7407407407407407
Recall   :  0.7272727272727273
F-measure:  0.7339449541284404
```

## 3. パイプライン化と学習

特徴量作成とモデルの訓練を、scikit-learnのパイプライン処理として定義します。まずは、以下のように特徴量作成処理をscikit-learnのTransformerとして定義し、`preprocessor.py`ファイルに保存しておきます。

```python:preprocessor.py
from sklearn.base import TransformerMixin

# 特徴量作成処理をTransformer化
class PreProcessor(TransformerMixin):
    def fit(self, X, y):
        return self

    def transform(self, X):
        # 不要な列を削除
        X = X.drop(['Cabin','Name','PassengerId','Ticket'],axis=1)

        #欠損値処理
        X['Fare'] = X['Fare'].fillna(X['Fare'].median())
        X['Age'] = X['Age'].fillna(X['Age'].median())
        X['Embarked'] = X['Embarked'].fillna('S')

        #カテゴリ変数の変換
        X['Sex'] = X['Sex'].apply(lambda x: 1 if x == 'male' else 0)
        X['Embarked'] = X['Embarked'].map(
                {'S': 0, 'C': 1, 'Q': 2}).astype(int)

        return X
```
 
次に、以下のように特徴量作成処理とモデル（ランダムフォレスト）とハイパーパラメータをまとめてパイプラインとして定義し、最終的な訓練と評価を行います。

```python
from sklearn.pipeline import Pipeline

# 自作の特徴量作成処理をimport
from preprocessor import PreProcessor

# 学習/予測パイプラインを定義
ml_pipeline = Pipeline([
        # 自作の特徴量作成を指定
        ('preprocessor', PreProcessor()), 
        # モデル（ランダムフォレスト）とハイパパラメータを指定
        ('random_forest', RandomForestClassifier(
                random_state=0, n_estimators=10))])

# 訓練データと評価データを用意
train = train.copy()
test = test.copy()

# 説明変数と目的を抽出
X_train = train.drop('Survived', axis=1)
X_test = test.drop('Survived', axis=1)
y_train = train.Survived
y_test = test.Survived

# パイプラインで訓練を実行
ml_pipeline.fit(X_train, y_train)

# パイプラインで予測を実行
pred = ml_pipeline.predict(X_test)

# 予測結果の評価指標を確認
# 正解率 (Accuracy)
print("Accuracy : ", accuracy_score(y_test, pred))
# 精度 (Precision)
print("Precision: ", precision_score(y_test, pred))
# 検出率 (Recall)
print("Recall   : ", recall_score(y_test, pred))
# F値 (F-measure) 
print("F-measure: ", f1_score(y_test, pred))
```

上記ソースコードの実行結果（評価データによる最終的な評価指標）を以下に示します。評価指標に問題がなければ、このパイプラインを推論サーバで使用することにします。

```
Accuracy :  0.7611940298507462
Precision:  0.6744186046511628
Recall   :  0.6170212765957447
F-measure:  0.6444444444444444
```

## 4. 学習済パイプラインのデプロイ

作成したパイプラインを推論システムにデプロイするため、pickleファイルに保存します。

```python
import pickle

# パイプラインをファイルに保存する
filename = 'ml_pipeline.pickle'
pickle.dump(ml_pipeline, open(filename, 'wb'))
```

学習サーバに保存したパイプラインのpickleファイルと、特徴量作成処理の定義コードを、推論サーバの下記のパスに配置します。

```shell
/home/<ユーザ>/ml_pipeline.pickle
/home/<ユーザ>/preprocessor.py
```

## 5. APIサーバでパイプラインを利用

推論サーバ上にFlaskを起動して、HTTP API経由でパイプラインを呼び出せるようにします。以下にFlaskサーバのソースファイル`api_server.py`を示します。

```python:api_server.py
import pickle
import pandas as pd
from flask import Flask, request, jsonify

# 自作の特徴量作成処理をimport
from preprocessor import PreProcessor

# ファイルに保存した学習済みパイプラインをロードする
filename = 'ml_pipeline.pickle'
ml_pipeline = pickle.load(open(filename, 'rb'))

# Flaskサーバを作成
app = Flask(__name__)

# エンドポイントを定義 http:/host:port/predict, POSTメソッドのみ受け付ける
@app.route('/predict', methods=['POST'])
def post_predict():
    # Jsonリクエストから値取得
    X_dict = request.json

    # DataFrame化
    X_df = pd.DataFrame([X_dict])

    # パイプラインで予測を実行
    pred = ml_pipeline.predict(X_df)

    # 予測結果をJSON化
    result = {"Survived": int(pred[0])}

    # 予測結果を返信
    return jsonify(result)

if __name__ == '__main__':
    # Flaskサーバをポート番号5060で起動
    app.run(port=5060)
```

上記の`api_server.py`ファイルを推論サーバの下記パスに配置します。

```shell
/home/<ユーザ>/api_server.py
```

そして下記コマンドでAPIサーバを起動します。

```shell
python /home/<ユーザ>/api_server.py
```

## 6. 推論を実行

AI利用サーバから、推論サーバのAPIを呼び出して予測結果を取得してみます。PythonのHTTPクライアントであるurllib.requestを使用して、APIを呼び出して結果を取得するソースコードを以下に示します。

```python:api_client.py
import json
import urllib.request

url = 'http://<推論サーバのホスト名・IPアドレス)>:5060/predict'
headers = {'Content-Type': 'application/json'}

if __name__ == '__main__':

    # 入力データを作成
    data = {
        'PassengerId': 294,
        'Pclass': 1,
        'Name': 'John, Mr. Smith',
        'Sex': 'male',
        'Age': 28,
        'SibSp': 0,
        'Parch': 0,
        'Ticket': 312453,
        'Fare': 18.2500,
        'Cabin': 'NaN',
        'Embarked': 'Q'
    }

    # 予測APIを呼び出し
    req = urllib.request.Request(url, json.dumps(data).encode(), headers)

    # 予測結果を確認
    with urllib.request.urlopen(req) as res:
        body = res.read()
        result = json.loads(body)
        print(result)
```

上記の実行結果（予測結果）を以下に示します。上記の入力データに該当する人間は、タイタニック号で生存できなかった（Survivedが0）と予測されました。

```json
{'Survived': 0}
```

# おわりに

本投稿では、Python製の機械学習フレームワークであるscikit-learnとWebサーバであるFlaskを活用して、機械学習モデルを用いたリアルタイム推論システムを構築する手順を紹介しました。scikit-learnのパイプライン機能を活用することで、学習時に作成したモデルだけでなく、モデルに入力する特徴量を作成する処理についても、推論システムへ簡単に移植（デプロイ）できることを示しました。