関数の入出力が一般のテンソルである場合のバックプロパゲーション
[履歴] [最終更新] (2020/07/12 03:26:28)
ここは
趣味のプログラミングを楽しむための情報共有サービス。記事の一部は有料設定にして公開できます。 詳しくはこちらをクリック📝
最近の投稿
注目の記事

概要

こちらのページでは、簡単のため関数の入出力となるテンソルの階数が零の場合を考えました。本ページでは一般の任意の階数のテンソルをバックプロパゲーションで扱う例を記載します。参考書籍: 『ゼロから作る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]]

行列の積 MatMul について

$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
関連ページ