Edited at

[初心者向け]herokuを使って爆速でAIを使ったサービスを作る


はじめに

前回の続きです。

https://qiita.com/pnyompen/items/412734d244d7ebb45ca7

Neuralnetを使ったwebサービスを作る際、私が行っている公開方法を紹介します。

実際にNeuralNetを使ったサービスを作ってみます。


herokuとは

heroku

Herokuとは、Webアプリケーションの開発から公開までを簡単にできるプラットフォームです。

プログラムが動作可能な環境を提供してあげますよ!ということですね。

ありがたや。

引用元: https://qiita.com/Arashi/items/b2f2e01259238235e187


公開方法いろいろ

herokuではGPUが使えないのでこんな感じでモデルを公開します。

この方法でも、1リクエスト/secくらいならさばけるので、流行る予定のないサービスなら十分だと思います。

推論の際は、サーバーのリソースを多く消費するため、Webアプリケーションサーバと、NeuralNetの推論用APIサーバーを分けて作ります。


  1. kerasでモデルを読み込んで使う

  2. TensorFlowのsaved_modelを読み込んで使う

  3. TensorFlow Servingを使う

  4. TensorFlowLiteに変換して使う


学習済みモデルの準備

公開するモデルが無いと始まらないので、jupyterとKerasを使って、ministのクラス分類モデルをさくっと学習させましょう。


モジュールのインポート

# 必要なライブラリをインポート

import keras
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras import backend as K


データの準備

num_classes = 10

img_rows, img_cols = 28, 28

# minist読み込んでone-hotに整形、画像も正規化
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train = x_train.reshape(x_train.shape[0], img_rows, img_cols, 1)
x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, 1)
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255

input_shape = (img_rows, img_cols, 1)


モデルのコンパイル

# モデルは畳み込み2層、maxpooling1層、全結合層2層

model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3),
activation='relu',
input_shape=input_shape))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))

# optimizerはAdam
optimizer = keras.optimizers.Adam()
model.compile(loss=keras.losses.categorical_crossentropy,
optimizer=optimizer,
metrics=['accuracy'])

model.summary()


サマリー

こんな感じ

_________________________________________________________________

Layer (type) Output Shape Param #
=================================================================
conv2d_1 (Conv2D) (None, 26, 26, 32) 320
_________________________________________________________________
conv2d_2 (Conv2D) (None, 24, 24, 64) 18496
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 12, 12, 64) 0
_________________________________________________________________
dropout_1 (Dropout) (None, 12, 12, 64) 0
_________________________________________________________________
flatten_1 (Flatten) (None, 9216) 0
_________________________________________________________________
dense_1 (Dense) (None, 128) 1179776
_________________________________________________________________
dropout_2 (Dropout) (None, 128) 0
_________________________________________________________________
dense_2 (Dense) (None, 10) 1290
=================================================================
Total params: 1,199,882
Trainable params: 1,199,882
Non-trainable params: 0
_________________________________________________________________


トレーニング

batch_size 128で、7epoch

batch_size = 128

epochs = 7
model.fit(x_train, y_train,
batch_size=batch_size,
epochs=epochs,
verbose=1,
validation_data=(x_test, y_test))


結果

Test loss: 0.025920250408061476

Test accuracy: 0.9922

きちんと学習できているようです。

score = model.evaluate(x_test, y_test, verbose=0)

print('Test loss:', score[0])
print('Test accuracy:', score[1])


モデルの保存

model.save('my_model.h5')


1. kerasでモデルを読み込んで使う

先程保存したh5をそのまま読み込んで使います。

変換などの手間がないのでKerasで作った場合は一番簡単な方法です。

まずはflaskを使ってAPIサーバーを作ります


作業環境を作る

適当なフォルダを作って、その中に以下のPipfileを作ります


Pipfile

[[source]]

name = "pypi"
verify_ssl = true
url = "https://pypi.org/simple"

[packages]
gunicorn = "*"
Flask = "*"
numpy = "*"
tf-nightly = "==1.13.0.dev20190104"
keras = "*"

[requires]
python_version = "3.7"

[dev-packages]

[scripts]
dev = 'python app.py'


新しく環境を作ります

エラーが出る場合はtf-nightlyをtensorflowにしてください。

pipenv install


flaskでAPIを作る

リクエストごとに毎回モデルを読み込んで破棄する方法もあるのですが、モデルの読み込みに時間がかかるので、グローバルで読み込みます。

