2021SC@SDUSC
为增强模型能力和缩小模型尺寸,PP-OCR采用了轻主干网、数据增强、输入分辨率和PACT量化等四种策略。其中轻主干网通过选择在预测时间相同时可以达到更高精度的MobileNetV3来实现。由于方向分类这个任务相对简单,PP-OCR使用MobileNetV3_small_x0.35来平衡准确性和效率,V3-small就是针对低资源情况下的使用。
MobileNetV3的网络结构可以分为三个部分:
- 起始部分:1个卷积层,通过3x3的卷积,提取特征
- 中间部分:多个卷积层,不同Large和Small版本,层数和参数不同
- 最后部分:通过两个1x1的卷积层,代替全连接,输出类别
1.起始部分,也就是结构列表中的第1个卷积层,其中包括3个部分,即卷积层、BN层、hard-swish激活层。
2.中间部分是多个含有卷积层的块(MobileBlock)的网络结构,包括SE(Squeeze-and-Excite结构,压缩和激发)、NL(Non-Linearity,非线性)、HS和RE(h-swish激活函数和ReLU激活函数)、bneck(bottleneck layers,瓶颈层)、exp size(expansion factor,膨胀参数)。每一行都是一个MobileBlock,即bneck。
3.最后部分(Last Stage),通过将Avg Pooling提前,减少计算量,将Squeeze操作省略,直接使用1x1的卷积。
MobileNetV3的相关特点有:
- 网络的架构为基于NAS实现的MnasNet
- 引入MobileNetV1的深度可分离卷积
- 引入MobileNetV2的具有线性瓶颈的倒残差结构
- 引入基于squeeze and excitation结构的轻量级注意力模型(SE)
- 使用了一种新的激活函数hard-swish(x)
- 网络结构搜索中结合两种技术:资源受限的NAS(platform-aware NAS)与NetAdapt
- 修改了MobileNetV2网络端部最后阶段
下图为MobileNetV3的网络结构图,large和small的整体结构一致,区别就是基本单元bneck的个数以及内部参数上,主要是通道数目。
1.在相同的准确度下,MnasNet 模型的运行速度比手工设计的最先进的MobileNetV2模型快 1.5 倍,并且比NASNet快 2.4 倍,而 NASNet 也是使用架构搜索的方法。所以MobileNetV3基于MnasNet架构可以获得更快的速度。而且采用NAS工具可以为不同的加速硬件平台挖掘具有高性能的网络架构。由于系统使用的是MobileNetV3,所以我就不再这深入探讨具体的MnasNet原理了。
2.MobileNetV1中实现了VGG中的标准卷积层换成深度可分离卷积。深度可分离卷积就是将普通卷积拆分成为一个深度卷积和一个逐点卷积。深度可分离卷积能让系统用更少的参数,做更少的运算,来达到和标准卷积差不多的结果。二者过程对比如下图所示(图源知乎R.JD)。标准卷积的卷积核的尺寸是Dk×Dk×M,
一共有N个,所以标准卷积的参数量是:Dk×Dk×M×N。每一个卷积核都要进行Dw×Dh次运算,所以标准卷积的计算量是:Dk×Dk×M×N×Dw×Dh。深度可分离卷积的参数量由深度卷积和逐点卷积两部分组成。深度卷积的卷积核尺寸Dk×Dk×M;逐点卷积的卷积核尺寸为1×1×M,一共有N个,所以深度可分离卷积的参数量是:Dk×Dk×M+M×N。深度可分离卷积的计算量也是由深度卷积和逐点卷积两部分组成。深度卷积的卷积核尺寸Dk×Dk×M,一共要做Dw×Dh次乘加运算;逐点卷积的卷积核尺寸为1×1×M,有N个,一共要做Dw×Dh次乘加运算,所以深度可分离卷积的计算量是:Dk×Dk×M×Dw×Dh+M×N×Dw×Dh。假如按照系统通常所使用的是3×3的卷积核,那么参数数量和乘加操作的运算量均可以下降为原来的九分之一到八分之一。在MobileNetV3中引入V1的深度可分离卷积思想可以大大降低模型的运算量,实现更高速的运算。所以MobileNetV3是目前运算速度较快的模型。代码实现如下:
class ConvBNLayer(nn.Layer):
def __init__(self,
num_channels,
filter_size,
num_filters,
stride,
padding,
channels=None,
num_groups=1,
act='hard_swish'):
super(ConvBNLayer, self).__init__()
self._conv = Conv2D(
in_channels=num_channels,
out_channels=num_filters,
kernel_size=filter_size,
stride=stride,
padding=padding,
groups=num_groups,
weight_attr=ParamAttr(initializer=KaimingNormal()),
bias_attr=False)
self._batch_norm = BatchNorm(
num_filters,
act=act,
param_attr=ParamAttr(regularizer=L2Decay(0.0)),
bias_attr=ParamAttr(regularizer=L2Decay(0.0)))
def forward(self, inputs):
y = self._conv(inputs)
y = self._batch_norm(y)
return y
class DepthwiseSeparable(nn.Layer):
def __init__(self,
num_channels,
num_filters1,
num_filters2,
num_groups,
stride,
scale,
dw_size=3,
padding=1,
use_se=False):
super(DepthwiseSeparable, self).__init__()
self.use_se = use_se
self._depthwise_conv = ConvBNLayer(
num_channels=num_channels,
num_filters=int(num_filters1 * scale),
filter_size=dw_size,
stride=stride,
padding=padding,
num_groups=int(num_groups * scale))
if use_se:
self._se = SEModule(int(num_filters1 * scale))
self._pointwise_conv = ConvBNLayer(
num_channels=int(num_filters1 * scale),
filter_size=1,
num_filters=int(num_filters2 * scale),
stride=1,
padding=0)
def forward(self, inputs):
y = self._depthwise_conv(inputs)
if self.use_se:
y = self._se(y)
y = self._pointwise_conv(y)
return y
class MobileNetV1Enhance(nn.Layer):
def __init__(self, in_channels=3, scale=0.5, **kwargs):
super().__init__()
self.scale = scale
self.block_list = []
self.conv1 = ConvBNLayer(
num_channels=3,
filter_size=3,
channels=3,
num_filters=int(32 * scale),
stride=2,
padding=1)
conv2_1 = DepthwiseSeparable(
num_channels=int(32 * scale),
num_filters1=32,
num_filters2=64,
num_groups=32,
stride=1,
scale=scale)
self.block_list.append(conv2_1)
conv2_2 = DepthwiseSeparable(
num_channels=int(64 * scale),
num_filters1=64,
num_filters2=128,
num_groups=64,
stride=1,
scale=scale)
self.block_list.append(conv2_2)
conv3_1 = DepthwiseSeparable(
num_channels=int(128 * scale),
num_filters1=128,
num_filters2=128,
num_groups=128,
stride=1,
scale=scale)
self.block_list.append(conv3_1)
conv3_2 = DepthwiseSeparable(
num_channels=int(128 * scale),
num_filters1=128,
num_filters2=256,
num_groups=128,
stride=(2, 1),
scale=scale)
self.block_list.append(conv3_2)
conv4_1 = DepthwiseSeparable(
num_channels=int(256 * scale),
num_filters1=256,
num_filters2=256,
num_groups=256,
stride=1,
scale=scale)
self.block_list.append(conv4_1)
conv4_2 = DepthwiseSeparable(
num_channels=int(256 * scale),
num_filters1=256,
num_filters2=512,
num_groups=256,
stride=(2, 1),
scale=scale)
self.block_list.append(conv4_2)
for _ in range(5):
conv5 = DepthwiseSeparable(
num_channels=int(512 * scale),
num_filters1=512,
num_filters2=512,
num_groups=512,
stride=1,
dw_size=5,
padding=2,
scale=scale,
use_se=False)
self.block_list.append(conv5)
conv5_6 = DepthwiseSeparable(
num_channels=int(512 * scale),
num_filters1=512,
num_filters2=1024,
num_groups=512,
stride=(2, 1),
dw_size=5,
padding=2,
scale=scale,
use_se=True)
self.block_list.append(conv5_6)
conv6 = DepthwiseSeparable(
num_channels=int(1024 * scale),
num_filters1=1024,
num_filters2=1024,
num_groups=1024,
stride=1,
dw_size=5,
padding=2,
use_se=True,
scale=scale)
self.block_list.append(conv6)
self.block_list = nn.Sequential(*self.block_list)
self.pool = nn.MaxPool2D(kernel_size=2, stride=2, padding=0)
self.out_channels = int(1024 * scale)
def forward(self, inputs):
y = self.conv1(inputs)
y = self.block_list(y)
y = self.pool(y)
return y
3.在MobileNetV2中发现,ReLU激活函数会导致信息损耗,所以深度卷积的卷积核有不少是空。将ReLU替换成线性激活函数就可以解决这个问题。这一部分被称为linear bottleneck,即线性瓶颈。MobileNetV2和ResNet都使用了Shortcut结构,但是ResNet 先降维 (0.25倍)、卷积、再升维,MobileNetV2 则是 先升维 (6倍)、卷积、再降维。V2的block刚好与Resnet的block相反,这称为Inverted residuals,即反向残差。Shortcut的代码如下:
#Shortcut代码段
class ShortCut(nn.Layer):
def __init__(self, in_channels, out_channels, stride, name, is_first=False):
super(ShortCut, self).__init__()
self.use_conv = True
if in_channels != out_channels or stride != 1 or is_first == True:
if stride == (1, 1):
self.conv = ConvBNLayer(in_channels, out_channels, 1, 1, name=name)
else: # stride==(2,2)
self.conv = ConvBNLayer(in_channels, out_channels, 1, stride, name=name)
else:
self.use_conv = False
def forward(self, x):
if self.use_conv:
x = self.conv(x)
return x
4.SENet的思想在于通过Feature Map为自身学习一个特征权值,通过单位乘的方式得到一组加权后的新的特征权值。SENet由一些列SE block组成,一个SE block的过程分为Squeeze(压缩)和Excitation(激发)两个步骤。引入SE模块,主要为了利用结合特征通道的关系来加强网络的学习能力。SE的代码如下:
#SE模块
class SEModule(nn.Layer):
def __init__(self, channel, reduction=4):
super(SEModule, self).__init__()
self.avg_pool = AdaptiveAvgPool2D(1)
self.conv1 = Conv2D(
in_channels=channel,
out_channels=channel // reduction,
kernel_size=1,
stride=1,
padding=0,
weight_attr=ParamAttr(),
bias_attr=ParamAttr())
self.conv2 = Conv2D(
in_channels=channel // reduction,
out_channels=channel,
kernel_size=1,
stride=1,
padding=0,
weight_attr=ParamAttr(),
bias_attr=ParamAttr())
def forward(self, inputs):
outputs = self.avg_pool(inputs)
outputs = self.conv1(outputs)
outputs = F.relu(outputs)
outputs = self.conv2(outputs)
outputs = hardsigmoid(outputs)
return paddle.multiply(x=inputs, y=outputs)
5.hard-swish是基于swish的改进。swish具备无上界有下界、平滑、非单调的特性。并且Swish在深层模型上的效果优于ReLU。仅仅使用Swish单元替换ReLU就能把MobileNet,NASNetA在 ImageNet上的top-1分类准确率提高0.9%,Inception-ResNet-v的分类准确率提高0.6%。这种非线性激活函数虽然提高了精度,但在嵌入式环境中会造成不少成本。设计者提出了基于ReLU6的hard-swish,hard-swish节省了时间和计算量。但是只有在更深层次使用h-swish才能得到比较大的好处,所以在上面的网络模型中,设计者只在模型的后半部分使用hard-swish。hard-swish代码如下:
class hswish(nn.Module):
def forward(self, x):
out = x * F.relu6(x + 3, inplace=True) / 6
return out
6.资源受限的NAS(platform-aware NAS)用于在计算和参数量受限的前提下搜索网络来优化各个块(block),所以称之为模块级搜索(Block-wise Search)。NetAdapt用于对各个模块确定之后网络层的微调每一层的卷积核数量,所以称之为层级搜索(Layer-wise Search)。一旦通过体系结构搜索找到模型,我们就会发现最后一些层以及一些早期层计算代价比较高昂。于是设计者决定对这些架构进行一些修改,以减少这些慢层(slow layers)的延迟,同时保持准确性。这些修改显然超出了当前搜索的范围。
7.使用1×1卷积来构建最后层可以便于拓展到更高维的特征空间。这样做的好处是,在预测时有更多更丰富的特征来满足预测,但是同时也引入了额外的计算成本与延时。所以,需要改进的地方是要保留高维特征的前提下减小延时。首先,还是将1×1层放在到最终平均池之后。这样的话最后一组特征现在不是7x7,而是以1x1计算。这样的好处是,在计算和延迟方面,特征的计算几乎是免费的。在不会造成精度损失的同时,减少10ms耗时,提速15%,减小了30m的MAdd操作。
以上就是方向分类器的轻型主干MobileNetV3的原理探究及PaddleOCR中代码展示。下图为V3的block结构。代码位置PaddleOCR-release-2.2>ppocr>modeling>backbones>det开头的文件。