やりたいこと
pythonの機械学習ライブラリkerasを利用して
手がグー、チョキ、パーのどれを出しているのを判別するAIを作る。
データセット
今回は、Kaggleの公式ページより 「Rock Paper Scissors Dataset」という
グー、チョキ、パーの画像が合計2,892 枚入ったデータセットを利用する。
データセットをダウンロードして展開するとフォルダ構成は以下のようになっている。
今回は、train と testを利用してモデルを作成する。
フォルダの中身は Rock、Paper、Scissorsとフォルダが分かれている。
今回のモデルの構成
以下のような畳み込み層を2つ持つニューラルネットワークモデルを作る。
やってみた
モデル作成
train.py
上で示したとおりモデルを構築して学習する。
*勉強のために、冗長なコメントがあります。
from keras.models import Sequential
from keras.layers import Activation, Dense, Conv2D, MaxPool2D, Flatten
from keras.preprocessing.image import ImageDataGenerator
from keras.callbacks import ModelCheckpoint
# それぞれのチェックポイント(epoch)で作成されたモデルの結果を、h5形式で保存するためのCallBack関数
checkpoint = ModelCheckpoint(
filepath="models/model-{epoch:02d}-{val_accuracy:.2f}.h5",
monitor='val_accuracy',
save_best_only=False,
period=1)
def train():
model = Sequential()
# 1つ目の畳み込み層。フィルターは3x3で、入力画像の形状は300x300x3を指定している。
model.add(Conv2D(64, (3, 3), input_shape=(300, 300, 3)))
model.add(Activation('relu'))
# 1つ目のプーリング層。フィルターは2x2
model.add(MaxPool2D(pool_size=(2, 2)))
# 2つ目の畳み込み層。フィルターは3x3
model.add(Conv2D(64, (3, 3)))
model.add(Activation('relu'))
# 2つ目のプーリング層。フィルターは2x2
model.add(MaxPool2D(pool_size=(2, 2)))
# 全結合層。上記まで2D(2次元)で画像を取り扱ってきたので1次元に変換
model.add(Flatten())
model.add(Dense(256))
model.add(Activation('relu'))
model.add(Dense(3))
# 出力層の活性化関数はsoftmax関数を指定することで、それぞれのラベル(グー、チョキ、パー)の確率が出力される
model.add(Activation('softmax'))
# モデルの学習プロセスを設定。最適化アルゴリズムはadam、損失関数は分類の問題なので交差エントロピーを指定。
model.compile(
optimizer='adam',
loss='categorical_crossentropy',
metrics=['accuracy'])
# ImageDataGeneratorで、画像データの前処理やデータ拡張を行う。
# 以下は、訓練データ、テストデータを正規化(0~1の値に変換)している
train_data_gen = ImageDataGenerator(rescale=1.0/255.0)
validation_data_gen = ImageDataGenerator(rescale=1.0/255.0)
# 画像のサイズを300x300に変換
train_generator = train_data_gen.flow_from_directory(
'data/train',
target_size=(300, 300),
batch_size=10)
validation_generator = validation_data_gen.flow_from_directory(
'data/test',
target_size=(300, 300),
batch_size=10)
# 学習
model.fit_generator(
train_generator,
epochs=10,
steps_per_epoch=10,
validation_data=validation_generator,
validation_steps=2,
callbacks=[checkpoint],
shuffle=True)
if __name__ == '__main__':
train()
学習結果
fit_generatorの引数epochsを10に指定したので10回学習が行われ、以下のログが出力された。
- loss: 訓練データに対する損失率で、低ければ低いほど良い学習。
- accuracy: 訓練データに対する精度(正解率)で、高ければ高いほど良い。1.0が最大。
- val_loss: テストデータに対する損失率で、低ければ低いほど良い学習。
- val_accuracy: テストデータに対する精度(正解率)で、高ければ高いほど良い。1.0が最大。
Epoch 1/10
10/10 [==============================] - 29s 3s/step - loss: 32.0930 - accuracy: 0.2500 - val_loss: 1.2552 - val_accuracy: 0.3500
Epoch 2/10
10/10 [==============================] - 30s 3s/step - loss: 1.0925 - accuracy: 0.5000 - val_loss: 1.0681 - val_accuracy: 0.5000
Epoch 3/10
10/10 [==============================] - 25s 3s/step - loss: 0.9632 - accuracy: 0.7600 - val_loss: 1.0420 - val_accuracy: 0.5500
Epoch 4/10
10/10 [==============================] - 25s 3s/step - loss: 0.6930 - accuracy: 0.8200 - val_loss: 1.0092 - val_accuracy: 0.4000
Epoch 5/10
10/10 [==============================] - 25s 3s/step - loss: 0.4654 - accuracy: 0.8600 - val_loss: 0.9532 - val_accuracy: 0.7000
Epoch 6/10
10/10 [==============================] - 25s 3s/step - loss: 0.5888 - accuracy: 0.8000 - val_loss: 0.9175 - val_accuracy: 0.6000
Epoch 7/10
10/10 [==============================] - 25s 3s/step - loss: 0.3620 - accuracy: 0.8900 - val_loss: 0.7328 - val_accuracy: 0.8000
Epoch 8/10
10/10 [==============================] - 25s 3s/step - loss: 0.2545 - accuracy: 0.9300 - val_loss: 2.5089 - val_accuracy: 0.2000
Epoch 9/10
10/10 [==============================] - 25s 3s/step - loss: 0.2912 - accuracy: 0.9400 - val_loss: 0.8526 - val_accuracy: 0.8500
Epoch 10/10
10/10 [==============================] - 24s 2s/step - loss: 0.1690 - accuracy: 0.9600 - val_loss: 0.4902 - val_accuracy: 0.7500
また今回CallBack関数で、それぞれのチェックポイント(epoch)で作成されたモデルの結果を、
h5形式で保存するようにしていたため、指定のフォルダにファイルが作成されている。
モデル選定
上記結果から、モデルを選ぶ。
今回は、精度(正解率)に着目する。ポイントは、accuracy と val_accuracyどちらも高いものを選ぶことらしい。特にaccuracyが高く、val_accuracyが低い場合は、未知のデータへの予測に弱いことを意味するため注意が必要。
今回はaccuracy と val_accuracyどちらも高いecoch数9回目に作成されたモデル(models/model-09-0.85.h5)を選択することにする。
モデルを試してみる
rps_classifier.py
画像からじゃんけんの手を分類するクラス。
from keras.models import load_model
import numpy as np
import cv2
class Rps_classifier:
'''
classify rps(Rock-Paper-Scissors)
'''
def __init__(self, model_path):
self.model = load_model(model_path)
self.labels = ['paper', 'rock', 'scissors', 'nothing']
def preprocess(self, image):
image = image / 255
image_resized = cv2.resize(image, (300, 300))
image_reshaped = image_resized[np.newaxis, :, :, :]
image_preprocessed = np.array(image_reshaped)
return image_preprocessed
def post_process(self, model_output, prob_threshold):
prob_paper = model_output[0][0] * 100
prob_rock = model_output[0][1] * 100
prob_scissors = model_output[0][2] * 100
rps_prob_list = [prob_paper, prob_rock, prob_scissors]
print(rps_prob_list)
max_prob = max(rps_prob_list)
if max_prob > prob_threshold:
max_index = rps_prob_list.index(max_prob)
return self.labels[max_index]
# 確率の閾値より低い場合は,nothingとして返す
else:
return self.labels[3]
def predict(self, image, prob_threshold):
input_image = self.preprocess(image)
model_output = self.model.predict(input_image)
result = self.post_process(model_output, prob_threshold)
return result
main.py
import cv2
from rps_classifier import Rps_classifier
def main():
model_path = 'models/model-09-0.85.h5'
model = Rps_classifier(model_path)
# 低い信頼度のモデル出力を除くための閾値
prob_threshold = 70.0
image_path = 'data/validation/paper/paper-hires2.png'
image = cv2.imread(image_path)
out_put = model.predict(image, prob_threshold)
print(out_put)
if __name__ == '__main__':
main()
インプット画像
学習にも、テストにも使っていない画像を入力に実行してみる。
実行結果
main.pyを実行した結果以下のようになった。
1行目は、モデルのアプトプットで、'paper', 'rock', 'scissors'の順に
信頼度(そのラベルである確率)が格納したリスト。
2行目は、後処理した結果で最も可能性の高いラベル名。
見てみると期待通りpaper(パー)という結果を得ることができている。
[78.24956178665161, 1.3062641955912113, 20.444172620773315]
paper