赛题背景
关于光学字符识别(Optical Character Recognition, 简称OCR),是指将图像上的文字转化为计算机可编辑的文字内容,众多的研究人员对相关的技术研究已久,也有不少成熟的OCR技术和产品产生,比如PaddleOCR。中文汉字识别是OCR的一个分支。因为汉语作为我们的母语,汉字主要在我国广泛使用,对汉字的种类、内涵、造字原理国内的掌握情况较透彻,所以关于汉字识别的深入研究主要集中在国内。
中文场景文字识别技术在人们的日常生活中受到广泛关注,具有丰富的应用场景,如:拍照翻译、图像检索、场景理解等。然而,中文场景中的文字面临着包括光照变化、低分辨率、字体以及排布多样性、中文字符种类多等复杂情况。如何解决上述问题成为一项极具挑战性的任务。
在本月中文场景文字识别赛中,笔者使用了飞桨开源深度学习框架,在百度学习与实训社区AI Studio上完成了数据处理、模型搭建、模型训练、模型预测等整个工作过程,拿到了6月份前十名,获得了800元京东卡。非常感谢AI Studio为参赛选手提供的GPU在线训练环境,以及丰厚GPU使用时长,对于在学习深度学习过程中硬件条件不足的学生党来说,提供了非常大的帮助。
比赛链接:
https://aistudio.baidu.com/ai...
项目链接:
https://aistudio.baidu.com/ai...
赛题数据分析
要求实现对街拍图片中的文字提取,并且提取出的文字序列还是长短不一的。考虑卷积神经网络在图像特征提取方面有很好的效果,输出可以采用CTC实现长度可变的预测。
1.图片信息:
(a) 标注:魅派集成吊顶
(b) 标注:母婴用品连锁
2.训练集标注文件:
h表示图片高,w表示图片宽,name为图片路径,value为图片对应的标注(如:母婴用品连锁)。
3.要求的预测结果格式为:
name为测试集图片名字,value为预测出的结果(如:母婴用品连锁)
模型原理
模型采用CRNN-CTC结构(CNN+RNN+CTC):先用CNN网络提取图像特征,转化为时间序列再传入RNN网络,最后输出使用CTC层(不同样本的标签序列长度可以不一致)。
结构图:
1. CNN层搭建:
卷积层的分量是通过从标准CNN模型中提取卷积层和最大池化层来构造的(全连接层被移除)。该模块用于从输入图像中提取序列特征表示。在被输入到网络之前,所有的图像都需要缩放到相同的高度。然后从卷积层分量产生的特征映射中提取一系列特征向量,这是递归层的输入。
#卷积层的Paddle实现
paddle.fluid.layers.conv2d(input, num_filters, filter_size, stride=1, padding=0, dilation=1, groups=None, param_attr=None, bias_attr=None, use_cudnn=True, act=None, name=None, data_format="NCHW")
#最大池化层的Paddle实现
paddle.fluid.layers.pool2d(input, pool_size=-1, pool_type='max', pool_stride=1, pool_padding=0, global_pooling=False, use_cudnn=True, ceil_mode=False, name=None, exclusive=True, data_format="NCHW")
#全连接层的Paddle实现
paddle.fluid.layers.fc(input, size, num_flatten_dims=1, param_attr=None, bias_attr=None, act=None, name=None)
2.LSTM层(递归层):
(a)图是传统的LSTM结构:一个LSTM由一个单元模块和三个门组成,即输入门、输出门和遗忘门。
(b)图是论文中使用的结构:深层双向LSTM的结构。将前向(从左到右)和后向(从右到左)LSTM相结合构成双向LSTM。堆叠2个双向LSTM构成深层双向LSTM。
本次比赛使用代码实现用的是双层GRU单元:
paddle.fluid.layers.dynamic_gru(input, size, param_attr=None, bias_attr=None, is_reverse=False, gate_activation='sigmoid', candidate_activation='tanh', h_0=None, origin_mode=False)
飞桨也提供了LSTM的实现方法
paddle.fluid.layers.dynamic_lstm(input, size, h_0=None, c_0=None, param_attr=None, bias_attr=None, use_peepholes=True, is_reverse=False, gate_activation='sigmoid', cell_activation='tanh', candidate_activation='tanh', dtype='float32', name=None)
3.CTC层(转录层):
转录是将RNN所做的每帧预测转换为标签序列的过程。从数学上讲,转录是找到基于每帧预测的概率最高的标签序列。在实践中,存在两种转录模式,即无词典转录和基于词典的转录。词汇是一组标签序列,预测是对的约束,例如。拼写检查字典。在无词汇模式下,预测是在没有任何词汇的情况下进行的。在基于词汇的模式下,预测是通过选择概率最高的标签序列来进行的。
我是使用第二种:预测通过选择概率最高的标签序列来进行。
#Paddle已经提供了代码实现
paddle.fluid.layers.ctc_greedy_decoder(input, blank, name=None)
4.现在来总结一下该模型的搭建吧!
论文中提供的网络层和参数的图片已经很直观了,稍微解释一下(从下往上看):
第一层(卷积层):图片(input)经过1层步长为1(s表示),填充为1(p表示)的3x3卷积,过滤器数量为64.
第二层(最大池化层):第一层的输出进行2x2的最大池化,步长为2,以此类推。BatchNormalization表示批归一化:用batch_norm实现
#batch_norm的Paddle实现
paddle.fluid.layers.batch_norm(input, act=None, is_test=False, momentum=0.9, epsilon=1e-05, param_attr=None, bias_attr=None, data_layout='NCHW', in_place=False, name=None, moving_mean_name=None, moving_variance_name=None, do_model_average_for_mean_and_var=False, use_global_stats=False)
Bidirectional-LSTM在论文中为2层的双向LSTM。实现代码中我使用的是2层的GRU单元,读者可以尝试使用LSTM。
回溯时间(BPTT)是在递归层的底部,将传播的差分序列连接成映射,将特征映射转换为特征序列的操作倒置,并反馈给卷积层。在实践中,我们创建了一个自定义网络层,称为“映射到等”,作为卷积层和递归层之间的桥梁。
完整步骤的代码实现为:
import paddle.fluid as fluid
from paddle.fluid import ParamAttr
from paddle.fluid.clip import GradientClipByNorm
from paddle.fluid.regularizer import L2Decay
from paddle.fluid.initializer import MSRA, Normal
from paddle.fluid.layers import conv2d, conv2d_transpose, batch_norm, fc, dynamic_gru, im2sequence, elementwise_mul, \
pool2d, dropout, concat
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'
def conv_bn_pool(self, x, n_filters, n_ConvBN, pool_stride, w_conv, is_test):
w_bn = ParamAttr(regularizer=L2Decay(0.001))#设置L2正则化,初始化权重
b_bn = ParamAttr(regularizer=L2Decay(0.001), initializer=Normal(0.0, 0.0))
for _ in range(n_ConvBN):
x = conv2d(x, n_filters, 3, 1, 1, param_attr=w_conv)#定义卷积层
#批归一化
x = batch_norm(x, act='relu', param_attr=w_bn, bias_attr=b_bn, is_test=is_test)
assert pool_stride in [2, (2, 1), (3, 1)]#使用断言
if pool_stride == 2:
x = pool2d(x, 2, 'max', pool_stride, 0, ceil_mode=True)#定义池化层,最大池化
elif pool_stride == (2, 1):
x = pool2d(x, (2, 1), 'max', pool_stride, 0, ceil_mode=True)
elif pool_stride == (3, 1):
x = pool2d(x, (3, 1), 'max', pool_stride, 0, ceil_mode=True)
return x
def ocr_convs(self, x, is_test):
w_conv1 = ParamAttr(regularizer=L2Decay(0.001))
w_conv2 = ParamAttr(regularizer=L2Decay(0.001))
w_conv3 = ParamAttr(regularizer=L2Decay(0.001))
x = self.conv_bn_pool(x, 128, 1, 2, w_conv1, is_test)
x = self.conv_bn_pool(x, 256, 1, 2, w_conv2, is_test)
x = self.conv_bn_pool(x, 512, 2, 2, w_conv2, is_test)
x = self.conv_bn_pool(x, 1024, 2, (2, 1), w_conv3, is_test)
return x
def net(self, images, rnn_hidden_size=750, is_test=False):
w_fc = ParamAttr(regularizer=L2Decay(0.001))
b_fc1 = ParamAttr(regularizer=L2Decay(0.001), initializer=Normal(0.0, 0.0))
b_fc2 = ParamAttr(regularizer=L2Decay(0.001), initializer=Normal(0.0, 0.0), learning_rate=2.0)
b_fc3 = ParamAttr(regularizer=L2Decay(0.001), initializer=Normal(0.0, 0.0))
x = self.ocr_convs(images, is_test)
x = im2sequence(x, (x.shape[2], 1), (1, 1))#用 filter 扫描输入的Tensor并将输入Tensor转换成序列
fc_1 = fc(x, rnn_hidden_size * 3, param_attr=w_fc, bias_attr=b_fc1)#定义全连接层,将cnn层输出处理成序列,用于代入RNN层
fc_2 = fc(x, rnn_hidden_size * 3, param_attr=w_fc, bias_attr=b_fc1)
gru_forward = dynamic_gru(fc_1, rnn_hidden_size, param_attr=w_fc, bias_attr=b_fc2, candidate_activation='relu')#用于在完整序列上逐个时间步的进行单层Gated Recurrent Unit(GRU)的计算
gru_backward = dynamic_gru(fc_2, rnn_hidden_size, param_attr=w_fc, bias_attr=b_fc2, candidate_activation='relu',
is_reverse=True)#使用2层结构
bigru = gru_forward + gru_backward
bigru = dropout(bigru, 0.5, is_test)#使用随机丢弃单元的正则化方法
fc_out = fc(bigru, self.num_classes + 1, param_attr=w_fc, bias_attr=b_fc3)#全连接层
self.outputs = fc_out
return fc_out
def get_infer(self, images):#CTC转录层
return fluid.layers.ctc_greedy_decoder(input=self.outputs, blank=self.num_classes)
方法总结
该项目基于官方基线(baseline)上进行优化,在测试集上的分数大约为75.79,我在此基础上大约提高了5%。本项目主要是做了数据增强,加上必要的调参,网络部分基本上改动不大。原数据集是21万张训练集,我将他增加到了42万张(翻了2倍)。
本次使用的数据增强方法有:
对这42万张训练集图片都进行调整亮度、色度、对比度、饱和度,增加模型鲁棒性;
对其中21万张图片再进行随机的小幅度左右旋转(旋转角度为10到10之间)以及随机放大(1-2倍之间)。
在比赛之前,通常掌握的都是各方面各种课程的比较零散的知识,通过比赛,用这些知识,一步步构建出一个项目,优化模型,达到比较好的效果,并且还拿到了奖品,这是非常值得开心的。
通过这篇文章大家学会如何做中文场景文字识别了吗?十一月初AI Studio继续上线中文场景文字识别常规赛,快来大展身手吧。每月前十名选手还有奖品相送。
如在使用过程中有问题,可加入飞桨官方QQ群进行交流:1108045677。
如果您想详细了解更多飞桨的相关内容,请参阅以下文档。
·飞桨开源框架项目地址·
GitHub: https://github.com/PaddlePadd...
Gitee: https://gitee.com/paddlepaddl...
·飞桨官网地址·