Deep Learning

誤差逆伝播法


誤差逆伝播法(back propagation) は、機械学習においてニューラルネットワークを学習させる際に 用いられるアルゴリズムである。

計算グラフからの逆伝播

$y = f(x)$ という計算がある。逆方向には $E$ が入力されると $\displaystyle E \frac{\partial y}{\partial x}$ が出力される。 たとえば $y = f(x) = x^2$ であれば、 $\displaystyle \frac{\partial y}{\partial x} = 2x$ となる。

合成関数を考える。たとえば $z = (x+y)^2$ とすると
$\quad z = t^2\\ \quad t = x + y \quad\quad\quad (5.1)$
のように2つの式で表現することができる。

$\displaystyle \frac{\partial z}{\partial x} = \frac{\partial z}{\partial t} \frac{\partial t}{\partial x} \\ \quad\quad\quad = 2t \times 1 \\ \quad\quad\quad = 2(x+y) \quad\quad\quad\quad (5.4) $

layer_naive.py
class MulLayer:
  def __init__(self):
    self.x = None
    self.y = None

  def forward(self, x, y):
    self.x = x
    self.y = y
    out = x * y
    return out

  def backward(self, dout):
    dx = dout * self.y
    dy = dout * self.x
    return dx, dy

class AddLayer:
  def __init__(self):
    pass

  def forward(self, x, y):
    out = x + y
    return out

  def backward(self, dout):
    dx = dout * 1
    dy = dout * 1
    return dx, dy

buy_apple.py
apple = 100
apple_num = 2
tax = 1.1

mul_apple_layer = MulLayer()
mul_tax_layer = MulLayer()

#
# forward
#

apple_price = mul_apple_layer.forward(apple, apple_num)  # 200
price = mul_tax_layer.forward(apple_price, tax)          # 220

print(price)   # 220

#
#backward
#

dprice = 1
dapple_price, dtax = mul_tax_layer.backward(dprice)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)
print(dapple, dapple_num, dtax)   # 2.2  110   200


活性化関数レイヤの実装

ReLUレイヤ