このとき、モデルがロードされているgraphを推論の際にも使わないとエラーが出るので、with graph.as_default()でgraphを指定してあげます。


app.py

#!/bin/env python

# coding: utf-8

import os
from flask import Flask, request, jsonify, send_file
import numpy as np
import keras
from keras.models import load_model
import tensorflow as tf

app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False
app.debug = True

model = load_model('my_model.h5')
graph = tf.get_default_graph()

@app.route('/', methods=['POST'])
def predict():
img_list = request.json.get('images')
img_list = np.array(img_list, dtype=np.float32)
global graph
with graph.as_default():
prediction = model.predict(img_list)
response = {
'data': {
'prediction': np.argmax(prediction, axis=1).tolist()
}
}
return jsonify(response), 200

if __name__ == '__main__':
port = int(os.environ.get('PORT', 8080))
app.run(port=port)



heroku用設定ファイルの作成

ローカルでテストしたら早速herokuにデプロイして動かしてみましょう

必要な設定ファイルを作って


runtime.txt

python-3.7.1



Procfile

web: gunicorn app:app



デプロイ

新規にアプリを作ってデプロイします。

{APP_NAME}は好きな名前で作ってください

グローバルにモデルをロードしているので、インスタンスが複数個できてしまうと、次のような問題を生じてしまいます。


  • インスタンスが生成されるたびにモデルがロードされるため、ロードに時間がかかる

  • モデルが巨大な場合メモリに収まらなくなり、Swapが発生してレスポンスが遅くなる

インスタンスが複数立ち上がらないように環境変数にWEB_CONCURRENCY=1を設定します

heroku create {好きなアプリの名前(以下APP_NAME)}

heroku config:set WEB_CONCURRENCY=1 -a {APP_NAME}
git add .
git commit -m 'first commit'
git push heroku master


テスト

jupyterなどを使ってテストします。

7が返ってくれば成功です

from keras.datasets import mnist

import requests

img_rows, img_cols = 28, 28

(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, 1)
x_test = x_test.astype('float32')
x_test /= 255

data = {
'images': x_test[:1].tolist()
}
res = requests.post(API_URL, json=data)
res.json()

# {'data': {'prediction': [7]}}


2.TensorFlowのsaved_modelを読み込んで使う

先程の方法でも良いのですが、Kerasを使わないので、saved_modelを使ったほうがより高速です。


saved_modelに変換

import tensorflow as tf

import keras
from keras.models import load_model, Model
import keras.backend as K

model = load_model('my_model.h5')
export_path = 'my_model'
!rm -r {export_path}

prediction_signature = tf.saved_model.signature_def_utils.predict_signature_def(
inputs={'images': model.input},
outputs={'classes': model.output})

builder = tf.saved_model.builder.SavedModelBuilder(export_path)
builder.add_meta_graph_and_variables(
sess=K.get_session(),
tags=[tf.saved_model.tag_constants.SERVING],
signature_def_map = {
tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY: prediction_signature
})
builder.save()


APIをsaved_model用に修正

globalで定義したsessionをそのまま使っているので、with graph.as_default()は必要ありません。


app.py

# 削除

import keras
from keras.models import load_model
model = load_model('my_model.h5')

# 追加
export_path = 'my_model'
sess = tf.InteractiveSession(graph=graph)
model =tf.saved_model.loader.load(sess, [tf.saved_model.tag_constants.SERVING], export_path)
inputs_name = model.signature_def[tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY].inputs['images'].name
inputs = graph.get_tensor_by_name(inputs_name)
outputs_name = model.signature_def[tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY].outputs['classes'].name
outputs = graph.get_tensor_by_name(outputs_name)

# 削除
global graph
with graph.as_default():
prediction = sess.run(outputs, feed_dict={inputs: img_list})

# 追加
prediction = model.predict(img_list)



テスト

先程のテストプログラムで7が帰ってくれば成功


Tensorflow Servingを使う


Tensorflow Servingとは

Tensorflowで作られた学習済みモデルを公開するためのソフトウェアです。

複数のモデルや、バージョンの管理なども簡単に行えて、なおかつ高速に動くらしいです。

とりあえず使ってみましょう。

https://www.tensorflow.org/serving/


Dockerfileを作る

https://hackernoon.com/tf-serving-keras-mobilenetv2-c167b4b2bb25

ここを参考に作っていきます


Dockerfile

# Copyright 2018 Google LLC

#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

