EfficientNetを使って簡単なWebアプリとLine Botを作ってみた

# はじめに
しばらくディープラーニングを使った画像分類や画像認識モデルを触っていなかったので、リハビリを兼ねて、
犬の画像からその犬種を推測するWEBアプリケーションを作りました。

コードはここにあります。
https://github.com/xcnkx/dogs_line_bot

LineのAPIを使ってline bot化とwebアプリ化をしています。


# やったこと
    1. 画像分類モデルの学習
    2. LineのAPIを使ってLine bot化
    3. WEB UIを作成
    ４. Herokuを使ってデプロイ (poetry ver.)


# 1. 画像分類モデルの学習

## モデルについて

予測モデルは最近kaggleでもbaselineとしてとりあえず使われることが多い[EfficientNet](https://arxiv.org/abs/1905.11946)を使いました。
これを選んだ理由は、自分の記憶上ではCNNモデルの論文とかはInceptionV3で止まっていたのでEfficientNetをとりあえず使ってみたかった。

### 詳細

- Model: EfficientNet B0
- 訓練データ:　[Stanford Dogs Dataset](http://vision.stanford.edu/aditya86/ImageNetDogs/)
- 環境: Google Colab with GPU
- 訓練コード ↓ もしくは → https://github.com/xcnkx/dogs_line_bot/blob/master/notebooks/efficient_net_train.ipynb

　　　 [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/xcnkx/dogs_line_bot/blob/master/notebooks/efficient_net_train.ipynb)


# 2. LineのAPIを使ってLine bot化

携帯で犬の写真を撮ってLINEでボットに送ると犬種を教えてくれるようにするまでがゴール。
簡単なアプリなので使用したフレームワークはflask。

### コード解説

```python
import os
import sys
import glob
from io import BytesIO
from tensorflow.keras.models import load_model
from tensorflow.keras.preprocessing.image import img_to_array
import efficientnet.tfkeras  # NOQA

from PIL import Image

from werkzeug.utils import secure_filename
from flask import Flask, request, abort, render_template
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import (
    MessageEvent,
    TextMessage,
    TextSendMessage,
    ImageMessage,
)
```

必要なライブラリをインポートする。
学習済みのEfficientNetをloadするのに`import efficientnet.tfkeras`が必要になるので忘れずに。


```py
app = Flask(__name__)
file_path = "/images"
# 環境変数からchannel_secret・channel_access_tokenを取得
channel_secret = os.environ["DOG_BOT_CHANNEL_SECRET"]
channel_access_token = os.environ["DOG_BOT_CHANNEL_ACCESS_TOKEN"]
app.secret_key = __name__

if channel_secret is None:
    print("Specify CHANNEL_SECRET as environment variable.")
    sys.exit(1)
if channel_access_token is None:
    print("Specify CHANNEL_ACCESS_TOKEN as environment variable.")
    sys.exit(1)

line_bot_api = LineBotApi(channel_access_token)
handler = WebhookHandler(channel_secret)
```

LineのAPIをのchannel_secret・channel_access_tokenを環境変数から取得。
そして`line_bot_api`, `handler`をインスタンス化
どちらもLine developersの管理画面から確認ができます。
詳しくは -> https://www.casleyconsulting.co.jp/blog/engineer/3028/



```py
# load model
model = load_model("./model/efficient_net_model.h5")


classes = [
    "Chihuahua",
    "Japanese_spaniel",
    "Maltese_dog",
    "Pekinese",
    "Shih-Tzu",
    "Blenheim_spaniel",
#    (省略)
    "African_hunting_dog",
]
```
学習済みのモデルをロード。またラベルをclassesリストに持つようにする


次は予測に必要な関数を定義

```py
def predict(img: Image):
    img = img.resize((224, 224), Image.NEAREST)
    x = img_to_array(img)
    x /= 255
    result = model.predict(x.reshape([-1, 224, 224, 3]))
    predicted = result.argmax()
    proba = result[0, predicted] * 100
    return classes[predicted], "{:.1f}".format(proba)


UPLOAD_FOLDER = "./static/images/cache"
ALLOWED_EXTENSIONS = set(["png", "jpg", "jpeg", "gif"])


def allowed_file(filename):
    return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS


def remove_glob(pathname, recursive=True):
    for p in glob.glob(pathname, recursive=recursive):
        if os.path.isfile(p):
            os.remove(p)
```
`predict`で


flaskでのルーティングの設定と、各イベントに対してのレスポンスを書いています。

- `/`ではWebアプリケーションが動くように。
- `callback`では Linebotが動くようにしています。
    - `handler`で各イベントに対してのレスポンスを捌いています。
    - 詳しくはこちら→　https://developers.line.biz/ja/docs/messaging-api/line-bot-sdk/

```py
@app.route("/", methods=["GET", "POST"])
def upload_file():
    if request.method == "POST":
        if "file" not in request.files:
            alert = "ファイルがありません"
            return render_template("index.html", alert=alert)

        file = request.files["file"]
        if file.filename == "":
            alert = "ファイルがありません"
            return render_template("index.html", alert=alert)

        if file and allowed_file(file.filename):
            remove_glob("./cache/*")
            img = BytesIO(file.stream.read())
            img = Image.open(img)
            pred, proba = predict(img)
            pred_answer = f"この犬は{proba}％の確率で[{pred}]ですね！"

            file_name = secure_filename(file.filename)
            img.save(os.path.join(UPLOAD_FOLDER, file_name))

            return render_template(
                "index.html", answer=pred_answer, file_name=file_name
            )

    return render_template("index.html", answer="")


@app.route("/callback", 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)
    app.logger.info("Request body: " + body)

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

    return "OK"


@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    messages = [
        TextSendMessage(text="犬の画像を送ってみて！品種当てちゃうぞ！"),
    ]

    line_bot_api.reply_message(event.reply_token, messages)


@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 = BytesIO(message_content.content)

    image = Image.open(image)
    pred, proba = predict(image)

    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text=f"この犬は{proba}％の確率で[{pred}]ですね！"),
    )
```

アプリの起動

```py

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


# 3. Web UIを作成

flaskでは`render_template`を使うことで`templates`フォルダに入っているHTMLをrenderすることができ、さらにパラメータも渡せます。

上記のコードでの例

```py
return render_template(
                "index.html", answer=pred_answer, file_name=file_name
            )
```

view側（HTML側）は[jinja2](https://jinja.palletsprojects.com/en/2.11.x/)というPython用のテンプレートエンジンをつかっているのでこういう風に書ける。
またCSSは`bootstrap`を使っています。

```html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Dog Recognition bot web app</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css"
        integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous">
    <link rel="icon" href="../static/images/pet_robot_dog.png" />
</head>

<body class="bg-secondary">
    <div class="d-flex p-3 flex-column align-items-center">
        <div class="card text-white bg-dark text-center mb-3" style="max-width: 36rem;">
            <div class="card-header">
                <h2><img class="img-thum" src="../static/images/pet_robot_dog.png" alt="dog bot" height="100">
                    Dog Recognition Bot </h2>
            </div>
            <div class="card-body">
                <h3 class="card-title"> 犬の画像を送ってください！</h3>
                <h4 class="card-title"> AIが犬種を推測します</h4>
            </div>
            <div class="card-body">
                <form method="POST" enctype="multipart/form-data">
                    <input class="file_choose" type="file" name="file">
                    <input class="btn btn-primary btn" value="submit!" type="submit">
                </form>
            </div>
            <div class="card-body">
                {% if answer %}
                <div>
                    <img src="../static/images/cache/{{ file_name }}" alt="uploaded_image" class="img-fluid">
                </div>
                <div class="alert alert-primary" role="alert">
                    <div class="answer">{{answer}}</div>
                </div>
                {% endif %}
                {% if alert %}
                <div class="alert alert-danger" role="alert">
                    <div class="answer">{{alert}}</div>
                </div>
                {% endif %}
                </div>
            <footer>
                <small>&copy; github.com/xcnkx</small>
            </footer>
        </div>
</body>
</html>

```

# 4. Herokuを使ってデプロイ (poetry ver.)

Herokuでアプリをデプロイするにはまずアカウントを作成する必要があります。そのあとherokuでアプリを作って自分のgithubと連携させます。
ここらへんの話はよくあるものなので詳しくは -> https://qiita.com/suigin/items/0deb9451f45e351acf92#line-bot%E3%82%92heroku%E3%81%ABdeploy
ここではpoetryを使っているpythonアプリをどうやってHerokuにデプロイをするかを書きます。

pythonの仮想環境管理ツールとして自分は普段poetryを使っているのですが、poetryで管理しているpythonのプロジェクトをherokuにデプロイしようとすると普通はできません。 herokuは`requirements.txt`（pipenv）を使ってpythonのパッケージをインストールをしているからです。

しかし、時代はpoetryなのでどうにかしてherokuにデプロイしたいと思い方法を探したところなんとある方法見つけました。

それはズバリ、　https://github.com/moneymeets/python-poetry-buildpack を使うことでした！
`pyproject.toml`から`requirements.txt`を自動で生成してくれるherokuの公式kビルドパックです。

これはherokuのGUIからでもインストールをして使うことも出来ますが、ターミナルから以下のようにインストールできます。

```shell
heroku buildpacks:clear
heroku buildpacks:add https://github.com/moneymeets/python-poetry-buildpack.git
heroku buildpacks:add heroku/python
```

そのあとは

```
git push heroku master:master
```
でherokuにデプロイできると思います。


##　デプロイでハマったこと

- herokuで使えるpythonのversionが指定されているので確認しないとエラーでます
- `tensorflow` をそのままインストールすると使用メモリが500MBを超えるので`tensorflow-cpu`をインストールしないといけません
- またEfficientNetB4以上を使用するとweightが大きすぎてメモリに乗らないのでモデルのサイズの調整が必要


# おわりに

久々に画像分類モデルとkerasを触ったので楽しかった。またEfficientNetの良さ（学習の速さと精度）がわかり、kaggleでとりあえずEfficientNetをbaselineとして使う人の気持ちもわかりました。
また気になるモデルや論文があれば、このアプリを使って実装していきたいです。

またQiitaで記事を書く時はこまめに保存をするように心がけましょう。途中でなぜか自動保存が動いてなく、実はこの記事を書くのが２回目です、、、、、