【Python】デレマスアイドルで学ぶ顔認識

2021年12月19日ブログAdC2021,ブログ

 この投稿は 東京工業大学アイドルマスター研究会 Advent Calendar 2021 13日目となります。

 現在、東京工業大学アイドルマスター研究会(テクマス)では、12月1日から25日まで毎日1~2個ずつリレー形式で記事を更新していくアドベントカレンダーという企画を行っています。

 記事の一覧は AdC2021 のタグで確認できるので、ぜひそちらも見に行ってみてください!


目次

長いので、結果だけ気になる方は 使ってみる からどうぞ。

自己紹介/Abstraction

 おはようございます。副代表2年目のこーちゃんです。Twitterは@fl_cl_skとか@fl_cl_pとかです。最近は専ら一個目のアカウントを使っています。
 突然ですが、アイドルの顔面って素敵ですよね。

これは最愛の少女である橘ありすさんのカード「[ありすの物語]橘ありす」
豆知識!

橘ありすさんの顔は、良い。

 そこで、アイドルの顔面を畳み込みニューラルネットワークに食わせてその良さを機械にも教え込んでやりましょう。

豆知識!

上の画像からは佐藤心さんの顔も抽出できます。
結城晴さんは横を向いているのでしんどい。

手法

 学習用の画像を用意し、 OpenCV に搭載されている顔検出用モデル lbpcascades を二次元画像用に調整したモデル lbpcascade_animeface を用いて顔部分を切り抜きます。
 切り抜いた画像を利用して、 Keras に搭載されている学習済みモデル Inception-V3 を、デレマスアイドルの顔の識別用に転移学習 / ファインチューニングします。

環境について

 今回はWindows10のWSL2を利用します。ディストリビューションは Ubuntu 20.04 です。Pythonのバージョンは 3.8.8 を使いました。 Tensorflow, Keras のバージョンはともに 2.7.0 です。
 詳しい方はお気づきかもしれませんが、Windows10のWSL2では CUDA on WSL が利用できません(最近のアップデートで使えるようになった?らしい)。そのため今回は全部CPUで学習させました。
 また、 Jupyter Lab を利用してコーディングをしていきました。細かい環境構築については割愛しますが、質問等ありましたらお気軽にどうぞ。

データ収集・準備

 学習用データとしてモバマスに実装されている(ほぼ)全カードの画像を用意しました。モバマスのカードをまとめているウェブサイトからスクレイピングしましたが、サーバー負荷の原因となってしまうと良くないので細かいコードは省略します。私はひと晩かけてゆっくり保存しました。また、モバマスにはASのアイドル等も実装されているので、そこらへんもまとめて持ってきました。

 次に、保存してきた画像から顔を切り抜いていきます。 OpenCV と、二次元画像の顔検出ができるモデル lbpcascades を利用します。出力される画像のサイズは 256*256 にしました。

import cv2
import os
import glob
cascade = cv2.CascadeClassifier('lbpcascade_animeface.xml')

load_img_list = []
face_cut = []
count = 0
# 切り抜いた画像は data/train/face/アイドル名 以下に保存される
data_face_path = 'data/train/face/'

# オリジナル画像は data/train/original/橘ありす/[ヴィンテージタイム]橘ありす.png のように保存されている
files = glob.glob('data/train/original/*/*.*')
for file in files:
    idol_name, card_name = file.split('/')[3:5]
    os.makedirs(f'{data_face_path}/{idol_name}', exist_ok=True)
    original = cv2.imread(file)
    face_rects = cascade.detectMultiScale(original, scaleFactor=1.11)
    print(card_name, len(face_rects))
    if(len(face_rects) !=0):
        for face_rect in face_rects:
            x = face_rect[0]
            y = face_rect[1]
            w = face_rect[2]
            h = face_rect[3]
            face = original[y:y + h, x:x + w]
            face = cv2.resize(face, dsize =(256, 256))
            cv2.imwrite(f'{data_face_path}/{idol_name}/face_{card_name}', face)

 これで、顔を切り抜いた画像が生成できます。正しく検出できなかったり、顔でない場所が切り抜かれてしまうこともありますが、今回は選別が面倒なので特に気にせず進めます。

