Kerasの使い方をDeep Learningする流れに沿ってまとめた(画像編)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404817/8a17909b-1e9c-e2ae-92af-922201a9a0ef.png)

バージョン: 2.2.4
[Keras Documentation](https://keras.io/)
[Keras Documentation (日本語)](https://keras.io/ja/) ←更新が遅い(？)ので非推奨

このまとめは、Kerasで画像を用いて深層学習をする際の流れに沿ってドキュメントから要点をまとめたものです。
一通りドキュメントに目を通すのが一番ですが、面倒くさい、あるいは読んだけどいちいちドキュメントから探すの面倒くさいって時にでも役に立ててください。

#  サンプル

* このまとめの手順に沿って作成したJupyter Notebook
https://github.com/souring001/deep-learning/blob/master/cifar10_cnn_keras.ipynb

* Keras公式のサンプル(画像分類、LSTM、文字認識、文章生成など)
https://github.com/keras-team/keras/tree/master/examples

#  インポート例
深層学習をするときに使うパッケージやOptimizer等のインポートは、必要に応じて以下のように行う。

```python
import tensorflow as tf
import numpy as np
import pandas as pd
import random as rn
import keras
import PIL
import matplotlib.pyplot as plt
from keras.preprocessing.image import ImageDataGenerator
from keras.datasets import cifar10
from keras.applications.vgg16 import VGG16
from keras.optimizers import Adam, SGD
from keras.models import Model
from keras.layers import Dense, Dropout, Activation, Flatten
from keras.layers import Conv2D, MaxPooling2D, BatchNormalization
```

#  乱数固定

開発中のパフォーマンスの変化が実際のモデルやデータの変更によるものなのか、単に新しいランダムサンプルの結果によるものなのかを判断するために、乱数を固定して再現性を持たせると良い。
(※しかし、以下のコードを冒頭に記してもlossやaccuracyが毎回異なる場合がある。原因究明中)

```python
import numpy as np
import tensorflow as tf
import random as rn

# Python 3.2.3以降で必要、ハッシュベースの操作に関して再現性を持たせる
import os
os.environ['PYTHONHASHSEED'] = '0'

# Numpy, Pythonでの乱数の固定
np.random.seed(42)
rn.seed(12345)

# TensorFlowはマルチスレッドだと再現性が無いので、シングルスレッドにする
session_conf = tf.ConfigProto(intra_op_parallelism_threads=1, inter_op_parallelism_threads=1)

# TensorFlowのバックエンドの乱数を固定
from keras import backend as K
tf.set_random_seed(1234)
sess = tf.Session(graph=tf.get_default_graph(), config=session_conf)
K.set_session(sess)
```

[Keras Documentation - How can I obtain reproducible results using Keras during development?](https://keras.io/getting-started/faq/#how-can-i-obtain-reproducible-results-using-keras-during-development)

#  データセットの準備
##  訓練、検証、テストデータに分割

データの数・バラつきとかハイパーパラメータの数によるが、比率70:15:15とか、60:20:20とかに分割する。
ImageDataGeneratorを使うと`validation_split`で好きな比率にtestとvalidationに分割できる。


##  画像のロード

### ImageDataGeneratorを使わない場合
x_train, y_train, x_test, y_testという配列(ndarray)を用意する。データ型は`float32`にする。

以下はCIFAR10のデータを読み込んで適切に変換した場合。`shape`と`dtype`が以下のようになっていれば良い。

```:output
x_train shape: (50000, 32, 32, 3) 	dtype: float32
y_train shape: (50000, 10) 			dtype: float32
x_test shape: (10000, 32, 32, 3) 	dtype: float32
y_test shape: (10000, 10) 			dtype: float32
```

`32x32x3`か`3x32x32`かは、バックエンドの設定が`channel_last`か`channel_first`かによる。
何もしていなければ`channel_last`のはず。詳しくはこちら→[Keras Documentation - Backend](https://keras.io/backend/)

画像分類の場合、ロス計算のためにOne-Hotベクトルに変換する。例：`6`→`[0. 0. 0. 0. 0. 0. 1. 0. 0. 0.]`

```python
num_classes = 10
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
```

画像はデータ型`float32`にして0~255を0~1の値にする。

```python
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255
```

####  画像が正しく設定できているか確認
データが思い通りになっているかのテストコード。
画像が横向きになっていたり、入力と出力のインデックスがずれていたりすることがある。

```python
idx = 0 # i番目のデータ
plt.imshow(x_train[idx])
plt.show()
print(y_train[idx])
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404817/b8a2d8f3-2e6d-b3cf-da86-d09cba5e502e.png)

6とはカエル(frog)を意味するラベル。これカエルなんだ...。

### ImageDataGeneratorを使う場合

画像分類の場合は以下のようなディレクトリ構造にしておく。

```text
data/
    train/
        dogs/
            dog001.jpg
            dog002.jpg
            ...
        cats/
            cat001.jpg
            cat002.jpg
            ...
    validation/
        dogs/
            dog001.jpg
            dog002.jpg
            ...
        cats/
            cat001.jpg
            cat002.jpg
            ...
```

class_modeは、2値分類ならば`binary`、多クラス分類なら`categorical`とする。

```python
train_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_directory(
        'data/train',
        target_size=(150, 150),
        batch_size=32,
        class_mode='binary')
        
test_datagen = ImageDataGenerator(rescale=1./255)
validation_generator = test_datagen.flow_from_directory(
        'data/validation',
        target_size=(150, 150),
        batch_size=32,
        class_mode='binary')
```

回帰問題を扱う場合は、`flow_from_dataframe`を使用する。
pandasでcsvから読み取ったデータを`dataframe`に指定して、`class_mode`は`other`とする。

[Keras Documentation - ImageDataGenerator class](https://keras.io/preprocessing/image/)

####  画像が正しく設定できているか確認

データの中身を表示するためのテストコード。
`next()`で中身を取得しているので、実行の都度内容が変わることに注意。
また、デフォルトだと`shuffle=True`なので、最初の画像とは限らない。

```python
batch = train_generator.next()
x, y = batch

idx = 0 # i番目のデータ
plt.imshow(x[idx])
plt.show()
print(y[idx])
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404817/ceb375c3-2e89-5a1c-1278-c8beda104ec7.png)

1とは飛行機(airplane)を意味するラベル。

##  既存のデータセット

Kerasには用意されたデータセットがあり、以下のようにすることで、使うことができる。
ただし、ラベルをOne-Hotベクトルやfloat32型に変換する必要あり。

```python
from keras.datasets import cifar10
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
```

以下のデータセットが使用可能(2019年6月17日現在)

* CIFAR10 画像分類
* CIFAR100 画像分類
* IMDB映画レビュー感情分類
* ロイターのニュースワイヤー トピックス分類
* MNIST 手書き数字データベース
* Fashion-MNIST ファッション記事データベース
* ボストンの住宅価格回帰データセット

[Keras Documentation - Datasets](https://keras.io/datasets/)

#  ネットワークモデルの作成

SequentialモデルAPIを使う方法と、Functional APIを使う方法の2種類がある。

以下のコードでは、Sequentialモデルを使ったCNNを作成する。
`input_shape`は32x32x3(`x_train.shape[1:]`)、outputは10(`num_classes`)としてある。

```python
model = Sequential()
model.add(Conv2D(32, (3, 3), padding='same', input_shape=x_train.shape[1:]))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(Conv2D(64, (3, 3), padding='same'))
model.add(Activation('relu'))
model.add(Conv2D(64, (3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(Flatten())
model.add(Dense(512))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes))
model.add(Activation('softmax'))
```

[Keras Documentation - About Keras models](https://keras.io/models/about-keras-models/)

##  最適化アルゴリズム(Optimizer)を定義
引数を設定しなければデフォルトの値となる。

```python
opt = RMSprop(lr=0.0001, decay=1e-6)
```

##  モデルにロス関数や最適化アルゴリズムを設定

```python
model.compile(loss='categorical_crossentropy',
              optimizer=opt,
              metrics=['accuracy'])
```

##  モデルの要約を出力

VGG16のモデルの場合、以下のような要約が出力される。

```python
model.summary()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404817/25b33da9-1337-6b66-2bea-1c09e658a55a.png)



#  学習


`model.fit`で学習が始まる。

```python
batch_size = 32
epochs = 20

model.fit(x_train, y_train,
            batch_size=batch_size,
            epochs=epochs,
            validation_data=(x_test, y_test),
            shuffle=True)
```


ImageDataGeneratorを使う場合は、`model.fit_generator`を使う。

```python
num_train_samples = 50000
model.fit_generator(
        train_generator,
        steps_per_epoch=num_train_samples/batch_size,
        epochs=epochs,
        validation_data=validation_generator,
        validation_steps=800)
```

#  テストデータで推定

`model.predict(x_test)`で学習済みモデルを用いて推定を行う。

```python
y = model.predict(x_test)
plt.imshow(x_test[0])
plt.show()
print(y[0])
print('pred_y:', np.argmax(y[0]), ', label:', np.argmax(y_test[0]))
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404817/cd16b1de-15c8-d9c9-2a45-e5ea50280766.png)

これが本当に猫なのかは人間にもわからない。

[Keras Documentation - predict](https://keras.io/models/sequential/#predict)

#  転移学習(fine-tuning)
VGGなどの学習済み(pre-trained)モデルをロードし、ネットワークの前半部分をfreezeして重みを更新できなくすると、少ないデータでも学習が可能となる。

以下の学習済みモデルが使用可能(2019年6月17日現在)

* Xception
* VGG16
* VGG19
* ResNet, ResNetV2, ResNeXt
* InceptionV3
* Inception-ResNetV2
* MobileNet
* MobileNetV2
* DenseNet
* NASNet

[Keras Documentation - Available models](https://keras.io/applications/#available-models)

##  学習済みモデルのロード

```python
from keras.applications.vgg16 import VGG16
model = VGG16(weights='imagenet', include_top=True)
```

##  特定のレイヤーの重み更新をしない(freeze)

コンストラクタから

```python
frozen_layer = Dense(32, trainable=False)
```

インスタンス化後にプロパティから変更

```python
layer = Dense(32)
layer.trainable = False
```

入力層側の18レイヤーをfreezeする。

```python
for layer in model.layers[:18]:
    print(layer.name)
    layer.trainable = False
```

[Keras Documentation - How can I "freeze" Keras layers?](https://keras.io/getting-started/faq/#how-can-i-freeze-keras-layers)

#  データ拡張(Data Augmentation)
入力画像に回転やシフトを施して入力データを拡張することで、より汎用的な結果が得られる。
KerasではImageDataGeneratorでデータ拡張ができる。

```python
batch_size = 32
epochs = 20
num_train_samples = 50000

datagen = ImageDataGenerator(
          width_shift_range=0.2,
          height_shift_range=0.2,
          shear_range=0.2,
          zoom_range=0.2,
          rotation_range=20,
          horizontal_flip=True
          )

train_generator = datagen.flow(x_train, y_train, batch_size=batch_size)

# 学習
model.fit_generator(train_generator,
                    epochs=epochs,
                    steps_per_epoch=num_train_samples/batch_size,
                    validation_data=(x_test, y_test))
```

|拡張前|拡張後|
|:-:|:-:|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404817/edd597b2-2492-dcae-c771-fce837e612e9.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404817/4f1dbe14-947e-baf2-7a7d-c68048d5a6af.png)|

ただし、物体検知や姿勢推定などの画像位置に関する回帰問題の場合は、回転や拡大した場合に出力値も同様の処理をする必要があるので、 generatorを自作する必要がある。

[Keras Documentation - ImageDataGenerator class](https://keras.io/preprocessing/image/)

#  多入力多出力

以下のような多入力多出力のモデルは、Functional APIによって作成できる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404817/ea3ab424-1b85-6468-1ca5-a25e4b03851c.png)

##  モデルの作成

モデルを作成した後に、入力と出力をリストで渡す。

```python
# 右上
main_input = Input(shape=(100,), dtype='int32', name='main_input')
x = Embedding(output_dim=512, input_dim=10000, input_length=100)(main_input)
lstm_out = LSTM(32)(x)

# 右下
auxiliary_output = Dense(1, activation='sigmoid', name='aux_output')(lstm_out)

# 左上
auxiliary_input = Input(shape=(5,), name='aux_input')
x = keras.layers.concatenate([lstm_out, auxiliary_input])

# 左下
x = Dense(64, activation='relu')(x)
x = Dense(64, activation='relu')(x)
x = Dense(64, activation='relu')(x)
main_output = Dense(1, activation='sigmoid', name='main_output')(x)

model = Model(inputs=[main_input, auxiliary_input], outputs=[main_output, auxiliary_output])
```

##  ロスの設定

出力に対してロス関数を個別で設定する場合は、リストで渡す。
以下のように、重みだけを個別で設定することもできる。

```python
model.compile(optimizer='rmsprop', loss='binary_crossentropy', loss_weights=[1., 0.2])
```

##  学習

入力、出力データもリストで渡す。

```python
model.fit([headline_data, additional_data], [labels, labels], epochs=50, batch_size=32)
```


[Keras Documentation - Multi-input and multi-output models](https://keras.io/getting-started/functional-api-guide/#multi-input-and-multi-output-models)

#  評価関数の作成

評価関数を自作したくなったら以下のように定義する。

```python
import keras.backend as K

def mean_pred(y_true, y_pred):
    return K.mean(y_pred)

model.compile(optimizer='rmsprop',
              loss='binary_crossentropy',
              metrics=['accuracy', mean_pred])
```

[Keras Documentation - Custom metrics](https://keras.io/metrics/#custom-metrics)

#  ロス関数の作成

ヒートマップの出力などをする場合は、通常のロス関数では全て0になってしまうなど、うまくいかないことがある。
その場合ロス関数を自作する。

MSEであれば以下のように定義できる。

```python
def my_loss(y_true,y_pred):
    return K.mean(K.square(y_pred - y_true), axis=-1)
```

使える関数(Backend functions)については、こちら→[Keras Documentation - Backend functions](https://keras.io/backend/#backend-functions)

#  学習したモデルの保存&ロード
モデルの保存

```python
model_name = 'model.h5'
model.save(model_name)
```

保存したモデルを使ってリアルタイムにCatかDogに分類するコード

```python
import tensorflow as tf
import cv2
import keras
import numpy as np

# モデルのロード
model = keras.models.load_model("model.h5")

# キャプチャの開始
cap = cv2.VideoCapture(0)

while True:
    ret, frame = cap.read()
    height, width = frame.shape[:2]
    # キャプチャの中心を正方形に切り取る
    frame = frame[0:height, int(width/2-height/2):int(width/2+height/2)]

    cv2.imshow('frame', frame)
    
    # 入力のサイズ(32x32)にリサイズする
    small = cv2.resize(frame,(32,32))
    # 4次元にリシェイプ
    small = small.reshape((1,32,32,3))/255
    # モデルで推定
    result = model.predict(small)

    if np.argmax(result) == 1:
        print("dog")
    else:
        print("cat")

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()
```

# おわりに
何か間違いとか、その他これもよく使うよなどあれば教えてください。