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

#はじめに
　[前回](https://qiita.com/chun1182/items/cbfc1cdd1422cb7218ea)では自作のデータセットから学習を進めました。ただ686枚と少ない~~DVD売上数~~・画像枚数では学習には足りないので少し工夫をする必要があります。
　具体的には水増しと転移学習です。水増しはその名の通り今持っているデータにいろいろ加工して画像を擬似的に増やすこと。転移学習は別の学習をして成長したモデルを流用することで、少ない枚数で学習をできるようにすることです。

#3ヶ月目でやること
　3ヶ月目の目標は「前処理や転移学習をkerasのページを見ながら使えるようになる」です。
　Qiitaやブログの記事を参考にして使うのも良いですが（自分も殆どの場合はそうですが）、それに加えてモジュールを使用する場合はその公開元のページを見るのが良いです。モジュールの引数や使い方の説明が丁寧に書かれているのでかなりためになります。kerasは[日本語ページ](https://keras.io/ja/)もあります。

#目標

- ImageDataGeneratorを使いこなす
- 転移学習をできるようになる。
- データセットを見直す

あたりです。このあたりから項目が少なくなりますが、考えることが増えるのでこんなもんです。特にデータセットの調整は特に。

#ImageDataGeneratorについて
ImageDataGeneratorとはkerasのモジュールの一つで前処理とかができるものです。使える機能は[いろいろ](https://keras.io/ja/preprocessing/image/)とありますが、よく使うのは平行移動、回転でしょうか。内容によっては反転入れたり、拡大も入れることがあります。あとは自作の処理関数を使って入れることもできます。
[Kerasでデータ拡張(Data Augmentation)後の画像を表示する](https://qiita.com/takurooo/items/c06365dd43914c253240)
が参考になります。今回はrandom erasingも入れてみたいと思います。[こちらの](https://github.com/yu4u/cutout-random-erasing)githubのページを参考にさせていただきました。
データの読み込みまでは前回と同じなので、要点のところのコードだけピックアップします。こちらに上げてあります。


```python
# random erasing用の関数
def get_random_eraser(p=0.5, s_l=0.02, s_h=0.4, r_1=0.3, r_2=1/0.3, v_l=0, v_h=255, pixel_level=False):
    def eraser(input_img):
        img_h, img_w, img_c = input_img.shape
        p_1 = np.random.rand()
        if p_1 > p:return input_img
        while True:
            s = np.random.uniform(s_l, s_h) * img_h * img_w
            r = np.random.uniform(r_1, r_2)
            w = int(np.sqrt(s / r))
            h = int(np.sqrt(s * r))
            left = np.random.randint(0, img_w)
            top = np.random.randint(0, img_h)
            if left + w <= img_w and top + h <= img_h:break

        if pixel_level:c = np.random.uniform(v_l, v_h, (h, w, img_c))
        else:c = np.random.uniform(v_l, v_h)
        input_img[top:top + h, left:left + w, :] = c
        return input_img
    return eraser

# 今回は左右反転、シフト10%、回転20°＋randomerasing
datagen = ImageDataGenerator(horizontal_flip=True, width_shift_range=0.1, height_shift_range=0.1,
                              rotation_range=20.0,
                            preprocessing_function=get_random_eraser(s_l=0.2, s_h=0.4, pixel_level=True))
# IDGをかけた画像を出力
g = datagen.flow(x_train, shuffle=False)
idg_x_train=next(g)

# 4枚出力して比較
plt.figure(figsize=(12, 6)) 
for i in range(4):
    plt.subplot(2,4,i+1),plt.imshow(cv2.cvtColor(x_train[i], cv2.COLOR_BGR2RGB)),plt.xticks([]),plt.yticks([])
    plt.subplot(2,4,i+5),plt.imshow(cv2.cvtColor(idg_x_train[i], cv2.COLOR_BGR2RGB)),plt.xticks([]),plt.yticks([])
plt.savefig('idg_agiri.png')
plt.show()
```
![idg_agiri.png](https://qiita-image-store.s3.amazonaws.com/0/189568/25eb2c45-0a49-2ff4-90ef-0547e7fe16f7.png)
　結果はこんな感じです。反転、回転が入っているのが確認できると思います。
　昔のテレビみたいなごま塩ノイズが入っているのがrandom erasingです。画像の一部を隠すことによって、いつも同じ特徴を見て判定するのを防ぐ役割がある、と信じられてます。
　IDGは学習のたびに違う処理がかかるので、同じ画像でも毎回違った形で出力されるので、同じ画像を記憶して判断するのを防ぐ役割があります。


#転移学習
　転移学習はコンペとかで使われた強いモデルが公開されているので、それを自分のやりたいタスクに使いまわそうってやつです。[kerasのページ](https://keras.io/ja/applications/)に使えるやつ一覧が紹介してあります。今回はvgg16を使用しました。畳み込み層→poolingを繰り返して全結合する、シンプルなわかりやすいやつです。
　公開されているのとは最終的に判断したいクラスの数が違うので今回は最後のConv層までだけ(include_top=False)読み込んで、全結合相は自分で追加しています。前回と層の組み方が違いますがこれはSecentialではなくて[functional](https://keras.io/ja/getting-started/functional-api-guide/)という方法で組んでいます。詳細は今度Autoencoderやresnetと一緒に解説します。
[２つのモデルを接続する（keras2.0_functional 編）](https://qiita.com/tom_eng_ltd/items/a6f1d8d34b2a32fdf7d4)

```python
from keras.applications.vgg16 import VGG16
from keras.layers import Dense, Flatten, Input, Dropout
from keras.models import Model
from keras.optimizers import Adam

def tuning_vgg16():
    input_shape = (128, 128, 3)
    input_tensor = Input(shape=input_shape)
    vgg16_model = VGG16(weights='imagenet', include_top=False, input_tensor=input_tensor)
    x = vgg16_model.output
    x = Flatten()(x)
    x = Dense(512, activation='relu')(x)
    x = Dropout(0.5)(x)
    x = Dense(8, activation='softmax', init='uniform')(x)
    model = Model(input=vgg16_model.input, output=x)
    return model

model=tuning_vgg16()
#for layer in model.layers[:15]:layer.trainable=False
model.compile(optimizer=Adam(),
              loss='categorical_crossentropy',
              metrics=['accuracy'])
model.summary()
```

#データセット調整
　ここまで技術的にいろいろやってきましたが、やっておくべき重要なこととして、データセットのバランス確認があります。今回だと複数人写っているやつは個別にフォルダ分けしていましたが、それだと非常に少ないフォルダができるので1つにまとめちゃいます。
　また、各フォルダの画像枚数が大きく異なると、数が多いフォルダに引きづられて学習がうまく進まないので、画像が多すぎるフォルダは一部減らすようにして、少ないフォルダは同じ画像でもいいからコピーして追加したりします。

#結果確認
　結果ですが、92%と前回よりも1枚だけ正判定が増えました！色々工夫した割に成果は薄い・・・。まあ根本的に学習枚数が足りないのでしょうがない面もあります。
　そして残念なことにrandom erasingは外しました。なんか収束しなかったので。入れ方がまずいのだろうか。
　こんな感じでmissフォルダを作って間違えたのを確認できます。


```python
import shutil
labels=[i.split('\\')[-1] for i in glob.glob(path+'/train/*')]
for i in range(66):
    if predict_classes[i]!=true_classes[i]:
        name_split=test_name[i].split('\\')
        shutil.copy2(test_name[i], path+'/miss/'+'{}_to_{}_'.format(labels[true_classes[i]], labels[predict_classes[i]])+name_split[-1])
```
例えばこの「others」を[sonya]と間違えました。色的にはわからなくもない。
![others_to_sonya_07_027.png](https://qiita-image-store.s3.amazonaws.com/0/189568/4ef7444c-dbbf-251a-2ec4-3bef4d88211c.png)


#まとめ
 これで3ヶ月目は終わりです。このあたりまでわかれば精度を出すには十分に近い技術を持っていると思います。その上で重要なのは学習データのバランス調整だったりします。基本的に各フォルダ同じくらいに来ることがないので、必ず調整する必要は出てくると思います。
　次回は学習がうまく行っているかのための確認として可視化の手法を紹介します。CAMでちゃんと特徴を捉えて判断したり、TSNEでうまく画像ごとに違いが出ているかを確認していきます。