成功例1 : [ガール・ソー・スイート]橘ありす+
かわいいね
豆知識!

筆者はこのカードから逃げていたので執筆中に初めて向き合ったのですが、あまりにもえっちすぎて動けなくなってしまったらしい。

成功例2 : [ちいさな手のひら]佐城雪美+
かわいいね

モデル学習

データ読み込み

 さて、本題に入っていきます。先程作ったデータを ImageDataGenerator を用いて読み込みます。これを用いると、Data Augmentation、即ち水増しが簡単に行なえます。例えば width_shift_range=0.2 とすると、横幅をランダムで 0.8~1.2 倍してくれます。水増しをすることでモデルの過学習を抑制し、汎用性を向上させることができます。また、訓練用データと検証用データへの分割も自動でやってくれます。

from tensorflow.keras.preprocessing import image
batch_size=32
seed=1

datagen = image.ImageDataGenerator(
    rescale=1./255,
    validation_split=0.1,
    horizontal_flip=True,
    width_shift_range=0.2,
    height_shift_range=0.2,
    shear_range=0.2,
    fill_mode='nearest'
    )

train_generator = datagen.flow_from_directory(
    'data/train/face',
    target_size=(256, 256),
    class_mode='categorical',
    batch_size=batch_size,
    subset='training',
    seed=seed,
)
val_generator = datagen.flow_from_directory(
    'data/train/face',
    target_size=(256, 256),
    class_mode='categorical',
    batch_size=batch_size,
    subset='validation',
    seed=seed,
)
Found 5477 images belonging to 212 classes.
Found 499 images belonging to 212 classes.

モデル準備

 次に、今回学習させるモデルを用意します。畳み込みニューラルネットワーク(CNN)を用いますが、新たなモデルをいちから作るのではなく、「学習済みモデル」を活用します。 Keras には学習済みモデルがいくつか搭載されていますが、今回は InceptionV3 を使います。これは、 ImageNet と呼ばれるカラー写真のデータベースをもとに学習したモデルです。めちゃめちゃ層があります。このモデルの重みを再学習させ、効率的にモデルを作ります。

from tensorflow.keras.applications.inception_v3 import InceptionV3
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Dense, GlobalAveragePooling2D
base_model = InceptionV3(weights='imagenet', include_top=False, input_shape=(256, 256, 3))

# 特徴量から推定結果を求める全結合層を再定義する
x = base_model.output
x = GlobalAveragePooling2D()(x)
x = Dense(1024, activation='relu')(x)
predictions = Dense(len(set(train_generator.labels)), activation='softmax')(x)

model = Model(inputs=base_model.input, outputs=predictions)

予習

 本格的な学習に入る前に、先程追加した全結合層のみ先行して学習させます。これをすると良いらしいです、多分。また、学習度合いを評価する値として loss, accuracy, val_loss, val_accuracy が存在します。 val_ とついているものは検証用データによるもの、ついていないものは訓練用データによるものです。 loss は予測した値と真の値の差を表します。今回は複数クラスへの分類なので categorical_crossentropy を用いました。 accuracy は名前の通り正解率です。
 また、学習用のoptimizerとして Momentum SGD を利用しました。確率的な勾配降下に加え前回の移動を慣性として利用することで振動等を抑制できるらしいです。へ~

from tensorflow.keras.optimizers import SGD

# 全結合層以外の重みを固定
for layer in base_model.layers:
    layer.trainable = False

model.compile(optimizer=SGD(learning_rate=0.001, momentum=0.9), loss='categorical_crossentropy', metrics=['accuracy'])
model.fit(train_generator, epochs=10, verbose=1, validation_data=val_generator)

 10Epoch (10世代) 学習させた結果が以下のとおりです。長いので興味がある人だけ見てください。基本的には val_loss が小さく、 val_accuracy が大きいほど嬉しいです。もちろん accuracy などが大きくてもいいのですが、 accuracy が大きいのにも関わらず val_accuracy が小さいと要注意です。過学習と呼ばれる、学習用のデータセットに過剰に適合してしまい、汎用性が失われた状態になってしまいます。

