TensorFlow + Kerasでフレンズ識別する - その2: 簡単なCNNを使った学習編

学習データが用意できたら、学習を行う。

Kerasを使った画像分類については[Building powerful image classification models using very little data](https://blog.keras.io/building-powerful-image-classification-models-using-very-little-data.html)がとてもわかりやすく、こちらに従って進めていった。

## 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。

[ImageDataGenerator](https://keras.io/preprocessing/image/)

## モデルの設定

```
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`を使ってモデルを復元することができるので、分類時にはスコアの良いスナップショットを選んで利用することになる。

## 学習完了

学習が完了したら、次はこの学習済みモデルを使って未学習の入力画像を判定する仕組みを作ってみる。

[TensorFlow + Kerasでフレンズ識別する - その3: 分類編](http://qiita.com/croquette0212/items/1f52913cc861ae8cebad)