概述

前几节我们尝试使用与房价预测相同的简单神经网络解决手写数字识别问题,但是效果并不理想。原因是手写数字识别的输入是28 × 28的像素值,输出是0-9的数字标签,而线性回归模型无法捕捉二维图像数据中蕴含的复杂信息,如 图1 所示。无论是牛顿第二定律任务,还是房价预测任务,输入特征和输出预测值之间的关系均可以使用“直线”刻画(使用线性方程来表达)。但手写数字识别任务的输入像素和输出数字标签之间的关系显然不是线性的,甚至这个关系复杂到我们靠人脑难以直观理解的程度。

【手写数字识别】之网络结构 - 图1

图1:数字识别任务的输入和输入不是线性关系

因此,我们需要尝试使用其他更复杂、更强大的网络来构建手写数字识别任务,观察一下训练效果,即将“横纵式”教学法从横向展开,如 图2 所示。本节主要介绍两种常见的网络结构:经典的多层全连接神经网络和卷积神经网络。

【手写数字识别】之网络结构 - 图2

图2:“横纵式”教学法 — 网络结构优化

数据处理

在介绍网络结构前,需要先进行数据处理,代码与上一节保持一致。

  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, Linear
  7. import numpy as np
  8. import matplotlib.pyplot as plt
  9. from PIL import Image
  10. import gzip
  11. import json
  12. # 定义数据集读取器
  13. def load_data(mode='train'):
  14. # 数据文件
  15. datafile = './work/mnist.json.gz'
  16. print('loading mnist dataset from {} ......'.format(datafile))
  17. data = json.load(gzip.open(datafile))
  18. train_set, val_set, eval_set = data
  19. # 数据集相关参数,图片高度IMG_ROWS, 图片宽度IMG_COLS
  20. IMG_ROWS = 28
  21. IMG_COLS = 28
  22. if mode == 'train':
  23. imgs = train_set[0]
  24. labels = train_set[1]
  25. elif mode == 'valid':
  26. imgs = val_set[0]
  27. labels = val_set[1]
  28. elif mode == 'eval':
  29. imgs = eval_set[0]
  30. labels = eval_set[1]
  31. imgs_length = len(imgs)
  32. assert len(imgs) == len(labels), \
  33. "length of train_imgs({}) should be the same as train_labels({})".format(
  34. len(imgs), len(labels))
  35. index_list = list(range(imgs_length))
  36. # 读入数据时用到的batchsize
  37. BATCHSIZE = 100
  38. # 定义数据生成器
  39. def data_generator():
  40. if mode == 'train':
  41. random.shuffle(index_list)
  42. imgs_list = []
  43. labels_list = []
  44. for i in index_list:
  45. img = np.reshape(imgs[i], [1, IMG_ROWS, IMG_COLS]).astype('float32')
  46. label = np.reshape(labels[i], [1]).astype('float32')
  47. imgs_list.append(img)
  48. labels_list.append(label)
  49. if len(imgs_list) == BATCHSIZE:
  50. yield np.array(imgs_list), np.array(labels_list)
  51. imgs_list = []
  52. labels_list = []
  53. # 如果剩余数据的数目小于BATCHSIZE,
  54. # 则剩余数据一起构成一个大小为len(imgs_list)的mini-batch
  55. if len(imgs_list) > 0:
  56. yield np.array(imgs_list), np.array(labels_list)
  57. return data_generator
  1. 2020-03-26 15:24:28,868-INFO: font search path ['/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/matplotlib/mpl-data/fonts/ttf', '/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/matplotlib/mpl-data/fonts/afm', '/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/matplotlib/mpl-data/fonts/pdfcorefonts']
  2. 2020-03-26 15:24:29,250-INFO: generated new fontManager

经典的全连接神经网络

经典的全连接神经网络来包含四层网络:两个隐含层,输入层和输出层,将手写数字识别任务通过全连接神经网络表示,如 图3 所示。

【手写数字识别】之网络结构 - 图3

图3:手写数字识别任务的全连接神经网络结构

  • 输入层:将数据输入给神经网络。在该任务中,输入层的尺度为28×28的像素值。
  • 隐含层:增加网络深度和复杂度,隐含层的节点数是可以调整的,节点数越多,神经网络表示能力越强,参数量也会增加。在该任务中,中间的两个隐含层为10×10的结构,通常隐含层会比输入层的尺寸小,以便对关键信息做抽象,激活函数使用常见的sigmoid函数。
  • 输出层:输出网络计算结果,输出层的节点数是固定的。如果是回归问题,节点数量为需要回归的数字数量。如果是分类问题,则是分类标签的数量。在该任务中,模型的输出是回归一个数字,输出层的尺寸为1。

