作者:欧新宇(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日
卷积神经网络
验证集精度
测试精度
预测结果
及该类别的概率
。实验摘要: 对于模型训练的任务,需要数据预处理,将数据整理成为适合给模型训练使用的格式。车牌识别的项目是一个多分类的任务,数据集中有65个文件夹,总共1602张图片,包含从0-9,A-Z,以及各省简称的图片。每个图片都是1×20×20的灰度图像,我们需要将图片读入,并按照1:9划分测试集和训练集。
实验目的:
VehicleLicense 车牌识别数据集包含16151张单字符数据,所有的单字符均为严格切割且都转换为黑白二值图像(如下第一行:训练数据所示)。真实检测的数据如下图(第二行:原始车牌)所示。第三行处理后的车牌是根据真实检测的车牌进行精致编辑,总共包含8幅720×170的测试样本(test01-08)。
注意:由于本例中的测试代码并没有包含严格图像分割及预处理代码,因此无法很好识别原始车牌及非标准车牌(标准车牌为蓝底白字,光线充足),此例仅供简单验证。
数据集中包含三个文件夹:Data, Infer, Infer0。其中dataset为训练验证测试数据,Infer为处理后的车牌,Infer0为原始车牌。
在很多时候,下载好的数据集无法直接使用,需要对其进行一定的预处理,并且这种预处理并没有通用的代码。但大体上可以分为以下几点:
值得注意的是,与数据预处理需要在每次训练和测试前执行不同。由于上面所描述的关于数据集的预处理并不需要多次执行,这主要是因为:
原始的数据集的名字有可能会存在特殊的命名符号,从而导致在某些情况下无法正确识别。因此,可以通过批量改名的重命名方式来解决该问题。
# ##################################################################################
# # 数据清理
# # 作者: Xinyu Ou (http://ouxinyu.cn)
# # 数据集名称:车牌识别数据集
# # 数据集简介: VehicleLicense车牌识别数据集包含16151张单字符数据,所有的单字符均为严格切割且都转换为黑白二值图像。
# # 本程序功能:
# # 1. 对样本文件进行改名,屏蔽特殊命名符号对训练的影响
# ###################################################################################
# import os
# dataset_root_path = 'D:\\WorkSpace\\ExpDatasets\\VehicleLicense'
# data_path = os.path.join(dataset_root_path, 'Data')
# character_folders = os.listdir(data_path)
# num_image = 0
# for character_folder in character_folders:
# character_imgs = os.listdir(os.path.join(data_path, character_folder))
# id = 0
# for character_img in character_imgs:
# newname = character_folder + '_' + str(id).rjust(4,'0') + os.path.splitext(character_img)[1]
# os.rename(os.path.join(data_path, character_folder, character_img), os.path.join(data_path, character_folder, newname))
# id += 1
# num_image += 1
# print('\r 已完成{}副图片的改名'.format(num_image), end='')
# print(', 已完成。')
若官方数据集没有数据集的划分列表,或者数据集为自建数据集,则需要手动生成数据集的划分,一般包括训练集、验证集和测试集。
值得注意的是,在进行数据划分的时候要注意类别的均衡性,处理的方法主要有两种。一是,对所有样本进行打乱,再进行划分;二是,顺序遍历不同类别的文件夹,然后均匀划分。下面的代码属于第二种。有兴趣的同学可以尝试第一种方法。例如在1.2.1节改名的时候,就收集文件名,并进行打乱。
##################################################################################
# 数据集预处理
# 作者: Xinyu Ou (http://ouxinyu.cn)
# 数据集名称:车牌识别数据集
# 数据集简介: VehicleLicense车牌识别数据集包含16151张单字符数据,所有的单字符均为严格切割且都转换为黑白二值图像。
# 本程序功能:
# 1. 将数据集按照7:1:2的比例划分为训练验证集、训练集、验证集、测试集
# 2. 代码将生成4个文件:训练验证集trainval.txt, 训练集列表train.txt, 验证集列表val.txt, 测试集列表test.txt, 数据集信息dataset_info.json
# 3. 代码输出信息:图像列表已生成, 其中训练验证集样本12877,训练集样本11232个, 验证集样本1645个, 测试集样本3274个, 共计16151个。
# 4. 生成数据集标签词典时,需要根据标签-文件夹列表匹配标签列表
###################################################################################
import os
import json
import codecs
num_trainval = 0
num_train = 0
num_val = 0
num_test = 0
class_dim = 0
dataset_info = {
'dataset_name': '',
'num_trainval': -1,
'num_train': -1,
'num_val': -1,
'num_test': -1,
'class_dim': -1,
'label_dict': {}
}
# 本地运行时,需要修改数据集的名称和绝对路径,注意和文件夹名称一致
dataset_name = 'VehicleLicense'
dataset_path = 'D:\\Workspace\\ExpDatasets\\'
dataset_root_path = os.path.join(dataset_path, dataset_name)
excluded_folder = ['.DS_Store', '.ipynb_checkpoints'] # 被排除的文件夹
# 获取标签和文件夹的对应关系,即省市拼音和中文对照关系
json_label_match = os.path.join(dataset_root_path, 'label_match.json')
label_match = json.loads(open(json_label_match, 'r', encoding='utf-8').read())
# 定义生成文件的路径
data_path = os.path.join(dataset_root_path, 'Data')
trainval_list = os.path.join(dataset_root_path, 'trainval.txt')
train_list = os.path.join(dataset_root_path, 'train.txt')
val_list = os.path.join(dataset_root_path, 'val.txt')
test_list = os.path.join(dataset_root_path, 'test.txt')
dataset_info_list = os.path.join(dataset_root_path, 'dataset_info.json')
# 检测数据集列表是否存在,如果存在则先删除。其中测试集列表是一次写入,因此可以通过'w'参数进行覆盖写入,而不用进行手动删除。
if os.path.exists(trainval_list):
os.remove(trainval_list)
if os.path.exists(train_list):
os.remove(train_list)
if os.path.exists(val_list):
os.remove(val_list)
if os.path.exists(test_list):
os.remove(test_list)
# 按照比例进行数据分割
class_name_list = os.listdir(data_path)
with codecs.open(trainval_list, 'a', 'utf-8') as f_trainval:
with codecs.open(train_list, 'a', 'utf-8') as f_train:
with codecs.open(val_list, 'a', 'utf-8') as f_val:
with codecs.open(test_list, 'a', 'utf-8') as f_test:
for class_name in class_name_list:
if class_name not in excluded_folder:
dataset_info['label_dict'][str(class_dim)] = label_match[class_name] # 按照文件夹名称和label_match进行标签匹配
images = os.listdir(os.path.join(data_path, class_name))
count = 0
for image in images:
if count % 10 == 0: # 抽取大约10%的样本作为验证数据
f_val.write("{0}\t{1}\n".format(os.path.join(data_path, class_name, image), class_dim))
f_trainval.write("{0}\t{1}\n".format(os.path.join(data_path, class_name, image), class_dim))
num_val += 1
num_trainval += 1
elif count % 10 == 1 or count % 10 == 2: # 抽取大约20%的样本作为测试数据
f_test.write("{0}\t{1}\n".format(os.path.join(data_path, class_name, image), class_dim))
num_test += 1
else:
f_train.write("{0}\t{1}\n".format(os.path.join(data_path, class_name, image), class_dim))
f_trainval.write("{0}\t{1}\n".format(os.path.join(data_path, class_name, image), class_dim))
num_train += 1
num_trainval += 1
count += 1
class_dim += 1
# 将数据集信息保存到json文件中供训练时使用
dataset_info['dataset_name'] = dataset_name
dataset_info['num_trainval'] = num_trainval
dataset_info['num_train'] = num_train
dataset_info['num_val'] = num_val
dataset_info['num_test'] = num_test
dataset_info['class_dim'] = class_dim
with codecs.open(dataset_info_list, 'w', encoding='utf-8') as f_dataset_info:
json.dump(dataset_info, f_dataset_info, ensure_ascii=False, indent=4, separators=(',', ':')) # 格式化字典格式的参数列表
print("图像列表已生成, 其中训练验证集样本{},训练集样本{}个, 验证集样本{}个, 测试集样本{}个, 共计{}个。".format(num_trainval, num_train, num_val, num_test, num_train+num_val+num_test))
图像列表已生成, 其中训练验证集样本12878,训练集样本11233个, 验证集样本1645个, 测试集样本3274个, 共计16152个。
实验摘要: 车牌识别是一个多分类问题,我们通过卷积神经网络来完成。这部分通过PaddlePaddle构造一个LeNet卷积神经的网络,最后一层采用Softmax激活函数完成分类任务。
实验目的:
# 1. 导入依赖库
import os
import cv2
import numpy as np
import codecs
import json
import time # 载入time时间库,用于计算训练时间
from random import randint # 导入随机数生成函数
import paddle as paddle # 载入PaddlePaddle基本库
import paddle.fluid as fluid # 载入基于fluid框架的paddle
from paddle.fluid.dygraph import Linear, Conv2D, Pool2D
from PIL import Image # 载入python的第三方图像处理库
import matplotlib.pyplot as plt # 载入matplotlib绘图库
from multiprocessing import cpu_count
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['font.sans-serif'] = 'SimHei,Times New Roman'# 中文设置成宋体,除此之外的字体设置成New Roman
np.set_printoptions(precision=5, suppress=True) # 设置numpy的精度,用于打印输出
# 2. 全局参数配置
# 定义使用CPU还是GPU,使用CPU时use_cuda = False,使用GPU时use_cuda = True
use_cuda = True # True, False 如果设备有GPU,怎么我们可以启用GPU进行快速训练
PLACE = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()
project_name = 'Project06CNN'
architecture = 'CNN'
dataset_name = 'VehicleLicense'
result_root_path = 'D:\\WorkSpace\\ExpResults\\'
model_name = dataset_name + '_' + architecture
# 2.1 定义数据集列表文件及模型路径
dataset_path = 'D:\\WorkSpace\\ExpDatasets\\'
dataset_root_path = os.path.join(dataset_path, dataset_name)
trainval_list = os.path.join(dataset_root_path, 'trainval.txt')
train_list = os.path.join(dataset_root_path, 'train.txt')
val_list = os.path.join(dataset_root_path, 'val.txt')
test_list = os.path.join(dataset_root_path, 'test.txt')
result_root_path = os.path.join(result_root_path, project_name, model_name)
final_models_path = os.path.join(result_root_path, 'final_models')
final_figures_path = os.path.join(result_root_path, 'final_figures')
# 2.2 图像基本信息
img_size = 20
img_channel = 1
# 2.3 训练参数定义
total_epoch = 40 # 总迭代次数, 代码调试好后考虑Epochs_num = 50
log_interval = 100
eval_interval = 1 # 设置在训练过程中,每隔一定的周期进行一次测试
learning_rate = 0.001 # 学习率
momentum = 0.9 # 动量
BUF_SIZE = 512 # 设置存储数据的缓存大小
BATCH_SIZE = 128 # 设置每个批次的数据大小,同时对训练提供器和测试提供器有效
# 定义数据集映射函数获取数据的图像矩阵和label
def data_mapper(sample):
img, label = sample
img = cv2.imread(img, 0)
img = cv2.resize(img, (img_size, img_size)) # 将图像尺度resize为指定尺寸
img = np.array(img).reshape(1, img_channel, img_size, img_size).astype('float32') # 将图像数据类型转化为float32
# img = img.transpose((2, 0, 1)) # 调整数据形状paddle默认格式(通道,高度,宽度)
img = img/255.0 # 将像素值归一化到[0,1]之间
return img, label
# 定义数据集reader,用于从列表文件中批量获取图像
def data_reader(data_list_path):
#定义读取函数,从列表文件中读取
def reader():
with open(data_list_path, 'r') as f:
lines = f.readlines()
for line in lines:
img_path, label = line.split('\t')
yield img_path, int(label)
#使用多线程方式,通过用户自定义的映射器mapper来映射reader返回的样本(到输出队列)
return paddle.reader.xmap_readers(data_mapper, reader, cpu_count(), 512)
C:\Users\Administrator\anaconda3\lib\site-packages\ipykernel\ipkernel.py:287: DeprecationWarning: `should_run_async` will not call `transform_cell` automatically in the future. Please pass the result to `transformed_cell` argument and any exception that happen during thetransform in `preprocessing_exc_tuple` in IPython 7.17 and above. and should_run_async(code)
对于要使用的所有数据均需要设置数据提供器,本例我们给出基于训练集、验证集和测试集和训练验证集划分的设置。
# 用于训练/测试的数据提供器,每次从缓存中随机读取批次大小的数据
trainval_reader = paddle.batch(paddle.reader.shuffle(reader=data_reader(trainval_list), buf_size=BUF_SIZE), batch_size=BATCH_SIZE, drop_last=False)
train_reader = paddle.batch(paddle.reader.shuffle(reader=data_reader(train_list), buf_size=BUF_SIZE), batch_size=BATCH_SIZE, drop_last=False)
val_reader = paddle.batch(paddle.reader.shuffle(reader=data_reader(val_list), buf_size=BUF_SIZE), batch_size=BATCH_SIZE, drop_last=False)
test_reader = paddle.batch(paddle.reader.shuffle(reader=data_reader(test_list), buf_size=BUF_SIZE), batch_size=BATCH_SIZE, drop_last=False)
# 测试:输出第0个batch的数据形态
for batch_id, data in enumerate(train_reader()):
print(data[0][0].shape)
break
(1, 1, 20, 20)
定义训练过程中用到的可视化方法, 包括训练损失, 训练集批准确率, 测试集准确率. 根据具体的需求,可以在训练后展示这些数据和迭代次数的关系. 值得注意的是, 训练过程中可以每个epoch绘制一个数据点,也可以每个batch绘制一个数据点,也可以每个n个batch或n个epoch绘制一个数据点.
# 绘制训练batch精度和平均loss
def draw_process(title, loss_label, accuracy_label, iters, losses, accuracies, figure_path=None, figurename=None, isShow=False):
# 1.第一组坐标轴 Loss
_, ax1 = plt.subplots() # plt.subplots(figsize=(14,6))
ax1.plot(iters, losses, color='red', label=loss_label)
ax1.set_xlabel('Iters', fontsize=20)
ax1.set_ylabel(loss_label, fontsize=20)
max_loss = max(losses)
ax1.set_ylim(0, max_loss*1.2)
# 2.第二组坐标轴 accuracy
ax2 = ax1.twinx()
ax2.plot(iters, accuracies, color='blue', label=accuracy_label)
ax2.set_ylabel(accuracy_label, fontsize=20)
max_acc = max(accuracies)
ax2.set_ylim(0, max_acc*1.2)
# 3.配置图例
plt.title(title, fontsize=24)
handles1, labels1 = ax1.get_legend_handles_labels()
handles2, labels2 = ax2.get_legend_handles_labels()
plt.legend(handles1+handles2, labels1+labels2, loc='best')
plt.grid()
# 4.将绘图结果保存到 final_figures 目录
plt.savefig(os.path.join(figure_path, figurename + '.png'))
# 5.显示绘图结果
if isShow is True:
plt.show()
### 测试可视化函数 ###################################################
if __name__ == '__main__':
try:
train_log = json.loads(open(os.path.join(final_figures_path, 'train.json'), 'r', encoding='utf-8').read())
draw_process('Training', 'loss', 'accuracy', train_log['iters'], train_log['losses'], train_log['accs_top1'], figure_path=final_figures_path, figurename='train', isShow=True)
except:
print('数据不存在,无法进行绘制')
实验摘要: 车牌识别是一个多分类问题,我们通过卷积神经网络来完成。这部分通过PaddlePaddle构造一个LeNet卷积神经的网络,最后一层采用Softmax激活函数完成分类任务。
实验目的:
无
将网络结构图转换为配置及参数表如下。
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")
# 定义多层感知机(CNN)
class CNN(fluid.dygraph.Layer):
name_scope = 'CNN'
def __init__(self, num_classes=65): # 初始化CNN类,并为CNN增加对象self.x
super(CNN, self).__init__()
self.conv1 = Conv2D(num_channels=1, num_filters=28, filter_size=5, stride=1, act='relu')
self.pool1 = Pool2D(pool_size=2, pool_stride=1, pool_type='max')
self.conv2 = Conv2D(num_channels=28, num_filters=32, filter_size=3, stride=1, act='relu')
self.pool2 = Pool2D(pool_size=2, pool_stride=1, pool_type='max')
self.conv3 = Conv2D(num_channels=32, num_filters=32, filter_size=3, stride=1, act='relu')
self.fc1 = Linear(input_dim=32*10*10, output_dim=num_classes)
def forward(self,input): # 为CN类增加forward方法
# print(input.shape)
x = self.conv1(input)
x = self.pool1(x)
x = self.conv2(x)
x = self.pool2(x)
x = self.conv3(x)
x = fluid.layers.reshape(x, [x.shape[0], -1]) # x_shape=[32*10*10, -1]
y = self.fc1(x)
return y
# Best result: epoch=94,
# 测试集精度accuracy=[0.97664]
if __name__ == '__main__':
model = CNN()
paddle.summary(model, (10,1,20,20))
--------------------------------------------------------------------------- Layer (type) Input Shape Output Shape Param # =========================================================================== Conv2D-1 [[10, 1, 20, 20]] [10, 28, 16, 16] 728 Pool2D-1 [[10, 28, 16, 16]] [10, 28, 15, 15] 0 Conv2D-2 [[10, 28, 15, 15]] [10, 32, 13, 13] 8,096 Pool2D-2 [[10, 32, 13, 13]] [10, 32, 12, 12] 0 Conv2D-3 [[10, 32, 12, 12]] [10, 32, 10, 10] 9,248 Linear-1 [[10, 3200]] [10, 65] 208,065 =========================================================================== Total params: 226,137 Trainable params: 226,137 Non-trainable params: 0 --------------------------------------------------------------------------- Input size (MB): 0.02 Forward/backward pass size (MB): 2.04 Params size (MB): 0.86 Estimated Total Size (MB): 2.92 ---------------------------------------------------------------------------
测试部分的具体流程包括:
在定义test()函数的时候,我们需要为其指定两个参数:model
是测试的模型,data_reader
是迭代的数据读取器,取值为val_reader(), test_reader(),分别对验证集和测试集。此处验证集和测试集数据的测试过程是相同的,只是所使用的数据不同。
def test(model, data_reader):
accs = []
losses = []
model.eval() #评估模式
for batch_id,data in enumerate(data_reader):#测试集
images = np.array([x[0] for x in data], dtype='float32').reshape(-1, img_channel, img_size, img_size)
labels = np.array([x[1] for x in data], dtype='int64').reshape(-1,1)
image = fluid.dygraph.to_variable(images)
label = fluid.dygraph.to_variable(labels)
digits = model(image)
predict = fluid.layers.softmax(digits)
loss = fluid.layers.cross_entropy(predict, label) # 获取批loss值
loss = fluid.layers.mean(loss) # 求单个样本的loss
acc = fluid.layers.accuracy(predict,label)
losses.append(loss.numpy())
accs.append(acc.numpy())
avg_loss = np.mean(losses)
avg_acc = np.mean(accs)
return avg_loss, avg_acc
在动态图模式下,所有的训练
和测试
代码都需要基于动态图守护进程fluid.dygraph.guard(PLACE)
。
训练部分的具体流程包括:
在训练过程中,可以将周期,批次,损失及精度等信息打印到屏幕。
值得注意的是,在每一轮的训练中,每100个batch之后会输出一次平均训练误差和准确率。每一轮训练之后,使用测试集进行一次测试,在每轮测试中,均打输出一次平均测试误差和准确率。
【注意】注意在下列的代码中,我们每个epoch都执行一次模型保存,这种方式一般应用在复杂的模型和大型数据集上。这种经常性的模型保存,有利于我们执行EarlyStopping策略,当我们发现运行曲线不再继续收敛时,就可以结束训练。下面的代码中,我们将保存验证集上精度最高的模型作为best_model
,同时保存训练结束时的模型model_final
.
def train(model):
# 启动训练和在线测试
start = time.perf_counter()
print('启动训练...')
optimizer = fluid.optimizer.Momentum(learning_rate=learning_rate, momentum=momentum, parameter_list=model.parameters())#优化器选用SGD随机梯度下降,学习率为0.001.
# optimizer = fluid.optimizer.AdamOptimizer(learning_rate=learning_rate, parameter_list=model.parameters())#优化器选用SGD随机梯度下降,学习率为0.001.
# optimizer = fluid.optimizer.SGDOptimizer(learning_rate=learning_rate, parameter_list=model.parameters())#优化器选用SGD随机梯度下降,学习率为0.001.
num_batch = 0
best_result = 0
best_result_id = 0
elapsed = 0
for epoch in range(1, total_epoch+1):
model.train() #训练模式
for batch_id, data in enumerate(train_reader()):
num_batch += 1
# 定义输入层数据的形状和类型
images = np.array([x[0] for x in data], dtype='float32').reshape(-1, img_channel, img_size, img_size)
labels = np.array([x[1] for x in data], dtype='int64').reshape(-1,1)
image = fluid.dygraph.to_variable(images)
label = fluid.dygraph.to_variable(labels)
# 定义输出层 loss+accuracy
# 预测结果 = softmax(预测概率),模型的输出是预测概率
# 损失loss = cross_entropy(预测结果 与 label 之间的距离)
# 精度acc = accuracy(预测结果 与 label之间的距离)
digits = model(image) # 前向传播输出的分值,未进行归一化
predict = fluid.layers.softmax(digits) # 预测结果,归一化概率 = softmax(输出概率)
# print(digits)
# print(predict)
loss = fluid.layers.cross_entropy(predict, label)
avg_loss = fluid.layers.mean(loss) # 获取一批的平均loss值
acc = fluid.layers.accuracy(predict, label) # 计算一批的精度
# 执行反向传播算法
avg_loss.backward() # 使用backward() 方法可以执行反向网络
optimizer.minimize(avg_loss)
model.clear_gradients() # 将参数梯度清零以保证下一轮训练的正确性
# 每隔log_interval个batch打印一次训练损失, 也可根据TOTAL_EPOCH设定按照周期epoch进行输出
if num_batch % log_interval == 0: # 每log_interval个batch打印一次信息
elapsed_step = time.perf_counter() - elapsed - start
elapsed = time.perf_counter() - start
print("Epoch:{}/{}, batch:{}, train_loss:{}, train_accuracy:{} ({:.2f}s)".format(epoch,total_epoch,num_batch,avg_loss.numpy(),acc.numpy(),elapsed_step))
# 记录训练过程,用于可视化训练过程中的loss和accuracy
train_log['iters'].append(num_batch)
train_log['losses'].append(float(avg_loss))
train_log['accs_top1'].append(float(acc))
# 每隔一定周期进行一次测试
if epoch % eval_interval == 0 or epoch == total_epoch:
#模型校验
val_avg_loss, val_avg_acc = test(model, val_reader())
print('[validation] Epoch:{}/{}, test_loss:[{:.5f}], test_accuracy:[{:.5f}]'.format(epoch, total_epoch, val_avg_loss, val_avg_acc))
# 将性能最好的模型保存为final模型
if val_avg_acc > best_result:
best_result = val_avg_acc
best_result_id = epoch
# 保存最优模型
fluid.save_dygraph(model.state_dict(), os.path.join(final_models_path, 'best_model'))
print('当前性能最好的模型 epoch_{} 的精度: {:.5f}, 已将其赋值为:best_model'.format(best_result_id, best_result))
# 记录测试过程,用于可视化训练过程中的loss和accuracy
val_log['iters'].append(epoch)
val_log['losses'].append(float(val_avg_loss))
val_log['accs_top1'].append(float(val_avg_acc))
# 输出训练过程图
# 将日志字典保存为json格式,绘图数据可以在训练结束后自动显示,也可以在训练中手动执行以显示结果
if not os.path.exists(final_figures_path):
os.makedirs(final_figures_path)
with codecs.open(os.path.join(final_figures_path, 'train.json'), 'w', encoding='utf-8') as f_train_log:
json.dump(train_log, f_train_log, ensure_ascii=False, indent=4, separators=(',', ':'))
with codecs.open(os.path.join(final_figures_path, 'val.json'), 'w', encoding='utf-8') as f_val_log:
json.dump(val_log, f_val_log, ensure_ascii=False, indent=4, separators=(',', ':'))
print('训练完成,最终性能accuracy={:.5f}(epoch={}), 总耗时{:.2f}s, 已将其保存为:best_model'.format(best_result, best_result_id, time.perf_counter() - start))
draw_process("Training Process", 'Train Loss', 'Train Accuracy', train_log['iters'], train_log['losses'], train_log['accs_top1'], figure_path=final_figures_path, figurename='train')
draw_process("Validation Results", 'Validation Loss', 'Validation Accuracy', val_log['iters'], val_log['losses'], val_log['accs_top1'], figure_path=final_figures_path, figurename='val')
if __name__ == '__main__':
# 初始化绘图列表
train_log = {'iters': [], 'losses': [], 'accs_top1': []}
val_log = {'iters': [], 'losses': [], 'accs_top1': []}
with fluid.dygraph.guard(PLACE):
model = CNN() #模型实例化
# 启动训练过程
train(model)
# 训练完成,最终性能accuracy=0.97864(epoch=10), 总耗时52.69s, 已将其保存为:best_model
# 训练完成,最终性能accuracy=0.98348(epoch=19), 总耗时105.66s, 已将其保存为:best_model
启动训练... [validation] Epoch:1/40, test_loss:[4.07275], test_accuracy:[0.14423] 当前性能最好的模型 epoch_1 的精度: 0.14423, 已将其赋值为:best_model Epoch:2/40, batch:100, train_loss:[3.07254], train_accuracy:[0.] (0.90s) [validation] Epoch:2/40, test_loss:[3.97742], test_accuracy:[0.17668] 当前性能最好的模型 epoch_2 的精度: 0.17668, 已将其赋值为:best_model Epoch:3/40, batch:200, train_loss:[2.03744], train_accuracy:[0.19531] (0.87s) [validation] Epoch:3/40, test_loss:[3.99710], test_accuracy:[0.12200] 当前性能最好的模型 epoch_2 的精度: 0.17668, 已将其赋值为:best_model Epoch:4/40, batch:300, train_loss:[0.0494], train_accuracy:[1.] (0.87s) [validation] Epoch:4/40, test_loss:[3.82511], test_accuracy:[0.29147] 当前性能最好的模型 epoch_4 的精度: 0.29147, 已将其赋值为:best_model Epoch:5/40, batch:400, train_loss:[0.88937], train_accuracy:[0.85156] (0.86s) [validation] Epoch:5/40, test_loss:[3.55012], test_accuracy:[0.32572] 当前性能最好的模型 epoch_5 的精度: 0.32572, 已将其赋值为:best_model Epoch:6/40, batch:500, train_loss:[3.35874], train_accuracy:[0.00781] (1.03s) [validation] Epoch:6/40, test_loss:[3.27110], test_accuracy:[0.39603] 当前性能最好的模型 epoch_6 的精度: 0.39603, 已将其赋值为:best_model Epoch:7/40, batch:600, train_loss:[3.55404], train_accuracy:[0.07812] (0.90s) [validation] Epoch:7/40, test_loss:[2.67955], test_accuracy:[0.47656] 当前性能最好的模型 epoch_7 的精度: 0.47656, 已将其赋值为:best_model Epoch:8/40, batch:700, train_loss:[3.85814], train_accuracy:[0.] (0.86s) [validation] Epoch:8/40, test_loss:[2.24445], test_accuracy:[0.56731] 当前性能最好的模型 epoch_8 的精度: 0.56731, 已将其赋值为:best_model [validation] Epoch:9/40, test_loss:[1.80623], test_accuracy:[0.61001] 当前性能最好的模型 epoch_9 的精度: 0.61001, 已将其赋值为:best_model Epoch:10/40, batch:800, train_loss:[0.45419], train_accuracy:[0.90625] (0.97s) [validation] Epoch:10/40, test_loss:[2.38987], test_accuracy:[0.53095] 当前性能最好的模型 epoch_9 的精度: 0.61001, 已将其赋值为:best_model Epoch:11/40, batch:900, train_loss:[0.18748], train_accuracy:[0.98438] (0.86s) [validation] Epoch:11/40, test_loss:[1.61611], test_accuracy:[0.62744] 当前性能最好的模型 epoch_11 的精度: 0.62744, 已将其赋值为:best_model Epoch:12/40, batch:1000, train_loss:[1.29553], train_accuracy:[0.71875] (0.86s) [validation] Epoch:12/40, test_loss:[1.37060], test_accuracy:[0.69882] 当前性能最好的模型 epoch_12 的精度: 0.69882, 已将其赋值为:best_model Epoch:13/40, batch:1100, train_loss:[0.3879], train_accuracy:[0.92188] (0.87s) [validation] Epoch:13/40, test_loss:[1.14783], test_accuracy:[0.72992] 当前性能最好的模型 epoch_13 的精度: 0.72992, 已将其赋值为:best_model Epoch:14/40, batch:1200, train_loss:[0.87254], train_accuracy:[0.6875] (0.88s) [validation] Epoch:14/40, test_loss:[1.10103], test_accuracy:[0.74189] 当前性能最好的模型 epoch_14 的精度: 0.74189, 已将其赋值为:best_model Epoch:15/40, batch:1300, train_loss:[1.48974], train_accuracy:[0.66406] (0.86s) [validation] Epoch:15/40, test_loss:[1.07390], test_accuracy:[0.74680] 当前性能最好的模型 epoch_15 的精度: 0.74680, 已将其赋值为:best_model Epoch:16/40, batch:1400, train_loss:[1.58466], train_accuracy:[0.60938] (0.87s) [validation] Epoch:16/40, test_loss:[1.21864], test_accuracy:[0.71283] 当前性能最好的模型 epoch_15 的精度: 0.74680, 已将其赋值为:best_model [validation] Epoch:17/40, test_loss:[0.91765], test_accuracy:[0.77633] 当前性能最好的模型 epoch_17 的精度: 0.77633, 已将其赋值为:best_model Epoch:18/40, batch:1500, train_loss:[0.31404], train_accuracy:[0.94531] (0.97s) [validation] Epoch:18/40, test_loss:[0.78194], test_accuracy:[0.81254] 当前性能最好的模型 epoch_18 的精度: 0.81254, 已将其赋值为:best_model Epoch:19/40, batch:1600, train_loss:[0.11698], train_accuracy:[0.96875] (0.89s) [validation] Epoch:19/40, test_loss:[0.74436], test_accuracy:[0.83530] 当前性能最好的模型 epoch_19 的精度: 0.83530, 已将其赋值为:best_model Epoch:20/40, batch:1700, train_loss:[0.15437], train_accuracy:[0.96094] (1.03s) [validation] Epoch:20/40, test_loss:[0.69880], test_accuracy:[0.82470] 当前性能最好的模型 epoch_19 的精度: 0.83530, 已将其赋值为:best_model Epoch:21/40, batch:1800, train_loss:[0.07969], train_accuracy:[0.97656] (0.97s) [validation] Epoch:21/40, test_loss:[0.70179], test_accuracy:[0.84374] 当前性能最好的模型 epoch_21 的精度: 0.84374, 已将其赋值为:best_model Epoch:22/40, batch:1900, train_loss:[0.54401], train_accuracy:[0.84375] (0.94s) [validation] Epoch:22/40, test_loss:[0.58002], test_accuracy:[0.85237] 当前性能最好的模型 epoch_22 的精度: 0.85237, 已将其赋值为:best_model Epoch:23/40, batch:2000, train_loss:[0.83155], train_accuracy:[0.75] (0.96s) [validation] Epoch:23/40, test_loss:[0.54806], test_accuracy:[0.87272] 当前性能最好的模型 epoch_23 的精度: 0.87272, 已将其赋值为:best_model Epoch:24/40, batch:2100, train_loss:[0.58758], train_accuracy:[0.89062] (0.90s) [validation] Epoch:24/40, test_loss:[0.50172], test_accuracy:[0.86901] 当前性能最好的模型 epoch_23 的精度: 0.87272, 已将其赋值为:best_model Epoch:25/40, batch:2200, train_loss:[0.82464], train_accuracy:[0.81443] (0.91s) [validation] Epoch:25/40, test_loss:[0.43689], test_accuracy:[0.88945] 当前性能最好的模型 epoch_25 的精度: 0.88945, 已将其赋值为:best_model [validation] Epoch:26/40, test_loss:[0.38249], test_accuracy:[0.90288] 当前性能最好的模型 epoch_26 的精度: 0.90288, 已将其赋值为:best_model Epoch:27/40, batch:2300, train_loss:[0.07135], train_accuracy:[0.97656] (1.11s) [validation] Epoch:27/40, test_loss:[0.33852], test_accuracy:[0.92130] 当前性能最好的模型 epoch_27 的精度: 0.92130, 已将其赋值为:best_model Epoch:28/40, batch:2400, train_loss:[0.03607], train_accuracy:[0.99219] (0.92s) [validation] Epoch:28/40, test_loss:[0.33881], test_accuracy:[0.90698] 当前性能最好的模型 epoch_27 的精度: 0.92130, 已将其赋值为:best_model Epoch:29/40, batch:2500, train_loss:[0.11255], train_accuracy:[0.98438] (0.87s) [validation] Epoch:29/40, test_loss:[0.30653], test_accuracy:[0.92391] 当前性能最好的模型 epoch_29 的精度: 0.92391, 已将其赋值为:best_model Epoch:30/40, batch:2600, train_loss:[0.15478], train_accuracy:[0.96094] (0.88s) [validation] Epoch:30/40, test_loss:[0.29564], test_accuracy:[0.92140] 当前性能最好的模型 epoch_29 的精度: 0.92391, 已将其赋值为:best_model Epoch:31/40, batch:2700, train_loss:[0.52736], train_accuracy:[0.84375] (0.87s) [validation] Epoch:31/40, test_loss:[0.26435], test_accuracy:[0.93282] 当前性能最好的模型 epoch_31 的精度: 0.93282, 已将其赋值为:best_model Epoch:32/40, batch:2800, train_loss:[0.30632], train_accuracy:[0.92969] (0.86s) [validation] Epoch:32/40, test_loss:[0.25930], test_accuracy:[0.92992] 当前性能最好的模型 epoch_31 的精度: 0.93282, 已将其赋值为:best_model Epoch:33/40, batch:2900, train_loss:[0.31913], train_accuracy:[0.94531] (0.86s) [validation] Epoch:33/40, test_loss:[0.24070], test_accuracy:[0.93554] 当前性能最好的模型 epoch_33 的精度: 0.93554, 已将其赋值为:best_model [validation] Epoch:34/40, test_loss:[0.23492], test_accuracy:[0.93684] 当前性能最好的模型 epoch_34 的精度: 0.93684, 已将其赋值为:best_model Epoch:35/40, batch:3000, train_loss:[0.07634], train_accuracy:[0.99219] (0.98s) [validation] Epoch:35/40, test_loss:[0.22327], test_accuracy:[0.93995] 当前性能最好的模型 epoch_35 的精度: 0.93995, 已将其赋值为:best_model Epoch:36/40, batch:3100, train_loss:[0.21794], train_accuracy:[0.97656] (0.86s) [validation] Epoch:36/40, test_loss:[0.21491], test_accuracy:[0.93995] 当前性能最好的模型 epoch_35 的精度: 0.93995, 已将其赋值为:best_model Epoch:37/40, batch:3200, train_loss:[0.08067], train_accuracy:[0.99219] (0.87s) [validation] Epoch:37/40, test_loss:[0.20955], test_accuracy:[0.94406] 当前性能最好的模型 epoch_37 的精度: 0.94406, 已将其赋值为:best_model Epoch:38/40, batch:3300, train_loss:[0.15168], train_accuracy:[0.96875] (0.88s) [validation] Epoch:38/40, test_loss:[0.20086], test_accuracy:[0.94667] 当前性能最好的模型 epoch_38 的精度: 0.94667, 已将其赋值为:best_model Epoch:39/40, batch:3400, train_loss:[0.32322], train_accuracy:[0.85156] (0.87s) [validation] Epoch:39/40, test_loss:[0.19395], test_accuracy:[0.94787] 当前性能最好的模型 epoch_39 的精度: 0.94787, 已将其赋值为:best_model Epoch:40/40, batch:3500, train_loss:[0.23203], train_accuracy:[0.96094] (0.88s) [validation] Epoch:40/40, test_loss:[0.18764], test_accuracy:[0.94957] 当前性能最好的模型 epoch_40 的精度: 0.94957, 已将其赋值为:best_model 训练完成,最终性能accuracy=0.94957(epoch=40), 总耗时31.99s, 已将其保存为:best_model
将训练过程中的损失函数和模型在训练集上的准确率可视化,有助于发现模型在训练中遇到的问题。损失函数小幅震荡属于正常现象,总体向下即可。
可以查看到损失值趋势下降,准确度在上升的趋势,趋近90~100%。
离线测试同样要基于动态守护框架fluid.dygraph.guard()。测试过程与训练过程中的在线测试流程基本一致,只需要提前实现载入已保存的模型即可,载入模型使用fluid.load_dygraph()方法。
with fluid.dygraph.guard(PLACE):
model_dict, _ = fluid.load_dygraph(os.path.join(final_models_path, 'best_model'))
model = CNN() #模型实例化
model.load_dict(model_dict) #加载模型参数
#启动训练过程
_, avg_acc = test(model, test_reader())
print('测试集精度为:{:.5f}'.format(avg_acc))
测试集精度为:0.95714
【结果分析】
需要注意的是此处的精度与训练过程中输出的测试精度是不相同的,因为训练过程中使用的是验证集VehicleLicense_val, 而这里的离线测试使用的是测试集VehicleLicense_test.
# 导入依赖库
import os
import cv2
import json
import numpy as np
import paddle # 载入PaddlePaddle基本库
import paddle.fluid as fluid # 载入基于fluid框架的paddle
from paddle.fluid.dygraph import Linear, Conv2D, Pool2D
import matplotlib.pyplot as plt # 载入python的第三方图像处理库
dataset_name = 'VehicleLicense'
architecture = 'CNN'
result_root_path = 'D:\\Workspace\\ExpResults\\'
final_model_path = os.path.join(result_root_path, 'Project06CNN', dataset_name + '_' + architecture, 'final_models')
在预测之前,通常需要对图像进行预处理。此处的预处理方案和训练模型时所使用的预处理方案必须是一致的。针对车牌识别等大多数字符识别任务,一般包含以下三个步骤:
color2bin()
;Segmentation()
;load_image()
。值得注意的是,二值化灰度图有利于提高系统的识别性能,是灰度图像预处理的一个重要步骤,在允许的情况下,尽量执行该操作。但选择二值分割阈值是一件经验性的数据驱动型工作,需要慎重选择。
def load_image(img_path):
img = cv2.imread(img_path, 0) # cv2.imread(path, 0|1),其中0表示灰度模式,1表示彩色模式
img = cv2.resize(img, (img_size, img_size)) # resize image with high-quality 图像大小为28*28
img = np.array(img).reshape(img_channel, img_size, img_size).astype('float32') # 返回新形状的数组,把它变成一个 numpy 数组以匹配数据馈送格式。
img = img/255.0 # 将数据归一化到[0~1]之间,也可以归一化为[-1,1]之间, img = img/255.0*2.0-1.0
return img
# 将图像转换为二值模式
def color2bin(img_path):
img_gray = cv2.imread(img_path, 0) # cv2.imread(path, 0|1),其中0表示灰度模式,1表示彩色模式
ret, img_bin = cv2.threshold(img_gray, 120, 255, cv2.THRESH_BINARY) # 将图像转换为二值模式,分割阈值为120
return img_bin
# 定义车牌字符分割函数,实现将车牌分割成单字符
def Segmentation(img_path, img_name):
img_bin = color2bin(os.path.join(img_path, img_name))
print(img_bin.shape)
# 对车牌图片进行处理,分割出车牌中的每一个字符并保存
result = []
for col in range(img_bin.shape[1]):
result.append(0)
for row in range(img_bin.shape[0]):
result[col] = result[col] + img_bin[row][col]/255
character_dict = {}
num = 0
i = 0
while i < len(result):
if result[i] == 0:
i += 1
else:
index = i + 1
while result[index] != 0:
index += 1
character_dict[num] = [i, index-1]
num += 1
i = index
# print(character_dict)
for i in range(8):
if i==2:
continue
padding = (170 - (character_dict[i][1] - character_dict[i][0])) / 2
ndarray = np.pad(img_bin[:,character_dict[i][0]:character_dict[i][1]], ((0,0), (int(padding), int(padding))), 'constant', constant_values=(0,0))
ndarray = cv2.resize(ndarray, (20,20))
tmp_path = os.path.join(img_path, 'tmp')
if not os.path.exists(tmp_path):
os.makedirs(tmp_path)
cv2.imwrite(os.path.join(tmp_path, str(i) + '.png'), ndarray)
######################################################################
# 输出二值化后的图像示例
if __name__ == "__main__":
img_name = 'test02.png'
img_path = os.path.join(dataset_root_path, 'Infer')
img_bin = color2bin(os.path.join(img_path, img_name))
print(img_bin)
plt.imshow(img_bin, cmap='gray')
[[0 0 0 ... 0 0 0] [0 0 0 ... 0 0 0] [0 0 0 ... 0 0 0] ... [0 0 0 ... 0 0 0] [0 0 0 ... 0 0 0] [0 0 0 ... 0 0 0]]
车牌识别需要有两个过程,1. 对原始车牌进行分割,分割成单字符;2. 对单字符进行预测,并输出预测结果
# 0. 设置待预测样本
img_name = 'test02.png'
img_path = os.path.join(dataset_root_path, 'Infer')
# 1. 获取标签名称和标签ID的对应关系
json_label_match = os.path.join(dataset_root_path, 'dataset_info.json')
label_match = json.loads(open(json_label_match, 'r', encoding='utf-8').read())
# 2. 构建预测动态图过程
with fluid.dygraph.guard():
model = CNN()#模型实例化
model_dict,_= fluid.load_dygraph(os.path.join(final_model_path, 'best_model'))
model.load_dict(model_dict)#加载模型参数
model.eval()#评估模式
Segmentation(img_path, img_name) # 原始车牌图片切割成单字符,并进行二值化处理
lab = [] # 定义每个预测车牌的预测标签
for i in range(8):
if i==2:
continue
infer_imgs = []
infer_imgs.append(load_image(os.path.join(img_path, 'tmp', str(i) + '.png')))
infer_imgs = np.array(infer_imgs)
infer_imgs = fluid.dygraph.to_variable(infer_imgs)
result = model(infer_imgs)
lab.append(np.argmax(result.numpy()))
# 3. 输出预测结果
print('\n车牌识别结果为:',end='')
for i in range(len(lab)):
print(label_match['label_dict'][str(lab[i])], end='')
display(Image.open(os.path.join(img_path, img_name)))
(170, 722) 车牌识别结果为:京N8P8F8
【结果分析】
本例代码并没有做严格的检测和分割,也没有做严格的预处理(例如光照和色彩),因此识别系统限制较多。例如:
有兴趣的同学建议进行一定的改进,处理以上问题。改进版的“车牌识别系统”,可以作为毕业设计(论文)进行提交,或用于参加各种计算机的竞赛。