论文 PP-PicoDet: A Better Real-Time Object Detector on Mobile Devices
paddleDetection代码:picodet
PP-PicoDet 是百度提出的移动端友好和高精度Anchor-Free 目标检测算法,实测性能非常优越。关于模型的配置与训练的解读可以参考https://www.guyuehome.com/36666。
本文将详解 picodet 的 backbone——Enhanced ShuffleNet (ESNet)。
注:paddleDetection 已更新至2.4 版本,picodet增强版 将 backbone 从 ESnet 换成了 LCNet。本文仍解读初始的backbone ESnet。LCNet 将在新的文章中解读。
ESNet
ESNet,即 Enhanced ShuffleNet 是在 ShuffleleNetV2 的基础上改进的一个移动端友好的轻量级网络。下图详细描述了ESNet的ES模块。
图中 (a), (b)描述了 ESnet 的两个基本模块,对应代码中的 InvertedResidualDS
类和 InvertedResidual
类,整个 ESnet 主要就是由这两个模块堆叠而成的。从图中我们可以看到,这两个基本模块由更基础的深度可分离卷积(pw conv,dw conv)、Ghost block、SE block 和 channel shuffle 模块组成。下面将对各基础模块的原理进行解读。
1. 深度可分离卷积
参考:卷积神经网络中的Separable Convolution
Depthwise Separable Convolution 是谷歌2017 年在 Xception 中提出的, 用于减少网络参数,提升计算效率。它的核心思想是将一个完整的卷积运算分解为两步进行,分别为 Depthwise Convolution 与 Pointwise Convolution。
常规的卷积操作
假设输入为一个 64 \times 64\times 3 的图片,经过 4 个的3 \times 3 \times 3 卷积核,输出 4 个 Feature Map,整个计算过程可以用下图来概括:
此时,卷积层的参数量为 3 \times 3 \times 3 \times 4 = 108.
Depthwise 卷积
Depthwise 卷积就是逐个通道分开卷积。常规卷积卷积核需要与输入的每个通道都进行卷积操作,而 Depthwise 卷积的一个卷积核只与一个通道做卷积。如下图所示:
此时,卷积层的参数量为 3 \times 3 \times 3 = 27.
Depthwise 卷积没有有效利用输入层不同通道在相同空间位置上的特征信息。因此需要增加一步操作来融合不同通道上相同位置的特征信息,即 Pointwise 卷积。
Pointwise 卷积
Poinwise 卷积与常规卷积运算相同,只不过卷积核尺寸为1 \times 1 \times M,M为输入层的通道数。
此例中输入通道数为 3,输出通道为 4 ,因此 Pointwise 卷积层参数量为1 \times 1 \times 3 \times 4 = 12.
可以看到深度可分离卷积的参数量只有常规卷积的参数量的 1/3.
pytorch 实现深度可分离卷积
可以使用 torch.nn.Conv2d() 中的卷积组参数 groups,来实现深度可分离卷积。groups
参数是用于控制输入和输出的连接的,表示要分的组数(in_channels 和 out_channels 都必须能被 groups 参数整除)。例如:
- 当 groups =1 (默认值)时,就是同普通的卷积。
- 当 groups=n 时,相当于把原来的卷积分成 n 组,每组 in_channels/n 的输入与 out_channels/n 个 kernel_size x kernel_size x in_channels/n的卷积核卷积,生成 out_channels/n 的输出 ,然后将各组输出连接起来,形成完整的 out_channels 的输出。
当 groups = in_channels 时,每个输入通道都只跟 out_channels/in_channels 个卷积核卷积。 out_channels = in_channels 时就是 Depthwise 卷积。
完整的深度可分离卷积代码如下
import torch.nn as nn
class depthwise_separable_conv(nn.Module):
def __init__(self, in_channels, out_channels):
super(depthwise_separable_conv, self).__init__()
self.depthwise_conv = nn.Conv2d(in_channels, in_channels, kernel_size=3, padding=1, groups=in_channels)
self.pointwise_conv = nn.Conv2d(in_channels, out_channels, kernel_size=1, padding=0)
def forward(self, input):
out = self.depthwise_conv(input)
out = self.pointwise_conv(out)
return out
2. channel shuffle 通道洗牌
参考:
channel shuffle 通道洗牌
https://zhuanlan.zhihu.com/p/51566209
假设一个深度可分离卷积由一个 3 \times 3 的 Depthwise 卷积核一个 1 \times 1 的 Pointwise 卷积组成。输入尺寸为c_1 \times h \times w,输出尺寸为 c_2 \times h \times w, 则 FLOPS 为:
3 \times 3 \times c_1 \times h \times w + c_2 \times c_1 \times h \times w
一般情况下 c_2 是远大于 3 \times 3 的,所以深度可分离卷积的计算性能主要取决于 Pointwise 卷积。
模型中大量的对于整个 Feature Maps 的 Pointwise 卷积成了限制模型计算性能的瓶颈。
分组 Pointwise 卷积可以有效提高计算效率,但是不利于通道组之间的信息流通,会削弱模型的表征能力。为了克服分组卷积带来的副作用,ShuffleNet v1中提出了 channel shuffle 操作。具体流程如下:
1.将输入Feature Maps 展开成 g \times n \times h \times w 的四维矩阵(即,将c_1 通道的输入划分为 g 组,每组n个通道)。
2.沿着 g \times n \times h \times w 矩阵的g轴和n轴进行转置。
3.将转置后的矩阵平铺,得到洗牌之后的 Feature Maps。
4.进行组内 Pointwize 卷积。
来自小松鼠的图解:
pytorch 代码实现
import torch
def channel_shuffle(x, groups):
batchsize, num_channels, height, width = x.data.size()
channels_per_group = num_channels // groups
# 通道分组. b, num_channels, h, w -> b, groups, channels_per_group, h, w
x = x.view(batchsize, groups, channels_per_group, height, width)
# channel shuffle
x = torch.transpose(x, 1, 2).contiguous() # x.shape=(batchsize, channels_per_group, groups, height, width)
x = x.view(batchsize, -1, height, width)
return x
关于代码中 contiguous() 的解释可以参考 PyTorch中的contiguous。
当然 pytorch 已经实现了channel_shuffle 模块,在实际应用中只需要:
import torch
channel_shuffle = torch.nn.ChannelShuffle(groups=2)
input = torch.randn(1,4,2,2)
output = channel_shuffle(input)
make_divisible(A,B):找到比A大的,能整除B的最小整数。
3. SE 模块
参考 https://www.jianshu.com/p/5af9f4811fbd
Squeeze-and-Excitation Networks, 2017, CVPR
SE 模块显式地建模特征通道之间的相互依赖关系。就是通过学习的方式获取每个 channel 的重要程度,然后依照这个重要程度来对各个通道上的特征进行加权,从而突出重要特征,抑制不重要的特征。简单说就是训练一组权重,对各个 channel 的特征图加权。
本质上,SE 模块是在 channel 维度上做 attention 或者 gating 操作,这种注意力机制让模型可以更加关注重要的 channel 的特征。SE模块可以轻松的移植到其他网络架构,能够以轻微的计算性能损失带来极大的准确率提升。
常规卷积操作会对输入各个通道做卷积,然后对个通道的卷积结果进行求和。这种操作将卷积学习到的空间特征和通道特征混合在一起了。而 SE 模块就是为了抽离这种混杂,让模型直接学习通道特征。
Ftr 是常规的卷积操作。U 后面是SE模块,包含 squeeze 和 excitation 两步。
Squeeze 操作
由于卷积只是在局部空间内进行操作(没有全局感受野),很难获得做够的信息来提取 channel 之间的关系特征。 为了提取 channel 之间的关系,首先要将每个 channel 上的空间特征编码(压缩)为一个全局特征(可以理解为对每个 channel 的特征信息的进行融合)。可以采用全局平局池化来实现:
z_{c}=\mathbf{F}_{s q}\left(\mathbf{u}_{c}\right)=\frac{1}{H \times W} \sum_{i=1}^{H} \sum_{j=1}^{W} u_{c}(i, j)
输出维度为 1 x 1 x C。
Excitation 操作
Sequeeze 操作获得了各个 channel 的全局特征,接下来要提取各个 channel 之间的关系。我们选择使用带有 sigmoid 激活的简单门控机制实现:
\mathbf{s}=\mathbf{F}_{e x}(\mathbf{z}, \mathbf{W})=\sigma(g(\mathbf{z}, \mathbf{W}))=\sigma\left(\mathbf{W}_{2} \delta\left(\mathbf{W}_{1} \mathbf{z}\right)\right)
公式中包含两个全连接层,第一个全连接层\mathbf{W}_{1} \mathbf{z}起到降维的作用,经过激活函数\delta(通常为 ReLU)后,再用一个全连接层 \mathbf{W}_{2} 恢复到原来的维度。最后经过 sigmoid 激活函数\sigma,得到输出\mathbf{s},其维度为 1 x 1 x C。
整个过程其实简单明了,本质的核心就是去学习各个通道数之间的权重关系,如上图的颜色所示,不同颜色就代表了不同的权重。
最后一步就十分简单了,就是拿各通道的权重s_{c}与各个通道的特征\mathbf{u}_{c}相乘:
\tilde{\mathbf{x}}_{c}=s_{c} \mathbf{u}_{c}
pytorch 实现
对1x1xC的输入进行1x1的卷积,其效果等同于全连接层,所以SE block 实现如下:
import torch.nn as nn
class SELayer(nn.Module):
def __init__(self, channels, ratio=8):
super(SELayer, self).__init__()
self.global_avgpool = nn.AdaptiveAvgPool2d(1)
self.conv1 = nn.Conv2d(
in_channels=channels,
out_channels=channels // ratio,
kernel_size=1,
stride=1)
self.relu = nn.ReLU()
self.conv2 = Conv2d(
in_channels=channels // ratio,
out_channels=channels,
kernel_size=1,
stride=1)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
outputs = self.global_avgpool(x)
outputs = self.conv1(outputs)
outputs = self.relu(outputs)
outputs = self.conv2(outputs)
outputs = self.sigmoid(outputs)
return x * outputs
4. Ghost 模块
原文链接:GhostNet: More Features from Cheap Operations;cvpr;2020
参考:GhostNet论文解析:Ghost Module
Ghost 模块可以用更少的参数生成更多的特征图。对比下图中 Resnet-50 网络第一个 Resdual group 输出的特征图可视化结果,可以发现很多特征图高度相似,一些特征图就像是另一些特征图的 ghost(可以理解为重影、影像)。如下图中三个颜色块分别框出的特征对。 我们常规思维会认为这些相似特征是冗余信息,而GhostNet作者却认为这些 ghost 特征不属于冗余信息,而是有助于增强模型的特征提取能力的。只不过这些 ghost 特征图
只需要通过对本征特征图
(intrinsic feature maps)做简单的线性操作即可获得,而没有必要通过卷积操作生成。这样可以大大缩减计算量。
常规卷积如下图所示,如果输入 X \in \mathbb{R}^{c \times h \times w},输出 Y \in \mathbb{R}^{h^{\prime} \times w^{\prime} \times n},卷积核f \in \mathbb{R}^{c \times k \times k \times n},则卷积操作的 FLOPs 约为 n \cdot h^{\prime} \cdot w^{\prime} \cdot c \cdot k \cdot k。
ghost 模块分为三步:
1.基础卷积 Conv 得到本征特征图 Y^{\prime} \in \mathbb{R}^{h^{\prime} \times w^{\prime} \times m}。
2.将 Y^{\prime} 每一个通道的特征图 y_{i}^{\prime} 用线性运算产生 Ghost 特征图y_{i j}:
y_{i j}=\Phi_{i, j}\left(y_{i}^{\prime}\right), \quad \forall i=1, \ldots, m, \quad j=1, \ldots, s
\Phi_{i, j} 表示生成第 j 个 ghost 特征图的线性运算。也就是说y_{i}^{\prime}可以有不止一个 ghost 特征图。
3.将本征特征图和 Ghost 特征图拼接得到最终结果。当然也可以认为(原文就是这样认为的)拼接的本征特征图是第 2 步\Phi_{i, j}的单位映射(恒等映射),即 s 个线性操作包含了 1 个单位映射和(s-1)个线性操作,那么就不需要第 3 步了。此时n=m·s。
关于线性操作 \Phi_{i, j}:论文中使用的线性操作并不是常见的旋转、平移、仿射变换、小波变换等,而是 Depthwise 卷积。论文中表示这样做是因为 Depthwise 卷积的计算效率高。
在输入输出尺寸与常规卷积相同的情况下,第 1 步 Conv 操作的 FLOPs 为 \frac{n}{s} \cdot h^{\prime} \cdot w^{\prime} \cdot c \cdot k \cdot k,第 2 步 假设Depthwise 卷积的kernel\_size=d,则 FLOPS 为 (s-1) \cdot \frac{n}{s} \cdot h^{\prime} \cdot w^{\prime} \cdot d \cdot d。通常 s<<{c} , 用Ghost模块升级普通卷积的理论加速比为
\begin{aligned} r_{s} &=\frac{n \cdot h^{\prime} \cdot w^{\prime} \cdot c \cdot k \cdot k}{\frac{n}{s} \cdot h^{\prime} \cdot w^{\prime} \cdot c \cdot k \cdot k+(s-1) \cdot \frac{n}{s} \cdot h^{\prime} \cdot w^{\prime} \cdot d \cdot d} \ &=\frac{c \cdot k \cdot k}{\frac{1}{s} \cdot c \cdot k \cdot k+\frac{s-1}{s} \cdot d \cdot d} \approx \frac{s \cdot c}{s+c-1} \approx s \end{aligned}
Ghost 模块的 pytorch 实现
class GhostModule(nn.Module):
def __init__(self, inp, oup, kernel_size=1, ratio=2, dw_size=3, stride=1, relu=True):
super(GhostModule, self).__init__()
self.oup = oup
init_channels = math.ceil(oup / ratio) # 经过基础卷积得到的 feature maps # ceil 向上取整
new_channels = init_channels*(ratio-1) # 经过线性变换得到的 feature maps
# 基础卷积部分。kernel_size=1时 ,实际上是 Pointwise 卷积
self.primary_conv = nn.Sequential(
nn.Conv2d(inp, init_channels, kernel_size, stride, kernel_size//2, bias=False),
nn.BatchNorm2d(init_channels),
nn.ReLU(inplace=True) if relu else nn.Sequential(),
)
# 线性变换部分。实际上是Depthwise 卷积,从参数 groups=init_channels 可以看出
self.cheap_operation = nn.Sequential(
nn.Conv2d(init_channels, new_channels, dw_size, 1, dw_size//2, groups=init_channels, bias=False),
nn.BatchNorm2d(new_channels),
nn.ReLU(inplace=True) if relu else nn.Sequential(),
)
def forward(self, x):
x1 = self.primary_conv(x)
x2 = self.cheap_operation(x1)
out = torch.cat([x1,x2], dim=1)
return out[:,:self.oup,:,:]
从代码可见,Ghost Module和深度可分离卷积十分相似,不同之处在于Ghost 模块先进行 PointwiseConv,后进行 DepthwiseConv,另外增加了 DepthwiseConv 的数量(包括一个恒定映射)。
从另一个角度理解 ghost 模块:相当于对 Feature Maps 做了数据增强。
评论(0)
您还未登录,请登录后发表或查看评论