ニューラルネットワークにおける学習機能を機械学習のライブラリを用いずに丁寧に理解しようとした（後半）

#はじめに

　ニューラルネットワーク（深層学習）における機能に学習があります。より予測モデルの予測率を上げるために行われているモデル内の計算を一から理解することを試みました。機械学習のライブラリを用いずに実装しました。

　前回の記事では、ニューラルネットワークにおける学習の意味合いやモデルの精度を上げるために必要な損失関数、微分の考え方をまとめました。
https://qiita.com/Fumio-eisan/items/c4b5b7da5b5976d09504
　
　今回は、後半としてニューラルネットワークへ実装していくところまでまとめたいと思います。

　今回も、オライリーのディープラーニング教科書を参考にさせて頂きました。非常にわかりやすく理解できます。
https://www.oreilly.co.jp/books/9784873117584/

　概要は以下です。


+ ニューラルネットワークにおける学習とは何か（前半）
+ 損失関数とは何か（前半）
+ 関数の微分を実装する（前半）
+ **勾配降下法について（後半：今回）**
+ **ニューラルネットワークにおける勾配（後半：今回）**
+ **学習アルゴリズムを実装してみる（後半：今回）**


##勾配降下法について

　前回の記事では、モデルの最適化のためには損失関数を最小化させることが必要であることを確認しました。また、その最小化のためには関数の微分がその手段であることを示してきました。
　それでは、実際にこの関数の微分を使ってモデルのパラメータを最適化させていくことを考えます。

　関数の微分により、その関数の値が減る方向を知ることができます。勾配法とは、現在の場所から勾配方向に一定の距離だけ進めます。そして移動した先でも同様の勾配を求め、またその勾配方向に進めることを言います。
　**最小値へ向かっていくことを勾配降下法、最大値へ向かっていくことを勾配上昇法と呼びます。**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/567823/494fdf72-4ff4-b1f2-f2a1-4a4ff1679e63.png)

　勾配法を数式で表すと上のようになります。$η$は更新の量を表し、**学習率**と呼ばれます。一回の学習でどれだけパラメータを更新するかを表します。
　この学習率が小さすぎると最小値へ近づくための時間がかかります。逆に、学習率が大きいと最小値を飛び越えてしまう可能性があります。従って、モデルごとに丁度良い値を見つける必要があります。
　
　実際に実装してみます。関数は前半でも使用したこちらになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/567823/b79649fa-fc2e-39ce-9281-14e0b26cfd22.png)

　この関数の最小値を求めてみたいと思います。

```python:nn.ipynb
def gradient_descent(f,init_x, lr=0.01, step_num=100):
    x = init_x
    
    for i in range(step_num):
        grad = numerical_gradient(f,x)
        x -= lr*grad
    
    return x

def function_2(x):
    return x[0]**2+x[1]**2

```

さて、初期値を$(x_0,x_1) =(-3,4)$として勾配法を使って最小値を求めます。真の最小値は$(0,0)$の時にとります。

```python:nn.ipynb
init_x = np.array([-3.0,4.0])
gradient_descent(function_2, init_x = init_x, lr =0.1, step_num=100)
```

```
array([-6.11110793e-10,  8.14814391e-10])
```

学習率$lr$が$0.1$の時は上記のような結果となりほとんど$(0,0)$に近い値を示すことが分かりました。この場合は、学習が上手くいったといっていいでしょう。


```python:nn.ipynb
init_x = np.array([-3.0,4.0])
gradient_descent(function_2, init_x = init_x, lr =10, step_num=100)
```

```
array([-2.58983747e+13, -1.29524862e+12])
```

次に、学習率を$10$にした場合がこちらです。値が発散してしまっています。これでは良い学習になっていないことが分かります。モデルごとに最適な学習率を設定しなければならないことがこの検討から分かります。



##ニューラルネットワークにおける勾配

 先ほどの勾配を求める方法をニューラルネットワークへ応用します。ニューラルネットワークでは、損失関数の勾配へ応用します。損失関数をＬとして、重み$w$で偏微分させる構造を取ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/567823/84c56a8a-2979-a6aa-7617-fc0034f334c8.png)




