深度学习笔记——误差的反向传播

小鸡
阅读865 喜欢6 算法 更新2019-5-20

通过数值微分计算神经网络的损失函数关于权重参数的梯度,虽然简单也容易实现,但是缺点是计算上比较费时间。而误差的反向传播法可以高效地计算梯度。

通过计算图来理解误差反向传播

什么是计算图

计算图将计算过程用图形表示出来。这里说的图形是数据结构图,通过多个节点和边表示(连接节点的直线称为“边”)。

一个例子

问题1:你在超市买了2个100日元一个的苹果,消费税是10%,请算支付金额

计算图通过节点和箭头表示计算过程。节点用○表示,○中是计算的内容。将计算的中间结果写在箭头的上方,表示各个节点的计算结果从左向右传递。用计算图解问题1,求解过程如图所示


开始时,苹果的100日元流到“×2”节点,变成200日元,然后被传递给下一个节点。接着,这个200日元流向“× 1.1”节点,变成220日元。因此,从这个计算图的结果可知,答案为220日元。


问题2:你在超市买了2个苹果、3个橘子。其中,苹果每个100日元,橘子每个150日元。消费税是10%,请计算支付金额。


综上,用计算图解题的情况下,需要按如下流程进行。

  1. 构建计算图。
  2. 在计算图上,从左向右进行计算。

这里的第2歩“从左向右进行计算”是一种正方向上的传播,简称为正向传播。正向传播是从计算图出发点到结束点的传播。

那么,可以联想到,反向传播应该就是在计算图上反方向传播数据,而这将会在导数计算中返回很大作用。

下面考虑“支付金额关于苹果地价格地导数”,可以通过计算图地反向传播求出来。


如上图所示,反向传播使用反向箭头(粗线)表示,反向传播的是局部导数,将导数地值写在箭头下方。

上图地意思是:支付金额关于苹果价格地导数为2.2。也就是说,如果苹果地价格上涨1元,那么最终地支付金额上涨2.2元。

链式法则与复合函数

高数基础知识,跳过

链式法则与计算图

比如,z = (x + y)^2是下面的两个式子构成的。

  • z = t^2
  • t = x + y

该计算式的反向传播如图所示


由链式法则计算z对于x的导数有

  • z’(t) = 2t
  • z’(x) = 2(x+y)


反向传播

加法节点的反向传播

加法节点的反向传播只是将输入信号原封不动输出到下一个节点


乘法节点的反向传播

乘法的反向传播会将上游的值乘以正向传播时的输入信号的“翻转值”后传递给下游。翻转值表示一种翻转关系,如下图所示,正向传播时信号是x的话,反向传播时则是y;正向传播时信号是y的话,反向传播时则是x。


另一个例子


用python来简单实现计算图

下面实现乘法层,两个乘数作为成员变量,一个成员函数forward()作为正向传播函数,一个成员函数backward()作为反向传播函数。

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

激活函数的实现

将计算图的思路应用到神经网络中,把构成神经网络的层实现为一个类,以激活函数的ReLU层和sigmoid层为例。

ReLU层

激活函数ReLU表达式如下

  • y = x (x>0)
  • y = 0 (x<=0)

ReLU的计算图如下


python代码如下。
ReLU有成员变量mask,这个变量是由truefalse构成的Numpy数组,将正向传播时小于0的地方保存为true,其他地方为false

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

sigmoid层的实现

y = 1 / (1 + exp(-x))

计算图表示如下


而它的反向传播如下图


省略中间过程,将其看作一个变换,结果相同,但是减少计算步骤,可以不用在意Sigmoid层中琐碎的细节,而只需要专注它的输入和输出,这一点也很重要。其输入输入的正反向传播可以简化如下



最后可以得到最终版的sigmoid层计算图如下


用python代码实现如下

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

Affine/Softmax层的实现

神经网络的正向传播中进行的矩阵的乘积运算在几何学领域被称为“仿射变换”A。因此,这里将进行仿射变换的处理实现为“Affine层”。

神经网络的正向传播,计算加权信号的总和可以用Y = np.dot(X, W) + B计算出来,然后Y经过激活函数转换后,传递给下一层,这是神经网络正向传播的流程。

需要注意的是之前介绍的计算图中,各节点之间流动的是标量,而现在流动的是矩阵。那么现在考虑这个计算图的反向传播。

从数学公式上(X为输入矩阵,W为权重矩阵,B为偏置矩阵,L为损失函数,WT为矩阵W的转置)

Y = XW + B
L = loss(Y)

dL/dX = (dL/dY)·WT
dL/dW = XT·(dL/dY)

反向传播的计算图如下,这里假设X的形状为(2,),W的形状为(2,3)


同样的,也可以获得批处理版本的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]])

