Batch Normalizationの効果を畳み込みニューラルネットワークで検証

Batch Normalization（BatchNorm）の効果を畳み込みニューラルネットワーク（CNN）で検証します。BatchNormがすごいとは言われているものの、具体的にどの程度精度が上昇するのか、あるいはどの程度計算速度とのトレードオフがあるのか知りたかったので実験してみました。

今回は隠れ層の数とBatch Normの有無、Batch Normの頻度ごとに誤差・精度を検証します。

## Batch Normの簡単な理論
ニューラルネットワークの入力層（訓練データ）に対して標準化をすると訓練速度が向上しますが、これを隠れ層に対しても標準化するのがBatch Normです。

```math
\begin{align}
\mu&=\frac{1}{m}\sum_i z^{(i)} \\
\sigma^2 &= \frac{1}{m}\sum_i (z^{(i)}-\mu)^2 \\
z_{\rm norm}^{(i)} &= \frac{z^{(i)}-\mu}{\sqrt{\sigma^2+\epsilon}} \\
\tilde{z}^{(i)} &= \gamma z_{\rm norm}^{(i)}+\beta
\end{align}
```

zがミニバッチなり訓練データに対する、活性化関数適用前の隠れ層の値とします。ここでの平均、分散を計算して、平均0、分散1になるようにシフトします。3つ目の式で微小な値εを足しているのは、0で割るのを避けるための技術的な理由です。βやγは学習からコントロールできますが、それぞれ平均、標準偏差に相当します。

