ラビットチャレンジ - 深層学習 Day2 Section1 勾配消失問題

# 0．概要
本記事は日本ディープラーニング協会認定の講座プログラムである「ラビット・チャレンジ」が提供している科目の1つである深層学習のレポートである。
記事タイトルに記載のとおり、Day2 Section1 勾配消失問題について以下にまとめる。

# 1．勾配消失問題
誤差逆伝播法が下位層に進んでいくにつれて勾配がどんどん緩やかになっていくことで下位層のパラメータがほとんど変わらなくなるため全体最適解に収束しなくなること。
グラフ等で可視化することで勾配消失を起こしているか確認する。

シグモイド関数の場合、微分の最大値は0.25となるため下位層に進むにつれて0.25の2乗、更に2乗となっていくため0に近づいていく。

# 2．勾配消失問題の解決方法

## 2.1．活性化関数の選択

### 2.1.1．ReLU関数(ランプ関数、正規化線形関数)
0～1の範囲の値を出力するシグモイド関数と違い、入力値が閾値(0)を超えている場合、入力値をそのまま出力するため勾配消失が起こりにくい。

## 2.2．重みの初期値設定

### 2.2.1．Xavier
各重みに対して前の層のノード数(ユニット数)の平方根で除算した値を初期値とするアルゴリズム。

Xavierで初期値を設定する際に合わせて用いられる活性化関数がReLU関数、シグモイド関数、双曲線正接関数(tanh)。

```Python:
self.params['W1'] = np.random.randn(input_size, hidden_size) / nq.sqrt(input_layer_size)
self.params['W2'] = np.random.randn(hidden_size, output_size) / nq.sqrt(hidden_layer_size)
```

### 2.2.2．He
各重みに対して前の層のノード数(ユニット数)の平方根で除算した値に$\sqrt{2}$を掛けた値を初期値とするアルゴリズム。

Heで初期値を設定する際に合わせて用いられる活性化関数がReLU関数。

```Python
self.params['W1'] = np.random.randn(input_size, hidden_size) / nq.sqrt(input_layer_size) * np.sqrt(2)
self.params['W2'] = np.random.randn(hidden_size, output_size) / nq.sqrt(hidden_layer_size) * np.sqrt(2)
```

## 2.3．バッチ正規化
ミニバッチ単位で入力値のデータの偏りを抑制する手法。

活性化関数に値を渡す前後にバッチ正規化の処理を含む層を加えると良い(入力値に重みをかけてバイアスを加えた部分または活性化関数の出力値)。

ミニバッチ$t$全体の平均

```math
\mu_{t}=\frac{1}{N}\sum_{i=1}^{N_{t}}{x_{ni}}
```

ミニバッチ$t$全体の標準偏差

```math
\sigma_{t}^{2}=\frac{1}{N}\sum_{i=1}^{N_{t}}{(x_{ni}-\mu_{t})}^2
```

0に値を近づける正規化の主たる部分

```math
\hat{x}_{ni}=\frac{x_{ni}-\mu_{t}}{\sqrt{\sigma_{t}^{2}+\theta}}
```

ミニバッチのインデックス値とスケーリングの席にシフトパラメータを加算した値

```math
y_{ni}=\gamma x_{ni}+\beta
```

逆伝播において、正規化するための最適な値へ$\gamma, \beta$を学習していく。

$N_{t}$：ミニバッチのインデックス
$\gamma$：スケーリングパラメータ(ハイパーパラメータ)
$\beta$：シフトパラメータ(ハイパーパラメータ)

# 3．確認テスト

## 3.1．確認テスト1
シグモイド関数を微分した時、入力値が0の時に最大値を取る。その値として正しいものはなにか。

回答：

シグモイド関数の微分の式は以下となる。
導出については要調査。

```math
f(u)=(1-sigmoid(u))\cdot sigmoid(u)
```

シグモイド関数の入力が0の時、出力は0.5となるため、代入すると$0.5\times0.5=0.25$となる。

## 3.2．確認テスト2
重みの初期値に0を設定するとどのような問題が発生するか。

回答：
全てのパラメータが同じ値で伝搬していくためチューニング(値の最適化)がされなくなるため学習が進まなくなる。

## 3.3．確認テスト3
バッチ正規化の効果を2点挙げよ。

回答：
・勾配消失問題の抑制
・計算の効率化

# 4．Jupter演習

## 4.1．勾配消失問題
シグモイド関数とReLU関数それぞれで実行した場合の違い。
学習データ60,000件、テストデータ10,000件だが2,000回の繰り返しで都度100件ずつ使っているだけでなので実は全部使っていないし、既に抽出したデータかも見ていないので同じデータ使っている可能性もある。
まあこの演習の目的は使う活性化関数によって勾配消失問題が起きるか抑制できるかなので多少使うデータが重複等していても問題なし。

