深層学習入門 ~学習則編~

# 対象者
前回の記事は[こちら](https://qiita.com/kuroitu/items/ea6ed8f614e65ec44976)です。
本記事では学習則の実装を見ていきます。と言っても[ここ](https://qiita.com/kuroitu/items/36a58b37690d570dc618)で既に本体は実装していますので併せてご覧ください。

次回の記事は[こちら](https://qiita.com/kuroitu/items/a6725ac0139d8eeb1e19)

# 目次
- [学習則](#学習則)
    - [学習則理論](#学習則理論)
    - [学習則実装](#学習則実装)
- [`__init__`メソッドの実装](#__init__メソッドの実装)
- [おわりに](#おわりに)


# 学習則
まずはいつも通りスカラで考えていきます。
ニューロンオブジェクトは変数$w, b$を持ちますね。

```math
y = \sigma(xw + b)
```

ここで、入力$x$を定数だとみなすと

```math
y = f(w, b) = \sigma(xw + b)
```

のように書くことができます。
つまり$w, b$の値を適切に変えて目的とする値$y^{\star}$に近づける、というのが学習則の目標になります。

## 学習則理論
では理論的に見ていきましょう。

```math
y = f(w, b) = \sigma(wx + b)
```

において

```math
\begin{align}
  y &= y^{\star} = 0.5 \\
  x &= x_0 = 1
\end{align}
```

とし、活性化関数をsigmoid関数にして$w, b$のパラメータ空間を図示すると以下のようになります。
![Loss_space.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/53a71469-df8e-1339-1d4f-cc5ca7b91c2f.png)
<details><summary>コード</summary><div>

```show_loss_space.py
%matplotlib nbagg
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D


x_0 = 1
y_star = 0.5
sigma = lambda x: 1/(1+np.exp(-x))

w = np.arange(-2, 2, 1e-2)
b = np.arange(-2, 2, 1e-2)
W, B = np.meshgrid(w, b)
y = 0.5*(sigma(x_0*W + B) - y_star)**2

elevation = np.arange(np.min(y), np.max(y), 1/2**8)

fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
ax.set_xlabel("w")
ax.set_ylabel("b")
ax.set_zlabel("loss")
ax.view_init(60)
ax.grid()
ax.plot_surface(W, B, y, cmap="autumn", alpha=0.8)
ax.contour(W, B, y, cmap="autumn", levels=elevation, alpha=0.8)
fig.suptitle("Loss space")
fig.show()
fig.savefig("Loss_space.png")
```
</div></details>

図は損失関数に二乗誤差を用いた場合の損失空間を示しています。
ランダムな初期値$w_0, b_0$から最適値$y^{\star}$を与える$w^{\star}, b^{\star}$へと漸近していくのが学習則の目的ですね。
この時学習則として使われるのが**最急降下法**から発展した**勾配降下法**です。
勾配降下法とは、各パラメータのある地点での**勾配(偏微分)を利用して坂を下る手法**のことを言います。
[ここ](https://qiita.com/kuroitu/items/36a58b37690d570dc618)で各手法について数式とコードを載せています。
また、[ここ](https://qiita.com/kuroitu/items/6695e0c79e888543e150)にいくつかの探索空間での降下の様子も載せています。
<img src=https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F640911%2F6f55cfbc-4a9f-d4fe-c70e-49dca7cbf683.gif?ixlib=rb-1.2.2&auto=format&gif-q=60&q=75&s=02b37020417dbead312cc8c82f5eac7e>

本記事では最もシンプルなSGDについて取りあげます。
SGDの数式は以下の通りです。

```math
\begin{align}
  g_t &= \nabla_{w_t}\mathcal{L}(w_t, b_t) \\
  \Delta w_t &= - \eta g_t \\
  w_{t+1} &= w_t + \Delta w_t
\end{align}
```

この数式では$w$についてのみしか載せていませんが、$b$と置き換えればバイアスへの学習則も同様になることがわかります。
$\mathcal{L}(w_t, b_t)$は損失関数で、$\nabla_{w_t}$は$w_t$に関する偏微分を表すと考えてください(上記の数式は行列表現になっています)。
上式を日本語で表すと

- 偏微分で勾配を求める
- 移動量を求める
- 移動する

のようになります。シンプルですね。詳しく見ていきましょう。

最初の「偏微分で勾配を求める」ところは[逆伝播](https://qiita.com/kuroitu/items/ea6ed8f614e65ec44976)で紹介した誤差逆伝播法を用います。これはそれでいいでしょう。「移動する」も文字通りです。

「移動量を求める」部分についてですが、

1. なぜマイナスをつけるのか
2. なぜ勾配をそのまま用いずに$\eta \ll 1$を乗算するのか

の2点についてお話しします。

まず、1.についてですが、これは実際に具体的に考えるとわかりやすいかと思います。
![y=x^2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/d0c4b328-0de0-ce84-2b31-71d7dcd5c436.png)
例えば$(x, y) = (1, 1)$の地点での傾きは$2$になりますが、移動して欲しい方向はマイナス方向ですよね。もちろん逆も然りです。ということで、移動して欲しい方向と勾配は常に逆符号となるためにマイナスをつけています。
2.についても、やはりグラフを見ればわかると思いますが、傾き$2$をそのまま用いて$\Delta x = -2$などとしてしまうと$x=-1$となり、最適値を通り越してしまいます。
![y=x^2_move.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/b47cc3ed-ee72-729b-fd5c-24a0a2c7ec14.png)
そのため、学習率と呼ばれる係数$\eta \ll 1$を乗算して移動量を制限し、最適値に向かって徐々に降っていくようにします。
この学習率は**ハイパーパラメータ**と呼ばれる値で、この部分を人間が設計しなければならない学習則が多いです。
大抵の場合は論文などで与えられるデフォルト値を用いれば上手く動作しますが、解きたい問題によっては色々と試してやる必要があったります。

## 学習則実装
まあ詳しいことはさておき、とりあえず実装しましょう。実装先はいつもの通り[baselayer.py](https://qiita.com/kuroitu/items/884c62c48c2daa3def08#%E3%83%AC%E3%82%A4%E3%83%A4%E3%83%BC%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%81%AE%E3%82%B3%E3%83%BC%E3%83%89%E6%BA%96%E5%82%99)です。
<details><summary>baselayer.py</summary><div>

```baselayer.py
    def update(self, **kwds):
        """
        パラメータ学習の実装
        """
        dw, db = self.opt.update(self.grad_w, self.grad_b, **kwds)

        self.w += dw
        self.b += db
```
</div></details>

`self.opt.update(self.grad_w, self.grad_b, **kwds)`の部分は[こちら](https://qiita.com/kuroitu/items/36a58b37690d570dc618)に丸投げしています。例としてSGDのコードを掲載しておきます。
<details><summary>optimizers.py</summary><div>

```optimziers.py
import numpy as np


class Optimizer():
    """
    最適化手法が継承するスーパークラス。
    """
    def __init__(self, *args, **kwds):
        pass


    def update(self, *args, **kwds):
        pass


class SGD(Optimizer):
    def __init__(self, eta=1e-2, *args, **kwds):
        super().__init__(*args, **kwds)

        self.eta = eta


    def update(self, grad_w, grad_b, *args, **kwds):
        dw = -self.eta*grad_w
        db = -self.eta*grad_b
        return dw, db
```
</div></details>

コードの内容は上で紹介した数式通りですね。
外から$w, b$に関する勾配を受け取り、学習則の通り$-\eta$倍して移動量を決定し投げ返しています。
この移動量をレイヤーオブジェクトが受け取り、自身のパラメータを更新します。

さて、今回の内容は以上です。「あれ？行列の場合は？」と思う方ももしかしたらいるかもしれませんね。
実は行列の場合も全く同様のコードになります。[optimizers.py](https://qiita.com/kuroitu/items/36a58b37690d570dc618#%E5%AE%9F%E8%A3%85%E3%82%B3%E3%83%BC%E3%83%89%E4%BE%8B)を見てみても行列積の演算などは出てきていません。なぜかというと、考えてみれば当然なのですが、例えミニバッチで学習するとしても流れてくる勾配は各パラメータごとに固有のものであるはずで、他のパラメータの勾配を絡めて演算する必然性がないためです。
ということで今回はスカラで考えて実装したら行列でも同じように計算できます。


# `__init__`メソッドの実装
では最後に`__init__`メソッドで最適化子`opt`をレイヤーオブジェクトに持たせるようにしておきましょう。

```__init__.py
    def __init__(self, *, prev=1, n=1,
                 name="", wb_width=5e-2,
                 act="ReLU", opt="Adam",
                 act_dic={}, opt_dic={}, **kwds):
        self.prev = prev  # 一つ前の層の出力数 = この層への入力数
        self.n = n        # この層の出力数 = 次の層への入力数
        self.name = name  # この層の名前

        # 重みとバイアスを設定
        self.w = wb_width*np.random.randn(prev, n)
        self.b = wb_width*np.random.randn(n)

        # 活性化関数(クラス)を取得
        self.act = get_act(act, **act_dic)

        # 最適化子(クラス)を取得
        self.opt = get_opt(opt, **opt_dic)
```


# おわりに
次回は活性化関数と最適化子のローカライズ、それから損失関数の紹介ですかね〜


# 深層学習シリーズ
- [深層学習入門 ~基礎編~](https://qiita.com/kuroitu/items/221e8c477ffdd0774b6b)
- [深層学習入門 ~コーディング準備編~](https://qiita.com/kuroitu/items/884c62c48c2daa3def08)
- [深層学習入門 ~順伝播編~](https://qiita.com/kuroitu/items/d22c8750e34d5d75fb6c)
- [深層学習入門 ~逆伝播編~](https://qiita.com/kuroitu/items/ea6ed8f614e65ec44976)
- [深層学習入門 ~学習則編~](https://qiita.com/kuroitu/items/ab5ad4ac716ae7a04891)
- [深層学習入門 ~ローカライズと損失関数編~](https://qiita.com/kuroitu/items/a6725ac0139d8eeb1e19)
- [深層学習入門 ~関数近似編~](https://qiita.com/kuroitu/items/4a8badcf7d3139d9ec7b)
- [深層学習入門 ~畳み込みとプーリング編~](https://qiita.com/kuroitu/items/48bf6bc42e4dfee52cde)
- [深層学習入門 ~CNN実験編~](https://qiita.com/kuroitu/items/d4716e754594a29b059d)
- [深層学習外伝 ~GPUプログラミング編~](https://qiita.com/kuroitu/items/2618c598f46d6ab3889c)
- [深層学習入門 ~ドロップアウト編~](https://qiita.com/kuroitu/items/d1bed8c216950d87be74)
- [活性化関数一覧 (2020)](https://qiita.com/kuroitu/items/73cd401afd463a78115a)
- [勾配降下法一覧 (2020)](https://qiita.com/kuroitu/items/36a58b37690d570dc618)
- [見てわかる！最適化手法の比較 (2020)](https://qiita.com/kuroitu/items/6695e0c79e888543e150)
- [im2col徹底理解](https://qiita.com/kuroitu/items/35d7b5a4bde470f69570)
- [col2im徹底理解](https://qiita.com/kuroitu/items/7877c002e192955c7857)
- [numpy.pad関数完全理解](https://qiita.com/kuroitu/items/51f4c867c8a44de739ec)