GoogleAppEngine(GAE)スタンダード環境でscikit-learnを動かしてみた(機械学習、予測)

#はじめに
以下の記事で、GoogleAppEngineスタンダード環境で、scikit-learnを利用することができるようになったとのことで、動作させてみました。
学習したモデルはGCSへ保存したり、モデルを読み込んで予測タスクも実行しています。

* [GAE スタンダード環境で scikit-learn を使う](https://medium.com/google-cloud-jp/gae-standard-with-scikit-learn-dff5ad1a0ea1)

Python 3.7 が動作する第２世代ランタイムでは、gVisor をベースとしたアイソレーション環境により安全性が担保できるため、内部的にCで書かれた Pythonライブラリもロードして使用することができます。

とのこと。



##環境

* GoogleAppEngine StandardEnvironment (Python3.7(ベータ版))
* GoogleCloudStorage
* scikit-learn 0.20.0
* Pickle

##簡易構成図と流れは、以下です。
1. 事前準備(学習データ読み込みと、モデル保存用に、GCSバケット準備)
2. GAEがGCSから学習データを取得
3. 機械学習実行
4. 学習モデルはGCSへ保存
5. 学習モデルを読み込み、予測


![構成図](https://storage.googleapis.com/permanent_koizumi/20181020_gae_ml.png "構成図")


#説明
## 1. 事前準備(GAE設定、学習データ読み込みと、モデル保存用に、GCSバケット準備)
GCS上に学習データを配置しておきます。GAEはそれを読み込んで学習することとします。

そして、GAE実行に、必要なライブラリを読み込んだり、環境準備をしておきます。

```Python:requirements.txt
Flask==1.0.2
google-cloud-storage== 1.7.0
gunicorn==19.8.1
WTForms-Appengine==0.1
pandas==0.23.4
scikit-learn==0.20.0
numpy==1.15.2
```

runtimeでpython37を指定します。また、学習タスクで負荷が高く、デフォルトのF1だとServerError504が発生したので、F2を指定しました。

```Python:app.yaml
runtime: python37
instance_class: F2
```

## 2. GAEがGCSから学習データを取得
GAEがGCSから学習データを取得します。取得したデータはsource_dataに格納します。
（今回、source_filenameはPOST request内のFORMデータに格納するようにしています。）

```Python:main.py(抜粋)
def download_blob(source_blob_name):
    """Downloads a blob from the bucket."""

    storage_client = storage.Client(project=PROJECT_ID)
    bucket = storage_client.get_bucket(CLOUD_STORAGE_BUCKET)
    blob = bucket.blob(source_blob_name)
    file = blob.download_as_string()
    # <class 'bytes'>
    return file

def multi_ml():
    source_filename = request.form['source_file']
    ## source_filename : alldata.csv
    source_data = download_blob(source_filename)
```

## 3. 機械学習実行
まず、取得したデータは、最後の要素が目的変数で、それ以外が説明変数である形式であることを前提とします。

| 説明変数1 | 説明変数2 |  目的変数 |
|---------:|----------:|----------:|
| 10       |  10       | 100       |
| 20       |  20       | 200       |
| 30       |  30       | 300       |

上記のデータを学習用。テスト用のデータフレームへ分けます。パラメータはデフォルト。

```Python:main.py
    import sklearn.model_selection
    df = pd.read_csv(BytesIO(source_data))
    X = df.drop(df.columns.values[len(df.columns)-1], axis=1)
    Y = df.iloc[:, [len(df.columns)-1]]
    X_train, X_test, Y_train, Y_test = sklearn.model_selection.train_test_split(X,Y)
```

今回テストとして、scikit-learnでランダムフォレストアルゴリズムを使います。パラメータはデフォルト。

```Python:main.py(抜粋)
    from sklearn.ensemble import RandomForestRegressor
    rf_reg = RandomForestRegressor()
    rf_reg.fit(X_train, Y_train)
```

##4. 学習モデルはGCSへ保存
上記で学習したモデルを、Pickleでdumpして、名前を指定してGCSへアップロードしておきます。予測で利用します。

```Python:main.py(抜粋)
    model = pickle.dumps(rf_reg)
    url = upload_file(model, 'RandomForestModel.pkl', 'application/octet-stream')
```

```Python:main.py(upload_file機能
def upload_file(file_stream, filename, content_type):
    storage_client = _get_storage_client()
    bucket = storage_client.bucket(CLOUD_STORAGE_BUCKET)
    blob = bucket.blob(filename)

    blob.upload_from_string(
        file_stream,
        content_type=content_type)

    url = 'gs://{}/{}'.format(CLOUD_STORAGE_BUCKET, filename)

    return url
```

学習フェーズはここまで。予測フェーズは以下。

##5. 予測のために、GCSから学習モデルを読み込み
GAEがGCSからモデルを取得し、Pickleでモデルをloadします。
（今回、モデルが格納されているURI(gs://)はPOST request内のFORMデータに格納するようにしています。）

```Python:main.py
    model_uri = request.form['model']
    ## gs://<uploadurl>/Model.pkl
    
    model_data = download_blob(model_uri)
    model = pickle.loads(model_data)
```

予測するための入力データもPOST requestで取得できる前提とします。カンマ区切り。
※この辺りは、学習したデータに合わせてください。

| input_data      |
|:----------------|
| 40,40,40        |

今回テストで、stringデータを入力した関係もあり、str～list～np.arrayへ変換しています。
このあたりは、入力データに合わせて適宜作り替えてください。

```Python:main.py
    input_data = request.form['input']
    input_list = input_data.split(',')
    input_array = np.array([input_list]).astype(np.float64)
```

##6. 予測を実行
あとは、読み込んだモデルを利用して、predictをするだけです。

```Python:main.py
    predict_kekka = model.predict(input_array)
```

結果は適宜利用しましょう。テスト実装では、結果をユーザに返しています。

```Python:main.py
    return render_template('predict_kekka.html', predict_kekka=predict_kekka, message=message)
```

# 参考
[GAE スタンダード環境で scikit-learn を使う](https://medium.com/google-cloud-jp/gae-standard-with-scikit-learn-dff5ad1a0ea1)
[GAE StandardEnvironmentのinstance class](https://cloud.google.com/appengine/docs/standard/#instance_classes)

# おわりに
基本的には機械学習のアーキテクチャではMachineLearningEngine(MLE)を使うものですし、GAEはinstance_classとして、GPUは選択できないので、学習タスクを行うことはあまり適していないと思われます。
ただ、予測タスク(5,6項)を行う分には、GAEは価格も安いので十分に使えるものではないかと思いました。

[コードはgithubに公開しました。](https://github.com/ko-ism/gae_de_ML)