概要
ネコの画像データセットをCNNに学習させ、
ネコの感情判定モデルを作成し、Webアプリとしてデプロイする。
まえがき
薬剤師資格持ち、駆け出しデータサイエンティストのhiroです。
我が家には三匹のネコがいる。
もちろん毎日愛でているが、やっぱり一番の悩みとしてお出かけがしにくい。
たまには遠出したい。
友人に留守番を頼むことはできるが、せっかくならネコたちと楽しく過ごしてほしい。
ネコと楽しく過ごすためには、ネコが今何を考えているのかがわからないといけない。
私は顔を見ているだけで大体何を考えているのかわかる(つもり)のだが、
友人に写真を送ると全部同じ表情に見えるようだ。
特に白猫のビリーは感情表現豊かなのだが、それでもやっぱりわかってもらえない。
happy | angry | sleepy | cool |
---|---|---|---|
そこで今回はビリーの写真をアップロードするとその時の感情を推定してくれるアプリを開発した。 |
開発環境
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枚
ビリーはどうやら眠いときによく写真を撮られているようだ。それか大抵眠い。
ディレクトリ構成
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
モデルのデプロイ
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)
今後の展望
実はYoloを利用した物体検出のアプローチも検討していたが、どうしてもHerokuの無料枠のメモリサイズに乗らなかった。
また精度も悪く、物体検出モデルの性質上ハイパーパラメータで何とかすることも難しかった。
物体検出のデータセットでいうと4クラス分類であっても10倍程度のデータは必要であると感じた。
ただ我が家のネコが3匹写っている写真などを処理する場合、やはり物体検出のアプローチで個々に認識させたい。
データセット数の確保とGCPのGAEの無料枠の調査ができたら、別の記事でまとめたい。
今まで医薬品データセットなどテーブルデータしか扱ってこなかった私だが、
画像認識/物体検出/WebAppに触れたことはとても有意義だった。
医薬品のテーブルデータに対してCNNを利用したアプローチがよい結果を得た研究結果などもあるため、今後は多角的に学んでいきたいと思う。