コメントに追記しているようにMNIST手書き数字文字データセットを使った中間層2層の10クラス分類DNN。

```Python:
import numpy as np
from data.mnist import load_mnist
from PIL import Image
import pickle
from common import functions
import matplotlib.pyplot as plt

# mnistをロード
# MNISTの手書き数字文字データ0～9
# 目的変数はOne-hotエンコーディング
(x_train, d_train), (x_test, d_test) = load_mnist(normalize=True, one_hot_label=True)
train_size = len(x_train)

print("データ読み込み完了")

# 重み初期値補正係数
weight_init = 0.01
# 入力層サイズ
# 入力層は784ユニット
input_layer_size = 784
# 中間層サイズ
# 1層目40ユニット、2層目20ユニット
hidden_layer_1_size = 40
hidden_layer_2_size = 20

#出力層サイズ(0~9の10クラス分類)
output_layer_size = 10
# 繰り返し数
iters_num = 2000
# ミニバッチサイズ
batch_size = 100
# 学習率
learning_rate = 0.1
# 描写頻度
plot_interval=10

# 初期設定
def init_network():
    network = {} 

    # 重みは0で初期化すると学習が進まないため適当な値で初期化するよう乱数を使用
    network['W1'] = weight_init * np.random.randn(input_layer_size, hidden_layer_1_size)
    network['W2'] = weight_init * np.random.randn(hidden_layer_1_size, hidden_layer_2_size)
    network['W3'] = weight_init * np.random.randn(hidden_layer_2_size, output_layer_size)
            
    network['b1'] = np.zeros(hidden_layer_1_size)
    network['b2'] = np.zeros(hidden_layer_2_size)
    network['b3'] = np.zeros(output_layer_size)

    return network

# 順伝播
def forward(network, x):
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']

    # ここで使用する活性化関数を決めている。
    # シグモイド関数では勾配消失が起きるが、ReLU関数なら抑制できる。
    #hidden_f = functions.sigmoid
    hidden_f = functions.relu

    u1 =  np.dot(x, W1) + b1
    z1 = hidden_f(u1)
    u2 =  np.dot(z1, W2) + b2
    z2 = hidden_f(u2)
    u3 =  np.dot(z2, W3) + b3
    y = functions.softmax(u3)
 
    return z1, z2, y

# 誤差逆伝播
def backward(x, d, z1, z2, y):
    grad = {}
    
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']

    # ここで使用する活性化関数を決めている。
    # シグモイド関数では勾配消失が起きるが、ReLU関数なら抑制できる。
    # hidden_d_f = functions.d_sigmoid
    hidden_d_f = functions.d_relu
    
    # 出力層でのデルタ
    delta3 = functions.d_softmax_with_loss(d, y)
    # b3の勾配
    grad['b3'] = np.sum(delta3, axis=0)
    # W3の勾配
    grad['W3'] = np.dot(z2.T, delta3)
    # 2層でのデルタ
    delta2 = np.dot(delta3, W3.T) * hidden_d_f(z2)
    # b2の勾配
    grad['b2'] = np.sum(delta2, axis=0)
    # W2の勾配
    grad['W2'] = np.dot(z1.T, delta2)
    # 1層でのデルタ
    delta1 = np.dot(delta2, W2.T) * hidden_d_f(z1)
    # b1の勾配
    grad['b1'] = np.sum(delta1, axis=0)
    # W1の勾配
    grad['W1'] = np.dot(x.T, delta1)

    return grad

# パラメータの初期化
network = init_network()

accuracies_train = []
accuracies_test = []

# 正答率
def accuracy(x, d):
    z1, z2, y = forward(network, x)
    y = np.argmax(y, axis=1)
    if d.ndim != 1 : d = np.argmax(d, axis=1)
    accuracy = np.sum(y == d) / float(x.shape[0])
    return accuracy

for i in range(iters_num):
    # ランダムにバッチを取得
    # 100件ずつのミニバッチでランダムに抽出
    batch_mask = np.random.choice(train_size, batch_size)
    # ミニバッチに対応する教師訓練画像データを取得    
    x_batch = x_train[batch_mask]
    # ミニバッチに対応する訓練正解ラベルデータを取得する
    d_batch = d_train[batch_mask]

    z1, z2, y = forward(network, x_batch)
    grad = backward(x_batch, d_batch, z1, z2, y)

    if (i+1)%plot_interval==0:
        accr_test = accuracy(x_test, d_test)
        accuracies_test.append(accr_test)
        
        accr_train = accuracy(x_batch, d_batch)
        accuracies_train.append(accr_train)

        print('Generation: ' + str(i+1) + '. 正答率(トレーニング) = ' + str(accr_train))
        print('                : ' + str(i+1) + '. 正答率(テスト) = ' + str(accr_test))
        
    # パラメータに勾配適用
    for key in ('W1', 'W2', 'W3', 'b1', 'b2', 'b3'):
        network[key]  -= learning_rate * grad[key]


lists = range(0, iters_num, plot_interval)
plt.plot(lists, accuracies_train, label="training set")
plt.plot(lists, accuracies_test,  label="test set")
plt.legend(loc="lower right")
plt.title("accuracy")
plt.xlabel("count")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
# グラフの表示
plt.show()
```

