AutoML Tablesの学習済みモデルをSageMakerにデプロイする

## はじめに

**注意：この記事は2019/11/22時点での情報です。**

LIFULLでは機械学習のサービング基盤にSageMakerを利用しています。
一方、LIFULLでは、データがBigQueryに集約される方針で、実際に集約されつつあるので、BQのデータを手軽に渡せるAutoML Tablesは簡単に試せて便利です。

AutoML Tablesにモデルのエクスポート機能がついたので、SageMakerのような別の環境で動かすできるようになりました。
エクスポート機能の使い方は[公式](https://cloud.google.com/automl-tables/docs/model-export)に書いてあります。

LIFULLではメインサービスがAWSで動いているためデプロイはAWSである方が都合が良いです。
ですので今回は、AutoML Tables公式のDockerイメージをSageMakerで動くようにしてみました。

## やったこと

### 要件の確認
SageMakerで動くDockerイメージの要件は最低以下を満たす必要があります。

- エンドポイント
  - `/ping`にGETされた時、ステータス`200`を返すこと
  - `/invocations`にPOSTされた時にモデルへの入力を受け取り60秒以内に予測結果を返すこと
- モデルの読み込み場所
  - `/opt/ml/model`以下にモデルが配置されるのでそこからモデルを読み取れるようにすること
- port
  - `8080`番でリクエストを受け付けること

以上を満たせば、とりあえず動くようになります。

### イメージの使い方の確認
AutoML Tablesで公式が提供するイメージの使い方を確認します。
先ほどの[公式](https://cloud.google.com/automl-tables/docs/model-export)を参考にします。

イメージは`gcr.io/cloud-automl-tables-public/model_server`を使うようなのでとりあえずpullします。

ボリュームは`-v $(pwd)/model-export/tbl/new_folder_name:/models/default/0000001`とあるので、エクスポートしてダウンロードしたモデルを`models/default/000001`にセットすればいいようです。
(あとでわかりますが、ここの000001は数字であればこの数字でなくても動きます。)

ポートは`-p 8080:8080`なのでそのまま利用できそうです。

リクエストは`curl -X POST --data @/tmp/request.json http://localhost:8080/predict`とあるように
`/predict`に対して送るようです。

### 修正する点の確認

- ポートはそのままで良さそう。
- モデルの読み込み場所は変える必要がある。
- エンドポイントは`/predict`を`/invocations`に変更し`/ping`を生やす必要がある。

### 具体的に調べる

やることがわかったので一つずつ対処していきます。
**イメージのソースを見つけられなかった**のでとりあえず動かして挙動を確認していきます。

とりあえず、公式通りに起動してentrypointを確認すると

```bash
/usr/bin/tf_serving_entrypoint.sh
```
を起動していることがわかります。(起動せずに確認する方法はあるのだろうか？)

中に入って確認していきます。

```bash
docker run --rm -it --entrypoint bash gcr.io/cloud-automl-tables-public/model_server
```

`/usr/bin/tf_serving_entrypoint.sh`から追っていった結果、
`/prediction_server.py`や`/translate.py`が重要であることがわかりました。

`/usr/bin/tf_serving_entrypoint.sh`では`tensorflow_model_server`を起動し`/prediction_server.py`を実行していることがわかります。

```bash
tensorflow_model_server --port=8500 --rest_api_port=8501 --model_name=${MODEL_NAME} --model_base_path=${MODEL_BASE_PATH}/${MODEL_NAME} "$@"
```
ここで`tensorflow_model_server`にモデルのパスを指定しています。
都合よく環境変数で指定されているのでここは弄らず、あとで環境変数で場所を`/opt/ml/model`になるように変更します。

`/prediction_server.py`ではtornadeを起動してリクエストとレスポンスを加工する処理を挟んでいるようです。これも都合がいいのでここで`/ping`と`/invocations`を作っていきます。

変更点した点を書いておきます。

```prediction_server.py

class PingHandler(tornado.web.RequestHandler):

    def get(self):
        self.set_header("Content-Type", "application/json")
        self.set_status(200)
        self.write({'status': 'ok'})

def make_app():
    return tornado.web.Application([
        (r"/invocations", PredictHandler),
        (r"/ping", PingHandler),
    ])
```

これでエンドポイントは要件を満たしました。🎉

あと、私が作業した時点のイメージ(tag:`11_21_2019`)では`/translate.py`にバグがあったので修正した点を書いておきます。

```translate.py
import os
...

def user_request_to_tf_serving_request(body):
    ...

    model_spec = model_pb2.ModelSpec(
        name=os.environ['MODEL_NAME'], signature_name="serving_default")
    ...

def tf_serving_response_to_user_response(resp, batch_size):
    ...

    values = [{} for _ in range(batch_size)]
    for key, value in outputs.items():
        for i, v in enumerate(value.tolist()):
            values[i][key] = v
```

`name=os.environ['MODEL_NAME']`の部分はモデルの場所を変更する都合で元の`name="default"`だと読み込めなくなるためです。
あと、元々は`values = [{} for _ in range(batch_size)]`の部分は`values = [{}] * batch_size`と書いてあり、複数個のデータをリクエストした場合に最後の結果で全て上書きされてしまう状態でしたので修正しました。

### Dockerイメージのビルド

修正を反映したイメージを作成します。

作業ディレクトリ

```
./
├ /override
│ ├ prediction_server.py
│ └ translate.py
└ Dockerfile
```

/overrideの中に先ほど修正した内容のファイルを入れました。

```Dockerfile
FROM gcr.io/cloud-automl-tables-public/model_server:11_21_2019

ENV LC_CTYPE="C.UTF-8"

ENV MODEL_BASE_PATH="/opt/ml" \
    MODEL_NAME="model" \
    PORT="8080"

COPY ./override/ /

ENTRYPOINT [ "/usr/bin/tf_serving_entrypoint.sh" ]
```

ここで先ほど保留にした、モデルディレクトリの要件も満たしました。🎉
イメージのタグは以下で確認し固定しました。
https://console.cloud.google.com/gcr/images/cloud-automl-tables-public/GLOBAL/model_server

### デプロイして確認
作ったイメージをECRにpushしてSageMakerで確認していきます。

ここで重要になるのはs3に置くモデルの圧縮ファイルです。
`/opt/ml/model`直下に置くモデルディレクトリは数字でないといけないので、AutoML Tablesからダウンロードしたモデルをリネームする必要があります。

具体的に言うと、以下の感じ。

```
/1
├ /assets
├ /variables
└ save_mode.pb
```
あとはこれを`tar zcvf ./1 model.tar.gz`してs3に置きそれをSageMakerでモデルに指定してエンドポイントを立てます。

エンドポイントを正常に起動できればミッションコンプリートです。お疲れ様でした。