Edited at

素人がAI部門に配属されてやること2ヶ月目(自作データセット、opencv、callback)


はじめに

前回の記事では、python(anaconda)のインストールとmnistでCNNを動かすというのをやりました。

 それでは2ヶ月目です。ここから自分で用意したデータセットで解析を始めていきます。ただ、mnistしかやってない、kerasやsklearnのデータセットでしか動かしてない段階から、自分のデータセットでの解析に移るときには大きな壁があります。

 そう、「どうやって画像を読み込ませたら、ラベルを付けたら良いんだ?」ということです。今月はそのあたりの実務的な流れを勧めたいと思います。


目標

 2ヶ月目の目標は「自作データセットをCNNで学習・分類する。」です。画像を読み込むのはどうすれば良いのか、ラベルをつけるとは何をすることか、学習を進める、結果を得るには何をすればいいか。を理解することが目標です。

 今月までの知識でも「持ってるデータをAIで解析した結果、精度は~%でした。」くらいの発表はできるようになります。工夫が足りないのでtest精度、汎化性能は低いですが、train画像ではある程度の精度は得ることができるはずです。


2ヶ月目でやること


  • データセットを自作する

  • opencvに慣れる

  • callbackを使用する

先月よりもやることが少ないように見えますが、データセットを作るのは手を動かす作業なので結構時間かかります。


説明用データセットについて

 データを集めるのは大変ですが、今回は下記の記事を参考にしてキルミーベイベーデータセットを使用します。なんと2つ目の記事は分類までしてくれています。キルミストとして感謝しかありません。

TensorFlowでキルミーアイコン686枚によるキルミー的アニメ絵分類

GANについて概念から実装まで ~DCGANによるキルミーベイベー生成~

みなさんもどんどんキルミーベイベーデータセットを利用していきましょう!


データセットを自作する

 データセットを作るときですが、機械学習でラベルを付けるやり方として一般的なのは「各ラベルごとにフォルダ分けをしておいて、フォルダ自体をラベルとする。」というやり方です。

 キルミーベイベーデータセットは、「agiri、botsu、others、sonya 、yasuna、yasuna_agiri、yasuna_sonya、yasuna_sonya_agiri」の8ラベルなので、それぞれの画像をこの名前のフォルダに振り分けてあります。

 これに対して各フォルダから取り出すときに、「agiriフォルダから取り出す画像のラベルは0」「botsuフォルダから取り出す画像のラベルは1」というふうにラベルを付けていきます。

この記事がわかりやすいです。

[Keras/TensorFlow] Kerasで自前のデータから学習と予測

今回はkill_me_baby_datasetsフォルダの下にtrainフォルダを作り、その中にキルミーベイベーデータセットの各フォルダを入れて処理を初めます。

import glob, os, shutil, cv2

import numpy as np
#フォルダリストを取得
path='kill_me_baby_datasets'
train_list=glob.glob(path+'/train/*')
#testフォルダvalフォルダを作成
for i in train_list:
os.makedirs(i.replace('train','test'))
os.makedirs(i.replace('train','val'))

これでtrainと同じ階層に、空の8種のフォルダを持つ「test, val」フォルダができます。

やってることとしては、glob.globで指定のフォルダ・ファイルのパスのリストを取得しています。これはよく使います。

os.makedirsでフォルダを作るのですが、中間フォルダがない場合、そこも作ってくれます。

for file in train_list:

#フォルダ内画像のパスを取得
pics = glob.glob(file+'/*.png')
pick_num = len(pics)//10
if not pick_num: continue
#20%を選び出す
choice = np.random.choice(pics, pick_num*2, replace=False)
#選んだ画像をtestとvalフォルダに移動
for test_pic in choice[:pick_num]:
shutil.move(test_pic, test_pic.replace('train','test'))
for val_pic in choice[pick_num:]:
shutil.move(val_pic, val_pic.replace('train','val'))

