自分で集めたデータを使った画像認識にデータの水増しを取り入れてみる(Keras、CNN)

はじめに

今回は、以前の記事で行ったネコ科の動物の画像識別に、前回覚えてデータの水増しを取り入れてみたいと思います.

shirakonotempura.hatenablog.com

おさらいしておきますと、前回行った8クラスの画像識別では、訓練データの精度は100%近く出るのに対し、検証データ・テストデータに対する精度は40%程度という結果でした.そこで、画像を水増しして、過学習を防ぎ汎化性能をあげてやろうというのが今回の目的です.

いきなりの挫折とお詫び

本当は8クラス分類を今回もやりたいのですが、実は枚数を増やした場合の計算時間がバカにならなくなってきました.そのため、泣く泣く4クラス分類としています. ですので今回対象とするネコ科の猛獣達はライオン、トラ、リンクス、ネコとさせていただきます.
ですので、あらためて水増しなしのデータに対しても結果を整理していくようにします.

f:id:shirakonotempura:20190110025419p:plain
対象の猛獣たち

画像データの変換

では、4クラスの画像データを学習用のデータに変更するところから始めます.ただし、前提として、必要な写真データは既にディレクトリごとに準備されている(今回は各280枚×4クラス)とします.

指定した4つのディレクトリから、280枚の写真を読み込みます. その後、30枚をテストデータ、残り250枚を学習用データとしてリストに保存します. ここでは、各フォルダから30枚をテスト用データに分け、テストデータに水増し後のデータが含まれることを避けています.

(一部省略)
classes = ["cat", "tiger", "lion","lynx"]
num_classes = len(classes)
image_size = 50
max_read = 280
num_testdata = 30

X_train = []
X_test = []
y_train = []
y_test = []

for index, classlabel in enumerate(classes):
    photos_dir = "./" + classlabel
    files = glob.glob(photos_dir + "/*.jpg")
    for i, file in enumerate(files):
        if i >= max_read: break
        image = Image.open(file)
        image = image.convert("RGB")
        image = image.resize((image_size, image_size))
        data = np.asarray(image)

        # あらかじめtest用は分けておく
        if i < num_testdata:
            X_test.append(data)
            y_test.append(index)
        # 残りを学習用として保存
        else:
            X_train.append(data)
            y_train.append(index)

グラフ用関数の定義

これまで同様、グラフ表示用関数を定義しておきます.

(一部省略してます)
def log2graph(log1, name1):
    # Accuracyの履歴
    plt.figure()
    # log1
    plt.plot(log1.history["acc"], label="Train_Accuracy_"+ name1, marker=".", color = "green")
    plt.plot(log1.history["val_acc"], label="Val_Accuracy_"+ name1, marker=".", color = "blue")
    
    plt.grid()
    plt.legend(loc="best")
    plt.title("Accuracy")
    file_name_acc = "graph_accuracy.png"
    plt.savefig(file_name_acc)
    plt.show()

データの水増しなしで学習

では、先に水増しを行わず250枚の学習データを使った学習を行っていきます.

いつものように入力データを正規化(最大値1.0へ変換)し、ラベルデータはone-hotベクトル化しています.

また、今回データを読みこむ際フォルダごとにリストに格納しているため、このままだとリストのラベルはフォルダ(つまりクラス)ごとに固まってしまいます.そのため、訓練データに対してシャッフル処理をしています.

# 正規化
X_train_no_aug = X_train_orig.astype("float")/255
X_test = X_test_orig.astype("float")/255 # テストデータはそのまま水増しありでも使う

# one-hot vector化
y_train_no_aug = np_utils.to_categorical(y_train_orig, num_classes)
y_test = np_utils.to_categorical(y_test_orig, num_classes) # テストデータはそのまま水増しありでも使う

# この時点では、訓練データがクラスごとに並んでいるのでシャッフル処理を加えておく
p = np.random.permutation(len(X_train_no_aug))
X_train_no_aug = X_train_no_aug[p]
y_train_no_aug = y_train_no_aug[p]

