[R][Keras]RとKerasによる画像分類(VGG16)

RとKerasによる画像分類を試してみたので、備忘のため記録メモ。
VGG16を使用して、CPU環境でほどほどの精度で高速に、任意の数のカテゴリに画像を分類する場合の例。
とりあえず、GPU環境がない or 設定してないけど画像分類やってみたいというレベルなので、精度はほどほどです。

#フォルダ構成
`image_data_generator`関数を使用する場合、分類したい画像ファイルを以下のようなフォルダ構成にしておきます。

訓練したい画像と検証したい画像を、それぞれのフォルダのサブフォルダに事前に分類して保存しておく。
また、テスト画像は予測対象であり、分類がもちろん事前には不明なので、フォルダ分けは不要ですが、任意の名前でサブフォルダには格納しておく必要があります。（結構ここでハマった・・・）

※`image_data_generator`関数はディスク上のファイルを前処理済みのテンソルのバッチに自動変換してくれる関数。

```

　 data/
  　 ┣　train_image/
   　┃         ┣ category_1/
  　 ┃         ┃       　　┣  t1_001.jpg
  　 ┃         ┃       　　┣  t1_002.jpg
  　 ┃         ┃            ...
   　┃         ┣ category_2/
  　 ┃         ┃       　　┣  t2_001.jpg
  　 ┃          ...      　┣  t2_002.jpg
  　 ┃                      ...
  　 ┃
  　 ┣　validation_image/
   　┃         ┣ category_1/
  　 ┃         ┃       　　┣  v1_001.jpg
  　 ┃         ┃       　　┣  v1_002.jpg
  　 ┃         ┃            ...
   　┃         ┣ category_2/
  　 ┃         ┃       　　┣  v2_001.jpg
  　 ┃       　 ...      　┣  v2_002.jpg
  　 ┃                     ...
  　 ┗　test_image/
   　          ┗ test_all/
  　             　      　┣  001.jpg
  　        　           　┣  002.jpg
  　                       ...

```

#モデル
kerasで使えるイメージ分類モデルは以下のようなものがあるそうです。
全てImageNetデータセットで訓練済みのもの。

- Xception 　
- Inception V3
- ResNet50
- VGG16
- VGG19
- MobileNet

今回はVGG16を用いて分類を実施しました。
（「ImageNet」と呼ばれる大規模画像データセットで学習された，16層からなるCNNモデル）


#コード

VGG16の畳み込み層インスタンス読み込み

```r
library(keras)
install_keras()

#VGG16
conv_base <- application_vgg16(weights = "imagenet",
                               include_top = F,
                               input_shape = c(96, 96, 3))

```
それぞれの引数の意味は以下。

- weights  : 重みの種類
- include_top : ネットワークの最上位（一番最後）の全結合層の分類器を含むかどうか。VGG16は、1000の全結合層を持つが、ここを2値分類とか、1000以外の分類に変更する場合はFalseに設定の上、全結合層を自前で準備する。
- input_shape : 入力画像の形状を指定

読み込んだ畳み込み層を確認。

