概要
TensorflowでMNISTの手書き文字認識するWebアプリをFlaskで実装し、Cloud Run上で結果を表示するところまで実装する。
モデルの訓練はローカルで行う。
前半はモデルの生成部分。後半はFlask部分の実装とCloud Runでの実行について扱う。
余談
この手の処理だとAI PlatformとかServing等を検討したいところだけど、今回の要件は月に数回だけ実行されて実行時間は数十秒かかっても構わないという要件だったので、それならCloud Runでのんびりモデル読み込んで実行すればいいじゃんという話になりました。一般的にはあまりオススメできない構成だと思います。
AWS Lambdaも検討しましたが、動くことは動くけどDeployment Package Sizeの制限(512MB)が厳しくCloud Runの方が楽に動かせました。
Cloud Runで気になる点はメモリ2GBという制限。そこを超えないサイズのモデルである必要があります。
バージョン情報
- tensorflow==2.0.1
- Flask==1.1.1
プロジェクトのフォルダ構成
Webアプリを動かすappと、モデルの生成を行うtrainの2つのディレクトリ(それぞれ別のDockerfileとrequirements.txtを持つ)を作成する。
. ├── app │ ├── Dockerfile │ ├── app.py │ ├── index.html │ └── requirements.txt ├── docker-compose.yml └── train ├── Dockerfile ├── requirements.txt └── train.py
docker-compose.yml
docker-composeの記述はこんな感じ。
version: '2' services: tf-cloudrun-app: build: app command: "python app.py" user: "${UID}:${GID}" volumes: - ./app:/usr/app/ ports: - 8000:8000 mem_limit: 1800m tf-cloudrun-train: build: train tty: true command: "bash" user: "${UID}:${GID}" volumes: - ./train:/usr/app/
train用の環境はbashだけ立ち上げてtrainのコマンドは中に入って打つ想定。
Cloud Runの制限に引っかからないようにmem_limitを1800m(なんとなくきもち小さめ)にしている。
train/train.py
適当な手書き文字認識のモデルを作るコード。
import tensorflow as tf from tensorflow.keras import layers # load mnist digits (x_train, y_train), (x_val, y_val) = tf.keras.datasets.mnist.load_data() x_train = x_train.reshape([60000, 28, 28, 1]) / 255. x_val = x_val.reshape([10000, 28, 28, 1]) / 255. # model model = tf.keras.models.Sequential([ layers.Convolution2D(64, (4, 4), activation='relu', input_shape=(28, 28, 1)), layers.MaxPooling2D((2, 2)), layers.Dropout(0.1), layers.Convolution2D(64, (4, 4), activation='relu'), layers.MaxPooling2D((2, 2)), layers.Dropout(0.3), layers.Flatten(), layers.Dense(256, activation='relu'), layers.Dropout(0.5), layers.Dense(64, activation='relu'), layers.BatchNormalization(), layers.Dense(10, activation='softmax') ]) model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy']) # tensoboard callback tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir="logs") # fit model.fit( x_train, y_train, epochs=100, validation_data=(x_val, y_val), callbacks=[tensorboard_callback]) # 保存 tf_conv = tf.lite.TFLiteConverter.from_keras_model(model) lite_model = tf_conv.convert() with open('digit_model.tflite', 'wb') as w: w.write(lite_model) # tensorflow 1.4系の場合はこっち # model.save('digit_model.h5') # tf_conv = tf.lite.TFLiteConverter.from_keras_model_file('digit_model.h5') # lite_model = tf_conv.convert() # with open('digit_model.tflite', 'wb') as w: # w.write(lite_model)
train/requirements.txt
requirementsのファイル作ったはいいけど特に追加で入れるものはなかったや。
# 特に記述なし
train/Dockerfile
Dockerfileはtensorflowの公式のイメージを使います。
自前でalpineからセットアップしようかと思ったけど、やってみるといろいろハマりどころが多いしバージョン変わると別のとこでハマるしでいいことがなかったので、公式を使うのが1番という気持ちになりました。
CMDはdocker-compose側で実行するので省略。
FROM tensorflow/tensorflow:2.0.1-py3 COPY . /usr/app/ WORKDIR /usr/app/ RUN pip install -r requirements.txt
trainの実行
.envにUIDとGIDを記述しておく。
echo "UID=`id -u`" > .env echo "GID=`id -g`" >> .env
build
$ docker-compose build
up
$ docker-compose up
trainの実行
$ docker-compose exec tf-cloudrun-train bash $ python train.py
これでスペックにもよるけど小一時間程度でモデルが出来上がります。
app/app.py
続いてwebserver側の実装。
毎回モデルを読み込んでpredictするという不経済なことをやっている。
import io import base64 import flask import flask_wtf import wtforms import numpy as np import tensorflow as tf from PIL import Image app = flask.Flask(__name__, template_folder='.') app.secret_key = 'o-reha-jaian-ga-kidai-sho' app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 @app.route('/', methods=['GET', 'POST']) def index(): result = None image_base64 = None form = FileForm() # POST時はモデルを読み込んで予測処理 if flask.request.method == 'POST' and form.validate_on_submit(): # 画像の読込みと整形 fs = flask.request.files['image'] image = Image.open(fs.stream).convert('L') buf = io.BytesIO() image.save(buf, format="JPEG") buf.seek(0) image_base64 = base64.b64encode(buf.getvalue()).decode() image = np.array(image.resize([28, 28])) / 255. image = image.astype(np.float32).reshape(1, 28, 28, 1) # モデルの読込みと推測 interpreter = tf.lite.Interpreter(model_path="digit_model.tflite") interpreter.allocate_tensors() input_details = interpreter.get_input_details() output_details = interpreter.get_output_details() interpreter.set_tensor(input_details[0]['index'], image) interpreter.invoke() result = interpreter.get_tensor(output_details[0]['index'])[0] # 結果を文字列に整形 result = '推測結果={}, {}'.format(np.argmax(result), list(result)) return flask.render_template( 'index.html', form=form, image=image_base64, result=result) class FileForm(flask_wtf.FlaskForm): image = wtforms.FileField('Image File', [wtforms.validators.DataRequired(message='ファイルを指定してください')]) if __name__ == "__main__": app.run(debug=True, host='0.0.0.0', port=5000)
app/index.html
HTMLテンプレートの記述。
最低限の入力フォームと結果表示だけを記述している。
<form method="post" enctype="multipart/form-data"> {{ form.csrf_token }} {{ form.image(class="form-input") }} <button>解析開始</button> </form> {% if image %} <p><img src='data:image/jpeg;base64, {{ image }}' /> {% endif %} {% if result %} <p>{{ result }} {% endif %}
app/Dockerfile
trainとほぼ同じ内容で、最後にgunicornでサーバーを起動するコマンドを入れています。
FROM tensorflow/tensorflow:2.0.1-py3 COPY . /usr/app/ WORKDIR /usr/app/ RUN pip install -r requirements.txt CMD exec gunicorn --bind :$PORT --workers 1 --threads 2 app:app
app/requrements.txt
gunicornやFlask、Pillow等の必要なライブラリを入れています。
WerkzeugはFlaskの依存で入るものだと思うけど、これを記述している時点でバージョンの噛み合わせでエラーが起こったのでバージョン指定して動くようにしています。
gunicorn==19.9.0 Flask==1.1.1 Flask-WTF==0.14.2 Cython==0.29.15 Pillow==6.0.0 Werkzeug==0.16.1
cloud runへのbuild
下記を実行。名前は適当にhelloにしています。
$ PROJECT_ID="ここに利用しているproject idを記述" $ cd app # Dockerfileを配置したappディレクトリで実行する $ gcloud builds submit --tag gcr.io/$PROJECT_ID/hello
これでContainer Registryにイメージが登録される。
続いてイメージをcloud runにデプロイ。
$ gcloud beta run deploy --image gcr.io/$PROJECT_ID/hello
実行すると下記のような選択肢がでるので1を選択して続行。
[1] Cloud Run (fully managed) [2] Cloud Run for Anthos deployed on Google Cloud [3] Cloud Run for Anthos deployed on VMware [4] cancel
リージョンはasia-northeast1(東京)を選択。
Please specify a region: [1] asia-east1 [2] asia-northeast1 [3] europe-north1 [4] europe-west1 [5] europe-west4 [6] us-central1 [7] us-east1 [8] us-east4 [9] us-west1 [10] cancel
デプロイが終わるとURLが表示されるので、そこにリクエストして利用する。
コールドスリープなので1回目のリクエストは少し時間がかかります。
自前で描いた数字(黒背景)をアップロードすると、こんな画面が表示されます。
後片付け
cloud runのサービスの削除
$ gcloud beta run services delete hello
Container Registryからの削除
$ gcloud container images delete gcr.io/$PROJECT_ID/hello
改定履歴
Author: Masato Watanabe, Date: 2020-04-06, 記事投稿