Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

前回の記事では、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のクラスがあまり良くないです。まあモデルも適当ですしこんなもんかと思います。
killmebaybe.JPG

まとめ

 これで2ヶ月目は終わりです。自作データを使って解析できたらまずは十分と言えるでしょう。あとはスライド力があれば月報も乗り切れるはずです。
 次の問題として、ここまでの知識だけではtest精度が足りないはずです。それは基本的にはデータが十分ではないせいですが、集めている側も大変ですし、きっと十分集めたと思っていることでしょう。
 そこで次の月はデータの水増し(拡張)と呼ばれる技術や、転移学習を使って少ない枚数でも成果を出せるようにしましょう。最後に、今回のコードもここに入れてあります。
素人がAI部門に配属されてやること3ヶ月目(ImageDataGenerator、random erasing、転移学習)

chun1182
プログラミング、機械学習を2018年から開始。python最高! atcoderは水色
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした