9
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

TensorflowモデルをクラウドAPI化して LINE BOTから推論する

Last updated at Posted at 2020-04-16

ディープラーニングの画像認識モデルをクラウド上に置いて、
スマホやパソコンから画像を送信すると、認識結果を返してくれるものを作ります。

TransferLearningImageClass.png

具体的には、今回はLINEのチャットボットに写真を送ると、画像認識結果を返すようにしました。
スクリーンショット 2020-04-16 23.15.47.png

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モデルの準備・ダウンロード

コード

.ipynb
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)

.ipynb
# ---- SavedModelをダウンロード
!zip -r /content/download.zip /content/mobilenetv2_base64input/{上のコードで設定されたversion_number}
# 圧縮した zip ファイルをダウンロードする
from google.colab import files
files.download("/content/download.zip")

参考

これらのコードを参考になんとか動く形に出来ましたが、基礎的なところがあまり分かっていないのでイマイチなコードかもしれません。

モデルの読み出し・実行テスト

.ipynb
# テスト用の画像(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にデプロイ

公式ドキュメント: モデルのデプロイ

手順は次の通り:

  1. 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_json.json
{"input_images": "_9j_4AAQSkZJRgAB...(略)"}

キー名称"input_images"は、serving()関数の引数に合わせています。

JSONの形式とb64デコード

公式ドキュメントを見ると、

input_json.json?
{'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

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ディレクトリにコードをおくためのものです。

setup.py
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さんの記事くらいしか見つけられませんでした。この記事がなかったら途中で諦めてた…。感謝です。

9
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?