【DCIC2022】科技金融子赛道验证码识别0.65+ baseline

2022-03-11 14:15:27 浏览数 (1)

刚开始做OCR比赛,周末补了下相关知识,主要参考内容来自【官方】十分钟掌握PaddleOCR使用,不过目前分数不是很高,0.65 ,主要存在过拟合问题,大家可以再修改配置或者模型再微调下,这里主要给大家提供一个流水线和科普下CRNN的相关原理

  • 比赛链接:https://www.dcic-china.com/competitions/10023
  • baseline链接:https://aistudio.baidu.com/aistudio/projectdetail/3438655

1 文本识别

在传统的文本识别方法中,任务分为3个步骤,即图像预处理、字符分割和字符识别。需要对特定场景进行建模,一旦场景变化就会失效。面对复杂的文字背景和场景变动,基于深度学习的方法具有更优的表现。

多数现有的识别算法可用如下统一框架表示,算法流程被划分为4个阶段:

我们整理了主流的算法类别和主要论文,参考下表:

算法类别

主要思路

主要论文

传统算法

滑动窗口、字符提取、动态规划

-

ctc

基于ctc的方法,序列不对齐,更快速识别

CRNN, Rosetta

Attention

基于attention的方法,应用于非常规文本

RARE, DAN, PREN

Transformer

基于transformer的方法

SRN, NRTR, Master, ABINet

校正

校正模块学习文本边界并校正成水平方向

RARE, ASTER, SAR

分割

基于分割的方法,提取字符位置再做分类

Text Scanner, Mask TextSpotter

2 CRNN 的原理及流程

2.1 所属类别

CRNN 是基于CTC的算法,在理论部分介绍的分类图中,处在如下位置。可以看出CRNN主要用于解决规则文本,基于CTC的算法有较快的预测速度并且很好的适用长文本。因此CRNN是PPOCR选择的中文识别算法。

2.2 算法详解

CRNN 的网络结构体系如下所示,从下往上分别为卷积层、递归层和转录层三部分:

1)backbone:

卷积网络作为底层的骨干网络,用于从输入图像中提取特征序列。由于 convmax-poolingelementwise 和激活函数都作用在局部区域上,所以它们是平移不变的。因此,特征映射的每一列对应于原始图像的一个矩形区域(称为感受野),并且这些矩形区域与它们在特征映射上对应的列从左到右的顺序相同。由于CNN需要将输入的图像缩放到固定的尺寸以满足其固定的输入维数,因此它不适合长度变化很大的序列对象。为了更好的支持变长序列,CRNN将backbone最后一层输出的特征向量送到了RNN层,转换为序列特征。

MobileNetV3为例

代码语言:javascript复制
class MobileNetV3(nn.Layer):
    def __init__(self,
                 in_channels=3,
                 model_name='small',
                 scale=0.5,
                 small_stride=None,
                 disable_se=False,
                 **kwargs):
        super(MobileNetV3, self).__init__()
        self.disable_se = disable_se
        
        small_stride = [1, 2, 2, 2]

        if model_name == "small":
            cfg = [
                # k, exp, c,  se,     nl,  s,
                [3, 16, 16, True, 'relu', (small_stride[0], 1)],
                [3, 72, 24, False, 'relu', (small_stride[1], 1)],
                [3, 88, 24, False, 'relu', 1],
                [5, 96, 40, True, 'hardswish', (small_stride[2], 1)],
                [5, 240, 40, True, 'hardswish', 1],
                [5, 240, 40, True, 'hardswish', 1],
                [5, 120, 48, True, 'hardswish', 1],
                [5, 144, 48, True, 'hardswish', 1],
                [5, 288, 96, True, 'hardswish', (small_stride[3], 1)],
                [5, 576, 96, True, 'hardswish', 1],
                [5, 576, 96, True, 'hardswish', 1],
            ]
            cls_ch_squeeze = 576
        else:
            raise NotImplementedError("mode["   model_name  
                                      "_model] is not implemented!")

        supported_scale = [0.35, 0.5, 0.75, 1.0, 1.25]
        assert scale in supported_scale, 
            "supported scales are {} but input scale is {}".format(supported_scale, scale)

        inplanes = 16
        # conv1
        self.conv1 = ConvBNLayer(
            in_channels=in_channels,
            out_channels=make_divisible(inplanes * scale),
            kernel_size=3,
            stride=2,
            padding=1,
            groups=1,
            if_act=True,
            act='hardswish')
        i = 0
        block_list = []
        inplanes = make_divisible(inplanes * scale)
        for (k, exp, c, se, nl, s) in cfg:
            se = se and not self.disable_se
            block_list.append(
                ResidualUnit(
                    in_channels=inplanes,
                    mid_channels=make_divisible(scale * exp),
                    out_channels=make_divisible(scale * c),
                    kernel_size=k,
                    stride=s,
                    use_se=se,
                    act=nl))
            inplanes = make_divisible(scale * c)
            i  = 1
        self.blocks = nn.Sequential(*block_list)

        self.conv2 = ConvBNLayer(
            in_channels=inplanes,
            out_channels=make_divisible(scale * cls_ch_squeeze),
            kernel_size=1,
            stride=1,
            padding=0,
            groups=1,
            if_act=True,
            act='hardswish')

        self.pool = nn.MaxPool2D(kernel_size=2, stride=2, padding=0)
        self.out_channels = make_divisible(scale * cls_ch_squeeze)

    def forward(self, x):
        x = self.conv1(x)
        x = self.blocks(x)
        x = self.conv2(x)
        x = self.pool(x)
        return x

