如何在中文场景文字识别赛中赢取飞桨周边?

246 阅读8分钟

【飞桨开发者说】魏宏炜,福建省三明学院,本科三年级,研究方向为目标检测、OCR识别。

赛题背景

关于光学字符识别(Optical Character Recognition, 简称OCR),是指将图像上的文字转化为计算机可编辑的文字内容,众多的研究人员对相关的技术研究已久,也有不少成熟的OCR技术和产品产生,比如PaddleOCR。中文汉字识别是OCR的一个分支。因为汉语作为我们的母语,汉字主要在我国广泛使用,对汉字的种类、内涵、造字原理国内的掌握情况较透彻,所以关于汉字识别的深入研究主要集中在国内。

中文场景文字识别技术在人们的日常生活中受到广泛关注,具有丰富的应用场景,如:拍照翻译、图像检索、场景理解等。然而,中文场景中的文字面临着包括光照变化、低分辨率、字体以及排布多样性、中文字符种类多等复杂情况。如何解决上述问题成为一项极具挑战性的任务。

在本月中文场景文字识别赛中,笔者使用了飞桨开源深度学习框架,在百度学习与实训社区AI Studio上完成了数据处理、模型搭建、模型训练、模型预测等整个工作过程,拿到了6月份前十名,获得了800元京东卡。非常感谢AI Studio为参赛选手提供的GPU在线训练环境,以及丰厚GPU使用时长,对于在学习深度学习过程中硬件条件不足的学生党来说,提供了非常大的帮助。

比赛链接:

aistudio.baidu.com/aistudio/co…

项目链接:

aistudio.baidu.com/aistudio/pr…

赛题数据分析

要求实现对街拍图片中的文字提取,并且提取出的文字序列还是长短不一的。考虑卷积神经网络在图像特征提取方面有很好的效果,输出可以采用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倍)。

本次使用的数据增强方法有:

  1. 对这42万张训练集图片都进行调整亮度、色度、对比度、饱和度,增加模型鲁棒性;

  2. 对其中21万张图片再进行随机的小幅度左右旋转(旋转角度为10到10之间)以及随机放大(1-2倍之间)。

在比赛之前,通常掌握的都是各方面各种课程的比较零散的知识,通过比赛,用这些知识,一步步构建出一个项目,优化模型,达到比较好的效果,并且还拿到了奖品,这是非常值得开心的。