1 導入
LDHファン歴10年、Aidemy研修生のSchwimmerです。EXILEをはじめとするLDH事務所に所属するアーティストは、10年前と比較して、メディアへの露出がとても多くなったと思います。しかし、私の身の回りでも「みんな同じに見える、、」とか「誰がどのグループに所属しているかわからない~」といったことを耳にします。
そこで、今回はLDHを代表するグループ、EXILE、EXILE THE SECOND、三代目JSOUL BROTHERS、GENERATIONSに所属する総勢24名をCNNおよび転移学習によって顔分類し、どこのグループに所属するかも併せて出力したいと思います。
ここで、LDHによく精通されてない方に、グループ構成を紹介したいと思います。
EXILE(15)
ATSUSHI, TAKAHIRO, AKIRA, SHOKICHI, NESMITH, TETSUYA, 黒木啓司, 橘ケンチ, NAOTO, 小林直己, 岩田剛典, 白濱亜嵐, 関口メンディー, 佐藤大樹, 世界
EXILE THE SECOND(6)
AKIRA, SHOKICHI, NESMITH, TETSUYA, 黒木啓司, 橘ケンチ
三代目JSOULBROTHERS(7)
NAOTO, 小林直己, 登坂広臣, 今市隆二, ELLY, 山下健二郎, 岩田剛典
GENERATIONS(7)
白濱亜嵐, 数原龍友, 片寄涼太, 小森隼, 佐野玲於, 中務裕太, 関口メンディー
詳しく知りたい方はこちらで名前と顔を照らし合わせてみてください!
このようにLDHではメンバーがほかのグループを掛け持ちしていることもあり、皆さんが誰がどこのグループ所属か存じ上げないのも致し方ないのかなと思います。
2 転移学習とは
機械学習において、0から学習を行い高精度な予測を行うには、場合によって数十万~数百万という学習データを要することもあります。また、現実的に大量のデータを収集することが困難であったり、学習データのラベル付けも必要となるため、機械学習を完了させるまでに相当な時間とコストがかかってしまいます。例えば、畳み込みニューラルネットワーク(CNN)で画像認識などを一からモデル構築するとなると、大量のサンプル画像を集めなければいけないですし、さらに学習にも多くの時間がかかります。この時間とコストを少なくするために、あらかじめ大量のデータを使って学習を行ったモデルを別のものにも使い回すという転移学習が用いられます。これにより、限られたデータの中でも精度の高い予測を行うことができるようになります。
VGG16
今回は、ImageNet(120万枚,1000クラスからなる巨大な画像のデータセット)で学習した画像分類モデルの一つである VGG16 というモデルを使用します。VGG16は13層の畳み込み層と3層の全結合層の計16層からなります.
今回の実装では、VGG16の全結合層を外して新たに全結合層を追加し、16層以降のみを学習させます。CNNにおいて浅い層では縦線・横線などのおおよその特徴を抽出し、深い層(VGG16の16層以降など)では、その画像特有の特徴を抽出することがわかっています。つまり、深い層を取り外し、浅い層を再利用することで効率よく転移学習することができます。
これによって、VGG16の高い特徴量抽出を継承しつつ、少サンプル・短時間で精度の高い学習モデルを構築できます。
3 手順
1 画像収集
2 集めた画像から、顔部分を切り抜く
3 画像の水増し
4 学習
5 テスト
4 準備
画像収集
まず、EXILE、三代目JSOULBROTHERS、GENERATIONSに所属するアーティストの画像を200枚ずつ集めました。今回、収集する画像のアーティストが計24名にも及ぶため、画像の収集方法としてpythonのライブラリBeautifulSoupを使ったスクレイピングではなく、より簡単に画像を集められるicrawlerというライブラリを使い、画像を集めました。
# 画像収集
from icrawler.builtin import BingImageCrawler
class_ = [""] #集めるアーティストの名前のリスト
for name in class_:
crawler = BingImageCrawler(storage={"root_dir": name})
crawler.crawl(keyword = name, max_num=200) # max_num:集める画像の枚数
顔領域の検出
次に集めたメンバーの画像から顔領域部分を検出したいと思います。この人間の顔を検出する分類器を自分で作ることも可能なのですが、一から作るのはなかなか大変です。しかし、pythonではOpenCVをインストールすると、デフォルトで顔や目などを分類することができるカスケード分類器が付随してくるため、今回はその中のhaarcascade_frontalface_default.xmlを用いて顔検出を行いたいと思います。
#顔検出
import cv2
import matplotlib.pyplot as plt
from PIL import Image
import os, glob, sys
import numpy as np
class_ = [""] #アーティストの名前リスト
#リストで結果を返す関数
def get_file(dir_path):
filenames = os.listdir(dir_path)
return filenames
#出入力ファイルのパスを指定
for name in class_:
in_jpg = "./" +name+"/"
out_jpg = "./face/"+name
os.makedirs(out_jpg, exist_ok=True)
pic = get_file(in_jpg)
for i in pic:
# 画像の読み込み
image_gs = cv2.imread(in_jpg + i)
# 顔認識用特徴量ファイルを読み込む --- (カスケードファイルのパスを指定)
cascade = cv2.CascadeClassifier("haarcascade_frontalface_default.xml")
# 顔認識の実行
face_list = cascade.detectMultiScale(image_gs,scaleFactor=1.1,minNeighbors=1,minSize=(1,1))
cnt = 1
# 顔だけ切り出して保存
for x,y,w,h in face_list:
dst = image_gs[y:y+h,x:x+w]
#保存する時の名前
save_path = out_jpg + '/' + 'out_(' + str(i) +')' +str(cnt) +'.jpg'
#認識結果の保存
a = cv2.imwrite(save_path, dst)
plt.show(plt.imshow(np.array(Image.open(save_path))))
cnt += 1
このカスケード分類器を使っても、顔以外のものや検出対象者以外の顔が検出されてしまうため、これらについては手作業によって削除しました。24人もいたため、この作業だけで2日間ほどかかってしまいました、
その後、顔検出した画像のサイズを64×64にリサイズし、名前ごとにfaceディレクトリに保存しました。
データの分割
これまで画像データの整理を行ってきましたが、ここで画像データを学習データとテストデータに分類します。テストデータに全データのうちの2割(40枚)のデータをランダムに振り分けます。
# 総データ40/200をテストデータに
import shutil
import random
import glob
import os
class_ = [""]
os.makedirs("./test", exist_ok=True)
for name in class_:
in_dir = "./face/"+name+"/*"
in_jpg=glob.glob(in_dir)
img_file_name_list=os.listdir("./face/"+name+"/")
#img_file_name_listをシャッフル、そのうち2割をtest_imageディテクトリに入れる
random.shuffle(in_jpg)
os.makedirs('./test/' + name, exist_ok=True)
for t in range(len(in_jpg)//5):
shutil.move(str(in_jpg[t]), "./test/"+name)
振り分けられたテストデータはtestディレクトリに保存します。
学習データの水増し
全データからテストデータを差し引いた160枚では学習を行うには足りないため、画像データの水増しを行います。画像の中には、顔が傾いている写真が含まれていることもあるので、左右15度に回転処理を行ったのち、閾値処理、ぼかし処理を行い、学習データ画像を9倍に水増ししました。
#水増し
import os
import matplotlib.pyplot as plt
import cv2
import glob
from scipy import ndimage
class_ = [""]
# 画像の読み込み
for name in class_:
in_dir = "./face/"+name+"/*"
out_dir = "./train/"+name
os.makedirs(out_dir, exist_ok=True)
in_jpg=glob.glob(in_dir)
img_file_name_list=os.listdir("./face/"+name+"/")
for i in range(len(in_jpg)):
print(str(in_jpg[i]))
img = cv2.imread(str(in_jpg[i]))
# 回転
for angle in [-15,0,15,]:
img_rot = ndimage.rotate(img,angle)
img_rot = cv2.resize(img_rot,(64,64))
fileName=os.path.join(out_dir,str(i)+"_"+str(angle)+".jpg")
cv2.imwrite(str(fileName),img_rot)
# 閾値
img_thr = cv2.threshold(img_rot, 100, 255, cv2.THRESH_TOZERO)[1]
fileName=os.path.join(out_dir,str(i)+"_"+str(angle)+"thr.jpg")
cv2.imwrite(str(fileName),img_thr)
# ぼかし
img_filter = cv2.GaussianBlur(img_rot, (5, 5), 0)
fileName=os.path.join(out_dir,str(i)+"_"+str(angle)+"filter.jpg")
cv2.imwrite(str(fileName),img_filter)
水増しにより1人当たり160×9の1440枚の学習データを得ることができました。
これらは学習用データとしてtrainディレクトリに保存します。
5 実装
今回、画像分類をするという目的とともに、転移学習の有無での精度の比較も行いたいため、ゼロから畳み込みニューラルネットワークを学習させる場合と、ImageNet(120万枚,1000クラスからなる巨大な画像のデータセット)で学習した画像分類モデル(今回扱うのはVGG16)とその重みを用いて転移学習した場合の2パターンで画像分類を行います。
また、参考までに水増し作業なしで転移学習を行った場合も比較したいと思います。
畳み込みニューラルネットワーク(CNN)
まず、ゼロから自分のモデルのみで畳み込みニューラルネットワークを学習させる場合のコードを下記に記載します。
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
names = [""] #アーティストの名前リスト
# 教師データのラベル付け
X_train = []
Y_train = []
for i in range(len(names)):
img_file_name_list=os.listdir("./train/"+names[i])
print(len(img_file_name_list))
for j in range(0,len(img_file_name_list)-1):
n=os.path.join("./train/"+names[i]+"/",img_file_name_list[j])
img = cv2.imread(n)
b,g,r = cv2.split(img)
img = cv2.merge([r,g,b])
X_train.append(img)
Y_train.append(i)
# テストデータのラベル付け
X_test = [] # 画像データ読み込み
Y_test = [] # ラベル(名前)
for i in range(len(names)):
img_file_name_list=os.listdir("./test/"+names[i])
print(len(img_file_name_list))
for j in range(0,len(img_file_name_list)-1):
n=os.path.join("./test/"+names[i]+"/",img_file_name_list[j])
img = cv2.imread(n)
b,g,r = cv2.split(img)
img = cv2.merge([r,g,b])
X_test.append(img)
# ラベルは整数値
Y_test.append(i)
X_train=np.array(X_train)
X_test=np.array(X_test)
y_train = to_categorical(Y_train)
y_test = to_categorical(Y_test)
from keras.layers import Activation, Conv2D, Dense, Flatten, MaxPooling2D, Input
from keras.models import Model, Sequential
from keras.utils.np_utils import to_categorical
from keras import optimizers
from tensorflow.keras.callbacks import ReduceLROnPlateau
from keras.callbacks import EarlyStopping
#----------------------------------------------------------------------
# モデルの定義
model = Sequential()
model.add(Conv2D(input_shape=(64, 64, 3), filters=32,kernel_size=(3, 3),
strides=(1, 1), padding="same"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(filters=32, kernel_size=(3, 3),
strides=(1, 1), padding="same"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(filters=32, kernel_size=(3, 3),
strides=(1, 1), padding="same"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dense(256))
model.add(Activation("sigmoid"))
model.add(Dense(128))
model.add(Activation('sigmoid'))
model.add(Dense(24))
model.add(Activation('softmax'))
# 評価値の改善が2エポック間止まった時に学習率を減らす
reduce_lr = ReduceLROnPlateau(monitor='val_loss',\
factor = 0.5,\
patience = 2,\
min_lr = 1e-08)
# 評価値の改善が3エポック間止まった時に学習を止める
es = EarlyStopping(monitor='val_loss', patience=3, verbose=0, mode='min')
# コンパイル
model.compile(optimizer='sgd',
loss='categorical_crossentropy',
metrics=['accuracy'])
#学習
history = model.fit(X_train, y_train,batch_size=32, epochs=50, verbose=1,\
validation_data=(X_test, y_test),\
callbacks = [reduce_lr, es])
#汎化制度の評価・表示
score = model.evaluate(X_test, y_test, batch_size=32, verbose=0)
print('validation loss:{0[0]}\nvalidation accuracy:{0[1]}'.format(score))
#acc, val_accのプロット
plt.plot(history.history["accuracy"], label="acc", ls="-", marker="o")
plt.plot(history.history["val_accuracy"], label="val_acc", ls="-", marker="x")
plt.ylabel("accuracy")
plt.xlabel("epoch")
plt.legend(loc="best")
plt.show()
VGG16による転移学習
転移学習により、VGG16の15層までの特徴抽出部分の重みづけを固定し、16層以降を自分のモデルで学習させます。
from keras.applications.vgg16 import VGG16
#input_tensorの定義
input_tensor = Input(shape=(64, 64, 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='sigmoid'))
top_model.add(Dense(24, activation='softmax'))
# 評価値の改善が2エポック間止まった時に学習率を減らす
reduce_lr = ReduceLROnPlateau(monitor='val_loss',\
factor = 0.5,\
patience = 2,\
min_lr = 1e-08)
# 評価値の改善が3エポック間止まった時に学習を止める
es = EarlyStopping(monitor='val_loss', patience=3, verbose=0, mode='min')
# vgg16とtop_modelを連結
model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))
# 15層目までの重みを固定
for layer in model.layers[:15]:
layer.trainable = False
model.compile(loss='categorical_crossentropy',
optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
metrics=['accuracy'])
history = model.fit(X_train, y_train,batch_size=32, epochs=50, verbose=1,\
validation_data=(X_test, y_test),\
callbacks = [reduce_lr, es])
# 汎化制度の評価・表示
score = model.evaluate(X_test, y_test, batch_size=32, verbose=0)
print('validation loss:{0[0]}\nvalidation accuracy:{0[1]}'.format(score))
#acc, val_accのプロット
plt.plot(history.history["accuracy"], label="acc", ls="-", marker="o")
plt.plot(history.history["val_accuracy"], label="val_acc", ls="-", marker="x")
plt.ylabel("accuracy")
plt.xlabel("epoch")
plt.legend(loc="best")
plt.show()
どちらのコードでも共通として、過学習を防ぐためにReduceLROnPlateauモジュールを導入し、評価値(今回はバリデーションデータの損失関数(val_loss))が2エポック間改善されなかった場合、学習率を0.5倍しました。
また、EarlyStoppingモジュールを使用し、3エポック間、評価値が改善されなかった場合、学習を打ち切りました。
6 結果
実装結果は以下のようになりました。
・通常の畳み込みニューラルネットワークのモデルで学習を行った場合、50エポック学習した時点での
分類精度は約70%でした。
・水増し作業無しで転移学習を行った場合も同様に約70%の精度に収束しました。
・水増し作業を行い、転移学習を行ったモデルでは、バリデーションデータの損失関数が早い段階で収
束し、28エポック時点で学習が打ち切られました。
精度に関しては約80%まで上がり、24分類にしては悪くない精度になったのではないかと思います。
また、上記のように通常の畳み込みニューラルネットワークでは、50エポック時点で学習データの精度も70%ほどだったため、100エポックまで学習を続けた様子も見てみました。結果は以下のようになり、テストデータの分類精度は、50エポックまでの時とあまり変わらず、70%前後に収束していっているのがわかります。
以上より、転移学習を行うことで精度が向上することが確認できたと思います。
また、水増し作業を行わずに転移学習した場合でも、通常のCNNモデル同等の精度が得られましたが、水増し作業を行うほうがより高い精度を出すことができるという結果も確認できました。
7 分類例
dic = {0:["AKIRA","EXILE・EXILE THE SECOND"], 1:["ATSUSHI","EXILE"], 2:["TAKAHIRO","EXILE"], 3:["SHOKICHI","EXILE・EXILE THE SECOND"],\
4:["NESMITH","EXILE・EXILE THE SECOND"],5:["TETSUYA","EXILE・EXILE THE SECOND"],6:["NAOTO","EXILE・三代目"],7:["黒木啓司","EXILE・EXILE THE SECOND"],\
8:["橘ケンチ","EXILE・EXILE THE SECOND"], 9:["小林直己","EXILE・三代目"],10:["佐藤大樹","EXILE"], 11:["世界","EXILE"],\
12:["登坂広臣","三代目"], 13:["今市隆二","三代目"], 14:["ELLY","三代目"],15:["山下健二郎","三代目"], 16:["岩田剛典","EXILE・三代目"],\
17:["白濱亜嵐","EXILE・GENERATIONS"], 18:["数原龍友","GENERATIONS"],19:["片寄涼太","GENERATIONS"], 20:["小森隼","GENERATIONS"],\
21:["中務裕太","GENERATIONS"], 22:["佐野玲於","GENERATIONS"], 23:["関口メンディー","EXILE・GENERATIONS"]}
image = cv2.imread("./predict/ファイル名")
b,g,r = cv2.split(image)
img = cv2.merge([r,g,b])
img= np.expand_dims(img,axis=0)
def detect(img):
#予測
plt.imshow(img)
plt.show()
name = np.argmax(model.predict(img))
print(dic[name][0]+"("+dic[name][1]+")")
detect(img)
以上のコードにより何枚か分類すると
TAKAHIRO(EXILE)
NAOTO(EXILE・三代目)
ATSUSHI(EXILE)
SHOKICHI(EXILE・EXILE THE SECOND)
全体的にうまく分類できていたようです。
ただ、ATSUSHIさんに関しては、サングラスATSUSHIは100%分類できているのですが、上のように眼鏡ATSUSHIやサングラス無しATSUSHIはうまく分類できていないようでした。
理由として、メディアでもサングラスでの出演が多いため、画像収集の際にATSUSHIさんの画像がサングラスATSUSHIに偏ってしまったことが考えられます。
また、他のメンバーもサングラスをしているとATSUSHIさんに間違われてしまうことがありました。
7 最後に
機械学習の学習をはじめて1か月ほどで、はじめて実装を行いましたが、自分で手を動かしてみることでCNNや転移学習の仕組みを理解しなおすとてもいい機会になりました。
また、今回は転移学習の学習済みモデルとしてVGG16を使用しましたが、近年では、どの学習済みモデルを使うことが最適か判断できる手法も明らかになってきており、このような手法を利用することで最適な学習済みモデルで転移学習を行い、今回の結果以上に良い精度が出ることも期待されます。
今後はこのようなモデルの選択といった分野にも精通できればと思います。
参考
https://www.ldh.co.jp/management/artist/
https://github.com/opencv/opencv/tree/master/data/haarcascades
https://qiita.com/nirs_kd56/items/bc78bf2c3164a6da1ded