2)neck:

递归层,在卷积网络的基础上,构建递归网络,将图像特征转换为序列特征,预测每个帧的标签分布。 RNN具有很强的捕获序列上下文信息的能力。使用上下文线索进行基于图像的序列识别比单独处理每个像素更有效。以场景文本识别为例,宽字符可能需要几个连续的帧来充分描述。此外,有些歧义字符在观察其上下文时更容易区分。其次,RNN可以将误差差分反向传播回卷积层,使网络可以统一训练。第三,RNN能够对任意长度的序列进行操作,解决了文本图片变长的问题。CRNN使用双层LSTM作为递归层,解决了长序列训练过程中的梯度消失和梯度爆炸问题。

代码语言:javascript复制
class Im2Seq(nn.Layer):
    def __init__(self, in_channels, **kwargs):
        """
        图像特征转换为序列特征
        :param in_channels: 输入通道数
        """ 
        super().__init__()
        self.out_channels = in_channels

    def forward(self, x):
        B, C, H, W = x.shape
        assert H == 1
        x = x.squeeze(axis=2)
        x = x.transpose([0, 2, 1])  # (NWC)(batch, width, channels)
        return x

class EncoderWithRNN(nn.Layer):
    def __init__(self, in_channels, hidden_size):
        super(EncoderWithRNN, self).__init__()
        self.out_channels = hidden_size * 2
        self.lstm = nn.LSTM(
            in_channels, hidden_size, direction='bidirectional', num_layers=2)

    def forward(self, x):
        x, _ = self.lstm(x)
        return x


class SequenceEncoder(nn.Layer):
    def __init__(self, in_channels, hidden_size=48, **kwargs):
        """
        序列编码
        :param in_channels: 输入通道数
        :param hidden_size: 隐藏层size
        """ 
        super(SequenceEncoder, self).__init__()
        self.encoder_reshape = Im2Seq(in_channels)

        self.encoder = EncoderWithRNN(
            self.encoder_reshape.out_channels, hidden_size)
        self.out_channels = self.encoder.out_channels

    def forward(self, x):
        x = self.encoder_reshape(x)
        x = self.encoder(x)
        return x

3)head:

转录层,通过全连接网络和softmax激活函数,将每帧的预测转换为最终的标签序列。最后使用 CTC Loss 在无需序列对齐的情况下,完成CNN和RNN的联合训练。CTC 有一套特别的合并序列机制,LSTM输出序列后,需要在时序上分类得到预测结果。可能存在多个时间步对应同一个类别,因此需要对相同结果进行合并。为避免合并本身存在的重复字符,CTC 引入了一个 blank 字符插入在重复字符之间。

代码语言:javascript复制
class CTCHead(nn.Layer):
    def __init__(self,
                 in_channels,
                 out_channels,
                 **kwargs):
        """
        CTC 预测层
        :param in_channels: 输入通道数
        :param out_channels: 输出通道数
        """ 
        super(CTCHead, self).__init__()
        self.fc = nn.Linear(
            in_channels,
            out_channels)
        
        # 思考:out_channels 应该等于多少?
        self.out_channels = out_channels

    def forward(self, x):
        predicts = self.fc(x)
        result = predicts

        if not self.training:
            predicts = F.softmax(predicts, axis=2)
            result = predicts

        return result

