前面使用与房价预测相同的简单神经网络解决手写数字识别问题,效果并不理想,原因有两点:
- 输入数据类型不同。房价预测的输入为离散一维数据。房价预测使用全连接神经网络无法学习到图像二维数据中的空间信息。
- 模型复杂度不够。因为手写数字识别任务涉及到图像信号,比房价预测任务更加复杂,模型的复杂度也会影响最终的效果,理论上复杂的模型能够表示更复杂的转换关系(从输入到输出)。
本节介绍两种常见的网络结构,全连接神经网络和卷积神经网络,观测卷积网络能否提升手写数字识别的训练效果。
在开始介绍网络结构前,复用上一节的数据处理代码,代码如下。
1 #数据处理部分之前的代码,保持不变 2 import os 3 import random 4 import paddle 5 import paddle.fluid as fluid 6 from paddle.fluid.dygraph.nn import Conv2D, Pool2D, FC 7 import numpy as np 8 import matplotlib.pyplot as plt 9 from PIL import Image 10 11 import gzip 12 import json 13 14 # 定义数据集读取器 15 def load_data(mode='train'): 16 17 # 数据文件 18 datafile = './work/mnist.json.gz' 19 print('loading mnist dataset from {} ......'.format(datafile)) 20 data = json.load(gzip.open(datafile)) 21 train_set, val_set, eval_set = data 22 23 # 数据集相关参数,图片高度IMG_ROWS, 图片宽度IMG_COLS 24 IMG_ROWS = 28 25 IMG_COLS = 28 26 27 if mode == 'train': 28 imgs = train_set[0] 29 labels = train_set[1] 30 elif mode == 'valid': 31 imgs = val_set[0] 32 labels = val_set[1] 33 elif mode == 'eval': 34 imgs = eval_set[0] 35 labels = eval_set[1] 36 37 imgs_length = len(imgs) 38 39 assert len(imgs) == len(labels), 40 "length of train_imgs({}) should be the same as train_labels({})".format( 41 len(imgs), len(labels)) 42 43 index_list = list(range(imgs_length)) 44 45 # 读入数据时用到的batchsize 46 BATCHSIZE = 100 47 48 # 定义数据生成器 49 def data_generator(): 50 if mode == 'train': 51 random.shuffle(index_list) 52 imgs_list = [] 53 labels_list = [] 54 for i in index_list: 55 img = np.reshape(imgs[i], [1, IMG_ROWS, IMG_COLS]).astype('float32') 56 label = np.reshape(labels[i], [1]).astype('float32') 57 imgs_list.append(img) 58 labels_list.append(label) 59 if len(imgs_list) == BATCHSIZE: 60 yield np.array(imgs_list), np.array(labels_list) 61 imgs_list = [] 62 labels_list = [] 63 64 # 如果剩余数据的数目小于BATCHSIZE, 65 # 则剩余数据一起构成一个大小为len(imgs_list)的mini-batch 66 if len(imgs_list) > 0: 67 yield np.array(imgs_list), np.array(labels_list) 68 69 return data_generator
数据处理部分结构不变。
1. 经典全连接神经网络
经典的全连接神经网络含有四层网络:两个隐含层,输入层和输出层。
- 输入层。准备数据,输入给神经网络。
- 隐含层。增加网络深度和复杂度,隐含层的节点数是可以调整的。节点数越多,神经网络表示能力越强,但同时参数量也会增加。
- 输出层。输出网络计算结果,输出层的节点数是固定的,比如手写数字识别有0-9十个数字,输出标签有10个,所以输出层必须是10个节点。
说明:
隐含层引入非线性激活函数sigmoid是为了增加神经网络的非线性能力,举例来说,一个神经网络有四个输入x1~x4,一个输出y,采用线性变换。假设第一层的变换是z1=x1-x2和z2=x3+x4,第二层的变换是y=z1+z2。将两层的变换展开后得到 y=x1-x2+x3+x4。原始输入和最终输出之间依然是线性关系,无论中间累积了多少层线性变换均是如此。
Sigmoid的函数曲线如下图所示,是早期神经网络模型中常见的非线性变换函数。
1 def sigmoid(x): 2 # 直接返回sigmoid函数 3 return 1. / (1. + np.exp(-x)) 4 5 # param:起点,终点,间距 6 x = np.arange(-8, 8, 0.2) 7 y = sigmoid(x) 8 plt.plot(x, y) 9 plt.show()
下述代码为经典全连接神经网络的实现。完成网络结构定义后,即可训练神经网络。
1 # 多层全连接神经网络实现 2 class MNIST(fluid.dygraph.Layer): 3 def __init__(self, name_scope): 4 super(MNIST, self).__init__(name_scope) 5 name_scope = self.full_name() 6 # 定义两层全连接隐含层,输出维度是10,激活函数为sigmoid 7 self.fc1 = FC(name_scope, size=10, act='sigmoid') # 隐含层节点为10,可根据任务调整 8 self.fc2 = FC(name_scope, size=10, act='sigmoid') 9 # 定义一层全连接输出层,输出维度是1,不使用激活函数 10 self.fc3 = FC(name_scope, size=1, act=None) 11 12 # 定义网络的前向计算 13 def forward(self, inputs, label=None): 14 outputs1 = self.fc1(inputs) 15 outputs2 = self.fc2(outputs1) 16 outputs_final = self.fc3(outputs2) 17 return outputs_final
训练部分与原来的一样,保持不变。
1 #网络结构部分之后的代码,保持不变 2 with fluid.dygraph.guard(): 3 model = MNIST("mnist") 4 model.train() 5 #调用加载数据的函数,获得MNIST训练数据集 6 train_loader = load_data('train') 7 # 使用SGD优化器,learning_rate设置为0.01 8 optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.01) 9 # 训练5轮 10 EPOCH_NUM = 5 11 for epoch_id in range(EPOCH_NUM): 12 for batch_id, data in enumerate(train_loader()): 13 #准备数据 14 image_data, label_data = data 15 image = fluid.dygraph.to_variable(image_data) 16 label = fluid.dygraph.to_variable(label_data) 17 18 #前向计算的过程 19 predict = model(image) 20 21 #计算损失,取一个批次样本损失的平均值 22 loss = fluid.layers.square_error_cost(predict, label) 23 avg_loss = fluid.layers.mean(loss) 24 25 #每训练了200批次的数据,打印下当前Loss的情况 26 if batch_id % 200 == 0: 27 print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy())) 28 29 #后向传播,更新参数的过程 30 avg_loss.backward() 31 optimizer.minimize(avg_loss) 32 model.clear_gradients() 33 34 #保存模型参数 35 fluid.save_dygraph(model.state_dict(), 'mnist')
同样的疑问,前向计算过程是这样完成的: predict=model(image),没有调用forward函数?
下面为训练后的输出结果:
loading mnist dataset from ./work/mnist.json.gz ...... epoch: 0, batch: 0, loss is: [22.33305] epoch: 0, batch: 200, loss is: [4.7322707] epoch: 0, batch: 400, loss is: [3.3345733] epoch: 1, batch: 0, loss is: [3.40572] epoch: 1, batch: 200, loss is: [3.1861389] epoch: 1, batch: 400, loss is: [3.113542] epoch: 2, batch: 0, loss is: [3.364307] epoch: 2, batch: 200, loss is: [3.6445987] epoch: 2, batch: 400, loss is: [2.8337402] epoch: 3, batch: 0, loss is: [2.29281] epoch: 3, batch: 200, loss is: [2.0806828] epoch: 3, batch: 400, loss is: [1.7476844] epoch: 4, batch: 0, loss is: [2.8141792] epoch: 4, batch: 200, loss is: [2.8806489] epoch: 4, batch: 400, loss is: [2.480993]
总的来讲,loss有下降趋势。
2.卷积神经网络
虽然使用经典的神经网络可以达到一定的准确率,但对于计算机视觉问题,效果最好的模型是卷积神经网络。卷积神经网络针对视觉问题的特点优化了网络结构,更适合处理视觉问题。卷积神经网络的原理会在第四章详述,在这里只展示如何将模型替换成卷积神经网络,以及它带来的效果提升。
卷积神经网络由多个卷积层和池化层组合而成,卷积层负责对输入进行扫描以生成更抽象的特征表示,而池化层则对这些特征表示进行过滤,保留最关键的信息。下面是卷积神经网络的实现,卷积和池化函数会在第四章详述,这里仅理解成一种比经典神经网络更强大的模型即可。
1 # 多层卷积神经网络实现 2 class MNIST(fluid.dygraph.Layer): 3 def __init__(self, name_scope): 4 super(MNIST, self).__init__(name_scope) 5 name_scope = self.full_name() 6 # 定义卷积层,输出特征通道num_filters设置为20,卷积核的大小filter_size为5,卷积步长stride=1,padding=2 7 # 激活函数使用relu 8 self.conv1 = Conv2D(name_scope, num_filters=20, filter_size=5, stride=1, padding=2, act='relu') 9 # 定义池化层,池化核pool_size=2,池化步长为2,选择最大池化方式 10 self.pool1 = Pool2D(name_scope, pool_size=2, pool_stride=2, pool_type='max') 11 # 定义卷积层,输出特征通道num_filters设置为20,卷积核的大小filter_size为5,卷积步长stride=1,padding=2 12 self.conv2 = Conv2D(name_scope, num_filters=20, filter_size=5, stride=1, padding=2, act='relu') 13 # 定义池化层,池化核pool_size=2,池化步长为2,选择最大池化方式 14 self.pool2 = Pool2D(name_scope, pool_size=2, pool_stride=2, pool_type='max') 15 # 定义一层全连接层,输出维度是1,不使用激活函数 16 self.fc = FC(name_scope, size=1, act=None) 17 18 # 定义网络前向计算过程,卷积后紧接着使用池化层,最后使用全连接层计算最终输出 19 def forward(self, inputs): 20 x = self.conv1(inputs) 21 x = self.pool1(x) 22 x = self.conv2(x) 23 x = self.pool2(x) 24 x = self.fc(x) 25 return x
接下来训练定义好的卷积神经网络,比较两个模型的损失变化(经典神经网络和卷积神经网络),可以发现卷积神经网络的损失值下降更快,且最终的损失值更小。
1 #网络结构部分之后的代码,保持不变 2 with fluid.dygraph.guard(): 3 model = MNIST("mnist") 4 model.train() 5 #调用加载数据的函数 6 train_loader = load_data('train') 7 optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.01) 8 EPOCH_NUM = 5 9 for epoch_id in range(EPOCH_NUM): 10 for batch_id, data in enumerate(train_loader()): 11 #准备数据 12 image_data, label_data = data 13 image = fluid.dygraph.to_variable(image_data) 14 label = fluid.dygraph.to_variable(label_data) 15 16 #前向计算的过程 17 predict = model(image) 18 19 #计算损失,取一个批次样本损失的平均值 20 loss = fluid.layers.square_error_cost(predict, label) 21 avg_loss = fluid.layers.mean(loss) 22 23 #每训练了100批次的数据,打印下当前Loss的情况 24 if batch_id % 200 == 0: 25 print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy())) 26 27 #后向传播,更新参数的过程 28 avg_loss.backward() 29 optimizer.minimize(avg_loss) 30 model.clear_gradients() 31 32 #保存模型参数 33 fluid.save_dygraph(model.state_dict(), 'mnist')
loading mnist dataset from ./work/mnist.json.gz ...... epoch: 0, batch: 0, loss is: [26.78181] epoch: 0, batch: 200, loss is: [9.082592] epoch: 0, batch: 400, loss is: [4.95523] epoch: 1, batch: 0, loss is: [3.5062227] epoch: 1, batch: 200, loss is: [2.9835818] epoch: 1, batch: 400, loss is: [3.0593839] epoch: 2, batch: 0, loss is: [2.8837323] epoch: 2, batch: 200, loss is: [2.5961332] epoch: 2, batch: 400, loss is: [1.5832586] epoch: 3, batch: 0, loss is: [1.398091] epoch: 3, batch: 200, loss is: [1.9496399] epoch: 3, batch: 400, loss is: [0.97344035] epoch: 4, batch: 0, loss is: [1.6800342] epoch: 4, batch: 200, loss is: [1.0535694] epoch: 4, batch: 400, loss is: [1.2734855]
训练过程的代码与上面的全连接神经网络的相同。从数据来看,卷积神经网络loss下降更快。