ARG TF_SERVING_VERSION=1.11.1
ARG TF_SERVING_BUILD_IMAGE=tensorflow/serving:${TF_SERVING_VERSION}-devel

FROM ${TF_SERVING_BUILD_IMAGE} as build_image
FROM ubuntu:16.04

ARG TF_SERVING_VERSION_GIT_BRANCH=r1.11
ARG TF_SERVING_VERSION_GIT_COMMIT=head

LABEL maintainer="mohammed.alnakli@gmail.com"
LABEL tensorflow_serving_github_branchtag=${TF_SERVING_VERSION_GIT_BRANCH}
LABEL tensorflow_serving_github_commit=${TF_SERVING_VERSION_GIT_COMMIT}

RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
&& \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

# Install TF Serving pkg
COPY --from=build_image /usr/local/bin/tensorflow_model_server /usr/bin/tensorflow_model_server

# Expose is NOT supported by Heroku

# Expose ports
# gRPC
# EXPOSE 8500

# REST
# EXPOSE 8501

# Set where models should be stored in the container
ENV MODEL_BASE_PATH=/models
RUN mkdir -p ${MODEL_BASE_PATH}
WORKDIR ${MODEL_BASE_PATH}
# The only required piece is the model name in order to differentiate endpoints
ENV MODEL_NAME=my_model

COPY models/my_model my_model

# Create a script that runs the model server so we can use environment variables
# while also passing in arguments from the docker command line

# $PORT is set by Heroku
RUN echo '#!/bin/bash \n\n\
tensorflow_model_server --rest_api_port=$PORT \
--model_name=${MODEL_NAME} --model_base_path=${MODEL_BASE_PATH}/${MODEL_NAME} \
"$@"' > /usr/bin/tf_serving_entrypoint.sh \
&& chmod +x /usr/bin/tf_serving_entrypoint.sh
# CMD is required to run on Heroku
CMD ["/usr/bin/tf_serving_entrypoint.sh"]



コンパイル

docker build -t tf-serving-heroku .


ローカルでテスト

docker run -p 8501:8501 -e PORT=8501 -e MODEL_NAME=my_model -t tf-serving-heroku

API_URL = 'http://localhost:8501/v1/models/my_model/versions/0:predict'

data = {
'inputs': {
'images': x_test[:1].tolist()
}
}
headers = {"content-type": "application/json"}
res = requests.post(API_URL, json=data, headers=headers)
res.json()


デプロイ

heroku container:login

heroku container:push web -a ${APP_NAME}
heroku container:release web -a ${APP_NAME}


4.TensorFlowLite

TensorflowLiteは現在nightly buildでしか提供されていないため、tf-nightlyをインストールします。


TensorflowLiteとは

精度を8bitにしてるから、最大3倍早くって、予測精度の劣化も少ない!

https://github.com/tensorflow/tensorflow/blob/master/tensorflow/lite/tutorials/post_training_quant.ipynb


変換

ここを参考にsaved_modelをtensorflowLiteに変換します。

https://github.com/tensorflow/tensorflow/blob/master/tensorflow/lite/tutorials/post_training_quant.ipynb

実際はエラーがでて変換ができません。

Kerasモデルを変換するとうまくいかないことが多いみたいです。

import tensorflow as tf

saved_model_dir = "my_model"
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
tflite_model = converter.convert()
open("my_model.tflite", "wb").write(tflite_model)

使うときはこんな感じ

quantized_model_path = 'my_model.tflite'

interpreter = tf.lite.Interpreter(model_path=quantized_model_path)
interpreter.allocate_tensors()
input_index = interpreter.get_input_details()[0]["index"]
output_index = interpreter.get_output_details()[0]["index"]

interpreter.set_tensor(input_index, x_test[:1])
interpreter.invoke()
predictions = interpreter.get_tensor(output_index)


速度の比較

リクエストを100回送るのにかかった時間です。


  • keras: 86.27s

  • saved_model: 84.96s

  • tf-serving: 93.15s

今回のような小さなモデルだとあまり違いはありませんが、大きなモデルな場合だと、herokuではsaved_modelを使った方法が一番速いように感じます。

もっときちんとした環境なら、Cで書かれているTensorFlowServingが一番速くなると思います。


実際にこの方法で動かしているサービス

https://www.youjo-generator.tokyo/

サイトレンダリングと、静的ファイルの配信用に1dyno

ニューラルネットの推論用に1dyno使っています。


おわり

記事自体のクオリティがかなり荒く申し訳ないですが何方かの参考になれば幸いです。