数据集
word-recognition, 数据集命名为1_1.png
pretrained-model参数,保存格式为
预处理数据
# 预处理数据,将其转化为标准格式。同时将数据拆分成两份,以便训练和计算预估准确率
import codecs
import os
import random
import shutil
from PIL import Image
train_ratio = 9 / 10 # 训练集大小
all_file_dir = "data/data6927/word-recognition" # 数据文件路径
image_path_pre = os.path.join(all_file_dir, "imageSet") # 路径
# 训练数据集路径
train_image_dir = os.path.join(all_file_dir, "trainImageSet")
if not os.path.exists(train_image_dir):
os.makedirs(train_image_dir)
# 评估数据集路径
eval_image_dir = os.path.join(all_file_dir, "evalImageSet")
if not os.path.exists(eval_image_dir):
os.makedirs(eval_image_dir)
# 训练集、评估集、标签文件
train_file = codecs.open(os.path.join(all_file_dir, "train.txt"), 'w')
eval_file = codecs.open(os.path.join(all_file_dir, "eval.txt"), 'w')
label_list = os.path.join(all_file_dir, "image_label.txt")
print(label_list)
train_count = 0 # 初始化训练集图像计数
eval_count = 0 # 初始化评估集图像计数
class_set = set() # # 初始化一个集合,用于存储所有唯一的字符标签
# 标注文件进行处理
with open(label_list) as f:
for line in f:
parts = line.strip().split() # 去除行尾的换行符并分割行内容,默认以空格为分隔符
file, label = parts[0], parts[1] # 将分割后的第一部分作为文件名,第二部分作为标签
# 标点符号跳过
if '/' in label or '\'' in label or '.' in label or '!' in label or '-' in label or '$' in label or '&' in label or '@' in label or '?' in label or '%' in label or '(' in label or ')' in label or '~' in label:
continue
# 将标签中的文字加入集合
for e in label:
class_set.add(e) # 将字符添加到集合中,集合会自动去重
# 分测试集、评估集
if random.uniform(0, 1) <= train_ratio:
shutil.copyfile(os.path.join(image_path_pre, file), os.path.join(train_image_dir, file)) # 将图像复制到训练集目录
train_file.write("{0}\t{1}\n".format(os.path.join(train_image_dir, file), label))
train_count += 1
else:
shutil.copyfile(os.path.join(image_path_pre, file), os.path.join(eval_image_dir, file))
eval_file.write("{0}\t{1}\n".format(os.path.join(eval_image_dir, file), label)) # 将图像复制到评估集目录
eval_count += 1
print("train image count: {0} eval image count: {1}".format(train_count, eval_count))
class_list = list(class_set)
class_list.sort()
print("class num: {0}".format(len(class_list)))
print(class_list)
with codecs.open(os.path.join(all_file_dir, "label_list.txt"), "w") as label_list:
label_id = 0
for c in class_list:
label_list.write("{0}\t{1}\n".format(c, label_id)) # 将字符标签和对应的ID写入文件,字符和ID之间用制表符分隔
label_id += 1
结果
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
模型搭建与训练
# -*- coding: UTF-8 -*-
"""
训练常基于crnn-ctc的网络,文字行识别
"""
from __future__ import absolute_import # 导入Python 2.x的绝对导入行为,以确保在Python 2.x和3.x中导入行为一致
from __future__ import division # 导入Python 3.x的除法行为,即除法总是返回浮点数
from __future__ import print_function # 导入Python 3.x的print函数行为,支持打印多值、改变分隔符等
import os
import uuid # 用于生成全局唯一的标识符
import numpy as np
import time
import six # # 导入six模块,用于Python 2.x和3.x的兼容性处理
import math
import random
import paddle
import paddle.fluid as fluid # 从paddle模块导入fluid子模块,并简称为fluid,fluid是PaddlePaddle的核心API
import logging
import xml.etree.ElementTree # 导入xml.etree.ElementTree模块,用于解析XML文件
import codecs # 导入codecs模块,用于编码和解码数据,特别是处理文件读写时的编码问题
import json
# 从paddle.fluid.initializer导入MSRA初始化器
# MSRA初始化器是一种自适应的权重初始化方法,常用于深度神经网络
from paddle.fluid.initializer import MSRA
# 从paddle.fluid.param_attr导入ParamAttr
# ParamAttr用于定义参数的属性,如名称、初始化方法、正则化等
from paddle.fluid.param_attr import ParamAttr
# 从paddle.fluid.regularizer导入L2Decay正则化器
# L2Decay是一种正则化方法,用于防止模型过拟合,通过对权重施加惩罚项来实现
from paddle.fluid.regularizer import L2Decay
# 从PIL库导入Image、ImageEnhance、ImageDraw模块
# PIL(Python Imaging Library)是一个强大的图像处理库,这里主要用于图像增强和绘制
from PIL import Image, ImageEnhance, ImageDraw
logger = None
train_params = {
"input_size": [1, 48, 512], # 输入数据维度
# 表示输入数据的形状为批量大小1(通常这个值在训练时会被覆盖为实际的批次大小),高度48,宽度512。对于图像数据,这通常意味着每个图像是单通道的(如灰度图),高度为48像素,宽度为512像素。
"data_dir": "data/data6927/word-recognition", # 数据集路径
"train_dir": "trainImageSet", # 训练数据目录
"eval_dir": "evalImageSet", # 评估数据目录
"train_list": "train.txt", # 训练集文件
"eval_list": "eval.txt", # 评估集文件
"label_list": "label_list.txt", # 标签文件
"class_dim": -1, # -1 表示这个值在后续代码中会被计算或指定
"label_dict": {}, # 标签字典
"image_count": -1,
"continue_train": True, #是否增量训练
"pretrained": True, # 预训练
"pretrained_model_dir": "./pretrained-model", # 预训练模型目录
"save_model_dir": "./crnn-model", # 模型保存目录
"num_epochs": 40, # 训练轮次
"train_batch_size": 256, # 训练批次大小
"use_gpu": True, # 是否使用gpu
"ignore_thresh": 0.7, # 阈值 用于忽略某些置信度低于此阈值的预测
"mean_color": 127.5, # 可能用于图像预处理中的归一化步骤
"mode": "train", # 模式
"multi_data_reader_count": 4, # reader数量,表示使用4个数据读取器并行读取数据,以加快数据加载速度。
"apply_distort": True, # 是否进行扭曲,在训练前对图像进行随机扭曲以增强模型的泛化能力
"image_distort_strategy": { # 扭曲策略,如放大比率、色调、对比度、饱和度和亮度的调整概率和幅度
"expand_prob": 0.5, # 放大比率,表示有50%的概率对图像进行放大。
"expand_max_ratio": 2, # 最大放大比率,表示图像可以被放大到原尺寸的2倍。
"hue_prob": 0.5, # 色调,表示有50%的概率对图像的色调进行调整。
"hue_delta": 18, # 表示色调可以在-18到+18度的范围内随机调整。
"contrast_prob": 0.5, # 对比度,表示有50%的概率对图像的对比度进行调整。
"contrast_delta": 0.5, # 表示对比度可以在0.5倍到1.5倍之间调整(具体实现可能有所不同,但通常意味着对比度会围绕原始值上下浮动50%)。
"saturation_prob": 0.5, # 表示有50%的概率对图像的饱和度进行调整。
"saturation_delta": 0.5, # 表示饱和度可以在0.5倍到1.5倍之间调整。
"brightness_prob": 0.5, # 亮度,表示有50%的概率对图像的亮度进行调整。
"brightness_delta": 0.125 # 表示亮度可以在-0.125到+0.125的范围内调整(具体取决于实现,可能是绝对亮度值的变化,也可能是相对于原始亮度的百分比变化)。
},
"rsm_strategy": { # 梯度下降配置
"learning_rate": 0.0005,
"lr_epochs": [70, 120, 170, 220, 270, 320], # 学习率衰减分段(6个数字分为7段)
"lr_decay": [1, 0.5, 0.1, 0.05, 0.01, 0.005, 0.001], # 每段采用的学习率,对应lr_epochs参数7段
},
"early_stop": { # 控制训练停止条件
"sample_frequency": 50, # 样本频率
"successive_limit": 5, #连续多少次未达到改进限制
"min_instance_error": 0.1 # 最小实例误差
}
}
CRNN网络模型
class CRNN(object):
def __init__(self,
num_classes, # 类别数量,即模型需要识别的字符种类数(包括一个空白符,用于CTC解码)。
label_dict): # 标签字典,用于将模型输出的索引映射回实际的字符。
self.outputs = None # 输出
self.label_dict = label_dict # 标签字典
self.num_classes = num_classes # 类别数量
def name(self):
return "crnn"
# 这个方法实现了卷积层、批量归一化(Batch Normalization)和池化层的组合。
def conv_bn_pool(self, input, group, # 输入数据,通常是前一层的输出或原始输入图像。# 分组数,表示将输入数据分成多少组进行独立的卷积操作。这在某些情况下(如分组卷积)用于减少计算量或增加模型的表示能力。
out_ch, # 每组卷积操作的输出通道数列表。
act="relu", # 激活函数
param=None, bias=None, # 卷积层和批量归一化层的参数和偏置设置。
param_0=None, is_test=False,#布尔值,指示是否处于测试模式。在测试模式下,批量归一化层会使用训练阶段学习到的参数,而不是更新它们。
pooling=True, # 是否执行池化
use_cudnn=False): # 是否对cuda加速
tmp = input
for i in six.moves.xrange(group): # six.moves.xrange是一个兼容Python 2和Python 3的函数,用于生成一个范围(range)的迭代器。在Python 3中,可以直接使用range函数。
# for i in range(group): # 也可以
# 卷积层,对每个分组执行卷积操作
tmp = fluid.layers.conv2d(
input=tmp, # 指定卷积层的输入数据。
num_filters=out_ch[i], # 指定卷积核(或称为滤波器)的数量,这里从out_ch列表中获取每个分组的输出通道数。
filter_size=3, # 设置卷积核的大小为3x3
padding=1, # 在输入数据的周围添加1层的填充,以保持输出数据的空间尺寸
param_attr=param if param_0 is None else param_0,#指定卷积层的参数属性。如果param_0不为None,则使用param_0;否则,使用param。
act=None, # 指定激活函数为None,因为激活函数将在批量归一化层之后应用。
use_cudnn=use_cudnn) # 指定是否使用CUDA深度神经网络库(cuDNN)进行加速。
# 批量归一化
tmp = fluid.layers.batch_norm(
input=tmp, # 前面卷基层输出作为输入
act=act, # 激活函数
param_attr=param, # 参数初始值
bias_attr=bias, # 偏置初始值
is_test=is_test) # 测试模型
# 根据传入的参数决定是否做池化操作
if pooling:
tmp = fluid.layers.pool2d(
input=tmp, # 前一层的输出作为输入
pool_size=2, # 池化区域,设置池化窗口的大小为2x2。
pool_type="max", # 池化类型最大池化。
pool_stride=2, # 步长为2。
use_cudnn=use_cudnn,#指定是否使用cuDNN进行加速。
ceil_mode=True) # 在计算输出尺寸时,向上取整。
return tmp
# 包含4个卷积层操作,定义了CRNN中的卷积部分,包含四层卷积和池化操作。
def ocr_convs(self, input,
regularizer=None, # 正则化
gradient_clip=None, # 梯度裁剪,防止梯度过大,防止梯度爆炸
is_test=False, use_cudnn=False):
# 指定偏置项的参数属性
b = fluid.ParamAttr(
regularizer=regularizer,
gradient_clip=gradient_clip,
initializer=fluid.initializer.Normal(0.0, 0.0)) # 它使用了正态分布初始化器,但均值和标准差均为0.0,这实际上是不正确的,因为标准差应该是一个正数。可能是一个错误或者笔误。
# 指定权重参数的属性
w0 = fluid.ParamAttr(
regularizer=regularizer,
gradient_clip=gradient_clip,
initializer=fluid.initializer.Normal(0.0, 0.0005))
# 指定权重参数的属性
w1 = fluid.ParamAttr(
regularizer=regularizer,
gradient_clip=gradient_clip,
initializer=fluid.initializer.Normal(0.0, 0.01))
tmp = input
# 第一组卷积池化,卷积、批量归一化和池化
tmp = self.conv_bn_pool(tmp,
2, [16, 16], # 组数量及卷积核数量
param=w1,
bias=b,
param_0=w0,
is_test=is_test,
use_cudnn=use_cudnn)
# 第二组卷积池化,卷积、批量归一化和池化
tmp = self.conv_bn_pool(tmp,
2, [32, 32], # 组数量及卷积核数量
param=w1,
bias=b,
is_test=is_test,
use_cudnn=use_cudnn)
# 第三组卷积池化,卷积、批量归一化和池化
tmp = self.conv_bn_pool(tmp,
2, [64, 64], # 组数量及卷积核数量
param=w1,
bias=b,
is_test=is_test,
use_cudnn=use_cudnn)
# 第四组卷积池化,卷积、批量归一化
tmp = self.conv_bn_pool(tmp,
2, [128, 128], # 组数量及卷积核数量
param=w1,
bias=b,
is_test=is_test,
pooling=False, # 不做池化
use_cudnn=use_cudnn)
return tmp
# 组网,包括卷积部分CNNo、全连接层、双向GRU和输出层。
def net(self, images,
rnn_hidden_size=200, # RNN隐藏层的大小,即输出值的数量,隐藏层输出值数量
regularizer=None, # 正则化
gradient_clip=None, # 梯度裁剪,防止梯度爆炸
is_test=False,#标记是否处于测试模式。
use_cudnn=True): # 标记是否使用CUDA深度神经网络库进行加速。
# 卷积池化,处理输入的图像数据 images 并返回卷积特征图 conv_features
conv_features = self.ocr_convs(
images, # 通常是一个四维张量(Tensor),形状为 [batch_size, channels, height, width]
regularizer=regularizer,# 一个正则化器对象或None,常见的正则化方法包括L1正则化和L2正则化
gradient_clip=gradient_clip,#一个梯度裁剪的阈值或None。梯度裁剪是一种防止梯度爆炸的技术。如果梯度的绝对值超过了指定的阈值,它们将被裁剪到这个阈值。这有助于保持训练的稳定性。
is_test=is_test,
use_cudnn=use_cudnn)
# 将卷积特征图(conv_features)转换为一维的序列
sliced_feature = fluid.layers.im2sequence(
input=conv_features, # 卷积得到的特征图作为输入
stride=[1, 1],#无论是沿着高度还是宽度方向,都会以1的步长进行
# 卷积核大小(高度等于原高度,宽度1)
# 沿着特征图的高度方向进行切片,但每个切片只包含一行(因为宽度被设置为1),
# 如果高度不是固定的,您可能需要重新考虑您的网络设计,以便能够动态地获取这个值
filter_size=[conv_features.shape[2], 1])
# 定义参数属性的类。它允许您为模型的权重和偏置设置特定的正则化、梯度裁剪和初始化策略。
# 权重
para_attr = fluid.ParamAttr(
regularizer=regularizer, # 正则化,减少模型过拟合的技术
gradient_clip=gradient_clip, # 梯度裁剪是一种防止梯度爆炸的技术
initializer=fluid.initializer.Normal(0.0, 0.02))#正态(高斯)分布初始化方法,权重和偏置将在训练开始前从这个分布中随机采样
# 偏置
bias_attr = fluid.ParamAttr(
regularizer=regularizer, # 正则化
gradient_clip=gradient_clip,
initializer=fluid.initializer.Normal(0.0, 0.02))
# 偏置
bias_attr_nobias = fluid.ParamAttr(
regularizer=regularizer, # 正则化
gradient_clip=gradient_clip,
initializer=fluid.initializer.Normal(0.0, 0.02))
# 定义了两个全连接层,都以序列化处理的特征图 sliced_feature 作为输入,具有相同的输出大小和参数属性
fc_1 = fluid.layers.fc(
input=sliced_feature, # 序列化处理的特征图
size=rnn_hidden_size * 3,
param_attr=para_attr,
bias_attr=bias_attr_nobias)
fc_2 = fluid.layers.fc(
input=sliced_feature, # 序列化处理的特征图
size=rnn_hidden_size * 3,
param_attr=para_attr,
bias_attr=bias_attr_nobias)
# 双向GRU(门控循环单元,LSTM变种, LSTM是RNN变种)
# 双向GRU是一种循环神经网络(RNN)的变体,它通过同时处理正向和反向的序列信息,来捕捉序列数据中更丰富的上下文特征
gru_foward = fluid.layers.dynamic_gru(
input=fc_1,#fc_1是正向GRU的输入
size=rnn_hidden_size,#指定GRU单元的隐藏层大小(即隐藏状态的维度)。
param_attr=para_attr,#指定GRU参数(权重和偏置)的属性
bias_attr=bias_attr,#指定偏置项的属性
candidate_activation="relu")#指定GRU单元中候选状态的激活函数为ReLU
gru_backward = fluid.layers.dynamic_gru(
input=fc_2,#fc_2是反向GRU的输入
size=rnn_hidden_size,#与正向GRU相同,指定隐藏层的大小
is_reverse=True, # 反向循环神经网络
param_attr=para_attr,
bias_attr=bias_attr,
candidate_activation="relu")
# 输出层,权重参数属性
w_attr = fluid.ParamAttr(
regularizer=regularizer, #为权重参数指定正则化方法
gradient_clip=gradient_clip,#为权重参数的梯度指定裁剪策略,梯度裁剪是一种防止梯度爆炸的技术,它限制了梯度的最大值
initializer=fluid.initializer.Normal(0.0, 0.02))#为权重参数指定初始化方法,这里使用的是正态分布初始化,均值为0.0,标准差为0.02。这意味着权重参数在训练开始前会从均值为0.0、标准差为0.02的正态分布中随机采样
# 输出层,偏置参数属性
b_attr = fluid.ParamAttr(
regularizer=regularizer,#指定正则化方法
gradient_clip=gradient_clip,
initializer=fluid.initializer.Normal(0.0, 0.0))#为偏置参数指定初始化方法,这里同样使用的是正态分布初始化,但均值为0.0,标准差为0.0
# 全连接层的输出
fc_out = fluid.layers.fc(
input=[gru_foward, gru_backward], # 双向RNN输出作为输入
#输出大小设置为类别数加一。这通常用于分类任务中,其中一个额外的类别用于表示“其他”或“未知”类别,
# 或者用于留出一个空间作为模型的“边距”或“安全阈值”。然而,更常见的情况可能是为了包含背景类(在目标检测中)或仅仅是为了留出空间以处理不平衡的数据集(通过添加一个“少数类”的额外类别)
size=self.num_classes + 1, # 输出类别,
param_attr=w_attr,
bias_attr=b_attr)
self.outputs = fc_out
return fc_out # 返回输出层的输出
# 用于推理阶段,将CRNN的输出传递给CTC解码器,CTC解码器能够处理序列数据中的对齐问题,适用于如OCR这样的任务
def get_infer(self):
# 将CRNN网络输出交给CTC层转录(纠错、去重)
# 用于处理连接时序分类CTC,任务的一个关键组件。CTC 是一种训练循环神经网络(RNN)以处理序列数据的算法,特别适用于那些序列长度与输入数据长度不一致的场景,例如语音识别和手写识别。
return fluid.layers.ctc_greedy_decoder(
input=self.outputs, # 输入为CRNN网络输出,以进行贪婪解码
blank=self.num_classes)#指定空白类的索引。在 CTC 中,空白类用于表示两个实际字符之间的间隔,或者序列的开始和结束
def init_train_params():
"""
初始化训练参数,主要是初始化图片数量,类别数
:return:
"""
train_list = os.path.join(train_params['data_dir'], train_params['train_list'])
label_list = os.path.join(train_params['data_dir'], train_params['label_list'])
index = 0
with codecs.open(label_list, encoding='utf-8') as flist:
lines = [line.strip() for line in flist]#读取文件中的每一行,并去除首尾的空白字符
for line in lines:
parts = line.split()
train_params['label_dict'][parts[0]] = int(parts[1])#将类别名映射到数字ID,并存储在 train_params['label_dict'] 中
index += 1
train_params['class_dim'] = index#通过累加索引 index 来计算总的类别数
with codecs.open(train_list, encoding='utf-8') as flist:
lines = [line.strip() for line in flist]
train_params['image_count'] = len(lines) # 通过计算行数来确定训练图片的总数
# 初始化日志相关配置
def init_log_config():
global logger
logger = logging.getLogger() #获取一个日志记录器实例
logger.setLevel(logging.INFO)#设置日志级别为 INFO
log_path = os.path.join(os.getcwd(), 'logs')
if not os.path.exists(log_path):
os.makedirs(log_path)
log_name = os.path.join(log_path, 'train.log')
sh = logging.StreamHandler()#创建一个控制台处理器 sh,用于将日志消息输出到控制台
fh = logging.FileHandler(log_name, mode='w')#配置文件处理器,用于将日志消息写入指定的日志文件
fh.setLevel(logging.DEBUG)#日志级别为 DEBUG,这意味着它将记录所有级别的日志消息
formatter = logging.Formatter("%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s")
fh.setFormatter(formatter)
sh.setFormatter(formatter)
logger.addHandler(sh)#添加处理器到日志记录器
logger.addHandler(fh)
# 重设图像大小
def resize_img(img, input_size): # img(一个图片对象)和 input_size(一个包含目标尺寸的元组
target_size = input_size
percent_h = float(target_size[1]) / img.size[1] # 计算目标高度与原始图片高度的比例。
percent_w = float(target_size[2]) / img.size[0]# 计算目标宽度与原始图片宽度的比例
percent = min(percent_h, percent_w) # 选择高度和宽度比例中的较小值,以保持图片的宽高比不变
resized_width = int(round(img.size[0] * percent))#根据计算出的比例调整图片的宽度
resized_height = int(round(img.size[1] * percent))# 根据计算出的比例调整图片的高度
w_off = (target_size[2] - resized_width) / 2#计算水平偏移量,以便将调整大小后的图片水平居中
h_off = (target_size[1] - resized_height) / 2#计算垂直偏移量,以便将调整大小后的图片垂直居中
img = img.resize((resized_width, resized_height), Image.ANTIALIAS)#调整图片大小
array = np.ndarray((target_size[1], target_size[2], 3), np.uint8)#创建一个NumPy数组作为新图片的背景
array[:, :, 0] = 127 #将背景图片的蓝色通道设置为127(中灰色)
array[:, :, 1] = 127 #将背景图片的绿色通道设置为127
array[:, :, 2] = 127 #将背景图片的红色通道设置为127
ret = Image.fromarray(array) #将NumPy数组转换为图片对象
ret.paste(img, (np.random.randint(0, w_off + 1), int(h_off))) #将调整大小后的图片粘贴到背景图片上
return ret
# 随机调整图像的亮度
def random_brightness(img):
prob = np.random.uniform(0, 1)
if prob < train_params['image_distort_strategy']['brightness_prob']: #判断这个随机数是否小于预设的训练参数
brightness_delta = train_params['image_distort_strategy']['brightness_delta'] #从训练参数中获取亮度调整的幅度范围
delta = np.random.uniform(-brightness_delta, brightness_delta) + 1#生成一个在这个幅度范围内的随机浮点数,并加1,确保调整后的亮度增强因子delta是正的
# 因为ImageEnhance.Brightness的enhance方法期望一个大于0的参数,其中1表示原图亮度,小于1使图像变暗,大于1使图像变亮
img = ImageEnhance.Brightness(img).enhance(delta)
return img
# 随机调整图像的对比度
def random_contrast(img):
prob = np.random.uniform(0, 1) #生成一个0到1之间的随机数
if prob < train_params['image_distort_strategy']['contrast_prob']: # 判断这个随机数是否小于预设的训练参数
contrast_delta = train_params['image_distort_strategy']['contrast_delta'] # 从训练参数中获取亮度调整的幅度范围
delta = np.random.uniform(-contrast_delta, contrast_delta) + 1 # 生成一个在这个幅度范围内的随机浮点数,并加1,确保调整后的亮度增强因子delta是正的
img = ImageEnhance.Contrast(img).enhance(delta)
return img
# 随机调整图像的饱和度
def random_saturation(img):
prob = np.random.uniform(0, 1)
if prob < train_params['image_distort_strategy']['saturation_prob']:
saturation_delta = train_params['image_distort_strategy']['saturation_delta']
delta = np.random.uniform(-saturation_delta, saturation_delta) + 1 #其中1表示原图饱和度,小于1降低饱和度,使图像看起来更灰暗,大于1增加饱和度,使颜色更加鲜艳
img = ImageEnhance.Color(img).enhance(delta)
return img
# 随机调整图像的色调
def random_hue(img):
prob = np.random.uniform(0, 1)
if prob < train_params['image_distort_strategy']['hue_prob']: #判断这个随机数是否小于预设的训练参数值
hue_delta = train_params['image_distort_strategy']['hue_delta'] #从训练参数中获取色调调整的幅度范围
delta = np.random.uniform(-hue_delta, hue_delta) # 生成一个在这个幅度范围内的随机浮点数作为色调的调整量delta
img_hsv = np.array(img.convert('HSV'))#将图像img从RGB颜色空间转换到HSV颜色空间
img_hsv[:, :, 0] = img_hsv[:, :, 0] + delta#对转换后的HSV图像的色调分量(第一个通道)进行调整
img = Image.fromarray(img_hsv, mode='HSV').convert('RGB')#将调整后的HSV图像转换回RGB颜色空间
return img
#对输入的图像img应用一系列随机的图像扭曲操作,包括亮度、对比度、饱和度和色调的调整,从而增强训练出的模型的泛化能力
def distort_image(img):
prob = np.random.uniform(0, 1) #生成一个0到1之间的随机数
# Apply different distort order
if prob > 0.5:
# 如果prob大于0.5,则按照亮度、对比度、饱和度、色调的顺序应用扭曲操作
img = random_brightness(img)
img = random_contrast(img)
img = random_saturation(img)
img = random_hue(img)
#小于或等于0.5,则按照亮度、饱和度、色调、对比度的顺序应用扭曲操作
else:
img = random_brightness(img)
img = random_saturation(img)
img = random_hue(img)
img = random_contrast(img)
return img
# 对输入的图像img进行随机旋转,以实现图像增强的效果
def rotate_image(img):
"""
图像增强,增加随机旋转角度
"""
prob = np.random.uniform(0, 1)
if prob > 0.5:
#如果决定进行旋转,则生成一个-8到7之间的随机整数angle作为旋转的角度
angle = np.random.randint(-8, 8)
img = img.rotate(angle)
return img
# 对输入的图像img进行随机扩展(放大并填充背景)
def random_expand(img, keep_ratio=True):
# # 生成一个0到1之间的随机数,并与预设的训练参数值进行比较。如果随机数小于expand_prob,则不对图像进行扩展,直接返回原图像
if np.random.uniform(0, 1) < train_params['image_distort_strategy']['expand_prob']:
return img
# 如果决定对图像进行扩展,从训练参数中获取最大扩展比例
max_ratio = train_params['image_distort_strategy']['expand_max_ratio']
# 获取输入图像的宽度w和高度h,以及颜色通道数c
w, h = img.size
c = 3
# 生成随机的水平扩展比例
ratio_x = random.uniform(1, max_ratio)
# 垂直扩展比例
# 根据是否保持纵横比
if keep_ratio:
ratio_y = ratio_x
else:
#否则,ratio_y在1到max_ratio之间随机生成
ratio_y = random.uniform(1, max_ratio)
#根据扩展比例计算新的图像高度oh和宽度ow
oh = int(h * ratio_y)
ow = int(w * ratio_x)
#水平偏移量off_x和垂直偏移量off_y
#偏移量是在新的图像尺寸内随机生成的,用于确定原图像在新图像中的位置
off_x = random.randint(0, ow - w)
off_y = random.randint(0, oh - h)
# 创建一个大小为(oh, ow, c)的全零数组out_img作为输出图像
out_img = np.zeros((oh, ow, c), np.uint8)
for i in range(c):
# 并使用训练参数中指定的mean_color填充背景。
out_img[:, :, i] = train_params['mean_color']
# 将原图像img复制到输出图像out_img的指定位置(由off_x和off_y确定)
out_img[off_y: off_y + h, off_x: off_x + w, :] = img
return Image.fromarray(out_img)
# 对输入的图像img进行一系列的预处理操作。这些操作包括图像扭曲(如果启用了扭曲参数)、随机扩展、随机旋转等
def preprocess(img, input_size):
#首先,获取输入图像的宽度img_width和高度img_height
img_width, img_height = img.size
if train_params['apply_distort']:
#对图像应用distort_image函数进行扭曲处理
img = distort_image(img)
# 对图像应用random_expand函数进行随机扩展,这个步骤会改变图像的大小,并可能添加一些背景颜色
img = random_expand(img)
#对图像应用rotate_image函数进行随机旋转。这个步骤会改变图像的方向
img = rotate_image(img)
# img = resize_img(img, input_size)
# img = img.convert('L')
# img = np.array(img).astype('float32') - train_params['mean_color']
# img *= 0.007843
return img
# reader,读取图像文件列表,并对这些图像进行预处理,最终生成一个生成器(generator),该生成器可以按需产生图像数据和对应的标签
def custom_reader(file_list, data_dir, input_size, mode):
def reader():
#随机打乱文件列表,这有助于在训练时减少模型对数据的顺序依赖
np.random.shuffle(file_list)
for line in file_list:
# img_name, label
parts = line.split()
image_path = parts[0]
img = Image.open(image_path)
# img = Image.open(os.path.join(data_dir, image_path))
if img.mode != 'RGB':
img = img.convert('RGB')
# 根据标签字典将标签字符串转换为整数列表
label = [int(train_params['label_dict'][c]) for c in parts[-1]]
# 如果标签列表为空,则跳过当前图像
if len(label) == 0:
continue
if mode == 'train':
# 如果是训练模式,对图像进行额外的预处理
img = preprocess(img, input_size)
img = resize_img(img, input_size)
# 将图像转换为灰度图
img = img.convert('L')
# img.save(image_path)
# 将图像转换为NumPy数组,并减去平均颜色值,这一步通常用于数据标准化。
img = np.array(img).astype('float32') - train_params['mean_color']
# img *= 0.007843
# 调整图像数据的形状,增加一个维度以符合模型的输入要求
img = img[np.newaxis, ...] # ... 表示“取 img 数组的所有其他维度,并将它们放在新增加的轴之后
# print("{0} {1}".format(image_path, label))
yield img, label
return reader
# 创建多进程数据读取器的函数,它特别适用于处理大规模数据集时的并行读取
# num_workers:用于数据读取的进程数。
def multi_process_custom_reader(file_path, data_dir, num_workers, input_size, mode):
"""
创建多进程reader
:param file_path:
:param data_dir:
:param num_workers:
:param input_size:
:param mode:
:return:
"""
file_path = os.path.join(data_dir, file_path)
readers = []
# 读取文件内容,并将每一行(即每个图像文件名)存储到 images 列表中
images = [line.strip() for line in open(file_path)]
# 计算每个工作进程应该处理的图像数量,并将图像列表分割成多个子列表,每个子列表对应一个工作进程
n = int(math.ceil(len(images) // num_workers))
image_lists = [images[i: i + n] for i in range(0, len(images), n)]
train_path = os.path.join(train_params['data_dir'], train_params['train_dir'])
# 对于每个图像子列表,创建一个 custom_reader,然后使用 paddle.batch 将图像数据打包成批次
for l in image_lists:
reader = paddle.batch(custom_reader(l, train_path, input_size, mode),
batch_size=train_params['train_batch_size'])
# 使用 paddle.reader.shuffle 对每个批次的图像数据进行打乱
readers.append(paddle.reader.shuffle(reader, train_params['train_batch_size']))
# 将所有打乱后的数据读取器添加到 readers 列表中,False 参数表示不使用进程内的共享内存
return paddle.reader.multiprocess_reader(readers, False) # 返回多进程读取器
# 评估reader,创建一个用于评估(或验证)阶段的数据读取器
# 读取一个包含图像文件名的文件,并利用 custom_reader 函数,该函数负责图像的读取与预处理)来生成预处理后的图像数据
# 随后,这些数据会被 paddle.batch 函数打包成指定的批次大小以供后续使用
def create_eval_reader(file_path, data_dir, input_size, mode):
file_path = os.path.join(data_dir, file_path)
images = [line.strip() for line in open(file_path)]
eval_path = os.path.join(train_params['data_dir'], train_params['eval_dir'])
return paddle.batch(custom_reader(images, eval_path, input_size, mode),
batch_size=train_params['train_batch_size'])
# 配置一个使用 RMSProp 优化算法的训练优化器
# 该函数根据预设的训练参数(train_params)来计算学习率衰减的边界和值,并据此创建优化器
def optimizer_rms_setting():
# 批次数量
batch_size = train_params["train_batch_size"]
# 计算总批次
iters = train_params["image_count"] // batch_size
# 学习策略
learning_strategy = train_params['rsm_strategy']
# 学习率
lr = learning_strategy['learning_rate']
# 使用列表推导式计算学习率衰减的边界点,每个学习率变化点所在的迭代次数
boundaries = [i * iters for i in learning_strategy["lr_epochs"]]
# 使用列表推导式计算每个学习率阶段对应的值
values = [i * lr for i in learning_strategy["lr_decay"]]
# 均方根传播(RMSProp)法,RMSProp优化器实例
# 为优化器指定了一个L2正则化项,其系数为0.00005
optimizer = fluid.optimizer.RMSProp(learning_rate=fluid.layers.piecewise_decay(boundaries, values),
regularization=fluid.regularizer.L2Decay(0.00005))
return optimizer
# 用于构建训练程序,包括异步数据读取、模型预测、损失函数计算、优化器设置以及CTC解码
def build_train_program_with_async_reader(main_prog, startup_prog):
"""
定义异步读取器、预测、构建损失函数及优化器
:param main_prog:
:param startup_prog:
:return:
"""
# 将main_prog, startup_prog设置为默认主program, startup_program
# 使用fluid.program_guard上下文管理器,将当前的操作和变量添加到指定的main_prog和startup_prog中
with fluid.program_guard(main_prog, startup_prog):
img = fluid.layers.data(name='img', shape=train_params['input_size'], dtype='float32')
# lod_level=1表示gt_label是一个序列数据。
gt_label = fluid.layers.data(name='gt_label', shape=[1], dtype='int32', lod_level=1)
# 创建reader
# 创建了一个Python读取器,用于异步地从Python生成器中读取数据。
# capacity指定了读取器的缓冲区大小,feed_list指定了读取器需要读取的数据层的名称
data_reader = fluid.layers.create_py_reader_by_data(capacity=train_params['train_batch_size'],
feed_list=[img, gt_label],
name='train')
# 创建多进程reader
multi_reader = multi_process_custom_reader(train_params['train_list'],
train_params['data_dir'],
train_params['multi_data_reader_count'],
train_params['input_size'],
'train')
# 将自定义的多进程读取器装饰到之前创建的Python读取器上
data_reader.decorate_paddle_reader(multi_reader)
# 使用上下文管理器,为每个操作生成唯一的名称,以避免在多次调用该函数时发生名称冲突。
with fluid.unique_name.guard(): # 更换namespace
# 从data_reader中读取数据,并赋值给img和gt_label。
img, gt_label = fluid.layers.read_file(data_reader)
# 实例化一个CRNN模型,用于处理图像数据并生成预测。
model = CRNN(train_params['class_dim'], train_params['label_dict']) # 实例化
fc_out = model.net(img) # 预测
# 使用CTC损失函数计算预测结果和真实标签之间的损失,blank参数指定了CTC中的空白字符的索引
cost = fluid.layers.warpctc(input=fc_out, label=gt_label, blank=train_params['class_dim'],
norm_by_times=True) # 计算CTC损失函数
# 对CTC损失进行求和,得到总的损失
loss = fluid.layers.reduce_sum(cost) # 损失函数求和
# 获取优化器实例
optimizer = optimizer_rms_setting()
# 使用优化器来最小化损失
optimizer.minimize(loss)
# 执行CTC去重,使用CTC贪婪解码器对预测结果进行解码
decoded_out = fluid.layers.ctc_greedy_decoder(input=fc_out,
blank=train_params['class_dim'])
# 将真实标签的数据类型转换为int64
casted_label = fluid.layers.cast(x=gt_label, dtype='int64')
# 计算字符串的编辑距离
# 编辑距离又称Levenshtein距离,由俄罗斯的数学家Vladimir Levenshtein在1965年提出
# 是指利用字符操作,把字符串A转换成字符串B所需要的最少操作数
# 例如:"kitten" -> "sitten" -> "sittin" -> "sitting"
# 计算解码后的输出和真实标签之间的编辑距离(Levenshtein距离)
distances, seq_num = fluid.layers.edit_distance(decoded_out, casted_label)
return data_reader, loss, distances, seq_num, decoded_out
# 主要用于评估一个基于PaddlePaddle框架的深度学习模型
# main_prog:主程序(Program),用于保存评估时的网络结构和参数。
# startup_prog:启动程序(Program),用于初始化参数。
# place:指定执行计算的设备,比如CPU或者GPU。
def build_eval_program_with_feeder(main_prog, startup_prog, place):
"""
执行评估
:param main_prog:
:param startup_prog:
:param place:
:return:
"""
# 这个上下文管理器用于设置当前的默认主程序和启动程序
with fluid.program_guard(main_prog, startup_prog):
# 定义了一个数据层,用于接收图像数据
img = fluid.layers.data(name='img', shape=train_params['input_size'], dtype='float32')
# 定义了另一个数据层,用于接收真实标签ground truth label
# lod_level=1表示这是一个LoDTensor,用于处理变长序列
gt_label = fluid.layers.data(name='gt_label', shape=[1], dtype='int32', lod_level=1)
# 创建数据喂入器,用于将数据(img和gt_label)和指定的程序(main_prog)绑定到指定的设备(place)上
feeder = fluid.DataFeeder(feed_list=[img, gt_label], place=place, program=main_prog)
# 创建评估数据读取器
reader = create_eval_reader(train_params['eval_list'],
train_params['data_dir'],
train_params['input_size'],
'eval')
# 在上下文中构建模型,这有助于确保每次调用时模型中的变量名都是唯一的,避免命名冲突。
with fluid.unique_name.guard():
# 实例化CRNN模型,参数包括类别数和标签字典
model = CRNN(train_params['class_dim'], train_params['label_dict'])
outputs = model.net(img) # 通过调用模型的net方法获取模型输出
return feeder, reader, outputs, gt_label
# 加载预训练或之前训练的模型参数的函数
# exe(执行器,用于执行PaddlePaddle中的操作)和program(程序,包含了模型的网络结构和参数)
def load_pretrained_params(exe, program):
# 如果设置了增量训练,则加载之前训练的模型
if train_params['continue_train'] and os.path.exists(train_params['save_model_dir']):
# 表示将从重新训练的模型中加载参数
logger.info('load param from retrain model')
# 加载持久化参数(通常是模型权重和偏置等),这些参数保存在指定的目录中。
fluid.io.load_persistables(executor=exe,
dirname=train_params['save_model_dir'],
main_program=program)
# 如果设置了预训练,则加载预训练模型
elif train_params['pretrained'] and os.path.exists(train_params['pretrained_model_dir']):
# 将从预训练模型中加载参数
logger.info('load param from pretrained model')
def if_exist(var):
return os.path.exists(os.path.join(train_params['pretrained_model_dir'], var.name))
# 加载变量,该函数接受一个predicate参数,用于指定一个函数来过滤需要加载的变量。在这里,predicate=if_exist意味着只有那些在预训练模型目录中存在的变量才会被加载
fluid.io.load_vars(exe, train_params['pretrained_model_dir'], main_program=program,
predicate=if_exist)
训练
def train():
"""
训练
:return:
"""
# 初始化日志配置和训练参数,分别用于配置日志记录(如日志文件的路径、日志级别等)和初始化训练参数(如是否使用GPU、训练轮数、早停策略等)
init_log_config()
init_train_params()
# 使用日志记录训练开始,并打印训练参数。
logger.info("start train crnn, train params:%s", str(train_params))
logger.info("create place, use gpu:" + str(train_params['use_gpu']))
place = fluid.CUDAPlace(0) if train_params['use_gpu'] else fluid.CPUPlace()
# 日志记录,说明开始构建网络
logger.info("build network and program")
# 用于训练
train_program = fluid.Program()
# 用于初始化参数
start_program = fluid.Program()
# 用于评估
eval_program = fluid.Program()
# start_program = fluid.Program() # wdb del 20200322
# 定义异步读取器、预测、构建损失函数及优化器
# 构建训练程序,包括定义数据读取器、网络结构、损失函数和优化器。
train_reader, loss, distances, seq_num, decoded_out = \
build_train_program_with_async_reader(train_program, start_program)
# 评估,用于构建评估程序,包括定义数据喂入器、评估数据读取器和网络输出。
eval_feeder, eval_reader, output, gt_label = \
build_eval_program_with_feeder(eval_program, start_program, place)
# 创建一个专为测试/评估设计的程序副本,将eval_program设置为测试模式,这在评估时是必要的
eval_program = eval_program.clone(for_test=True)
# 日志记录,说明开始创建执行器和初始化参数
logger.info("build executor and init params")
# 创建 fluid.Executor 执行器,并在 start_program 上初始化参数
exe = fluid.Executor(place)
exe.run(start_program)
# 定义训练和评估的 fetch 列表,用于在执行器运行时获取相应的输出
train_fetch_list = [loss.name, distances.name, seq_num.name, decoded_out.name]
eval_fetch_list = [output.name]
# 加载预训练或之前训练的模型参数
load_pretrained_params(exe, train_program)
# 读取早停策略的相关参数,包括连续多少次满足条件后停止训练、采样频率和最小实例误差。
stop_strategy = train_params['early_stop']
successive_limit = stop_strategy['successive_limit']
sample_freq = stop_strategy['sample_frequency']
min_instance_error = stop_strategy['min_instance_error']
#初始化一些控制变量,用于控制训练过程
stop_train = False
successive_count = 0
total_batch_count = 0
# 创建一个EditDistance评估器,用于计算文本识别任务的编辑距离
distance_evaluator = fluid.metrics.EditDistance("edit-distance")
# 执行训练,外层循环,遍历每个训练轮次(epoch)
for pass_id in range(train_params["num_epochs"]):
# 日志记录,说明开始读取图像数据
logger.info("current pass: %d, start read image", pass_id)
# 初始化批处理ID和启动数据读取器线程
batch_id = 0
train_reader.start() # 启动reader线程
# 重置评估器
distance_evaluator.reset()
try:
while True:
# 在循环中,执行训练程序,更新评估器,计算损失和评估指标,并记录日志
t1 = time.time()
# 获取当前批次的数据
loss, distances, seq_num, decoded_out = exe.run(train_program,
fetch_list=train_fetch_list,
return_numpy=False)
# 将数据转换为NumPy数组
distances = np.array(distances)
seq_num = np.array(seq_num)
# 更新评估器
distance_evaluator.update(distances, seq_num)
# 计算平均损失和记录执行时间
period = time.time() - t1
loss = np.mean(np.array(loss))
batch_id += 1
total_batch_count += 1
# 每10个批次记录一次日志
if batch_id % 10 == 0:
distance, instance_error = distance_evaluator.eval()
# logger.info(np.array(decoded_out))
logger.info("Pass {0}, trainbatch {1}, loss {2} distance {3} instance error {4} time {5}"
.format(pass_id, batch_id, loss, distance, instance_error, "%2.2f sec" % period))
# 采用简单的定时采样停止办法,可以调整为更精细的保存策略
if total_batch_count % 100 == 0:
logger.info("temp save {0} batch train result".format(total_batch_count))
fluid.io.save_persistables(dirname=train_params['save_model_dir'],
main_program=train_program,
executor=exe)
# 根据采样频率和实例误差判断是否满足早停条件
if total_batch_count % sample_freq == 0:
if instance_error <= min_instance_error:
successive_count += 1
logger.info("instance error {0} successive count {1}".format(instance_error, successive_count))
if successive_count >= successive_limit:
stop_train = True
break
else:
successive_count = 0
except fluid.core.EOFException:
# 捕获EOFException异常,当数据读取完毕时重置读取器
train_reader.reset()
# 在每个epoch结束时,记录总的距离和实例误差
distance, instance_error = distance_evaluator.eval()
logger.info("Pass {0} distance {1} instance error {2}".format(pass_id, distance, instance_error))
# 如果满足早停条件,则提前结束训练
if stop_train:
logger.info("early stop")
break
# 训练结束后,保存最终的模型参数
logger.info("training till last, end training")
fluid.io.save_persistables(dirname=train_params['save_model_dir'], main_program=train_program, executor=exe)
if __name__ == '__main__':
train()
结果:
train params:{'input_size': [1, 48, 512], 'data_dir': 'data/data6927/word-recognition', 'train_dir': 'trainImageSet', 'eval_dir': 'evalImageSet', 'train_list': 'train.txt', 'eval_list': 'eval.txt', 'label_list': 'label_list.txt', 'class_dim': 63, 'label_dict': {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, 'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15, 'G': 16, 'H': 17, 'I': 18, 'J': 19, 'K': 20, 'L': 21, 'M': 22, 'N': 23, 'O': 24, 'P': 25, 'Q': 26, 'R': 27, 'S': 28, 'T': 29, 'U': 30, 'V': 31, 'W': 32, 'X': 33, 'Y': 34, 'Z': 35, '`': 36, 'a': 37, 'b': 38, 'c': 39, 'd': 40, 'e': 41, 'f': 42, 'g': 43, 'h': 44, 'i': 45, 'j': 46, 'k': 47, 'l': 48, 'm': 49, 'n': 50, 'o': 51, 'p': 52, 'q': 53, 'r': 54, 's': 55, 't': 56, 'u': 57, 'v': 58, 'w': 59, 'x': 60, 'y': 61, 'z': 62}, 'image_count': 10846, 'continue_train': True, 'pretrained': True, 'pretrained_model_dir': './pretrained-model', 'save_model_dir': './crnn-model', 'num_epochs': 40, 'train_batch_size': 256, 'use_gpu': True, 'ignore_thresh': 0.7, 'mean_color': 127.5, 'mode': 'train', 'multi_data_reader_count': 4, 'apply_distort': True, 'image_distort_strategy': {'expand_prob': 0.5, 'expand_max_ratio': 2, 'hue_prob': 0.5, 'hue_delta': 18, 'contrast_prob': 0.5, 'contrast_delta': 0.5, 'saturation_prob': 0.5, 'saturation_delta': 0.5, 'brightness_prob': 0.5, 'brightness_delta': 0.125}, 'rsm_strategy': {'learning_rate': 0.0005, 'lr_epochs': [70, 120, 170, 220, 270, 320], 'lr_decay': [1, 0.5, 0.1, 0.05, 0.01, 0.005, 0.001]}, 'early_stop': {'sample_frequency': 50, 'successive_limit': 5, 'min_instance_error': 0.1}}
将模型转化为固化的模型
# 兼容Python 2.x
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import os
# 它是一个Python 2和3兼容性库
import six
import numpy as np
import random
import time
# 提供了编码和解码功能,用于处理不同编码的文本文件
import codecs
# 它提供了一些变量和函数,用来操纵Python运行时环境
import sys
# 提供了高阶函数和可调用对象操作的工具
import functools
import math
import paddle
import paddle.fluid as fluid
# 它提供了PaddlePaddle底层C++ API的Python接口
from paddle.fluid import core
# 用于定义参数的属性,比如名称、初始化方法、学习率等
from paddle.fluid.param_attr import ParamAttr
# 从PIL中导入Image和ImageEnhance模块。Image模块提供了打开、操作和保存多种不同格式的图像文件的能力
# ImageEnhance模块提供了对图像进行增强(比如亮度、对比度、色彩等)的功能
from PIL import Image, ImageEnhance
# 读取 label_list.txt 文件获取类别数量
# class_dim 被初始化为 -1,表示类别数量未知。稍后,你将从这个文件中读取实际的类别数量
class_dim = -1
# all_file_dir 变量存储了包含数据集和 label_list.txt 文件的目录路径
all_file_dir = "data/data6927/word-recognition"
# 使用 codecs.open 函数打开 label_list.txt 文件(这里假设文件使用默认编码,如 UTF-8,因为 codecs.open 允许指定编码,但这里没有指定)
with codecs.open(os.path.join(all_file_dir, "label_list.txt")) as label_list:
class_dim = len(label_list.readlines())
# 定义了输入图像的目标尺寸
# 1 可能表示批量大小(batch size),48 是图像的高度,512 是图像的宽度
# 但是,在训练深度学习模型时,批量大小通常是在数据加载器中设置的,而不是作为图像尺寸的一部分。因此,这里的 1 可能是一个误导,或者这个列表的用途可能与常规的图像尺寸设置有所不同。
target_size = [1, 48, 512]
# 设置了图像预处理时用于归一化的 RGB 均值
mean_rgb = 127.5
save_freeze_dir = "./crnn-model"
# 定义了一个卷积循环神经网络(CRNN),通常用于光学字符识别(OCR)等序列到序列的任务
class CRNN(object):
def __init__(self, num_classes, label_dict):
self.outputs = None #存储网络的最终输出
self.label_dict = label_dict #标签字典,可能用于将索引映射回人类可读的标签
self.num_classes = num_classes #类别数量
def name(self):
return 'crnn'
# 卷积、批量归一化和池化方法
# group表示分组卷积的数量
# out_ch是一个列表,包含每个卷积层的输出通道数
# act是激活函数的名称,默认为'relu'
# param和bias是卷积层的参数和偏置的属性
# param_0是第二个卷积层(如果group为2)的参数属性
# is_test表示是否处于测试模式
# pooling表示是否执行池化操作
# use_cudnn表示是否使用CUDA深度神经网络库进行加速
def conv_bn_pool(self, input, group, out_ch, act="relu", param=None, bias=None, param_0=None, is_test=False, pooling=True, use_cudnn=False):
tmp = input #输入特征图
for i in six.moves.xrange(group):
tmp = fluid.layers.conv2d(
input=tmp,
num_filters=out_ch[i],
filter_size=3,
padding=1,
param_attr=param if param_0 is None else param_0,
act=None, # LinearActivation
use_cudnn=use_cudnn)
tmp = fluid.layers.batch_norm(
input=tmp,
act=act,
param_attr=param,
bias_attr=bias,
is_test=is_test)
if pooling:
tmp = fluid.layers.pool2d(
input=tmp,
pool_size=2,
pool_type='max',
pool_stride=2,
use_cudnn=use_cudnn,
ceil_mode=True)
return tmp
#OCR卷积方法
def ocr_convs(self, input, regularizer=None, gradient_clip=None, is_test=False, use_cudnn=False):
# 使用了不同的参数初始化器来初始化权重和偏置
b = fluid.ParamAttr(
regularizer=regularizer,
gradient_clip=gradient_clip,
initializer=fluid.initializer.Normal(0.0, 0.0))
w0 = fluid.ParamAttr(
regularizer=regularizer,
gradient_clip=gradient_clip,
initializer=fluid.initializer.Normal(0.0, 0.0005))
w1 = fluid.ParamAttr(
regularizer=regularizer,
gradient_clip=gradient_clip,
initializer=fluid.initializer.Normal(0.0, 0.01))
tmp = input
# 构建卷积、批量归一化和池化层
tmp = self.conv_bn_pool(
tmp,
2, [16, 16],
param=w1,
bias=b,
param_0=w0,
is_test=is_test,
use_cudnn=use_cudnn)
tmp = self.conv_bn_pool(
tmp,
2, [32, 32],
param=w1,
bias=b,
is_test=is_test,
use_cudnn=use_cudnn)
tmp = self.conv_bn_pool(
tmp,
2, [64, 64],
param=w1,
bias=b,
is_test=is_test,
use_cudnn=use_cudnn)
tmp = self.conv_bn_pool(
tmp,
2, [128, 128],
param=w1,
bias=b,
is_test=is_test,
pooling=False,
use_cudnn=use_cudnn)
return tmp
#网络定义,定义了整个CRNN网络
# rnn_hidden_size是RNN隐藏层的大小
def net(self, images, rnn_hidden_size=200, regularizer=None,
gradient_clip=None, is_test=False, use_cudnn=True):
# ocr_convs方法获取卷积特征
conv_features = self.ocr_convs(
images,
regularizer=regularizer,
gradient_clip=gradient_clip,
is_test=is_test,
use_cudnn=use_cudnn)
# im2sequence将卷积特征图转换为序列数据
sliced_feature = fluid.layers.im2sequence(
input=conv_features,
stride=[1, 1],
filter_size=[conv_features.shape[2], 1])
para_attr = fluid.ParamAttr(
regularizer=regularizer,
gradient_clip=gradient_clip,
initializer=fluid.initializer.Normal(0.0, 0.02))
bias_attr = fluid.ParamAttr(
regularizer=regularizer,
gradient_clip=gradient_clip,
initializer=fluid.initializer.Normal(0.0, 0.02))
bias_attr_nobias = fluid.ParamAttr(
regularizer=regularizer,
gradient_clip=gradient_clip,
initializer=fluid.initializer.Normal(0.0, 0.02))
# 定义了两个全连接层(fc_1和fc_2),它们的输出作为双向GRU的输入
fc_1 = fluid.layers.fc(input=sliced_feature,
size=rnn_hidden_size * 3,
param_attr=para_attr,
bias_attr=bias_attr_nobias)
fc_2 = fluid.layers.fc(input=sliced_feature,
size=rnn_hidden_size * 3,
param_attr=para_attr,
bias_attr=bias_attr_nobias)
# 使用dynamic_gru构建双向GRU
gru_forward = fluid.layers.dynamic_gru(
input=fc_1,
size=rnn_hidden_size,
param_attr=para_attr,
bias_attr=bias_attr,
candidate_activation='relu')
gru_backward = fluid.layers.dynamic_gru(
input=fc_2,
size=rnn_hidden_size,
is_reverse=True,
param_attr=para_attr,
bias_attr=bias_attr,
candidate_activation='relu')
w_attr = fluid.ParamAttr(
regularizer=regularizer,
gradient_clip=gradient_clip,
initializer=fluid.initializer.Normal(0.0, 0.02))
b_attr = fluid.ParamAttr(
regularizer=regularizer,
gradient_clip=gradient_clip,
initializer=fluid.initializer.Normal(0.0, 0.0))
# # 最后,使用一个全连接层将GRU的输出转换为类别概率
fc_out = fluid.layers.fc(input=[gru_forward, gru_backward],
size=self.num_classes + 1,
param_attr=w_attr,
bias_attr=b_attr)
self.outputs = fc_out
return fc_out
# 计算网络的损失
def get_loss(self, label):
# 计算CTC(连接主义时间分类)损失,这是OCR任务中常用的损失函数
# blank参数是CTC损失中的空白标签索引
# norm_by_times=True表示损失将按时间步长归一化
cost = fluid.layers.warpctc(input=self.outputs, label=label, blank=self.num_classes, norm_by_times=True)
# 使用reduce_sum计算总损失
sum_cost = fluid.layers.reduce_sum(cost)
return sum_cost
def get_infer(self):
# 执行贪婪解码,以获取网络的预测输出
return fluid.layers.ctc_greedy_decoder(input=self.outputs, blank=self.num_classes)
# 将训练好的CRNN模型冻结(或导出)为可用于推理(inference)的格式
def freeze_model():
# 创建一个PaddlePaddle的执行器(Executor),并指定其运行在CPU上
exe = fluid.Executor(fluid.CPUPlace())
# 指定模型输入数据的格式
image = fluid.layers.data(name='image', shape=target_size, dtype='float32')
# 实例化一个CRNN模型。class_dim指定了分类的维度(即模型最终输出的类别数)
model = CRNN(class_dim, {})
# 调用模型的net方法,将前面定义的image数据层作为输入,得到模型的预测输出pred
pred = model.net(image)
# 调用模型的get_infer方法,该方法通常用于获取模型用于推理的输出层
out = model.get_infer()
# 获取当前的默认主程序(main program)
freeze_program = fluid.default_main_program()
# 加载持久化的变量(即模型的参数)
fluid.io.load_persistables(exe, save_freeze_dir, freeze_program)
# 克隆当前的freeze_program,并设置for_test=True,表示这是一个用于测试(或推理)的程序
freeze_program = freeze_program.clone(for_test=True)
# 将模型保存为推理格式
fluid.io.save_inference_model("./freeze-model", ['image'], out, exe, freeze_program)
if __name__ == '__main__':
freeze_model()
print("保存模型成功.")
在验证集合上测试单词的准确率
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import os
import numpy as np
import random
import time
import codecs
import sys
import functools
import math
import paddle
import paddle.fluid as fluid
from paddle.fluid import core
from paddle.fluid.param_attr import ParamAttr
from PIL import Image, ImageEnhance
import matplotlib.pyplot as plt
target_size = [1, 48, 512]
mean_rgb = 127.5 # 定义了图像预处理时使用的RGB均值
data_dir = 'data/data6927/word-recognition'
eval_file = "eval.txt" # 指定了包含评估数据列表的文件名。这个文件通常包含图像文件的路径和对应的标签
label_list = "label_list.txt" # 指定了包含所有可能标签的文件名。这个文件通常用于将模型输出的索引映射到人类可读的标签
use_gpu = True
label_dict = {}
save_freeze_dir = "./freeze-model"
place = fluid.CPUPlace()
exe = fluid.Executor(place)
# 加载模型,返回三个组件,推理程序,输入变量的名称列表,输出变量的名称列表
[inference_program, feed_target_names, fetch_targets] = \
fluid.io.load_inference_model(dirname=save_freeze_dir, executor=exe)
# print(fetch_targets)
# 初始化评估所需的参数,特别是与标签列表相关的参数
def init_eval_parameters():
"""
初始化训练参数,主要是初始化图片数量,类别数
:return:
"""
label_list_path = os.path.join(data_dir, label_list)
index = 0
# 读取样本文件内容,并存入字典
with codecs.open(label_list_path, encoding='utf-8') as flist:
lines = [line.strip() for line in flist]
for line in lines:
parts = line.split()
# # 假设每行包含两个由空格分隔的部分:索引和标签 ,将索引映射到标签名称
label_dict[int(parts[1])] = parts[0]
# 调整图像大小,并将其粘贴到一个具有特定目标大小的新图像背景上
def resize_img(img):
"""
重设图像大小
:param img:
:return:
"""
# 提取目标高度和宽度
target_height, target_width, _ = target_size
# 计算缩放比例
percent_h = float(target_height) / img.size[1]
percent_w = float(target_width) / img.size[0]
percent = min(percent_h, percent_w)
# 调整图像大小
resized_width = int(round(img.size[0] * percent))
resized_height = int(round(img.size[1] * percent))
# 计算偏移量
w_off = (target_size[2] - resized_width) / 2
h_off = (target_size[1] - resized_height) / 2
# 创建背景图像
img = img.resize((resized_width, resized_height), Image.ANTIALIAS)
array = np.ndarray((target_size[1], target_size[2], 3), np.uint8)
# 为新图像设置了一个灰色背景(RGB值为127, 127, 127)
array[:, :, 0] = 127
array[:, :, 1] = 127
array[:, :, 2] = 127
## 创建背景图像
ret = Image.fromarray(array)
# 将调整大小后的图像粘贴到背景图像上
ret.paste(img, (np.random.randint(0, w_off + 1), int(h_off)))
return ret
# 读取图像,调整大小,转换为RGB(如果尚未是),减去RGB均值,并重塑为适合模型输入的格式
def read_image(img_path):
"""
读取图像
:param img_path:
:return:
"""
img = Image.open(img_path)
if img.mode != 'RGB':
img = img.convert('RGB')
img = resize_img(img)
img = img.convert('L') # 返回一个转换后的副本,L模式进行转换
img = np.array(img).astype('float32') - mean_rgb # 从每个通道中减去RGB均值
img = img[..., np.newaxis]
img = img.transpose((2, 0, 1))# 重塑为(C, H, W)
img = img[np.newaxis, :]# 添加一个批次维度,变为(1, C, H, W)
return img
def infer(image_path):
"""
执行预测,对给定路径的图像执行预测,并返回预测结果的字符串
:param image_path:
:return:
"""
# 读取并处理图像
tensor_img = read_image(image_path)
# 执行预测
# 注意:这里假设exe.run返回的是Tensor对象,我们需要将其转换为NumPy数组
# 由于您之前设置了return_numpy=False,这里需要改为True或者手动转换
# 但为了保持一致性,我假设有一个正确的方法来获取NumPy数组
# (在实际应用中,您可能需要调整这部分代码以适应您的PaddlePaddle版本和API)
label = exe.run(inference_program,
feed={feed_target_names[0]: tensor_img},
fetch_list=fetch_targets,
return_numpy=False) # 修改为True以直接获取NumPy数组
# 处理预测结果
# 假设result是一个包含预测标签的NumPy数组
# 这里需要根据实际情况调整索引和形状
# label[0] : 获取第一个输出(假设只有一个输出)
label = np.array(label[0])
ret = ""
if label[0] != -1:
# 将标签转换为字符串表示
# 假设label是一个一维数组,每个元素是标签的索引
ret = ret.join([label_dict[int(c[0])] for c in label])
return ret
def eval_all():
"""
评估所有,从评估文件中读取数据并评估模型性能
:return:
"""
eval_file_path = os.path.join(data_dir, eval_file) # 评估文件路径
total_count = 0
right_count = 0
"""
# 如果需要评估文件中的数据,请取消以下代码的注释
with codecs.open(eval_file_path, encoding='utf-8') as flist:
lines = [line.strip() for line in flist]
t1 = time.time()
random.shuffle(lines) # 打乱样本
i = 0
for line in lines:
i += 1
if i > 3:
break
total_count += 1
parts = line.strip().split()
result = infer(parts[0]) # 执行推测
img = Image.open(parts[0])
plt.imshow(img)
plt.show()
print("infer result:{0} answer:{1}".format(result, parts[1]))
if str(result) == parts[1]:
right_count += 1
period = time.time() - t1
print("count:{0} time:{1} accuracy:{2}".format(total_count,
"%2.2f sec" % period, right_count / total_count))
"""
#测试自定义图片
img_file ="2.png"
result = infer(img_file) # 执行推测
print("infer result:{0}".format(result))
img = Image.open(img_file)
plt.imshow(img)
plt.show()
if __name__ == '__main__':
init_eval_parameters()
eval_all()