【项目04】基于卷积神经网络N的图片识别 —— 车牌识别

作者:欧新宇(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月14日


【实验目的】

  1. 学会对下载的数据集进行初步整理,包括处理非法文件名、创建数据列表
  2. 学会将数据集划分为训练集、验证集和测试集
  3. 学会按照神经网络的设计要求创建神经网络类
  4. 学会使用mini-batch方法实现卷积神经网络的训练并进行预测
  5. 学会保存模型,并使用保存的模型进行预测(即应用模型到生产环境)
  6. 学会使用函数化编程方法完成卷积神经网络的训练和测试
  7. 学会在已有代码的基础上完成新任务的迭代

【实验要求】

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

【实验一】 数据集准备

实验摘要: 对于模型训练的任务,需要数据预处理,将数据整理成为适合给模型训练使用的格式。车牌识别的项目是一个多分类的任务,数据集中有65个文件夹,总共1602张图片,包含从0-9,A-Z,以及各省简称的图片。每个图片都是1×20×20的灰度图像,我们需要将图片读入,并按照1:9划分测试集和训练集。

实验目的:

  1. 学会观察数据集的文件,并实现基本的数据清理方法,包括删除无法读取的样本、处理冗长不合规范的文件命名等
  2. 能够按照训练集、验证集、训练验证集、测试集四种子集对数据集进行划分,并生成数据列表
  3. 能简单展示和预览数据的基本信息,包括数据量,规模,数据类型和位深度等

1.1 数据集介绍

VehicleLicense 车牌识别数据集包含16151张单字符数据,所有的单字符均为严格切割且都转换为黑白二值图像(如下第一行:训练数据所示)。真实检测的数据如下图(第二行:原始车牌)所示。第三行处理后的车牌是根据真实检测的车牌进行精致编辑,总共包含8幅720×170的测试样本(test01-08)。

注意:由于本例中的测试代码并没有包含严格图像分割及预处理代码,因此无法很好识别原始车牌及非标准车牌(标准车牌为蓝底白字,光线充足),此例仅供简单验证。

数据集中包含三个文件夹:Data, Infer, Infer0。其中dataset为训练验证测试数据,Infer为处理后的车牌,Infer0为原始车牌。

Ch02ex01

1.2 数据集预处理

在很多时候,下载好的数据集无法直接使用,需要对其进行一定的预处理,并且这种预处理并没有通用的代码。但大体上可以分为以下几点:

值得注意的是,与数据预处理需要在每次训练和测试前执行不同。由于上面所描述的关于数据集的预处理并不需要多次执行,这主要是因为:

  1. 只需要执行一次即可生成相应的内容;
  2. 这些处理可能会比较耗时。
    因此该类代码通常是独立于模型训练,只需要完成一次即可。

1.2.1 处理数据集中样本命名的非法字符

原始的数据集的名字有可能会存在特殊的命名符号,从而导致在某些情况下无法正确识别。因此,可以通过批量改名的重命名方式来解决该问题。

1.2.2 生成数据列表

若官方数据集没有数据集的划分列表,或者数据集为自建数据集,则需要手动生成数据集的划分,一般包括训练集、验证集和测试集。

值得注意的是,在进行数据划分的时候要注意类别的均衡性,处理的方法主要有两种。一是,对所有样本进行打乱,再进行划分;二是,顺序遍历不同类别的文件夹,然后均匀划分。下面的代码属于第二种。有兴趣的同学可以尝试第一种方法。例如在1.2.1节改名的时候,就收集文件名,并进行打乱。

【实验二】 全局参数设置及数据准备

实验摘要: 车牌识别是一个多分类问题,我们通过卷积神经网络来完成。这部分通过PaddlePaddle构造一个LeNet卷积神经的网络,最后一层采用Softmax激活函数完成分类任务。

实验目的:

  1. 学会使用配置文件定义全局参数
  2. 学会设置和载入数据集
  3. 学会对输入样本进行基本的预处理
  4. 学会定义可视化函数,可视化训练过程

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

2.2 数据预处理

2.3 设置训练和测试数据提供器

对于要使用的所有数据均需要设置数据提供器,本例我们给出基于训练集、验证集和测试集和训练验证集划分的设置。

2.4 定义过程可视化函数

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

【实验三】 模型训练与评估

实验摘要: 车牌识别是一个多分类问题,我们通过卷积神经网络来完成。这部分通过PaddlePaddle构造一个LeNet卷积神经的网络,最后一层采用Softmax激活函数完成分类任务。

实验目的:

  1. 掌握卷积神经网络的构建和基本原理
  2. 深刻理解训练集、验证集、训练验证集及测试集在模型训练中的作用
  3. 学会在线测试和离线测试两种测试方法

3.1 配置网络

3.1.1 网络拓扑结构图

3.1.2 网络参数配置表

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

Layer Input Kernels_num Kernels_size Stride Padding PoolingType Output Parameters
Input 1×20×20
Conv1 1×20×20 28 1×5×5 1 0 28×16×16 (1×5×5+1)×28=728
Pool1 28×16×16 28 6×2×2 1 0 max 28×15×15 0
Conv2 28×15×15 32 28×3×3 1 0 32×13×13 (28×3×3+1)×32=8096
Pool2 32×13×13 16 32×2×2 2 0 max 32×12×12 0
Conv3 32×12×12 32 32×3×3 1 0 32×10×10 (32×3×3+1)×32=9248
FC1 (32×10×10)×1 65×1 (32×10×10+1)×65=208065
Output 65×1
Total = 226137

使用动态图模式比静态图要简单很多,只需要定义模型结构即可。模型定义需要使用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")

3.1.3 定义神经网络类

3.2 模型训练及评估

3.2.1 定义测试函数

测试部分的具体流程包括:

  1. 设置模型运行模式为验证模式model.eval()
  2. 基于周期epoch-批次batch的结构进行两层循环训练,具体包括:
    1). 定义输入层(image,label),图像输入维度 [batch, channel, Width, Height] (-1,imgChannel,imgSize,imgSize),标签输入维度 [batch, 1] (-1,1)
    2). 定义输出层,包括前向传播的输出predict=model(image)及精度accuracy。如果需要,还可以输出针对测试集的损失loss。
    值得注意的,在计算测试集精度的时候,需要对每个批次的精度/损失求取平均值。