##ニューラルネットワークを実装する
　下記手順を行うニューラルネットワークを実装していきたいと思います。

1. ミニバッチ
　訓練データの中から一部ランダムでデータを抽出します（ミニバッチ）。そのミニバッチにおける損失関数を最小化させることが目的です。

2. 勾配の算出
　ミニバッチの損失関数を減らすために、重みパラメータの勾配を求めます。

3. パラメータの更新
　重みパラメータをマイナスの勾配方向に微小量更新します。

4. 繰り返す
　$1.～3.$を繰り返し回数だけ実施します。



###プログラムの構成（モジュールの読み込み等）

　さて、実際に学習機能がある２層のニューラルネットワークを実装していきたいと思います。
　今回実装したモデルのコンポーネント図は下記になります。
　
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/567823/e507c429-47c2-c8d7-890c-717e6c84c232.png)

　主にパラメータを設定するときは、nn.ipynbで決めます。また、今回はMNISTのデータセットを用いますので、元のYann氏らのURLから読み込んでくるようになっています。
　実際のニューラルネットワークで行う計算はtwo_layer_net.py上で記述しています。下記の図のようなネットワークになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/567823/33adc168-62e0-25f4-39dd-a4edc1b94a27.png)


　MNISTは元々28×28ピクセルの画像データですので、28×28 = 784次元の数値が最初の入力層に入ります。今回は、隠れ層を１００とし、さらに最後の出力層は数字の１０種類として吐き出すために１０次元としています。
　この計算を行う上で、活性化関数であるシグモイド関数や最後の確率を出すためのソフトマックス関数はさらに別のfunctions.py上に記述してある関数を読んできて計算をします。
　そこで得られた出力値と教師データの正解インデックスを損失関数へ代入します。そして、重みパラメータの勾配を求めるためにgradient.py上に記述しているnumerical_garadientメソッドで計算します。
　得られた勾配を次の重みパラメータへ更新するため、元のnn.ipynb上で更新するように記述しています。
　その一連の動作を繰り返し回数分行います。

　２層のニューラルネットワークの計算と学習を行うだけでも、これだけのメソッドを読み込んで計算をする必要があります。とても人で出来る量ではないことが分かります。また、クラスやメソッド含めてプログラムの構造理解が必要になることが分かります。


ここで、重みパラメータとバイアスの更新部分を見てみます。


```python:nn.ipynb
    # 重みパラメータ、バイアスの更新
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[key]　#この符号がマイナスであることがポイントです
    
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)
    
```

重みパラメータ、バイアスであるW1,W2,b1,b2に対してそれぞれ勾配（grad[key]）と学習率を掛けた値を**引いている**ことがポイントになります。
　微分して得られる勾配が正の値であるとき、負の方向に動けば最小値に向かっていくことから引いている意味合いになります。
　試しにこの符号を逆にしてプラスにするとどうなるでしょうか。
　


![006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/567823/6460d662-6e55-7393-1ee6-8a3a4143a861.png)

　横軸が計算回数、縦軸が損失関数の値です。すぐに値が上昇してしまっていることが分かります。
符号をマイナスで記述するとこうなります。

![007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/567823/9558fc73-0151-b4b6-2063-99d754707798.png)

　きちんと値が下がっていくことが分かりますね。これで２層のニューラルネットワークを機械学習の既存のライブラリを用いずに構築することができました。

#終わりに

　今回、機械学習のライブラリを用いずにニューラルネットワークの計算と学習をモデル化しました。
　モデルの学習においては、損失関数の考え方及び勾配（＝関数の微分）がポイントであることを理解しました。また、クラス・モジュールを上手く組み合わせて計算を行う必要があり、Python自体の勉強にもなりました。
　

プログラム全文はこちらに格納しています。
https://github.com/Fumio-eisan/nn2layer_20200321