$\displaystyle \begin{equation} y = \left\{ \begin{matrix} x && (x \gt 0) \\ 0 && (x \leqq 0) \end{matrix} \right. \quad\quad\quad\quad (5.7) \end{equation}$
$\displaystyle \begin{equation} \frac{\partial y}{\partial x} = \left\{ \begin{matrix} 1 && (x \gt 0) \\ 0 && (x \leqq 0) \end{matrix} \right. \quad\quad\quad\quad (5.8) \end{equation}$

式 (5.8) は、順伝播時の入力である $x$ が $0$ より大きければ、 逆伝播は上流の値を下流にそのまま流し、$0$以下であれば下流に流さない。

relu.py
class Relu:
  def __init__(self):
    self.mask = None

  def forward(self, x):
    self.mask = (x <= 0)
    out = x.copy()
    out[self.mask] = 0
    return out

  def backward(self, dout):
    dout[self.mask] = 0
    dx = dout
    return dx

#
# example
#

x = np.array([[1.0, -0.5], [-2.0, 3.0]])
mask = (x <= 0)

print(mask)
# [[False, True],
#  [True, False]]

out = x.copy()
out[mask] = 0

print(out)
# [[1.0, 0.0],
#  [0.0, 3.0]]


sigmoidレイヤ

sigmoid関数は次の通り。

$\displaystyle h(x) = \frac{1}{1+ e^{-x}} \quad\quad\quad\quad (5.9)$

この関数を数ステップに分解して計算の伝播を考える。

$\displaystyle p = -x \\ q = e^p \\ r = 1 + q \\ y = \frac{1}{r} \\ $
  1. ステップ1: $\displaystyle y = \frac{1}{r}$

  2. $\displaystyle \frac{\partial y}{\partial r} = - \frac{1}{r^2} \\ \quad\quad = -y^2 \quad\quad\quad (5.10) $

    式 (5.10) より、逆伝播のときは、上流の値に対して $-y^2$ を乗算して下流へ伝播する。

  3. ステップ2: $\displaystyle r = 1 + q$

  4. $\displaystyle r = 1 + q \\ \frac{\partial r}{\partial q} = 1 $

    したがって、上流の値をそのまま下流に流せばよい。

  5. ステップ3: $\displaystyle q = e^p$

  6. $\displaystyle \frac{\partial q}{\partial p} = e^p $

    上流の値に $e^p = e^{-x}$ を乗算して下流に流す。

  7. ステップ4: $\displaystyle p = -x$

  8. $\displaystyle \frac{\partial p}{\partial x} = -1 $

    上流の値に $-1$ を乗算して下流に流す。

$\displaystyle \frac{\partial y}{\partial x} = \frac{\partial y}{\partial r} \frac{\partial r}{\partial q} \frac{\partial q}{\partial p} \frac{\partial p}{\partial x} = (-y^2) \cdot 1 \cdot e^p \cdot (-1) = y^2 e^{-x} \\ = y^2 (\frac{1}{y} - 1) \quad\quad (\because y = \frac{1}{1+e^{-x}} より e^{-x} = \frac{1}{y}-1) \\ = y (1 - y) $

[自分へのメモ] 単に合成関数の微分の説明をしているだけ。

sigmoid.py
class Sigmoid:
  def __init__(self):
    self.out = None

  def forward(self, x):
    out = 1 / (1 + np.exp(-x))
    self.out = out
    return out

  def backward(self, dout):
    dx = dout * (1.0 - self.out) * self.out
    return dx


Affine/Softmax レイヤの実装

Affineレイヤ

$\displaystyle Y = X \cdot W + B \\ \displaystyle \frac{\partial L}{\partial X} = \frac{\partial L}{\partial Y} \cdot W^T \\ \displaystyle \frac{\partial L}{\partial W} = X^T \cdot \frac{\partial L}{\partial Y} \quad\quad\quad (5.13) $

$L$ はスカラーであり、$X$ や $W$ は行列である。 $\displaystyle \frac{\partial L}{\partial X}$ とは、 スカラー $L$ を行列 $X$ の各要素 $x_i$ で微分して、元の行列 $X$ と同じ形に並べたものである。

$\displaystyle X = (x_0, x_1, \cdots, x_n ) \\ \displaystyle \frac{\partial L}{\partial X} = (\frac{\partial L}{\partial x_0}, \frac{\partial L}{\partial x_1}, \cdots, \frac{\partial L}{\partial x_n} ) \quad\quad\quad (5.15) $

バッチ版 Affine レイヤ

affine_batch.py
class Affine:
  def __init__(self, W, b):
    self.W = w
    self.b = b
    self.x = None
    self.dW = None
    self.db = None
  def forward(self, x):
    self.x = x
    out = np.dot(x, self.W) + self.b
    return out
  def backward(self, dout):
    dx = np.dot(dout, self.W.T)
    self.dW = np.dot(self.x.T, dout)
    self.db = np.sum(dout, axis=0)
    return dx

#
# sample
#
X_dot_W = np.array([[0, 0, 0], [10, 10, 10]])
B =  np.arary([1, 2, 3])
pritn(X_dot_W + B)
#[[  1,  2,  3 ],
# [ 11, 12, 13 ]]

dY = np.array([[1, 2, 3], [4, 5, 6]])
dB = np.sum(dY, axis=0)
print(dB)
#[ 5, 7, 9 ]


Softmax-with-Loss レイヤ

ソフトマックス関数は、入力された値を正規化して出力する。

前レイヤから3入力を受け取り、3クラス分類を行う場合を考える。 順伝播は次のようになる。

$\displaystyle \left( \begin{matrix} a_1 \\ a_2 \\ a_3 \end{matrix} \right) \rightarrow Softmax \rightarrow \displaystyle \left( \begin{matrix} y_1 \\ y_2 \\ y_3 \end{matrix} \right) + \left( \begin{matrix} t_1 \\ t_2 \\ t_3 \end{matrix} \right) \rightarrow \begin{matrix} Cross \\ Entropy \\ Error \end{matrix} \rightarrow L $

このとき、逆伝播は次のようになる。

$\displaystyle \left( \begin{matrix} y_1 - t_1 \\ y_2 - t_2 \\ y_3 - t_3 \end{matrix} \right) \leftarrow Softmax \leftarrow \begin{matrix} Cross \\ Entropy \\ Error \end{matrix} \leftarrow l $
softmax_with_loss.py
class SoftmaxWithLoss:
  def __init__(self):
    self.loss = None
    self.y = None    # output
    self.t = None    # training data (one-hot vector)

  def forward(self, x, t):
    self.t = t
    self.y = softmax(x)
    self.loss = cross_entropy_error(self.y, self.t)
    return self.loss

  def backward(self, dout=1):
    batch_size = self.t.shape[0]
    dx = (self.y - self.t) / batch_size
    return dx


誤差逆伝播法の実装

2層のニューラルネットワークを実装する。

TwoLayerNet.py
import sys, os
sys.path.append(os.pardir)
import numpy as np
from common.layers import *
from common.gradient import numerical_gradient
from collections import OrderedDict

class TwoLayerNet:

  def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
    self.params = {}
    self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
    self.params['b1'] = np.zeros(hidden_size)
    self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
    self.params['b2'] = np.zeros(output_size)
    # layer
    self.layers = OrderedDict()
    self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
    self.layers['Relu1'] = Relu()
    self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])
    self.lastLayer = SoftmaxWithLoss()

  def predict(self, x):
    for layer in self.layers.values():
      x = layer.forward(x)
    return x

  # x: input data, t: training data
  def loss(self, x, t):
    y = self.predict(x)
    return self.lastLayer.forward(y, t)

  def accuracy(self, x, t):
    y = self.predict(x)
    y = np.argmax(y, axis=1)
    if t.ndim != 1 : t = np.argmax(t, axis=1)
    accuracy = np.sum(y == t) / float(x.shape[0])
    return accuracy

  def numerical_gradient(self, x, t):
    loss_W = lambda W: self.loss(x, t)
    grads = {}
    grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
    grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
    grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
    grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
    return grads

  def gradient(self, x, t):
    # forward
    self.loss(x, t)
    # backward
    dout = 1
    dout = self.lastLayer.backward(dout)
    #
    layers = list(self.layers.values())
    layers.reverse()
    for layer in layers:
      dout = layer.backward(dout)
    #
    grads = {}
    grads['W1'] = self.layers['Affine1'].dW
    grads['b1'] = self.layers['Affine1'].db
    grads['W2'] = self.layers['Affine2'].dW
    grads['b2'] = self.layers['Affine2'].db
    return grads