说明:

隐含层引入非线性激活函数sigmoid是为了增加神经网络的非线性能力。

举例来说,如果一个神经网络采用线性变换,有四个输入

【手写数字识别】之网络结构 - 图4 ~ 【手写数字识别】之网络结构 - 图5 ,一个输出 【手写数字识别】之网络结构 - 图6 。假设第一层的变换是 【手写数字识别】之网络结构 - 图7【手写数字识别】之网络结构 - 图8 ,第二层的变换是 【手写数字识别】之网络结构 - 图9 ,则将两层的变换展开后得到 【手写数字识别】之网络结构 - 图10 。也就是说,无论中间累积了多少层线性变换,原始输入和最终输出之间依然是线性关系。


Sigmoid是早期神经网络模型中常见的非线性变换函数,通过如下代码,绘制出Sigmoid的函数曲线。

  1. def sigmoid(x):
  2. # 直接返回sigmoid函数
  3. return 1. / (1. + np.exp(-x))
  4. # param:起点,终点,间距
  5. x = np.arange(-8, 8, 0.2)
  6. y = sigmoid(x)
  7. plt.plot(x, y)
  8. plt.show()

【手写数字识别】之网络结构 - 图11

  1. <Figure size 432x288 with 1 Axes>

针对手写数字识别的任务,网络层的设计如下:

  • 输入层的尺度为28×28,但批次计算的时候会统一加1个维度(大小为bitchsize)。
  • 中间的两个隐含层为10×10的结构,激活函数使用常见的sigmoid函数。
  • 与房价预测模型一样,模型的输出是回归一个数字,输出层的尺寸设置成1。

下述代码为经典全连接神经网络的实现。完成网络结构定义后,即可训练神经网络。

  1. # 多层全连接神经网络实现
  2. class MNIST(fluid.dygraph.Layer):
  3. def __init__(self, name_scope):
  4. super(MNIST, self).__init__(name_scope)
  5. # 定义两层全连接隐含层,输出维度是10,激活函数为sigmoid
  6. self.fc1 = Linear(input_dim=784, output_dim=10, act='sigmoid') # 隐含层节点为10,可根据任务调整
  7. self.fc2 = Linear(input_dim=10, output_dim=10, act='sigmoid')
  8. # 定义一层全连接输出层,输出维度是1,不使用激活函数
  9. self.fc3 = Linear(input_dim=10, output_dim=1, act=None)
  10. # 定义网络的前向计算
  11. def forward(self, inputs, label=None):
  12. inputs = fluid.layers.reshape(inputs, [inputs.shape[0], 784])
  13. outputs1 = self.fc1(inputs)
  14. outputs2 = self.fc2(outputs1)
  15. outputs_final = self.fc3(outputs2)
  16. 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, parameter_list=model.parameters())
  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. predict = model(image)
  19. #计算损失,取一个批次样本损失的平均值
  20. loss = fluid.layers.square_error_cost(predict, label)
  21. avg_loss = fluid.layers.mean(loss)
  22. #每训练了200批次的数据,打印下当前Loss的情况
  23. if batch_id % 200 == 0:
  24. print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy()))
  25. #后向传播,更新参数的过程
  26. avg_loss.backward()
  27. optimizer.minimize(avg_loss)
  28. model.clear_gradients()
  29. #保存模型参数
  30. fluid.save_dygraph(model.state_dict(), 'mnist')
  1. loading mnist dataset from ./work/mnist.json.gz ......
  2. epoch: 0, batch: 0, loss is: [27.740425]
  3. epoch: 0, batch: 200, loss is: [5.4588423]
  4. epoch: 0, batch: 400, loss is: [3.9063952]
  5. epoch: 1, batch: 0, loss is: [3.8620145]
  6. epoch: 1, batch: 200, loss is: [4.6423216]
  7. epoch: 1, batch: 400, loss is: [3.9099925]
  8. epoch: 2, batch: 0, loss is: [3.3493927]
  9. epoch: 2, batch: 200, loss is: [2.8054562]
  10. epoch: 2, batch: 400, loss is: [2.8475616]
  11. epoch: 3, batch: 0, loss is: [3.1059093]
  12. epoch: 3, batch: 200, loss is: [2.8764062]
  13. epoch: 3, batch: 400, loss is: [2.248354]
  14. epoch: 4, batch: 0, loss is: [2.3325133]
  15. epoch: 4, batch: 200, loss is: [2.9140906]
  16. epoch: 4, batch: 400, loss is: [1.6771106]