Epoch 1/10 172/172 [==============================] - 101s 575ms/step - loss: 5.3155 - accuracy: 0.0131 - val_loss: 5.1399 - val_accuracy: 0.0341 
Epoch 2/10 172/172 [==============================] - 96s 556ms/step - loss: 5.0802 - accuracy: 0.0314 - val_loss: 4.9781 - val_accuracy: 0.0200 
Epoch 3/10 172/172 [==============================] - 94s 544ms/step - loss: 4.8808 - accuracy: 0.0652 - val_loss: 4.7750 - val_accuracy: 0.0641 
Epoch 4/10 172/172 [==============================] - 94s 545ms/step - loss: 4.6776 - accuracy: 0.0913 - val_loss: 4.6110 - val_accuracy: 0.0902 
Epoch 5/10 172/172 [==============================] - 94s 543ms/step - loss: 4.4640 - accuracy: 0.1223 - val_loss: 4.4066 - val_accuracy: 0.1403 
Epoch 6/10 172/172 [==============================] - 94s 543ms/step - loss: 4.2471 - accuracy: 0.1658 - val_loss: 4.2414 - val_accuracy: 0.1683 
Epoch 7/10 172/172 [==============================] - 93s 542ms/step - loss: 4.0436 - accuracy: 0.1917 - val_loss: 4.0589 - val_accuracy: 0.1784 
Epoch 8/10 172/172 [==============================] - 93s 542ms/step - loss: 3.8365 - accuracy: 0.2319 - val_loss: 3.9492 - val_accuracy: 0.1844 
Epoch 9/10 172/172 [==============================] - 93s 542ms/step - loss: 3.6672 - accuracy: 0.2631 - val_loss: 3.7774 - val_accuracy: 0.1984 
Epoch 10/10 172/172 [==============================] - 93s 542ms/step - loss: 3.4986 - accuracy: 0.2868 - val_loss: 3.6397 - val_accuracy: 0.2405

 現在の学習結果は loss: 3.4986 – accuracy: 0.2868 – val_loss: 3.6397 – val_accuracy: 0.2405 となりました。予習なのでこのくらいでいいでしょう。それでは、本学習に入ります。

豆知識!

このPCを買ってから初めてCPU使用率が70%を上回りました。つよつよCPUでよかった~

ファインチューニング

 いよいよモデルを本格的に学習させていきます。全ての層の重みを学習させるのではなく、下の方の層、具体的には250層以降のみ学習させていきます。また、 70Epoch 学習させるのですが、最後に完成するのが最も優秀なモデルとは限りません。過学習が起こってしまうからです。そのため、学習していく中で val_loss が最も小さいモデルを自動で保存するように ModelCheckpoint を作ります。

from tensorflow.keras.callbacks import ModelCheckpoint
checkpoint = ModelCheckpoint(
                    filepath='model_with_inceptionv3.h5',
                    monitor='val_loss',
                    save_best_only=True,
                )

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

model.compile(optimizer=SGD(learning_rate=0.001, momentum=0.9), loss='categorical_crossentropy', metrics=['accuracy'])

model.fit(train_generator, epochs=70, verbose=1, validation_data=val_generator, callbacks=[checkpoint])

