はじめに
しばらくディープラーニングを使った画像分類や画像認識モデルを触っていなかったので、リハビリを兼ねて、
犬の画像からその犬種を推測するWEBアプリケーションを作りました。
コードはここにあります。
https://github.com/xcnkx/dogs_line_bot
LineのAPIを使ってline bot化とwebアプリ化をしています。
やったこと
1. 画像分類モデルの学習
2. LineのAPIを使ってLine bot化
3. WEB UIを作成
4. Herokuを使ってデプロイ (poetry ver.)
1. 画像分類モデルの学習
モデルについて
予測モデルは最近kaggleでもbaselineとしてとりあえず使われることが多いEfficientNetを使いました。
これを選んだ理由は、自分の記憶上ではCNNモデルの論文とかはInceptionV3で止まっていたのでEfficientNetをとりあえず使ってみたかった。
詳細
- Model: EfficientNet B0
- 訓練データ: Stanford Dogs Dataset
- 環境: Google Colab with GPU
- 訓練コード ↓ もしくは → https://github.com/xcnkx/dogs_line_bot/blob/master/notebooks/efficient_net_train.ipynb
2. LineのAPIを使ってLine bot化
携帯で犬の写真を撮ってLINEでボットに送ると犬種を教えてくれるようにするまでがゴール。
簡単なアプリなので使用したフレームワークはflask。
コード解説
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
が必要になるので忘れずに。
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/
# 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リストに持つようにする
次は予測に必要な関数を定義
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/
-
@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}]ですね!"),
)
アプリの起動
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することができ、さらにパラメータも渡せます。
上記のコードでの例
return render_template(
"index.html", answer=pred_answer, file_name=file_name
)
view側(HTML側)はjinja2というPython用のテンプレートエンジンをつかっているのでこういう風に書ける。
またCSSはbootstrap
を使っています。
<!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>© 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からでもインストールをして使うことも出来ますが、ターミナルから以下のようにインストールできます。
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で記事を書く時はこまめに保存をするように心がけましょう。途中でなぜか自動保存が動いてなく、実はこの記事を書くのが2回目です、、、、、