LoginSignup
29
32

More than 3 years have passed since last update.

誤差逆伝播ででてくるアフィン変換の微分演算の導出と、Pythonで実際に計算してみた

Last updated at Posted at 2019-06-05

概要

機械学習のとくにアフィン変換の誤差逆伝播のときに突然でてくる、

\begin{align}
\frac{\partial f}{\partial \boldsymbol{X}} &= \frac{\partial f}{\partial \boldsymbol{Y}} \cdot W^T \tag{1} \\
\frac{\partial f}{\partial W} &= \boldsymbol{X}^T \cdot \frac{\partial f}{\partial \boldsymbol{Y}} \tag{2}   \\
\frac{\partial f}{\partial \boldsymbol{b}} &= \frac{\partial f}{\partial \boldsymbol{Y}} \tag{3}  \\
\end{align}

これについて。 ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装 では、これは認めてつかいましょう、って書いてあるのですが、それが気になる方むけに。。この式を導出しつつ、アフィン変換の部分の誤差逆伝播について理解を深めてみようとおもいます。1

対象の方

  • 上記の式が、なんで??って気になる方
  • アフィン変換の誤差逆伝播の計算を実際に目で見たい方
  • そもそも、誤差逆伝播のやってることがよく分からない方

変数の定義

ちなみに上記に出てくる各変数たちは下記の通り定義します。

ある行列$W$を下記のとおり定義:

\begin{align}
W &:= 
\begin{pmatrix}
w_{11} & w_{21} &\cdots &w_{m1}\\
w_{12} & w_{22} &\cdots &w_{m2}\\
\vdots & \ddots & w_{jk} & \vdots \\
w_{1n} &\cdots  &\cdots &w_{mn}
\end{pmatrix}\\
\end{align}

$\boldsymbol{X}$はベクトルで、下記のとおり定義:

\begin{align}
\boldsymbol{X} &:=(x_1,x_2,\cdots,x_k,\cdots,x_n)
\end{align}

つづいて関数$f$は、$\boldsymbol{X}$や$W$を変数にとる、多変数関数とします。誤差逆伝播の際には損失関数として$L$とか$E$が使われる事がありますが、ここではいったん$f$でいきます。

あるベクトル$\boldsymbol{X}$にたいして

\frac{\partial f}{\partial X} := (
\frac{\partial f}{\partial x_1},
\frac{\partial f}{\partial x_2},
\cdots,
\frac{\partial f}{\partial x_k},
\cdots 
\frac{\partial f}{\partial x_n})

と定義します。

ある行列$\boldsymbol{W}$にたいして

\frac{\partial f}{\partial W} := 
\begin{pmatrix}
\frac{\partial \ f}{\partial w_{11}} &  \frac{\partial \ f}{\partial w_{21}} &\cdots&  \frac{\partial \ f}{\partial w_{m1}}\\
\frac{\partial \ f}{\partial w_{12}} &  \frac{\partial \ f}{\partial w_{22}}&\cdots&  \frac{\partial \ f}{\partial w_{m2}}\\
\vdots &\vdots  \\
\frac{\partial \ f}{\partial w_{1n}} &  \frac{\partial \ f}{\partial w_{2n}}&\cdots&  \frac{\partial \ f}{\partial w_{mn}}
\end{pmatrix}

と定義します。

$\boldsymbol{X}$ の関数として定義されるベクトル$\boldsymbol{Y}$を下記の通り定義:

\begin{align}
\boldsymbol{Y} &:=(y_1,y_2,\cdots,y_j,\cdots,y_m)\\
\end{align}

$\boldsymbol{Y}$はたとえば次節のアフィン変換などで$\boldsymbol{X}$ の関数として定義されているとします。

アフィン変換の微分

先ほどの行列$W$とベクトル$\boldsymbol{X} $,$\boldsymbol{Y} $,あと下記のベクトル$\boldsymbol{b}$:

\boldsymbol{b} :=(b_1,b_2,\cdots,b_j,\cdots,b_m)\\

があるとき、

\boldsymbol{Y} = \boldsymbol{X}\cdot W + \boldsymbol{b} \tag{4}\\

というアフィン変換を考えます。ベクトルの成分のひとつを書くと、$j=1,2,\cdots,m$ にたいして

\begin{align}
y_j &= \sum_{k=1}^{n} w_{jk}x_k + b_j  \\
&= w_{j1}x_1 + w_{j2}x_2 +\cdots w_{jk}x_k \cdots+w_{jn}x_n + b_j  \tag{4'}\\
\end{align}

となり、したがって $y_j$は $w_{jk}$, $x_k$, $b_j$ という変数から構成される変数となります。

さて、構成するそれら変数の変化における $y_j$の変化(つまり微分)を、下記の通り調べておきます。

y_j を x_kで微分する

このとき $y_j$は$x_k$の一次関数なので$x_k$で微分すると、$x_k$の重み$w_{jk}$だけが残ります。つまり、

\frac{\partial y_j}{\partial x_k} = w_{jk} \tag{5}  

となります。あとでつかいますので覚えときましょう。。

y_j を w_ik で微分する

またまた、$y_j$は$w_{ik}$の一次関数でもあるので$w_{ik}$で微分すると、$y_j$には $w_{ik}$のうち$i=j$の項しか存在しないので、$i\neq j$の時はゼロで、結果、

\frac{\partial y_j}{\partial w_{ik}} = 
\left\{
\begin{array}
&x_k&(if \ i=j) \\
0 &(if \ i \neq j)
\end{array}
\right. \tag{6}  

となります。こちらもあとでつかいますので覚えときましょう。。

y_j を b_iで微分する

またまた、$y_j$は$b_i$の一次関数でもあるので$b_i$で微分すると、$y_j$には $b_i$のうち$i=j$の項しか存在しないので、$i\neq j$の時はゼロで、結果、

\frac{\partial y_j}{\partial b_i} = 
\delta_{ij} = 
\left\{
\begin{array}
&1 &(if \ i=j) \\
0 &(if \ i \neq j)
\end{array}
\right. \tag{7}  

となります。あとでつかいますので覚えときましょう。。

合成関数の微分の一般化

さてさて、突然ですが合成関数の微分のおさらいです。

ある関数$f(x,y)$について、さらに$x,y$がそれぞれ$u,v$の関数の場合、つまり $(u,v) \rightarrow (x,y) \rightarrow f$ のばあい

\begin{align}
\frac{\partial f}{\partial u}= \frac{\partial \ f}{\partial x}\frac{\partial \ x}{\partial u} + \frac{\partial \ f}{\partial y}\frac{\partial \ y}{\partial u}\\
\frac{\partial f}{\partial v}= \frac{\partial \ f}{\partial x}\frac{\partial \ x}{\partial v} + \frac{\partial \ f}{\partial y}\frac{\partial \ y}{\partial v}
\end{align}

という合成関数の微分の法則がありました。
参考:合成関数の微分がなぜかけ算になる(連鎖律)のかを理解する

$(u,v) \rightarrow (x,y)$ という2次元から2次元を、$n$次元から$m$次元へ一般化すると、

\begin{align}
\boldsymbol{X} &:=(x_1,x_2,\cdots,x_k,\cdots,x_n)\\
\boldsymbol{Y} &:=(y_1,y_2,\cdots,y_j,\cdots,y_m)\\
\end{align}

について、$\boldsymbol{Y}$が$\boldsymbol{X}$の関数のばあい、($k=1,2,\cdots,n$として)

\begin{align}
\frac{\partial f}{\partial x_k}&= \sum_{j=1}^{m}\frac{\partial \ f}{\partial y_j}\frac{\partial \ y_j}{\partial x_k} \\
&=(\frac{\partial \ f}{\partial y_1},\frac{\partial \ f}{\partial y_2},\cdots,\frac{\partial \ f}{\partial y_m}) 
\left(
\begin{array}{c}
\frac{\partial \ y_1}{\partial x_k}\\ \frac{\partial \ y_2}{\partial x_k}\\ \vdots \\ \frac{\partial \ y_m}{\partial x_k} \tag{8}
\end{array}
\right) 
\end{align}

が成立します。コレは認めて使ってしまおうということで、これらを横に $k=1,2,\cdots,n$ ってならべると、、、

\begin{align}
(\frac{\partial f}{\partial x_1},\cdots,\frac{\partial f}{\partial x_k},\cdots \frac{\partial f}{\partial x_n})&=
(
\sum_{j=1}^{m}\frac{\partial \ f}{\partial y_j}\frac{\partial \ y_j}{\partial x_1},
\cdots,
\sum_{j=1}^{m}\frac{\partial \ f}{\partial y_j}\frac{\partial \ y_j}{\partial x_k},
\cdots,
\sum_{j=1}^{m}\frac{\partial \ f}{\partial y_j}\frac{\partial \ y_j}{\partial x_n}
)\\
&=(\frac{\partial \ f}{\partial y_1},\frac{\partial \ f}{\partial y_2},\cdots,\frac{\partial \ f}{\partial y_m}) 

\begin{pmatrix}
\frac{\partial \ y_1}{\partial x_1} & \cdots& \frac{\partial \ y_1}{\partial x_k} &\cdots&  \frac{\partial \ y_1}{\partial x_n}\\
\frac{\partial \ y_2}{\partial x_1} & \cdots& \frac{\partial \ y_2}{\partial x_k}&\cdots&  \frac{\partial \ y_2}{\partial x_n}\\
\vdots &\vdots  \\
\frac{\partial \ y_m}{\partial x_1} & \cdots& \frac{\partial \ y_m}{\partial x_k}&\cdots&  \frac{\partial \ y_m}{\partial x_n}
\end{pmatrix}   \tag{8'}

\end{align}

となることが分かりました。

導出する

ひとつめの式の導出

さて、合成関数の微分の節では $\boldsymbol{Y}$が$\boldsymbol{X}$のある関数としてすすめてきましたが、ここからは$\boldsymbol{X}$をアフィン変換したものを$\boldsymbol{Y}$とします。

さて冒頭のひとつめを導出していきます。$\text{(8)}$にあるようにさきほどの合成関数の微分で変数$y_j$を経由した後、$\text{(5)}$のアフィン変換の$x_k$の微分により、

\begin{align}
\frac{\partial f}{\partial x_k}&= \sum_{j=1}^{m}\frac{\partial \ f}{\partial y_j}\frac{\partial \ y_j}{\partial x_k} \\
&= \sum_{j=1}^{m}\frac{\partial \ f}{\partial y_j}w_{jk} \\
&=(\frac{\partial \ f}{\partial y_1},\frac{\partial \ f}{\partial y_2},\cdots,\frac{\partial \ f}{\partial y_j},\cdots,\frac{\partial \ f}{\partial y_m}) 
\left(
\begin{array}{c}
w_{1k}\\ w_{2k}\\ \vdots \\ w_{jk} \\ \vdots \\ w_{mk}
\end{array}
\right) 
\end{align}

したがって$\boldsymbol{X}$と$\boldsymbol{Y}$の関係がアフィン変換のときは、上記が成立するようです。さきほどの$\text{(8')}$のように横にならべてベクトルの成分表示にすることで、、

\frac{\partial f}{\partial \boldsymbol{X}} = \frac{\partial f}{\partial \boldsymbol{Y}} \cdot W^T  \tag{1} 

が得られました。

つづいてふたつめの式の導出

\frac{\partial f}{\partial W} := 
\begin{pmatrix}
\frac{\partial \ f}{\partial w_{11}} &  \frac{\partial \ f}{\partial w_{21}} &\cdots&  \frac{\partial \ f}{\partial w_{m1}}\\
\frac{\partial \ f}{\partial w_{12}} &  \frac{\partial \ f}{\partial w_{22}}&\cdots&  \frac{\partial \ f}{\partial w_{m2}}\\
\vdots &\vdots  \\
\frac{\partial \ f}{\partial w_{1n}} &  \frac{\partial \ f}{\partial w_{2n}}&\cdots&  \frac{\partial \ f}{\partial w_{mn}}
\end{pmatrix}

の $ik$要素を考えてみますが、さきほどの$\text{(8)}$の合成関数の微分で変数$y_j$を経由した後$\text{(6)}$の「$i\neq j$の時はゼロ」に着目すると、

\begin{align}
\frac{\partial f}{\partial w_{ik}}&= \sum_{j=1}^{m}\frac{\partial \ f}{\partial y_j}\frac{\partial \ y_j}{\partial w_{ik}} \\
&= \frac{\partial \ f}{\partial y_i}\frac{\partial \ y_i}{\partial w_{ik}} \\
&= \frac{\partial \ f}{\partial y_i}x_k \\
\end{align}

$(i=1,2,\cdots m,k=1,2,\cdots n)$

したがって、これらを縦横に並べると、

\begin{align}
\frac{\partial f}{\partial W} &= 
\begin{pmatrix}
\frac{\partial \ f}{\partial y_{1}}x_1 &  \frac{\partial \ f}{\partial y_{2}}x_1 &\cdots&  \frac{\partial \ f}{\partial y_{m}}x_1\\
\frac{\partial \ f}{\partial y_{1}}x_2 &  \frac{\partial \ f}{\partial y_{2}}x_2&\cdots&  \frac{\partial \ f}{\partial y_{m}}x_2\\
\vdots &\vdots  \\
\frac{\partial \ f}{\partial y_{1}}x_n &  \frac{\partial \ f}{\partial y_{2}}x_n&\cdots&  \frac{\partial \ f}{\partial y_{m}}x_n
\end{pmatrix} \\
&=
\left(
\begin{array}{c}
x_{1}\\ x_{2}\\ \vdots  \\ x_{n}
\end{array}
\right) 

\begin{pmatrix}
\frac{\partial \ f}{\partial y_{1}} &  \frac{\partial \ f}{\partial y_{2}}&\cdots&  \frac{\partial \ f}{\partial y_{m}}
\end{pmatrix} \\
&=
\boldsymbol{X}^T \cdot \frac{\partial f}{\partial \boldsymbol{Y}}   \tag{2} \\
\end{align}

できましたね。

みっつめの式の導出

さいごです。またまた $\text{(8)}$の合成関数の微分で変数$y_j$を経由した後、$\text{(7)}$ により、

\begin{align}
\frac{\partial f}{\partial b_i}&= \sum_{j=1}^{m}\frac{\partial \ f}{\partial y_j}\frac{\partial \ y_j}{\partial b_i} \\
&= \sum_{j=1}^{m}\frac{\partial \ f}{\partial y_j} \delta_{ij} \\
&= \frac{\partial \ f}{\partial y_i} \\
\end{align}

$(i=1,2,\cdots m)$

横に並べることで、、、、

\frac{\partial f}{\partial \boldsymbol{b}} = \frac{\partial f}{\partial \boldsymbol{Y}}  \tag{3} \\

できました。。

つかれましたね。。。

Pythonで計算してみる

さて実際にPythonで計算してみましょう。$n=2,m=3$でやってみます。
ある行列$W$とベクトル$\boldsymbol{b}$:

\begin{align}
W &:= 
\begin{pmatrix}
w_{11} & w_{21} & w_{31}\\
w_{12} & w_{22} & w_{32}\\
\end{pmatrix}\\

\boldsymbol{b} &:=(b_1,b_2,b_3)\\
\end{align}

があるとき、あるベクトル$\boldsymbol{X} $,$\boldsymbol{Y} $:

\begin{align}
\boldsymbol{X} &:=(x_1,x_2)\\
\boldsymbol{Y} &:=(y_1,y_2,y_3)\\
\end{align}

にたいして

\begin{align}
\boldsymbol{Y} &= \boldsymbol{X}\cdot W + \boldsymbol{b} \\
&=
(x_1,x_2)
\begin{pmatrix}
w_{11} & w_{21} & w_{31}\\
w_{12} & w_{22} & w_{32}\\
\end{pmatrix}
+ (b_1,b_2,b_3)
\\
\end{align}

というアフィン変換を考えます。ベクトルの成分のひとつを書くと、$j=1,2,3$ にたいして

\begin{align}
y_j &= \sum_{k=1}^{2} w_{jk}x_k + b_j  \\
&= w_{j1}x_1 + w_{j2}x_2 + b_j  \\
\end{align}

です。

ためしにある関数$f$を、

f(y_1,y_2,y_3)= y_1+y_2+y_3

としてみます。通常はここで平均二乗誤差とか、クロスエントロピー誤差などの「損失関数」が登場するんですが、単純な関数でやってみましょう。

さてさて実際の数値として、

\begin{align}
W &= 
\begin{pmatrix}
1 & 2 & 3\\
4 & 5 & 6\\
\end{pmatrix}\\

\boldsymbol{b} &:=(1,2,3)\\
\boldsymbol{x} &:=(1,2)\\
\end{align}

としてみて、関数 $f$ を、$W$や$\boldsymbol{b}$で微分してみます。

まずは数値微分

まずは微分の定義に従って、数値微分した結果を計算します。ちなみに数値微分の関数def numerical_gradient(f, x):は、

を利用させていただいています。この関数は「多次元配列$x$」と「それを引数にとる$f$」を引数に、$x$の各要素ごとに偏微分した値を$x$と同じshapeで返す関数です(いわゆる $\frac{\partial f}{\partial X}$)。

コード(クリックで表示/非表示)
affine_numerical_grad.py
# !/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import numpy as np

W = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
b = np.array([1.0, 2.0, 3.0])


def main(args):
    # 数値微分
    x = np.array([[1.0, 2.0]])
    grads = numerical_gradients(x)

    print('f ここではloss を x_kで微分する')
    print(grads['x'])
    print('f ここではloss を w_ik で微分する')
    print(grads['W'])
    print('f ここではloss を b_iで微分する')
    print(grads['b'])


def affine(vectors):
    y = np.dot(vectors, W) + b
    return y


def loss(vectors):
    return np.sum(vectors)


def numerical_gradients(vectors):
    def loss_x(x):
        y = affine(x)
        return loss(y)

    def loss_w(WW):
        y = affine(vectors)
        return loss(y)

    def loss_b(BB):
        y = affine(vectors)
        return loss(y)

    grads = {}
    grads['x'] = numerical_gradient(loss_x, vectors)
    grads['W'] = numerical_gradient(loss_w, W)
    grads['b'] = numerical_gradient(loss_b, b)
    return grads


def numerical_gradient(f, x_W_b):
    # print('---')
    # print(x_W_b.shape)
    # print(x_W_b)
    # print('---')
    h = 1e-4  # 0.0001
    grad = np.zeros_like(x_W_b)

    it = np.nditer(x_W_b, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        tmp_val = x_W_b[idx]
        x_W_b[idx] = float(tmp_val) + h
        fxh1 = f(x_W_b)  # f(x+h)

        x_W_b[idx] = tmp_val - h
        fxh2 = f(x_W_b)  # f(x-h)
        grad[idx] = (fxh1 - fxh2) / (2 * h)

        x_W_b[idx] = tmp_val  # 値を元に戻す
        it.iternext()

    return grad


if __name__ == "__main__":
    main(sys.argv)

実行すると、

(venv) $ python affine_numerical_grad.py 
f ここではloss を x_kで微分する
[[ 6. 15.]]
f ここではloss を w_ik で微分する
[[1. 1. 1.]
 [2. 2. 2.]]
f ここではloss を b_iで微分する
[1. 1. 1.]
(venv) $ 

となりました。

f(y_1,y_2,y_3)= y_1+y_2+y_3

と定義したので、

\frac{\partial f}{\partial \boldsymbol{Y}} =(1 ,1,1)

に注意して

\begin{align}
\frac{\partial f}{\partial \boldsymbol{X}} &= \frac{\partial f}{\partial \boldsymbol{Y}} \cdot W^T \tag{1} \\
\frac{\partial f}{\partial W} &= \boldsymbol{X}^T \cdot \frac{\partial f}{\partial \boldsymbol{Y}} \tag{2}   \\
\frac{\partial f}{\partial \boldsymbol{b}} &= \frac{\partial f}{\partial \boldsymbol{Y}} \tag{3}  \\
\end{align}

にあてはめてみて、一致する事を確認してみてください。いちおう、ひとつめの式だけやっておくと、

\begin{align}
\frac{\partial f}{\partial \boldsymbol{X}} &= \frac{\partial f}{\partial \boldsymbol{Y}} \cdot W^T \tag{1} \\
&= 
(1 ,1,1)
\begin{pmatrix}
1 & 4 \\
2 & 5 \\
3 & 6 \\
\end{pmatrix}\\
&=(6,15)
\end{align}

うん、なんだかよさそうですね。

つづいて誤差逆伝播による計算

上記で実際に机上で計算してみたわけですが、誤差逆伝播を利用した微分計算もコードを書いてやってみます。
うーん誤差逆伝播の話というより、冒頭の公式を用いて微分を計算する話になっちゃってますが、、気にしないですすめます。。

コード(クリックで表示/非表示)
affine_grad.py
# !/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import numpy as np

W = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
b = np.array([1.0, 2.0, 3.0])


def main(args):
    # 逆伝播
    x = np.array([[1.0, 2.0]])
    grads = gradients(x)

    print('f ここではloss を x_kで微分する')
    print(grads['x'])
    print('f ここではloss を w_ik で微分する')
    print(grads['W'])
    print('f ここではloss を b_iで微分する')
    print(grads['b'])


def affine(vectors):
    y = np.dot(vectors, W) + b
    return y


def loss(vectors):
    return np.sum(vectors)


def gradients(vectors):
    y = affine(vectors)
    dy = np.full(y.shape, 1.0)  # df/dy
    # dy = np.array([[1.0, 1.0, 1.0]])  # df/dy
    grads = {}
    # print(dy.shape)
    dx = np.dot(dy, W.T)
    # print(dx.shape)
    dw = np.dot(vectors.T, dy)
    db = dy  # 入力x が多次元の場合はbはコレじゃダメ。
    # db = np.sum(dy, axis=0)

    grads['x'] = dx
    grads['W'] = dw
    grads['b'] = db
    return grads


if __name__ == "__main__":
    main(sys.argv)

実行したら、下記のように同じ結果が得られました!

(venv) $ python affine_grad.py 
f ここではloss を x_kで微分する
[[ 6. 15.]]
f ここではloss を w_ik で微分する
[[1. 1. 1.]
 [2. 2. 2.]]
f ここではloss を b_iで微分する
[[1. 1. 1.]]
(venv) 

バッチ版の誤差逆伝播の注意点

さて、上記の例は x = np.array([[1.0, 2.0]]) という、$x$のデータをひとつだけ処理するパタンでした。ですが機械学習では実際は「バッチ処理」といって、$x$のデータを100コ単位とかで計算させたりします。つまるところ、

\begin{align}
\begin{pmatrix}
y_1^0 & y_2^0 & y_3^0\\
y_1^1 & y_2^1 & y_3^1\\
\vdots & \vdots & \vdots\\
y_1^{99} & y_2^{99} & y_3^{99}\\
\end{pmatrix}
&=
\begin{pmatrix}
x_1^0 & x_2^0 \\
x_1^1 & x_2^1 \\
\vdots & \vdots \\
x_1^{99} & x_2^{99} \\
\end{pmatrix}
\begin{pmatrix}
w_{11} & w_{21} & w_{31}\\
w_{12} & w_{22} & w_{32}\\
\end{pmatrix}
+
\begin{pmatrix}
b_1 & b_2 & b_3\\
b_1 & b_2 & b_3\\
\vdots & \vdots & \vdots\\
b_1 & b_2 & b_3\\
\end{pmatrix}
\\
\end{align}

こんな感じの計算を行ったりします。

一方、

f(y_1,y_2,y_3)= y_1+y_2+y_3

は実際は numpyを用いて

def loss(x):
    y = affine(x)
    return np.sum(y)

と定義していました。 $x_1,x_2$が上記のように行列となるため $y_1,y_2,y_3$ も行列となり、すなわち$f(y_1,y_2,y_3)$ は正確には、

f(y_1,y_2,y_3)=\sum_{k=0}^{99}( y_1^{k}+y_2^{k}+y_3^{k})

と定義していたことになります。損失関数などを計算する場合はバッチ単位で損失の総和を見て評価するため、この定義は違和感はないというか、理にかなってる(?)かんじですね。

よって、$b_j$を微小変化させたときの$f$の変化量$\frac{\partial f}{\partial \boldsymbol{b}}$は、バッチ用に記述する際には

\begin{align}
\frac{\partial f}{\partial \boldsymbol{b}} &=\sum_{k=0}^{99(batchsize)} \frac{\partial f}{\partial \boldsymbol{Y}} \tag{3'}  \\
\end{align}

となることに注意しましょう。コードに落とすと、

def gradients(vectors):
    y = affine(vectors)
    dy = np.full(y.shape, 1.0)  # df/dy
    # dy = np.array([[1.0, 1.0, 1.0], [1.0, 1.0, 1.0]])  # df/dy
    grads = {}
    dx = np.dot(dy, W.T)
    dw = np.dot(vectors.T, dy)
    # db = dy  # 入力x が多次元の場合はbはコレじゃダメ。
    db = np.sum(dy, axis=0) # ← 縦方向(?)に足し合わせる

    grads['x'] = dx
    grads['W'] = dw
    grads['b'] = db
    return grads

となります。

では入力をx = np.array([[1.0, 2.0], [2.0, 3.0]]) などバッチバージョンにして、数値微分と誤差逆伝播、それぞれで微分係数を計算してみます。

数値微分(バッチ版)

こちらは入力の$x$以外、コードに変更なしです。

コード(クリックで表示/非表示)
affine_numerical_grad.py
# !/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import numpy as np

W = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
b = np.array([1.0, 2.0, 3.0])


def main(args):
    # 数値微分
    x = np.array([[1.0, 2.0], [2.0, 3.0]])
    grads = numerical_gradients(x)

    print('f ここではloss を x_kで微分する')
    print(grads['x'])
    print('f ここではloss を w_ik で微分する')
    print(grads['W'])
    print('f ここではloss を b_iで微分する')
    print(grads['b'])


def affine(vectors):
    y = np.dot(vectors, W) + b
    return y


def loss(vectors):
    return np.sum(vectors)


def numerical_gradients(vectors):
    def loss_x(x):
        y = affine(x)
        return loss(y)

    def loss_w(WW):
        y = affine(vectors)
        return loss(y)

    def loss_b(BB):
        y = affine(vectors)
        return loss(y)

    grads = {}
    grads['x'] = numerical_gradient(loss_x, vectors)
    grads['W'] = numerical_gradient(loss_w, W)
    grads['b'] = numerical_gradient(loss_b, b)
    return grads


def numerical_gradient(f, x_W_b):
    # print('---')
    # print(x_W_b.shape)
    # print(x_W_b)
    # print('---')
    h = 1e-4  # 0.0001
    grad = np.zeros_like(x_W_b)

    it = np.nditer(x_W_b, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        tmp_val = x_W_b[idx]
        x_W_b[idx] = float(tmp_val) + h
        fxh1 = f(x_W_b)  # f(x+h)

        x_W_b[idx] = tmp_val - h
        fxh2 = f(x_W_b)  # f(x-h)
        grad[idx] = (fxh1 - fxh2) / (2 * h)

        x_W_b[idx] = tmp_val  # 値を元に戻す
        it.iternext()

    return grad


if __name__ == "__main__":
    main(sys.argv)

実行してみると

(venv) $ python affine_numerical_grad.py 
f ここではloss を x_kで微分する
[[ 6. 15.]
 [ 6. 15.]]
f ここではloss を w_ik で微分する
[[3. 3. 3.]
 [5. 5. 5.]]
f ここではloss を b_iで微分する
[2. 2. 2.]
(venv) $

さて誤差逆伝播で同じ出力が得られるか確認します。

誤差逆伝播による計算(バッチ版)

先のとおり db の計算を変更しています。

コード(クリックで表示/非表示)
affine_grad.py
# !/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import numpy as np

W = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
b = np.array([1.0, 2.0, 3.0])


def main(args):
    # 逆伝播
    x = np.array([[1.0, 2.0], [2.0, 3.0]])
    grads = gradients(x)

    print('f ここではloss を x_kで微分する')
    print(grads['x'])
    print('f ここではloss を w_ik で微分する')
    print(grads['W'])
    print('f ここではloss を b_iで微分する')
    print(grads['b'])


def affine(vectors):
    y = np.dot(vectors, W) + b
    return y


def loss(vectors):
    return np.sum(vectors)


def gradients(vectors):
    y = affine(vectors)
    dy = np.full(y.shape, 1.0)  # df/dy
    # dy = np.array([[1.0, 1.0, 1.0], [1.0, 1.0, 1.0]])  # df/dy
    grads = {}
    # print(dy.shape)
    dx = np.dot(dy, W.T)
    # print(dx.shape)
    dw = np.dot(vectors.T, dy)
    # db = dy  # 入力x が多次元の場合はbはコレじゃダメ。
    db = np.sum(dy, axis=0)

    grads['x'] = dx
    grads['W'] = dw
    grads['b'] = db
    return grads


if __name__ == "__main__":
    main(sys.argv)

実行してみると

(venv) $ python affine_grad.py 
f ここではloss を x_kで微分する
[[ 6. 15.]
 [ 6. 15.]]
f ここではloss を w_ik で微分する
[[3. 3. 3.]
 [5. 5. 5.]]
f ここではloss を b_iで微分する
[2. 2. 2.]
(venv) $ 

同じ結果が得られました。。

まとめ

  • アフィン変換の微分について学習しました。
  • 誤差逆伝播というか、アフィン変換の微分の公式に基づいて、数値微分と同じ出力が得られるコードを学びました
  • ただコレだと誤差逆伝播の理解というか公式で計算しただけなので、アフィン変換にさらに活性化関数を合成するなどしないと臨場感がないですね

てことで、アフィン変換と活性化関数を合成するなどして、誤差逆伝播計算をする記事を追加で書いてみたいと思います。

おつかれさまでした。

関連リンク


  1. が、先に書いておくと誤差逆伝播の理解というより、この公式による微分係数の計算の理解になってしまいましたorz 

29
32
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
32