次にこれを動かすことで8割はtrainに残しておいて、1割をvalフォルダ、残りの1割をtestフォルダに移動させます。

元のパスの名前のところをreplaceで書き換えることで、保存先フォルダを指定します。


OpenCVについて

画像処理が簡単にできるモジュールです。

「pip install opencv-python」でインストールするのに「import cv2」でインポートします。

学習にはこのページがおすすめです。使い方がいろいろと乗っています。

色々できますが、今回使うのは画像を読みこむ「cv2.imread(filepath)」と画像のサイズを調整する「cv2.resize(image, (wideth, height))」です。他の機能はおいおい使うときにでも。

それではtrain、val、testフォルダから画像を取り出していきます。

def get_data(dpath):

path_list = glob.glob(dpath+'/*')
#画像ファイル、ラベルを入れる箱を用意
x=[]
y=[]
#フォルダ名でループを回す
for label, pic_path in enumerate(path_list):
#サブフォルダ内のファイルリストを取得
train_pic_list = glob.glob(pic_path+'/*.png')
print(pic_path, len(train_pic_list))
#画像ファイルを取得+リサイズ、ラベルを用意
x += [cv2.resize(cv2.imread(i), (128, 128)) for i in train_pic_list]
y += [label]*len(train_pic_list)

#画像をfloat32に変換正規化、ラベルをカテゴリカル化
x = np.float32(x)/255
y = keras.utils.to_categorical(y, len(path_list))
return x, y

x_train, y_train = get_data(path+'/train')
x_val, y_val = get_data(path+'/val')
x_test, y_test = get_data(path+'/test')
print(x_train.shape, y_train.shape)
print(x_val.shape, y_val.shape)
print(x_test.shape, y_test.shape)

こんな感じでファイルを読み込んでいきます。

①agiri等のフォルダリストを取得

②フォルダリストでループ回してフォルダ内画像を取得+画像分のラベルを用意、x, yに渡す

③全部取得したらxはfloat32化、yはone-hot化する

④この関数をtrain、val、testについて動かす

という流れです。初めはこまめにx.shapeなどで次元がどうなっている確認しておくと良いです。


モデルについて

今回はモデルもちょっと工夫します。VGGっぽくconv2D層を積み重ねました。

BatchNormalizationとかGlobalAveragePooling2Dも使ってみました。

前者は収束の改善、後者はモデルを軽くする等の効果があるそうです。

この記事とかが効果がわかって良いと思います。

畳込みニューラルネットワークの基本技術を比較する ーResnetを題材にー

from keras.models import Sequential

from keras.layers import Dense, Dropout, Flatten,GlobalAveragePooling2D
from keras.layers import Conv2D, MaxPooling2D, BatchNormalization
from keras.optimizers import Adam