正向传播时,偏置会被加到每一个数据(第1个、第2个……)上。因此,反向传播时,各个数据的反向传播的值需要汇总为偏置的元素。用代码表示的话,如下所示。

>>> 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])

上面的例子中,批处理的偏置的反向传播会对N个数据进行求和。

综上所诉,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层的输出如下


神经网络中进行的处理有推理(inference)和学习两个阶段。神经网络的推理通常不使用 Softmax层。当神经网络的推理只需要给出一个答案的情况下,因为此时只对得分最大值感兴趣,所以不需要Softmax层。不过,神经网络的学习阶段则需要 Softmax层。

实现Softmax层。考虑到这里也包含作为损失函数交叉熵误差(cross entropy error),所以称为“Softmax-with-Loss层”。Softmax-withLoss层(Softmax函数和交叉熵误差)的计算图如图,略复杂。


这里假设要进行3类分类,从前面的层接收3个输入(得分)。Softmax层将输入(a1, a2, a3)正规化,输出(y1,y2, y3)。Cross Entropy Error层接收Softmax的输出(y1, y2, y3)和教师标签(t1,t2,t3),从这些数据中输出损失L


由于(y1, y2, y3)是Softmax层的输出,(t1, t2, t3)是监督数据,所以(y1 − t1, y2 − t2, y3 − t3)是Softmax层的输出和教师标签的差分。神经网络的反向传播会把这个差分表示的误差传递给前面的层,这是神经网络学习中的重要性质

使用交叉熵误差作为softmax函数的损失函数后,反向传播得到(y1 − t1, y2 − t2, y3 − t3)这样“简约”的结果。实际上,这样“简约”的结果并不是偶然的,而是为了得到这样的结果,特意设计了交叉熵误差函数。回归问题中输出层使用“恒等函数”,损失函数使用“平方和误差”,也是出于同样的理由

Softmax-with-Loss层的python代码实现

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]
dx = (self.y - self.t) / batch_size
return dx

误差反向传播法的实现

将上面介绍的各个层的类组装起来,就可以构建可以反向传播的神经网络。步骤如下

前提
神经网络中有合适的权重和偏置,调整权重和偏置以便拟合训练数据的过程称为学习。神经网络的学习分为下面4个步骤。误差反向传播法会在步骤2中出现

步骤1(mini-batch)
从训练数据中随机选择一部分数据。

步骤2(计算梯度)
计算损失函数关于各个权重参数的梯度。

步骤3(更新参数)
将权重参数沿梯度方向进行微小的更新。

步骤4(重复)
重复步骤1、步骤2、步骤3。

数值微分虽然实现简单,但是计算要耗费较多的时间。和需要花费较多时间的数值微分不同,误差反向传播法可以快速高效地计算梯度。

至于为什么?因为数值微分通过计算两次在输入值X附近的近似值的输出值之差,并除于这两个近似值的距离来计算近似导数。数学公式如下。

f‘(x) = [f(x + dx) - f(x - dx)] / 2dx

这样的话,就意味着需要进行多两次正向传输才可以计算出这次的误差导数。即f(x + dx)和 f(x - dx)的计算。这样往往很耗费时间,而反向误差计算只有一次,而且反向误差计算一次的计算量往往还很小,就像上面只需要计算(y-t),这点计算量相比数值微分的计算小太多。

对应误差反向传播法的神经网络的实现

这个的源码还没自己动手写就不上代码了,书上给的思路如下

TwoLayerNet类的实例变量

实例变量 说明
params 保存神经网络的参数的字典型变量。params[‘W1’]是第1层的权重,params[‘b1’]是第1层的偏置。params[‘W2’]是第2层的权重,params[‘b2’]是第2层的偏置
layers 保存神经网络的层的有序字典型变量。以layers[‘Affine1’]、layers[‘ReLu1’]、layers[‘Affine2’]的形式,通过有序字典保存各个层
lastLayer 神经网络的最后一层。本例中为SoftmaxWithLoss层

TwoLayerNet类的方法

方法 说明
__init__(self, input_size,hidden_size,output_size,weight_init_std) 进行初始化。参数从头开始依次是输入层的神经元数、隐藏层的神经元数、输出层的神经元数、初始化权重时的高斯分布的规模
predict(self, x) 进行识别(推理)。参数x是图像数据
loss(self, x, t) 计算损失函数的值。参数X是图像数据、t是正确解标签
accuracy(self, x, t) 计算识别精度
numerical_gradient(self, x, t) 通过数值微分计算关于权重参数的梯度(同上一章)
gradient(self, x, t) 通过误差反向传播法计算关于权重参数的梯度

:本文为斋藤康毅的《深度学习入门:基于Python的理论与实现》片段摘抄与学习笔记