【项目05】基于LeNet的手写字体识别

作者:欧新宇(Xinyu OU)
开发平台:Paddle 2.1
运行环境:Intel Core i7-7700K CPU 4.2GHz, nVidia GeForce GTX 1080 Ti
特别注意:本案例使用Paddle 2.1进行开发,但所使用的动态图库函数为Paddle 1.8,这些API将在未来的版本中被废弃。

本教案所涉及的数据集仅用于教学和交流使用,请勿用作商用。

最后更新:2021年8月13日


【实验目的】

  1. 熟悉卷积神经网络的基本结构,特别是卷积层+激活函数+池化层的叠加结构
  2. 学会使用mini-batch方法实现卷积神经网络的训练并进行预测
  3. 学会保存模型,并使用保存的模型进行预测(即应用模型到生产环境)
  4. 学会使用函数化编程方法完成卷积神经网络的训练和测试
  5. 学会依照网络结构图构建网络模型

【实验要求】

  1. 按照给定的网络体系结构图设计卷积神经网络
  2. 使用MNIST训练集训练设计好的卷积神经网络,并给出测试集上的测试精度
  3. 对给定的测试样本进行预测,输出每一个样本的预测结果该类别的概率
  4. 尽力而为地在测试集上获得最优精度(除网络模型不能更改,其他参数均可修改)

【实验一】数据准备

一. 模型训练及评估

1.0 导入依赖及全局参数设置

1.1 数据准备

1.1.1 数据集简介

MNIST数据集包含60000个训练集和10000测试数据集。分为图片和标签,图片是28*28的像素矩阵,标签为0~9共10个数字。

Ch02ex01

1.1.2 观察和展示数据(非必须步骤)

该步骤是非必须的必要步骤非必须指它并不是训练和展示数据所必须的步骤;必要步骤是因为观察数据集有利于我们更好地选择合适的方法进行数据预处理。

1.1.3 定义数据读取器获取数据

由于MNIST是paddle内置的toy数据集,因此可以使用paddle.dataset.mnist.train()paddle.dataset.mnist.test()从直接从库中获取数量为BATCH_SIZE的数据,该接口在载入数据的同时,实现了对样本的灰度化、归一化和居中等预处理。

【实验二】 模型配置和训练

实验目的:

  1. 理解训练集、验证集、训练验证集及测试集在模型训练中的作用
  2. 掌握卷积神经网络的构建、训练方法
  3. 学会可视化训练过程
  4. 学会在线测试和离线测试两种方法

2.1 配置网络

2.1.1 网络结构的定义

LeNet-5是一个包含三个卷积层、两个最大池化层和两个全连接层的卷积神经网络,基本接入如下图所示

Ch02ex01

将网络结构图转换为配置及参数表如下。

Layer Input Kernels_num Kernels_size Stride Padding PoolingType Output Parameters
Input 1×28×28 1×28×28
Conv1 1×28×28 6 1×5×5 1 0 6×24×24 (1×5×5+1)×6=156
Pool1 6×24×24 6 6×2×2 2 0 Max 6×12×12 0
Conv2 6×12×12 16 6×5×5 1 0 16×8×8 (6×5×5+1)×16=2416
Pool2 16×8×8 16 16×2×2 2 0 Max 16×4×4 0
Conv3 16×4×4 120 16×4×4 1 0 120×1×1 (16×4×4+1)×120=30720
FC1 120×1 64×1 ×1120×64=7680
FC2 64×1 10×1 64×10=640
Output 10×1
Total = 41612

使用动态图模式比静态图要简单很多,只需要定义模型结构即可。模型定义需要使用Object-Oriented-Designed面向对象的类进行定义。以下为该前馈神经网络(多层感知机)的网络构建函数。此处我们使用动态图模式进行网络结构的定义。首先定义了一个多层感知机的类class CNN(fluid.dygraph.Layer)。在该类别使用__inin__(self)对参数进行初始化,并定义前向传播forward()方法。

# 以上是线性核Linear的说明,其中第一项为输入维度,第二项为输出维度,act为激活函数。对应于公式 $Out=Act(XW+b)$. 其中,X 为输入的 Tensor, W 和 b 分别为权重和偏置。
class paddle.fluid.dygraph.Linear(input_dim, output_dim, param_attr=None, bias_attr=None, act=None, dtype='float32')
# num_channels:输入维度, num_filters:卷积核个数, filter_size:卷积核尺度, stride:步长, padding:填充尺度, dilation:洞的尺度, dtype='float32')
class paddle.fluid.dygraph.Conv2D(num_channels, num_filters, filter_size, stride=1, padding=0, dilation=1, dtype='float32')
# input:输入维度, pool_size:池化尺度, pool_type:池化类型'max|avg', pool_stride:池化步长, pool_padding:池化填充, global_pooling:是否使用全局池化'False|True', 
class paddle.fluid.layers.pool2d(input, pool_size=-1, pool_type='max', pool_stride=1, pool_padding=0, global_pooling=False, data_format="NCHW")

2.1.2 定义神经网络类

2.1.3 测试模型并输出网络各层的超参数

