LoginSignup

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

More than 1 year has passed since last update.

AIアプリで友人にネコと留守番してもらおう

Last updated at Posted at 2021-11-06

概要

ネコの画像データセットをCNNに学習させ、
ネコの感情判定モデルを作成し、Webアプリとしてデプロイする。

まえがき

薬剤師資格持ち、駆け出しデータサイエンティストのhiroです。

我が家には三匹のネコがいる。
もちろん毎日愛でているが、やっぱり一番の悩みとしてお出かけがしにくい。
たまには遠出したい。

友人に留守番を頼むことはできるが、せっかくならネコたちと楽しく過ごしてほしい。
ネコと楽しく過ごすためには、ネコが今何を考えているのかがわからないといけない。
私は顔を見ているだけで大体何を考えているのかわかる(つもり)のだが、
友人に写真を送ると全部同じ表情に見えるようだ。
特に白猫のビリーは感情表現豊かなのだが、それでもやっぱりわかってもらえない。

happy angry sleepy cool
36232320.6E92999631FE9A2F4B9C609FA5536F7C.18101218.jpg IMG_2401.JPG 36232320.0E1574D20D98A36DFAA66A8D07F85424.18101223.jpg 36232320.0E64940F2C828860DDB6042F54974538.18101223.jpg

そこで今回はビリーの写真をアップロードするとその時の感情を推定してくれるアプリを開発した。

開発環境

Windows10 (Intel i7-8750H, GeForce RTX2080)
python 3.8.12
tensorflow 2.3.0
flask 2.0.2

前準備

以前から取りだめていたビリーの写真を4つの感情別にフォルダ分けした。
・happy 50枚
・angry 42枚
・sleepy 139枚
・cool 69枚
ビリーはどうやら眠いときによく写真を撮られているようだ。それか大抵眠い。
image.png

ディレクトリ構成

dev_home
┣ dataset_classifier
┃  ┣ happy
┃  ┣ angry
┃  ┣ sleepy
┃  ┗ cool
┗ app_classifier
   ┣ main.py # アプリ実行
   ┣ train.py # モデル学習
   ┣ model.h5 # モデル
   ┣ Procfile # Heroku設定ファイル
   ┣ requirements.txt # 仮想環境設定ファイル
   ┣ runtime.txt # Heroku設定ファイル
   ┣ static
   ┃  ┗ stylesheet.css
   ┣ templates
   ┃  ┗ index.html
   ┗ uploads # アップロードされた画像ファイルの格納先

モデルの学習

ライブラリのインポート

import os
import numpy as np
import cv2
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from tensorflow import keras
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras.layers import Dense, Dropout, Flatten, Input, Conv2D, MaxPooling2D
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.applications.vgg16 import VGG16
import matplotlib.pyplot as plt

学習データセットの生成

感情をlabel名としてデータセットフォルダ名と同一にした。
リストで扱うことにより、今後扱う感情が増えた際にもリストにlabelを追加するだけでよい。
今回はデータセットが合計300枚しかないため、テストデータは10%とした。
またラベル毎の偏りが大きいため、分割はstratifyを利用した。

label_list = ['happy', 'angry', 'sleepy', 'cool']

img_list = []
for label in label_list:
    img_list_temp = []
    img_file_list = os.listdir('../dataset_classifier/' + label + '/')
    for i in range(len(img_file_list)):
        img = cv2.imread('../dataset_classifier/' + label + '/' + img_file_list[i])
        img = cv2.resize(img, (200, 200))
        img_list_temp.append(img)
    img_list.append(img_list_temp)

img_list_concat = []
label_list_concat = []
for i, img_list_temp in enumerate(img_list):
    img_list_concat.extend(img_list_temp)
    label_list_concat.extend([i]*len(img_list_temp))

X = np.array(img_list_concat)
y = np.array(label_list_concat)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, stratify=y)

y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

モデルの学習

モデルはデータセットが少なかったため、VGG16の転移学習モデルとした。

