絵音かな？キノコかな？

# はじめに
PythonとAutoML Visionで画像認識LINE BOTを作ってみたので、参考にした記事やサイトをまとめます。（ほぼ個人的なメモ書きです）

できたものがこちら。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309745/ce48801d-adf2-08a4-6543-5e00da57f360.jpeg" width=50%>

機能は以下の3パターン。
①テキストを送るとこちらのユーザ名とイカしたフレーズを返してくれます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309745/c9e348ec-c284-acc1-23ab-be31e0791328.jpeg" width=50%>

②スタンプを送るとスタンプを返してくれます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309745/b751404f-a2b4-f275-07a7-ccfc8ef3d985.jpeg" width=50%>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309745/eb7f896e-678e-6041-6b17-76b0403df589.jpeg" width=50%>

そして、
③画像を送ると**「絵音かキノコか」**を判別してくれます。
（絵音かキノコかの2択です:sob:）
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309745/534de347-c2a7-0d83-a031-084836af32f0.jpeg" width=50%>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309745/8ea17cb3-9f23-4122-5cb4-ac0ddc828e02.jpeg" width=50%>


# 概要
- Python環境構築
- Pythonのスクレイピングで画像収集
- AutoML Visionで機械学習モデル作成
- LINE Developers登録とBOT実装
- 学習モデルをAPIで呼び出す
- GitHubからHerokuへデプロイ


# Python環境構築
どの方法が最適か分かりませんが、Anacondaを使用することにしました。
OSはWindowsです。