実はこれまでもデータの保存はフォルダごとに行っていたのですが、train_test_splitを使ってデータを訓練用、テスト用に分けていたので、デフォルトでシャッフルされていました.

当初、このシャッフル処理を入れずに学習を行ったところ、テストデータに対する精度は40%ほどでした.バッチ学習を行う際、1回の学習に同じラベルのものばかりを使ってしまうことになり学習がうまくいかないようです.

データの中身の確認(必須ではないです)

一度、データの中身を確認しておきます.適当に選んだインデックスのインプットデータの画像が、ちゃんと正解ラベルと対応しているかを確認しています.

# データの中身の確認
from keras.preprocessing import image
idx = random.randint(0:999)
img = image.array_to_img(X_train_no_aug[idx])
plt.imshow(img)
plt.show()
print(classes[list(y_train_no_aug[idx]).index(1)])

50×50ピクセルにリサイズしているので、画像が粗いのですが、なんとか認識できるかと思います.

f:id:shirakonotempura:20190110025535p:plain
50ピクセル×50ピクセルの画像(ライオン)

f:id:shirakonotempura:20190110032753p:plain
たぶんトラ


この粗さだとヒョウとチーター、ジャガーとかの判別は無理なのでは・・.サイズを150x150くらいにすればできるんでしょうか.今回はネコ、リンクス、トラ、ライオンなので、その辺の検討はしていませんが少し気になるところです.

共通事項の定義

以下の内容は、水増しあり・なしに限らず共通のパラメータとするので、ここで定義しておきます.

  • エポック数(epochs): 100
  • loss関数(loss):"categorical_crossentropy"
  • 最適化手法(optimizer):Adadelta
  • 評価関数(metics):accuracy
  • バッチサイズ(batch_size):50
  • 検証用に使うデータの割合(validation_split):0.2
epochs = epochs
loss="categorical_crossentropy"
optimizer=keras.optimizers.Adadelta()
metrics=['accuracy']
batch_size = 50
validation_split = 0.2

水増しなしの学習データによる学習および学習結果

CNN用のネットワークを定義します.ネットワークの構成は水増しありと変わりませんが、それぞれモデルを設定する必要があります.

  • 水増しなし:model
  • 水増しあり:model_aug

としています.

# CNNの構築(model)
model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3), padding="same",activation='relu', input_shape=X_train.shape[1:]))
model.add(Conv2D(64, (3, 3),  padding="same",activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))

# mode_cnn(畳み込みNN)のコンパイル
model.compile(loss      = loss,
              optimizer = optimizer,
              metrics   = metrics)
print("model_normal")
print(model.summary())

# 学習(水増しなし)
log_no_aug = model.fit(X_train_no_aug, y_train_no_aug,
                    batch_size=batch_size,
                    epochs=epochs,
                    verbose=1,
                    validation_split = validation_split)
## 結果表示
score = model.evaluate(X_test, y_test, verbose=1)
print('Test loss:', score[0])
print('Test accuracy:', score[1])
# グラフの描画
log2graph(log_no_aug, "model")

f:id:shirakonotempura:20190110025736p:plain


Test loss: 2.6128105481465655 Test accuracy: 0.6416666626930236


結果ですが、識別精度はテストデータに対してほぼ100%、検証データに対して約60%、テストデータに対して64.2%とちゃんと過学習っぽい結果となってくれています.


データの水増しありで学習

続けて、画像データの水増しを行って、同じように識別精度を見ていきたいと思います. 先ほど作成したオリジナルデータ(正規化、one-hotベクトル化をしていないデータ)のうち、訓練データを水増ししていきます.

from keras.preprocessing.image import ImageDataGenerator

# 画像の意味が変わらない程度に適当に組み合わせ
datagen = ImageDataGenerator(
            rotation_range=20,
            width_shift_range=0.1,
            height_shift_range=0.1,
            horizontal_flip=True,
            channel_shift_range=70
          )

