深層学習入門 ~順伝播編~

# 対象者
[深層学習シリーズ](#深層学習シリーズ)の記事です。
前回の記事は[こちら](https://qiita.com/kuroitu/items/884c62c48c2daa3def08)です。
ここでは順伝播について、まずはスカラでの理論を説明して、それから行列に拡張します。
前回記事で紹介したコードに追加していったり修正していく形となるので、まずは前回記事からコードを取ってきておいてくださいね〜
次回の記事は[こちら](https://qiita.com/kuroitu/items/ea6ed8f614e65ec44976)


# 目次
- [スカラでの順伝播](#スカラでの順伝播)
    - [スカラでの順伝播理論](#スカラでの順伝播理論)
    - [スカラでの順伝播実装](#スカラでの順伝播実装)
- [行列での順伝播](#行列での順伝播)
    - [行列での順伝播理論](#行列での順伝播理論)
    - [行列での順伝播実装](#行列での順伝播実装)
- [`__init__`メソッドの実装](#__init__メソッドの実装)
- [行列演算について](#行列演算について)
    - [行列和](#行列和)
    - [行列の要素積](#行列の要素積)
    - [行列積](#行列積)
    - [転置](#転置)


# スカラでの順伝播
ここでは、スカラ(実数)での順伝播の理論と実装を説明します。といっても、だいたい[基礎編](https://qiita.com/kuroitu/items/221e8c477ffdd0774b6b)で既に述べている通りです。

## スカラでの順伝播理論
まずは理論ですね。
![sinple_neuron_model.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/ed6ee1d2-adec-1878-7365-3494f2c5b0a6.png)
このニューロンモデルから見ていきます。
これを定式化すると$f(x) = \sigma(wx + b)$となることは[ここ](https://qiita.com/kuroitu/items/221e8c477ffdd0774b6b#活性化関数)で述べている通りです。活性化関数$\sigma(•)$を通すことで非線形化し、層を重ねる意味を持たせています。
では、この操作を計算グラフで表すとどのようになるでしょうか？
![neuron_object.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/d6326ae4-44fe-0056-5178-0211bd7a2ef2.png)

こんな感じになります。今までは入力を$x$だけとしており他の要素は略していましたが、計算グラフでは **重み$w$** と **バイアス(閾値)$b$** もきちんと記述し、活性化関数を通し出力する、というように書き換えられます。
この変数たち$x, w, b, y$は諸々ニューロンオブジェクトが持つべき変数になります。
また活性化関数についても、Pythonは関数やクラスをオブジェクトとして変数に格納することが可能なので、ニューロンオブジェクトに持たせると実装がカプセル化できて良さそうですね。
スカラでの順伝播の理論は以上です。シンプルでいいですね〜。

## スカラでの順伝播実装
では実装してみます。実装するコードは[baselayer.py](https://qiita.com/kuroitu/items/884c62c48c2daa3def08#レイヤーモジュールのコード準備)です。
<details><summary>baselayer.py</summary><div>

```baselayer.py
    def forward(self, x):
        """
        順伝播の実装
        """
        # 入力を記憶しておく
        self.x = x.copy()

        # 順伝播
        self.u = self.w * x + self.b
        self.y = self.act.forward(self.u)
        
        return self.y
```
</div></details>

入力を保持しているのは逆伝播の時に必要になるからです。
また活性化関数はまだ実装に触れませんが、上記みたいに使えるように実装しようと考えています。あと持っているメソッドは微分を計算する`backward`です。物によっては`update`メソッドを持たせる必要もあるかもしれません。

順伝播自体は至極簡単ですね。数式通りです。
スカラでの実装は以上となります。続いてベクトル(というより行列)での実装を考えます。


# 行列での順伝播
続いて行列での順伝播を考えます。
行列は線形代数の行列積の概念を知っていないと厳しいので、もしご存知でない方がいらっしゃいましたら[ここ](#行列演算について)で簡単に説明しています。

## 行列での順伝播理論
まずは先ほどのニューロンオブジェクトを2つ重ねたようなレイヤーオブジェクトを考えましょう。
![layer_object.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/90fa9b33-e28a-c0a6-619b-e3df706f1720.png)
図のうまい表現が思いつかなかったのでちょっと解説します。
まず黒の矢印たちは先ほどのニューロンオブジェクトという理解でOKです。他のカラーの矢印たちのことですね。
<font color="DeepSkyBlue">水色の矢印</font>は上側のニューロンから下側のニューロンへとつながるシナプスを表しています。
真ん中の掛け算のノードを通る際に<font color="DeepSkyBlue">水色の重み$w_{1, 2}$</font>を乗算して、下側の足し算のノードへ合流します。
<font color="red">赤色の矢印</font>も同様ですね。
真ん中の掛け算のノードを通る際に<font color="red">赤色の重み$w_{2, 1}$</font>を乗算して、上側の足し算のノードへ合流します。
足し算のノードの入力が3つになってしまっていますが、複数の足し算のノードを用いることで2入力に分解できますので、それを省略したとお考えください。
![tri_add_cal_graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/e72eb848-bf53-63ac-e81c-8011955473f1.png)
それでは、これを数式で追いかけてみましょう。以下では簡単のために$\sigma_i(•)$は恒等関数とします。
まずは書き下しからです。

```math
y_1 = w_{1, 1}x_1 + w_{2, 1}x_2 + b_1 \\
y_2 = w_{1, 2}x_1 + w_{2, 2}x_2 + b_2
```

これ自体は図を見ていただければ明白だと思います。
では、これを行列表現で表しましょう。

```math
\left(
  \begin{array}{c}
    y_1 \\
    y_2
  \end{array}
\right)
=
\left(
  \begin{array}{cc}
    w_{1, 1} & w_{2, 1} \\
    w_{1, 2} & w_{2, 2}
  \end{array}
\right)
\left(
  \begin{array}{c}
    x_1 \\
    x_2
  \end{array}
\right)
+
\left(
  \begin{array}{c}
    b_1 \\
    b_2
  \end{array}
\right)
```

こんな感じになります。行列積を理解していれば等価な式になることがわかると思います。
ところで$w_{i, j}$についてですが、添字に注目してください。
ふつう添字は **$i$行$j$列** のように読みますよね？ですが、上の数式では **$j$行$i$列** のようになっていますね。このままでは理論的にも実装的にも扱いづらいため転置しましょう。

```math
\left(
  \begin{array}{c}
    y_1 \\
    y_2
  \end{array}
\right)
=
\left(
  \begin{array}{cc}
    w_{1, 1} & w_{1, 2} \\
    w_{2, 1} & w_{2, 2}
  \end{array}
\right)^{\top}
\left(
  \begin{array}{c}
    x_1 \\
    x_2
  \end{array}
\right)
+
\left(
  \begin{array}{c}
    b_1 \\
    b_2
  \end{array}
\right) \\
\Leftrightarrow
\boldsymbol{Y} = \boldsymbol{W}^{\top}\boldsymbol{X} + \boldsymbol{B}
```

転置についても[ここ](#転置)で触れています。
とりあえずこれで2入力2出力のレイヤーオブジェクトの数式表現が完成です。簡単ですね。
ではこれを一般化します。と言っても特に変わりありません。数式的には

```math
\boldsymbol{Y} = \boldsymbol{W}^{\top}\boldsymbol{X} + \boldsymbol{B}
```
のままです。これを詳しく見ていきます。
$L$入力$M$出力のレイヤーを考えると

```math
\underbrace{\boldsymbol{Y}}_{M \times 1} = \underbrace{\boldsymbol{W}^{\top}}_{M \times L}\underbrace{\boldsymbol{X}}_{L \times 1} + \underbrace{\boldsymbol{B}}_{M \times 1}
```

のようになります。$\boldsymbol{W}^{\top}$は転置後の形状を示しています。転置前は$\underbrace{\boldsymbol{W}}_{L \times M}$ですね。
理論は以上となります。では実装に移りましょう。

****
~~**2020/6/6追記1**
**上記とは異なる計算式(例えば転置なしで立式されているなど)が色々なところで紹介されていると思います。**
**本記事で述べている式が他と違うのは$w$の添え字の置き方が他と違っていたためです。**
**本記事では$w$の添え字を`入力側, 出力側`のように置いており、そこから式を整理すると上記のようになります。**
**他では$w$の添え字を`出力側, 入力側`のように置いているため転置する必要がありません。**
**数学的な間違いはないためどちらも正しい結果を得ることができますが、転置する分オーバーヘッドが多いため本記事のコードは少し遅い可能性があります。**
**いずれ検証して、明確な差があった場合は差し替えるつもりです。**~~

**追記2**
バッチまで考慮する場合、バッチサイズを$N$とすると

```math
\underbrace{\boldsymbol{Y}}_{M \times N} = \underbrace{\boldsymbol{W}^{\top}}_{M \times L}\underbrace{\boldsymbol{X}}_{L \times N} + \underbrace{\boldsymbol{B}}_{M \times N}
```

となり、実装上コードを変更する必要はありません。
ちなみにバイアス$\boldsymbol{B}$は全ての列が同じ値を取ります。つまり$\underbrace{\boldsymbol{B}}_{M \times 1}$でnumpy配列を生成しておけばブロードキャスト機能によって勝手に$M \times N$で計算してくれます。
****
**2020/6/7追記3**
**追記1は誤りでした。改めて考えた結果、異なっているのは$w$の添字の置き方ではなく$x$や$y$を縦に置いている点でした。**

```math
\left(
  \begin{array}{cc}
    y_1 & y_2
  \end{array}
\right)
=
\left(
  \begin{array}{cc}
    x_1 & x_2
  \end{array}
\right)
\left(
  \begin{array}{cc}
    w_{1, 1} & w_{1, 2} \\
    w_{2, 1} & w_{2, 2}
  \end{array}
\right)
+
\left(
  \begin{array}{cc}
    b_1 & b_2
  \end{array}
\right)\\
\Leftrightarrow
\underbrace{\boldsymbol{Y}}_{N \times M} = \underbrace{\boldsymbol{X}}_{N \times L} \underbrace{\boldsymbol{W}}_{L \times M} + \underbrace{\boldsymbol{B}}_{N \times M}
```

とするのが一般的です。上記の数式ではバッチサイズ$N=1$としていますね。


## 行列での順伝播実装
実装場所はスカラでの実装と同じく[baselayer.py](https://qiita.com/kuroitu/items/884c62c48c2daa3def08#レイヤーモジュールのコード準備)です。
スカラでの実装を書き換えます。
<details><summary>baselayer.py</summary><div>

```baselayer.py
    def forward(self, x):
        """
        順伝播の実装
        """
        # 入力を記憶しておく
        self.x = x.copy()

        # 順伝播
        y = self.w.T @ x + self.b
        self.y = self.act.forward(y)
        
        return self.y
```
****
**2020/6/7追記4**
**実装を追記3に合わせたものに変更します。**

```baselayer.py
    def forward(self, x):
        """
        順伝播の実装
        """
        # 入力を記憶しておく
        self.x = x.copy()

        # 順伝播
        self.u = x@self.w + self.b
        self.y = self.act.forward(self.u)
        
        return self.y
```
</div></details>
変わったのはわずか1行ですね。

```baselayer.py
        y = self.w * x + self.b
```

が

```baselayer.py
        y = self.w.T @ x + self.b
```

となっています。
****
**2020/6/7追記5**
**実装を変更したので、それに伴い変更されている部分は**

```baselayer.py
        y = self.w * x + self.b
```

が

```baselayer.py
        y = x@self.w + self.b
```

となっています。
****
numpy配列では転置を`ndarray.T`で行うことができます。
`@`演算子はあまり馴染みがない方もいらっしゃるかもしれませんが、`np.dot`と同じ処理になっています。
**Numpyのバージョン1.10以上で利用できますので、それ以下のバージョンをお使いの場合は注意してください。**
<details><summary>`@`演算子の説明</summary><div>

```test_at.py
x = np.array([1, 2])
w = np.array([[1, 0], [0, 1]])
b = np.array([1, 1])

y = w.T @ x + b
print(y)
print(y == np.dot(w.T, x) + b)

#----------
# 出力は
# [2 3]
# [ True  True]
# となります。
```

ここでは実は`@`演算子の行列とベクトルとの演算機能を暗に用いています。きちんと行列として演算させたい場合は`np.array`の代わりに`np.matrix`として、さらに`reshape`してやる必要があったりします。

```test_at.py
x = np.matrix([1, 2]).reshape(2, -1)
w = np.matrix([[1, 0], [0, 1]])
b = np.matrix([1, 1]).reshape(2, -1)

y = w.T @ x + b
print(y)
print(y == np.dot(w.T, x) + b)

#----------
# 出力は
# [[2]
#  [3]]
# [[ True]
#  [ True]]
# となります。
```

まあめんどくさいですよね。`np.array`でいいでしょう。
</div></details>
これでもう順伝播の実装は終了です。~~これ以上変更することはないでしょう(多分)。~~ちょっと変更しました(2020/7/8)


# `__init__`メソッドの実装
さて、ここまででレイヤーオブジェクトが持つべきメンバがいくつか出てきましたね。これを実装しておきましょう。
あとついでにいくつかメンバを持たせておきます。
<details><summary>`__init__`の実装</summary><div>

```baselayer.py
    def __init__(self, *, prev=1, n=1, 
                 name="", wb_width=5e-2,
                 act="ReLU",
                 act_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)
```
</div></details>
それぞれ書いている通りですね。重みとバイアスは`numpy.random.randn`メソッドで標準正規分布に従った乱数を設定しています。またそれに`wb_width`を乗算し大きさを調整できるようにしています。
活性化関数はactivations.pyから`get_act`メソッドをインポートして取得する形にしています。いずれ[ここ](https://qiita.com/kuroitu/items/73cd401afd463a78115a)のコードを調整して持ってくる予定です。


# 行列演算について
ここでは簡単に行列演算について紹介します。こんなふうに計算するんだな〜ということだけで、数学的なことは一切説明しません(ていうかできません)のでご注意ください。

## 行列和
まずは行列和です。

```math
\left(
  \begin{array}{cc}
    a & b \\
    c & d
 \end{array}
\right)
+
\left(
  \begin{array}{cc}
    A & B \\
    C & D
  \end{array}
\right)
=
\left(
  \begin{array}{cc}
    a + A & b + B \\
    c + C & d + D
  \end{array}
\right)
```

まあ当然の結果ですね。要素ごとに足し算します。足し算はもちろん**行列の形状が完全に一致している必要があります。**

## 行列の要素積
本記事では全く出てきていない要素積についても触れておきます。
要素積のことはアダマール積とも言いますね。

```math
\left(
  \begin{array}{cc}
    a & b \\
    c & d
 \end{array}
\right)
\otimes
\left(
  \begin{array}{cc}
    A & B \\
    C & D
  \end{array}
\right)
=
\left(
  \begin{array}{cc}
    aA & bB \\
    cC & dD
  \end{array}
\right)
```

ちなみにアダマール積の記号は`\otimes`で書けます。他にも[こちら](https://qiita.com/simonritchie/items/deee5ce1a2b86ed5374e)に記号関連が色々乗っていますね。

## 行列積
こちらは要素積ではなく行列積です。形状の制約とか、可換ではないとかが要素積との大きな違いですね。

```math
\left(
  \begin{array}{cc}
    a & b \\
    c & d
 \end{array}
\right)
\left(
  \begin{array}{cc}
    A & B \\
    C & D
  \end{array}
\right)
=
\left(
  \begin{array}{cc}
    aA + bC & aB + bD \\
    cA + dC & cB + dD
  \end{array}
\right)
```

計算方法としては、横$\times$縦をしまくる感じですね。
![matrix_product_image.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/c085384e-57fc-9d89-3196-d46ddda81eaf.gif)
こんな感じです。
このように計算できるためには、<font color="orange">**横**</font>の要素数と<font color="DeepSkyBlue">**縦**</font>の要素数が一致している必要がありますね。
つまり一つ目の行列の<font color="orange">**列**</font>と二つ目の行列の<font color="DeepSkyBlue">**行**</font>が一致している必要があります。
一般化すると、**$L \times M$行列と$M \times N$行列の行列積の結果できる行列は$L \times N$となります。**

## 転置
転置とは行列の行と列を交換する操作のことです。

```math
\left(
  \begin{array}{cc}
    a & b & c \\
    d & e & f
 \end{array}
\right)^{\top}
=
\left(
  \begin{array}{cc}
    a & d \\
    b & e \\
    c & f
 \end{array}
\right)
```

転置の記号は`\top`で書いています。
この操作はこれくらいの説明だけでいいでしょう。


# おわりに
これで順伝播の実装は終了です。特に中間層と出力層で違う処理を行う必要とかもないので[middlelayer.pyやoutputlayer.py](https://qiita.com/kuroitu/items/884c62c48c2daa3def08#レイヤーモジュールのコード準備)ではオーバーライドする必要もありません。順伝播は簡単でいいですね。


# 参考
- [Qiitaのマークダウンで色をつける方法[140色]](https://qiita.com/twipg/items/d8043cd4681a2780c160)
- [Qiitaでの数式記述のベクトル表記](https://qiita.com/mu-777/items/13e0481f149e9cbba3ce)
- [機械学習などの本でよく出てくる数式の、Qiitaでの書き方集](https://qiita.com/simonritchie/items/deee5ce1a2b86ed5374e)


# 深層学習シリーズ
- [深層学習入門 ~基礎編~](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)