3 基于CRNN实现文本字符交易验证码识别

本节主要使用PaddleOCR源码来实现 2022数字中国创新大赛(简称2022 DCIC)的科技金融子赛道——基于文本字符的交易验证码识别比赛baseline

3.1 比赛简介

验证码作为性价较高的安全验证方法,在多场合得到了广泛的应用,有效地防止了机器人进行身份欺骗,其中,以基于文本字符的静态验证码最为常见。随着使用的深入,噪声点、噪声线、重叠、形变等干扰手段层出不穷,不断提升安全防范级别。RPA技术作为企业数字化转型的关键,因为其部署的非侵入式备受企业青睐,验证码识别率不高往往限制了RPA技术的应用。一个能同时过滤多种干扰的验证码模型,对于相关自动化技术的拓展使用有着一定的商业价值。

赛题任务

本次大赛以已标记字符信息的实例字符验证码图像数据为训练样本,参赛选手需基于提供的样本构建模型,对测试集中的字符验证码图像进行识别,提取有效的字符信息。训练数据集不局限于提供的数据,可以加入公开的数据集。

数据与评测

数据简介

此次比赛为选手提供15000张带标注信息的训练数据集,每张训练数据都是包含一个4位文本字符的验证码图像,并对当前图像中的文本字符进行了标注;测试数据集含25000张验证码图像。

数据说明

提供训练数据集打包文件train_imgs.zip(文件名称即对应该图片文本字符标签);提供测试数据集打包文件test_imgs.zip,测试数据集包含待识别的图像文件。

文件名称

说明

train_imgs.zip

训练集图片,包含15000张验证码图片

test_imgs.zip

测试集图片,里面包含25000张待识别验证码图片

submit_example.csv

提交样例,参赛者参考此数据格式进行提交

评测标准

本次比赛采用评价方式为准确率(accuracy),对于参赛者提交的结果,要求完全识别出完整的验证码文本信息,最终根据测试图像数据预测的准确率进行从高到低的排序。 同等准确率的以提交结果的时间排名,先提交者胜出。

3.2 数据划分

我们可以将15000张训练集按照8:2进行划分,12000张作为训练集 3000作为验证集

代码语言:javascript复制
import pandas as pd
import shutil
import os
import glob
from tqdm import tqdm

from sklearn.model_selection import train_test_split

data_path = 'train_data/'
dcic_data_path = './PaddleOCR/train_data/dcic_data/'
dcic_train = './PaddleOCR/train_data/dcic_data/train'
dcic_valid = './PaddleOCR/train_data/dcic_data/valid'
dcic_test = './PaddleOCR/train_data/dcic_data/test'

os.makedirs(dcic_data_path, exist_ok=True)
os.makedirs(dcic_train, exist_ok=True)
os.makedirs(dcic_valid, exist_ok=True)
os.makedirs(dcic_test, exist_ok=True)

# print([filepath for filepath in glob.glob('data/dcic_data/training_dataset/')])
# print(glob.glob('data/dcic_data/training_dataset/*.png'))
# print(os.listdir('data/training_dataset'))

train_images = os.listdir('data/training_dataset')
test_images = os.listdir('data/test_dataset')
train_imgs, valid_imgs = train_test_split(train_images, test_size=0.2, random_state=42, shuffle=True)
print(len(train_imgs), len(valid_imgs))

all_txts = []
# shutil.copy('data/dcic_data/training_dataset/0A5o.png', 'train_data/dcic_data/train/0A5o.png')
with open('./PaddleOCR/train_data/dcic_data/rec_gt_train.txt', 'w', encoding='utf-8') as f:
    for image in tqdm(train_imgs):
        shutil.copy(f'data/training_dataset/{image}', f'./PaddleOCR/train_data/dcic_data/train/{image}')
        txt = image.split('.png')[0]
        all_txts.append(txt)
        f.write(f'train/{image}t{txt}'   'n')
with open('./PaddleOCR/train_data/dcic_data/rec_gt_valid.txt', 'w', encoding='utf-8') as f:
    for image in tqdm(valid_imgs):
        shutil.copy(f'data/training_dataset/{image}', f'./PaddleOCR/train_data/dcic_data/valid/{image}')
        txt = image.split('.png')[0]
        all_txts.append(txt)
        f.write(f'valid/{image}t{txt}'   'n')