在定义test()函数的时候,我们需要为其指定两个参数:model是测试的模型,data_reader是迭代的数据读取器,取值为val_reader(), test_reader(),分别对验证集和测试集。此处验证集和测试集数据的测试过程是相同的,只是所使用的数据不同。

3.2.2 定义训练函数

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

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

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

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

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

【注意】注意在下列的代码中,我们每个epoch都执行一次模型保存,这种方式一般应用在复杂的模型和大型数据集上。这种经常性的模型保存,有利于我们执行EarlyStopping策略,当我们发现运行曲线不再继续收敛时,就可以结束训练。下面的代码中,我们将保存验证集上精度最高的模型作为best_model,同时保存训练结束时的模型model_final.

3.2.3 训练主函数

将训练过程中的损失函数和模型在训练集上的准确率可视化,有助于发现模型在训练中遇到的问题。损失函数小幅震荡属于正常现象,总体向下即可。
可以查看到损失值趋势下降,准确度在上升的趋势,趋近90~100%。

3.2.4 离线测试

离线测试同样要基于动态守护框架fluid.dygraph.guard()。测试过程与训练过程中的在线测试流程基本一致,只需要提前实现载入已保存的模型即可,载入模型使用fluid.load_dygraph()方法。

【结果分析】

需要注意的是此处的精度与训练过程中输出的测试精度是不相同的,因为训练过程中使用的是验证集VehicleLicense_val, 而这里的离线测试使用的是测试集VehicleLicense_test.

【实验四】 模型预测(应用)

实验摘要: 对训练过的模型,我们通过测试集进行模型效果评估,并可以在实际场景中进行预测,查看模型的效果。

实验目的:

  1. 掌握模型转换与推理

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

4.2 获取待预测数据及数据预处理

在预测之前,通常需要对图像进行预处理。此处的预处理方案和训练模型时所使用的预处理方案必须是一致的。针对车牌识别等大多数字符识别任务,一般包含以下三个步骤:

  1. 将所有样本转换为灰度图,并通过阈值变换将其转换为二值图像。其中黑色部分为背景,白色部分为字符 color2bin()
  2. 将二值图像按照字符进行分割,每个字符构成一个子文件。本例中使用比较粗糙的方法(等间隔)进行拆分 Segmentation()
  3. 将每个字符进行二次预处理,包括缩放至训练样本的尺度(20×20),数据格式规范为paddle要求的浮点型数据,且形态为[C,H,W],数值归一化到[0~1] load_image()

值得注意的是,二值化灰度图有利于提高系统的识别性能,是灰度图像预处理的一个重要步骤,在允许的情况下,尽量执行该操作。但选择二值分割阈值是一件经验性的数据驱动型工作,需要慎重选择。

4.3 载入模型并开始进行推理

车牌识别需要有两个过程,1. 对原始车牌进行分割,分割成单字符;2. 对单字符进行预测,并输出预测结果

【结果分析】

本例代码并没有做严格的检测和分割,也没有做严格的预处理(例如光照和色彩),因此识别系统限制较多。例如:

  1. 原始车牌无法识别,包括尺度不规范的图片
  2. 色彩不符合规则的无法识别,例如 test04, test07
  3. 光照不足的无法识别,例如 test05
  4. 车牌不规范的无法很好识别,例如 test08

有兴趣的同学建议进行一定的改进,处理以上问题。改进版的“车牌识别系统”,可以作为毕业设计(论文)进行提交,或用于参加各种计算机的竞赛。