前言
通过数值微分的方法计算了神经网络中损失函数关于权重参数的梯度,虽然容易实现,但缺点是比较费时间,本章节将使用一种高效的计算权重参数梯度的方法——误差方向传播法 本文将通过①数学式、②计算图,这两种方法去理解误差方向传播法。
计算图
这里说的图形是数据结构图,通过多个节点和边表示(连接节点的直线称为“边”)。 下面将举出几个例子来掌握计算图的使用。 例1:小明在超市买了2个100日元一个的苹果,消费税是10%,请计 算支付金额。 计算图通过节点和箭头表示计算过程。节点用○表示,○中是计算的内容。将计算的中间结果写在箭头的上方,表示各个节点的计算结果从左向右传递。 也可以这样表示: 例2: 小明在超市买了2个苹果、3个橘子。其中,苹果每个100日元,橘子每个150日元。消费税是10%,请计算支付金额。 整个计算图的计算过程就是: 1.构建计算图。 2.在计算图上,从左向右进行计算 ———————————————————————————————— “从左向右进行计算”是一种正方向上的传播,简称为正向传播(forward propagation)。正向传播是从计算图出发点到结束点的传播。 顾名思义,若从右至左计算,称为反向传播。反向传播将在接下来的导数计算中发挥重要作用。
局部计算
计算图的特征是可以通过传递“局部计算”获得最终结果。无论全局发生了什么,局部只关心自己相关信息的输出。 计算图将复杂的计算分割成简单的局部计算,和流水线作业一样,将局部计算的结果传递给下一个节点。
使用计算图截图的优点
①将复杂的全局计算,通过局部计算使得各个节点执行简单的计算 ②计算图可以将中间的计算结果全部保存起来 ③最重要的原因是通过反向传播高效计算导数
对于问题1,假设我们想知道苹果价格的上涨会在多大程度上影响最终的支付金额,即求“支付金额关于苹果的价格的导数”。设苹果的价格为x,支付金额为L,dL/dx,这个导数的值表示当苹果的价格稍微上涨时,支付金额会增加多少。 图中的"2.2" 是计算出来的导数的值,在这个例子中,反向传播从右向左传递导数的值(1 → 1.1 → 2.2),这意味着,如果苹果的价格上涨1日元,最终的支付金额会增加2.2日元*(严格地讲,如果苹果的价格增加某个微小值,则最终的支付金额将增加那个微小值的2.2倍)。* 计算中途求得的导数的结果(中间传递的导数)可以被共享,从而可以高效地计算多个导数。综上,计算图的优点是,可以通过正向传播和反向传播高效地计算各个变量的导数值
链式法则
传递这个局部导数的原理,是基于链式法则(chain rule)的。
计算图的反向传播
假设存在y = f(x)的计算,这个计算的反向传播如图。 反向传播的计算顺序是,将信号E乘以节点的局部导数(dy/dx ),然后将结果传递给下一个节点。这里所说的局部导数是指正向传播中y = f(x)的导数,也就是y关于x的导数(dy/dx )。比如,假设y = f(x) = x^2,则局部导数为 = 2x。把这个局部导数乘以上游传过来的值(本例中为E),然后传递给前面的节点。
通过链式法则实现
什么是链式法则
复合函数是由多个函数构成的函数。比如,z = (x + y)^2是由式(5.1)所示的两个式子构成的。 参考高等数学教材中的“复合函数求导”法则: 结果如下:
链式法则和计算图
现在我们尝试将式(5.4)的链式法则的计算用计算图表示出来。如果用 “**2”节点表示平方运算的话,则计算图如图。 最后得到dz/dx的导数值为2(x+y)
反向传播
本节将以“+”和“×”等运算为例,介绍反向传播的结构。
加法节点的反向传播
这里以z = x + y为对象,观察它的反向传播。z = x + y的导数可由下式(解析性地)计算出来。 因为加法节点的反向传播只是将输入信号输出到下一个节点,所以如图 5-11所示,反向传播将1.3向下一个节点传递。
乘法节点的反向传播
乘法节点的反向传播。这里我们考虑z = xy。这个式子的导数用式(5.6)表示。
加法的反向传播只是将上游的值传给下游,并不需要正向传播的输入信号。 但是,乘法的反向传播需要正向传播时的输入信号值。因此,实现乘法节点的反向传播时,要保存正向传播的输入信号。
在对比苹果的例子
里要解的问题是苹果的价格、苹果的个数、消费税这3个变量各自如何影响最终支付的金额。 相当于求“支付金额关于苹果的价格的导数”“支付金额关于苹果的个数的导数”“支付金额关于消费税的导数”。用计算图的反向传播来解的话,求解过程如图。 乘法节点的反向传播会将输入信号翻转后传给下游。从结果可知,苹果的价格的导数是2.2,苹果的个数的导数是110,消费税的导数是200。这可以解释为,如果消费税和苹果的价格增加相同的值,则消费税将对最终价格产生200倍大小的影响,苹果的价格将产生2.2倍大小的影响。
不过,因为这个例子中消费税和苹果的价格的量纲不同,所以才形成了这样的结果(消费税的1是100%,苹果的价格的1是1日元)。
练习: 填入空的方格内
简单层的实现
我们把要实现的计算图的乘法节点称为“乘法层”(MulLayer),加法节点称为“加法层”。
我们将把构建神经网络的“层”实现为一个类。这里所说的“层”是神经网络中功能的单位。比如,负责 sigmoid函数的Sigmoid、负责矩阵乘积的Affine等,都以层为单位进行实现。因此,这里也以层为单位来实现乘法节点和加法节点。
乘法层的实现中有两个共通的方法(接口)forward()和backward()。forward()对应正向传播,backward()对应反向传播。 乘法层作为MulLayer类,加法层的实现也类似,其实现过程如下;
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
加法层的forward()接收x和y两个参数,将它们相加后输出。backward()将上游传来的导数(dout)原封不动地传递给下游。具体为什么“原封不动地传递给下游”,在上文中的复合函数求导推导过程中已做解释。
现在我们将计算图中的过程用python代码实现。
apple=100
apple_num=2
orange=150
orange_num=3
tax=1.1
mul_apple_layer=MulLayer()
mul_orange_layer=MulLayer()
add_apple_orange_layer=AddLayer()
mul_tax_layer=MulLayer()
apple_price=mul_apple_layer.forward(apple,apple_num)
orange_price=mul_orange_layer.forward(orange,orange_num)
all_price=add_apple_orange_layer.forward(apple_price,orange_price)
price=mul_tax_layer.forward(all_price,tax)
print("%d"%price)
dprice=1
dall_price,dtax=mul_tax_layer.backward(dprice)
dapple_price,dorange_price=add_apple_orange_layer.backward(dall_price)
dapple,dapple_num=mul_apple_layer.backward(dapple_price)
dorange,dorange_num=mul_orange_layer.backward(dorange_price)
print(dapple_price,dorange_price,dapple,dapple_num,dorange,dorange_num,dtax)
715 1.1 1.1 2.2 110 3.3 165.0 650 可以看到党apple和orange的价格发生变化时,总体价格的变化为1.1倍,这个变化的影响只有1.1,相较于其他因素的变化影响最小。 ————————————————————————————————
激活函数层的实现
我们把构成神经网络的层实现为一个类。先来实现激活函数的ReLU层和Sigmoid层。
ReLU层
通过这个式子可以看出,如果正向传播时输入的x大于0,对x求导之后的导数值为1,则反向传播会将上游的值原封不动的传递给下游。相反,如果输入x的值小于0,则反向传播中的给下游的信号会停留在此处。 现在假定forward()和backward()的参数是numpy数组。 计算图如下 ReLU类如下:
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
ReLU层的作用就像电路中的开关一样。正向传播时,有电流通过的话,就将开关设为 ON;没有电流通过的话,就将开关设为 OFF。反向传播时,开关为ON的话,电流会直接通过;开关为OFF的话,则不会有电流通过。
Sigmoid层
将上述过程集约化的“sigmoid”节点。 化简得: 即:通过正向传播的输出值 y 就可以计算反向传播 用python代码表示如下
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-self.out)*self.out
return dx
layer=Sigmoid()
layer.forward(2)
layer.backward(1)
Affine/Softmax 层的实现
Affine层
神经网络的正向传播中,为了计算加权信号的总和,使用了矩阵的乘 积运算(NumPy中是np.dot())
神经网络的正向传播中进行的矩阵的乘积运算在几何学领域被称为“仿 射变换”A。因此,这里将进行仿射变换的处理实现为“Affine层”。
现在我们来考虑图5-24的计算图的反向传播。以矩阵为对象的反向传播,按矩阵的各个元素进行计算时,步骤和以标量为对象的计算图相同。 具体的推导过程已经省略。 “X ·W”的导数 “dot”节点的反向传播,与矩阵的转置有关。
批版本的Affine层
现在考虑N个数据一起进行的正向传播,也就是批版本的Affine层。 X是一个N行2列的数据。
用python代码实现的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—with—Loss 层
softmax函数会将输入值正规化之后再输出。 Softmax层将输入值正规化(将输出值的和调整为1)之后再输出。另外,因为手写数字识别要进行10类分类,所以向Softmax层的输入也有10个。
神经网络中进行的处理有推理(inference)和学习两个阶段
当神经网络的推理只需要给出一个答案的情况下,因为此时只对得分最大值感兴趣,所以不需要 Softmax层。不过,神经网络的学习阶段则需要Softmax层。 下面来实现Softmax层。考虑到这里也包含作为损失函数的交叉熵误 差(cross entropy error),所以称为“Softmax-with-Loss层”。 这里假设要进行3类分类,从前面的层接收3个输入(得分)。如图5-30所示,Softmax层将输入(a1, a2, a3)正规化,输出(y1, y2, y3)。Cross Entropy Error层接收Softmax的输出(y1, y2, y3)和教师标签(t1, t2, t3),从这些数据中输出损失L。
将Softmax-with-Loss层的过程简化之后的效果为: Softmax层的反向传播得到了(y1 ? t1, y2 ? t2, y3 ? t3)这样“漂亮”的结果。由于(y1, y2, y3)是Softmax层的输出,(t1, t2, t3)是监督数据,所以(y1 ? t1, y2 ? t2, y3 ? t3)是Softmax层的输出和教师标签的差分。神经网络的反向传播会把这个差分表示的误差传递给前面的层,这是神经网络学习中的重要性质。 神经网络学习的目的就是通过调整权重参数,使神经网络的输出(Softmax的输出)接近教师标签。 因此,必须将神经网络的输出与教师标签的误差高效地传递给前面的层。刚刚的(y1 ? t1, y2 ? t2, y3 ? t3)正是Softmax层的输出与教师标签的差,直截了当地表示了当前神经网络的输出与教师标签的误差。 这里考虑一个具体的例子,比如思考教师标签是(0, 1, 0),Softmax层 的输出是(0.3, 0.2, 0.5)的情形。因为正确解标签处的概率是0.(20%),这个时候的神经网络未能进行正确的识别。此时,Softmax层的反向传播传递的是(0.3, ?0.8, 0.5)这样一个大的误差。因为这个大的误差会向前面的层传播,所以Softmax层前面的层会从这个大的误差中学习到“大”的内容。 一句话就是会将大的误差向前传播。 两种组合: ①使用交叉熵误差作为 softmax函数的损失函数后,反向传播得到 (y1 ? t1, y2 ? t2, y3 ? t3)这样“漂亮”的结果。 ②使用“平方和误差”作为“恒等函数”的损失函数,反向传播才能得到(y1 ? t1, y2 ? t2, y3 ? t3)这样“漂亮”的结果。 ———————————————————————————————— 代码实现Softmax-with-Loss层。
import sys, os
sys.path.append(os.pardir)
from common.functions import *
class SoftmaxWithLoss:
def __init__(self):
self.loss = None
self.y = None
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]
dx = (self.y - self.t) / batch_size
return dx
请注意反向传播时,将要传播的值除以批的大小(batch_size)后,传递给前面的层的是单个数据的误差。
误差反向传播法的实现
可以理解为将每一层的功能封装成一个模块,将整个模块组装起来,就可以构建一个神经网络。
神经网络学习的全过程
前提 神经网络中有合适的权重和偏置,调整权重和偏置以便拟合训练数据的 过程称为学习。神经网络的学习分为下面4个步骤。 步骤1(mini-batch) 从训练数据中随机选择一部分数据。 步骤2(计算梯度) 计算损失函数关于各个权重参数的梯度。 步骤3(更新参数) 将权重参数沿梯度方向进行微小的更新。 步骤4(重复) 重复步骤1、步骤2、步骤3。
上文中通过大量篇幅所述的误差反向传播法在步骤2中出现,之前的梯度计算,我使用的数值微分,但是缺点是耗时,因此接下来将用反向传播的方法来高效的计算梯度。
对应误差反向传播法的神经网络的实现
实现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['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
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):
self.loss(x, t)
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
是将神经网络的层保存为OrderedDict这一点非常重要。OrderedDict是有序字典,“有序”是指它可以记住向字典里添加元素的顺序。因此,神经网络的正向传播只需按照添加元素的顺序调用各层的forward()方法就可以完成处理,而反向传播只需要按照相反的顺序调用各层即可。 因为Affine层和ReLU层的内部会正确处理正向传播和反向传播,所以这里要做的事情仅仅是以正确的顺序连接各层,再按顺序(或者逆序)调用各层。
误差反向传播法的梯度确认
两种求梯度的方法。一种是基于数值微分的方法,另一种是解析性地求解数学式的方法。 后一种方法通过使用误差反向传播法,即使存在大量的参数,也可以高效地计算梯度。 但是也不能抛弃掉数值微分,在确认误差反向传播法的实现是否正确时,是需要用到数值微分的。 我们用数值微分去确认误差方向传播法的实现是否正确,这一过程叫做**“梯度确认”**
梯度确认的实现:
import sys, os
sys.path.append(os.pardir)
import numpy as np
from dataset.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))
受到数值精度的限制,刚才的误差一般不会为 0,但是如果实现正确的话,可以期待这个误差是一个接近 0的很小的值。如果这个值很大,就说 明误差反向传播法的实现存在错误。
使用误差反向传播法的学习
import sys, os
sys.path.append(os.pardir)
import numpy as np
from dataset.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.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)
小结
①使用计算图,直观的把握计算过程。计算图的节点由局部计算构成,局部计算构成全局计算。 ②计算图的反向传播,可以计算各个节点的导数,这是关键。 ③不同类型节点的计算导数的方式:加法、乘法、矩阵相乘等等。 ④将神经网络的组成元素封装成一层一层,高效地计算梯度(反向传播法) ⑤通过比较数值微分和误差反向传播法的结果,可以确认误差方向传播法是否正确,这一过程叫做 梯度确认。
参考
《深度学习入门:基于Python的理论与实现 》 斋藤康毅
|