之前我们讲解了通过数值微分计算神经网络的权重参数的梯度从而能够完成神经网络的学习过程。数值微分的优点是简单、比较容易实现,但缺点是计算上比较费时间。我们将要学习的误差反向传播法能能够高效计算权重参数的梯度。
首先来考虑加法节点的反向传播。这里以 z = x + y为对象,观察它的反向传播。z = x + y的导数可由下式(解析性地)计算出来。
∂ z ∂ x = 1 \frac{\partial z}{\partial x}\;=\;1 ∂x∂z=1
∂ z ∂ y = 1 \frac{\partial z}{\partial y}\;=\;1 ∂y∂z=1
因此,用计算图表示的话,如下图所示。
在上图中,反向传播将从上游传过来的导数乘以1,然后传向下游。也就是说,因为加法节点的反向传播只乘以1,所以输入的值会原封不动地流向下一个节点。
现在来看一个加法的反向传播的例子。“10 + 5 = 15”这一计算,反向传播时,从上游传来值1.3,用计算图表示的话,如图所示:
因为加法节点的反向传播只是将输入信号输出到下一个节点,所以如上图所示,反向传播将1.3向下一个节点传递。
我们看一下乘法节点的反向传播这里我们考虑z = xy。这个式子的导数用下面的式子表示。
∂ z ∂ x = y \frac{\partial z}{\partial x}\;=\;y ∂x∂z=y
∂ z ∂ y = x \frac{\partial z}{\partial y}\;=\;x ∂y∂z=x
乘法的反向传播会将上游的值乘以正向传播时的输入信号的“翻转值”后传递给下游。
现在我们来看一个具体的例子。比如,假设“10 × 5 = 50”这一计算,反向传播时,从上游会传来值1.3。用计算图表示的话,如图所示:
因为乘法的反向传播会乘以输入信号的翻转值,所以各自可按1.3 × 5 = 6.5、1.3 × 10 = 13计算。另外,加法的反向传播只是将上游的值传给下游,并不需要正向传播的输入信号。但是,乘法的反向传播需要正向传播时的输入信号值。因此,实现乘法节点的反向传播时,要保存正向传播的输入信号。
本节将用Python实现前面的购买苹果的例子。这里,我们把要实现的计算图的乘法节点称为“乘法层”(MulLayer),加法节点称为“加法层”(AddLayer)。
层的实现中有两个共通的方法(接口)forward()和backward()。forward()对应正向传播,backward()对应反向传播。
现在来实现乘法层。乘法层作为MulLayer类,其实现过程代码如下:
class MulLayer:
# 初始化实例变量 x 和 y,保存正向传播时的输入值
def __init__(self):
self.x = None
self.y = None
# forward()接收x和y两个参数,相乘后输出
def forward(self, x, y):
self.x = x
self.y = y
out = x * y
return out
# backward()将从上游传来的导数乘以正向传播的翻转值,然后传给下游
def backward(self, dout):
dx = dout * self.y # 翻转x和y
dy = dout * self.x
return dx, dy
上面就是MulLayer的实现。现在我们使用MulLayer实现购买苹果的例子(2个苹果和消费税)。如下图所示:
使用这个乘法层的话,上图的正向传播和反向传播可以像下面这样实现
if __name__ == "__main__":
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) # 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
这里,调用backward()的顺序与调用forward()的顺序相反。此外,要注意backward()的参数中需要输入“关于正向传播时的输出变量的导数”。比如, mul_apple_layer乘法层在正向传播时会输出apple_price,在反向传播时,则会将apple_price的导数dapple_price设为参数。
实现加法节点的加法层,代码如下:
clss 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
加法层不需要特意进行初始化,所以__init__()中什么也不运行(pass语句表示“什么也不运行”)。加法层的forward()接收x和y两个参数,将它们相加后输出。backward()将上游传来的导数(dout)原封不动地传递给下游。
现在,我们使用加法层和乘法层,实现下面的计算图所表示的计算过程。
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()
# 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) # 715
print(dapple_num, dapple, dorange, dorange_num, dtax) # 110 2.2 3.3 165 650
这个实现虽然有一点长,但是每一条命令都很简单。首先,生成必要的层,以合适的顺序调用正向传播的forward()方法。然后,用与正向传播相反的顺序调用反向传播的backward()方法,就可以求出想要的导数。
综上,计算图中层的实现(这里是加法层和乘法层)非常简单,使用这些层可以进行复杂的导数计算。下面,我们来实现神经网络中使用的层。
现在,我们将计算图的思路应用到神经网络中。这里,我们把构成神经网络的层实现为一个类。先来实现激活函数的Relu层和Sigmoid层。
激活函数ReLU(Rectified Linear Unit)由下式表示:
y = { x ( x > 0 ) 0 ( x ≤ 0 ) y\;=\;\left\{\begin{array}{l}x\;\;\;\;\;\;\;(x\;>\;0)\\0\;\;\;\;\;\;\;(x\;\leq\;0)\end{array}\right. y={x(x>0)0(x≤0)
通过上面的式子,可以求出y关于x的导数,如下面式子所示。
∂ y ∂ x = { 1 ( x > 0 ) 0 ( x ≤ 0 ) \frac{\partial y}{\partial x}\;=\;\left\{\begin{array}{l}1\;\;\;\;\;\;\;\;(x\;>\;0)\\0\;\;\;\;\;\;\;\;(x\;\leq\;0)\end{array}\right. ∂x∂y={1(x>0)0(x≤0)
在上面的式子中,如果正向传播时的输入x大于0,则反向传播会将上游的值原封不动地传给下游。反过来,如果正向传播时的x小于等于0,则反向传播中传给下游的信号将停在此处。用计算图表示如下图:
现在我们来实现ReLU层。在神经网络的层的实现中,一般假定forward()和backward()的参数是Numpy数组。
class ReLU:
def __init__(self):
# 实例变量mask,是由True/False构成的Numpy数组
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
Relu类有实例变量mask。这个变量mask是由True/False构成的NumPy数组,它会把正向传播时的输入x的元素中小于等于0的地方保存为True,其他地方(大于0的元素)保存为False。如下例所示,mask变量保存了由True/False构成的Numpy数组。
>>> x = np.array([[1.0, -0.5], [-2.0, 3.0]])
>>> print(x)
[[ 1. -0.5]
[-2. 3. ]]
>>> mask = (x <= 0)
>>> print(mask)
[[False True]
[ True False]]
如果正向传播时的输入值小于等于0,则反向传播的值为0。因此,反向传播中会使用正向传播时保存的mask,将从上游传来的dout的mask中的元素为True的地方设为0。
ReLU层的作用就像电路中的开关一样。正向传播时,有电流通过的话,就将开关设为ON;没有电流通过的话,就将开关设为OFF。反向传播时,开关为ON的话,电流会直接通过;开关为OFF的话,则不会有电流通过。
sigmoid函数如下表示:
y = 1 1 + e x p ( − x ) y\;=\frac1{1\;+\;exp(-x)} y=1+exp(−x)1
用计算图表示的话,如下图:
下面我们来进行计算图的反向传播。这里作为总结,来看一下反向传播的流程。
步骤1
“/”节点表示 ,它的导数可以解析性地表示为下式。
$$\frac{\partial y}{\partial x};=;-\frac1{x2};=;-y2\\$$
根据上式,反向传播时,会将上游的值乘以-y2(正向传播的输出的平方乘以-1后的值)后,再传给下游。计算图如下所示。
步骤3
“exp”节点表示y = exp(x),它的导数由下式表示。
$$\frac{\partial y}{\partial x};=;exp(x)\\$$
计算图中,上游的值乘以正向传播时的输出(这个例子中是exp(-x))后,再传给下游。
步骤4
“×”节点将正向传播时的值翻转后做乘法运算。因此,这里要乘以−1。
上面的两个图的计算图是相同的,但是,第二张图可以省略方向传播中的计算过程,因此计算效率更高。此外,通过对节点进行集约化,可以不用在意Sigmoid中的繁琐细节,而只专注输入输出,这一点很重要。
另外:
现在,我们用Python实现Sigmoid层。参考上图,可以像下面这样实现。
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
这个实现中,正向传播时将输出保存在了实例变量out中。然后,反向传播时,使用该变量out进行计算。
神经网络的正向传播中,为了计算加权信号的总和,使用了矩阵的乘积运算(Numpy中是np。dot())。
例如:
>>> X = np.random.randn(2) # 输入
>>> W = np.random.randn(2, 3) # 权重
>>> B = np.random.randn(3) # 偏置
>>>
>>> X.shape # (2, )
>>> W.shape # (2, 3)
>>> B.shape # (3, )
>>>
>>> Y = np.dot(X, W) + B
这里,X、W、B 分别是形状为(2,)、(2,3)、(3,)的多维数组。这样一来,神经元的加权和可以用Y = np.dot(X, W) + B计算出来。然后,Y经过 激活函数转换后,传递给下一层。这就是神经网络正向传播的流程。此外,我们来复习一下,矩阵的乘积运算的要点是使对应维度的元素个数一致。比如,如下面的图5-23所示,X和W 的乘积必须使对应维度的元素个数一致。另外,这里矩阵的形状用(2, 3)这样的括号表示(为了和NumPy的shape属 性的输出一致)。
现在将这里进行的求矩阵的乘积与偏置的和的运算用计算图表示出来。将乘积运算用"dot"节点表示的话,则np.dot(X, W) + B
的运算可用下图表示出来。另外,在各个变量的上方标记了它们的形状(比如,计算图上显示X的形状为(2,),X·W的形状为(3,)等)。
以矩阵为对象的反向传播,按矩阵的各个元素进行计算时,步骤和以标量为对象的计算图相同。可得到如下公式:
$$\frac{\partial L}{\partial X};=;\frac{\partial L}{\partial Y}\cdot W^T\\ \frac{\partial L}{\partial W};=;X^T\cdot\frac{\partial L}{\partial Y}\\$$
现在我们根据上面的式子,写出计算图的反向传播。
前面介绍的Affine层的输入X是以单个数据为对象的。现在我们考虑N个数据一起进行正向传播的情况,也就是批版本的Affine层。
批版本的Affine层的计算图如下:
加上偏置时,需要特别注意。正向传播时,偏置被加到X·W的各个数据上。比如,N = 2(数据为2个时,偏置会被分别加到这2个数据(各自的计算结果)上,具体的例子如下所示。
>>> X_dot_W = np.array([[0, 0, 0], [10, 10, 10]])
>>> B = np.array([1, 2, 3])
>>>
>>> X_dot_W
array([[0, 0, 0],
[10, 10, 10]])
>>> X_dot_W + B
array([[1, 2, 3],
[11, 12, 13]])
正向传播时,偏置会被加到每一个数据上。因此,反向传播时,各个数据的反向传播的值需要汇总为偏置的元素。
>>> dY = np.array([[1, 2, 3], [4, 5, 6]])
>>> dY
array([[1, 2, 3],
[4, 5, 6]])
>>>
>>> dB = np.sum(dY, axis=0)
>>> dB
array([5, 7, 9])
这个例子中,假定数据有2个(N=2)。偏置的反向传播会对这2个数据的导数按元素进行求和。Affine的实现如下:
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
神经网络中进行的处理有推理和学习两个阶段。神经网络的推理通常不使用Softmax层。神经网络中未被正规化的输出结果有时被称为“得分”。也就是说,当神经网络的推理只需要给出一个答案的情况下,因此此时只对得分最大值感兴趣,所以不需要Softmax层。不过,神经网络得学习阶段则需要Softmax层。
下面来实现Softmax层。考虑到这里也包含作为损失函数得交叉熵误差(cross entropy error),所以称为“Softmax-with-Loss层”。Softmax-with-Loss层(Softmax函数和交叉熵误差)的计算图如下图:
在上面的计算图中,softmax函数记为Softmax层,交叉熵误差即为Cross Entropy Error层。这里假设要进行3类分类,从前面的层接收3个输入(得分)。上图所示,Softmax层将输入(a1, a2, a3)正规化,输出(y1, y2, y3)。Cross Entropy Error层接收Softmax的输出(y1, y2, y3)和教师标签(t1, t2, t3),从这些数据中输出损失L。
神经网络学习的目的就是通过调整权重参数,使神经网络的输出(Softmax的输出)接近教师标签。因此,必须将神经网络的输出与教师标签的误差高效地传递给前面的层。刚刚的(y1-t2, y2-t2, y3-t3)正是Softmax层的输出与教师标签的差,直截了当的表示了当前神经网络的输出与教师标签的误差。
我们来实现Softmax-with-Loss层的代码:
class SoftmaxWithLoss:
def __init__(self):
self.loss = None # 损失
self.y = None # softmax的输出
self.t = None # 监督数据(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
这个实现利用了softmax()和cross_entropy_error()函数。注意:反向传播时,将要传播的值除以批的大小(batch_size)后,传递给前面的层的是单个数据的误差。
现在来进行神经网络的实现。这里我们要把2层神经网络实现为TwoLayerNet。首先,将这个类的实例变量和方法整理成如下两张表:
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['b1'] = 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 numercial_gradient(self, x, t):
loss_W = lambda W: self.loss(x, t)
grads = {}
grads['W1'] = numercial_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())
layer.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
上述代码中,OrderedDict是有序字典,“有序”是指它可以集主像字典里添加元素的顺序。因此,神经网络的正向传播只需按照添加元素的顺序调用各层的forward()方法就可以完成处理,而反向传播只需要按照相反的顺序调用各层即可。因为Affine层和Relu层的内部会正确处理正向传播和反向传播,所以这里要做的事情仅仅是以正确的顺序连接各层,再按顺序(或者逆序)调用各层。像这样通过将神经网络的组成元素以层的方式实现,可以轻松地构建神经网络。
现在,我们学习了两种求梯度地方法。一种是基于数值微分地方法,另一种是解析性地求解数学式地方法。后一种方法通过使用误差反向传播法,即使存在大量的参数,也可以高效地计算梯度。在之后的学习中,我们将误差反向传播法作为求解梯度的主要手段。那么,数值微分法有什么作用呢?实际上,再确认误差反向传播法的实现是否正确时,需要用到数值微分。
数值微分的优点是实现简单,因此,一般情况下不太容易出错。而误差反向传播法的实现很复杂,易出错。所以常常使用数值微分来判断误差反向传播法的结果是否正确。这个操作称作梯度确认。
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)
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))
上述代码依然是读入MNIST数据集。然后,使用训练数据得一部分,确认数值微分求出得梯度和误差反向传播求出得梯度的误差。这里误差的计算方法是求出各个权重参数中对应元素的差的绝对值,并计算其平均值。运行后的结果如下:
W1:3.599836124323742e-10
b1:2.037941098141996e-09
W2:5.291201893967323e-09
b2:1.3948171118244313e-07
上述结果可以看出,通过数值微分和误差反向传播法求出的梯度的差非常小。
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]
# 通过误差反向传播法求梯度
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()
if i % iter_per_epoch == 0:
train_acc = netwprk.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)
小结