深度学习入门:第五章误差反向传播法

深度学习入门:第五章误差反向传播法


高效计算权重参数的梯度的方法——误差反向传播法。

5.1计算图

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

5.1.1 用计算图求解

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

如下例子:问题1,太郎在超市买了2个100日元一个的苹果,消费税是10%,请计 算支付金额。

对应计算图为,可以发现圈内为计算内容。

image-20230530110214783

也可以把符号放在圈外面,如下图

image-20230530110422919

故计算图解题时,步骤为

  1. 构建计算图。
  2. 从左至右进行计算

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

从右到左则是反向传播

5.1.2 局部计算

计算图的特征是可以通过传递“局部计算”获得最终结果。

下面解释局部计算

image-20230530111036192

在计算4000 + 200 → 4200中,该计算过程不考虑值4000和200是如何计算来的,它只要做两个数字相加就可以了。

综上,计算图可以集中精力于局部计算。无论全局的计算有多么复杂, 各个步骤所要做的就是对象节点的局部计算。虽然局部计算非常简单,但是 通过传递它的计算结果,可以获得全局的复杂计算的结果。

5.1.3 为何用计算图解题

使用计算图解题有两个优点

  • 局部计算
  • 可以将中间的计算结果全部保存起来

最重要的原因是可以通过反向传播高效计算导数。在问题1中,我们 想知道苹果价格的上涨会在多大程度上影响最终的支付金额,即求“支付金 额关于苹果的价格的导数”。而“支付金额关于苹果的价格的导数”的值可以通过计算图的 反向传播求出来。如图

image-20230530112103848

可以发现反向传播传递局部导数(粗线),导数值在箭头下方。

同时,计算中途求得的导数的结果(中间传递的导数)可以被共享,从而可以高效地计算多个导数。综上,计算图的优点是,可以通过正向传播和反向传播高效地计算各个变量的导数值。

5.2 链式法则

5.2.1 计算图的反向传播

计算机的反向传播例图。

image-20230530171926626

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

5.2.2  什么是链式法则

链式法则是关于复合函数的导数的性质,定义如下。

如果某个函数由复合函数表示,则该复合函数的导数可以用构成复合函数的各个函数的导数的乘积表示。很容易发现就是复合函数的求导法则。

举个例子z = (x + y) ^2,求z关于x的导数为

  1. image-20230530193307870
  2. image-20230530193407888
  3. image-20230530195242807

5.2.3  链式法则和计算图

尝试将式(5.4)的链式法则的计算用计算图表示出来。

image-20230530195314674

如图所示,计算图的反向传播从右到左传播信号。反向传播的计算顺序 是,先将节点的输入信号乘以节点的局部导数(偏导数),然后再传递给下一 个节点。

比如,反向传播时,“**2”节点的输入是dz/dx ,将其乘以局部导数 dz/dt(因 为正向传播时输入是t、输出是z,所以这个节点的局部导数是 dz/dt),然后传递给下一个节点。

5.3 反向传播

5.3.1 加法节点的反向传播

image-20230530200147367

加法节点的反向传播:左图是正向传播,右图是反向传播。如右图的反向传播所示, 加法节点的反向传播将上游的值原封不动地输出到下游

具体例子,假设有“10 + 5=15”这一计算,反向传播时,从上游会传来值1.3。计算图如图

image-20230530200500350

因为加法节点的反向传播只是将输入信号输出到下一个节点,所以如图 5-11所示,反向传播将1.3向下一个节点传递。

5.3.2 乘法节点的反向传播

考虑z = xy,导数式表示

image-20230531191137846

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

加法的反向传播只是将上游的值传给下游, 并不需要正向传播的输入信号。但是,乘法的反向传播需要正向传播时的输 入信号值。因此,实现乘法节点的反向传播时,要保存正向传播的输入信号。

5.3.3 苹果例子

对应5.1.3苹果例子。对应的反向传播为

image-20230531192200476

可以发现乘法反转为除法。

5.4 简单层的实现

