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

# はじめに
[前回](https://qiita.com/chun1182/items/5b07a60f5c5e52a352c1)の記事では、python(anaconda)のインストールとmnistでCNNを動かすというのをやりました。
　それでは２ヶ月目です。ここから自分で用意したデータセットで解析を始めていきます。ただ、mnistしかやってない、kerasやsklearnのデータセットでしか動かしてない段階から、自分のデータセットでの解析に移るときには大きな壁があります。
　そう、**「どうやって画像を読み込ませたら、ラベルを付けたら良いんだ？」**ということです。今月はそのあたりの実務的な流れを勧めたいと思います。

#目標
　２ヶ月目の目標は**「自作データセットをCNNで学習・分類する。」**です。画像を読み込むのはどうすれば良いのか、ラベルをつけるとは何をすることか、学習を進める、結果を得るには何をすればいいか。を理解することが目標です。
　今月までの知識でも「持ってるデータをAIで解析した結果、精度は～％でした。」くらいの発表はできるようになります。工夫が足りないのでtest精度、汎化性能は低いですが、train画像ではある程度の精度は得ることができるはずです。

#２ヶ月目でやること
- データセットを自作する
- opencvに慣れる
- callbackを使用する

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

#説明用データセットについて
　データを集めるのは大変ですが、今回は下記の記事を参考にしてキルミーベイベーデータセットを使用します。なんと２つ目の記事は分類までしてくれています。キルミストとして感謝しかありません。
[TensorFlowでキルミーアイコン686枚によるキルミー的アニメ絵分類](https://qiita.com/domkade/items/fc9903c2119fdfa9a234)
[GANについて概念から実装まで　～DCGANによるキルミーベイベー生成～](https://qiita.com/taku-buntu/items/0093a68bfae0b0ff879d)
みなさんもどんどんキルミーベイベーデータセットを利用していきましょう！

#データセットを自作する
　データセットを作るときですが、機械学習でラベルを付けるやり方として一般的なのは「各ラベルごとにフォルダ分けをしておいて、フォルダ自体をラベルとする。」というやり方です。
　キルミーベイベーデータセットは、「agiri、botsu、others、sonya 、yasuna、yasuna_agiri、yasuna_sonya、yasuna_sonya_agiri」の８ラベルなので、それぞれの画像をこの名前のフォルダに振り分けてあります。
　これに対して各フォルダから取り出すときに、「agiriフォルダから取り出す画像のラベルは0」「botsuフォルダから取り出す画像のラベルは1」というふうにラベルを付けていきます。
この記事がわかりやすいです。
[[Keras/TensorFlow] Kerasで自前のデータから学習と予測](https://qiita.com/agumon/items/ab2de98a3783e0a93e66)

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

```python
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でフォルダを作るのですが、中間フォルダがない場合、そこも作ってくれます。

```python
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」でインポートします。
学習には[このページ](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_tutorials.html)がおすすめです。使い方がいろいろと乗っています。
色々できますが、今回使うのは画像を読みこむ「cv2.imread(filepath)」と画像のサイズを調整する「cv2.resize(image, (wideth, height))」です。他の機能はおいおい使うときにでも。

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

```python
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を題材にー](https://qiita.com/shinmura0/items/2f34e81e371ded8b4afc)

```python
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：モデルの改善が停滞したときに学習率を減らしてくれます。

```python
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」は結構使えます。

```python
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](https://qiita-image-store.s3.amazonaws.com/0/189568/29d9fb59-5488-881a-50a6-2db3a39c905d.jpeg)

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