作者:欧新宇(Xinyu OU)
当前版本:Release v2.0
开发平台:Paddle 2.5.2
运行环境:Intel Core i7-7700K CPU 4.2GHz, nVidia GeForce GTX 1080 Ti
本教案所涉及的数据集仅用于教学和交流使用,请勿用作商用。
最后更新:2024年5月17日
车牌识别(License Plate Recognition, LPR) 是计算机视觉领域的一个重要应用,广泛应用于智能交通系统、停车场管理、车辆追踪等领域。本案例教学将通过构建一个简单的车牌识别系统,来介绍使用卷积神经网络(Convolutional Neural Network, CNN)进行车牌识别的基本原理和实现步骤。本教学案例可以直接作为常规教学的实训课进行开设,建议4-8学时。
本案例主要包括下图所示五个部分,分别是数据集准备、全局参数定义及数据准备、训练与评估、推理预测及结果分析。
卷积神经网络
并补全网络参数配置表和神经网络类的定义(Q3:10分, Q4:10分)验证集精度
(Q5:20分, Q6:10分)测试精度
(Q7:10分)实验摘要: 对于模型训练的任务,需要数据预处理,将数据整理成为适合给模型训练使用的格式。车牌识别的项目是一个多分类的任务,数据集中有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='')
已完成16151副图片的改名, 已完成。
若官方数据集没有数据集的划分列表,或者数据集为自建数据集,则需要手动生成数据集的划分,一般包括训练集、验证集和测试集。
值得注意的是,在进行数据划分的时候要注意类别的均衡性,处理的方法主要有两种。一是,对所有样本进行打乱,再进行划分;二是,顺序遍历不同类别的文件夹,然后均匀划分。下面的代码属于第二种。有兴趣的同学可以尝试第一种方法。例如在1.2.1节改名的时候,就收集文件名,并进行打乱。
Q1: 补全下列代码,实现将数据集按照7:1:2的比例分为训练集train, 验证集val 和测试集test(10分) ([Your codes 1~3])
##################################################################################
# 数据集预处理
# 作者: 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())
# Q1-1: 补全下列代码实现数据集列表路径的路径定义和数据集信息文件的路径定义
# [Your codes 1]
data_path = os.path.join(dataset_root_path, 'Data')
trainval_list =
train_list =
val_list =
test_list =
dataset_info_list =
# 检测数据集列表是否存在,如果存在则先删除。其中测试集列表是一次写入,因此可以通过'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)
# Q1-2:补全下列代码,按照7:2:1的比例将数据集划分为训练集train、验证集val、测试集test和训练验证集trainval
# [Your codes 2]
class_name_list =
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:
# Q1-3: 将程序运行的相关结果保存到数据集信息json文件中
# [Your codes 3]
dataset_info['dataset_name'] =
dataset_info['num_trainval'] =
dataset_info['num_train'] =
dataset_info['num_val'] =
dataset_info['num_test'] =
dataset_info['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))
图像列表已生成, 其中训练验证集样本12877,训练集样本11232个, 验证集样本1645个, 测试集样本3274个, 共计16151个。
实验摘要: 车牌识别是一个多分类问题,我们通过卷积神经网络来完成。这部分通过PaddlePaddle构造一个LeNet卷积神经的网络,最后一层采用Softmax激活函数完成分类任务。
实验目的:
# 1. 导入依赖库
import os
import cv2
import numpy as np
import codecs
import json
import time # 载入time时间库,用于计算训练时间
import paddle as paddle # 载入PaddlePaddle基本库
import matplotlib.pyplot as plt # 载入matplotlib绘图库
import warnings
warnings.filterwarnings("ignore", category=UserWarning)
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. 全局参数配置
project_name = 'Project009CNNVehicleLicense' # 定义项目名称(用于存储时的标识)
dataset_name = 'VehicleLicense' # 定义数据集名称
# 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 = 'D:\\WorkSpace\\ExpResults\\'
result_root_path = os.path.join(result_root_path, project_name) # 定义结果保存路径
final_models_path = os.path.join(result_root_path, 'final_models', 'best_model') # 定义模型的保存的路径
final_figures_path = os.path.join(result_root_path, 'final_figures') # 定义可视化图的输出路径
# 2.2 图像基本信息
img_size = 20
img_channel = 1
# 2.3 训练参数定义
total_epoch = 20 # 总迭代次数, 代码调试好后考虑Epochs_num = 50
log_interval = 100 # 训练时显示训练日志的间隔
eval_interval = 1 # 设置在训练过程中,每隔一定的周期进行一次测试
learning_rate = 0.001 # 学习率
momentum = 0.9 # 动量(Momentum方法时使用)
BATCH_SIZE = 64 # 设置每个批次的数据大小,同时对训练提供器和测试提供器有效
通过集成飞桨内置的 paddle.io.Dataset
实现数据集类的定义,包括数据列表读取和数据预处理。
Q2:完成数据集类定义的部分代码(10分) ([Your codes 4~7])
import paddle.vision.transforms as T
from paddle.io import DataLoader
# 1. 数据集的定义
class Dataset(paddle.io.Dataset):
def __init__(self, dataset_root_path, mode='test'):
assert mode in ['train', 'val', 'test', 'trainval']
self.data = [] # 创建空列表文件,用于保存数据的路径和标签
# Q2-1 读取数据集列表文件,并将路径路径和标签进行拆分,其中测试集若不存在标签则复制为"-1"
# [Your codes 4]
# Q2-2 使用transform接口定义数据预处理,本例需要规范图像尺度为[20,20], 并将图像转换为Paddle要求的Tensor
# [Your codes 5]
# 根据索引获取单个样本
def __getitem__(self, index):
# Q2-3: 对self.data变量进行拆分,划分为图像路径及标签,并按照路径进行图像载入
# [Your codes 6]
return img, label
# Q2-4 获取样本总数
# [Your codes 7]
对于要使用的所有数据均需要设置数据提供器,本例我们给出基于训练集、验证集和测试集和训练验证集划分的设置。
# 1. 实例化数据类
dataset_train = VehicleLicenseDataset(dataset_root_path, mode='train')
dataset_val = VehicleLicenseDataset(dataset_root_path, mode='val')
dataset_trainval = VehicleLicenseDataset(dataset_root_path, mode='trainval')
dataset_test = VehicleLicenseDataset(dataset_root_path, mode='test')
# 2. 创建迭代读取器
# 使用paddle.io.DataLoader 定义DataLoader对象用于加载Python生成器产生的数据,
# DataLoader 返回的是一个批次数据迭代器,并且是异步的。
train_reader = DataLoader(dataset_train, batch_size=64, shuffle=True, drop_last=True)
val_reader = DataLoader(dataset_val, batch_size=64, shuffle=False, drop_last=False)
trainval_reader = DataLoader(dataset_trainval, batch_size=64, shuffle=True, drop_last=True)
test_reader = DataLoader(dataset_test, batch_size=64, shuffle=False, drop_last=False)
#####################################################################################################
# 数据迭代器测试
# 1. 输出数据集的基本情况
print('数据集包含训练数据{}个,验证数据{}个,训练验证集{}个,测试数据{}个。'.format(len(dataset_train),len(dataset_val),len(dataset_trainval),len(dataset_test)))
print('数据的形态为:{}'.format(dataset_val[0][0].shape))
# 2. 迭代的读取数据并打印数据的形状
for i, (img, label) in enumerate(val_reader()):
if i > 2:
break
print('验证集batch_{}的图像形态:{}, 标签形态:{}'.format(i, img.shape, label.shape))
数据集包含训练数据11232个,验证数据1645个,训练验证集12877个,测试数据3274个。
数据的形态为:[1, 20, 20]
验证集batch_0的图像形态:[64, 1, 20, 20], 标签形态:[64]
验证集batch_1的图像形态:[64, 1, 20, 20], 标签形态:[64]
验证集batch_2的图像形态:[64, 1, 20, 20], 标签形态:[64]
定义训练过程中用到的可视化方法, 包括训练损失, 训练集批准确率, 测试集准确率. 根据具体的需求,可以在训练后展示这些数据和迭代次数的关系. 值得注意的是, 训练过程中可以每个epoch绘制一个数据点,也可以每个batch绘制一个数据点,也可以每个n个batch或n个epoch绘制一个数据点.
def draw_process_ch6(visualization_log, show_top5=False, figure_path=final_figures_path, figurename='visualization_log', isShow=True):
"""绘制训练过程中的训练误差、训练精度、验证误差和验证精度四个重要输出"""
train_losses = visualization_log['train_losses'] # 训练集的损失值
train_accs_top1 = visualization_log['train_accs_top1'] # 训练集的top1精确度
train_accs_top5 = visualization_log['train_accs_top5'] # 训练集的top5精确度
val_losses = visualization_log['val_losses'] # 验证集的损失值
val_accs_top1 = visualization_log['val_accs_top1'] # 验证集的精确度
val_accs_top5 = visualization_log['val_accs_top5'] # 验证集的精确度
epoch_iters = visualization_log['epoch_iters'] # 周期epoch迭代次数
batch_iters = visualization_log['batch_iters'] # 批次batch迭代次数
# 第一组坐标轴 Loss
_, ax1 = plt.subplots()
ax1.plot(batch_iters, train_losses, color='orange', linestyle='--', label='train_loss')
ax1.plot(epoch_iters, val_losses, color='cyan', linestyle='--', label='val_loss')
ax1.set_xlabel('Iters', fontsize=16)
ax1.set_ylabel('Loss', fontsize=16)
max_loss = max(max(train_losses), max(val_losses))
ax1.set_ylim(0, max_loss*1.2)
# 第二组坐标轴 accuracy
ax2 = ax1.twinx()
ax2.plot(epoch_iters, train_accs_top1, 'o-', color='red', markersize=3, label='train_accuracy(top1)')
ax2.plot(epoch_iters, val_accs_top1, 'o-', color='blue', markersize=3, label='val_accuracy(top1)')
if show_top5==True:
ax2.plot(epoch_iters, train_accs_top5, 'o-', color='magenta', markersize=3, label='train_accuracy(top5)')
ax2.plot(epoch_iters, val_accs_top5, 'o-', color='pink', markersize=3, label='val_accuracy(top5)')
ax2.set_ylabel('Accuracy', fontsize=16)
max_accs = max(max(train_accs_top1), max(train_accs_top5), max(val_accs_top1), max(val_accs_top5))
ax2.set_ylim(0, max_accs*1.2)
# 3.配置图例
plt.title('Training and Validation Results', fontsize=18)
handles1, labels1 = ax1.get_legend_handles_labels() # 图例1
handles2, labels2 = ax2.get_legend_handles_labels() # 图例2
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:
log_file = json.loads(open(os.path.join(final_figures_path, 'visualization_log.json'), 'r', encoding='utf-8').read())
draw_process_ch6(log_file, show_top5=True)
except:
print('数据不存在,无法进行绘制')
实验摘要: 车牌识别是一个多分类问题,我们通过卷积神经网络来完成。这部分通过PaddlePaddle构造一个LeNet卷积神经的网络,最后一层采用Softmax激活函数完成分类任务。
实验目的:
Q3: 根据拓扑结构图补全网络参数配置表(10分)
Layer | Input | Kernels_num | Kernels_size | Stride | Padding | PoolingType | Output | Parameters |
---|---|---|---|---|---|---|---|---|
Input | 1×20×20 | - | - | - | - | - | - | |
Conv1 | - | - | - | 1 | 0 | - | - | - |
Pool1 | - | - | - | 1 | 0 | max | - | 0 |
Conv2 | - | - | - | 1 | 0 | - | - | - |
Pool2 | - | - | - | 1 | 0 | max | - | 0 |
Conv3 | - | - | - | 1 | 0 | - | - | - |
FC1 | - | - | - | - | - | - | - | - |
Output | - | - | - | - | - | - | 65×1 | - |
- | - | - | - | - | - | - | - | Total = 226137 |
Q4:根据网络拓扑结构图和网络参数配置表完成神经网络类的定义(10分)([Your codes 8~10])
from paddle.nn import Sequential, Conv2D, MaxPool2D, Linear, ReLU
# 定义多层感知机(CNN)
class myCNN(paddle.nn.Layer):
name_scope = 'myCNN'
def __init__(self, num_classes=65): # 初始化CNN类,并为CNN增加对象self.x
super(myCNN, self).__init__()
# Conv2D(in_channels, out_channels, kernel_size, stride: int = 1, padding: int = 0)
# Q3-1:定义模型的卷积、激活、池化层及全连接层
# [Your codes 8]
self.features = Sequential(
)
self.fc = Sequential(
)
# Q3-2: 完成前向传输方法的定义
# [Your codes 9]
def forward(self, input): # 为CNN类增加forward方法
return y
############################################################################
# 模型测试
if __name__ == '__main__':
# Q3-3:完成模型测试代码的定义
# [Your codes 10]
---------------------------------------------------------------------------
Layer (type) Input Shape Output Shape Param #
===========================================================================
Conv2D-1 [[10, 1, 20, 20]] [10, 28, 16, 16] 728
ReLU-1 [[10, 28, 16, 16]] [10, 28, 16, 16] 0
MaxPool2D-1 [[10, 28, 16, 16]] [10, 28, 15, 15] 0
Conv2D-2 [[10, 28, 15, 15]] [10, 32, 13, 13] 8,096
ReLU-2 [[10, 32, 13, 13]] [10, 32, 13, 13] 0
MaxPool2D-2 [[10, 32, 13, 13]] [10, 32, 12, 12] 0
Conv2D-3 [[10, 32, 12, 12]] [10, 32, 10, 10] 9,248
ReLU-3 [[10, 32, 10, 10]] [10, 32, 10, 10] 0
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): 3.24
Params size (MB): 0.86
Estimated Total Size (MB): 4.12
---------------------------------------------------------------------------
在飞桨2.0+的环境中,模型验证/测试使用 model.eval_batch()
方法实现,一般包含如下几个步骤:
model.eval_batch()
方法,并载入 [image]
和 [label]
作为输入执行前向出传输。模型的输出为 model.prepare()
方法所指定的评价指标,一般包括损失值、top1和top5精度。此外,在定义eval()函数的时候,我们需要为其指定两个参数:model
是测试的模型,data_reader
是迭代的数据读取器,取值为val_reader(), test_reader(),分别对验证集和测试集。此处验证集和测试集数据的测试过程是相同的,只是所使用的数据不同。
from paddle.static import InputSpec
def eval(model, data_reader, verbose=0):
acc_top1 = []
acc_top5 = []
losses = []
n_total = 0
for batch_id, (image, label) in enumerate(data_reader):#测试集
n_batch = len(label)
n_total = n_total + n_batch
label = paddle.unsqueeze(label, axis=1) # 将图像转换为4D张量
loss, acc = model.eval_batch([image], [label])
losses.append(loss[0]*n_batch)
acc_top1.append(acc[0][0]*n_batch)
acc_top5.append(acc[0][1]*n_batch)
avg_loss = np.sum(losses)/n_total # loss 记录的是当前batch的累积值
avg_acc_top1 = np.sum(acc_top1)/n_total # metric 是当前batch的平均值
avg_acc_top5 = np.sum(acc_top5)/n_total
return avg_loss, avg_acc_top1, avg_acc_top5
##############################################################
if __name__ == '__main__':
try:
# 设置输入样本的维度
input_spec = InputSpec(shape=[None, img_channel, img_size, img_size], dtype='float32', name='image')
label_spec = InputSpec(shape=[None, 1], dtype='int64', name='label')
# 载入模型
network = myCNN()
model = paddle.Model(network, input_spec, label_spec) # 模型实例化
model.load(final_models_path) # 载入调优模型的参数
model.prepare(loss=paddle.nn.CrossEntropyLoss(), # 设置loss
metrics=paddle.metric.Accuracy(topk=(1,5))) # 设置评价指标
# 执行评估函数,并输出验证集样本的损失和精度
print('开始评估...')
avg_loss, avg_acc_top1, avg_acc_top5 = eval(model, val_reader(), verbose=1)
print('\r [验证集] 损失: {:.5f}, top1精度:{:.5f}, top5精度为:{:.5f} \n'.format(avg_loss, avg_acc_top1, avg_acc_top5), end='')
avg_loss, avg_acc_top1, avg_acc_top5 = eval(model, test_reader(), verbose=1)
print('\r [测试集] 损失: {:.5f}, top1精度:{:.5f}, top5精度为:{:.5f}'.format(avg_loss, avg_acc_top1, avg_acc_top5), end='')
except:
print('数据不存在跳过测试')
开始评估...
[验证集] 损失: 1.18715, top1精度:0.92766, top5精度为:0.97325
[测试集] 损失: 1.03287, top1精度:0.93128, top5精度为:0.97404
在飞桨2.0+中,动态图模式是默认模式,所有的训练
和测试
代码都需要基于动态图进行创建。由于是默认模式,因此不需要再像1.8版本中一样使用守护进程进行启用。
训练部分的具体流程,与验证部分大体相同,主要包括如下几个部分:
model.prepare()
实现定义。model.train_batch()
方法,并载入 [image]
和 [label]
作为输入执行前向出传输。在飞桨2.0+中,反向求导部分不需要进行显示定义,train_batch()会自动执行。此外,在训练过程中,我们可以每个一定的周期调用一次验证函数 eval()
,来对验证集进行测试。一般来说,每个epoch都可以进行一次验证。另外,可视化也训练和验证的loss和accuracy也是训练中常用的模型选择方法。在训练过程中,可以将周期,批次,损失及精度等信息打印到屏幕。
在本项目中,我们在训练中,每100个batch之后会输出一次平均训练误差和准确率;每一轮训练之后,使用测试集进行一次测试,在每轮测试中,均打输出一次平均测试误差和准确率。
【注意】注意在下列的代码中,我们每个epoch都会判断当前的模型是否是最优模型,如果是最优模型,我们会进行一次模型保存,并将其名命名为 best_model
。对于复杂的模型和大型数据集上,我们通常还会在每个周期训练结束后都保存一个 checkpoint_model
。这种经常性的模型保存,有利于我们执行EarlyStopping策略,并回退到任意一个时间节点。也便于当我们发现运行曲线不再继续收敛时,就可以结束训练。
Q5. 完成下列模型训练函数的主体部分(20分) ([Your codes 11~13])
from paddle.static import InputSpec
import paddle.optimizer as optimizer
visualization_log = { # 初始化状态字典
'train_losses': [], # 训练损失值
'train_accs_top1': [], # 训练top1精度
'train_accs_top5': [], # 训练top5精度
'val_losses': [], # 验证损失值
'val_accs_top1': [], # 验证top1精度
'val_accs_top5': [], # 验证top5精度
'batch_iters': [], # 批次batch迭代次数
'epoch_iters': [], # 周期epoch迭代次数
}
def train(model):
print('启动训练...')
start = time.perf_counter()
num_batch = 0
best_result = 0
best_result_id = 0
elapsed =0
for epoch in range(1, total_epoch+1):
for batch_id, (image, label) in enumerate(train_reader()):
num_batch += 1
# Q5-1. 调用model.train_batch方法进行训练,注意需要对label的尺度进行规范化
# [Your codes 11]
label =
loss, acc =
if num_batch % log_interval == 0: # 每10个batch显示一次日志,适合大数据集
# Q5-2. 从训练的输出中获取损失值,top1精度和top5精度
# [Your codes 12]
avg_loss =
acc_top1 =
acc_top5 =
elapsed_step = time.perf_counter() - elapsed - start
elapsed = time.perf_counter() - start
print('Epoch:{}/{}, batch:{}, train_loss:[{:.5f}], acc_top1:[{:.5f}], acc_top5:[{:.5f}]({:.2f}s)'
.format(epoch, total_epoch, num_batch, loss[0][0], acc[0][0], acc[0][1], elapsed_step))
# 记录训练过程,用于可视化训练过程中的loss和accuracy
visualization_log['train_losses'].append(float(avg_loss))
visualization_log['batch_iters'].append(num_batch)
# 每隔一定周期进行一次测试
if epoch % eval_interval == 0 or epoch == total_epoch:
# 模型校验
val_loss, val_acc_top1, val_acc_top5 = eval(model, val_reader())
print('[validation] Epoch:{}/{}, val_loss:[{:.5f}], val_top1:[{:.5f}], val_top5:[{:.5f}]'.format(epoch, total_epoch, val_loss, val_acc_top1, val_acc_top5))
# 记录测试过程,用于可视化训练过程中的loss和accuracy
visualization_log['epoch_iters'].append(num_batch)
visualization_log['train_accs_top1'].append(float(acc_top1))
visualization_log['train_accs_top5'].append(float(acc_top5))
visualization_log['val_losses'].append(float(val_loss))
visualization_log['val_accs_top1'].append(float(val_acc_top1))
visualization_log['val_accs_top5'].append(float(val_acc_top5))
# Q5-3. 将性能最好的模型保存为final模型,注意同时保存调优模型和推理模型
# model.save(<path>, training=False|True),True:调优模型 | False:推理模型
# [Your codes 13]
# 输出训练过程数据,将日志字典保存为json格式,绘图数据可以在训练结束后自动显示,也可以在训练中手动执行以显示结果
if not os.path.exists(final_figures_path):
os.makedirs(final_figures_path)
with codecs.open(os.path.join(final_figures_path, 'visualization_log.json'), 'w', encoding='utf-8') as f_log:
json.dump(visualization_log, f_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))
Q6. 完成主函数的定义(10分) ([Your codes 14])
#### 训练主函数 ########################################################3
if __name__ == '__main__':
# [Your codes 14]
# 1. 设置输入样本的维度
input_spec =
label_spec =
# 2. 载入设计好的网络,并实例化model变量
network =
model =
# 3. 设置学习率、优化器、损失函数和评价指标
optimizer =
model.prepare()
# 4. 启动训练过程
train(model)
print('训练完毕,结果路径{}.'.format(result_root_path))
# 5. 输出训练过程图
draw_process_ch6(visualization_log)
启动训练...
Epoch:1/20, batch:100, train_loss:[1.26940], acc_top1:[0.76562], acc_top5:[0.85938](4.61s)
[validation] Epoch:1/20, val_loss:[1.26940], val_top1:[0.93128], val_top5:[0.97404]
Epoch:2/20, batch:200, train_loss:[0.89697], acc_top1:[0.78125], acc_top5:[0.89062](25.26s)
Epoch:2/20, batch:300, train_loss:[0.58039], acc_top1:[0.82812], acc_top5:[0.90625](2.58s)
[validation] Epoch:2/20, val_loss:[0.58039], val_top1:[0.93128], val_top5:[0.97404]
Epoch:3/20, batch:400, train_loss:[0.48632], acc_top1:[0.87500], acc_top5:[0.95312](1.22s)
Epoch:3/20, batch:500, train_loss:[0.39505], acc_top1:[0.90625], acc_top5:[0.96875](0.96s)
[validation] Epoch:3/20, val_loss:[0.39505], val_top1:[0.93128], val_top5:[0.97404]
Epoch:4/20, batch:600, train_loss:[0.14182], acc_top1:[0.95312], acc_top5:[1.00000](1.24s)
Epoch:4/20, batch:700, train_loss:[0.31880], acc_top1:[0.93750], acc_top5:[0.98438](0.97s)
[validation] Epoch:4/20, val_loss:[0.31880], val_top1:[0.93128], val_top5:[0.97404]
Epoch:5/20, batch:800, train_loss:[0.11153], acc_top1:[0.95312], acc_top5:[1.00000](1.19s)
[validation] Epoch:5/20, val_loss:[0.11153], val_top1:[0.93128], val_top5:[0.97404]
Epoch:6/20, batch:900, train_loss:[0.15027], acc_top1:[0.95312], acc_top5:[1.00000](1.20s)
Epoch:6/20, batch:1000, train_loss:[0.25309], acc_top1:[0.93750], acc_top5:[1.00000](0.93s)
[validation] Epoch:6/20, val_loss:[0.25309], val_top1:[0.93128], val_top5:[0.97404]
Epoch:7/20, batch:1100, train_loss:[0.05757], acc_top1:[0.98438], acc_top5:[1.00000](1.18s)
Epoch:7/20, batch:1200, train_loss:[0.24150], acc_top1:[0.93750], acc_top5:[0.98438](0.98s)
[validation] Epoch:7/20, val_loss:[0.24150], val_top1:[0.93128], val_top5:[0.97404]
Epoch:8/20, batch:1300, train_loss:[0.08737], acc_top1:[0.98438], acc_top5:[1.00000](1.35s)
Epoch:8/20, batch:1400, train_loss:[0.06368], acc_top1:[0.95312], acc_top5:[1.00000](0.97s)
[validation] Epoch:8/20, val_loss:[0.06368], val_top1:[0.93128], val_top5:[0.97404]
Epoch:9/20, batch:1500, train_loss:[0.01878], acc_top1:[1.00000], acc_top5:[1.00000](1.16s)
[validation] Epoch:9/20, val_loss:[0.01878], val_top1:[0.93128], val_top5:[0.97404]
Epoch:10/20, batch:1600, train_loss:[0.14690], acc_top1:[0.96875], acc_top5:[0.98438](1.18s)
Epoch:10/20, batch:1700, train_loss:[0.08274], acc_top1:[0.98438], acc_top5:[1.00000](0.95s)
[validation] Epoch:10/20, val_loss:[0.08274], val_top1:[0.93128], val_top5:[0.97404]
Epoch:11/20, batch:1800, train_loss:[0.08237], acc_top1:[0.98438], acc_top5:[1.00000](1.16s)
Epoch:11/20, batch:1900, train_loss:[0.13697], acc_top1:[0.93750], acc_top5:[1.00000](0.92s)
[validation] Epoch:11/20, val_loss:[0.13697], val_top1:[0.93128], val_top5:[0.97404]
Epoch:12/20, batch:2000, train_loss:[0.00066], acc_top1:[1.00000], acc_top5:[1.00000](1.14s)
Epoch:12/20, batch:2100, train_loss:[0.04592], acc_top1:[0.98438], acc_top5:[1.00000](0.95s)
[validation] Epoch:12/20, val_loss:[0.04592], val_top1:[0.93128], val_top5:[0.97404]
Epoch:13/20, batch:2200, train_loss:[0.07770], acc_top1:[0.96875], acc_top5:[1.00000](1.27s)
[validation] Epoch:13/20, val_loss:[0.07770], val_top1:[0.93128], val_top5:[0.97404]
Epoch:14/20, batch:2300, train_loss:[0.04654], acc_top1:[0.96875], acc_top5:[1.00000](1.30s)
Epoch:14/20, batch:2400, train_loss:[0.05807], acc_top1:[0.96875], acc_top5:[1.00000](0.95s)
[validation] Epoch:14/20, val_loss:[0.05807], val_top1:[0.93128], val_top5:[0.97404]
Epoch:15/20, batch:2500, train_loss:[0.09849], acc_top1:[0.96875], acc_top5:[1.00000](1.21s)
Epoch:15/20, batch:2600, train_loss:[0.04895], acc_top1:[0.98438], acc_top5:[1.00000](0.93s)
[validation] Epoch:15/20, val_loss:[0.04895], val_top1:[0.93128], val_top5:[0.97404]
Epoch:16/20, batch:2700, train_loss:[0.22798], acc_top1:[0.93750], acc_top5:[1.00000](1.20s)
Epoch:16/20, batch:2800, train_loss:[0.08528], acc_top1:[0.96875], acc_top5:[0.98438](0.94s)
[validation] Epoch:16/20, val_loss:[0.08528], val_top1:[0.93128], val_top5:[0.97404]
Epoch:17/20, batch:2900, train_loss:[0.05738], acc_top1:[0.98438], acc_top5:[1.00000](1.14s)
[validation] Epoch:17/20, val_loss:[0.05738], val_top1:[0.93128], val_top5:[0.97404]
Epoch:18/20, batch:3000, train_loss:[0.10596], acc_top1:[0.96875], acc_top5:[1.00000](1.23s)
Epoch:18/20, batch:3100, train_loss:[0.38073], acc_top1:[0.93750], acc_top5:[1.00000](0.92s)
[validation] Epoch:18/20, val_loss:[0.38073], val_top1:[0.93128], val_top5:[0.97404]
Epoch:19/20, batch:3200, train_loss:[0.00568], acc_top1:[1.00000], acc_top5:[1.00000](1.15s)
Epoch:19/20, batch:3300, train_loss:[0.00253], acc_top1:[1.00000], acc_top5:[1.00000](0.95s)
[validation] Epoch:19/20, val_loss:[0.00253], val_top1:[0.93128], val_top5:[0.97404]
Epoch:20/20, batch:3400, train_loss:[0.30926], acc_top1:[0.96875], acc_top5:[1.00000](1.20s)
Epoch:20/20, batch:3500, train_loss:[0.27312], acc_top1:[0.98438], acc_top5:[1.00000](0.90s)
[validation] Epoch:20/20, val_loss:[0.27312], val_top1:[0.93128], val_top5:[0.97404]
训练完成,最终性能accuracy=0.94468(epoch=20), 总耗时67.65s, 已将其保存为:best_model
训练完毕,结果路径D:\WorkSpace\ExpResults\Project009CNNVehicleLicense.
将训练过程中的损失函数和模型在训练集上的准确率可视化,有助于发现模型在训练中遇到的问题。损失函数小幅震荡属于正常现象,总体向下即可。可以查看到损失值趋势下降,准确度在上升的趋势,趋近90~100%。
离线测试与验证几乎相同,直接调用 eval()
方法即可,唯一的区别是离线测试通常是先读取保存的模型,再进行测试。
Q7. 完成离线测试的模型载入代码(10分) ([Your codes 15])
# [Your codes 15]
# 1. 设置输入样本的维度
input_spec = InputSpec(shape=[None, img_channel, img_size, img_size], dtype='float32', name='image')
label_spec = InputSpec(shape=[None, 1], dtype='int64', name='label')
# 2. 载入模型
# 3. 执行评估函数,并输出验证集样本的损失和精度
print('开始评估...')
print('\r [验证集] 损失: {:.5f}, top1精度:{:.5f}, top5精度为:{:.5f} \n'.format(avg_loss, avg_acc_top1, avg_acc_top5), end='')
print('\r [测试集] 损失: {:.5f}, top1精度:{:.5f}, top5精度为:{:.5f}'.format(avg_loss, avg_acc_top1, avg_acc_top5), end='')
开始评估...
[验证集] 损失: 1.47688, top1精度:0.94468, top5精度为:0.97872
[测试集] 损失: 1.35058, top1精度:0.94227, top5精度为:0.98167
【结果分析】
需要注意的是此处的精度与训练过程中输出的测试精度是不相同的,因为训练过程中使用的是验证集VehicleLicense_val, 而这里的离线测试使用的是测试集VehicleLicense_test.
实验摘要: 对训练过的模型,我们通过测试集进行模型效果评估,并可以在实际场景中进行预测,查看模型的效果。
实验目的:
Q8:使用训练好的模型对给定的车牌进行预测,尽力而为地获得最优的预测结果(20分)([Your codes 16~18])
# 0.导入依赖库
import os
import cv2
import json
import numpy as np
import paddle # 载入PaddlePaddle基本库
import matplotlib.pyplot as plt # 载入python的第三方图像处理库
# Q8-1:全局参数定义
# [Your codes 16]
# 1. 定义全局路径,包括项目名称、结果路径、模型路径等
project_name =
dataset_name =
root_path = 'D:\\Workspace\\'
final_model_path = os.path.join(root_path, 'ExpResults', project_name, 'final_models', 'best_model')
dataset_root_path = os.path.join(root_path, 'ExpDatasets', dataset_name)
# 2. 图像基本信息
img_size = 20
img_channel = 1
在预测之前,通常需要对图像进行预处理。此处的预处理包含两部分,一部分是常规的预处理,一部分是针对数据集的特殊预处理。
load_image()
进行定义。color2bin()
对该功能进行定义。Segmentation()
对该功能进行定义。有兴趣的同学可以扩展该函数,以实现更好的字符分割。值得注意的是,二值化灰度图有利于提高系统的识别性能,是灰度图像预处理的一个重要步骤,在允许的情况下,尽量执行该操作。但选择二值分割阈值是一件经验性的数据驱动型工作,需要慎重选择。
import paddle.vision.transforms as T
# Q8-2:定义数据读取函数,包括读入数据,将数据转换为float32,尺度规范为[20,20],格式转换为Tesnsor并转置为4D形态
# [Your codes 17]
def load_image(img_path):
img = # cv2.imread(path, 0|1),其中0表示灰度模式,1表示彩色模式
img = # 将图像数据类型转化为float32
transforms = T.Compose([ # 传入定义好的数据处理方法,作为自定义数据集类的一个属性
])
img = # 调用transforms方法对数据进行预处理
img = # 调整数据形状paddle默认格式
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))
# 对车牌图片进行处理,分割出车牌中的每一个字符并保存
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))
plt.imshow(img_bin, cmap='gray')
车牌识别需要有两个过程,1. 对原始车牌进行分割,分割成单字符;2. 对单字符进行预测,并输出预测结果
### Q8-3: 载入模型并实现车牌预测
# [Your codes 18]
# 0. 设置待预测样本 (示例样本test02.png)
img_name = 'test02.png'
img_path =
# 1. 载入模型并进行实例化
model =
# 2. 获取标签名称和标签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())
# 3. 将原始车牌图片切割成单字符,原始车牌图片切割成单字符,并进行二值化处理
# 4. 对拆分好的车牌,进行依次预测
# 5. 输出预测结果,要求:1.输出文字预测:形式为“车牌识别结果为:云A·XXXXX”;2.输出待预测车牌
车牌识别结果为:京N·8P8F8
【结果分析】
本例代码并没有做严格的检测和分割,也没有做严格的预处理(例如光照和色彩),因此识别系统限制较多。例如:
有兴趣的同学建议进行一定的改进,处理以上问题。改进版的“车牌识别系统”,可以作为毕业设计(论文)进行提交,或用于参加各种计算机的竞赛。