要点:
前两章主要介绍了DBNet文字检测算法以及CRNN文字识别算法。然而对于我们实际场景中的一张图像,想要单独基于文字检测或者识别模型,是无法同时获取文字位置与文字内容的,因此,我们将文字检测算法以及文字识别算法进行串联,构建了PP-OCR文字检测与识别系统。在实际使用过程中,检测出的文字方向可能不是我们期望的方向,最终导致文字识别错误,因此我们在PP-OCR系统中也引入了方向分类器。
本章主要介绍PP-OCR文字检测与识别系统以及该系统中涉及到的优化策略。通过本节课的学习,您可以获得:
PP-OCR系统共经历了2次优化,下面对PP-OCR系统和这2次优化进行简单介绍。
PP-OCR中,对于一张图像,如果希望提取其中的文字信息,需要完成以下几个步骤:
上面便完成了对于一张图像的文本检测与识别过程。
文本检测基于后处理方案比较简单的DBNet,文字区域校正主要使用几何变换以及方向分类器,文本识别使用了基于融合了卷积特征与序列特征的CRNN模型,使用CTC loss解决预测结果与标签不一致的问题。
PP-OCR从骨干网络、学习率策略、数据增广、模型裁剪量化等方面,共使用了19个策略,对模型进行优化瘦身,最终打造了面向服务器端的PP-OCR server系统以及面向移动端的PP-OCR mobile系统。
相比于PP-OCR, PP-OCRv2 在骨干网络、数据增广、损失函数这三个方面进行进一步优化,解决端侧预测效率较差、背景复杂以及相似字符的误识等问题,同时引入了知识蒸馏训练策略,进一步提升模型精度。具体地:
从效果上看,主要有三个方面提升:
PP-OCR系统包括文本检测器、方向分类器以及文本识别器。本节针对这三个方向的模型优化策略进行详细介绍。
PP-OCR中的文本检测基于DBNet (Differentiable Binarization)模型,它基于分割方案,后处理简单。DBNet的具体模型结构如下图。
DBNet通过骨干网络(backbone)提取特征,使用DBFPN的结构(neck)对各阶段的特征进行融合,得到融合后的特征。融合后的特征经过卷积等操作(head)进行解码,生成概率图和阈值图,二者融合后计算得到一个近似的二值图。计算损失函数时,对这三个特征图均计算损失函数,这里把二值化的监督也也加入训练过程,从而让模型学习到更准确的边界。
DBNet中使用了6种优化策略用于提升模型精度与速度,包括骨干网络、特征金字塔网络、头部结构、学习率策略、模型裁剪等策略。在验证集上,不同模块的消融实验结论如下所示。
骨干网络的大小对文本检测器的模型大小有重要影响。因此,在构建超轻量检测模型时,应选择轻量的骨干网络。随着图像分类技术的发展,MobileNetV1、MobileNetV2、MobileNetV3和ShuffleNetV2系列常用作轻量骨干网络。每个系列都有不同的模型大小和性能表现。PaddeClas提供了20多种轻量级骨干网络。他们在ARM上的精度-速度
曲线如下图所示。
在预测时间相同的情况下,MobileNetV3系列可以实现更高的精度。作者在设计的时候为了覆盖尽可能多的场景,使用scale这个参数来调整特征图通道数,标准为1x,如果是0.5x,则表示该网络中部分特征图通道数为1x对应网络的0.5倍。为了进一步平衡准确率和效率,在V3的尺寸选择上,我们采用了MobileNetV3_large 0.5x的结构。
下面打印出DBNet中MobileNetV3各个阶段的特征图尺寸。
import os
import sys
# 下载代码
os.chdir("/home/aistudio/")
!git clone https://gitee.com/paddlepaddle/PaddleOCR.git
# 切换工作目录
os.chdir("/home/aistudio/PaddleOCR/")
!pip install -U pip
!pip install -r requirements.txt
# 具体代码实现位于:
# https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.4/ppocr/modeling/backbones/det_mobilenet_v3.py
import numpy as np
import paddle
# 设置随机输入
inputs = np.random.rand(1, 3, 640, 640).astype(np.float32)
x = paddle.to_tensor(inputs)
# 导入MobileNetV3库
from ppocr.modeling.backbones.det_mobilenet_v3 import MobileNetV3
# 模型定义
backbone_mv3 = MobileNetV3(scale=0.5, model_name='large')
# 模型forward
bk_out = backbone_mv3(x)
# 模型中间层打印
for i, stage_out in enumerate(bk_out):
print("the shape of ",i,'stage: ',stage_out.shape)
文本检测器的特征融合(neck)部分DBFPN与目标检测任务中的FPN结构类似,融合不同尺度的特征图,以提升不同尺度的文本区域检测效果。
为了方便合并不同通道的特征图,这里使用1×1
的卷积将特征图减少到相同数量的通道。
概率图和阈值图是由卷积融合的特征图生成的,卷积也与inner_channels相关联。因此,inner_channels对模型尺寸有很大的影响。当inner_channels由256减小到96时,模型尺寸由7M减小到4.1M,速度提升48%,但精度只是略有下降。
下面打印DBFPN的结构以及对于骨干网络特征图的融合结果。
# 具体代码实现位于:
# https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.4/ppocr/modeling/necks/db_fpn.py
from ppocr.modeling.necks.db_fpn import DBFPN
neck_bdfpn = DBFPN(in_channels=[16, 24, 56, 480], out_channels=96)
# 打印 DBFPN结构
print(neck_bdfpn)
# 先对原始的通道数降到96,再降到24,最后4个feature map进行concat
fpn_out = neck_bdfpn(bk_out)
print('the shape of output of DBFPN: ', fpn_out.shape)
SE是squeeze-and-excitation
的缩写(Hu, Shen, and Sun 2018)。如图所示
SE块显式地建模通道之间的相互依赖关系,并自适应地重新校准通道特征响应。在网络中使用SE块可以明显提高视觉任务的准确性,因此MobileNetV3的搜索空间包含了SE模块,最终MobileNetV3中也包含很多个SE模块。然而,当输入分辨率较大时,例如640×640
,使用SE模块较难估计通道的特征响应,精度提高有限,但SE模块的时间成本非常高。在DBNet中,我们将SE模块从骨干网络中移除,模型大小从4.1M
降到2.6M
,但精度没有影响。
PaddleOCR中可以通过设置disable_se=True
来移除骨干网络中的SE模块,使用方法如下所示。
# 具体代码实现位于:
# https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.4/ppocr/modeling/backbones/det_mobilenet_v3.py
x = paddle.rand([1, 3, 640, 640])
from ppocr.modeling.backbones.det_mobilenet_v3 import MobileNetV3
# 定义模型
backbone_mv3 = MobileNetV3(scale=0.5, model_name='large', disable_se=True)
# 模型forward
bk_out = backbone_mv3(x)
# 输出
for i, stage_out in enumerate(bk_out):
print("the shape of ",i,'stage: ',stage_out.shape)
梯度下降算法需要我们设置一个值,用来控制权重更新幅度,我们将其称之为学习率。它是控制模型学习速度的超参数。学习率越小,loss的变化越慢。虽然使用较低的学习速率可以确保不会错过任何局部极小值,但这也意味着模型收敛速度较慢。
因此,在训练前期,权重处于随机初始化状态,我们可以设置一个相对较大的学习速率以加快收敛速度。在训练后期,权重接近最优值,使用相对较小的学习率可以防止模型在收敛的过程中发生震荡。
Cosine学习率策略也就应运而生,Cosine学习率策略指的是学习率在训练的过程中,按照余弦的曲线变化。在整个训练过程中,Cosine学习率衰减策略使得在网络在训练初期保持了较大的学习速率,在后期学习率会逐渐衰减至0,其收敛速度相对较慢,但最终收敛精度较好。下图比较了两种不同的学习率衰减策略piecewise decay
和cosine decay
。
模型刚开始训练时,模型权重是随机初始化的,此时若选择一个较大的学习率,可能造成模型训练不稳定的问题,因此学习率预热的概念被提出,用于解决模型训练初期不收敛的问题。
学习率预热指的是将学习率从一个很小的值开始,逐步增加到初始较大的学习率。它可以保证模型在训练初期的稳定性。使用学习率预热策略有助于提高图像分类任务的准确性。在DBNet中,实验表明该策略也是有效的。学习率预热策略与Cosine学习率结合时,学习率的变化趋势如下代码演示。
# 具体代码实现位于
# https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.4/ppocr/optimizer/__init__.py
# 导入学习率优化器构建的函数
from ppocr.optimizer import build_lr_scheduler
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
# 咱们也可以看看warmup_epoch为2时的效果
lr_config = {'name': 'Cosine', 'learning_rate': 0.1, 'warmup_epoch': 2}
epochs = 20 # config['Global']['epoch_num']
iters_epoch = 100 # len(train_dataloader)
lr_scheduler=build_lr_scheduler(lr_config, epochs, iters_epoch)
iters = 0
lr = []
for epoch in range(epochs):
for _ in range(iters_epoch):
lr_scheduler.step() # 对应 https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.4/tools/program.py#L262
iters += 1
lr.append(lr_scheduler.get_lr())
x = np.arange(iters,dtype=np.int64)
y = np.array(lr,dtype=np.float64)
plt.figure(figsize=(15, 6))
plt.plot(x,y,color='red',label='lr')
plt.title(u'Cosine lr scheduler with Warmup')
plt.xlabel(u'iters')
plt.ylabel(u'lr')
plt.legend()
plt.show()
深度学习模型中一般有比较多的参数冗余,我们可以使用一些方法,去除模型中比较冗余的地方,从而提升模型推理效率。
模型裁剪指的是通过去除网络中冗余的通道(channel)、滤波器(filter)、神经元(neuron)等,来得到一个更轻量的网络,同时尽可能保证模型精度。
相比于裁剪通道或者特征图的方法,裁剪滤波器的方法可以得到更加规则的模型,因此减少内存消耗,加速模型推理过程。
之前的裁剪滤波器的方法大多基于范数进行裁剪,即,认为范数较小的滤波器重要程度较小,但是这种方法要求存在的滤波器的最小范数应该趋近于0,否则我们难以去除。
针对上面的问题,基于几何中心点的裁剪算法(Filter Pruning via Geometric Median, FPGM)被提出。FPGM将卷积层中的每个滤波器都作为欧几里德空间中的一个点,它引入了几何中位数这样一个概念,即与所有采样点距离之和最小的点。如果一个滤波器的接近这个几何中位数,那我们可以认为这个滤波器的信息和其他滤波器重合,可以去掉。
FPGM与基于范数的裁剪算法的对比如下图所示。
在PP-OCR中,我们使用FPGM对检测模型进行剪枝,最终DBNet的模型精度只有轻微下降,但是模型大小减小46%,预测速度加速19%。
关于FPGM模型裁剪实现的更多细节可以参考PaddleSlim。
注意:
下面给出DBNet的训练配置简要说明,完整的配置文件可以参考:ch_det_mv3_db_v2.0.yml。
Architecture: # 模型结构定义
model_type: det
algorithm: DB
Transform:
Backbone:
name: MobileNetV3 # 配置骨干网络
scale: 0.5
model_name: large
disable_se: True # 去除SE模块
Neck:
name: DBFPN # 配置DBFPN
out_channels: 96 # 配置 inner_channels
Head:
name: DBHead
k: 50
Optimizer:
name: Adam
beta1: 0.9
beta2: 0.999
lr:
name: Cosine # 配置cosine学习率下降策略
learning_rate: 0.001 # 初始学习率
warmup_epoch: 2 # 配置学习率预热策略
regularizer:
name: 'L2' # 配置L2正则
factor: 0 # 正则项的权重
上面给大家介绍了PP-OCR中文字检测算法的优化策略,这里再给大家回顾一下不同优化策略对应的消融实验与结论。
通过轻量级骨干网络、轻量级neck结构、SE模块的分析和去除、学习率调整及优化、模型裁剪等策略,DBNet的模型大小从7M减少至1.5M。通过学习率策略优化等训练策略优化,DBNet的模型精度提升超过1%。
PP-OCR中,超轻量DBNet检测效果如下所示:
!mkdir inference
!cd inference && wget https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_det_infer.tar -O ch_PP-OCRv2_det_infer.tar && tar -xf ch_PP-OCRv2_det_infer.tar
!python tools/infer/predict_det.py --image_dir="./doc/imgs/00111002.jpg" --det_model_dir="./inference/ch_PP-OCRv2_det_infer" --use_gpu=False
from PIL import Image
img_det = Image.open('./inference_results/det_res_00111002.jpg')
plt.figure(figsize=(14, 10)) # 图像窗口大小
plt.imshow(img_det)
plt.axis('on')
plt.title('Detection')
plt.show()
方向分类器的任务是用于分类出文本检测出的文本实例的方向,将文本旋转到0度之后,再送入后续的文本识别器中。PP-OCR中,我们考虑了0度和180度2个方向。下面详细介绍针对方向分类器的速度、精度优化策略。
与文本检测器相同,我们仍然采用MobileNetV3作为方向分类器的骨干网络。因为方向分类的任务相对简单,我们使用MobileNetV3 small 0.35x来平衡模型精度与预测效率。实验表明,即使当使用更大的骨干时,精度不会有进一步的提升。
数据增强指的是对图像变换,送入网络进行训练,它可以提升网络的泛化性能。常用的数据增强包括旋转、透视失真变换、运动模糊变换和高斯噪声变换等,PP-OCR中,我们统称这些数据增强方法为BDA(Base Data Augmentation)。结果表明,BDA可以明显提升方向分类器的精度。
除了BDA外,我们还加入了一些更高阶的数据增强操作来提高分类的效果,例如 AutoAugment (Cubuk et al. 2019), RandAugment (Cubuk et al. 2020), CutOut (DeVries and Taylor 2017), RandErasing (Zhong et al. 2020), HideAndSeek (Singh and Lee 2017), GridMask (Chen 2020), Mixup (Zhang et al. 2017) 和 Cutmix (Yun et al. 2019)。
这些数据增广大体分为3个类别:
(1)图像变换类:AutoAugment、RandAugment
(2)图像裁剪类:CutOut、RandErasing、HideAndSeek、GridMask
(3)图像混叠类:Mixup、Cutmix
下面给出不同高阶数据增广的可视化对比结果。
# 参考代码:
# https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.4/ppocr/data/imaug/__init__.py
import random
from PIL import Image
from ppocr.data.imaug import DecodeImage, RandAugment, transform
np.random.seed(1)
random.seed(1)
img = Image.open('./doc/imgs_words/ch/word_4.jpg')
# 绘制原图
plt.figure("Image1") # 图像窗口名称
plt.imshow(img)
plt.axis('on') # 关掉坐标轴为 off
plt.title('Before RandAugment') # 图像题目
plt.show()
data = {'image':None}
with open('./doc/imgs_words/ch/word_4.jpg', 'rb') as f:
img = f.read()
data['image'] = img
# 定义变换算子
ops_list = [DecodeImage(), RandAugment()]
# 数据变换
data = transform(data,ops_list)
img_auged = data['image']
# 显示
img_auged = Image.fromarray(img_auged, 'RGB')
plt.figure("Image") # 图像窗口名称
plt.imshow(img_auged)
plt.axis('on') # 关掉坐标轴为 off
plt.title('After RandAugment') # 图像标题
plt.show()
下面展示快速使用方向分类器模型的预测效果。具体的预测推理代码,我们在第五章会进行详细说明。
# 参考代码:
# https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.4/tools/infer/predict_cls.py
!cd inference && wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar -O ch_ppocr_mobile_v2.0_cls_infer.tar && tar -xf ch_ppocr_mobile_v2.0_cls_infer.tar
# 方向分类器分类
!python tools/infer/predict_cls.py --image_dir="./doc/imgs_words/ch/word_1.jpg" --cls_model_dir="./inference/ch_ppocr_mobile_v2.0_cls_infer" --use_gpu=False
# 读入图像
import cv2
img = cv2.imread("./doc/imgs_words/ch/word_1.jpg")
plt.imshow(img[:,:,::-1])
plt.show()
# 旋转180度
img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
cv2.imwrite("./test.png", img)
# 对旋转后图像使用方向分类器进行分类
!python tools/infer/predict_cls.py --image_dir="./test.png" --cls_model_dir="./inference/ch_ppocr_mobile_v2.0_cls_infer" --use_gpu=False
plt.imshow(img[:,:,::-1])
plt.show()
PP-OCR中,文本识别器使用的是CRNN模型。训练的时候使用CTC loss去解决不定长文本的预测问题。
PP-OCR针对文本识别器,从骨干网络、头部结构优化、数据增强、正则化策略、特征图下采样策略、量化等多个角度进行模型优化,
数据增广是提升模型泛化能力重要的手段之一,CopyPaste 是一种新颖的数据增强技巧,已经在目标检测和实例分割任务中验证了有效性。利用 CopyPaste,可以合成文本实例来平衡训练图像中的正负样本之间的比例。相比而言,传统图像旋转、随机翻转和随机裁剪是无法做到的。
CopyPaste 主要步骤包括:
这样就比较好地提升了样本丰富度,同时也增加了模型对环境的鲁棒性。如下图所示,通过在左下角的图中裁剪出来的文本,随机旋转缩放之后粘贴到左上角的图像中,进一步丰富了该文本在不同背景下的多样性。