OrderedDict は順番付きの辞書であり、順伝播では追加した順にレイヤのforward()関数を呼び出し、 逆伝播では追加した逆順にレイヤのbackward()関数を呼び出せばよい。

誤差逆伝播法の勾配確認

勾配を求めるには、「数値微分で求める」方法と「解析的に求める」方法がある。 「解析的に求める」方法では、誤差逆伝播法を用いるとパラメータが大量でも効率良く計算できる。 「数値微分で求める」方法は、誤差伝播法の実装のチェックに使える。 どちらの方法でもほぼ同じ結果が得られる「勾配確認 (gradient check)」の作業が必要である。

誤差逆伝播法を使った学習

train_neuralnet.py
import sys, os
sys.path.append(os.pardir)
import numpy as np
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet

(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

iters_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1
train_loss_list = []
train_acc_list = []
test_acc_list = []

iter_per_epoch = max(train_size / batch_size, 1)

for i in range(iters_num):
  batch_mask = np.random.choice(train_size, batch_size)
  x_batch = x_train[batch_mask]
  t_batch = t_train[batch_mask]
  # gradient
  grad = network.gradient(x_batch, t_batch)
  # update
  for key in ('W1', 'b1', 'W2', 'b2'):
    network.params[key] -= learning_rate * grad[key]
  loss = network.loss(x_batch, t_batch)
  train_loss_list.append(loss)
  if i % iter_per_epoch == 0:
    train_acc = network.accuracy(x_train, t_train)
    test_acc = network.accuracy(x_test, t_test)
    train_acc_list.append(train_acc)
    test_acc_list.append(test_acc)
    print(train_acc, test_acc)


Yoshihisa Nitta

http://nw.tsuda.ac.jp/