Epoch 1/70
172/172 [==============================] - 114s 645ms/step - loss: 3.9103 - accuracy: 0.2353 - val_loss: 3.1705 - val_accuracy: 0.2886
Epoch 2/70
172/172 [==============================] - 109s 633ms/step - loss: 2.7611 - accuracy: 0.4614 - val_loss: 2.4455 - val_accuracy: 0.4770
Epoch 3/70
172/172 [==============================] - 109s 632ms/step - loss: 2.0461 - accuracy: 0.6211 - val_loss: 1.8998 - val_accuracy: 0.6373
Epoch 4/70
172/172 [==============================] - 109s 631ms/step - loss: 1.5752 - accuracy: 0.7128 - val_loss: 1.6203 - val_accuracy: 0.6653
Epoch 5/70
172/172 [==============================] - 108s 627ms/step - loss: 1.2187 - accuracy: 0.7836 - val_loss: 1.3517 - val_accuracy: 0.7415
Epoch 6/70
172/172 [==============================] - 108s 625ms/step - loss: 1.0062 - accuracy: 0.8169 - val_loss: 1.1345 - val_accuracy: 0.7495
Epoch 7/70
172/172 [==============================] - 108s 626ms/step - loss: 0.8404 - accuracy: 0.8486 - val_loss: 1.0248 - val_accuracy: 0.7916
Epoch 8/70
172/172 [==============================] - 107s 619ms/step - loss: 0.7138 - accuracy: 0.8674 - val_loss: 0.9408 - val_accuracy: 0.8156
Epoch 9/70
172/172 [==============================] - 107s 621ms/step - loss: 0.5898 - accuracy: 0.8967 - val_loss: 0.8675 - val_accuracy: 0.8176
Epoch 10/70
172/172 [==============================] - 107s 623ms/step - loss: 0.5286 - accuracy: 0.9118 - val_loss: 0.8268 - val_accuracy: 0.8297
Epoch 11/70
172/172 [==============================] - 110s 639ms/step - loss: 0.4657 - accuracy: 0.9177 - val_loss: 0.7773 - val_accuracy: 0.8156
Epoch 12/70
172/172 [==============================] - 109s 631ms/step - loss: 0.4082 - accuracy: 0.9330 - val_loss: 0.6919 - val_accuracy: 0.8597
Epoch 13/70
172/172 [==============================] - 107s 624ms/step - loss: 0.3621 - accuracy: 0.9421 - val_loss: 0.7113 - val_accuracy: 0.8497
Epoch 14/70
172/172 [==============================] - 108s 625ms/step - loss: 0.3205 - accuracy: 0.9496 - val_loss: 0.7050 - val_accuracy: 0.8677
Epoch 15/70
172/172 [==============================] - 108s 627ms/step - loss: 0.2828 - accuracy: 0.9544 - val_loss: 0.6494 - val_accuracy: 0.8657
Epoch 16/70
172/172 [==============================] - 108s 625ms/step - loss: 0.2636 - accuracy: 0.9598 - val_loss: 0.6736 - val_accuracy: 0.8537
Epoch 17/70
172/172 [==============================] - 108s 626ms/step - loss: 0.2287 - accuracy: 0.9664 - val_loss: 0.6250 - val_accuracy: 0.8637
Epoch 18/70
172/172 [==============================] - 108s 624ms/step - loss: 0.2145 - accuracy: 0.9673 - val_loss: 0.6093 - val_accuracy: 0.8697
Epoch 19/70
172/172 [==============================] - 108s 626ms/step - loss: 0.1950 - accuracy: 0.9741 - val_loss: 0.6015 - val_accuracy: 0.8677
Epoch 20/70
172/172 [==============================] - 108s 625ms/step - loss: 0.1707 - accuracy: 0.9790 - val_loss: 0.6313 - val_accuracy: 0.8577
Epoch 21/70
172/172 [==============================] - 109s 632ms/step - loss: 0.1519 - accuracy: 0.9805 - val_loss: 0.6314 - val_accuracy: 0.8537
Epoch 22/70
172/172 [==============================] - 108s 625ms/step - loss: 0.1431 - accuracy: 0.9828 - val_loss: 0.6435 - val_accuracy: 0.8457
Epoch 23/70
172/172 [==============================] - 108s 624ms/step - loss: 0.1375 - accuracy: 0.9812 - val_loss: 0.6053 - val_accuracy: 0.8737
Epoch 24/70
172/172 [==============================] - 107s 623ms/step - loss: 0.1215 - accuracy: 0.9869 - val_loss: 0.5984 - val_accuracy: 0.8737
Epoch 25/70
172/172 [==============================] - 107s 622ms/step - loss: 0.1134 - accuracy: 0.9870 - val_loss: 0.6285 - val_accuracy: 0.8577
Epoch 26/70
172/172 [==============================] - 108s 629ms/step - loss: 0.1078 - accuracy: 0.9890 - val_loss: 0.5823 - val_accuracy: 0.8838
Epoch 27/70
172/172 [==============================] - 108s 627ms/step - loss: 0.0997 - accuracy: 0.9883 - val_loss: 0.5572 - val_accuracy: 0.8978
Epoch 28/70
172/172 [==============================] - 108s 627ms/step - loss: 0.0824 - accuracy: 0.9927 - val_loss: 0.5903 - val_accuracy: 0.8798
Epoch 29/70
172/172 [==============================] - 108s 626ms/step - loss: 0.0944 - accuracy: 0.9870 - val_loss: 0.5572 - val_accuracy: 0.8798
Epoch 30/70
172/172 [==============================] - 108s 625ms/step - loss: 0.0843 - accuracy: 0.9901 - val_loss: 0.5756 - val_accuracy: 0.8717
Epoch 31/70
172/172 [==============================] - 108s 625ms/step - loss: 0.0763 - accuracy: 0.9934 - val_loss: 0.5783 - val_accuracy: 0.8697
Epoch 32/70
172/172 [==============================] - 108s 625ms/step - loss: 0.0711 - accuracy: 0.9929 - val_loss: 0.5826 - val_accuracy: 0.8737
Epoch 33/70
172/172 [==============================] - 108s 625ms/step - loss: 0.0711 - accuracy: 0.9934 - val_loss: 0.5319 - val_accuracy: 0.8778
Epoch 34/70
172/172 [==============================] - 108s 625ms/step - loss: 0.0620 - accuracy: 0.9934 - val_loss: 0.5279 - val_accuracy: 0.9058
Epoch 35/70
172/172 [==============================] - 110s 641ms/step - loss: 0.0674 - accuracy: 0.9923 - val_loss: 0.5683 - val_accuracy: 0.8717
Epoch 36/70
172/172 [==============================] - 108s 627ms/step - loss: 0.0607 - accuracy: 0.9921 - val_loss: 0.5715 - val_accuracy: 0.8737
Epoch 37/70
172/172 [==============================] - 107s 622ms/step - loss: 0.0546 - accuracy: 0.9949 - val_loss: 0.5611 - val_accuracy: 0.8898
Epoch 38/70
172/172 [==============================] - 106s 618ms/step - loss: 0.0493 - accuracy: 0.9965 - val_loss: 0.5690 - val_accuracy: 0.8878
Epoch 39/70
172/172 [==============================] - 106s 615ms/step - loss: 0.0538 - accuracy: 0.9945 - val_loss: 0.5794 - val_accuracy: 0.8778
Epoch 40/70
172/172 [==============================] - 107s 620ms/step - loss: 0.0540 - accuracy: 0.9936 - val_loss: 0.5131 - val_accuracy: 0.8878
Epoch 41/70
172/172 [==============================] - 107s 621ms/step - loss: 0.0488 - accuracy: 0.9951 - val_loss: 0.5481 - val_accuracy: 0.8778
Epoch 42/70
172/172 [==============================] - 106s 617ms/step - loss: 0.0471 - accuracy: 0.9942 - val_loss: 0.5189 - val_accuracy: 0.8858
Epoch 43/70
172/172 [==============================] - 106s 613ms/step - loss: 0.0461 - accuracy: 0.9942 - val_loss: 0.5638 - val_accuracy: 0.8737
Epoch 44/70
172/172 [==============================] - 106s 616ms/step - loss: 0.0435 - accuracy: 0.9958 - val_loss: 0.5443 - val_accuracy: 0.8878
Epoch 45/70
172/172 [==============================] - 106s 615ms/step - loss: 0.0424 - accuracy: 0.9958 - val_loss: 0.5390 - val_accuracy: 0.8858
Epoch 46/70
172/172 [==============================] - 106s 614ms/step - loss: 0.0391 - accuracy: 0.9971 - val_loss: 0.5297 - val_accuracy: 0.8918
Epoch 47/70
172/172 [==============================] - 106s 615ms/step - loss: 0.0377 - accuracy: 0.9969 - val_loss: 0.5252 - val_accuracy: 0.8938
Epoch 48/70
172/172 [==============================] - 107s 620ms/step - loss: 0.0349 - accuracy: 0.9980 - val_loss: 0.5341 - val_accuracy: 0.8878
Epoch 49/70
172/172 [==============================] - 107s 622ms/step - loss: 0.0411 - accuracy: 0.9949 - val_loss: 0.5603 - val_accuracy: 0.8938
Epoch 50/70
172/172 [==============================] - 107s 620ms/step - loss: 0.0401 - accuracy: 0.9956 - val_loss: 0.5689 - val_accuracy: 0.8858
Epoch 51/70
172/172 [==============================] - 107s 623ms/step - loss: 0.0376 - accuracy: 0.9956 - val_loss: 0.5428 - val_accuracy: 0.8898
Epoch 52/70
172/172 [==============================] - 107s 624ms/step - loss: 0.0363 - accuracy: 0.9962 - val_loss: 0.5107 - val_accuracy: 0.8878
Epoch 53/70
172/172 [==============================] - 108s 625ms/step - loss: 0.0352 - accuracy: 0.9962 - val_loss: 0.5066 - val_accuracy: 0.9038
Epoch 54/70
172/172 [==============================] - 108s 626ms/step - loss: 0.0285 - accuracy: 0.9982 - val_loss: 0.5585 - val_accuracy: 0.8938
Epoch 55/70
172/172 [==============================] - 107s 623ms/step - loss: 0.0293 - accuracy: 0.9974 - val_loss: 0.5565 - val_accuracy: 0.8978
Epoch 56/70
172/172 [==============================] - 107s 623ms/step - loss: 0.0316 - accuracy: 0.9967 - val_loss: 0.5535 - val_accuracy: 0.8998
Epoch 57/70
172/172 [==============================] - 107s 621ms/step - loss: 0.0316 - accuracy: 0.9963 - val_loss: 0.5298 - val_accuracy: 0.8938
Epoch 58/70
172/172 [==============================] - 107s 621ms/step - loss: 0.0269 - accuracy: 0.9991 - val_loss: 0.5292 - val_accuracy: 0.8838
Epoch 59/70
172/172 [==============================] - 107s 622ms/step - loss: 0.0271 - accuracy: 0.9978 - val_loss: 0.5850 - val_accuracy: 0.8858
Epoch 60/70
172/172 [==============================] - 107s 623ms/step - loss: 0.0280 - accuracy: 0.9973 - val_loss: 0.5839 - val_accuracy: 0.8758
Epoch 61/70
172/172 [==============================] - 107s 620ms/step - loss: 0.0291 - accuracy: 0.9969 - val_loss: 0.5906 - val_accuracy: 0.8858
Epoch 62/70
172/172 [==============================] - 107s 620ms/step - loss: 0.0267 - accuracy: 0.9984 - val_loss: 0.5301 - val_accuracy: 0.8958
Epoch 63/70
172/172 [==============================] - 107s 619ms/step - loss: 0.0238 - accuracy: 0.9978 - val_loss: 0.5541 - val_accuracy: 0.8938
Epoch 64/70
172/172 [==============================] - 106s 614ms/step - loss: 0.0225 - accuracy: 0.9987 - val_loss: 0.5440 - val_accuracy: 0.8918
Epoch 65/70
172/172 [==============================] - 106s 616ms/step - loss: 0.0217 - accuracy: 0.9989 - val_loss: 0.5507 - val_accuracy: 0.8998
Epoch 66/70
172/172 [==============================] - 108s 625ms/step - loss: 0.0197 - accuracy: 0.9989 - val_loss: 0.5542 - val_accuracy: 0.8818
Epoch 67/70
172/172 [==============================] - 107s 620ms/step - loss: 0.0214 - accuracy: 0.9980 - val_loss: 0.5391 - val_accuracy: 0.8978
Epoch 68/70
172/172 [==============================] - 107s 619ms/step - loss: 0.0233 - accuracy: 0.9982 - val_loss: 0.5311 - val_accuracy: 0.9058
Epoch 69/70
172/172 [==============================] - 107s 623ms/step - loss: 0.0226 - accuracy: 0.9980 - val_loss: 0.5170 - val_accuracy: 0.8858
Epoch 70/70
172/172 [==============================] - 107s 624ms/step - loss: 0.0214 - accuracy: 0.9984 - val_loss: 0.5350 - val_accuracy: 0.8938

 70Epoch 学習させました。結構時間がかかりましたね。 val_loss が最小になっているのは 53Epoch 目の loss: 0.0352 – accuracy: 0.9962 – val_loss: 0.5066 – val_accuracy: 0.9038 ですので、このモデルが自動で保存されました。では、このモデルを実際に使ってみましょう。

