ディープラーニングの画像認識モデルをクラウド上に置いて、
スマホやパソコンから画像を送信すると、認識結果を返してくれるものを作ります。
具体的には、今回はLINEのチャットボットに写真を送ると、画像認識結果を返すようにしました。
LINE BOT自体は単なる使用例であり、画像認識モデル自体も今回はサンプルをそのまま使います。
TensorflowモデルをクラウドAPI化するというところがメインです。
画像認識モデル
ここでは、写真を送ると何が写っているかを認識するものを扱います。
もし認識対象が一般的なものなら、GoogleのCloud Vision APIなどを使うのが良さそうです。
例えば、自転車とか、テーブルとか。
こういう一般的なものは既に他の人が学習させているので、自分でやる必要はなさそうです。
では、認識対象が一般的なものでない場合はどうでしょうか。
例えば、写真にGoogle Homeが写っているか、Amazon Echo(Alexa)かとか。(いずれもスマートスピーカーの機種です)
こんなのは、たぶんGoogleの汎用のモデルには用意されていないでしょう。
そんなときは、GoogleのAutoML Visionというものを使えば、そんなカスタムの画像認識モデルが簡単に作れます。画像をアップロードするだけ。
また、過去の記事で紹介したとおり、M5StickVでも同様にお手軽なツールが用意されています。
ですが、学習の際に細かいところをいじりたい場合もあるでしょうし、もう少し基礎的な部分も触ってみたい。
そこで、Tensorflowを使ってモデルをつくります。メジャーなプラットフォームです。
学習・デプロイ環境
学習環境は、お手軽なものとしてGoogle Colabを使います。
学習させたモデルをクラウド上にデプロイするには、AI Platformを使います。
AI Platformはデプロイだけでなく、学習から何から出来るプラットフォームのようですが、とりあえず今回はデプロイだけに使います。
Tensorflowモデルの準備・ダウンロード
コード
from __future__ import absolute_import, division, print_function, unicode_literals
try:
%tensorflow_version 2.x
except Exception:
pass
import tensorflow as tf
from matplotlib import pyplot as plt
import numpy as np
# ---- 学習済みモデルを読み込み ----
model = tf.keras.applications.MobileNetV2()
# ---- 学習済みモデルに入力・出力の変換処理を加えたものをつくる ----
@tf.function(input_signature=[tf.TensorSpec(shape=[None], dtype=tf.string)])
def serving(input_images):
def _img_to_array(img_base64):
img = tf.io.decode_base64(img_base64)# tensor string -> tensor string
img = tf.io.decode_image(img, dtype=tf.float32)
img.set_shape((None, None, 3)) # これがないと下のresize()で"no shape"エラー
img = tf.image.resize(img, (224, 224), method="nearest")
x = tf.keras.applications.mobilenet.preprocess_input(
img[tf.newaxis,...])
# 正規化
x_fl = tf.reshape(x, [-1])
x_min = x_fl[tf.argmin(x_fl)]
x_max = x_fl[tf.argmax(x_fl)]
x = ((x - x_min) *2 / (x_max - x_min)) -1
return x
with tf.device('/cpu:0'): # これがないと”non-DMA-copy..."エラー (https://github.com/tensorflow/tensorflow/issues/28007)
x = tf.map_fn(_img_to_array, input_images, dtype=tf.float32)
x = x[0,:,:,:]
prediction = model(x)
def _convert_to_label(candidates):
labels_path = tf.keras.utils.get_file(
'ImageNetLabels.txt',
'https://storage.googleapis.com/download.tensorflow.org/data/ImageNetLabels.txt')
imagenet_labels = np.array(open(labels_path).read().splitlines())
idx = tf.argsort(candidates)[0,::-1][:5]+1
label = tf.squeeze(tf.gather(imagenet_labels, idx))
return label
with tf.device('/cpu:0'):
print(prediction)
return _convert_to_label(prediction)
# ---- SavedModelとして保存 ----
from datetime import datetime
version_number = int(datetime.now().timestamp())
export_dir='/content/mobilenetv2_base64input/{}'.format(version_number)
tf.saved_model.save(
model,
export_dir=export_dir,
signatures=serving)
# ---- SavedModelをダウンロード
!zip -r /content/download.zip /content/mobilenetv2_base64input/{上のコードで設定されたversion_number}
# 圧縮した zip ファイルをダウンロードする
from google.colab import files
files.download("/content/download.zip")
参考
- 学習済みモデルの部分: Transfer learning with a pretrained ConvNet
- SavedModelとして保存する手順: Using the SavedModel format
- 入力・出力の変換処理を加える部分(詳細は後述): CyberAgent | AI Tech Studioの記事 TensorFlow2.0時代のTensorFlow Serving向けモデル出力
これらのコードを参考になんとか動く形に出来ましたが、基礎的なところがあまり分かっていないのでイマイチなコードかもしれません。
モデルの読み出し・実行テスト
# テスト用の画像(LINE Botで実際に受信したbyteデータ)
img_line = b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01...(略)'
import base64
img_base64 = base64.urlsafe_b64encode(img_line)
loaded = tf.saved_model.load(export_dir)
infer = loaded.signatures["serving_default"]
output_infer = infer(tf.constant([img_base64, img_base64], dtype=tf.string))
labeling = output_infer["output_0"]
print(labeling)
備考)Grace Hopperの画像での実行結果:
tf.Tensor([b'military uniform' b'suit' b'ballplayer' b'Windsor tie' b'bearskin'], shape=(5,), dtype=string)
解説
学習済みモデルと転移学習
今回は学習済みモデルをそのまま使います: tf.keras.applications.MobileNetV2()
認識対象をカスタムしたい場合には、このような学習済みモデルをベースに「転移学習」をするのがよさそうです。
なお転移学習についてはこちらのチュートリアルが参考になりそうです。
入力・出力の変換処理を加える
画像から認識結果を得るまでは次のような流れとなります。
入力時の処理(画像サイズ変更など) → モデルを実行 → 出力の処理(数値列をラベル名に変換など)
この入出力の処理も含めた形でモデルをつくります。そうすれば、クライアント側(例えばチャットボット)で画像サイズ変更やラベル名への変換をする必要がなくなり、ミスも減ります。
AI Platformにデプロイ
公式ドキュメント: モデルのデプロイ
手順は次の通り:
- SavedModelを Cloud Storage バケットにアップロード
- AI Platform のモデルリソースを作成
- AI Platform のバージョン リソースを作成し、保存済みモデルの Cloud Storage パスを指定
Google Cloud Storageにバケット作成
公式ドキュメントではgsutilコマンドでの作成方法が記載されていますが、なんだか成功しなかったので、GCPのコンソール画面(ウェブブラウザ)から作成しました。
REGIONはus-central1にしました。AI Platformのリージョンの説明を見ると、このリージョンが何かと制約なさそうだったのでとりあえず。
その他の設定は適当に凝ってないっぽい選択肢として、リージョン:Region、アクセス制御:均一にしました。
Google Cloud Storageにモデルをアップロード
こちらはエラーなく出来たので、gsutilコマンドで実行しました。
上でダウンロードしたSavedModelのあるディレクトリにて、下記を実行。
gsutil cp -r $SAVEDMODEL_NAME gs://$BUCKET_NAME
SAVEDMODEL_NAME:上のコード中のversion_number(例:1587280290)
BUCKET_NAME:上で作成したバケット名
ローカルで推論実行テスト
デプロイ前に動作チェックします。
gcloud ai-platform local predict --model-dir $SAVEDMODEL_NAME \
--json-instances input_json.json \
--framework tensorflow
実行結果の末尾には、先ほどのGoogle Colab上での実行結果と同じものが得られます。
参考)Grace Hopperの画像での実行結果はこちら:
:
:
OUTPUT_0
military uniform
suit
ballplayer
Windsor tie
bearskin
以下、解説。
JSONデータ
上記の"--json-instances"で指定するJSONファイルの例:
{"input_images": "_9j_4AAQSkZJRgAB...(略)"}
キー名称"input_images"は、serving()関数の引数に合わせています。
JSONの形式とb64デコード
公式ドキュメントを見ると、
{'image_bytes': {'b64': base64.b64encode(jpeg_data).decode()}}
の形式のJSONにしておけば、モデル入力部での"tf.io.decode_base64()"の処理は不要になるのかとも思いましたが、エラーになったのでそのような形式にはしていません。
都度.pycファイルを削除する
.pycファイルが悪さをして、エラーが出たり、エラーも何もなくコマンドが終了したりします(Issue)ので、.pycファイルを都度削除します。
とりあえずこんな感じでやりました:
sudo find ~ -name '*.pyc' -delete
デプロイ
gcloud ai-platform models create "[YOUR-MODEL-NAME]"
[YOUR-MODEL-NAME]は、このタイミングで命名します。
モデルバージョンの作成
MODEL_DIR="gs://[BUCKET_NAME]/[SAVEDMODEL_NAME]"
VERSION_NAME="[YOUR-VERSION-NAME]"
MODEL_NAME="[YOUR-MODEL-NAME]"
FRAMEWORK="TENSORFLOW"
BUCKET_NAME, SAVEDMODEL_NAMEは、上の「Google Cloud Storageにモデルをアップロード」での値と同じ。
YOUR-MODEL-NAMEは、すぐ上のデプロイのところで設定したもの。
YOUR-VERSION-NAMEは、このタイミングで命名するものです。
gcloud ai-platform versions create $VERSION_NAME \
--model $MODEL_NAME \
--origin $MODEL_DIR \
--runtime-version=1.14 \
--framework $FRAMEWORK \
--python-version=3.5
LINE BOTから推論する
では、実際にデプロイしたモデルを使ってみます。
こちらをベースに作ります:
30分くらいでClova Extension Kit SDK for Python を使ったClova スキルを作る!(後編:実装編)
AWS Lambda、zappaを使います。pythonで書きます。
コード
ディレクトリ構成
├── main.py
├── setup.py
├── helper
│ ├── predict.py
│ └── predict_byte_input.py
└── zappa_settings.json
main.py
from flask import Flask, request, jsonify, abort
import os
import logging
from linebot import LineBotApi, WebhookHandler
from linebot.models import (
CarouselColumn, CarouselTemplate, ImageMessage,
MessageEvent, TemplateSendMessage, TextMessage,
TextSendMessage, URITemplateAction, PostbackAction,
MessageAction, URIAction, ImageCarouselColumn,
ImageCarouselTemplate, PostbackEvent
)
from linebot.exceptions import (
LineBotApiError,
InvalidSignatureError
)
from helper import predict_byte_input
# -- Flask --
app = Flask(__name__)
# -- Line bot --
line_access_token = os.environ.get('LINE_BOT_ACCESS_TOKEN')
line_bot_api = LineBotApi(line_access_token)
line_channel_secret = os.environ.get('LINE_BOT_CHANNEL_SECRET')
handler = WebhookHandler(line_channel_secret)
#-- logger --
logging.basicConfig()
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
@app.route("/line_bot", methods=['POST'])
def callback():
# get X-Line-Signature header value
signature = request.headers['X-Line-Signature']
# get request body as text
body = request.get_data(as_text=True)
# handle webhook body
try:
handler.handle(body, signature)
except LineBotApiError as e:
print("Got exception from LINE Messaging API: %s\n" % e.message)
for m in e.error.details:
print(" %s: %s" % (m.property, m.message))
print("\n")
except InvalidSignatureError:
print("InvalidSignatureError")
abort(400)
return 'OK'
# -- 画像受信時
@handler.add(MessageEvent, message=(ImageMessage))
def handle_content_message(event):
# GOOGLE_APPLICATION_CREDENTIALS
google_credentials_json_text = os.environ.get('GOOGLE_APPLICATION_CREDENTIALS_JSON')
path_google_credentials = os.environ.get('GOOGLE_APPLICATION_CREDENTIALS')
import json
d = json.loads(google_credentials_json_text)
with open(path_google_credentials, 'w') as f:
json.dump(d, f, indent=4)
# 画像の受信
message_content = line_bot_api.get_message_content(event.message.id)
print("message_content.content: ", message_content.content)
# 予測(画像認識)
predictions_raw = predict_byte_input.predict_byte_input(message_content.content)
print('predictions_raw: ', predictions_raw) # [{'output_0': 'military uniform'}, {'output_0': 'suit'}, {'output_0': 'ballplayer'}, {'output_0': 'Windsor tie'}, {'output_0': 'bearskin'}]
prediction = []
for prediction_raw in predictions_raw:
prediction.append(prediction_raw['output_0'])
print('prediction: ', prediction)
# テキストメッセージ送信
message = "認識結果\n" + "\n".join(prediction)
print('message: ', message)
line_bot_api.reply_message(
event.reply_token, TextSendMessage(text=message))
if __name__ == "__main__":
app.run()
setup.py
helperディレクトリにコードをおくためのものです。
from setuptools import setup
setup(
name='helper',
packages=['helper'],
include_package_data=True,
install_requires=['flask']
)
predict.py
https://github.com/GoogleCloudPlatformのコードをベースに下記部分を変更。
これをしないと、"No module named..."のエラーが出ましたので、こちらのissueを参考に変更しました。
# service = googleapiclient.discovery.build('ml', 'v1')
service = googleapiclient.discovery.build('ml', 'v1', cache_discovery=False)
predict_byte_input.py
モデルに送信するデータの作成(b64エンコードなど)と、
predict.pyの実行。
import base64
import os
from helper import predict
def convert_for_predict(message_content_content):
img_base64 = base64.urlsafe_b64encode(message_content_content)
img_base64_utf = img_base64.decode('utf-8')
img_dict = {"input_images": img_base64_utf}
return img_dict
def predict_byte_input(message_content_content):
project = os.environ.get('AI_PLATFORM_PROJECT')
model = os.environ.get('AI_PLATFORM_MODEL')
user_input = convert_for_predict(message_content_content)
try:
predictions = predict.predict_json(
project, model, user_input)
except RuntimeError as err:
print(str(err))
return
else:
return predictions
解説
Googleの認証情報
predict.pyを実行するには、GCPのAPIを使うときにお馴染みの"GOOGLE_APPLICATION_CREDENTIALS"の環境変数が必要になります。
普段ローカルで使うときには、
-
GCPコンソールからjsonファイルをダウンロード
-
"GOOGLE_APPLICATION_CREDENTIALS"の環境設定として jsonファイルのパスを定義
という流れです。
では、AWS Lambda上で同様のことをどうやるか?
jsonファイルをアップロードするのは避けたいし、
そもそもコード中に直接環境変数を記載するのは避けたい。
今回やってみた方法:
- jsonファイルの中身をテキストとして環境変数にする
- 環境変数のテキストを読み出して、辞書にして、書き出し(例えば./xxx.json)
- (GOOGLE_APPLICATION_CREDENTIALSは、./xxx.jsonとして定義しておく)
とりあえずは出来ましたが、これでよかったのか自信ないです。
正解があればおしえてください。
おわりに
Tensorflowに触りなれていなかったのでかなり大変でした。
また、比較的最近バージョン2.0になったのでweb上の情報もあまり見つけられず。
AI Platformも最近出来たものだし(名前が変わった)。
同様のことをやりたい人は多いはずだと思うのですが、上のCyberAgentさんの記事くらいしか見つけられませんでした。この記事がなかったら途中で諦めてた…。感謝です。