目录
前言
计算图
计算图求解示例
计算图的优点
反向传播
思考一个问题
链式法则
计算图的反向传播
链式法则和计算图
加法节点的反向传播
乘法节点的反向传播
“购买水果”问题的反向传播
激活函数(层)的反向传播
激活函数ReLU的反向传播
激活函数Sigmoid的反向传播
Affine/softmax激活函数的反向传播
Affine层
softmax-with-loss层的反向传播
误差反向传播法的Python实现
乘法层的Python实现
加法层的Python实现
购买水果问题的Python实现
激活函数层的Python实现
ReLU层的Python实现
sigmoid层的Python实现
Affine层的Python实现
softmax-with-loss层的Python实现
误差反向传播法的Python总体实现
小结
计算梯度的传统方法一般采用基于数值微分实现,如下式:
虽然数值微分方法比较直观、简单、易于理解,但是计算比较费时间,不适合需频繁计算导数的场合,比如多层神经网络中权重参数的梯度计算。前面我们讲过利用数值微分计算了神经网络中的损失函数关于权重参数的梯度,而本专题我们将介绍“误差反向传播法”,实现损失函数关于权重参数梯度的高效计算。本主题将从简单的问题开始,逐步深入,最终直达误差反向传播法。
理解误差反向传播法,一般有基于数学式的方法和基于计算图的方法。前者比较常见、简洁和严密,但是不直观,因此笔者选择了更加直观、易于理解的计算图来讲解误差反向传播法。
计算图,即将计算过程用图形表示出来,当然,这里的图形指的是具有数据结构(和流程图类似的图形),一般有节点和连接节点的边(线)组成,如图1所示。
图1中圆圈O表示节点,圈内的符号表示计算符号(比如加减乘除),箭头的指向表示节点计算结果的传递方向(图1为从左向右传播,即正向传播),直线上方一般放置中间计算结果(如100)。下面我们基于计算图来分析一个具体的示例。
问题描述:
小明在超市买了2个苹果、3个橘子。其中,苹果每个100日元,橘子每个150日元。消费税是10%,请计算支付金额。
首先,我们采用传统的数学思路来计算支付金额:2个苹果,单价为100日元,因此购买苹果花了200日元;3个橘子,单价150日元,因此,够买橘子花了450日元;因此购买苹果和橘子一共花了650日元,由于消费税是10%,所以支付金额为:650+650·10%=715日元。
下面我们采用基于计算图来计算支付金额,先直接给出计算图如图2所示,再分析。
由计算图分析得到的结果和传统方法分析得到的结果一样,均为715日元。现在我们结合图2来分析该计算图,计算图的输入有苹果单价、苹果数量、橘子单价、橘子数量、消费税,中间的计算结果均放置(保存)在直线上面,这里需要强调的是初始输入量的值我们也视为中间结果,比如苹果单价视为输入量(实际等效于一个变量),而100为中间变量(等效于一个实例值)。改图一共有4个节点,其中3节点运算为乘法运算,1个节点运算为加法运算。计算方向为从左至右,这是一种正方向的传播,简称为正向传播。指向节点的箭头可以视为输入,比如上图中输入至加法节点的有两个输入量(苹果总价和橘子总价),当然输入量是没有限制的。图中消费税的中间计算过程为1.1(1+10%),这么做的目的主要是可以直接和水果总价进行乘法运算,当然读者可以自行设计图2中的消费税这一节点。
我们不难理解,正向传播就是从计算图出发点到结束点的传播,既然有正向传播,那么应该也有反向传播。是的,从右向左的传播就是我们后面将重点关注的反向传播。
通过上面给出的计算图求解实际问题的示例可知,计算图的特征是可以通过传递“局部计算”获得最终结果。所谓“局部”,指的是无论全局发生了什么,都能只根据与自己相关的信息输出接下来的结果。假设上面的例子中,购买水果总共花费了650日元,我们不关心650日元是通过什么样的计算得到的,只关心把650日元作为该节点的输出并和其他节点进行运算。换句话说,各个节点处只需进行与自己有关的计算,不用考虑全局。无论全局是多么复杂的计算,都可以通过局部计算使各个节点致力于简单的计算,从而简化问题。
另一个优点是,利用计算图可以将中间的计算结果全部保存起来(比如200、450、650....),为反向传播的计算提供已知数据。
在上面的问题中,我们计算了购买苹果和橘子时加上消费税最终需要支付的金额。假设我们想知道苹果价格的上涨会在多大程度上影响最终的支付金额,即求“支付金额关于苹果价格的导数”。设苹果的价格为,支付金额为,则相当于求。这个导数的值表示当苹果的价格稍微上涨时,支付金额会增加多少。
首先,我们利用传统的数学解题思路来求解,假设苹果价格上涨了日元,支付金额增加了日元,则有:
通过数学解题思路,我们得到了支付金额关于苹果的价格的导数为2.2,即苹果价格上涨1日元,则最终的支付金额将会增加2.2日元。现在我们先直接给出利用反向传播法分析得到的结果。图中加粗的箭头表示反向传播,箭头下面的结果表示“局部导数”,也就是说,反向传播传递的是导数。从图中可知,支付金额关于苹果单价的导数的值是2.2,这和数学解题思路得到的答案一样。当然,除了求关于苹果的价格的导数,其他的比如支付金额关于消费税的导数、支付金额关于橘子价格的导数等问题也可以采用同样的方式算出来。
从图3中还可发现,计算中途求得的导数的结果(比如1.1)可以被共享,从而高效地计算多个导数。因此,计算图可以通过正向传播和反向传播高效地计算各个变量的导数值。反向传播传递导数的原理,是基于链式法则。
反向传播将局部导数从右到左进行传递的原理是基于链式法则,要理解链式法则,我们还得从复合函数说起。复合函数是由多个函数构成的函数。比如是由下面的两个式子构成的。
(1)
这里,链式法则是关于符合函数的导数的性质,如下:
如果某个函数由复合函数表示,则该复合函数的导数可以用构成复合函数的各个函数的导数的乘积表示。
例如,可以用和的乘积表示。即:
现在使用链式法则,我们来求式(1)的导数。首先要求它的局部导数:
所以的导数为:
假设存在的计算,则这个计算的反向传播如图4所示。
如图所示,反向传播的计算顺序是:将信号乘以节点的局部导数,然后将结果传递给下一个节点。这里所说的局部导数是指正向传播中的导数,也就是,比如,则局部导数为。把这个局部导数乘以上游传过来的值(本例中的),然后传递给前面的节点。(这里给大家说一下,如果是神经网络,那么最上游应该是损失函数)。
这就是反向传播的计算程序,结合链式法则可以高效地求出多个导数的值。
现在我们用计算图的方法把式(1)的链式法则表示出来。如图5所示,这里我们用“**2”表示平方运算。
反向传播时,“**2”节点的输入是,将其乘以局部导数(因为正向传播时输入是,输出是,所以这个节点的局部导数是),然后传递给下一个节点。这里需要提醒的是,反向传播最开始的信号在前面的数学式中没有出现,因为。根据链式法则,最左边的反向传播结果成立,对应于“关于的导数”。
到这里,读者也许会生产疑问:反向传播过程中的数字1是怎么得到的,下面的内容将为大家解释这个问题。
加法节点指的是节点运算为加法运算,以 为例,则关于和的导数为
假设 通过某种运算的结果为,则加法节点的正向传播和反向传播的计算图如下:
我们通过解析性求导,得到关于和的导数均为1,因此计算图中,反向传播将上游传过来的导数值(本例中是,因为正向传播的输入为,输出为)乘以1,然后传向下游。也就是说,加法节点的反向传播只乘以1,所以输入的值会原封不动地流向下一个节点。
假设有,则关于和的导数为:
用计算图表示乘法节点的正向传播和反向传播如图8所示。
乘法的反向传播会将上游的值乘以正向传播时的输入信号的'翻转值"后传递给下游。翻转值表示一种翻转关系,正向传播时信号是的话,反向传播时则是;正向传播时信号是的话,反向传播时则是。这里需要提醒大家的是,加法的反向传播只是将上游的值传递给下游,并不需要正向传播的输入信号。而乘法的反向传播需要正向传播时的输入信号值,因此要实现乘法节点的反向传播时,需要保存正向传播的输入信号。
现在我们回到前面给出的问题“购买水果,求支付金额”,因为我们已经介绍了加法和乘法的反向传播,所以我们试着来分析“购买水果”的反向传播,即求包括金额关于苹果单价的导数等其他变量的导数。读者只需记住两点:加法的反向传播将上游传递来的值会原封不动地传递给下游;乘法的反向传播会将输入信号翻转后传递给下游。因此“购买水果”的反向传播的计算图如图9所示。
可知,苹果的价格的导数为2.2,橘子的价格的导数为3.3(说明橘子的价格的波动比苹果价格的波动对最终的支付金额的影响更大),消费税的导数是650(消费税的1是100%,水果的价格的1是1日元,所以才形成了这么大的消费税的导数)。
激活函数ReLU的表达式如下式(3):
则关于的导数如式(4):
由式(4)可知,如果正向传播时的输入大于0,则反向传播会将上游的值原封不动地传递给下游。如果正向传播时的小于等于0,则反向传播中传给下游的信号将停止在此处,即反向传播的值为0。用计算图表示如图9所示。
sigmoid函数的表达式如式(5)所示。
其计算图如图10所示。
说明一下,式(5)的计算由局部计算的传播构成,“exp”节点会进行的计算,“/”会进行的计算。下面我们来分析图10的计算图的反向传播。
第一步:
(6)
可知,“/”节点运算时的反向传播会将上游的值乘以(正向传播的输出的平方乘以-1后的值)后,再传给下游。计算图如图11所示。
第二步:
“+”节点将上游的值原封不动地传给下游。计算图如图12所示。
第三步:
“exp”节点表示,则它的导数如式(7)所示。
可知,“exp”节点的反向传播将上游的值乘以正向传播时的输出(这个例子的输出是)后,再传给下游。计算图如图13所示。
第四步:
“x”节点的反向传播将正向传播时的值翻转后做乘法运算,因此计算图如图14所示。
综上,sigmoid函数的反向传播的输出为,这个值会传递给下游的节点。我们发现,该值可只根据正向传播时的输入和输出就可以计算出来。所以,sigmoid函数的反向传播可以简化为如图15所示的计算图。
简洁后的反向传播可以忽视中间计算过程,因此大幅度提高了计算效率。其实,我们可以对作进一步的处理,如式(8)所示。
因此,sigmoid函数的反向传播只需根据正向传播的输出就能计算出来,这里我们选择图16所示的计算图作为sigmoid函数的反向传播的最终计算图。
在前面的专题讲解中,我们介绍了计算加权信号的总和,即输入信号与权重的乘积之和,再加上偏置。在实现过程中,我们利用了矩阵的乘积运算(Numpy库中的np.dot())来计算了神经元(节点)加权和,即,然后将经激活函数转换后,传递给下一层。这就是神经网络的正向传播的流程。一般地,神经网络的正向传播涉及矩阵的乘积运算(信号的加权和计算)的过程(变换),我们称为Affine层
Affine层:
神经网络的正向传播中进行的矩阵的乘积运算在几何学领域被称为“仿射变换”,它包括一次线性变换和一次平移,分别对应神经网络的加权和运算与加偏置预算。在这里,我们将进行仿射变换的处理实现为“Affine”层。
图17为神经网络正向传播的Affine层的计算图,我们需要注意的是,图中的变量均为矩阵形式,所以在进行矩阵运算时,要注意矩阵的形状是否正确。这里我们假设了各变量矩阵的形状,注意这里的计算图中各节点间传递的是矩阵,不是标量。
通过Affine层的正向传播,我们如何求它的反向传播呢?在这里我们需要记住两点:第一点是、和均为变量,不是常量;第二点是节点中的运算步骤和以标量为对象的计算图相同。因此,我们很容易得到如图18所示的反向传播的计算图。
图18中的反向传播的加法节点将上游传递来的值原封不动地传递给下游。"dot"节点可以看做乘法节点,但又有区别,即它是矩阵乘法,所以在考虑将上游传递来的值乘以正向传播的翻转值的同时,还要注意矩阵的形状。这里我们可以肯定的是:为和的某种乘积关系,而为和的某种乘积关系。因此,仔细分析可知:
为的转置,比如的形状为(2,3),则的形状就是(3,2)。所以图18中Affine层的反向传播的完整的计算图如图19所示:
当然,这里介绍的Affine层的输入是以单个数据为对象的,如果我们将个数据样本(假设数据的特征有2个,则的形状为(N,2))一起进行正向传播,即批版本的Affine层。那么它的计算图如图20所示。
神经网络涉及输入信号与权重参数的乘积的加权和(即Affine层)、激活函数、输出层激活函数(softmax)和损失函数(主要使用交叉熵误差)。在这之前,我们已经介绍了Affine层和激活函数的反向传播,下面我们将softmax层和损失函数一起作为对象来分析它们的反向传播的计算图。在这之前,我们以手写数字识别为例,回顾神经网络的推理过程。示意图如图21所示。
图21中,softmax层将输入值正规化(输出值的和调整为1)之后再输出,此外,手写数字识别要进行10类分类,所以向softmax层的输入也有10个。输入图像为“0”,得分为10.1分,经softmax层转换为0.991。
一般情况下,我们会把softmax层和损失函数一起考虑,由于softmax-with-loss层比较复杂,这里我们直接给出其正向和反向传播的简易计算图如图22所示。具体的分析过程后面我们会专门花一个专题来讲。
这里我们重点关注反向传播的结果。softmax层的反向传播得到了()这样漂亮的结果。由于()是softmax层的输出,是监督数据,所以()是softmax层的输出和监督标签的差分。神经网络的反向传播会把这个差分表示的误差传递给前面的层,这是神经网络学习中的重要性质。
神经网络的学习的目的就是通过调整权重参数,使神经网络的输出(softmax层的输出)接近监督标签。因此,必须将神经网络的输出与监督标签的误差高效地传递给前面的层。前面的()直截了当地表示了当前神经网络的输出与监督标签的误差。比如监督标签(0,1,0),softmax层的输出是(0.3,0.2,0.5)。由于正确解标签处的概率是20%,这时候神经网络未能进行正确的识别。此时,softmax层的反向传播传递的是(0.3,-0.8,0.5)这样一个大的误差。这个大的误差会向前面的层传播,所以softmax层前面的层会从这个大的误差中学习到“大”的内容。
使用交叉熵误差作为softmax函数的损失函数后,反向传播得到()这样漂亮的结果。实际上,这样的结果并不是偶然的,而是为了得到这样的结果,特意设计了交叉熵误差函数。
这里我们把乘法节点的计算图用“乘法层”(MulLayer),在Python中用类表示,类中有两个方法(函数),正向传播forward(),和反向传播backward()。代码如下:
# coding: utf-8
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 #翻转x和y
dy = dout * self.x
return dx, dy
代码中,__init__()会初始化实例变量x和y,它们主要用来保存正向传播时的输入值。forward()接收x和y两个参数,将它们相乘后输出。backward()将从上游传来的导数dout乘以正向传播的翻转值,然后传给下游。
# coding: utf-8
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
由于加法节点的反向传播不需要输入值,所以__init()__中无特意执行语句。forward()接收x和y,将它们相加后输出。backword()将上游传来的导数dout原封不动地传递给下游。
# coding: utf-8
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)) #715
print("dApple:", dapple) #2.2
print("dApple_num:", int(dapple_num)) #110
print("dOrange:", dorange) #3.3
print("dOrange_num:", int(dorange_num)) #165
print("dTax:", dtax) #650
# coding: utf-8
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
需要提醒大家的是,神经网络的层的实现中,一般假定forward()和backward()的参数是numPy数组。代码中变量mask是由true/false构成的NumPy数组,它会正向传播时的输入x的元素中小于等于0的地方保存为true,大于0的地方保存为false。
class Sigmoid:
def __init__(self):
self.out = None
def forward(self, x):
out = sigmoid(x)
self.out = out
return out
def backward(self, dout):
dx = dout * (1.0 - self.out) * self.out
return dx
正向传播时将输出保存到了变量out中,反向传播时,使用该变量out进行计算。
class Affine:
def __init__(self, W, b):
self.W =W
self.b = b
self.x = None
self.original_x_shape = None
# 权重和偏置参数的导数
self.dW = None
self.db = None
def forward(self, x):
# 对应张量
self.original_x_shape = x.shape
x = x.reshape(x.shape[0], -1)
self.x = x
out = np.dot(self.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)
dx = dx.reshape(*self.original_x_shape) # 还原输入数据的形状(对应张量)
return dx
需要注意的是,Affine的实现考虑了输入数据为张量(四维数据)的情况。
# coding: utf-8
import numpy as np
def softmax(x):
if x.ndim == 2:
x = x.T
x = x - np.max(x, axis=0)
y = np.exp(x) / np.sum(np.exp(x), axis=0)
return y.T
x = x - np.max(x) # 溢出对策
return np.exp(x) / np.sum(np.exp(x))
def cross_entropy_error(y, t):
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
# 监督数据是one-hot-vector的情况下,转换为正确解标签的索引
if t.size == y.size:
t = t.argmax(axis=1)
batch_size = y.shape[0]
return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size
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]
if self.t.size == self.y.size: # 监督数据是one-hot-vector的情况
dx = (self.y - self.t) / batch_size
else:
dx = self.y.copy()
dx[np.arange(batch_size), self.t] -= 1
dx = dx / batch_size
return dx
神经网络中有合适的权重和偏置,调整权重和偏置以便拟合训练数据的过程称为学习。神经网络的学习一般分为以下四个步骤:
(1)从训练数据中随机选择一部分数据
(2)计算损失函数关于各个权重参数的梯度(采用误差反向传播法)
(3)将权重参数沿梯度方向进行微小的更新
(4) 重复步骤1至步骤3
下面的代码完成了2层神经网络的实现
# coding: utf-8
import numpy as np
from collections import OrderedDict
# coding: utf-8
import numpy as np
#这里被调用的部分函数可在之前的专题中查找
def numerical_gradient(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x)
it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
while not it.finished:
idx = it.multi_index
tmp_val = x[idx]
x[idx] = float(tmp_val) + h
fxh1 = f(x) # f(x+h)
x[idx] = tmp_val - h
fxh2 = f(x) # f(x-h)
grad[idx] = (fxh1 - fxh2) / (2*h)
x[idx] = tmp_val # 还原值
it.iternext()
return grad
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
代码中使用了OrderDict()函数,它是有序字典,即它可以记住向字典里添加元素的顺序。因此,神经网络的正向传播只需按照添加元素的顺序调用各层的forward()方法就可以完成处理,而反向传播只需要按照相反的顺序调用各层即可。
我们构造了神经网络之后,就可以进行学习了,在前面的专题我们讲过神经网络的学习,其中介绍了用数值微分的方法求梯度,而这里我们则采用误差反向传播法求梯度。除此之外,程序几乎一样。神经网络的学习的Python实现如下:
# 读入数据
(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)
本章我们介绍了计算图,并使用计算图介绍了神经网络的误差反向传播法,并以层为单位实现了神经网络中的处理。通过将数据正向和反向地传播,可以高效地计算权重参数的梯度。
欢迎关注微信公众号“Python生态智联”,学知识,享生活!