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

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


1 文本识别


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


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


15.png


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


算法类别 主要思路 主要论文
传统算法 滑动窗口、字符提取、动态规划 -
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选择的中文识别算法。


16.png


2.2 算法详解


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

1)backbone:


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


17.png


MobileNetV3为例

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作为递归层,解决了长序列训练过程中的梯度消失和梯度爆炸问题。


18.png

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 字符插入在重复字符之间。


19.png

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

[图片上传失败...(image-4da6cb-1642969983804)]


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作为验证集

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,为了方面大家理解,我这里加了一些核心注释:

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


训练评估与预测

  • 训练

!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


  • 评估

!python tools/eval.py -c configs/rec/rec_dcic_train.yml -o Global.checkpoints=output/rec/dcic/best_accuracy


  • 预测

# 预测全部测试集
!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


20.png


相关文章
|
5月前
|
人工智能 自然语言处理 Cloud Native
《百炼成金-大金融模型新篇章》––01.大模型是DT时代标志性产物
百炼必定成金,新质生产力会催生新质劳动力,谨以此文抛砖引玉,希望与业内的各位朋友一同探讨如何积极拥抱并运用大模型技术,以应对和驾驭不断变化的市场环境,实现科技金融持续稳定的提质增效和创新发展,携手开启金融大模型未来新篇章。
|
4月前
|
人工智能 自然语言处理 自动驾驶
AI大模型的战场:通用与垂直的较量
AI大模型的战场:通用与垂直的较量
214 0
|
6月前
|
人工智能 自动驾驶 安全
破壁人AI百度:科技公司反内卷的典型样本
互联网整个行业都在陷入被动且尴尬的局面。去年开始流行的“内卷”一词,恰如其分的描述了互联网的现状,比如抖音开始做外卖,微信强推视频号,一直硝烟弥漫的电商市场,更是激战在社区团购上。 内卷背后也有人感慨,互联网到了尽头。支撑这一论述的是,移动互联网的人口红利已经消失,几款国民型APP用户增长都固定在了10亿这个级别,只能依靠自然人口的增长和迁移。
52 0
|
人工智能 自然语言处理 安全
在AIGC浪潮之下,人脸生成、保ID方向的应用和发展方向
随着人工智能技术的不断发展和应用,人脸生成和身份保护方向成为了人工智能技术发展的重要方向之一。在这个领域,阿里云的智能开放平台提供了强大的技术支持和应用场景,为人脸生成和身份保护的应用和发展提供了新的可能性。接下来将结合阿里云的智能开放平台来谈AIGC浪潮下人脸生成、保ID方向的应用和发展方向。
409 1
在AIGC浪潮之下,人脸生成、保ID方向的应用和发展方向
|
数据采集 机器学习/深度学习 人工智能
通用VS垂直,讯飞星火与网易子曰不同的“大模型解法”
随着大模型商业化应用的提速,全世界各国都开始孵化和孕育各自的行业大模型。
123 0
|
机器学习/深度学习 存储 人工智能
上海数字大脑研究院发布国内首个多模态决策大模型DB1,可实现超复杂问题快速决策
上海数字大脑研究院发布国内首个多模态决策大模型DB1,可实现超复杂问题快速决策
203 0
|
机器学习/深度学习 人工智能 自然语言处理
上海数字大脑研究院首次发布《2022上半年度人工智能行业报告》,多层面深度分析全球AI发展
上海数字大脑研究院首次发布《2022上半年度人工智能行业报告》,多层面深度分析全球AI发展
239 0
|
机器学习/深度学习 人工智能 监控
AI在智慧城市的十种应用方式
智慧城市对气候变化、更明智的决策和提高生活质量的影响较小,下面,我们来看看人工智能帮助实现这一目标的十种方式。
AI在智慧城市的十种应用方式
|
机器学习/深度学习 人工智能 算法
CV之Face Change:基于人工智能实现国内众多一线美女明星换脸(基于Face++输出4*106个特征点定位+融合代码、deepfake技术)
CV之Face Change:基于人工智能实现国内众多一线美女明星换脸(基于Face++输出4*106个特征点定位+融合代码、deepfake技术)
CV之Face Change:基于人工智能实现国内众多一线美女明星换脸(基于Face++输出4*106个特征点定位+融合代码、deepfake技术)
|
人工智能 Android开发
荣耀手机四周年大猜想,AI战略与海外布局或成主体
几年间,互联网手机变天。来自赛诺的数据显示,2017年1-9月,荣耀超越小米,成为互联网手机出货量、销售额双料第一。至此,荣耀将曾经以12.5%份额在国内市场领头的小米斩落马下,互联网手机已经告别了三年前诸侯混战的大乱局,国内大势已成。
173 0
荣耀手机四周年大猜想,AI战略与海外布局或成主体