for image in tqdm(test_images):
    shutil.copy(f'data/test_dataset/{image}', f'./PaddleOCR/train_data/dcic_data/test/{image}')

# with open('train_data/dcic_data/captcha.txt', 'w', encoding='utf-8') as f:
#     all_str = ''.join(all_txts)
#     dict_char=sorted(set(all_str))
#     for char in dict_char:
#         f.write(char 'n')

3.3 数据划分

将以下内容填充到./PaddleOCR/configs/rec/rec_dcic_train.yml,为了方面大家理解,我这里加了一些核心注释:

代码语言:javascript复制
Global:
  use_gpu: true
  # 训练轮数
  epoch_num: 300 
  log_smooth_window: 20
  print_batch_step: 10
  # 模型保存路径
  save_model_dir: ./output/rec/dcic/
  save_epoch_step: 3
  # evaluation is run every 2000 iterations
  eval_batch_step: [0, 2000]
  cal_metric_during_train: True
  pretrained_model: pretrain_models/rec_mv3_none_bilstm_ctc/best_accuracy
  checkpoints:
  save_inference_dir: ./
  use_visualdl: False
  infer_img: doc/imgs_words_en/word_10.png
  # for data or label process
  character_dict_path: ppocr/utils/en_dict.txt
  max_text_length: 4
  infer_mode: False
  use_space_char: False
  save_res_path: ./output/rec/predicts_dcic.txt
# 优化器设置
Optimizer:
  name: Adam
  beta1: 0.9
  beta2: 0.999
  lr:
    learning_rate: 0.0005
  regularizer:
    name: 'L2'
    factor: 0
# 模型结构
Architecture:
  model_type: rec
  algorithm: CRNN
  Transform:
  Backbone:
    name: MobileNetV3
    scale: 0.5
    model_name: large
  Neck:
    name: SequenceEncoder
    encoder_type: rnn
    # rnn隐层单元个数,超参数
    hidden_size: 96
  Head:
    name: CTCHead
    fc_decay: 0

Loss:
  name: CTCLoss

PostProcess:
  name: CTCLabelDecode

Metric:
  name: RecMetric
  main_indicator: acc

Train:
  dataset:
    name: SimpleDataSet
    # 训练集路径
    data_dir: ./train_data/dcic_data/
    # 训练集标签文件
    label_file_list: ["./train_data/dcic_data/rec_gt_train.txt"]
    transforms:
      - DecodeImage: # load image
          img_mode: BGR
          channel_first: False
      - CTCLabelEncode: # Class handling label
      - RecResizeImg:
          image_shape: [3, 32, 96]
      - KeepKeys:
          keep_keys: ['image', 'label', 'length'] # dataloader will return list in this order
  loader:
    shuffle: True
    batch_size_per_card: 256
    drop_last: True
    num_workers: 0
    use_shared_memory: False

Eval:
  dataset:
    name: SimpleDataSet
    data_dir: ./train_data/dcic_data
    label_file_list: ["./train_data/dcic_data/rec_gt_valid.txt"]
    transforms:
      - DecodeImage: # load image
          img_mode: BGR
          channel_first: False
      - CTCLabelEncode: # Class handling label
      - RecResizeImg:
          image_shape: [3, 32, 96]
      - KeepKeys:
          keep_keys: ['image', 'label', 'length'] # dataloader will return list in this order
  loader:
    shuffle: False
    drop_last: False
    batch_size_per_card: 256
    num_workers: 4
    use_shared_memory: False

训练评估与预测

  • 训练
代码语言:javascript复制
!python3 tools/train.py -c configs/rec/rec_dcic_train.yml 
   -o Global.pretrained_model=./pretrain_models/rec_mv3_none_bilstm_ctc_v2.0_train/best_accuracy
  • 评估
代码语言:javascript复制
!python tools/eval.py -c configs/rec/rec_dcic_train.yml -o Global.checkpoints=output/rec/dcic/best_accuracy
  • 预测
代码语言:javascript复制
# 预测全部测试集
!python tools/infer_rec.py -c configs/rec/rec_dcic_train.yml 
-o Global.checkpoints=./output/rec/dcic/best_accuracy 
Global.infer_img=../data/test_dataset

0 人点赞