误差反向传播法的实现
通过像组装乐高积木一样组装上一节中实现的层,可以构建神经网络。
本节我们将通过组装已经实现的层来构建神经网络。
神经网络学习的全貌图
在进行具体的实现之前,我们再来确认一下神经网络学习的全貌图。神
经网络学习的步骤如下所示。
前提
神经网络中有合适的权重和偏置,调整权重和偏置以便拟合训练数据的
过程称为学习。神经网络的学习分为下面4 个步骤。
步骤1(mini-batch)
从训练数据中随机选择一部分数据。
步骤2(计算梯度)
计算损失函数关于各个权重参数的梯度。
步骤3(更新参数)
将权重参数沿梯度方向进行微小的更新。
之前介绍的误差反向传播法会在步骤2 中出现。上一章中,我们利用数
值微分求得了这个梯度。数值微分虽然实现简单,但是计算要耗费较多的时
间。和需要花费较多时间的数值微分不同,误差反向传播法可以快速高效地
计算梯度。
现在来进行神经网络的实现。这里我们要把2层神经网络实现为TwoLayerNet。
首先,将这个类的实例变量和方法整理成表5-1 和表5-2。
这个类的实现稍微有一点长,但是内容和4.5 节的学习算法的实现有很
多共通的部分,不同点主要在于这里使用了层。通过使用层,获得识别结果
的处理(predict())和计算梯度的处理(gradient())只需通过层之间的传递就能完成。下面是TwoLayerNet的代码实现。
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()方法就可以完成处理,而反向传播只需要按
照相反的顺序调用各层即可。因为Affine层和ReLU层的内部会正确处理正
向传播和反向传播,所以这里要做的事情仅仅是以正确的顺序连接各层,再
按顺序(或者逆序)调用各层。
像这样通过将神经网络的组成元素以层的方式实现,可以轻松地构建神
经网络。这个用层进行模块化的实现具有很大优点。因为想另外构建一个神
经网络(比如5 层、10 层、20 层……的大的神经网络)时,只需像组装乐高
积木那样添加必要的层就可以了。之后,通过各个层内部实现的正向传播和
反向传播,就可以正确计算进行识别处理或学习所需的梯度。
误差反向传播法的梯度确认
到目前为止,我们介绍了两种求梯度的方法。一种是基于数值微分的方
法,另一种是解析性地求解数学式的方法。后一种方法通过使用误差反向传
播法,即使存在大量的参数,也可以高效地计算梯度。因此,后文将不再使
用耗费时间的数值微分,而是使用误差反向传播法求梯度。
数值微分的计算很耗费时间,而且如果有误差反向传播法的(正确的)
实现的话,就没有必要使用数值微分的实现了。那么数值微分有什么用呢?
实际上,在确认误差反向传播法的实现是否正确时,是需要用到数值微分的。
数值微分的优点是实现简单,因此,一般情况下不太容易出错。而误差
反向传播法的实现很复杂,容易出错。所以,经常会比较数值微分的结果和
误差反向传播法的结果,以确认误差反向传播法的实现是否正确。确认数值
微分求出的梯度结果和误差反向传播法求出的结果是否一致(严格地讲,是
非常相近)的操作称为梯度确认(gradient check)。梯度确认的代码实现如下
所示(源代码在ch05/gradient_check.py中)。
importsys,os sys.path.append(os.pardir)importnumpyasnpfromdataset.mnistimportload_mnistfromtwo_layer_netimportTwoLayerNet# 读入数据(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)# 求各个权重的绝对误差的平均值forkeyingrad_numerical.keys():diff=np.average(np.abs(grad_backprop[key]-grad_numerical[key]))print(key+":"+str(diff))和以前一样,读入MNIST数据集。然后,使用训练数据的一部分,确
认数值微分求出的梯度和误差反向传播法求出的梯度的误差。这里误差的计
算方法是求各个权重参数中对应元素的差的绝对值,并计算其平均值。运行
上面的代码后,会输出如下结果。
b1:9.70418809871e-13 W2:8.41139039497e-13 b2:1.1945999745e-10 W1:2.2232446644e-13从这个结果可以看出,通过数值微分和误差反向传播法求出的梯度的差
非常小。比如,第1 层的偏置的误差是9.7 e − 13 ( 0.00000000000097 ) 9.7e-13(0.00000000000097)9.7e−13(0.00000000000097)。这样一来,
我们就知道了通过误差反向传播法求出的梯度是正确的,误差反向传播法的
实现没有错误。
数值微分和误差反向传播法的计算结果之间的误差为0 是很少见的。
这是因为计算机的计算精度有限(比如,32 位浮点数)。受到数值精
度的限制,刚才的误差一般不会为0,但是如果实现正确的话,可
以期待这个误差是一个接近0 的很小的值。如果这个值很大,就说
明误差反向传播法的实现存在错误。
使用误差反向传播法的学习
最后,我们来看一下使用了误差反向传播法的神经网络的学习的实现。
和之前的实现相比,不同之处仅在于通过误差反向传播法求梯度这一点。这
里只列出了代码,省略了说明(源代码在ch05/train_neuralnet.py中)。
importsys,os sys.path.append(os.pardir)importnumpyasnpfromdataset.mnistimportload_mnistfromtwo_layer_netimportTwoLayerNet# 读入数据(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=10000train_size=x_train.shape[0]batch_size=100learning_rate=0.1train_loss_list=[]train_acc_list=[]test_acc_list=[]iter_per_epoch=max(train_size/batch_size,1)foriinrange(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)# 更新forkeyin('W1','b1','W2','b2'):network.params[key]-=learning_rate*grad[key]loss=network.loss(x_batch,t_batch)train_loss_list.append(loss)ifi%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)