みらいテックラボ

音声・画像認識や機械学習など, 管理人が興味のある技術の紹介や実際にトライしてみた様子などメモしていく.

「Kingyo AI Navi」のアプリ化を考える (2)

今月中頃(2019.3.16), Urban Data Challenge 2018のファイナルが行われ, CODE for YAMATOKORIYAMAが金魚愛(AI)育成プロジェクトとして取り組んでいる「Kingyo AI Navi」が, アイデア部門の金賞[1]を受賞した.

f:id:moonlight-aska:20190324195724p:plain:w400

CODE for YAMATOKORIYAMAでは, 今年度このアイデアをアプリ化したいと考えており, LINEの枠組みを利用したアプリ化の検討を開始した.


関連記事:
「Kingyo AI Navi」のアプリ化を考える (1)
・「Kingyo AI Navi」のアプリ化を考える (2)
「Kingyo AI Navi」のアプリ化を考える (3)
「Kingyo AI Navi」のアプリ化を考える (4)


1. 概要
「Kingyo AI Navi」をLINEのMessaging API + サービスで実現すべく, 今回はLINE Botから金魚の画像を送信して種類を識別し, 関連情報を返す部分の基本的な動作を試した.
サービス側は, 金魚種別の識別に前回[2]記載したCloud AutoML Vitionを利用するので, GCP(Google Cloud Platform)の枠組みで構築することにした.

[構成図]
f:id:moonlight-aska:20190331172938p:plain:w500

① LINEアプリからLINE Botサーバへ金魚の画像を送信する.
② LINE Botサーバは, Messaging API経由で, Kingyo AI NaviサービスのWebhook URLをコールする.
③ Kingyo AI Naviサービスは, 画像データをAutoML Vision APIに送信し, 金魚の分類を行う.
④ Kingyo AI Naviサービスは, AutoML Visionから分類結果を受信する.
⑤ LINE Botサーバは, Messaging API経由で, Kingyo AI Naviサービスから応答(金魚の種類, 関連情報URL等)を受信する.
⑥ LINE Botサーバは, 受信した応答をLINEアプリに送信する.


2. サービス開発
2.1 LINE側設定
LINEのMessaging APIを利用する.
LINEのMessaging APIを使ったLINE Botの作成については, ネット上に多くの記事[3][4]があるので, そちらを参照のこと.

あと, LINE DevelopersでChannelにWebhook URLを登録する際に, 「SSLのみ対応」ということでドメイン名およびSSL証明書が必要となり, とりあえず以下のサービスを利用した.
ドメイン
 無料でドメイン名を取得できるfreenomを利用. 12カ月まで無料.
SSL証明書
 無料でSSL証明書を発行してくれるLet's Encryptを利用. 3カ月まで無料.

2.2 サーバ側開発環境
サーバ構築等に詳しくないので, あえて勉強のためGAE(Google App Engine)でなくGCE(Google Compute Engine)を使用してみた.

[開発環境]
サーバ:GCE (f1-micro; usリージョン)
OS:Ubuntu 18.04 LTS
開発言語:Python 3.6

GCEのインスタンス生成等については, こちらもネット上に多くの記事[4]があるので, そちらを参照のこと.

2. 3 アプリ実装
今回サーバ側アプリは, python + flaskで実装した.

まずは, LINE Messaging APIからのイベントを処理する部分を実装する.
(1) LINE Messaging APIがWebhookすると, callback()関数が呼び出される. ここでは, MessageEventがTextMessageかImageMessageかを判断し, 処理を振り分ける.
(2) handle_text_message()関数で, 送信されたテキストをそのまま返す.
(3) handle_image_message()関数で, 画像データを識別する処理を呼び, 識別結果をCarouselTemplateを使って応答メッセージを返す.

[コード]

import os
import ssl
from argparse import ArgumentParser
import datetime

import settings
import automl
import pandas as pd

# Import from Flask Module                                                             
from flask import Flask, request, abort
# Import from Line Bot SDK                                                             
from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, ImageMessage, TextMessage, TextSendMessage,
    CarouselColumn, CarouselTemplate, TemplateSendMessage
)

context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.load_cert_chain('/etc/letsencrypt/live/{Domain Name}/fullchain.pem',
                        '/etc/letsencrypt/live/{Domain Name}/privkey.pem')

# Instance of Flask                                                                    
app = Flask(__name__)

# Message App ID                                                                       
line_bot_api = LineBotApi(os.environ['LINE_CHANNEL_ACCESS_TOKEN'])
handler = WebhookHandler(os.environ['LINE_CHANNEL_SECRET'])

category = pd.read_csv('./resources/kingyo.csv', index_col=0)

@app.route("/callback", methods=['POST'])
def callback():
    print('callback : ', datetime.datetime.today())
    signature = request.headers['X-Line-Signature']

    # get request body as text                                                         
    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    # handle webhook body                                                              
    try:
        handler.handle(body, signature)

    except InvalidSignatureError as e:
        print('InvalidSignatureError : ', e)
        abort(400)

    return 'OK'

