KerasでCIFAR-10画像データセットの分類モデルを作る：ResNetでテキスト通りにやったら精度がでなかった

#はじめに
CNNなどディープラーニングを使って、CIFAR-10データセットを分類するモデルをいくつか作ってみました。
この記事では、純粋にモデルアーキテクチャの違いによる性能を評価したいので、**Data Augmentationは実施しません。**
参考にしたのは、[Advanced Deep Learning with Keras](https://www.amazon.co.jp/Advanced-Deep-Learning-Keras-reinforcement-ebook/dp/B078N8RDCP) という本です。
はじめに畳み込みニューラルネットワーク（CNN）で精度がどこまで上がるかを試した後で、ResNetなどのアーキテクチャを試してみます。
ResNetを参考にした本通りに作ってみたのですが、このコードはData Augmentationを前提としており、最初は全然精度が出ませんでしたので、少し工夫してみました。

##CIFAR-10について
CIFAR-10は60,000件の画像データセットで、1枚の画像は32×32ピクセル、RGBの3チャンネルです。訓練用データセットが50,000、テスト用データが10,000件がら構成されています。
クラスは10種類あり、飛行機、自動車、鳥、猫、鹿、犬、蛙、馬、船、トラックです。
![001_CIFAR10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225870/0122ca52-6f09-d02e-33af-f5def39b0fa1.png)
Kerasを使うときは、以下のコードでCIFAR-10データセットをロードできます。深層学習を行うときの常として、255で割って数値を0から1の間で規格化しておきます。

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

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

#基準となる簡単なCNNモデル
2層の畳み込み層、MaxPooling、Dropoutで構成される簡単なモデルでどの程度精度が出るか試してみました。

```python
input_shape = (32, 32, 3)

model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3),
                activation='relu',
                input_shape=input_shape))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))

# 損失関数,最適化関数,評価指標を指定してモデルをコンパイル
model.compile(loss=keras.losses.categorical_crossentropy,
             optimizer=keras.optimizers.Adam(),
             metrics=['accuracy'])

#学習を実施
history = model.fit(x_train, y_train,
         batch_size=batch_size,
         epochs=epochs,
         verbose=1,
         validation_data=(x_test, y_test))
```

学習した結果です。テストデータに対する精度は**71.6%**でした。
![011_simpleCNN.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225870/6deceda9-7d67-9a78-7e2d-e660d38acb1e.png)



#複雑なCNNモデル
畳み込み層の数を増やし、フィルタ数も増やし、Batch Normalizationを入れます。

```python
input_shape = (32, 32, 3)

model = Sequential()
model.add(Conv2D(64, kernel_size=(3, 3),
                input_shape=input_shape, padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))

model.add(Conv2D(128, kernel_size=(3, 3),  padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))

model.add(Conv2D(128, kernel_size=(3, 3),  padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))

model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(Conv2D(256, (2, 2), padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))

model.add(Conv2D(256, (2, 2), padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))

model.add(Conv2D(256, (2, 2), padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))

model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(Conv2D(512, (2, 2), padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))

model.add(Conv2D(512, (2, 2), padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))

model.add(Conv2D(512, (2, 2), padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))

model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(Flatten())
model.add(Dense(1024))
model.add(BatchNormalization())
model.add(Activation('relu'))

model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))

# 損失関数,最適化関数,評価指標を指定してモデルをコンパイル
model.compile(loss=keras.losses.categorical_crossentropy,
             optimizer=keras.optimizers.Adam(),
             metrics=['accuracy'])
```

また、以下の関数を定義して学習率を学習回数に応じて減衰させます。

```python
def step_decay(epoch):
    lr = 0.001
    if(epoch >= 20):
        lr/=2
    if(epoch>=30):
        lr/=4
    if(epoch>=50):
        lr/=5
    return lr

#学習率減衰
lr_decay = LearningRateScheduler(step_decay)
#学習の実施
history = model.fit(x_train, y_train,
         batch_size=batch_size,
         epochs=epochs,
         verbose=1,
         validation_data=(x_test, y_test),
         callbacks = [lr_decay])
```
学習した結果です。訓練データに対する精度は98.9%となり、特徴をかなり学習しています。また、テストデータに対する精度は**90.5%**になりました。
![012_complicated_CNN.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225870/e9948cde-7209-6824-0757-27280066fc74.png)

#ResNet
## ResNetについて
ResNetは勾配消失問題を解決するために、Deep Residual Learningという手法を用いた畳み込みニューラルネットワークのです。論文"[Deep Residual Learning for Image Recognition](https://arxiv.org/abs/1512.03385)"で提唱されました。畳み込み層を重ねていくと同時に、下図の右側の線の様に入力をそのまま畳み込みからの出力に足し合わせるという操作をします。このことによって、残差が下の層まで伝わっていくようになっています。
![101_ResNet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225870/391092dd-bc67-9ed6-eba5-51143e60d49b.png)
ResNetの詳細、なぜ性能を出すかについては、例えば[この記事](https://qiita.com/koharite/items/3be5bea73925b609f6b0)を参照してください。

##ResNetをKerasで実装する
冒頭で参照したテキスト通りに実装します。ただし、Data Augumentationだけは実施しないようにしました。

まず、共通で使う最小の要素を定義します。最小の要素は「2次元畳み込み」→「Batch Normalization」→「Activation（ReLU）」です。ただし、上のDeep Residual Learningの図にもある通り、畳み込み層の出力と入力そのものを足した（図の⊕の部分）後にActivation（ReLU）を適用することもあるので、Activationを実行するかを制御できるようにしています。

```python
def resnet_layer(inputs, num_filters=32, kernel_size=3,
                 strides=1, activation='relu', batch_normalization=True, conv_first=True):

    conv = Conv2D(num_filters,
                  kernel_size=kernel_size,
                  strides=strides,
                  padding='same',
                  kernel_initializer='he_normal',
                  kernel_regularizer=l2(1e-4))

    x = inputs
    if conv_first:
        x = conv(x)
        if batch_normalization:
            x = BatchNormalization()(x)
        if activation is not None:
            x = Activation(activation)(x)
    else:
        if batch_normalization:
            x = BatchNormalization()(x)
        if activation is not None:
            x = Activation(activation)(x)
        x = conv(x)
    return x
```

この要素を繰り返し重ねることで、全体のResNetアーキテクチャが構成されます。
nというパラメータで重ねる層の数を指定します。
フィルター数は16から始めて、32、64までの３段階で増えていきます。これがfor文のstack変数に相当します。
この段階の中で、パラメータnの数だけDeep Residual Learningを繰り返します。つまり、２つの畳み込みニューラルネットワークを重ね、その後で畳み込みの出力と元の出力を足し、最後にReLUで活性化する構造をn回繰り返します。

畳み込みの出力と元の入力を足し合わせるというのを表現するために、Kerasではadd([x,y])と実装します。実に簡単に実現できますね。ただし、フィルタ数を増やした直後では、畳み込みの出力と元の入力の次元が違うため、次元を合わせる操作が入っています。

```python
    n=3
    
    num_filters = 16
    num_res_blocks = n

    inputs = Input(shape=input_shape)
    x = resnet_layer(inputs=inputs)
    # instantiate the stack of residual units
    for stack in range(3):
        for res_block in range(num_res_blocks):
            strides = 1
            # first layer but not first stack
            if stack > 0 and res_block == 0:  
                strides = 2  # downsample
            y = resnet_layer(inputs=x,
                             num_filters=num_filters,
                             strides=strides)
            y = resnet_layer(inputs=y,
                             num_filters=num_filters,
                             activation=None)
            # first layer but not first stack
            if stack > 0 and res_block == 0:
                # 畳み込みの出力と、元の入力の次元を合わせる
                x = resnet_layer(inputs=x,
                                 num_filters=num_filters,
                                 kernel_size=1,
                                 strides=strides,
                                 activation=None,
                                 batch_normalization=False)
            x = add([x, y])
            x = Activation('relu')(x)
        num_filters *= 2
   
    # add classifier on top.
    x = AveragePooling2D(pool_size=8)(x)
    y = Flatten()(x)
    outputs = Dense(num_classes,
                    activation='softmax',
                    kernel_initializer='he_normal')(y)
```

##テキスト通りの実装で動かしてみたら、、
まずテキスト通りに実装して動かしてみました。
n=3としたResNet20の結果です。学習の結果は下図の通りです。
![102_ResNet_DropOutなしn3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225870/3738336c-c240-3b95-7fb3-3807a3dedbf2.png)
訓練データに対しては96.2%の精度が出ていますが、テストデータに対しては**66.2%**しかでていません。基準となる簡単なCNNよりも精度が出ていない。。。
訓練データに対して過学習しています。Data Augmentationでデータ水増しをやらない場合、モデルが複雑すぎて過学習が起きてしまい、テストデータに対する精度が出ないようです。

## Dropoutを入れてみたら精度が上がった
過学習している時の常套手段として、Dropoutを入れてみました。Dropoutの入れ方は、[Residual Network(ResNet)の理解とチューニングのベストプラクティス](https://deepage.net/deep_learning/2016/11/30/resnet.html)という記事を参考に、畳み込み層の間にDropoutを入れてみました。

```python
            y = resnet_layer(inputs=x,
                             num_filters=num_filters,
                             strides=strides)
           #畳み込み層の間にDropoutを追加
            y = Dropout(0.3)(y)
            y = resnet_layer(inputs=y,
                             num_filters=num_filters,
                             activation=None)
```
また、最後の全結合層による分類の部分にも、Flattenを実施した後にDropoutを入れました。

```python

    #最後の全結合層による分類にもDropOutを入れる。
    # add classifier on top.
    # v1 does not use BN after last shortcut connection-ReLU
    x = AveragePooling2D(pool_size=8)(x)
    y = Flatten()(x)
    y = Dropout(0.4)(y) #Dropoutを追加
    outputs = Dense(num_classes,
                    activation='softmax',
                    kernel_initializer='he_normal')(y)
```

先ほどと同じ、n=3としたResNet20の結果です。
![111_ResNet_Dropoutありn3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225870/27de3d73-80e1-67da-5b1c-8117fdef715e.png)
訓練データに対して95.0%、テストデータに対して**86.1%**の精度がでていて、さきほどよりだいぶ改善されました。

n=8にしたResNet50の結果は以下の通りです。
![121_ResNet_Dropoutありn8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225870/17ab4280-4115-a7be-18ee-bb32e41ad08a.png)
訓練データに対して99.2%、テストデータに対して**87.4%**と、さらに改善されました。

ただし、複雑にしたCNNには劣っていますね。ハイパーパラメータのチューニングを時間かけてやれば精度はさらに上がっていくのでしょうが、ちょっと試したくらいでは従来のCNN以上の性能を実感することはできませんでした。

#まとめ
CIFAR-10に対して、簡単なCNN、複雑なCNN、元の論文通りのDropoutを入れないResNet、過学習を避けるためDropoutを入れたResNetを試してみました。
今回私が試した範囲では、ResNetの優位性を示すことはできませんでした。CIFAR-10のようは比較的簡単なデータセットだと、ResNetのようなアーキテクチャは必要ないということなのかもしれませんね。DataAugmentationをうまく行う方が、単純に精度だけを目指すだけなら効果がありそうです。
私のハイパーパラメータチューニングの能力が足りず、短い時間では示せなかっただけかもしれませんが。もしCIFAR-10でも、DataAugmentationを使わないでResNetの優位性を示す方法があれば、ぜひご教示いただきたいです。