Kerasを使って2クラス分類のCNN

## はじめに

『PythonとKerasによるディープラーニング』の5章のサンプルをお手本に、自分のデータセットでCNNによる良品と不良品の判別をやってみたので、本のサンプルからの変更点を中心にメモしておきます。

## 使用するデータセットについて

- 写真のサイズは(h, w) = (475, 161)で、グレイ画像。
- データセットの写真は全部で18000枚。内訳は、
    - train: 良品、不良品それぞれ8000枚
    - validation: 良品、不良品それぞれ500枚
    - test: 良品、不良品それぞれ500枚
- 不良品画像は良品画像の9000枚に、人工的に作ったキズの画像をランダムな位置、貼り付け角度で張り付けたもの。（実際の不良品写真は使用していない）

実際の不良品画像を使用しないのは、写真のサイズを縮小しても不良箇所がわかるようにするためです。また、簡単に精度が出るように不良箇所をやや強調しています。
今回は実験なので、縮小画像を読み込むことでモデルを小さくして、学習時間も短くなるようにしています。

写真の保存先を設定しておきます。

```python
import os

# データセットを格納するディレクトリへのパス
base_dir = 'c:/temp/images'

# 訓練データセット、検証データセット、テストデータセットを配置するディレクトリ
train_dir = os.path.join(base_dir, 'train')
validation_dir = os.path.join(base_dir, 'validation')
test_dir = os.path.join(base_dir, 'test')
```

## モデルの定義


モデルの構成は犬猫判別の例を参考に次のように設定しました。

良品と不良品を見分ける2クラス分類なので、モデルの最終出力は1つで、活性化関数にはsigmoidを使っています。（犬猫の例と同じ）

犬猫判別の例との違いは層構成と入力画像サイズだけです。

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

# モデルの定義
model = models.Sequential()
model.add(layers.Conv2D(16, (3, 3), activation='relu', input_shape=(120, 40, 1)))
model.add(layers.Conv2D(16, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(32, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Flatten())
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(256, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))        # 犬猫と同じ2クラス分類で

# モデルのコンパイル
model.compile(loss='binary_crossentropy',
              optimizer=optimizers.RMSprop(lr=1e-4),
              metrics=['acc'])
```

`model.summary()`で確認すると次のようになります。
学習可能なパラメータ数は約380万と、犬猫判別の例と同等にできました。

```
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 118, 38, 16)       160       
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 116, 36, 16)       2320      
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 58, 18, 16)        0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 56, 16, 32)        4640      
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 28, 8, 32)         0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 7168)              0         
_________________________________________________________________
dense_1 (Dense)              (None, 512)               3670528   
_________________________________________________________________
dense_2 (Dense)              (None, 256)               131328    
_________________________________________________________________
dense_3 (Dense)              (None, 1)                 257       
=================================================================
Total params: 3,809,233
Trainable params: 3,809,233
Non-trainable params: 0
_________________________________________________________________
```

## ImageDataGeneratorの準備

犬猫判別の例と同じように、ImageDataGeneratorを使ってモデルに画像を供給します。
例との違いは`color_mode='grayscale'`を追加したことです。
画像の縮小もここで設定しています。

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

# すべての画像を1/255でスケーリング
train_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)

# ImageDataGeneratorを使ってディレクトリから画像を読み込む
train_generator = train_datagen.flow_from_directory(
    train_dir,                      # ターゲットディレクトリ
    target_size=(120, 40),          # すべての画像サイズを120*40に変更
    color_mode='grayscale',     # ここを追加
    batch_size=20,                  # バッチサイズ
    class_mode='binary')            # binary_crossentropyを使用するため2値のラベルが必要
validation_generator = test_datagen.flow_from_directory(
    validation_dir,
    target_size=(120, 40),
    color_mode='grayscale',
    batch_size=20,
    class_mode='binary')
```

## モデルの学習と保存

犬猫判別の例との違いはエポック数を10に減らした点のみです。

```python
# モデルの訓練
history = model.fit_generator(train_generator,
                              steps_per_epoch=100,
                              epochs=10,
                              validation_data=validation_generator,
                              validation_steps=50)

# モデルの保存
model.save('c:/temp/models/model_01_2class.h5')
```

## 学習の軌跡の表示

犬猫判別の例と同じように学習の軌跡を表示します。

```python
acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(acc) + 1)

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()
```

学習の結果、精度は次のようになりました。
過学習も見られず、精度も99%超と高い水準で張り付いており、うまくいっているようです。

```python
 89/100 [=========================>....] - ETA: 0s - loss: 0.0159 - acc: 0.9949
 91/100 [==========================>...] - ETA: 0s - loss: 0.0157 - acc: 0.9951
 93/100 [==========================>...] - ETA: 0s - loss: 0.0155 - acc: 0.9952
 95/100 [===========================>..] - ETA: 0s - loss: 0.0154 - acc: 0.9953
 97/100 [============================>.] - ETA: 0s - loss: 0.0151 - acc: 0.9954
 99/100 [============================>.] - ETA: 0s - loss: 0.0149 - acc: 0.9955
100/100 [==============================] - 4s 45ms/step - loss: 0.0150 - acc: 0.9955 - val_loss: 0.0203 - val_acc: 0.9960
```
![Figure_1.png](https://qiita-image-store.s3.amazonaws.com/0/280874/db880f77-036d-b117-301a-f56c8202574c.png)
![Figure_2.png](https://qiita-image-store.s3.amazonaws.com/0/280874/0aee57a2-7ebe-5ec7-1779-4e7fcd9130ee.png)