対象者
深層学習シリーズの記事です。
前回の記事はこちらです。
ここでは順伝播について、まずはスカラでの理論を説明して、それから行列に拡張します。
前回記事で紹介したコードに追加していったり修正していく形となるので、まずは前回記事からコードを取ってきておいてくださいね〜
次回の記事はこちら
目次
スカラでの順伝播
ここでは、スカラ(実数)での順伝播の理論と実装を説明します。といっても、だいたい基礎編で既に述べている通りです。
スカラでの順伝播理論
まずは理論ですね。
このニューロンモデルから見ていきます。
これを定式化すると$f(x) = \sigma(wx + b)$となることはここで述べている通りです。活性化関数$\sigma(•)$を通すことで非線形化し、層を重ねる意味を持たせています。
では、この操作を計算グラフで表すとどのようになるでしょうか?
こんな感じになります。今までは入力を$x$だけとしており他の要素は略していましたが、計算グラフでは 重み$w$ と バイアス(閾値)$b$ もきちんと記述し、活性化関数を通し出力する、というように書き換えられます。
この変数たち$x, w, b, y$は諸々ニューロンオブジェクトが持つべき変数になります。
また活性化関数についても、Pythonは関数やクラスをオブジェクトとして変数に格納することが可能なので、ニューロンオブジェクトに持たせると実装がカプセル化できて良さそうですね。
スカラでの順伝播の理論は以上です。シンプルでいいですね〜。
スカラでの順伝播実装
では実装してみます。実装するコードはbaselayer.pyです。
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
入力を保持しているのは逆伝播の時に必要になるからです。
また活性化関数はまだ実装に触れませんが、上記みたいに使えるように実装しようと考えています。あと持っているメソッドは微分を計算するbackward
です。物によってはupdate
メソッドを持たせる必要もあるかもしれません。
順伝播自体は至極簡単ですね。数式通りです。
スカラでの実装は以上となります。続いてベクトル(というより行列)での実装を考えます。
行列での順伝播
続いて行列での順伝播を考えます。
行列は線形代数の行列積の概念を知っていないと厳しいので、もしご存知でない方がいらっしゃいましたらここで簡単に説明しています。
行列での順伝播理論
まずは先ほどのニューロンオブジェクトを2つ重ねたようなレイヤーオブジェクトを考えましょう。
図のうまい表現が思いつかなかったのでちょっと解説します。
まず黒の矢印たちは先ほどのニューロンオブジェクトという理解でOKです。他のカラーの矢印たちのことですね。
水色の矢印は上側のニューロンから下側のニューロンへとつながるシナプスを表しています。
真ん中の掛け算のノードを通る際に水色の重み$w_{1, 2}$を乗算して、下側の足し算のノードへ合流します。
赤色の矢印も同様ですね。
真ん中の掛け算のノードを通る際に赤色の重み$w_{2, 1}$を乗算して、上側の足し算のノードへ合流します。
足し算のノードの入力が3つになってしまっていますが、複数の足し算のノードを用いることで2入力に分解できますので、それを省略したとお考えください。
それでは、これを数式で追いかけてみましょう。以下では簡単のために$\sigma_i(•)$は恒等関数とします。
まずは書き下しからです。
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
これ自体は図を見ていただければ明白だと思います。
では、これを行列表現で表しましょう。
\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$列 のようになっていますね。このままでは理論的にも実装的にも扱いづらいため転置しましょう。
\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出力のレイヤーオブジェクトの数式表現が完成です。簡単ですね。
ではこれを一般化します。と言っても特に変わりありません。数式的には
\boldsymbol{Y} = \boldsymbol{W}^{\top}\boldsymbol{X} + \boldsymbol{B}
のままです。これを詳しく見ていきます。
$L$入力$M$出力のレイヤーを考えると
\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$とすると
\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$を縦に置いている点でした。
\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です。
スカラでの実装を書き換えます。
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に合わせたものに変更します。
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
y = self.w * x + self.b
が
y = self.w.T @ x + self.b
となっています。
2020/6/7追記5
実装を変更したので、それに伴い変更されている部分は
y = self.w * x + self.b
が
y = x@self.w + self.b
となっています。
numpy配列では転置をndarray.T
で行うことができます。
@
演算子はあまり馴染みがない方もいらっしゃるかもしれませんが、np.dot
と同じ処理になっています。
Numpyのバージョン1.10以上で利用できますので、それ以下のバージョンをお使いの場合は注意してください。
`@`演算子の説明
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
してやる必要があったりします。
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
でいいでしょう。
__init__
メソッドの実装
さて、ここまででレイヤーオブジェクトが持つべきメンバがいくつか出てきましたね。これを実装しておきましょう。
あとついでにいくつかメンバを持たせておきます。
`__init__`の実装
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)
行列演算について
ここでは簡単に行列演算について紹介します。こんなふうに計算するんだな〜ということだけで、数学的なことは一切説明しません(ていうかできません)のでご注意ください。
行列和
まずは行列和です。
\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)
まあ当然の結果ですね。要素ごとに足し算します。足し算はもちろん行列の形状が完全に一致している必要があります。
行列の要素積
本記事では全く出てきていない要素積についても触れておきます。
要素積のことはアダマール積とも言いますね。
\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
で書けます。他にもこちらに記号関連が色々乗っていますね。
行列積
こちらは要素積ではなく行列積です。形状の制約とか、可換ではないとかが要素積との大きな違いですね。
\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$縦をしまくる感じですね。
こんな感じです。
このように計算できるためには、横の要素数と縦の要素数が一致している必要がありますね。
つまり一つ目の行列の列と二つ目の行列の行が一致している必要があります。
一般化すると、$L \times M$行列と$M \times N$行列の行列積の結果できる行列は$L \times N$となります。
転置
転置とは行列の行と列を交換する操作のことです。
\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ではオーバーライドする必要もありません。順伝播は簡単でいいですね。