# Handle text message                                                                  
@handler.add(MessageEvent, message=TextMessage)
def handle_text_message(event):
    print('handle_text_message : ', datetime.datetime.today())
    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text=event.message.text)
    )

# Handle image message                                                                 
@handler.add(MessageEvent, message=ImageMessage)
def handle_image_message(event):
    print('handle_image_message : ', datetime.datetime.today())
    message_content = line_bot_api.get_message_content(event.message.id)
    try:
        image_info = automl.get_info_by_automl(message_content.content)
	if isinstance(image_info, str):
            messages = [
		TextSendMessage(text=image_info)
            ]
        elif isinstance(image_info, list):
            columns = []
            for res in image_info:
		label = res['Label']
                column = CarouselColumn(
                    thumbnail_image_url = category.loc[label, 'url'],
                    title = '種類:{}'.format(category.loc[label, 'name']),
                    text = '確信度:{:.3f}'.format(res['Score']),
                    actions = [
                        {"type": "uri",
                         "label": "関連サイト",
                         "uri": "https://www.kingyoen.com/"}
                    ]
                )
                columns.append(column)

	    messages = TemplateSendMessage(
                alt_text = 'template',
		template = CarouselTemplate(columns=columns, imageSize='contain'),
            )

        print('reply message : ', datetime.datetime.today())
        reply_message(event, messages)

    except Exception as e:
        import traceback
        traceback.print_exc()
        reply_message(event, TextSendMessage(text='何か調子が悪いなー.'))

def reply_message(event, messages):
    line_bot_api.reply_message(
        event.reply_token,
        messages = messages,
    )

if __name__ == '__main__':
    arg_parser = ArgumentParser(
	usage = 'Usage: python ' + __file__ + ' [--help]'
    )
    arg_parser.add_argument('-d', '--debug', default=False, help='debug')
    options = arg_parser.parse_args()

    app.run(host=os.environ['HOST_ADDRESS'], port=os.environ['HTTPS_PORT'], ssl_contex\
t=context, threaded=True, debug=options.debug)


次に, Cloud AutoML Visionを呼び出す処理を実装する.
(1) setup_automl_vision()関数で, Cloud AutoMLを使うための準備を行う.
(2) get_info_by_automl()関数で, 画像データをCloud AutoML Visionに送信して金魚識別を行い, 結果を返す.

[コード]

import os
import settings
from google.cloud import automl_v1beta1 as automl

SCORE_THRESHOLD = '0.2'

# init AutoML Vision                                                                   
def setup_automl_vision():
    automl_client = automl.AutoMlClient()
    prediction_client = automl.PredictionServiceClient()
    model_id = automl_client.model_path(os.environ['PROJECT_ID'],
                                        os.environ['COMPUTE_REGION'],
                                        os.environ['MODEL_ID'])
    print(prediction_client, automl_client, model_id)
    return prediction_client, model_id

prediction_client, model_id = setup_automl_vision()

# predict the image using AutoML Vision                                                
def get_info_by_automl(image):
    payload = {"image": {"image_bytes" : image}}
    params = {"score_threshold" : SCORE_THRESHOLD}

    try:
        response = prediction_client.predict(model_id, payload, params)
	results_info = []
	for result in response.payload:
            print('Predicted class name: {}'.format(result.display_name))
            print('Predicted class score: {}'.format(result.classification.score))
            info = {
		"Label" : result.display_name,
		"Score" : result.classification.score}
            results_info.append(info)

	if len(results_info) > 0:
            return results_info
	else:
            return '私もよく判りましぇーん.'

    except:
        import traceback
	traceback.print_exc()
        return '何か調子悪いなー.'


[動作例]
LINE画面から, 金魚の写真を撮って送信すると, 識別結果に応じた応答が表示される.
f:id:moonlight-aska:20190331171621p:plain:w300

「関連サイト」をタッチすると, そのサイトが表示される.
f:id:moonlight-aska:20190331172228p:plain:w300

課題:
画像を送信して応答で結果が表示されるまでに, 約5秒かかっている.
そこで, サーバ側の処理時間をちょっと測定してみたところ, どうもCloud AutoML Visionでの金魚識別に3秒程度かかっている感じ.
もしかしたら, 実際にサービス動かす際にはCloud AutoML Visionを使わず, サーバ側で独自に金魚識別をやらなければいけないかも....


次回は, できればLINE Botからテキストを送信すると, それに応じて対話を行う仕組みをDialogflowでも利用して試してみたいと思う. (現状はEcho)
ただ, 何か対話タスクを決めないと応答内容(対話シナリオ)を準備できないので, どうしようかな~?

----
参照URL:
[1] UDC2018審査結果 | アーバンデータチャレンジ
[2] 「Kingyo AI Navi」のアプリ化を考える (1)
[3] ボットを作成する | LINE Developers
[4] LINE Messaging API を使ってLINEにメッセージ送信/メッセージ返信する
[4] VM インスタンスの作成と起動 | Compute Engine ドキュメント | Google Cloud






GCPの教科書

GCPの教科書