学習データが用意できたら、学習を行う。
Kerasを使った画像分類についてはBuilding powerful image classification models using very little dataがとてもわかりやすく、こちらに従って進めていった。
ImageDataGeneratorを使った読み込み
学習用/テスト用の画像データの読み込みにはImageDataGeneratorを使用する。
train_datagen = ImageDataGenerator(rescale=1./255,
zoom_range=0.2,
horizontal_flip=True,
rotation_range=5)
test_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_directory(
train_dir,
target_size=(image_size, image_size),
batch_size=batch_size,
class_mode="categorical")
test_generator = test_datagen.flow_from_directory(
test_dir,
target_size=(image_size, image_size),
batch_size=batch_size,
class_mode="categorical")
それぞれtrain_dir
(./data/train)とtest_dir
(./data/test)で指定したディレクトリから、データを読み込む。
まず、下のtrain_generator
とtest_generator
の方から見て行く。flow_from_directory
はその名の通り、指定したディレクトリから画像を取り出していってくれる。
画像一式はtarget_size
で指定した大きさに自動的にリサイズされて読み込まれる。ここでは、横image_size
* 縦image_size
のサイズになるように指定している。image_size
は後述するニューラルネットワークの入力層のサイズになる。image_size
が小さすぎると判定の精度が下がり、大きすぎると学習がうまくいかなかったり、処理が重くなりすぎてしまう。今回のフレンズ識別器では128
にしているが、ここは扱う内容とマシンスペックなどに合わせて調整する。
batch_size
はミニバッチ学習に使われるバッチのサイズ。これも値は任意に調整すれば良いが、テスト用データの枚数を割り切れる数にしないとおかしなことになる可能性があるので注意する。
class_mode
は学習に使われる「正解ラベル」をどのように生成するか、に影響する。チュートリアル記事は「犬か猫か」の2値(0か1)の判定になるため、binary
を指定しているが、今回のように複数クラスの分類の場合はcategorical
を指定する。categorical
を指定すると、カテゴリ(ディレクトリ)ごとに正解フラグ(1)がたったone-hot
ベクトルが生成される。(one-hot
ベクトルについては、TensorFlowのMNISTのチュートリアルに詳しい説明がある)
今回はカラーで学習/識別させたいのでその機能は使っていないが、color_mode
という設定を使うとグレースケール化して読み込むこともできる。
さて、ImageDataGenerator
にはデータオーギュメンテーションのための機能が用意されている。画像の読み込みの際に、ランダムに回転させたり傾けたり引き延ばしたりする機能で、これを使うと過剰適合を防いだり、ロバスト性を改善する(入力画像がちょっと傾いてたり偏っていても判定できる)効果があると言われている。
学習に使うデータを生成するためのtrain_datagen
では、この機能を使って引き延ばし(zoom_range
)や左右反転(horizontal_flip
)、回転(rotation_range
)を行いながら画像を読み込んで行くようになっている。今回は使わないが、vertical_flip
やshear_range
など、他にも色々データオーギュメンテーションのための機能は用意されているので、入力画像の性質に合わせて利用すると良い。
なお、rescale
は正規化のための仕組みで、RGB(0〜255)で読み込まれた各画素のRGB値を0.0〜1.0の間に収まるように正規化している。
本当に便利だね、ImageDataGenerator。
モデルの設定
model = Sequential()
model.add(Conv2D(32, (3, 3), input_shape=(image_size, image_size, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(32, (3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(64, (3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dense(64))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(units=num_categories, activation="sigmoid"))
adam = Adam(lr=1e-5)
model.compile(loss='categorical_crossentropy', optimizer=adam, metrics=['accuracy'])
ここで使っているモデルは、入力層と出力層を除いて基本的にチュートリアルの内容そのままとなっている。
入力層は、チュートリアルの方では(3, 150, 150)
となっているが、最新のTensorFlow + Kerasの組み合わせだとこれでは動かない。調べていくと、どうやらKerasはもともとTensorFlowではなくTheanoという別のライブラリをバックエンドに使って動くように設計されていたらしく、TheanoのConvolutionレイヤーとTensorFlowのConvolutionレイヤーで配列の並びに違いがあることが原因のようだ。(おそらくこの記事はTheanoを使う時代に書かれた)。TensorFlowでは(横, 縦, チャンネル数(RGB))の順番で指定してやるのが正しいので、(image_size, image_size, 3)
を指定している。
出力層はカテゴリ数分のsigmoidとした。通常、カテゴリ分類ではsoftmaxを使うことが多いが、softmaxを使うと「どの特徴にも当てはまらない」ものも高い数字が出力されてしまう可能性があり、対象外のフレンズ(のけもの)の画像が入って来たときにおかしなことになってしまう。
OptimizerにはAdam
を利用。これも記事ではrmsprop
を使っているが、特に理由がないのでより性能が良いと言われているAdam
を利用している。サンプルが少ないせいか、モデルとマッチしないせいか、学習が発散する傾向にあったので、学習レートは若干低めの1e-5
を指定した。
損失関数はcategorical_crossentropy
。これも記事中では2値の判定のためbinary_crossentropy
が使われていたが、今回は複数カテゴリの分類なので、複数カテゴリ用のcategorical_crossentropy
を使う。
学習
checkpoint_cb = ModelCheckpoint("snapshot/{epoch:03d}-{val_loss:.5f}.hdf5", save_best_only=True)
model.fit_generator(
train_generator,
steps_per_epoch=num_train_images // batch_size,
epochs=200,
validation_data=test_generator,
validation_steps=num_test_images // batch_size,
callbacks=[checkpoint_cb])
200エポックほど学習を行う。
本質的ではない割に長いコードになるので省略したが、学習用の画像の枚数とテスト用のデータの枚数をnum_train_images
とnum_test_images
にセットしている。ImageDataGeneratorにこの値を自動取得する機能が欲しいのだが、なぜか用意されていない。ここだけ不便。
良い結果が得られたら、その時点のスナップショット(ModelCheckpoint)をsnapshot
ディレクトリに保存するようにしている。save_best_only
を指定すると、損失関数のスコア(val_loss
)が下がった際に、その時点のモデルのスナップショットをhdf5
形式で保存してくれる。(指定しないと、全てのエポックのスナップショットが保存される)
なおあとで、このhdf5
を使ってモデルを復元することができるので、分類時にはスコアの良いスナップショットを選んで利用することになる。
学習完了
学習が完了したら、次はこの学習済みモデルを使って未学習の入力画像を判定する仕組みを作ってみる。