Google ColaboratoryでKerasを使ってCNNを実装してみた

# はじめに
### 目的
**Keras**を使って、**CNN（畳み込みニューラルネットワーク）**を実装し、初心者なので、とりあえず動かしてみることを目的とする。（よって、パラメータとかは適当に設定している。）
実行環境としては、**Google Colaboratory**を使用した。（とても便利）
### テーマ
シンプルな画像の2値分類に挑戦してみた。テーマも簡単そうな「日本人と外国人の分類」としてみた。

# 画像収集
**icrawler**を使って画像収集を行った。今回は、日本人と外国人の画像を各200枚程度収集した。

```python
!pip install icrawler
```

```python
from icrawler.builtin import BingImageCrawler
crawler = BingImageCrawler(storage={"root_dir": "input"})
crawler.crawl(keyword="日本人", max_num=200)
```
続いて、画像の保存先となるGoogleドライブのマウントを行った。
Colab上では、「ドライブをマウントする」(赤点線部分)を押下するだけでマウントが完了する。
![cnn_図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696527/1e0b9256-1484-39ca-170d-ef2e8364d5d8.png)

# 顔検出と切り出し
先ほど、収集した画像には背景など余計な情報が含まれているため、顔検出をして、切り出しをする。
顔検出には、OpenCVのカスケード識別器（haarcascade_frontalface_alt.xml）を利用した。

```python
import glob
import os
import cv2

# 検出対象のディレクトリを指定
in_dir = "XXX"
# 保存先のディレクトリを指定
out_dir = "XXX"

os.makedirs(out_dir, exist_ok=True)
in_jpg = os.listdir(in_dir)
for i in in_jpg:
  image = cv2.imread(in_dir + i)

  # カスケード型識別器の読み込み
  cascade = cv2.CascadeClassifier("XXX/haarcascade_frontalface_alt.xml")
  
  image_gs = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

  #顔検出の実行
  face_list = cascade.detectMultiScale(image_gs,scaleFactor=1.1,minNeighbors=2,minSize=(64,64))
  for rect in face_list:
    x,y,width,height = rect
    dst = image_gs[y:y + height, x:x + width]
    image = image[rect[1]:rect[1]+rect[3],rect[0]:rect[0]+rect[2]]
    image = cv2.resize(image,(64,64))

    #保存（任意のファイル名）
    fileName = out_dir + "CutOut_" + i
    cv2.imwrite(str(fileName),image)
```

# モデルの構築
参考書などを基にモデルを構築した。

```python
from keras import layers
from keras import models
from keras import optimizers
from keras.layers import GlobalAveragePooling2D

# 画像サイズ
input_width = 64
input_height = 64

# CNN
model = models.Sequential()
model.add(layers.Conv2D(64, (3, 3), activation='relu', padding='same', 
                        input_shape=(input_width, input_height, 3)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu', padding='same'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(256, (3, 3), activation='relu', padding='same'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(GlobalAveragePooling2D())
model.add(layers.Dense(256, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(loss='binary_crossentropy', optimizer=optimizers.RMSprop(lr=1e-4), 
              metrics=['acc'])
```


# データの水増し
ImageDataGeneratorを利用して、画像を回転、拡大・縮小をすることで、顔検出した学習データの水増しを行う。学習データは以下のディレクトリ構成のように、「日本人」「外国人」とフォルダ分けをしておくことで、勝手にラベル付けをしてくれる。

 &emsp;train/
  &emsp;&emsp;japanese/
   &emsp;&emsp;&emsp;japanese001.jpg
   &emsp;&emsp;&emsp;japanese002.jpg
   &emsp;&emsp;&emsp;...
  &emsp;&emsp;others/
   &emsp;&emsp;&emsp;american.jpg
   &emsp;&emsp;&emsp;...
    

また、validation_splitを指定することで、学習データの10%を検証データとして使用した。

```python
from keras.preprocessing.image import ImageDataGenerator

# 学習データのディレクトリを指定
train_dir = "XXX/train"

# バッチサイズを指定
batch_size = 1

# データの水増し
datagen = ImageDataGenerator(
    rescale=1./255,
    rotation_range=30,
    width_shift_range=0.1,
    height_shift_range=0.1,
    shear_range=0.1,
    zoom_range=0.1,
    horizontal_flip=True,
    validation_split = 0.1
    )

# データの読み込み
generator = datagen.flow_from_directory(
            train_dir,
            target_size=(input_width, input_height),
            batch_size=batch_size,
            class_mode='binary',
            shuffle=True,
            subset = "training" 
            )
```

# 学習

エポック数とステップ数を指定して、学習を実施した。
正解率は88%程度だった。

```python
history = model.fit_generator(
      generator,
      steps_per_epoch=len(generator)/batch_size,
      epochs=1000,
      validation_steps=len(generator)*0.1/batch_size,
      validation_data=generator
      )
```

# テスト
最後に、構築したモデルが「日本人」と「外国人」に正しく分類ができるかをテストデータを用いて確認した。

```python
from skimage import io
import numpy as np

# テストデータのディレクトリを指定
img = io.imread("XXX")

io.imshow(img)
classes = model.predict_classes(np.expand_dims(img[:,:,0:3]/255, axis=0))
print(classes)
```

![cnn_図2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696527/ab1343c7-0536-0348-45f6-cfceddc4ddbc.png)
![cnn_図3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696527/3e437c4a-9dfa-ed87-bfa4-b7b449646b8f.png)
上記の通り、概ね「日本人」と「外国人」を分類することが出来ていたが、ハーフ顔のタレントを「外国人」に分類していたりしたため、学習データ不足を痛感した。