卷积神经网络

虽然使用经典的神经网络可以提升一定的准确率,但对于计算机视觉问题,效果最好的模型仍然是卷积神经网络。卷积神经网络针对视觉问题的特点进行了网络结构优化,更适合处理视觉问题。

卷积神经网络由多个卷积层和池化层组成,如 图4 所示。卷积层负责对输入进行扫描以生成更抽象的特征表示,池化层对这些特征表示进行过滤,保留最关键的特征信息。

【手写数字识别】之网络结构 - 图12

图4:在处理计算机视觉任务中大放异彩的卷积神经网络


说明:

本节只介绍手写数字识别在卷积神经网络的实现以及它带来的效果提升。读者可以将卷积神经网络先简单的理解成是一种比经典的全连接神经网络更强大的模型即可,更详细的原理和实现在接下来的第四章-计算机视觉-卷积神经网络基础中讲述。


两层卷积和池化的神经网络实现如下代码所示。

  1. # 多层卷积神经网络实现
  2. class MNIST(fluid.dygraph.Layer):
  3. def __init__(self, name_scope):
  4. super(MNIST, self).__init__(name_scope)
  5. # 定义卷积层,输出特征通道num_filters设置为20,卷积核的大小filter_size为5,卷积步长stride=1,padding=2
  6. # 激活函数使用relu
  7. self.conv1 = Conv2D(num_channels=1, num_filters=20, filter_size=5, stride=1, padding=2, act='relu')
  8. # 定义池化层,池化核pool_size=2,池化步长为2,选择最大池化方式
  9. self.pool1 = Pool2D(pool_size=2, pool_stride=2, pool_type='max')
  10. # 定义卷积层,输出特征通道num_filters设置为20,卷积核的大小filter_size为5,卷积步长stride=1,padding=2
  11. self.conv2 = Conv2D(num_channels=20, num_filters=20, filter_size=5, stride=1, padding=2, act='relu')
  12. # 定义池化层,池化核pool_size=2,池化步长为2,选择最大池化方式
  13. self.pool2 = Pool2D(pool_size=2, pool_stride=2, pool_type='max')
  14. # 定义一层全连接层,输出维度是1,不使用激活函数
  15. self.fc = Linear(input_dim=980, output_dim=1, act=None)
  16. # 定义网络前向计算过程,卷积后紧接着使用池化层,最后使用全连接层计算最终输出
  17. def forward(self, inputs):
  18. x = self.conv1(inputs)
  19. x = self.pool1(x)
  20. x = self.conv2(x)
  21. x = self.pool2(x)
  22. x = fluid.layers.reshape(x, [x.shape[0], -1])
  23. x = self.fc(x)
  24. 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, parameter_list=model.parameters())
  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. predict = model(image)
  17. #计算损失,取一个批次样本损失的平均值
  18. loss = fluid.layers.square_error_cost(predict, label)
  19. avg_loss = fluid.layers.mean(loss)
  20. #每训练了100批次的数据,打印下当前Loss的情况
  21. if batch_id % 200 == 0:
  22. print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy()))
  23. #后向传播,更新参数的过程
  24. avg_loss.backward()
  25. optimizer.minimize(avg_loss)
  26. model.clear_gradients()
  27. #保存模型参数
  28. fluid.save_dygraph(model.state_dict(), 'mnist')
  1. loading mnist dataset from ./work/mnist.json.gz ......
  2. epoch: 0, batch: 0, loss is: [31.675833]
  3. epoch: 0, batch: 200, loss is: [9.248349]
  4. epoch: 0, batch: 400, loss is: [3.2532346]
  5. epoch: 1, batch: 0, loss is: [2.5735705]
  6. epoch: 1, batch: 200, loss is: [2.7086043]
  7. epoch: 1, batch: 400, loss is: [2.351327]
  8. epoch: 2, batch: 0, loss is: [2.2003784]
  9. epoch: 2, batch: 200, loss is: [2.53069]
  10. epoch: 2, batch: 400, loss is: [2.154322]
  11. epoch: 3, batch: 0, loss is: [1.8227897]
  12. epoch: 3, batch: 200, loss is: [1.8546791]
  13. epoch: 3, batch: 400, loss is: [2.3879793]
  14. epoch: 4, batch: 0, loss is: [2.6370738]
  15. epoch: 4, batch: 200, loss is: [1.6437341]
  16. epoch: 4, batch: 400, loss is: [1.6468849]

比较经典全连接神经网络和卷积神经网络的损失变化,可以发现卷积神经网络的损失值下降更快,且最终的损失值更小。