【学习笔记】深度学习入门:基于Python的理论与实现-误差反向传播法

CONTENTS

    • 五、误差反向传播法
      • 5.1 计算图
      • 5.2 链式法则
      • 5.3 反向传播
      • 5.4 简单层的实现
      • 5.5 激活函数层的实现
      • 5.6 Affine/Softmax层的实现
      • 5.7 误差反向传播法的实现

五、误差反向传播法

5.1 计算图

先引入一个很简单的问题:在超市买了 2 2 2 100 100 100元一个的苹果,消费税是 10 % 10\% 10%,请计算支付金额。我们画出计算图如下:

在这里插入图片描述

接着进行简单的修改,如下图所示:

在这里插入图片描述

现在我们换个问题:在超市买了 2 2 2个苹果、 3 3 3个橘子。其中,苹果每个 100 100 100元,橘子每个 150 150 150元。消费税是 10 % 10\% 10%,请计算支付金额。同样,我们画出相应的计算图如下:

在这里插入图片描述

在计算图上,我们是从左向右进行计算的,是一种正方向上的传播,简称为正向传播 f o r w a r d   p r o p a g a t i o n forward\ propagation forward propagation)。既然有正向传播这个名称,当然也可以考虑反向(从图上看的话,就是从右向左)的传播。实际上,这种传播称为反向传播 b a c k w a r d   p r o p a g a t i o n backward\ propagation backward propagation)。反向传播将在接下来的导数计算中发挥重要作用。

计算图的特征是可以通过传递局部计算获得最终结果。即无论全局发生了什么,都能只根据与自己相关的信息输出接下来的结果。比如,在超市买了 2 2 2个苹果和其他很多东西,可以用以下计算图表示:

在这里插入图片描述

这里的重点是,各个节点处的计算都是局部计算。这意味着,例如苹果和其他很多东西的求和运算( 4000 + 200 = 4200 4000+200=4200 4000+200=4200)并不关心 4000 4000 4000这个数字是如何计算而来的,只要把两个数字相加就可以了。此外,计算图的另一个优点是可以将中间的计算结果全部保存起来。

假设我们想知道苹果价格的上涨会在多大程度上影响最终的支付金额,即求支付金额关于苹果的价格的导数。设苹果的价格为 x x x,支付金额为 L L L,则相当于求 ∂ L ∂ x \frac {\partial L}{\partial x} xL,这个导数的值表示当苹果的价格稍微上涨时,支付金额会增加多少。

这个值可以通过计算图的反向传播求出来。先看一下结果,如下图所示:

在这里插入图片描述

从这个结果中可知,“支付金额关于苹果的价格的导数”的值是 2.2 2.2 2.2。这意味着,如果苹果的价格上涨 1 1 1元,最终的支付金额会增加 2.2 2.2 2.2元。

5.2 链式法则

反向传播将局部导数向正方向的反方向(从右到左)传递,其原理是基于链式法则的。我们先来看一个使用计算图的反向传播的例子。假设存在 y = f ( x ) y=f(x) y=f(x)的计算,其反向传播如下图所示:

在这里插入图片描述

反向传播的计算顺序是,将信号E乘以节点的局部导数 ∂ y ∂ x \frac {\partial y}{\partial x} xy,然后将结果传递给下一个节点。这里所说的局部导数是指正向传播中 y = f ( x ) y=f(x) y=f(x)的导数。

介绍链式法则时,我们需要先从复合函数说起。复合函数是由多个函数构成的函数。比如, z = ( x + y ) 2 z=(x+y)^2 z=(x+y)2是由以下两个式子构成的:

在这里插入图片描述

链式法则是关于复合函数的导数的性质,定义为:如果某个函数由复合函数表示,则该复合函数的导数可以用构成复合函数的各个函数的导数的乘积表示。

以上式为例, ∂ z ∂ x \frac {\partial z}{\partial x} xz可以用 ∂ z ∂ t \frac {\partial z}{\partial t} tz ∂ t ∂ x \frac {\partial t}{\partial x} xt的乘积表示,即:

在这里插入图片描述

现在我们尝试将以上链式法则的计算用计算图表示出来:

在这里插入图片描述

在这里插入图片描述

根据链式法则, ∂ z ∂ z ∂ z ∂ t ∂ t ∂ x = ∂ z ∂ t ∂ t ∂ x = ∂ z ∂ x \frac {\partial z}{\partial z}\frac {\partial z}{\partial t}\frac {\partial t}{\partial x}=\frac {\partial z}{\partial t}\frac {\partial t}{\partial x}=\frac {\partial z}{\partial x} zztzxt=tzxt=xz成立,对应“ z z z关于 x x x的导数”。也就是说,反向传播是基于链式法则的。

5.3 反向传播

首先来考虑加法节点的反向传播。这里以 z = x + y z=x+y z=x+y为对象,观察它的反向传播。 z = x + y z=x+y z=x+y的导数可由下式(解析性地)计算出来:

在这里插入图片描述

我们假定一个最终输出值为 L L L的大型计算图。 z = x + y z=x+y z=x+y的计算位于这个大型计算图的某个地方,从上游会传来 ∂ L ∂ z \frac {\partial L}{\partial z} zL的值,并向下游传递 ∂ L ∂ x \frac {\partial L}{\partial x} xL ∂ L ∂ y \frac {\partial L}{\partial y} yL,如下图所示:

在这里插入图片描述

用计算图表示的话,如下图所示,因为加法节点的反向传播只乘以 1 1 1,所以输入的值会原封不动地流向下一个节点:

在这里插入图片描述

接下来,我们看一下乘法节点的反向传播。这里我们考虑 z = x y z=xy z=xy。这个式子的导数用下式表示:

在这里插入图片描述

乘法的反向传播会将上游的值乘以正向传播时的输入信号的“翻转值”后传递给下游,如下图所示:

在这里插入图片描述

假设有 10 × 5 = 50 10\times 5=50 10×5=50这一计算,反向传播时,从上游会传来值 1.3 1.3 1.3。用计算图表示的话如下图所示:

在这里插入图片描述

乘法的反向传播需要正向传播时的输入信号值。因此,实现乘法节点的反向传播时,要保存正向传播的输入信号。

使用反向传播计算最初的两个问题时,可画出计算图如下:

在这里插入图片描述

在这里插入图片描述

5.4 简单层的实现

我们把要实现的计算图的乘法节点称为“乘法层”(MulLayer),加法节点称为“加法层”(AddLayer)。层的实现中有两个共通的方法(接口)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  # 翻转x和y
		dy = dout * self.x
		return dx, dy

现在我们使用MulLayer实现前面的购买苹果的例子:

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

接下来,我们实现加法节点的加法层:

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

使用MulLayerAddLayer实现前面的购买苹果和橘子的例子:

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)  # 715
print(dapple_num, dapple, dorange, dorange_num, dtax)  # 110 2.2 3.3 165 650

5.5 激活函数层的实现

这里,我们把构成神经网络的层实现为一个类。先来实现激活函数的 R e L U ReLU ReLU层和 S i g m o i d Sigmoid Sigmoid层。 R e L U ReLU ReLU如下式所示:

在这里插入图片描述

可以求出 y y y关于 x x x的导数如下:

在这里插入图片描述

因此 R e L U ReLU ReLU层的计算图如下图所示:

在这里插入图片描述

使用Python实现代码如下:

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类有实例变量mask。这个变量是由True/False构成的NumPy数组,它会把正向传播时的输入 x x x的元素中小于等于 0 0 0的地方保存为True,其他地方(大于 0 0 0的元素)保存为False

接下来,我们来实现 S i g m o i d Sigmoid Sigmoid函数,其公式如下:

在这里插入图片描述

其正向传播的计算图如下图所示,除了 “ × ” “\times” × “ + ” “+” +节点外,还出现了新的 “ e x p ” “exp” exp “ / ” “/” /节点。 “ e x p ” “exp” exp节点会进行 y = e x p ( x ) y=exp(x) y=exp(x)的计算, “ / ” “/” /节点会进行 y = 1 x y=\frac {1}{x} y=x1的计算:

在这里插入图片描述

现在我们来分析其反向传播流程:

(1) “ / ” “/” /节点表示 y = 1 x y=\frac {1}{x} y=x1它的导数可以解析性地表示为下式:

在这里插入图片描述

反向传播时,会将上游的值乘以 − y 2 -y^2 y2后,再传给下游,计算图如下图所示:

在这里插入图片描述

(2) “ + ” “+” +节点将上游的值原封不动地传给下游,计算图如下图所示:

在这里插入图片描述

(3) “ e x p ” “exp” exp节点表示 y = e x p ( x ) y=exp(x) y=exp(x),它的导数由下式表示:

在这里插入图片描述

计算图中,上游的值乘以正向传播时的输出(这个例子中是 e x p ( − x ) exp(-x) exp(x))后,再传给下游,计算图如下图所示:

在这里插入图片描述

(4) “ × ” “×” ×节点将正向传播时的值翻转后做乘法运算。因此,这里要乘以 − 1 -1 1。至此, S i g m o i d Sigmoid Sigmoid层整体的计算图如下图所示:

在这里插入图片描述

从上图的结果可知,反向传播的输出为 ∂ L ∂ y y 2 e x p ( − x ) \frac {\partial L}{\partial y}y^2exp(-x) yLy2exp(x),这个值会传播给下游的节点。这里要注意, ∂ L ∂ y y 2 e x p ( − x ) \frac {\partial L}{\partial y}y^2exp(-x) yLy2exp(x)这个值只根据正向传播时的输入 x x x和输出 y y y就可以算出来。因此,以上的计算图可以画成下图所示的集约化的 “ S i g m o i d ” “Sigmoid” Sigmoid节点:

在这里插入图片描述

另外, ∂ L ∂ y y 2 e x p ( − x ) \frac {\partial L}{\partial y}y^2exp(-x) yLy2exp(x)可以进一步整理如下:

在这里插入图片描述

使用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

5.6 Affine/Softmax层的实现

神经网络的正向传播中,为了计算加权信号的总和,使用了矩阵的乘积运算。现在将这里进行的求矩阵的乘积与偏置的和的运算用计算图表示出来。将乘积运算用 “ d o t ” “dot” dot节点表示的话,则 n p . d o t ( X , W ) + B np.dot(X,W)+B np.dot(X,W)+B的运算可用下图所示的计算图表示出来(注意: X , W , B X,W,B X,W,B是矩阵):

在这里插入图片描述

神经网络的正向传播中进行的矩阵乘积运算的层就称为 A f f i n e Affine Affine层。

现在我们来考虑上图的反向传播。以矩阵为对象的反向传播,按矩阵的各个元素进行计算时,步骤和以标量为对象的计算图相同。实际写一下的话,可以得到下式:

在这里插入图片描述

式中 W T W^T WT T T T表示转置。转置操作会把 W W W的元素 ( i , j ) (i,j) (i,j)换成元素 ( j , i ) (j,i) (j,i)。用数学式表示的话,可以写成下面这样:

在这里插入图片描述

现在我们尝试写出计算图的反向传播,如下图所示:

在这里插入图片描述

我们看一下上图中各个变量的形状。尤其要注意, X X X ∂ L ∂ X \frac {\partial L}{\partial X} XL形状相同, W W W ∂ L ∂ W \frac {\partial L}{\partial W} WL形状相同。从下面的数学式可以很明确地看出 X X X ∂ L ∂ X \frac {\partial L}{\partial X} XL形状相同。

在这里插入图片描述

为什么要注意矩阵的形状呢?因为矩阵的乘积运算要求对应维度的元素个数保持一致,通过确认一致性,就可以很容易地推导出公式,如下图所示:

在这里插入图片描述

现在我们考虑 N N N个数据一起进行正向传播的情况,也就是批版本的 A f f i n e Affine Affine层。先给出批版本的 A f f i n e Affine Affine层的计算图如下图所示:

在这里插入图片描述

加上偏置时,需要特别注意。正向传播时,偏置被加到 X ⋅ W X\sdot W XW的各个数据上。比如, N = 2 N=2 N=2(数据为 2 2 2个)时,偏置会被分别加到这 2 2 2个数据(各自的计算结果)上,具体的例子如下所示:

X_dot_W = np.array([[0, 0, 0], [10, 10, 10]])
B = np.array([1, 2, 3])
X_dot_W + B  # array([[1, 2, 3], [11, 12, 13]])

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

dY = np.array([[1, 2, 3], [4, 5, 6]])
dB = np.sum(dY, axis=0)  # array([5, 7, 9])

综上所述, A f f i n e Affine 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

最后介绍一下输出层的 S o f t m a x Softmax Softmax函数。前面我们提到过, S o f t m a x Softmax Softmax函数会将输入值正规化之后再输出。比如手写数字识别时, S o f t m a x Softmax Softmax层的输出如下图所示:

在这里插入图片描述

下面来实现 S o f t m a x Softmax Softmax层。考虑到这里也包含作为损失函数的交叉熵误差( c r o s s   e n t r o p y   e r r o r cross\ entropy\ error cross entropy error),所以称为 S o f t m a x − w i t h − L o s s Softmax-with-Loss SoftmaxwithLoss层,其化简后的计算图如下图所示:

在这里插入图片描述

图中要注意的是反向传播的结果, ( y 1 − t 1 , y 2 − t 2 , y 3 − t 3 ) (y_1-t_1,y_2-t_2,y_3-t_3) (y1t1,y2t2,y3t3) S o f t m a x Softmax Softmax层的输出和教师标签的差分。神经网络的反向传播会把这个差分表示的误差传递给前面的层,这是神经网络学习中的重要性质。

现在来进行 S o f t m a x − w i t h − L o s s Softmax-with-Loss SoftmaxwithLoss层的实现,实现过程如下所示,请注意反向传播时,将要传播的值除以批的大小( b a t c h _ s i z e batch\_size batch_size)后,传递给前面的层的是单个数据的误差:

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

5.7 误差反向传播法的实现

现在我们可以通过上一节实现的层来构建神经网络,误差反向传播法会在上一章构建神经网络的步骤(2)中出现,之前利用数值微分求得了这个梯度,现在使用误差反向传播法可以更高效地求解梯度。

同样我们实现一个 2 2 2层的神经网络TwoLayerNet

class TwoLayerNet:
    def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
        # initialize the weight
        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)
        
        # creat the layers
        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):
        # 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)
        
        # setting grads
        grads = {}
        grads['W1'] = self.layers['Affine1'].dW
        grads['b1'] = self.layers['Affine1'].db
        grads['W2'] = self.layers['Affine2'].dW
        grads['b2'] = self.layers['Affine2'].db
        return grads

将神经网络的层保存为OrderedDict这一点非常重要。OrderedDict是有序字典,“有序”是指它可以记住向字典里添加元素的顺序。因此反向传播只需要按照相反的顺序调用各层即可。

最后,我们来看一下使用了误差反向传播法的神经网络的学习的实现,和之前的实现相比,不同之处仅在于通过误差反向传播法求梯度这一点:

# 导入模块
...

# 读入数据
(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)

# 设定超参数
...

for i in range(iters_num):
    # mini-batch
    ...
    
    # 计算梯度
    # grad = network.numerical_gradient(x_batch, t_batch)
    grad = network.gradient(x_batch, t_batch)  # 误差反向传播法计算梯度
    
    # 更新参数
    ...
    
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)
    
    if i % iter_per_epoch == 0:
    	# 记录accuracy
        ...
        
# 绘制图形
...

下一节:【学习笔记】深度学习入门:基于Python的理论与实现-与学习相关的技巧。

你可能感兴趣的:(Artificial,Intelligence,python,深度学习,学习,人工智能,神经网络)