正解率は最終結果のみ抜粋。

シグモイド関数

> Generation: 2000. 正答率(トレーニング) = 0.14
> 　　　　　: 2000. 正答率(テスト) = 0.1135

![sigmoid.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389699/bf8c2d2a-49fd-7809-6d25-89dc0d971089.png)

ReLU関数

> Generation: 2000. 正答率(トレーニング) = 0.92
> 　　　　　: 2000. 正答率(テスト) = 0.9183

![relu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389699/7ad85f2d-6a80-a248-df4a-7a55ce5cfd63.png)

シグモイド関数では勾配消失問題を起こしているため一向に正解率が上がっていないが、ReLU関数では抑制できているため正解率が上がっている。

## 4.2．Xavier
上記コードでは活性化関数にシグモイド関数を使うと勾配消失を起こして学習が進まなくなってしまっていたが重みの初期化にXavierを用いた場合どうなるか。
4.1のコードでもやっていたように乱数をまず発生させているが、それに対して前の層のノード数(ユニット数)の平方根で除算する(つまりXavier)。

```Python:
# 変更点
network['W1'] = np.random.randn(input_layer_size, hidden_layer_1_size) / (np.sqrt(input_layer_size))
network['W2'] = np.random.randn(hidden_layer_1_size, hidden_layer_2_size) / (np.sqrt(hidden_layer_1_size))
network['W3'] = np.random.randn(hidden_layer_2_size, output_layer_size) / (np.sqrt(hidden_layer_2_size))
```

> Generation: 2000. 正答率(トレーニング) = 0.81
> 　　　　　: 2000. 正答率(テスト) = 0.8016

![xavier.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389699/fb5e907f-dd59-508f-c0d9-7417f6ca8748.png)

ReLU関数には及ばなかったが勾配消失は抑えられていることが分かる。
シグモイド関数だからダメ、ではなく重みの初期化等の工夫が大切。

## 4.3．He
活性化関数にReLU、重みの初期化にHeを使用した場合、どうなるか。
Xavierに対して$\sqrt{2}$で割って重みの初期化している(つまりHe)。

```Python:
# 変更点
network['W1'] = np.random.randn(input_layer_size, hidden_layer_1_size) / np.sqrt(input_layer_size) * np.sqrt(2)
network['W2'] = np.random.randn(hidden_layer_1_size, hidden_layer_2_size) / np.sqrt(hidden_layer_1_size) * np.sqrt(2)
network['W3'] = np.random.randn(hidden_layer_2_size, output_layer_size) / np.sqrt(hidden_layer_2_size) * np.sqrt(2)
```

> Generation: 2000. 正答率(トレーニング) = 0.96
> 　　　　　: 2000. 正答率(テスト) = 0.9556

![he.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389699/2de7c16c-6055-9e87-d104-caa887829bee.png)

Heを使わずただ乱数で初期化していたときでも90%ほどの正解率が出ていたがさらに上がっており、かつ学習が早い(すぐに正解率が上がっている)

## 4.4．実験
シグモイド関数とHe(ReLU関数向けのものだけど)で90%を目指してみた。

```Python:
# 変更点
#中間層サイズ
hidden_layer_1_size = 80
hidden_layer_2_size = 40
# 学習率
learning_rate = 0.2
```

> Generation: 2000. 正答率(トレーニング) = 0.91
> 　　　　　: 2000. 正答率(テスト) = 0.9079

![test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389699/e68f1ba5-4a15-967f-ba53-778b0000ced0.png)

中間層のユニット数を倍にしてみたが90%には届かなかったので学習が早く進むよう学習率を少し上げてみたら一応90%になった。

# X．ラビットチャレンジとは
ラビットチャレンジとは、日本ディープラーニング協会認定の講座プログラムの1つ。
E資格を受験するためにはこのラビットチャレンジ等、いずれかの講座プログラムを修了しなければならない。

ラビットチャレンジの特徴は「現場で潰しが効くディープラーニング講座」の通学講座録画ビデオを編集した教材を使用した自習スタイルであるという点。
サポートは他の講座より少なく、受け身ではなく自主的に学んでいく姿勢でなければ進められないが、その分、他の講座に比べると安価であり、手が出しやすい。
ある程度知識がある人、自力で頑張るぞというガッツのある人向けではないかと感じる。

![bnr_jdla.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389699/bce66671-a2f5-a7f9-de6f-3f87e15c253b.png)