```
> summary(conv_base)
____________________________________________________________________________________________
Layer (type)                             Output Shape                         Param #       
============================================================================================
input_3 (InputLayer)                     (None, 96, 96, 3)                    0             
____________________________________________________________________________________________
block1_conv1 (Conv2D)                    (None, 96, 96, 64)                   1792          
____________________________________________________________________________________________
block1_conv2 (Conv2D)                    (None, 96, 96, 64)                   36928         
____________________________________________________________________________________________
block1_pool (MaxPooling2D)               (None, 48, 48, 64)                   0             
____________________________________________________________________________________________
block2_conv1 (Conv2D)                    (None, 48, 48, 128)                  73856         
____________________________________________________________________________________________
block2_conv2 (Conv2D)                    (None, 48, 48, 128)                  147584        
____________________________________________________________________________________________
block2_pool (MaxPooling2D)               (None, 24, 24, 128)                  0             
____________________________________________________________________________________________
block3_conv1 (Conv2D)                    (None, 24, 24, 256)                  295168        
____________________________________________________________________________________________
block3_conv2 (Conv2D)                    (None, 24, 24, 256)                  590080        
____________________________________________________________________________________________
block3_conv3 (Conv2D)                    (None, 24, 24, 256)                  590080        
____________________________________________________________________________________________
block3_pool (MaxPooling2D)               (None, 12, 12, 256)                  0             
____________________________________________________________________________________________
block4_conv1 (Conv2D)                    (None, 12, 12, 512)                  1180160       
____________________________________________________________________________________________
block4_conv2 (Conv2D)                    (None, 12, 12, 512)                  2359808       
____________________________________________________________________________________________
block4_conv3 (Conv2D)                    (None, 12, 12, 512)                  2359808       
____________________________________________________________________________________________
block4_pool (MaxPooling2D)               (None, 6, 6, 512)                    0             
____________________________________________________________________________________________
block5_conv1 (Conv2D)                    (None, 6, 6, 512)                    2359808       
____________________________________________________________________________________________
block5_conv2 (Conv2D)                    (None, 6, 6, 512)                    2359808       
____________________________________________________________________________________________
block5_conv3 (Conv2D)                    (None, 6, 6, 512)                    2359808       
____________________________________________________________________________________________
block5_pool (MaxPooling2D)               (None, 3, 3, 512)                    0             
============================================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0
____________________________________________________________________________________________
```
最終的な特徴量は、(3, 3, 512)という形状になっていることが確認できます。
この最終出力の特徴量を、任意の数の全結合層へつなぎます。
今回は10カテゴリの画像分類として進めます。

```r
# ファイルパスの指定
train_image_files_path <- file.path(".../train_image/")
validation_image_files_path <- file.path(".../validation_image/")

datagen <- image_data_generator(rescale = 1/255)
batch_size <- 20

# 特徴量作成用に2つ関数を作成（train/val用とtest用）
# train/val画像用
extract_features <- function(directory, sample_count){
  
  features <- array(0, dim = c(sample_count, 3, 3, 512))
  labels <- array(0, dim = c(sample_count, 10))
  
  generator <- flow_images_from_directory(
    directory = directory,
    generator = datagen,
    target_size = c(96, 96),
    batch_size = batch_size,
    class_mode = "categorical",
    classes = as.character(c(0:9)) #任意のカテゴリ
  )
  
  i <- 0
  while(T){
    batch <- generator_next(generator)
    inputs_batch <- batch[[1]] # 特徴量
    labels_batch <- batch[[2]] # ラベル。2値分類はベクトル。カテゴリ分類は行列で出力される。
    features_batch <- conv_base %>% predict(inputs_batch)
    
    index_range <- ((i * batch_size) + 1):((i + 1) * batch_size)
    features[index_range,,,] <- features_batch
    labels[index_range, ] <- labels_batch #2値分類の場合は labels[index_range]
    
    i <- i + 1
    if (i * batch_size >= sample_count)
      break
  }
  
  list(
    features = features,
    labels = labels
  )
}

# test画像用の特徴量作成関数。testファイルについては、batch_size = 1とshuffle = Fにする必要がある。
extract_features_test <- function(directory, sample_count){
  
  features <- array(0, dim = c(sample_count, 3, 3, 512))
  labels <- array(0, dim = c(sample_count, 10))
  
  generator <- flow_images_from_directory(
    directory = directory,
    generator = datagen,
    target_size = c(96, 96),
    batch_size = 1,
    shuffle = F,
    class_mode = "categorical"
  )
  
  i <- 0
  while(T){
    batch <- generator_next(generator)
    inputs_batch <- batch[[1]]
    labels_batch <- batch[[2]]
    features_batch <- conv_base %>% predict(inputs_batch)

    index_range <- ((i * 1) + 1):((i + 1) * 1) # batch_sizeが1
    features[index_range,,,] <- features_batch
    labels[index_range, ] <- labels_batch
    
    i <- i + 1
    if (i * 1 >= sample_count)
      break
  }
  
  list(
    features = features,
    labels = labels
  )
}
```
以下、特記とハマった注意事項
それぞれの関数について、