使ってみる

 上で学習し、自動的に保存されたモデルは model_with_inceptionv3.h5 という名前で保存されています。これを読み込みましょう。

from tensorflow.keras.models import load_model

model = load_model('model_with_inceptionv3.h5')

 まず、試しに検証用データとして用いたモバマスの画像で確認してみましょう。

from tensorflow.keras.preprocessing.image import ImageDataGenerator
import cv2
from matplotlib import pyplot as plt
import pandas as pd
import numpy as np
data_generator = ImageDataGenerator(rescale=1./255, validation_split=0.1).flow_from_directory(
    'data/face',
    target_size=(256, 256),
    class_mode='categorical',
    batch_size=1,
    subset='validation',
    seed=1
)

for _ in range(5):
    image = data_generator.next()
    
    plt.axis("off")
    plt.imshow(image[0][0])
    plt.show()

    indices_to_class = {val: key for key, val in data_generator.class_indices.items()}
    pred = np.array(sorted([(indices_to_class[i], x) for i, x in enumerate(model.predict(image[0])[0])], key=lambda x: -x[1]))

    display(pd.DataFrame({
        '名前': pred[:5].T[0],
        '推測値': pred[:5].T[1],
    }))

[《偶像》のフラグメント]二宮飛鳥
[ひな祭り]松尾千鶴+
[お屋敷セレクション]川島瑞樹+
[峻烈闘技]大和亜季+
[お化け屋敷I.C]古澤頼子+

 いい感じに分類できていますね。次に、確認用のデータを用意します。学習に使用しなかったデレステのカードを引っ張ってきて、 data/test/original/ に保存しておきます。今回は顔が複数検出されることが想定できるので、各カードごとにフォルダを作り、検出された顔を全て保存するようにしてあります。

