scikit-learn を GAE で動かしてみる

よく sklearn を使って予測器を作ることや、GAE の上で API を作ることがあるんですが、
ふと、「GAE の上で sklearn のロジスティック回帰を使って予測API作ってみるか」と思い立ち、やってみました。

## SE では scikit-learn は動かない

GAE には Standard Environment(SE) と Flexible Environment(FE) の二つの環境が用意されています。
これまで SE をメインで使ってきたので、Google さん便利なもん作ったなあなどと関心しておりました。
しかし、 sklearn を入れようとすると、以下のエラーが出ます。


> It seems that scikit-learn has not been built correctly. If you have installed scikit-learn from source, please do not forget to build the package before using it: run python setup.py install or make in the source directory. If you have used an installer, please check that it is suited for your Python version, your operating system and your platform.

scikit-learn が正常にビルドされてないよ、と言われます。
（もしここで「app engine では sklearn 対応してないよ」と言ってくれればもうちょっと早く解決した気がする）

何度も sklearn をビルドし直しては lib フォルダにインストールしてあげるもダメ。
そんなことを1時間ぐらいして再度調べ直したときに、ここの書き込みにたどり着き、サポートしてないことを知ったのでした。
https://stackoverflow.com/questions/35987785/using-scikit-learn-google-app-engine

Cに大きく依存するようなライブラリはサポートされていないようで、 numpy や scipy も古いバージョンまでしか対応していないようです。

## じゃあ FE でやってみよう
Flexible Environment(FE) とは、デプロイ・サービス・スケーリングなどの機能は SE そのままで、アプリケーションの実行に関するカスタマイズが可能な環境です。 SE ではサポートされていないライブラリなどを自由に入れることができる代わりに、 SE では勝手によしなにやってくれていたところを自分で設定したりしないといけなくなります。
ただ、これまで作ってきたものすべてを FE にする必要はなく、カスタムしたいライブラリが必要な部分だけサービスとして切り出すのがいいらしいです。：https://cloud.google.com/appengine/docs/flexible/java/flexible-for-standard-users?hl=ja

### サービスとして切り出す方針

* sklearn のロジスティック回帰を使って2値分類の予測をやる部分を、新しいサービスとして切り出す
    * 今回は、学習済みの sklearn.linear_model.LogisticRegression インスタンスを `model.pkl` に保存されている前提
    * それを呼び出して、リクエストに含まれる特徴データを食わせて [-1, 1] の値を返すサービスにする

#### ディレクトリ構成
    
```
.
|-- original  # 元のサービス（ここに prediction.py 的にクラス作ってモデルを呼びだそうと思っていた）
|   |-- __init__.py
|   |-- app.yaml
|   |-- appengine_config.py
|   |-- lib
|   |-- main.py
|   |-- queue.yaml
|   `-- requirements.txt
|-- prediction # 新しく作る予測するサービス
|   |-- __init__.py
|   |-- app.yaml
|   |-- main.py
|   |-- requirements.txt
|   `-- resources
|       `-- model.pkl
|-- app.yaml
|-- main.py
|-- readme.md
|-- static
    `-- index.html
``` 

#### 切り出したサービスを FE にするには

* `app.yaml` を以下のように指定

    ```prediction/app.yaml
    service: prediction
    runtime: python
    env: flex
    entrypoint: gunicorn -b :$PORT main:app
    
    runtime_config:
      python_version: 2
    ```

    * `runtime:` の指定を `runtime: python27` のようにせず `python` に。
        * そして `runtime_config` でバージョン指定
    * `env: flex` を追加
    * `entrypoint: ` に起動コマンドを指定（詳しくはこちらを参照：https://cloud.google.com/appengine/docs/flexible/python/runtime?hl=ja#application_startup）

* `requirements.txt` に入れたいライブラリとバージョンを指定しておく

    ```prediction/requirements.txt
flask==0.12.1
gunicorn==19.7.1
attrdict==2.0.0
numpy==1.13.1
scikit-learn==0.19.0
scipy==0.19.1
```

* あとは `main.py` でモデルの読み込みと予測をして、値を返す

    ```prediction/main.py
    @app.route("/prediction", methods=['POST'])
    def prediction():
    
        # get payload from request
        payload = request.json
        features = payload["features"]

        # prediction
        clf = joblib.load("resources/model.pkl")
        predicted = clf.predict(features)[0]

        reply = {"predicted": predicted}
        return jsonify(reply)
```

### FE で作ったサービスを呼び出す

* 今回は POST メソッドでリクエストを送り、レスポンスから予測値を取り出す

    ```original/main.py
### (中略) ###
API_URL = "https://[YOUR_PROJECT_DOMAIN].appspot.com/prediction"
payload = {"features": features}
result = urlfetch.fetch(
        url=API_URL,
        payload=json.dumps(payload),
        method=urlfetch.POST,
        headers={'content-type': 'application/json'},
        deadline=5)
result_dict = json.loads(result.content)
predicted = result_dict.get("predicted", -1)
```


## FE に移して感じたこと

### 良かったこと

* **Python 3系が使える**
    * 文字列処理するならやっぱり3系にしたくなる（また使ってないけど
    * FE では好きな runtime で実行できるので、 Python3系でどうしても作業したい場合はオススメ
* **入れたいライブラリはなんでも使える（肌感）**
    * 今のところ、入れたいライブラリは何でもインストールすることができています
* **必要な部分だけ切り出して FE に載せられる**
    * これまで SE で作ってきた全てを FE に変える必要がない
    * ライブラリや runtime をカスタマイズしたい部分をサービスとして切り出して FE 環境に乗せることが上策
    * 再度、マイクロサービスとしての構成を考え直すいい機会になった
    * 切り出した部分とは HTTP や TaskQueue でやりとりすればよい

### 困ったこと

* **とにかくデプロイが遅い…**
    * 一からコンテナつくって立て直して、とやってくれてるのはわかるが、とにかく遅い
    * SE で利用していた `appcfg.py` でのデプロイは利用できず、 `gcloud app deploy` でしかデプロイできない
    * 最初の方は `gcloud app deploy` してから20分ぐらいかかった（今は定常的に5分前後
    * 5分以上かかると違う作業をしはじめてしまうので、「あれ、そういえば dev のデプロイしてたんだった」みたいなことになって作業効率悪くなる
* **ログの吐き場所が stdout か stderr ぐらいしかない**
    * 単純に `logging.info()` などをやればよかった SE 時代が懐かしい
    * logging の出力先を stdout に設定してやれば、 ログビューアの stdout ログで確認できる（ひとまずこれでなんとかなった）
    * fluentd に乗っけるやり方をやれば、stdout ではない自分のログの吐き場所を作って確認できるらしい：https://qiita.com/chidakiyo/items/1452ab1ea83279a8e485


### おわりに

GAE をとても気に入っていたので、 scikit-learn というメジャーなライブラリが使えないとわかった時の驚きはすごかったです。「サポートしてないの?!」と声に出しちゃうくらい。
FE が今年の3月にベータ版を終えて正式リリースされたことがほんとに救いだったなと思いました。
デプロイが遅いのだけがまんすれば、FE はとても柔軟性の高い環境で使いやすいはず。
これからももっと GAE をいじってみようと思います。

※ どこか間違いがありましたらツッコミいただけると助かります。