input_tensor = Input(shape=(200, 200, 3))
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(0.2))
top_model.add(Dense(128, activation='relu'))
top_model.add(Dropout(0.2))
top_model.add(Dense(len(label_list), activation='softmax'))

model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))

for layer in model.layers[:19]:
    layer.trainable = False

callbacks = [keras.callbacks.EarlyStopping(monitor='val_loss', patience=5, verbose=0, mode='auto')]

model.compile(loss='categorical_crossentropy', optimizer='adam')
history = model.fit(X_train, y_train, batch_size=32, epochs=100,
                    validation_data=(X_test, y_test), callbacks=callbacks, verbose=2)

model.save('model.h5')

y_train = np.argmax(y_train, axis=1)
y_train_pred = np.argmax(model.predict(X_train), axis=1)
y_test = np.argmax(y_test, axis=1)
y_test_pred = np.argmax(model.predict(X_test), axis=1)

print('train accuracy: {}'.format(accuracy_score(y_train, y_train_pred)))
print('test accuracy: {}'.format(accuracy_score(y_test, y_test_pred)))

plt.plot(history.history['loss'], label="training")
plt.plot(history.history['val_loss'], label="validation")
plt.xlabel('Epochs')
plt.ylabel('loss')
plt.legend()
plt.savefig('./learning_curve.png')

epochsは100としたが、こちらもデータセットが少ないためEarlyStoppingで9epochsで学習は終了した。
ハイパーパラメータは適当なパターンで調整したが、過学習傾向が一番少なかった上記パラメータを採用した。

train accuracy: 0.9814814814814815
test accuracy: 0.8

learning_curve.png

モデルのデプロイ

FlaskサーバでHerokuにデプロイした。
Uploadされた画像はキャッシュして増えるのも困るのでbase64形式で埋め込むことにした。

import os
import base64
import numpy as np
from flask import Flask, request, redirect, render_template, flash
from werkzeug.utils import secure_filename
from tensorflow.keras.models import Sequential, load_model
from tensorflow.keras.preprocessing import image

classes = ['happy', 'angry', 'sleepy', 'cool']
image_size = 200

UPLOAD_FOLDER = 'uploads'
ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])

app = Flask(__name__)

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

model = load_model('./model.h5')


@app.route('/', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        if 'file' not in request.files:
            flash('ファイルがありません')
            return redirect(request.url)
        file = request.files['file']
        if file.filename == '':
            flash('ファイルがありません')
            return redirect(request.url)
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(UPLOAD_FOLDER, filename))
            filepath = os.path.join(UPLOAD_FOLDER, filename)

            img = image.load_img(filepath, grayscale=False, target_size=(image_size, image_size))
            img = image.img_to_array(img)
            data = np.array([img])

            result = model.predict(data)[0]
            predicted = result.argmax()
            pred_answer = 'Billy is ' + classes[predicted] + '.'

            with open(filepath, 'rb') as f:
                img_base64 = base64.b64encode(f.read()).decode('utf-8')

            return render_template('index.html', answer=pred_answer, img=img_base64)

    return render_template('index.html',answer='', img='')


if __name__ == '__main__':
    port = int(os.environ.get('PORT', 8080))
    app.run(host ='0.0.0.0',port = port)

良い感じ!
image.png

今後の展望

実はYoloを利用した物体検出のアプローチも検討していたが、どうしてもHerokuの無料枠のメモリサイズに乗らなかった。
また精度も悪く、物体検出モデルの性質上ハイパーパラメータで何とかすることも難しかった。
物体検出のデータセットでいうと4クラス分類であっても10倍程度のデータは必要であると感じた。
ただ我が家のネコが3匹写っている写真などを処理する場合、やはり物体検出のアプローチで個々に認識させたい。
データセット数の確保とGCPのGAEの無料枠の調査ができたら、別の記事でまとめたい。

今まで医薬品データセットなどテーブルデータしか扱ってこなかった私だが、
画像認識/物体検出/WebAppに触れたことはとても有意義だった。
医薬品のテーブルデータに対してCNNを利用したアプローチがよい結果を得た研究結果などもあるため、今後は多角的に学んでいきたいと思う。

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