import cv2
import glob
import os
import shutil
cascade = cv2.CascadeClassifier('lbpcascade_animeface.xml')
load_img_list = []
face_cut = []
count = 0
data_face_path = 'data/test/face'

shutil.rmtree(data_face_path)
os.mkdir(data_face_path)

files = glob.glob('data/test/original/*.*')
for file in files:
    file_name = file.split('/')[3]
    original = cv2.imread(file)
    face_rects = cascade.detectMultiScale(original, scaleFactor=1.11)
    if(len(face_rects) != 0):
        os.mkdir(f'{data_face_path}/{file_name.split(".")[0]}')
        for face_rect in face_rects:
            count += 1
            x = face_rect[0]
            y = face_rect[1]
            w = face_rect[2]
            h = face_rect[3]
            face = original[y:y + h, x:x + w]
            face = cv2.resize(face, dsize =(256, 256))
            cv2.imwrite(f'{data_face_path}/{file_name.split(".")[0]}/{count}.jpg', face)

 続けて、生成した画像を先程のモデルに通してみます。これらの画像は学習に直接用いてはいないので、正確に分類できていれば嬉しいですね。

data_generator2 = ImageDataGenerator(rescale=1./255).flow_from_directory(
    'data/test/face',
    target_size=(256, 256),
    batch_size=1,
    seed=1
)
print()
indices_to_class = {val: key for key, val in data_generator.class_indices.items()}
indices_to_class_2 = {val: key for key, val in data_generator2.class_indices.items()}
for _ in range(count):    
    image = data_generator2.next()
    print(indices_to_class_2[np.where(image[-1][0]==1)[0][0]])
    plt.axis("off")
    plt.imshow(image[0][0])
    plt.show()
    
    pred = np.array(sorted([(indices_to_class[i], x) for i, x in enumerate(model.predict(image[0])[0])], key=lambda x: -x[1]))

    display(pd.DataFrame({
        '名前': pred[:5].T[0],
        '推測値': pred[:5].T[1],
    }))