- `image_data_generator`:リアルタイムのデータ拡張により、画像データのバッチを生成。上記の場合は、
- `flow_images_from_directory`:ディレクトリ内の画像からデータのバッチを生成。
- `generator_next` :`flow_images_from_directory`で読み込んだバッチから、ラベルと特徴量を生成。
- `predict`:モデルとバッチから特徴量を予測。

`flow_images_from_directory`の引数について、

- class_mode : 2値分類なら"binary"、それ以上なら"categorical"を指定すればよい。
- classes : 10分類にしたいので今回は0:9で設定。binaryの場合は不要。
- target_size : 入力の画像サイズ。

また、labelsについては、binaryの場合は0 or 1の一次元ベクトルとなる。
categoricalを指定した場合は、各ラベル数の列を持った0 or 1の行列になるので注意。


以下のコードで、batch/generatorのデータを確認できます。

```r:参考
# generator_nextで作成したバッチの例。特徴量とラベルのリストとなる。
> str(batch)
List of 2
 $ : num [1:20, 1:96, 1:96, 1:3] 0.128 0.352 0.132 0.098 0.786 ...
 $ : num [1:20, 1:10] 1 0 0 0 0 1 0 0 0 1 ...

# flow_images_from_directoryの読み込んだ画像の数。nで指定。
> generator$n
# flow_images_from_directoryの読み込んだ画像のファイル名。filenamesで指定。
> generator$filenames
# flow_images_from_directoryの読み込んだ画像のラベル。labelsで指定。
> generator$labels
# flow_images_from_directoryの読み込んだ画像のクラスを確認。class_indicesで指定。
> generator$class_indices
```

以下続き。

```r
# train/val/testでそれぞれ特徴量とラベルを作成。
train <- extract_features(train_image_files_path, 4000)
validation <- extract_features(validation_image_files_path, 1000)
test <- extract_features_test(test_image_files_path, 3000)

reshape_features <- function(features){
  array_reshape(features, dim = c(nrow(features), 3 * 3 * 512))
}

# 全結合層につなぐため、特徴量を一次元に変換。
train$features <- reshape_features(train$features)
validation$features <- reshape_features(validation$features)
test$features <- reshape_features(test$features)

# msequential modelの作成
model <- keras_model_sequential() %>% 
  layer_dense(units = 256, activation = "relu",
              input_shape = 3 * 3 * 512) %>% 
  layer_dropout(rate = 0.4) %>% 
  layer_dense(units = 10, activation = "softmax")

model %>% compile(
  optimizer = optimizer_rmsprop(lr = 1.5e-5),
  loss = "categorical_crossentropy",
  metrics = c("acc")
)

history <- model %>% fit(
  train$features, train$labels,
  epochs = 40,
  batch_size = 20,
  validation_data = list(validation$features, validation$labels)
)


# plotで評価結果を可視化できる。
plot(history)

# testファイルを予測したい場合は以下。
model %>% predict(test$features)


```

全結合層を処理するだけなので、1epoch毎4-5秒で、高速に処理が終了しました。
精度をより上げたいのであれば、GPU環境の元、データ拡張を用いた特徴量抽出等を行う必要があるようですが、低コストでとりあえずお試しで分類したいという場合には、良い方法かもしれません。

<img src="https://i.imgur.com/maX9xeh.png" width="100%">



#参考

- [RとKerasによるディープラーニング](https://www.amazon.co.jp/R%E3%81%A8Keras%E3%81%AB%E3%82%88%E3%82%8B%E3%83%87%E3%82%A3%E3%83%BC%E3%83%97%E3%83%A9%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0-Fran%C3%A7ois-Chollet/dp/4873118573)
- [It's that easy! Image classification with keras in roughly 100 lines of code.](https://shirinsplayground.netlify.com/2018/06/keras_fruits/)
- [自前の写真をKerasで学習したい [4/18コード追記]](https://plant-raspberrypi3.hatenablog.com/entry/2018/04/14/150012)