把要实现的计算图的乘法节点称为“乘法层”(MulLayer),加法节点称为“加法层” (AddLayer)。

5.4.1 乘法层的实现

层的实现中有两个共通的方法(接口)forward()和backward()。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 # 翻转x和y
 	dy = dout * self.x
 
 	return dx, dy

__ init __()中会初始化实例变量x和y,它们用于保存正向传播时的输入值。 forward()接收x和y两个参数,将它们相乘后输出。backward()将从上游传来的导数(dout)乘以正向传播的翻转值,然后传给下游。

使用MulLayer实现前面的购买苹果 的例子(2个苹果和消费税)

image-20230531192729931

使用乘法层后代码如下

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()求出。

# 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

这里,调用backward()的顺序与调用forward()的顺序相反。此外,要注 意backward()的参数中需要输入“关于正向传播时的输出变量的导数”。比如, mul_apple_layer乘法层在正向传播时会输出apple_price,在反向传播时,则 会将apple_price的导数dapple_price设为参数。

5.4.2 加法层的实现

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

pass 语句表示“什么也不运行“,加法层的forward()接收x和y两个参数,将它 们相加后输出。backward()将上游传来的导数(dout)原封不动地传递给下游。

上述代码汇总即可实现苹果例子,代码长但是比较简单。首先,生成必要的 层,以合适的顺序调用正向传播的forward()方法。然后,用与正向传播相反的顺序调用反向传播的backward()方法,就可以求出想要的导数。

综上,计算图中层的实现(这里是加法层和乘法层)非常简单,使用这些层可以进行复杂的导数计算。

5.5 激活函数层的实现

5.5.1 ReLU层

image-20230531193253867

在式(5.8)中,如果正向传播时的输入x大于0,则反向传播会将上游的 值原封不动地传给下游。反过来,如果正向传播时的x小于等于0,则反向 传播中传给下游的信号将停在此处。如图

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

ReLU层的作用就像电路中的开关一样。正向传播时,有电流通过 的话,就将开关设为 ON;没有电流通过的话,就将开关设为 OFF。 反向传播时,开关为ON的话,电流会直接通过;开关为OFF的话, 则不会有电流通过

5.5.2 Sigmoid层

sigmoid函数由式(5.9)表示

image-20230531193555138

计算图表示

image-20230531193622478

除了“×”和“+”节点外,还出现了新的“exp”和“/”节点。 “exp”节点会进行y = exp(x)的计算,“/”节点会进行1/x的计算。对应的反向传播流程如下图,可以自己查看。

image-20230531194336961 image-20230531194406299

反向输出的值只根据正向传播时的输入x和输出y就可以算出来。因此,图5-20的计算图可以画成图5-21的集约化的“sigmoid”节点。

image-20230531194629306

通过对节点进行集约化,可以不用在意Sigmoid层中琐碎的细节,而只需要 专注它的输入和输出,这一点也很重要。

对上图输出结点进行简化,可得到

image-20230531194738110

代码实现

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

这个实现中,正向传播时将输出保存在了实例变量out中。然后,反向 传播时,使用该变量out进行计算。

5.6 Affine/Softmax层的实现

5.6.1 Affine层

神经网络的正向传播中,为了计算加权信号的总和,使用了矩阵的乘积运算(NumPy中是np.dot())。

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

使用以下例子进行讲解。

>>> X = np.random.rand(2) # 输入
>>> W = np.random.rand(2,3) # 权重
>>> B = np.random.rand(3) # 偏置
>>>
>>> X.shape # (2,)
>>> W.shape # (2, 3)
>>> B.shape # (3,)
>>>
>>> Y = np.dot(X, W) + B

这里,X、W、B 分别是形状为(2,)、(2, 3)、(3,)的多维数组。这样一 来,神经元的加权和可以用Y = np.dot(X, W) + B计算出来。X和W 的乘积必须使对应维度的元素个数一致。

image-20230531200858089

将上述代码进行计算图表示

image-20230531201008285

之前我们见到的计算图中各个节点间流动的是标量,而这个例子中各个节点间传播的是矩阵。此时要考虑矩阵的反向传播。

