In [9]:
import matplotlib.pyplot as plt
from graphviz import Digraph
from matplotlib.image import imread
f = Digraph(format="png")
f.attr(rankdir='LR', size='8,5')
f.attr('node', shape='circle')
f.edge('apple', '×2', label='100')
f.edge('×2', '×1.1', label='200')
f.edge('×1.1', 'cash', label='220')
f.render("../docs/5_1_1")
img = imread('../docs/5_1_1.png')
plt.figure(figsize=(10,8))
plt.imshow(img)
plt.show()
In [10]:
f = Digraph(format="png")
f.attr(rankdir='LR', size='8,5')
f.attr('node', shape='circle')
f.node('apple', 'apple')
f.node('apple_num', 'apple_num')
f.node('tax', 'tax')
f.node('mul1', '×')
f.node('mul2', '×')
f.node('cash', 'cash')
f.body.append('{rank=same; apple; apple_num; tax;}')
f.edge('apple', 'mul1', label='100')
f.edge('apple_num', 'mul1', label='2')
f.edge('mul1', 'mul2', label='200')
f.edge('tax', 'mul2', label='1.1')
f.edge('mul2', 'cash', label='220')
f.render("../docs/5_1_1")
img = imread('../docs/5_1_1.png')
plt.figure(figsize=(10,8))
plt.imshow(img)
plt.show()
グラフの左から右へ計算を進めることを「順伝播(forward propergation)」という。逆に右から左に計算を遡ることを「逆伝播(backward propergation)」という。
各ノードにおける計算は局所的なものであり、それ以前の計算されてくる過程は考慮する必要なく計算が行なうことが出来る。
計算グラフで解く利点
りんごの値段が少し上がった場合に総支払額が同変化するか確認することが出来る。
局所的な微分を伝達する原理は、「連鎖律(chain rule)」によるものである。
逆伝播の計算手順は信号$E$に対して、ノードの局所的な微分$\frac{\delta y}{\delta x}$を乗算し次のノードへ伝達していく。
In [11]:
import matplotlib.pyplot as plt
from graphviz import Digraph
from matplotlib.image import imread
f = Digraph(format="png")
f.attr(rankdir='LR', size='8,5')
f.attr('node', shape='circle')
f.edge('start', 'f', label='x')
f.edge('f', 'start', label='E*δy/δy')
f.edge('f', 'end', label='y')
f.edge('end', 'f', label='E')
f.render("../docs/5_2_1")
img = imread('../docs/5_2_1.png')
plt.figure(figsize=(10,8))
plt.imshow(img)
plt.show()
連鎖律は合成関数の微分についての性質である。
合成関数の微分は、合成関数を構成するそれぞれの関数の微分の積によって表すことが出来る
上記例でいうと以下の様に表すことが出来る。 $$\frac{\delta z}{\delta x}= \frac{\delta z}{\delta t}\frac{\delta t}{\delta x}$$
計算を進めると以下となり、連鎖律をもちいて合成関数の微分が行える。
$$ \frac{\delta z}{\delta t} = 2t \\ \frac{\delta t}{\delta x} = 1 \\ \frac{\delta z}{\delta x}= \frac{\delta z}{\delta t}\frac{\delta t}{\delta x} = 2t・1 = 2(x + y) $$加算や乗算を例に逆伝播の仕組みを考える。
$z=x+y$について逆伝播を考える。この式について微分を行なうと以下となる。 $$ \frac{\delta z}{\delta x}=1 \\ \frac{\delta z}{\delta y}=1 \\ $$
逆伝播の際には、前の計算から伝わってきた$\frac{\delta L}{\delta z}$を乗算して次のノードに渡す。(この場合はx,yの微分とも1なので$\frac{\delta L}{\delta z}・1$となる)
$z=xy$について逆伝播を考える。この式について微分を行なうと以下となる。 $$ \frac{\delta z}{\delta x}=y \\ \frac{\delta z}{\delta y}=x \\ $$
乗算の逆伝播では入力した値をひっくり返した値が用いられることとなる。 $$ xの逆伝播:\frac{\delta L}{\delta z}\frac{\delta z}{\delta x} = \frac{\delta L}{\delta z}・y\\ yの逆伝播:\frac{\delta L}{\delta z}\frac{\delta z}{\delta y} = \frac{\delta L}{\delta z}・x \\ $$
加算の逆伝播では上流の値をただ流すだけ立ったので順伝播の入力信号は不要だったが、乗算の逆伝播では順伝播の入力信号を保持しておかなければならない。
割愛
In [12]:
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
In [13]:
# リンゴ2個と消費税を計算する実装
apple = 100
apple_num = 2
tax = 1.1
# layer
mul_apple_layer = MulLayer()
mul_tax_layer = MulLayer()
# forward
apple_price = mul_apple_layer.forward(apple, apple_num)
price = mul_tax_layer.forward(apple_price, tax)
print(price)
# 各変数に関する微分
dprice = 1
dapple_price, dtax = mul_tax_layer.backward(dprice)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)
print(dapple, dapple_num, dtax)
In [14]:
# 加算レイヤ
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
In [15]:
# りんごとみかんの買い物を実装
apple = 100
apple_num = 2
orange = 150
orange_num = 3
tax = 1.1
# layer
mul_apple_layer = MulLayer()
mul_orange_layer = MulLayer()
add_apple_orange_layer = AddLayer()
mul_tax_layer = MulLayer()
# forward
apple_price = mul_apple_layer.forward(apple, apple_num) # (1)
orange_price = mul_orange_layer.forward(orange, orange_num) # (2)
all_price = add_apple_orange_layer.forward(apple_price, orange_price) # (3)
price = mul_tax_layer.forward(all_price, tax) # (4)
# backward
dprice = 1
dall_price, dtax = mul_tax_layer.backward(dprice) # (4)
dapple_price, dorange_price = add_apple_orange_layer.backward(dall_price) # (3)
dorange, dorange_num = mul_orange_layer.backward(dorange_price) # (2)
dapple, dapple_num = mul_apple_layer.backward(dapple_price) # (1)
print("price:", int(price))
print("dApple:", dapple)
print("dApple_num:", int(dapple_num))
print("dOrange:", dorange)
print("dOrange_num:", int(dorange_num))
print("dTax:", dtax)
活性化関数してとして使われるReLU(Rectified Linear Unit)は次式で表される。
$$ y = \begin{cases} & x \; (x>0) \\ & 0 \; (x\leq0) \end{cases} $$xに関するyの微分は以下のようになる。 $$ \frac{\delta y}{\delta x} = \begin{cases} & 1 \; (x>0) \\ & 0 \; (x\leq0) \end{cases} $$
順伝播時入力のxが0より大きければ、逆伝播は上流の値をそのまま下流に流す。逆にxが0以下であれば下流への信号はストップする。 実装は以下となる。
In [16]:
class Relu:
def __init__(self):
self.mask = None
def forward(self, x):
# maskはxが0以下の場合false、それ以外はtrueを保持。xの配列の形で保持
self.mask = (x <= 0)
out = x.copy()
# maskでtrueである要素(xが0以下)は0を代入
out[self.mask] = 0
return out
def backward(self, dout):
dout[self.mask] = 0
dx = dout
return dx
シグモイド関数は次式で表される。 $$ y=\frac{1}{1+exp(-x)} $$
計算グラフでは「×」「exp」「+」「/」の順でのノードが連結する。 逆伝播の流れを順に沿って見ていく。
ステップ1
「/」ノードは$y=\frac{1}{x}$を表す(上記式で「1+exp(-x)」を分母にするところから分かる)が、この微分は解析的に次式のようになる。
$$
\frac{\delta y}{\delta x} = -\frac{1}{x^{2}} \\
= -y^{2}
$$
ステップ2
「+」ノードは上流の値を下流にそのまま流すだけ。
ステップ3
「exp」ノードは$y=exp(x)$であり、微分は以下式で表される。
$$
\frac{\delta y}{\delta x}= exp(x)
$$
計算グラフでは順伝播時の出力を乗算して下流へ伝搬する。($exp(-x)$)
ステップ4
「×」ノードは順伝播時の値をひっくり返して乗算する。(-1)
以上によりSigmoidの逆伝播の出力は以下となる。 $$ \frac{\delta L}{\delta y}y^{2}exp(-x) \\ =\frac{\delta L}{\delta y}\frac{1}{(1+exp(-x))^{2}}exp(-x) \\ =\frac{\delta L}{\delta y}\frac{1}{1+exp(-x)} \frac{exp(-x)}{1+exp(-x)} \\ =\frac{\delta L}{\delta y}y(1-y) $$
これによりSigmoidレイヤの逆伝播は順伝播の出力だけから求めることが出来る。 実装は以下となる。
In [17]:
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レイヤ」という名前で実装する。
ニューロンの重み付き和$Y = np.dot(X,W)+B$における計算グラフで考えてみる。
この計算グラフではこれまでのような「スカラ値」ではなく「行列」が伝播していく。 逆伝播は以下のように導出される。
$$ \frac{\delta L}{\delta X}=\frac{\delta L}{\delta Y}・W^{T} \\ \frac{\delta L}{\delta W}=W^{T}\frac{\delta L}{\delta Y} $$$W^{T}$のTは転置を表す。行列の内積の逆伝播は対応する次元の要素数を一致させるように内積を組み立てる必要がある。次元数は以下の通りである。
X:(2,)
W:(2,3)
X・W:(3,)
B:(3,)
Y:(3,)
$\frac{\delta L}{\delta Y}:(3,)$
$W^{T}:(3,2)$
$X^{T}:(2,1)$
In [18]:
import numpy as np
X_dot_W = np.array([[0, 0, 0], [10, 10, 10]])
B = np.array([1, 2, 3])
print(X_dot_W)
print(X_dot_W + B)
順伝播では各データに対して加算がされていたが、逆伝播においてはそれぞれの逆伝播のデータからバイアスに集約される必要がある。
In [19]:
dY = np.array([[1, 2, 3], [4, 5, 6]])
print(dY)
dB = np.sum(dY, axis=0)
print(dB)
出力層であるソフトマックス関数について考える。ソフトマックス関数は入力された値を正規化して出力する(出力の和が1になる)。 ニューラルネットワークにおける推論ではソフトマックス関数は不要(Affineレイヤの出力のうち最も高い値(スコア)を推論値とすれば良いので)だが、学習時には必要になる。
ここでは損失関数である交差エントロピー誤差(cross entoropy error)も含めて「Softmax-with-Loss レイヤ」という名前で実装する。 このレイヤはSoftmaxレイヤを通した後にCross Emtropy Errorレイヤを通す構造になっている。
順伝播ではSoftmaxレイヤの出力と教師データのラベルがCross Emtropy Errorレイヤの入力となり、損失Lを出力する。
逆伝播ではSoftmaxレイヤからは$(y_{1}-t_{1}, y_{2}-t_{2}, y_{3}-t_{3})$というシンプルな形で流れてくる。これにより、誤った推論を行った場合は大きな誤差が逆伝播されるが、正解している場合は小さい誤差が伝搬される。
このようにシンプルな形で流せるのはソフトマックス関数の損失関数として交差エントロピー誤差を選んでいるからである。(回帰問題における出力層に恒等関数、損失関数として2乗和誤差を用いることも同様(3.5参照))
In [20]:
def softmax(a):
exp_a = np.exp(a)
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
def cross_entropy_error(y, t):
delta = 1e-7
return -np.sum(t * np.log(y + delta))
class SoftmaxWithLoss:
def __init__(self):
self.loss = None
self.y = None # softmaxの出力
self.t = None # 教師データ
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]
# データ1個あたりの誤差を全データに伝播させる
dx = (self.y - self.t) / batch_size
return dx
In [22]:
import sys, os
sys.path.append(os.pardir) # 親ディレクトリのファイルをインポートするための設定
import numpy as np
from src.gradient import numerical_gradient
from collections import OrderedDict
from src.layer import *
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)
# レイヤの生成
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:入力データ, t:教師データ
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
# x:入力データ, t:教師データ
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'], grads['b1'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
grads['W2'], grads['b2'] = self.layers['Affine2'].dW, self.layers['Affine2'].db
return grads
In [24]:
# 数値微分と誤差逆伝播法の誤差確認
import sys, os
sys.path.append(os.pardir) # 親ディレクトリのファイルをインポートするための設定
import numpy as np
from src.mnist import load_mnist
# データの読み込み
(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)
x_batch = x_train[:3]
t_batch = t_train[:3]
grad_numerical = network.numerical_gradient(x_batch, t_batch)
grad_backprop = network.gradient(x_batch, t_batch)
for key in grad_numerical.keys():
diff = np.average( np.abs(grad_backprop[key] - grad_numerical[key]) )
print(key + ":" + str(diff))
In [47]:
import sys, os
sys.path.append(os.pardir)
import numpy as np
from src.mnist import load_mnist
# データの読み込み
(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]
# 勾配
#grad = network.numerical_gradient(x_batch, t_batch)
grad = network.gradient(x_batch, t_batch)
# 更新
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)