Keras で少ない画像データから学習したい

深層学習で画像分類をしてみたいときとかあると思います。でも画像を用意するのが面倒です。すると Keras ブログに「少ないデータから強力な画像分類」とあります。GitHubスクリプトもあります。これを使ってみます。
Building powerful image classification models using very little data
fchollet/classifier_from_little_data_script_1.py - GitHub

データの準備

Google 検索で出てきた順にカレーとラーメンの画像を保存しました。でも時間がなかったので訓練用とテスト用に各クラス10枚ずつしか保存しませんでした。Keras ブログに書いてあるように、「少ないデータ」とは少ないといえども "just a few hundred or thousand pictures" ということなので、真面目にやる場合はきちんと用意してください。これらの画像は Keras ブログの指示通り、data/train/curry/ のようにフォルダを切った下に置きます。

訓練

このカレーとラーメンの画像分類を、畳み込み層とプーリング層を積み上げたニューラルネットワークで訓練するわけですが、少ないデータを最大限に活用して学ぶために、画像を常にランダムに少しの回転や平行移動、シア変形(平行四辺形のように歪める変形)、拡大縮小、左右反転などをさせながらモデルに入力します(どれだけの変形まで許容するかは自分で指定します)。こうすることによって過学習が抑制されモデルの汎化性能が上がるそうです。

結果

訓練データが20枚というやる気のなさでもテストデータの9割を正解してくれました(下表)。何回か訓練を試行するとテスト正解率が100%になることも多かったです。

f:id:cookie-box:20170810000634p:plain:w480
なお「カレー」と誤判断されたラーメンは以下のサイトの「天日地鶏」というお店のもので、ニューラルネットはこのチャーシューがカレールーに、麺とネギがライスに見えたのかもしれません(?)。
キングオブ静岡ラーメン~人気ランキングTOP10|静岡新聞SBS-アットエス
「ラーメン」と誤判断されたカレーは以下でした。何がラーメン要素だったのかよくわかりません。
​新潟発。衝撃200円カレーが東京に初進出 - エキサイトニュース(1/2)
逆に以下のあまりカレーに見えない MOKUBAZA のチーズキーマカレーはカレーに判定されました。ラーメンにも見えませんが。
【保存版】2015年のカレーまとめ / もっとも印象に残ったお店20選 | ロケットニュース24

スクリプト

ほぼ GitHubスクリプトの通りですが、サンプル数を合わせてあるのと、最後に個別データの確率を csv 出力する部分を追加してあります。

from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Activation, Dropout, Flatten, Dense
from keras import backend as K
import pandas

if __name__ == '__main__':
  img_width, img_height = 150, 150 # 訓練時の画像サイズ
  
  train_data_dir = 'data/train'
  validation_data_dir = 'data/validation'
  nb_train_samples = 20
  nb_validation_samples = 20
  epochs = 50
  batch_size = 5
  
  if K.image_data_format() == 'channels_first':
    input_shape = (3, img_width, img_height)
  else:
    input_shape = (img_width, img_height, 3)
  
  # モデル構築
  model = Sequential()
  model.add(Conv2D(32, (3, 3), activation='relu', input_shape=input_shape))
  model.add(MaxPooling2D(pool_size=(2, 2)))
  model.add(Conv2D(32, (3, 3), activation='relu'))
  model.add(MaxPooling2D(pool_size=(2, 2)))
  model.add(Conv2D(64, (3, 3), activation='relu'))
  model.add(MaxPooling2D(pool_size=(2, 2)))
  model.add(Flatten())
  model.add(Dense(64, activation='relu'))
  model.add(Dropout(0.5))
  model.add(Dense(1, activation='sigmoid'))
  
  model.compile(loss='binary_crossentropy', optimizer='rmsprop', metrics=['accuracy'])

  # 訓練データのプレ処理の設定: RGB値のスケーリングに加え、シア変形や拡大縮小によって訓練増強
  train_datagen = ImageDataGenerator(rescale=1. / 255, shear_range=0.2, zoom_range=0.2, horizontal_flip=True)
  # テストデータのプレ処理の設定: こちらはRGB値のスケーリングのみ
  test_datagen = ImageDataGenerator(rescale=1. / 255)
  
  # 訓練データをディレクトリ内のデータから随時作成するジェネレータ
  train_generator = train_datagen.flow_from_directory(train_data_dir,
    target_size=(img_width, img_height), batch_size=batch_size, class_mode='binary')
  # テストデータ作成をディレクトリ内のデータから随時作成するジェネレータ
  validation_generator = test_datagen.flow_from_directory(validation_data_dir,
    target_size=(img_width, img_height), batch_size=batch_size, class_mode='binary')
  
  # 訓練
  model.fit_generator(train_generator, steps_per_epoch=nb_train_samples//batch_size,
    epochs=epochs, validation_data=validation_generator, validation_steps=nb_validation_samples//batch_size)
  # 訓練済重みの保存
  model.save_weights('weight.h5')
  
  # 具体的に個々のデータに対してどのような確率分布になったかの出力
  train_generator = test_datagen.flow_from_directory(train_data_dir,
    target_size=(img_width, img_height), batch_size=1, class_mode='binary', shuffle=False)
  validation_generator = test_datagen.flow_from_directory(validation_data_dir,
    target_size=(img_width, img_height), batch_size=1, class_mode='binary', shuffle=False)
  
  predict = model.predict_generator(train_generator, steps=nb_train_samples)
  prob = pandas.DataFrame(predict, columns = ['curry'])
  prob['ramen'] = 1.0 - prob['curry']
  prob['predict'] = prob.idxmax(axis=1)
  prob.to_csv('train.csv', index=False, encoding='utf-8')
  
  predict = model.predict_generator(validation_generator, steps=nb_validation_samples)
  prob = pandas.DataFrame(predict, columns = ['curry'])
  prob['ramen'] = 1.0 - prob['curry']
  prob['predict'] = prob.idxmax(axis=1)
  prob.to_csv('test.csv', index=False, encoding='utf-8')