2021SC@SDUSC
目录
一、前情回顾:文字识别器模型的策略介绍
1.文字识别器的总体策略选用
回顾
策略的选用主要是用来增强模型能力和减少模型大小。下面是PP-OCR采用的九种策略回顾。
- 轻主干,选用采用 MobileNetV3 large x0.5 来权衡精度和效率;
- 数据增强,BDA (Base Dataaugmented)和TIA (Luo et al. 2020);
- 余弦学习率衰减,有效提高模型的文本识别能力;
- 特征图辨析,适应多语言识别,进行向下采样 feature map的步幅修改;
- 正则化参数,权值衰减避免过拟合;
- 学习率预热,同样有效;
- 轻头部,采用全连接层将序列特征编码为预测字符,减小模型大小;
- 预训练模型,是在 ImageNet 这样的大数据集上训练的,可以达到更快的收敛和更好的精度;
- PACT量化,略过 LSTM 层;
简单介绍
为了更加明确了解PP-OCR的策略选择,下面简单了解一下策略选择的一些基础。
PP-OCR所选用文字识别的Benchmark评价方式(该方式适配强、效率高)如下:
在上图四个模块,共24种算法组合中,Paddle OCR根据实验评估,选择4种效率最高的算法:
根据不同算法的实验训练与评估结果选择骨干网络为mobilenet_v3和resnet。
2.本人负责的文字识别器模块策略
- 轻主干,选用采用 MobileNetV3 large x0.5 来权衡精度和效率;
- 数据增强,BDA (Base Dataaugmented)和TIA (Luo et al. 2020);
- 余弦学习率衰减,有效提高模型的文本识别能力;
- 特征图辨析,适应多语言识别,进行向下采样 feature map的步幅修改
- 轻头部,采用全连接层将序列特征编码为预测字符,减小模型大小;
3.本文所介绍的文字识别器模块策略
- 轻主干,选用采用 MobileNetV3 large x0.5 来权衡精度和效率;
- 轻头部,采用全连接层将序列特征编码为预测字符,减小模型大小;
二、Paddle OCR组网介绍
因为本文要介绍文字识别器的轻主干和轻头部策略以及相关关键代码选择,这里用一部分篇幅来介绍一下Paddle OCR的组网。
1. 组网包目录介绍
ppocr源码的modeling包结构如下图所示:
首先是architectures包,构造model相关代码,是总体的基础架构BaseModel。
紧接着是backbones包,构造backbones相关代码。该包是骨干网络代码,主要是PPOCR所选用的骨干网络mobilenet_v3和resnet。包内包含检测mobilenet_v3骨干网络和resnet骨干网络、SAST算法的resnet骨干网络、以及识别mobilenet_v3骨干网络和resnet骨干网络。
然后是heads包,构造head相关代码。该包是头函数代码,主要是在保证性能同时减小模型大小所采用的头函数。包含分类头、检测的EAST头、检测的SAST头、检测的DB头、识别的attention头、ctc头以及srn头。
接下来是necks包,构造neck相关代码,主要是组网所选用的颈函数。包含EAST算法的FPN、SAST算法的FPN、DB算法的FPN、以及文字识别模型中的识别序列编码。
最后是transforms包,构造transform相关代码,主要是图像变换相关代码。包含TPS变换。
2. 组网包中本次分析的关键部分
一是backbones包中识别mobilenet_v3骨干网络和resnet骨干网络。主要是文件rec_mobilenet_v3.py、rec_resnet_fpn.py和rec_resnet_vd.py
二是heads包中识别的attention头、ctc头以及srn头。主要是文件rec_att_head.py、rec_ctc_head.py和rec_srn_head.py
三是necks包中的识别序列编码,主要是文件 rnn.py
三、主要策略与代码解释
1.轻主干策略
根据几种主干在Snapdragon 855 (SD855)上测试推理时间(如下图所示),PP-OCR采用了mobilenet_v3和resnet骨干网络来保证性能和减小模型大小。
关键代码:
1.文件rec_mobilenet_v3.py
MobileNetV3类
class MobileNetV3(nn.Layer):
def __init__()
# 步长设置
if small_stride is None:small_stride = [2, 2, 2, 2]
if large_stride is None:large_stride = [1, 2, 2, 2]
# length和type
assert isinstance(large_stride, list), "large_stride type must " \
"be list but got {}".format(type(large_stride))
assert isinstance(small_stride, list), "small_stride type must " \
"be list but got {}".format(type(small_stride))
assert len(large_stride) == 4, "large_stride length must be " \
"4 but got {}".format(len(large_stride))
assert len(small_stride) == 4, "small_stride length must be " \
"4 but got {}".format(len(small_stride))
# 根据不同步长进行文字检测和方向分类处理
if model_name == "large":
cfg = [
# k, exp, c, se, nl, s,
# ……
]
cls_ch_squeeze = 960
elif model_name == "small":
cfg = [
# k, exp, c, se, nl, s,
# ……
]
cls_ch_squeeze = 576
else:
raise NotImplementedError(……)
# supported_scale
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)
#两次处理识别变换:conv1和conv2
self.conv1 = ConvBNLayer(……)
# ……
self.conv2 = ConvBNLayer(……)
#相关数据输出
def forward(self, x):
x = self.conv1(x)
x = self.blocks(x)
x = self.conv2(x)
x = self.pool(x)
return x
ResNetFPN类()
class ResNetFPN(nn.Layer):
def __init__(self, in_channels=1, layers=50, **kwargs):
super(ResNetFPN, self).__init__()
#层设置
supported_layers = {
}
stride_list = [(2, 2), (2, 2), (1, 1), (1, 1)]
num_filters = [64, 128, 256, 512]
self.depth = supported_layers[layers]['depth']
self.F = []
#conv操作
self.conv = ConvBNLayer()
self.block_list = []
in_ch = 64
#根据层进行图像设置self.block_list和 self.F
if layers >= 50:
else:
out_ch_list = [in_ch // 4, in_ch // 2, in_ch]
self.base_block = []
self.conv_trans = []
self.bn_block = []
#对self.base_block设置
for i in [-2, -3]:
self.base_block.append(
self.add_sublayer(
"F_{}_base_block_3".format(i),
nn.Conv2D(
in_channels=out_ch_list[i],
out_channels=512,
kernel_size=1,
bias_attr=ParamAttr(trainable=True),
weight_attr=ParamAttr(trainable=True))))
self.out_channels = 512
#设置base
def __call__(self, x):
#ConvBNLayer类
class ConvBNLayer(nn.Layer):
def __init__():
super(ConvBNLayer, self).__init__()
self.conv = nn.Conv2D()
#设置name
if name == "conv1":
bn_name = "bn_" + name
else:
bn_name = "bn" + name[3:]
#设置BatchNorm
self.bn = nn.BatchNorm(
)
def __call__(self, x):
x = self.conv(x)
x = self.bn(x)
return x
#ShortCut类
class ShortCut(nn.Layer):
#设置self.use_conv
def __init__():
def forward(self, x):
if self.use_conv:
x = self.conv(x)
return x
#BottleneckBlock
class BottleneckBlock(nn.Layer):
def __init__(s):
super(BottleneckBlock, self).__init__()
self.conv0 = ConvBNLayer(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=1,
act='relu',
name=name + "_branch2a")
self.conv1 = ConvBNLayer(
in_channels=out_channels,
out_channels=out_channels,
kernel_size=3,
stride=stride,
act='relu',
name=name + "_branch2b")
self.conv2 = ConvBNLayer(
in_channels=out_channels,
out_channels=out_channels * 4,
kernel_size=1,
act=None,
name=name + "_branch2c")
self.short = ShortCut(
in_channels=in_channels,
out_channels=out_channels * 4,
stride=stride,
is_first=False,
name=name + "_branch1")
self.out_channels = out_channels * 4
def forward(self, x):
y = self.conv0(x)
y = self.conv1(y)
y = self.conv2(y)
y = y + self.short(x)
y = F.relu(y)
return y
#BasicBlock类
class BasicBlock(nn.Layer):
def __init__():
super(BasicBlock, self).__init__()
self.conv0 = ConvBNLayer(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=3,
act='relu',
stride=stride,
name=name + "_branch2a")
self.conv1 = ConvBNLayer(
in_channels=out_channels,
out_channels=out_channels,
kernel_size=3,
act=None,
name=name + "_branch2b")
self.short = ShortCut(
in_channels=in_channels,
out_channels=out_channels,
stride=stride,
is_first=is_first,
name=name + "_branch1")
self.out_channels = out_channels
def forward(self, x):
y = self.conv0(x)
y = self.conv1(y)
y = y + self.short(x)
return F.relu(y)
总结
因为涉及原理基础介绍部分较多,本次仅介绍了文字识别整体策略回顾和文字识别的轻骨干策略及部分代码的介绍。之后将继续发文介绍轻头部策略等。