2.2 定义过程可视化函数

定义训练过程中用到的可视化方法, 包括训练损失, 训练集批准确率, 测试集准确率. 根据具体的需求,可以在训练后展示这些数据和迭代次数的关系. 值得注意的是, 训练过程中可以每个epoch绘制一个数据点,也可以每个batch绘制一个数据点,也可以每个n个batch或n个epoch绘制一个数据点.

2.3 模型训练及评估

在Paddle 1.8的动态图模式下,所有的训练测试代码都需要基于动态图守护进程fluid.dygraph.guard(PLACE)

2.3.1 定义测试函数

模型测试一般包含两个类型,分别是:

  1. 在线测试,在线测试通常是针对验证集进行,并在训练过程中,每隔一定的周期输出一次精度和损失值
  2. 离线测试,指在模型训练结束之后,使用测试集进行的评估。

两种测试模式,在代码上基本上是一致的,只是所使用的数据不同。因此,我们可以定义一个eval()函数来实现代码复用,在进行评估时,分别调用val_reader()test_reader()的即可。在Paddle 1.8的动态图模式下,测试进程同样要基于动态守护框架fluid.dygraph.guard()

  1. 设置模型运行模式为验证模式model.eval()
  2. 基于周期epoch-批次batch的结构进行两层循环训练,具体包括:
    1). 定义输入层(image,label)
    2). 定义输出层,包括前向传播的输出predict=model(image)及精度accuracy。如果需要,还可以输出针对测试集的损失loss。

    值得注意的,在计算测试集精度的时候,应该是针对所有测试样本的平均精度。在获得每个批次的精度test_accuracy时,通常情况下,有以下两种处理方法:

# 方法一:计算每个批次的平均精度
   accs = []
   accs.append(test_accuracy.numpy()[0])
   avg_acc = np.mean(accs)

# 方法二:计算每个样本的平局精度
   accs = []
   accs.append(test_accuracy.numpy()[0]*num_current_batch)) 
   # 当前批次的样本数 num_current_batch = len(labels)
   avg_acc = sum(accs)/NUM_TEST

不难发现,方法二是最标准的计算方法,但相对繁琐。方法一是一种常见的方法啊,更为简单快速,但存在一定的误差。主要原因来自于最后一个批次的样本数并不等于前面的批次时,因此这种简化会带来一定的误差。

2.3.2 定义训练函数

在动态图模式下,所有的训练测试代码都需要基于动态图守护进程fluid.dygraph.guard(PLACE)

训练部分的具体流程包括:

  1. 模型实例化,并设置为训练模式model.train()
  2. 定义优化器optimizer
  3. 基于周期epoch-批次batch的结构进行两层循环训练,具体包括:
    1). 定义输入层(image,label),图像输入维度 [batch, channel, Width, Height] (-1,3,32,32),标签输入维度 [batch, 1] (-1,1)
    2). 定义输出层,包括前向传播的输出predict=model(image),损失loss及平均损失,精度accuracy。
    3). 执行反向传播,并将损失最小化,清除梯度

在训练过程中,可以将周期,批次,损失及精度等信息打印到屏幕。

【注意】

  1. 值得注意的是,在每一轮的训练中,每N个batch之后会输出一次平均训练误差和准确率。每一轮训练之后,使用测试集进行一次测试,在每轮测试中,均打输出一次平均测试误差和准确.

  2. 在下列的代码中,我们每个(若干个)epoch都执行一次模型保存,这种方式一般应用在复杂的模型和大型数据集上。这种经常性的模型保存,有利于我们执行EarlyStopping策略,当我们发现运行曲线不再继续收敛时,就可以结束训练,并选择之前保存的最好的一个模型作为最终的模型。

  3. 在下面的代码中,我们设置了一个最优精度,并将每次对验证集的评估与最优精度进行对比,若当前的验证精度比最优精度更好,则将当前轮次的模型保存为最终模型;若当前验证精度比最优精度差,则跳过,继续进行训练。

2.3.3 模型训练及在线测试

2.3.4 离线测试

离线测试同样要基于动态守护框架fluid.dygraph.guard()。测试过程与训练过程中的在线测试流程基本一致。只是需要实现载入已保存的模型,可以使用fluid.load_dygraph()方法。 观察训练过程,我们可以使用np.argmax()获取测试性能最好的模型作为最终模型进行使用。

【实验三】 模型评估与推理(应用)

实验目的:

  1. 学会载入已训练好的模型
  2. 掌握模型的推理方法

在实际应用中,下面的工作可以视作是生产环境的具体应用。在研究中,也可以当作最终的结果对比。模型预测包含三个部分:

  1. 导入依赖库及全局参数配置
  2. 获取待预测数据,并进行初始化
  3. 载入模型并开始进行预测

3.1 导入依赖库及全局参数配置

3.2 获取待预测数据

获取待测数据包含两个步骤: 1.获取图像路径和标签; 2. 根据图像路径读取图像并进行预处理。两个功能分别由下面两个函数实现:

3.3 载入模型并开始进行预测

# 模型定义
class CNN(fluid.dygraph.Layer)