1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

kerasを使って、画像からじゃんけんの手を判別する分類AIを作る

Last updated at Posted at 2023-05-31

やりたいこと

pythonの機械学習ライブラリkerasを利用して
手がグー、チョキ、パーのどれを出しているのを判別するAIを作る。

データセット

今回は、Kaggleの公式ページより 「Rock Paper Scissors Dataset」という
グー、チョキ、パーの画像が合計2,892 枚入ったデータセットを利用する。
スクリーンショット 2023-05-29 23.10.17.png
データセットをダウンロードして展開するとフォルダ構成は以下のようになっている。
今回は、train と testを利用してモデルを作成する。
スクリーンショット 2023-05-29 23.05.28.png
フォルダの中身は Rock、Paper、Scissorsとフォルダが分かれている。
スクリーンショット 2023-05-31 23.45.52.png

今回のモデルの構成

以下のような畳み込み層を2つ持つニューラルネットワークモデルを作る。
スクリーンショット 2023-05-30 0.18.11.png

やってみた

モデル作成

train.py

上で示したとおりモデルを構築して学習する。
*勉強のために、冗長なコメントがあります。

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形式で保存するようにしていたため、指定のフォルダにファイルが作成されている。
スクリーンショット 2023-06-01 0.04.45.png

モデル選定

上記結果から、モデルを選ぶ。
今回は、精度(正解率)に着目する。ポイントは、accuracy と val_accuracyどちらも高いものを選ぶことらしい。特にaccuracyが高く、val_accuracyが低い場合は、未知のデータへの予測に弱いことを意味するため注意が必要。

今回はaccuracy と val_accuracyどちらも高いecoch数9回目に作成されたモデル(models/model-09-0.85.h5)を選択することにする。

モデルを試してみる

スクリプトを追加。
スクリーンショット 2023-06-01 0.18.40.png

rps_classifier.py

画像からじゃんけんの手を分類するクラス。

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

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()

インプット画像

学習にも、テストにも使っていない画像を入力に実行してみる。

paper-hires2.png

実行結果

main.pyを実行した結果以下のようになった。
1行目は、モデルのアプトプットで、'paper', 'rock', 'scissors'の順に
信頼度(そのラベルである確率)が格納したリスト。
2行目は、後処理した結果で最も可能性の高いラベル名。

見てみると期待通りpaper(パー)という結果を得ることができている。

[78.24956178665161, 1.3062641955912113, 20.444172620773315]
paper
1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?