image-20230531201232079

T标志着转置,即会把W的元素(i, j)换成元素 (j, i)。此时可表示出上述例子的计算图,点乘以W则乘以W^T.

image-20230531201403846

X和 dL/dx形状相同,W和 dL/dw形状相同。为矩阵的乘积运算要求对应维度的元素 个数保持一致.

image-20230531201747260

5.6.2 批版本的Affine层

批版本的Affine层的计算图。

image-20230531201840642

与刚刚不同的是,现在输入X的形状是(N, 2),N代表N个数据。之后就和前面一样,在 计算图上进行单纯的矩阵计算。

加上偏置时,需要特别注意。正向传播时,偏置被加到X·W的各个 数据上。比如,N = 2(数据为2个)时,偏置会被分别加到这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])

这个例子中,假定数据有2个(N = 2)。偏置的反向传播会对这2个数据 的导数按元素进行求和。因此,这里使用了np.sum()对第0轴(以数据为单 位的轴,axis=0)方向上的元素进行求和。

综上所述,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

5.6.3 Softmax-with-Loss 层

手写数字识别时,Softmax层的输出如图5-28所示。

image-20230531202443009

在图5-28中,Softmax层将输入值正规化(将输出值的和调整为1)之后 再输出。另外,因为手写数字识别要进行10类分类,所以向Softmax层的输 入也有10个。

神经网络中进行的处理有推理(inference)和学习两个阶段。神经网 络的推理通常不使用 Softmax层。学习则需要Softmax层。

Softmax-withLoss层(Softmax函数和交叉熵误差)。

image-20230602163415819

softmax函数记为Softmax层,交叉熵误差记为 Cross Entropy Error层。这里假设要进行3类分类,从前面的层接收3个输 入(得分)。如图5-30所示,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层的输出 与教师标签的差,直截了当地表示了当前神经网络的输出与教师标签的误差。

Softmax-with-Loss层的实现

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

反向传播时,将要传播的值除以批的大小(batch_size)后,传递给前面的层的是单个数据的误差

5.7 误差反向传播法的实现

5.7.1 神经网络学习的全貌图

神经网络学习步骤(前提:神经网络中有合适的权重和偏置,调整权重和偏置以便拟合训练数据的 过程称为学习。)

  1. 从训练数据中随机选择一部分数据。
  2. 计算损失函数关于各个权重参数的梯度。
  3. 将权重参数沿梯度方向进行微小的更新。
  4. 重复前面步骤

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

实现2层神经网络,命名为TwoLayerNet。这个类的实例变量和方法

image-20230602164134291

这里通过使用层,获得识别结果 的处理(predict())和计算梯度的处理(gradient())只需通过层之间的传递就能完成。

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
 # 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'] = 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是一个有序字典,会记录元素放进去的顺序。因此,神经网络的正向传播只需按照添加元 素的顺序调用各层的forward()方法就可以完成处理,而反向传播只需要按 照相反的顺序调用各层即可。

通过层搭建可以轻松实现神经网络,通过各个层内部实现的正向传播和 反向传播,就可以正确计算进行识别处理或学习所需的梯度。

5.7.3 误差反向传播法的梯度确认

数值微分的优点是实现简单,因此,一般情况下不太容易出错。而误差 反向传播法的实现很复杂,容易出错。所以要对比两个方法输出的梯度。确认数值 微分求出的梯度结果和误差反向传播法求出的结果是否一致(严格地讲,是非常相近)的操作称为梯度确认(gradient check)。

梯度确认代码

import sys, os
sys.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)
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))

读入MNIST数据集。然后,使用训练数据的一部分,确 认数值微分求出的梯度和误差反向传播法求出的梯度的误差。这里误差的计 算方法是求各个权重参数中对应元素的差的绝对值,并计算其平均值。

5.7.4 使用误差反向传播法的学习

和第四章的实现相比,不同之处仅在于通过误差反向传播法求梯度这一点。

import sys, os
sys.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 * 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)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值