## 実験
CPUで訓練できそうな軽いデータにしたかったので[Fashion-MNIST](https://github.com/zalandoresearch/fashion-mnist/blob/master/README.ja.md)にしました。MNISTも食傷気味なので。MNISTよりも若干難しいデータセットですが、CIFAR-10といったカラーのデータセットよりは全然簡単です。

* Fashion-MNISTを使った基本的な多クラス分類
* 畳み込み層だけの簡単なCNN（プーリングは使わない）。活性化関数は全てReLU
* 隠れ層を1～10に変えて、BatchNorm有無で、訓練誤差、訓練精度、テスト誤差、テスト精度、実行時間を比較（全20ケース）
* Conv2dの設定：フィルター数は層の番号×2で定義。例えば1番目の隠れ層ではフィルター数が2、2番目の隠れ層ではフィルター数は4……。カーネルサイズは全ての層で3とし、パディングはなし。したがって、1層ごとに縦横のサイズが2ずつ減って、チャンネル数が2ずつ増える。(26, 26, 2)→(24, 24, 4)→(22, 22, 6)→……
* 全結合以降は即softmaxを食わせて出力層とする
* BatchNormを入れる場合は、Conv2d→BatchNorm→Activation→Conv2d…の順で入れる
* 訓練は全てCPU
* どのモデルも20epoch訓練させる
* ドロップアウトは使わない
* オプティマイザはAdam、誤差関数はcategorical_crossentropy
* ハイパーパラメータは全てKerasのデフォルト設定。学習率も変えない。ミニバッチサイズは全て64とする。
* 訓練・テストの分割はKeras.datasets.fashion_mnistから読める分割

## 実験コード
前処理は255で割って0～1のスケールにしただけです。Fashion-MNISTの訓練データが(60000, 28, 28)の次元なのでCNNに食わせるために(60000, 28, 28, 1)に変形させています。

コード全体：https://gist.github.com/koshian2/de97a35e4e4f4860c86f90d0620fc76a

```python
import numpy as np
import matplotlib.pyplot as plt
import pickle
import time
from keras.datasets import fashion_mnist
from keras.models import Model
from keras.layers import Input, Activation, Conv2D, BatchNormalization, Flatten, Dense
from keras.optimizers import Adam
from keras.utils import to_categorical

(X_train, y_train), (X_test, y_test) = fashion_mnist.load_data()
# 標準化
X_train, X_test = X_train / 255.0, X_test / 255.0
# ランク4にする
X_train, X_test = X_train[:, :, :, np.newaxis], X_test[:, :, :, np.newaxis]
# One-hotベクトル化
y_train, y_test = to_categorical(y_train), to_categorical(y_test)
```

モデルを作る根幹部分はこちら。use_bnのフラグを入れたときのみBatchNormを入れています。簡単に動的なモデルを作れるKerasすごいですね。

```python
# モデルの作成
def create_model(nb_layers, use_bn, bn_freq=1):
    # 入力層
    input = Input(shape=(28, 28, 1))
    x = input
    # 隠れ層
    for i in range(nb_layers):
        x = Conv2D(filters=2*(i+1), kernel_size=(3, 3))(x)
        if use_bn and ((i+1)%bn_freq==0):
            x = BatchNormalization()(x)
        x = Activation("relu")(x)
    # FC
    x = Flatten()(x)
    # 出力層
    y = Dense(10, activation="softmax")(x)
    # モデル
    model = Model(inputs=input, outputs=y)
    return model
```

bn_freqでBatchNormを入れる間隔を指定できますが、ここでは1（全ての層に対して入れる）ものとします。後でこの値を変更して別に実験します。次に実験本体部分です。隠れ層1＋BNなし→隠れ層1＋BNあり→隠れ層2＋BNなし→隠れ層2＋BNあり→…の順でテストしていきます。

```python
# 定数
nb_epoch = 20
# 結果の保存用
result = []
times = []
# レイヤー数10、BN有りなしの計20
for i in range(20):
    nb_layers = int(i/2) + 1
    use_bn = i % 2 == 1        
    print(f"★★ i = {i}, #layers = {nb_layers}, bn = {use_bn} ★★")
    # モデルの取得
    model = create_model(nb_layers, use_bn)
    # コンパイル
    model.compile(optimizer=Adam(), loss="categorical_crossentropy", metrics=["accuracy"])
    # フィット
    start_time = time.time()
    history = model.fit(X_train, y_train, batch_size=64,
                        epochs=nb_epoch, validation_data=(X_test, y_test)).history
    elapsed = time.time() - start_time
    # 結果に追加
    result.append(history)
    times.append(elapsed)
# 保存
with open("result.dat", "wb") as fp:
    pickle.dump(result, fp)
with open("times.dat", "wb") as fp:
    pickle.dump(times, fp)
```

Pickleでhistoryをファイルに保存して終わりです。


## 結果
###処理時間
BatchNorm有無での訓練時間の推移は次のようになります。
![times.png](https://qiita-image-store.s3.amazonaws.com/0/232088/2d90f3ff-8fb1-9801-e537-5b87db27addd.png)
これは全ての層でBatchNormを入れた場合です。GPUで訓練したり、データがカラーになるとまた変わるかもしれませんが、CPUでFashion-MNISTを訓練した限りでは、**BatchNormが入るとほぼ処理時間は2.5倍～3倍**になりました。

### 誤差、精度
BatchNorm有無での訓練/テストの誤差/精度は次のようになります。左上が訓練誤差、右上が訓練精度、左下がテスト誤差、右下がテスト精度です。横軸はepoch数、縦軸が評価尺度です。
![plot1.gif](https://qiita-image-store.s3.amazonaws.com/0/232088/4ac768f5-bc7e-b4e8-686e-15dc2d6c0299.gif)

テストの場合はまちまちですが、**訓練では必ずBatchNorm入れたほうが精度が良くなっている**（誤差が小さくなっている）のが確認できます。BatchNormの有無に関わらず訓練誤差を下げても、テスト誤差は同じように下がる保証はありません（オーバーフィットしているかどうかの問題なので）。隠れ層の数が大きくなると精度が下がっていますが、おそらく20epoch程度では足りなかったのだと思います。もう少し長く訓練させたらちゃんと精度が上がるはずです。

必ず訓練誤差が下がるのはすごいことです。ニューラルネットワークの収束を速くするテクニックは数多くあれど、このようにほぼ必ず効果があるテクニックはかなり限られます。

ただ問題は時間とのトレードオフでしょう。計算資源が潤沢にあり、「BatchNormの計算時間ぐらい気にならないや」というのであれば全部入れてしまえばいいのですが、先程見たように全部入れると2.5倍～3倍になります。例えば隠れ層が2のときに、BatchNormありで5epochのときの精度≒BatchNormなしの10～15epochのときの精度なので、計算量が3倍程度になれど、BatchNormを入れるのはかなり合理的なように考えられます。

もちろん**BatchNormを入れる頻度はハイパーパラメータ**なので、データやモデル、計算環境に合わせて調整するといいと思います。次はBatchNormの頻度を変えて実験してみます。

## 追加実験
* 隠れ層の数は6とする（そこそこに数が大きくてBatchNormの有効性の差が出たのがここだったので）。
* BatchNormの頻度を1～7に変更して同様に見る。1は全ての隠れ層に対して、7は一切BatchNormを入れない場合。
* その他の条件は前の実験と一緒

## 実装
一部だけ。全体は[こちら](https://gist.github.com/koshian2/d658d9ae18600aaef2ea1891e23c047b)を見てください。

```python
# レイヤー数6、BN有りの計7
for i in range(7):
    print(f"★★ i = {i}★★")
    # モデルの取得
    model = create_model(6, True, i+1)
    # コンパイル
    model.compile(optimizer=Adam(), loss="categorical_crossentropy", metrics=["accuracy"])
    # フィット
    start_time = time.time()
    history = model.fit(X_train, y_train, batch_size=64,
                        epochs=nb_epoch, validation_data=(X_test, y_test)).history
    elapsed = time.time() - start_time
    # 結果に追加
    result.append(history)
    times.append(elapsed)
```

## 結果
### 時間
bn_freqを横軸に、時間（分）を縦軸にプロットしました。bn_freq=1では全てのレイヤーでBNあり、bn_freq=2では2層ごと、……、bn_freq=7ではBNなしとなります。この場合、BatchNormの頻度と処理時間は直線的には変化しませんでした。なぜなら**bn_freq=1では6回、bn_freq=2では3回、bn_freq=3では2回、bn_freq=4では1回入れていてあとは入れる層が異なるだけだから**です。BatchNormの回数で見ると、処理時間は直線的な関係がありそうです。
![times2.png](https://qiita-image-store.s3.amazonaws.com/0/232088/dd5a0d81-d1df-69cd-3057-0e5550fc09fc.png)
この例だと、BNありで処理時間1.4倍ぐらいですんでいるbn_freq=4あたりがコストパフォーマンス良さそうですね。もちろんこれは隠れ層の数や、学習率、データ、CPUやGPUかによって変わるので絶対ではありません。

### 誤差、精度
1回でもBatchNorm入れれば結構上がりますね。bn_freq=2、4あたりがいい感じです。全てのレイヤーに対してBatchNormを入れる必要はこの場合はなさそうです。
![plot2.gif](https://qiita-image-store.s3.amazonaws.com/0/232088/8dbb1c41-9393-06d4-3103-dffe08ea5e1d.gif)


## まとめ
**BatchNorm強い**。Fashion-MNIST＋Adamだと訓練精度基準で、epoch数でせいぜい2倍程度の加速でしたが、CIFAR-10＋SGDだともっと（5倍や10倍）[加速した](https://deepage.net/deep_learning/2016/10/26/batch_normalization.html#cifar-10%E3%81%AE%E7%94%BB%E5%83%8F%E5%88%86%E9%A1%9E%E3%81%A7%E6%A4%9C%E8%A8%BC)という例もあります。

## 参考記事
* [Batch Normalization の理解](https://qiita.com/t-tkd3a/items/14950dbf55f7a3095600)
* [多層ニューラルネットでBatch Normalizationの検証](https://qiita.com/satopirka/items/4f6e3702947f7ef8f1bf)
* [Batch Normalizationによる収束性能向上](https://qiita.com/sergeant-wizard/items/052c98c6e712a4a8df6a)
* [シンプルなNNで 学習失敗時の挙動と Batch Normalization の効果を見る](https://qiita.com/t-tkd3a/items/fc0c9f7923847a44962c)
* [Deep LearningにおけるBatch Normalizationの理解メモと、実際にその効果を見てみる](https://qiita.com/cfiken/items/b477c7878828ebdb0387)
* [[Survey]Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift](https://qiita.com/supersaiakujin/items/8a465ecb1dcbc7df8b02)