5. 误差反向传播法
上一节介绍了神经网络的学习,并通过数值微分计算了神经网络的权重参数的梯度(严格地说,是损失函数关于权重参数的梯度)。数值微分简单、容易实现,但是计算很费时间。
我们将介绍误差反向传播法,要正确理解它,有两种方法:基于数学式和基于计算图(computational graph)。前者在各大图书中经常出现,但是如果直接从数学式子开始探讨,很容易止步于式子的罗列。本书希望通过计算图来更直观地理解误差反向传播法
通过计算图来理解误差反向传播法的想法,参考了 AndrejKarpathy 的博客和他与 Fei-Fei Li 教师复杂的斯坦福大学的深度学习课程CS231n
5.1 计算图
计算题将计算过程用图形表示出来,这里说的图形是数据结构图,通过多个节点和边表示(连接节点的直线称为‘边’)。
5.1.1 用计算图求解
- 问题 1:太郎在超市买了 2 个 100 日元一个的苹果,消费税是 10%,请计算支付金额
计算题通过节点和箭头表示计算过程。节点用〇表示,〇中是计算的内容。将计算的中间结果写在箭头的上方,表示各个节点的计算结果从左向右传递。
如图所示,开始时,苹果的 100 日元流到‘x2’节点,变成 200 日元,然后被传递到下一个节点。接着,这个 200 日元流向‘x1.1’节点,变成
220 日元,因此,答案为 220 日元
上图吧‘x2’和‘x1.1’等作为一个运算符整体用〇括起来了,不过只用〇表示乘法运算‘x’也是可行的
- 问题 2:太郎在超市买了 2 个苹果、3 个橘子。其中,苹果每个 100 日元,橘子每个 150 日元。消费税是 10%,请计算支付金额
综上,用计算图解题的情况下,需要按如下流程进行:
- 构建计算图
- 在计算图上,从左向右进行计算
‘从左向右进行计算’是一种正方向上的传播,简称为正向传播(forward propagation)。正向传播是从计算图出发点到结束点的传播。自然,也有反方向的传播,
反向传播(backward propagation),反向传播在接下来的导数计算中发挥重要作用
5.1.2 局部计算
计算图的特征是可以通过传递‘局部计算’获得最终结果。‘局部’是指,‘与自己有关的某个小范围’。局部计算是指,无论全局发生了什么,都能只根据与自己相关的信息输出接下来的结果。
比如在超市里买了两个苹果和其他很多东西
假设(经过复杂的计算)购买的其他很多东西总共花费 4000 日元。这里的重点是,各个节点处的计算都是局部计算。也就是说,苹果和其他很多东西的求和计算并不关心
4000 这个数字是怎么来的,只进行求和。换言之,各个节点处只需进行与自己有关的运算,不用考虑全局
综上,计算图专注局部计算,各个步骤所要做的就是对象节点的局部计算。通过传递它的计算结果,可以获得全局的复杂计算的结果
- 组装汽车是一个复杂的工作,通常需要进行‘流水线’作业。每个工人(机器)承担的都是简化了的工作,这个工作的成果会传给下一个工人,直至汽车组装完成。计算图将复杂计算分割成简单的局部计算,和流水线作业一样,将局部计算的结果传递给下一个节点。
5.1.3 为何用计算图解题
计算图的优点:1. 无论全局计算多么复杂,都可以通过局部计算使各个节点致力于简单的计算,从而简化问题。2.
利用计算图可以将中间的计算结果全部保存起来。3. 最重要的一点,可以通过反向传播高效计算导数。
我们思考一下问题 1,假设我们现在的苹果价格的上涨会在多大程度上影响需要支付的金额,即求‘支付金额关于苹果的价格的导数’。设苹果价格为
x,支付金额为 L,则相对于求 əL/əx。这个导数的值表示当苹果价格稍微上涨时,支付金额会增加多少。
‘支付金额关于苹果的导数’可以通过计算图的反向传播求出来
这里反向箭头的下方是局部导数。结果是 2.2,也就是说,如果苹果的价格上涨 1 日元,则支付金额会增加 2.2 日元
‘支付金额关于消费税的导数’,‘支付金额关于苹果的个数的导数’也可以通过相同的方式求出。并且,计算中途求得的导数的结果(中间传递的导数)可以被共享,从而可以高效地计算多个导数。综上,计算图的优点是,可以通过正向传播和反向传播高效地计算各个变量的导数值
5.2 链式法则
5.2.1 计算图的反向传播
假设存在 y=f(x),反向传播如图所示
反向传播的计算顺序是,将信号 E 乘以节点的局部导数(əy/əx),然后将结果传递给下一个节点。这里的局部导数是指正向传播中 y=f(x)
的导数,也就是 y 关于 x 的导数。比如 y=f(x)=x^2,则 əy/əx=2x,把这个导数乘以上游传过来的值(E),然后传递给前面的节点。
这就是反向传播的计算顺序。通过这种计算,可以高效求出导数的值,这是反向传播的要点。这是如何实现的?可以通过链式法则的原理进行解释。
5.2.2 什么是链式法则
先从复合函数说起,复合函数是由多个函数构成的函数。比如 z=(x+y)^2
链式法则是关于复合函数的导数的性质
如果某个函数由复合函数表示,则该复合函数的导数可以用构成复合函数的各个函数的导数的乘积表示。
5.2.3 链式法则和计算图
5.3 反向传播
本节以‘+’和‘x’等计算为例,介绍反向传播的结构。
5.3.1 加法节点的反向传播
这里以 z=x+y 为对象,观察它的反向传播
z=x+y 的导数可以由上面这两个式子计算出来。此时,两式都等于 1。在下图中,反向传播将从上游传过来的导数乘以
1,然后传向下游。因为加法节点的反向传播只乘以 1,所以输入的值会原封不动地流向下一个节点。
另外,本例将上游传来的导数值设为 əL/əx。这是因为,我们假定最终输出值为 L 的大型计算图。z=x+y 的计算位于其中,从上游会传来
əL/əz 的值,并向下游传递 əL/əx 和 əL/əy
5.3.2 乘法节点的反向传播
考虑 z=xy
乘法的反向传播会将上游的值乘以正向传播时的输入信号的‘翻转值’后传递给下游。翻转值表示一种翻转关系,如下图 5-12,正向传播时信号是
x 的话,反向传播时则是 y;正向传播时的信号是 y 的话,反向传播时则是 x。
乘法的反向传播会乘以输入信号的翻转值,而加法的反向传播只是将上游的值传给下游,并不需要正向传播的输入信号。但是,乘法的反向传播需要正向传播时的输入信号值。因此,实现乘法节点的反向传播时,要保证正向传播的输入信号
5.3.3 苹果的例子
最开始苹果的例子(2 个苹果和消费税)。这里要解的问题是苹果的价格、苹果的个数、消费税这 3
个变量各自如何影响最终支付的金额。相对于求‘支付金额关于苹果的价格/苹果的个数/消费税的导数’
乘法节点的反向传播会将输入信号翻转后传给下游。从图 5-14 可知,苹果的价格的导数是 2.2,苹果的个数的导数是 110,消费税的导数是
200。这可以解释为,如果消费税和苹果的价格增加相同的值,则消费税将对最终价格产生 200 倍大小的影响,苹果的价格将产生 2.2
倍大小的影响。不过,这个例子里消费税和苹果的价格的量纲不同,所以才形成了这样的结果(消费税的 1 是 100%,苹果的价格的 1 是 1 日元。
5.4 简单层的实现
我们将要实现的计算图的乘法节点称为‘乘法层’(MulLayer),加法节点称为‘加法层’(AddLayer)
- 下一节中,我们将把构建神经网络的‘层’实现为一个类。这里说的‘层’是神经网络中功能的单位。比如,负责 sigmoid 函数的
sigmoid、负责矩阵乘积的 Affine 等,都以层为单位实现。
5.4.1 乘法层的实现
层的实现中有两个共通的方法(接口)forward()和 backward()。forward 对应正向传播,backward 对应反向传播
class MulLayer:def __init__(self):self.x = Noneself.y = Nonedef forward(self, x, y):self.x = xself.y = yout = x * yreturn outdef backward(self, dout):dx = dout * self.y # 翻转x和ydy = dout * self.xreturn dx,dy
init 方法中初始化实例变量 x 和 y,它们用于保存正向传播时的输入值。forward 接收 x 和 y 两个参数,将它们相乘后输出。backward
将从上游传来的导数(dout)乘以正向传播的翻转值,然后传给下游。
使用这个乘法层实现上图的正向传播
apple = 100
apple_num = 2
tax = 1.1# layer
mul_apple_layer = MulLayer()
mul_tex_layer = MulLayer()# forward
apple_price = mul_apple_layer.forward(apple, apple_num)
price = mul_tex_layer.forward(apple_price, tax)print(price) # 220.00000000000003# 各个变量的导数
# backward
dprice = 1
dapple_price, dtax = mul_tex_layer.backward(dprice)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)# 2.2 110.00000000000001 200
print(dapple, dapple_num, dtax)
调用 backward 的顺序和调用 forward 的顺序相反。此外,backward 的参数中需要输入‘关于正向传播时的输出变量的导数’。比如,mul_apple_layer
乘法层在正向传播时会输出 apple_price,在反向传播时,则会将 apple_price 的导数 dapple_price 设为参数。
5.4.2 加法层的实现
class AddLayer:def __init__(self):passdef forward(self, x, y):out = x + yreturn outdef backward(self, dout):dx = dout * 1dy = dout * 1return dx, dy
现在,我们使用乘法层和加法层实现图 5-17 购买 2 个苹果和 3 个橘子的例子
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)print(apple_price,orange_price,all_price,price) # 200 450 650 715# 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(dall_price) # 715
print(dapple_num, dapple, dorange, dorange_num, dtax)# 110 2.2 3.3 165 650
5.5 激活函数层的实现
我们将计算图的思路应用到神经网络中。这里,我们把构成神经网络的层实现为一个类。先实现激活函数的 ReLU 层和 Sigmoid 层
5.5.1 ReLU 层
在式子(5.8)中,如果正向传播的输入 x 大于 0,则反向传播会将上游的值原封不动地传给下游。反过来,如果正向传播时 x 小于等于
0,则反向传播中传给下游的信号将停在此处。实现 ReLU 层,其中,假定传入 forward 和 backward 的参数是 numpy 数组
class Relu:def __init__(self):self.mask = Nonedef forward(self, x):self.mask = (x <= 0)out = x.copy()# 小于0的都设为0out[self.mask] = 0def backward(self, dout):dout[self.mask] = 0dx = doutreturn dximport numpy as np# mask
x = np.array([[1.0, -0.5], [-2.0, 3.0]])
# [[ 1. -0.5]
# [-2. 3. ]]
print(x)mask = (x <= 0)
# [[False True]
# [ True False]]
print(mask)
Relu 类有实例变量 mask。这个变量是 numpy 布尔数组,它会把正向传播时的输入 x 的元素中小于等于 0 的地方保存为 True,大于零的保存为
False。
如果正向传播时的输入值小于等于 0,则反向传播的值为 0。因此,反向传播中会使用正向传播时保存的 mask,将从上游传来的 dout 的
mask 中的元素为 True 的地方设为 0。
- ReLU 层的作用就像电路开关。正向传播时,有电流通过的话,就将开关设为 ON;没有就设为 OFF。反向传播时,开关为 ON,电流会直接通过;开关为
OFF 则不会有电流通过
5.5.2 Sigmoid 层
反向传播步骤
反向传播的输出为 əL/əy * y^2 * exp(-x),这个值会传播给下游的节点。这个结果只根据正向传播时的输入 x 和输出 y
就可以算出来,可以将图 5-20 画成下面的集约化的 sigmoid 节点。
简洁版的计算图可以省略反向传播中的计算过程,计算效率更高。此外,通过对节点进行集约化,可以不用在意 sigmoid
层中琐碎的细节,而只需要专注它的输入输出。
class Sigmoid:def __init__(self):self.out = Nonedef forward(self, x):out = 1 / (1 + np.exp(-x))self.out = outreturn outdef backward(self, dout):dx = dout * (1.0 - self.out) * self.outreturn dx
5.6 Affine/Softmax 层的实现
5.6.1 Affine 层
神经网络的正向传播中,为了计算加权信号的总和,使用了矩阵的乘积运算(numpy 中的 np.dot())
X = np.random.rand(2) # 输入
W = np.random.rand(2, 3) # 输入
B = np.random.rand(3) # 输入print(X.shape) # (2,)
print(W.shape) # (2, 3)
print(B.shape) # (3,)Y = np.dot(X, W) + B
神经元的加权和可以用 Y=np.dot(X,W)+B 计算。然后,Y 经过激活函数转换后,传递给下一层。这就是神经网络正向传播的流程。此外,矩阵乘法计算的要点是使对应维度的元素个数一致。
- 神经网络的正向传播中进行的矩阵乘法计算在几何领域称为‘仿射变换’。(几何中,仿射变换包括一次线性变换和一次平移,分别对应神经网络的加权和运算与加偏置运算)。因此,这里将进行仿射变换的处理实现为‘Affine
层’
将求矩阵的乘积与偏置的和的运算用计算图表示出来。
这里,X、W、B 是矩阵(多维数组),而之前的计算图中各个节点间流动的是标量。
以矩阵为对象的反向传播,按矩阵的各个元素进行计算时,步骤和以标量为对象的计算图相同
W^T 表示 W 的转置。上面的式子用数学式表示:
5.6.2 批处理版本的 Affine 层
现在考虑 N 个数据一起进行正向传播的情况
输入 X 的形状是(N,2)
正向传播时,偏置被加到 X·W 的各个数据上,设 N=2
X_dot_W = np.array([[0, 0, 0], [10, 10, 10]])
B = np.array([1, 2, 3])
# [[ 1 2 3]
# [11 12 13]]
print(X_dot_W + B)
正向传播时,偏置加到每一个数据上,因此,反向传播时,各个数据的反向传播的值需要汇总为偏置的元素
dY = np.array([[1, 2, 3], [4, 5, 6]])
dB = np.sum(dY, axis=0)
# [5 7 9]
print(dB)
假定数据有 2 个,偏置的反向传播会对这 2 个元素的导数按元素进行求和。
下面是 Affine 函数的实现,common/layers.py 中的函数考虑到输入为张量(四维数据)的的情况,所以有所不同
class Affine:def __int__(self, W, b):self.W = Wself.b = bself.x = Noneself.dW = Noneself.db = Nonedef forward(self, x):self.x = xout = np.dot(x, self.W) + self.breturn outdef 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
5.6.3 Softmax-with-Loss 层
softmax 函数会将输入值正规化后再输出。比如手写数字识别时,softmax 层的输出如下
图中,softmax 层将输入值正规化(将输出值的和调整为 1)之后再输出。另外,因为手写数字识别要 10 分类,所以向 softmax 层的输入也有
10 个。
- 神经网络中有推理(inference)和学习两个阶段。神经网络的推理通常不使用 Softmax 层,比如刚才那个例子,会将 Affine
层的输出作为识别结果。神经网络中未被正规化的输出结果有时称为‘得分’,也就是说,神经网络的推理只需要一个答案的情况下,因为此时只对得分最大值感兴趣,所以不需要
Softmax 层。不过,神经网络的学习阶段需要 Softmax 层。
下面实现 Softmax 层。这里也包含作为损失函数的交叉熵误差(cross entropy error),所以称为‘Softmax-with-Loss’层
这里假设要进行 3 分类,Softmax 层从前面的层接收 3 个输入(得分)。将输入(a1,a2,a3)正规化,输出(y1,y2,y3)。Cross Entropy
Error 层接收 Softmax 层的输出(y1,y2,y3)和监督标签(t1,t2,t3),从这些数据中输出损失 L
注意图中反向传播的结果。Softmax 层的反向传播得到了(y1-t1, y2-t2, y3-t3)这样‘漂亮’的结果,(y1, y2, y3)是 Softmax
层的输出,(t1, t2, t3)是监督数据,所以该结果表示 Softmax 层的输出和监督标签的差分。神经网络的反向传播会把这个差分表示的误差传递给前面的层,这是神经网络学习中的重要性质。
神经网络学习的目的就是通过调整权重参数,使神经网络的输出(Softmax
的输出)接近监督标签。因此,必须将神经网络的输出和监督标签的误差高效地传递给前面的层。(y1-t1, y2-t2, y3-t3)
则直截了当地表示了当前神经网络的输出与监督标签的误差。
假设监督标签是(0,1,0),Softmax 层输出是(0.3,0.2,0.5),因为正确解标签处的概率是 0.2(20%),这个时候神经网络未能正确识别。此时,Softmax
层的反向传播传递的是(0.3,-0.8,0.5)这样一个大误差。并传给前面的层,它们将从中学习‘大’的内容。
- 使用交叉熵误差作为 softmax 函数的损失函数得到漂亮的输出结果不是偶然,而是为了得到这样的结果特意设计的交叉熵误差函数。同理,回归问题中输出层使用‘恒等函数’,损失函数使用‘平方和误差’也是出于同样的理由。
假设监督标签(0,1,0),Softmax 层的输出是(0.01,0.99,0),此时,Softmax 层反向传播传递的是(0.01,-0.01,0)
,这样小的误差,前面的层也学习到‘小’的内容。
class SoftmaxWithLoss:def __init__(self):self.loss = None # 损失self.y = None # softmax的输出self.t = None # 监督数据(ont-hot vector)def forward(self, x, t):self.t = tself.y = softmax(x) # 3.5.2 实现self.loss = cross_entropy_error(self.y, self.t) # 4.2.4 实现def backward(self, dout=1):batch_size = self.t.shape[0]dx = (self.y - self.t) / batch_sizereturn dx
注意这里的反向传播的值,将要传播的值除以批大小(batch_size)后,传递给前面的是单个数据的误差
5.7 误差反向传播法的实现
5.7.1 神经网络学习的全貌图
- 前提
神经网络中有合适的权重和偏置,调整权重和偏置以便拟合训练数据的过程称为学习。分为以下 4 个步骤 - 步骤 1(mini-batch)
从训练数据中随机选择一部分数据 - 步骤 2(计算梯度)
计算损失函数关于各个权重参数的梯度 - 步骤 3(更新参数)
将权重参数沿梯度方向进行微小的更新 - 步骤 4(重复)
重复步骤 1、2、3
之前,我们通过数值微分来计算梯度,数值微分实现简单,但是计算需要耗费很多时间,而误差反向传播法可以快速高效地计算梯度
5.7.2 对应误差反向传播法的神经网络的实现
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 OrderedDictclass 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['b1'])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 gradsdef gradient(self, x, t):# forwardself.loss(x, t)# backwarddout = 1dout = 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'].dWgrads['b1'] = self.layers['Affine1'].dbgrads['W2'] = self.layers['Affine2'].dWgrads['b2'] = self.layers['Affine2'].dbreturn grads
将神经网络的层保存为有序字典很重要,这样一来,神经网络的正向传播只需要按照添加元素的顺序调用各层的 forward
方法,而反向传播则按照相反的顺序调用各层即可。像这样模块化地构建神经网络,非常简单快捷。
5.7.3 误差反向传播法的梯度确认
数值微分的优点是简单, 一般情况下不容易出错, 所以用来做梯度确认(
确认数值微分求出的梯度结果和误差反向传播求出的结果是否一致)
import sys, ossys.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, y_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))# 结果
W1:1.8113681777006966e-10
b1:8.844119984470098e-10
W2:6.888036763008642e-08
b2:1.378154677281196e-07
5.7.4 使用误差反向传播法的学习
import sys, ossys.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 * gradloss = 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)
5.8 小结
- 通过使用计算图,可以直观地把握计算过程。
- 计算图的节点是由局部计算构成的。局部计算构成全局计算。
- 计算图的正向传播进行一般的计算。通过计算图的反向传播,可以计算各个节点的导数。
- 通过将神经网络的组成元素实现为层,可以高效地计算梯度(反向传播法)
- 通过比较数值微分和误差反向传播法的结果,可以确认误差反向传播法的实现是否正确(梯度确认)。