[Anaconda で Python 環境をインストールする]
(https://qiita.com/t2y/items/2a3eb58103e85d8064b6)
[Windows での Python 環境構築から実行までのあれこれ](https://qiita.com/tentom/items/fdf02d4df8edba050c9c)


# Pythonのスクレイピングで画像収集
Google検索結果から絵音とキノコの画像を100枚ほど集めます。
集めた画像の中から、明らかに関係のないものは手作業で削除しておきましょう。

[API を叩かずに Google から画像収集をする](https://qiita.com/naz_/items/efc296ae1bf0e62f6704)

枚数が不足している場合や精度を上げたい場合は画像の水増しも行います。

[機械学習のデータセット画像枚数を増やす方法](https://qiita.com/bohemian916/items/9630661cd5292240f8c7)

# AutoML Visionで機械学習モデル作成
[Google Cloud AutoML Vision の使い方 機械学習モデルを作ってみよう](https://blog.apar.jp/web/10326/)

絵音のラベルがこんな感じ。
![AutoML Vision - line-ai-sample1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309745/4bc4b07e-e611-7598-a436-ac112ba6f464.jpeg)

キノコのラベルがこんな感じ。
![AutoML Vision - line-ai-sample2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309745/e4d790dc-346a-771e-2005-70e8c4866d1d.jpeg)

一応、その他の人物のラベルも入れてみます。
![AutoML Vision - line-ai-sample3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309745/ba4e5638-a0cf-6ffd-60e8-b22b2968287f.jpeg)


# LINE Developers登録とBOT実装
[Messaging APIを利用するには](https://developers.line.biz/ja/docs/messaging-api/getting-started/)
[【初心者向け】PythonによるHeroku環境で簡単LINEBot開発](https://miyabi-lab.space/blog/21)
[line-bot-sdk-python](https://github.com/line/line-bot-sdk-python)
[Messaging APIリファレンス](https://developers.line.biz/ja/reference/messaging-api/)

テキストメッセージまたはスタンプメッセージを受信した場合の処理はこんな感じ。

```python:main.py
from flask import Flask, request, abort

from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError, LineBotApiError
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage, ImageMessage,
    StickerMessage, StickerSendMessage
)

import os

import random

app = Flask(__name__)

LINE_CHANNEL_ACCESS_TOKEN = os.environ["LINE_CHANNEL_ACCESS_TOKEN"]
LINE_CHANNEL_SECRET = os.environ["LINE_CHANNEL_SECRET"]

line_bot_api = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(LINE_CHANNEL_SECRET)


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

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

    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)

    return 'OK'

# テキストメッセージの場合
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    message = ''

    # ユーザ名を取得
    print(event.source)
    if event.source.type == 'user':
        profile = line_bot_api.get_profile(event.source.user_id)
    elif event.source.type == 'group':
        profile = line_bot_api.get_group_member_profile(
            event.source.group_id, event.source.user_id)
    elif event.source.type == 'room':
        profile = line_bot_api.get_room_member_profile(
            event.source.room_id, event.source.user_id)

    if profile is not None:
        name = profile.display_name
        message = name + 'さん\n'

    str_list = [
        '私以外私じゃないの あたりまえだけどね',
        '両成敗が止まらないもう泊まらない 呆れちゃうよな',
        'ダルマさんが転んだ あっかんべーあっかんべーって',
        '僕にはありあまる ロマンスがありあまる',
        'ぼんやり浮かぶ悲しいメロディー またふと流れる美しいメロディー',
        'たった今わかったんだ キラーボールが回る最中に',
        '戦ってしまうよ戦ってしまうよ 境界を観ながら',
        '猟奇的なキスを私にして 最後まで離さないで',
        'どうやって抱きしめたら 心が弄ばれないのか',
        '雨にまで流されて 影に紛れてたんだよ',
        'どうせアイツいつものように 色目使ってんでしょ',
        '誰が理想ってやつなんだ これが理想ってやつなんか？',
        'ホワイトなエッジが効いたワルツ 小気味良く鳴り響くワルツ',
        'ナイチンゲールが恋に落ちたって風の噂流れた',
        '今日もまた嫌なことばっかり 泣いたふりで避けてばっかり',
        '大人じゃないからさ 無理をしてまで笑えなくてさ'
    ]
    message += random.choice(str_list)
    send_message(event, message)

# スタンプメッセージの場合
@handler.add(MessageEvent, message=StickerMessage)
def handle_sticker(event):
    sticker_list = [
        '51626496', '51626497', '51626502', '51626504',
        '51626508', '51626511', '51626517', '51626530'
    ]

    sticker_message = StickerSendMessage(
        package_id='11538',
        sticker_id=random.choice(sticker_list)
    )

    line_bot_api.reply_message(
        event.reply_token,
        sticker_message
    )


def send_message(event, message):
    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text=message)
    )


if __name__ == "__main__":
    port = int(os.getenv("PORT"))
    app.run(host="0.0.0.0", port=port)
```


- ユーザ名などのプロフィール情報は、1対1のトークなのか、グループなのか、トークルームなのかによって取得方法が異なります。
- 使用できるスタンプは限られていて（[スタンプメッセージ](https://developers.line.biz/ja/reference/messaging-api/#sticker-message)）、その中からパッケージIDとスタンプIDで指定します。


# 学習モデルをAPIで呼び出す
[Google Cloud AutoML Vision 機械学習モデルをAPI経由で使う手順]
(https://blog.apar.jp/web/10404/)
[Cloud AutoML Vision を使って、どの芸能人に似てるか試してみた（API編）]
(https://qiita.com/pyru89kwmr/items/c723bb4456443bcaafa2)


画像メッセージを受信した場合の処理を追加します。
AutoML VisionのPREDICTタブにPythonのコードが載っています。

```python:main.py
from io import BytesIO

from google.cloud import automl_v1beta1

# 画像メッセージの場合
@handler.add(MessageEvent, message=ImageMessage)
def handle_image(event):
    message_id = event.message.id
    message_content = line_bot_api.get_message_content(message_id)

    image_bin = BytesIO(message_content.content)
    image = image_bin.getvalue()
    request = get_prediction(image)
    print(request)

    score = request.payload[0].classification.score
    display_name = request.payload[0].display_name

    message = str(round(score * 100, 3)) + '％の確率で'
    if display_name == 'enon':
        message += '絵音だね\nロマンスがありあまる'
    elif display_name == 'kinoko':
        message += 'キノコだね\n私以外私じゃないの'
    elif display_name == 'other':
        message += '...\n絵音でもキノコでもないんじゃない？'

    send_message(event, message)


def get_prediction(content):
    project_id = 'プロジェクトID'
    model_id = 'モデルID'
    prediction_client = automl_v1beta1.PredictionServiceClient()
    # 環境変数にGOOGLE_APPLICATION_CREDENTIALSを設定していない場合は以下とする.
    # KEY_FILE = "keyfile.json"
    # prediction_client = automl_v1beta1.PredictionServiceClient.from_service_account_json(KEY_FILE)

    name = 'projects/{}/locations/us-central1/models/{}'.format(
        project_id, model_id)
    payload = {'image': {'image_bytes': content}}
    params = {}
    request = prediction_client.predict(name, payload, params)
    return request
```

- プロジェクトIDとモデルIDは自分のものを設定してください。
- モデルを再トレーニングするとモデルIDが変わるので、ここも変更が必要になります。
- keyfile.jsonの取り扱いに注意。

# GitHubからHerokuへデプロイ
AtomとGitHub、GitHubとHerokuを連携し、Atom上でコミット&プッシュすると自動でデプロイされるようにしています。

```
ファイル構成
line_ai_sample
 ├── Procfile
 ├── keyfile.json
 ├── main.py
 ├── requirements.txt
 └── runtime.txt
```

[【atomをgithubと連携させよう！】導入手順まとめ](https://www.sejuku.net/blog/73327)
[【初心者向け】PythonによるHeroku環境で簡単LINEBot開発](https://miyabi-lab.space/blog/21)
[heroku 初級編 - GitHub から deploy してみよう -](https://qiita.com/sho7650/items/ebd87c5dc2c4c7abb8f0)