X_train_aug = []
y_train_aug = []
#for i in range(3):
for i in range(len(X_train_orig)):
    x = np.expand_dims(X_train_orig[i], axis=0)
    max_img_num = 16

    for d in datagen.flow(x, batch_size=1):
        X_train_aug.append(d[0])
        y_train_aug.append(y_train_orig[i])
  
        if (len(X_train_aug) % max_img_num) == 0:
            break
        
# Keras(Tensor flow)用にnumpy arrayに変更
X_train_aug = np.array(X_train_aug)
y_train_aug = np.array(y_train_aug)

# 正規化
X_train_aug = X_train_aug.astype("float")/255
# one-hot vector化
y_train_aug = np_utils.to_categorical(y_train_aug, num_classes)

# シャッフル処理
p = np.random.permutation(len(X_train_aug))
X_train_aug = X_train_aug[p]
y_train_aug = y_train_aug[p]

水増しありの学習データによる学習および学習結果

先と同じように水増しありデータによるCNN用のモデル(model_aug)を定義します.
基本的に、学習用データが異なるだけなのでサラッと行きます.

# CNNの構築(model)
model_aug = Sequential()
model_aug.add(Conv2D(32, kernel_size=(3, 3), padding="same",activation='relu', input_shape=X_train.shape[1:]))
model_aug.add(Conv2D(64, (3, 3),  padding="same",activation='relu'))
model_aug.add(MaxPooling2D(pool_size=(2, 2)))
model_aug.add(Dropout(0.25))
model_aug.add(Flatten())
model_aug.add(Dense(128, activation='relu'))
model_aug.add(Dropout(0.5))
model_aug.add(Dense(num_classes, activation='softmax'))

# mode_cnn(畳み込みNN)のコンパイル
model_aug.compile(loss      = loss,
              optimizer = optimizer,
              metrics   = metrics)
print("model_aug")
print(model_aug.summary())

# 学習(水増しなし)
log_aug = model_aug.fit(X_train_aug, y_train_aug,
                    batch_size=batch_size,
                    epochs=epochs,
                    verbose=1,
                    validation_split = validation_split)
## テストデータで結果表示
score = model_aug.evaluate(X_test, y_test, verbose=1)
print('Test loss:', score[0])
print('Test accuracy:', score[1])
# グラフの描画
log2graph(log_aug, "model_aug")

f:id:shirakonotempura:20190110033623p:plain

結果が良くなっているのかちょっと分かりづらいので、2つのグラフを重ねてみました.

f:id:shirakonotempura:20190110035312p:plain

はい、そんなに見やすくはなりませんでしたね.
まだ過学習気味ですが、水増ししたデータによる学習結果の識別精度は、テストデータに対してほぼ100%、検証データに対しても90%弱とかなりいい感じに向上しているんじゃないでしょうかと思ったのも束の間・・.テストデータの識別精度を確認したところ

Test loss: 5.049707873662313 Test accuracy: 0.5916666666666667

ん?・・?59.1%だ・・と??なんと水増し前の64.2%よりかなり下がってしまいました.

学習の途中経過を見る限り順調かと思ったのですが、軽く困惑しています. 過学習気味な件も含め、まだ検討が必要そうです.


なんとなくですが、訓練データに対して、テストデータが少なすぎるのでしょうか. あるいは、水増し方法がひどくてデータの質が下がっているのかもしれません. ただちょっと疲れてきましたので、長くなってきましたので一旦ここでやめて、水増し方法の影響などは次回以降見ていこうかと思います.

まとめ

4クラスの識別問題に対して、水増しの有無で識別精度の比較を行いました.
水増しの結果、検証データは改善されたのですが、肝心のテストデータの識別精度が下がるという残念な結果になりました.おそらく水増しの方法に問題があるのだと推定しています.
心が折れそうですが、頑張ります.


結果に問題が残ったままなのですが、記録のため コチラに残しておきます.