結果

[ありすの物語]橘ありす
結果1 : 橘ありす
結果2 : 佐藤心

 いい感じですね。他のカードでも試してみましょう。

[アドマイヤ・ブライド]橘ありす+
結果 : 橘ありす
豆知識!

橘ありすさんと結婚させていただくことになりました、こーちゃんです。これからも日々精進し、彼女を幸せにしてみせます。今後ともよろしくお願いいたします。

 どんどん行きましょう。次はこのカードです。

[キトゥンズガーデン]佐城雪美
結果1 : 佐城雪美
豆知識!

佐城雪美さんと結婚させていただくことになりました、こーちゃんです。これからも日々精進し、彼女を幸せにしてみせます。今後ともよろしくお願いいたします。

結果2 : 佐々木千枝
結果3 : 榊原里美…………??????

 ということで、誤検出です。これは成宮由愛ちゃんのはずですが、榊原里美ちゃんであると認識してしまいました。確かに似ている気もする。試しに、由愛ちゃんのカードと里美ちゃんのカードを持ってきて食わせてみましょう。

[色とりどりのゆめ]成宮由愛+
結果 : 成宮由愛

 圧倒的なスコアで由愛ちゃんになりましたが、2番目の予測結果は里美ちゃんになっていますね。

[ほわあまプリンセス]榊原里美+
結果 : 榊原里美

 なるほど、こちらも予測結果に由愛ちゃんがいますが、数値としてはかなり小さいですね。これは勘なのですが、瞳の色あたりが関係している気がします。残念ながら、デレステのカードで目を瞑っているものが2人とも無かったため、今回の真相は不明です。

 他の似ているアイドルでも試してみましょう。

