Cloud ML Engine に自動で 予測モデルをデプロイするパイプラインを作ってみた

この記事は[Brainpad Advent Calendar 2018](https://qiita.com/advent-calendar/2018/brainpad)の7日目の記事です。

#### 自己紹介と機械学習基盤を簡単に
初めまして、えいりんぐーです。
データ分析や機械学習まわりの仕事をしています。

このテの仕事をしていて感じるのは、基盤づくり・環境づくりって大切だということです。とくに、アドホックに分析や予測モデルを作って試すだけなら各々好きなようにやっていいのですが、仕事としてチームで、データ分析とその共有や機械学習サービスの開発・運用をやるには、ある程度定型化された環境というものが必要だと思います。

最近では[DevOps](https://ja.wikipedia.org/wiki/DevOps)の文脈から派生して、[MLOps](https://ml-ops.connpass.com/event/73814/)という言葉も聞くようになりまして、もろもろの事業会社さんを見ると、やはりそれぞれ取り組みはあるようです。

* [メルカリさんの事例](https://tech.mercari.com/entry/2018/08/28/152438)
* [Wantedlyさんの事例](https://www.wantedly.com/companies/wantedly/post_articles/82967)
* [リクルートライフスタイルさんの事例](https://engineer.recruit-lifestyle.co.jp/techblog/2018-10-04-ml-platform/)
* [Cookpadさんの事例](https://speakerdeck.com/ayemos/machine-learning-platform-at-cookpad-2018)

私もそれっぽいことなんかやりたいな〜、個人でちょこっと遊べるものないかな〜ということで、今回はGoogle Cloudのサービスの一つである Cloud Machine Learning Engine (以下、ML Engine) に予測モデルを自動でデプロイするパイプラインを作ったお話しをします。

#### 題材はHouse Price Predictionで
主目的は機械学習モデル[^1]の学習とデプロイプロセスをそれっぽく作ることなので、めちゃくちゃリッチにするつもりはありません。とりあえず、これまたお遊びでさわっていた[KaggleのHouse Price Prediction](https://www.kaggle.com/c/house-prices-advanced-regression-techniques)のコードがありますので、それを流用します。

筋書きとしては、
1. 既存の予測モデルが稼働している。
2. 新しいデータが取得されたので、データを検証の上、モデルを再学習する
3. 主要な性能指標を評価して、モデルをGoogle Cloud Storageに保存する
4. 保存されたモデルをML Engineにデプロイする
といった流れになっています。

#### 参考資料とリポジトリなど
Airflowは[公式のドキュメント](https://airflow.apache.org/)、Cloud ML Engineについては[こちら](https://cloud.google.com/ml-engine/docs/scikit/using-pipelines-for-preprocessing)を参考にして進めていきました。また、今回書いたコードは[GitHub](https://github.com/ieiriyuki/ml-engine)に公開しています。以降では、このリポジトリをクローンしたディレクトリが、カレントディレクトリだと仮定して話を進めます。

#### とりあえず開発環境を
Google Cloudのコンソールをポチポチするのも手間なので、認証用のサービスアカウントこそコンソールで作成しますが、それ以降はgcloudコマンドを使います。ただ、Google Cloud SDKはPython2系に依存している一方で、今回使いたいコードはPython3系なので、ローカルではちょっと設定がややこしくなります。

そこで今回は、gcloudコマンドが入った[コンテナー](https://hub.docker.com/r/google/cloud-sdk/)を使います。このイメージは`debian:stretch`ベースでpython3が入っていませんので、他に必要なモジュールも合わせてインストールします。

```dockerfile:Dockerfile
FROM google/cloud-sdk:226.0.0

RUN apt-get -qy update \
    && apt-get -qy install python3 python3-pip

# ~~~ skip ~~~

RUN pip install \
        enum==0.4.7 \
        scipy==1.1.0 \
        scikit-learn==0.19.2 \
        tensorflow==1.12.0 \
    && pip3 install -r requirements.txt

CMD ["python3"]
```

上記のイメージをビルドして、コンテナー内で作業します。5000ポートは[Mlflow](https://mlflow.org/)用、8080ポートはAirflow用です。

```bash
docker build -t houseprice:1.0.0 .
docker run -it --rm -p 5000:5000 -p 8080:8080 houseprice:1.0.0 /bin/bash
```

コンテナー内ではひとまず、[GCPの認証と初期設定](https://cloud.google.com/ml-engine/docs/scikit/using-pipelines-for-preprocessing#gcp-)を行ってください。


#### 一連の作業フローはAirflowに 
ジョブ実行ツールには、古き良きcron、[Digdag](https://qiita.com/hiroysato/items/d0fe5e2d88c267413a82)、[Luigi](https://www.rco.recruit.co.jp/career/engineer/blog/akiba-python-luigi-01/)などがあります。今回はたまたま使っている人が身近にいたので、Airbnbが開発した[Airflow](https://airflow.apache.org/)を使います[^2][^3]。Airflowに関しては、[誰かが書いてくれる](https://qiita.com/advent-calendar/2018/brainpad)はずです...

Airflowを初期化、起動すればサーバーが立ち上がります

```bash
airflow initdb
airflow webserver -p 8080 2>&1 > /airflow/logs/webserver.log &
airflow scheduler 2>&1 > /airflow/logs/scheduler/scheduler.log &
```

Airflowは記述したジョブをDAGの形で表示・実行できます。
<img width="1209" alt="airflow.png" src="https://qiita-image-store.s3.amazonaws.com/0/267335/92f643a1-89ec-03b5-dc88-553e704278c9.png">

こちらでは、
1. inspection: データの型チェック
2. alignment: 列の並べ替え
3. split: 学習とバリデーションデータの分割
4. train: 学習とモデルや指標の保存
5. prediction: ローカルで予測が実行できることを確認
6. put: Cloud Storageに保存
7. create: ML Engineにバージョンの作成
8. REST: REST API形式で予測の確認
というジョブを順に実行しています。

実際には、このあたりの書き方・作り方はデータサイエンティストとエンジニアとできちんと設計・開発・検証するところですね。今回は再学習からデプロイまでを一連のジョブに落とし込むだけでよかったので、お手軽に済ませています。

#### モデルと指標管理はMLflowに
[MLflow](https://www.mlflow.org/docs/latest/index.html)は機械学習システムをメンテする際の一つのプラットフォームです。MLflowはモデル、パラメータ、性能指標の管理や、エクスペリメントと呼ばれる、まとまった処理の実行ができます。サーバーを起動した初期状態[^4]では次のようになっており、train.pyによって性能指標が生成されているのがわかります[^5]。
<img width="1099" alt="mlflow.png" src="https://qiita-image-store.s3.amazonaws.com/0/267335/f0d8173d-53e8-a60a-4f4b-d04c00f2a504.png">

実行の詳細を追うと、モデル情報の詳細が記録されているのもわかります。
<img width="923" alt="mlflow.2.png" src="https://qiita-image-store.s3.amazonaws.com/0/267335/0461d6b8-19b6-c045-49fe-fc986a78775e.png">

#### Google Cloudでは
Google CloudのStorageとML Engineでは下準備として、既に古いモデルがアップロード・デプロイされていると想定します。
<img width="954" alt="gcs.1.png" src="https://qiita-image-store.s3.amazonaws.com/0/267335/ca156f66-672c-da04-aa45-d27fb1a343be.png">

<img width="780" alt="mlengine.1.png" src="https://qiita-image-store.s3.amazonaws.com/0/267335/3665fcd4-0107-6969-08e1-d6118bbbe341.png">

#### 動かしてみると
Airflowをキックすると、新しいデータによる学習と、作成された新しいモデルの保存・デプロイが進みます。
<img width="941" alt="airflow.2.png" src="https://qiita-image-store.s3.amazonaws.com/0/267335/fdda70c2-fc82-ec4b-3c42-aa5184b7ed4c.png">

モデルのデプロイは、Airflowのジョブとして次のコマンドを実行するようにしています。

```python:dags.py
t60 = BashOperator(
    task_id='gcloud_ml-engine_create_version',
    depends_on_past=True,
    bash_command='gcloud ml-engine versions \
        create {{ params.version_name }} \
        --model {{ params.model_name }} \
        --origin {{ params.model_dir }} \
        --runtime-version=1.10 \
        --framework "SCIKIT_LEARN" \
        --python-version="3.5"',
    params={ 'version_name': VERSION_NAME,
             'model_name': MODEL_NAME,
             'model_dir': DEPLOYMENTURI },
    dag=dag
)

```

その結果、ちゃんとモデルはStorageに保存されており、ML Engineにデプロイされていることが分かります。
<img width="935" alt="gcs.2.png" src="https://qiita-image-store.s3.amazonaws.com/0/267335/ea8489cf-33f2-35b9-12fd-4e5857558bcf.png">

<img width="973" alt="mleigine.2.png" src="https://qiita-image-store.s3.amazonaws.com/0/267335/d10adc0e-5175-c2e7-69ce-ad2c27630bfc.png">


またMLflowのほうにも新しいモデルとその性能指標が記録されているのが分かります。
<img width="918" alt="mlflow.3.png" src="https://qiita-image-store.s3.amazonaws.com/0/267335/75d18b2c-3fb8-4553-8a66-9c29e449a483.png">

新しくデプロイされたモデルに対して、curlでポストリクエストすると、予測が帰ってきます。

```bash
curl -X POST -H "Content-Type: application/json" \
>    -d @data/prepared/test.json \
>    -H "Authorization: Bearer `gcloud auth print-access-token`" \
>       "https://ml.googleapis.com/v1/projects/${PROJECT_ID}/models/${MODEL_NAME}/versions/v1_0_5:predict"
{"predictions": [165099.27191280792, 184508.264037045, 175908.7052910972, 176725.19047667703, 140080.75051509973, 171560.74454709014, 165292.94862796998, 171263.10326839934, 152066.53775823937, 154048.07134814674]}
```

無事に予測モデルがデプロイされたのが分かります。

#### まとめ
今回は、予測モデルを再学習するパイプラインをご紹介しました。
実際には、データベースを設計したり、Airflowのサーバーを切り離したり、学習の過程でCVを回したりと色々と検証しなければならないところは多いですが、個人でお試しで作るにはなかなか楽しめる題材でした。

ML EngineはモデルをデプロイするだけでREST APIを提供できるので、簡単に機械学習システムのバックエンドを作ることができます。そのため、ある程度モデルの再学習の仕組みや管理が定まっているとすると、お手軽手はありましたが、今回作成したようなパイプラインを設計するだけで、機械学習システムの継続的なデプロイに結びつけることができます。フレームワークとしてはscikit-learnだけでなく、TensorFlowやXGBoostも利用できる[^6]ので、みなさんもぜひ試してみていただければと思います。お読みいただきありがとうございました。


[^1]: といっても中身はお手軽線形モデルですが。
[^2]: 楽しいけど、今回扱ったようなお手軽サービスには少々リッチなフレームワークでした (^^;
[^3]: Cloud Composerは使わないのか、ですか?ローカルでごちゃごちゃ試したかったのです...
[^4]: こういう初期状態であるという下準備をしています。
[^5]: 今回はパラメーターチューニングはしていません。
[^6]: なお、Cloud ML Engineも[ランタイムによってサポートするフレームワーク、Pythonとモジュールのバージョンが異なる](https://cloud.google.com/ml-engine/docs/scikit/runtime-version-list)ので注意が必要です。