input_shape = (128, 128, 3)
model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3), activation='relu', padding='same', input_shape=input_shape))
model.add(BatchNormalization())
model.add(Conv2D(32, kernel_size=(3, 3), activation='relu', padding='same'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Conv2D(64, kernel_size=(3, 3), activation='relu', padding='same'))
model.add(BatchNormalization())
model.add(Conv2D(64, kernel_size=(3, 3), activation='relu', padding='same'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Conv2D(128, kernel_size=(3, 3), activation='relu', padding='same'))
model.add(BatchNormalization())
model.add(Conv2D(128, kernel_size=(3, 3), activation='relu', padding='same'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Conv2D(256, kernel_size=(3, 3), activation='relu', padding='same'))
model.add(BatchNormalization())
model.add(Conv2D(256, kernel_size=(3, 3), activation='relu', padding='same'))
model.add(BatchNormalization())
model.add(GlobalAveragePooling2D())

model.add(Dropout(0.5))
model.add(Dense(8, activation='softmax'))

model.summary()
model.compile(loss='categorical_crossentropy',
optimizer=Adam(),
metrics=['accuracy'])

本来はBatchnormalization→reluですが、見づらくなるのでActivation層分けませんでした。実際のところ効果の差もわからないですし。


callbackについて

callbackはkerasが用意してくれてる学習中に働く便利ツールです。自分がよく使ってるのを紹介します。

ModelCheckpoint:途中の重み保存。必須。今回はval_lossの値が改善したら重みを保存します。Trueのところを変えれば毎回保存したり、モデル、コンパイルごと保存するようにもできます。

EarlyStopping:モデルの改善が見られなくなったら学習を止めます。時間気にしないなら別に使わなくても良いかも。ちっこいデータセットだと覚えきってacc:1.000になり学習しなくなるので、自分はaccを監視してます。

CSVlogger:学習過程をCSVに吐き出します。学習の過程のグラフ作るときにどうぞ。

ReduceLROnPlateau:モデルの改善が停滞したときに学習率を減らしてくれます。

from keras.callbacks import ModelCheckpoint, EarlyStopping, CSVLogger, ReduceLROnPlateau

#val_lossがbestの値を出したときにモデルの重みのみ(weight_only)を保存
checkpoint_path = path+"/weights/weights_{epoch:02d}_{val_loss:.3f}_{val_acc:.3f}.h5"
print(checkpoint_path)
checkpoint = ModelCheckpoint(checkpoint_path, monitor='val_loss', verbose=0,
save_best_only=True, save_weights_only=True, mode='auto')
#accの向上がない=学習が止まっているとみてストップする。
earlystop = EarlyStopping(monitor='acc', patience=15, verbose=1, mode='auto')
# CSV出力、下に重ねていく
csv_logger = CSVLogger(path+'/weights/training.csv', append=True)
# val_lossの向上が見られない時に学習率を減らす。
lr_reducer = ReduceLROnPlateau(monitor='val_loss', factor=0.1**0.5, cooldown=0, patience=15,
verbose=1, min_lr=0.5e-4)
call_backs = [csv_logger,checkpoint, lr_reducer]
print('use:csv_logger,checkpoint2,lr_reducer')

history = model.fit(x_train, y_train, batch_size=32, epochs=30, verbose=1, callbacks=call_backs,
validation_data=(x_val, y_val))


結果出力

 学習が終わったら、保存された重みの中で一番val_lossが少ないものを選んで読み込みます。読み込まないと学習終了時の重みを使用することになるので、過学習してるときなどは性能が良くないです。

 テストデータを評価するときにevaluateで精度は見れますが、ちょっと情報が足りないです。どれをどこに間違えたか知っておきたい時に「confusion_matrix」は結構使えます。

weight_list=glob.glob(path+'/weights/*.h5')

print(weight_list[-1])
model.load_weights(weight_list[-1])

score = model.evaluate(x_test, y_test, verbose=0)
print('Test loss:', score[0])
print('Test accuracy:', score[1])

from sklearn.metrics import confusion_matrix
predictions = model.predict(x_test)
print('predictions,shape', predictions.shape)
predict_classes = np.argmax(predictions, 1)
true_classes = np.argmax(y_test, 1)
print(confusion_matrix(true_classes, predict_classes))

結果は以下のようになりました。30epochくらい回して精度は91%です。3つ目・otherのクラスがあまり良くないです。まあモデルも適当ですしこんなもんかと思います。


まとめ

 これで2ヶ月目は終わりです。自作データを使って解析できたらまずは十分と言えるでしょう。あとはスライド力があれば月報も乗り切れるはずです。

 次の問題として、ここまでの知識だけではtest精度が足りないはずです。それは基本的にはデータが十分ではないせいですが、集めている側も大変ですし、きっと十分集めたと思っていることでしょう。

 そこで次の月はデータの水増し(拡張)と呼ばれる技術や、転移学習を使って少ない枚数でも成果を出せるようにしましょう。最後に、今回のコードもここに入れてあります。

素人がAI部門に配属されてやること3ヶ月目(ImageDataGenerator、random erasing、転移学習)