[笑顔のレセプション]高森藍子
結果1 : 高森藍子

 やはり十時愛梨さんが上位に出てきました。数値も結構大きいです。この2人は似ているらしいです。一緒に検出された智絵里ちゃんの結果も貼っておきます。道明寺歌鈴さんは顔が検出されませんでした。かなしいね。

結果2 : 緒方智絵里

 十時愛梨さんの方も試してみましょう。

[エレガンス・プラス]十時愛梨+
結果 : 十時愛梨

 こちらは完全に十時愛梨さんですね。今回記事を書いていて気付いたのですが、目尻の形が違うのでそこで見分けられそうですね。

 某双子でも試してみましょうか。とはいっても、彼女たちは瞳の色が違うのでカラーで学習している今回はかなり簡単に見分けられそうです。

[オンタイム・ハーモニー]久川颯+
結果 : 久川颯
[オフタイム・ナギルーム]久川凪+
結果 : 久川凪

 このように正しく推測できています。お互いの結果Top5にお互いが存在するの、いいですね。あと凪と依田は似ているらしいです。

おわりに

 さて、今回はアイドルの顔面をCNNに食わせて調教してきました。あまり丁寧な実装はしていないのですが、それでもこれだけの精度を出すことができました。今後の課題としましては、
 1. 各種ハイパーパラメータを調整し、より精度を上げる
 2. グレースケールで学習させる
 3. デレマス以外のアイドルでも学習してみる
 4. 学習結果をもとに、似ているアイドルランキングを作る
等が考えられます。また、発見もありました。
 1. 成宮由愛ちゃんと榊原里美ちゃんは似ている
 2. 高森藍子さんと十時愛梨さんは案の定似ている
 3. 久川姉妹は意外と見分けがつく
 4. 橘ありすさんと結婚した
 5. 佐城雪美さんと結婚した
みなさんも是非CNNを利用した画像分類に挑戦してみてください!

 明日はねねこさんの記事になります。また、他の記事は AdC2021 から確認できますので、是非読んでみてください。明日もお楽しみに!


おまけ

[オフタイム・ナギルーム]久川凪

 こちらは[オフタイム・ナギルーム]久川凪(特訓前)なのですが、顔が3件検出されたので結果を報告させていただきます。

結果1 : 久川凪
結果2 : 輿水幸子
結果3 : 星輝子

 凪の部屋には幸子と輝子の生霊が居るらしいです。おばけ要素は小梅ということにして、カワイイボクと142’sが揃いました。対戦ありがとうございました。

2021年12月19日ブログAdC2021,ブログ

Posted by こーちゃん