こちらのページでは、簡単のため関数の入出力となるテンソルの階数が零の場合を考えました。本ページでは一般の任意の階数のテンソルをバックプロパゲーションで扱う例を記載します。参考書籍: 『ゼロから作るDeep Learning ❸』
なお、本ページでも同様に簡単のため高階微分は考えません。
テンソルのある要素に着目すると、それは零階のテンソルでありスカラです。こちらのページに記載の Add では、内部的に NumPy の ndarray 同士の加算が行われます。バックプロパゲーションの考え方が要素毎の計算で行われるため、一般のテンソルの場合でも問題なく動作します。
#!/usr/bin/python
# -*- coding: utf-8 -*-
from autograd.variable import Variable
import numpy as np
def Main():
x = Variable(np.array([
[1, 2, 3],
[4, 5, 6],
]))
y = Variable(np.array([
[10, 20, 30],
[40, 50, 60],
]))
z = x + y
z.Backward()
print(z)
print(x.GetGrad())
print(y.GetGrad())
if __name__ == '__main__':
Main()
実行例
$ python3 main.py
Variable([[11 22 33]
[44 55 66]])
[[1 1 1]
[1 1 1]]
[[1 1 1]
[1 1 1]]
テンソルのある要素に着目すると、それは零階のテンソルでありスカラです。要素の値を変更せずに、順番を入れ換えるだけの関数において、バックプロパゲーションではもとの形状に戻す処理が必要になります。
#!/usr/bin/python
# -*- coding: utf-8 -*-
from autograd.variable import Variable
from autograd.function import Function
import numpy as np
def Main():
x = Variable(np.array([
[1, 2, 3],
[4, 5, 6],
]))
y = Reshape((6,))(x)
y.Backward()
print(y)
print(x.GetGrad())
class Reshape(Function):
def __init__(self, shape):
self.__shape = shape # 目標となる shape
self.__xShape = None # もとの shape
def Forward(self, x):
self.__xShape = x.shape
y = np.reshape(x, self.__shape)
return y
def Backward(self, gy):
return np.reshape(gy, self.__xShape)
if __name__ == '__main__':
Main()
実行例
$ python3 main.py
Variable([1 2 3 4 5 6])
[[1 1 1]
[1 1 1]]
転置行列を計算する関数も、テンソルの形状を変更する関数の一つです。バックプロパゲーションでも転置行列を計算します。
#!/usr/bin/python
# -*- coding: utf-8 -*-
from autograd.variable import Variable
from autograd.function import Function
import numpy as np
def Main():
x = Variable(np.array([
[1, 2, 3],
[4, 5, 6],
]))
y = Transpose()(x)
y.Backward()
print(y)
print(x.GetGrad())
class Transpose(Function):
def Forward(self, x):
y = np.transpose(x)
return y
def Backward(self, gy):
gx = np.transpose(gy)
return gx
if __name__ == '__main__':
Main()
実行例
$ python3 main.py
Variable([[1 4]
[2 5]
[3 6]])
[[1 1 1]
[1 1 1]]
テンソルの各要素の和を出力する関数も、テンソルの形状を変更する関数の一つです。バックプロパゲーションにおいては Add の実装と同様の考え方をします。逆伝搬されてきた勾配 gy
を、値を変更せずに、記憶しておいた形状になるようにコピーします。
#!/usr/bin/python
# -*- coding: utf-8 -*-
from autograd.variable import Variable
from autograd.function import Function
import numpy as np
def Main():
x = Variable(np.array([
[1, 2, 3],
[4, 5, 6],
]))
y = Sum()(x)
y.Backward()
print(y)
print(x.GetGrad())
class Sum(Function):
def Forward(self, x):
self.__xShape = x.shape
y = x.sum()
return y
def Backward(self, gy):
gx = np.broadcast_to(gy, self.__xShape)
return gx
if __name__ == '__main__':
Main()
実行例
$ python3 main.py
Variable(21)
[[1 1 1]
[1 1 1]]
行列の積も、テンソルの形状を変更する関数の一つです。バックプロパゲーションが以下の実装になることは後述のとおりです。
#!/usr/bin/python
# -*- coding: utf-8 -*-
from autograd.variable import Variable
from autograd.function import Function
import numpy as np
def Main():
x = Variable(np.random.randn(2, 3))
w = Variable(np.random.randn(3, 4))
y = MatMul()(x, w)
y.Backward()
print(y.GetData().shape)
print(x.GetGrad().shape)
print(w.GetGrad().shape)
class MatMul(Function):
def Forward(self, x, w):
y = x.dot(w)
return y
def Backward(self, gy):
x, w = self.GetInputs()
gx = gy.dot(w.GetData().T)
gw = x.GetData().T.dot(gy)
return gx, gw
if __name__ == '__main__':
Main()
実行例
$ python3 main.py
(2, 4)
(2, 3)
(3, 4)
出力されるテンソルの個数が複数である関数の例として、例えば以下のようなものが考えられます。
#!/usr/bin/python
# -*- coding: utf-8 -*-
from autograd.variable import Variable
from autograd.function import Function
import numpy as np
def Main():
x = Variable(np.array([
[1, 2, 3],
[4, 5, 6],
]))
y, z = Separate()(x)
w = y + z
w.Backward()
print(y)
print(z)
print(w)
print(x.GetGrad())
class Separate(Function):
def Forward(self, x):
y = x[:1]
z = x[1:]
return y, z
def Backward(self, gy0, gy1):
gx = np.vstack((gy0, gy1))
return gx
if __name__ == '__main__':
Main()
実行例
$ python3 main.py
Variable([[1 2 3]])
Variable([[4 5 6]])
Variable([[5 7 9]])
[[1 1 1]
[1 1 1]]
$X$ は N x D
行列、$W$ は D x H
行列であるとします。
$$X = \left( \begin{array}{rr} x_{1,1} & ... & x_{1,D} \\ ... & ... & ... \\ x_{N,1} & ... & x_{N,D} \\ \end{array} \right) $$
$$W = \left( \begin{array}{rr} w_{1,1} & ... & w_{1,H} \\ ... & ... & ... \\ w_{D,1} & ... & w_{D,H} \\ \end{array} \right) $$
このとき $Y = X W$ は N x H
行列です。
$$Y = X W = \left( \begin{array}{rr} y_{1,1} & ... & y_{1,H} \\ ... & ... & ... \\ y_{N,1} & ... & y_{N,H} \\ \end{array} \right) $$
$$y_{i,j} = \sum_{k=1}^{D} x_{i,k} w_{k,j} $$
ここで、$X$ と $W$ の関数である、あるスカラ値 $L(X, W)$ を考えます。$L$ の偏微分は、連鎖律によって以下のように計算できます。
$$\frac{\partial L}{\partial x_{i,j}} = \sum_{k=1}^{N} \sum_{l=1}^{H} \frac{\partial L}{\partial y_{k,l}} \frac{\partial y_{k,l}}{\partial x_{i,j}} $$
ここで $Y = XW$ の要素に関する上述の式を $x_{i,j}$ で偏微分します。$k = i$ 以外の場合は偏微分の結果が 0 になります。
$$\begin{eqnarray} \frac{\partial y_{k,l}}{\partial x_{i,j}} &=& \sum_{m=1}^{D} \frac{\partial (x_{k,m} w_{m,l})}{\partial x_{i,j}} \\ &=& \frac{\partial x_{k,j}}{\partial x_{i,j}} w_{j,l} \end{eqnarray} $$
これを先程の式に代入すると以下のようになります。
$$\begin{eqnarray} \frac{\partial L}{\partial x_{i,j}} &=& \sum_{k=1}^{N} \sum_{l=1}^{H} \frac{\partial L}{\partial y_{k,l}} \frac{\partial x_{k,j}}{\partial x_{i,j}} w_{j,l} \\ &=& \sum_{l=1}^{H} \frac{\partial L}{\partial y_{i,l}} w_{j,l} \\ \end{eqnarray} $$
これは以下の行列計算が成り立つことを意味しています。
$$\begin{eqnarray} \frac{\partial L}{\partial X} &=& \left( \begin{array}{ccc} \frac{\partial L}{\partial x_{1,1}} & ... & \frac{\partial L}{\partial x_{1,D}} \\ ... & ... & ... \\ \frac{\partial L}{\partial x_{N,1}} & ... & \frac{\partial L}{\partial x_{N,D}} \\ \end{array} \right) = \left( \begin{array}{ccc} \frac{\partial L}{\partial y_{1,1}} & ... & \frac{\partial L}{\partial y_{1,H}} \\ ... & ... & ... \\ \frac{\partial L}{\partial y_{N,1}} & ... & \frac{\partial L}{\partial y_{N,H}} \\ \end{array} \right) W^T \\ &=& \frac{\partial L}{\partial Y} W^T \end{eqnarray} $$
$X$ と $W$ の対称性から、同様の考え方で、以下の行列計算も成り立つことが分かります。
$$\frac{\partial L}{\partial W} = X^T \frac{\partial L}{\partial Y} $$
MatMul
のバックプロパゲーションでは、これら行列計算を実装しています。
class MatMul(Function):
def Forward(self, x, w):
y = x.dot(w)
return y
def Backward(self, gy):
x, w = self.GetInputs()
gx = gy.dot(w.GetData().T)
gw = x.GetData().T.dot(gy)
return gx, gw