SqueezeNet
Fire Module: Squeeze and Expand
- SqueezeNet 的主要模块为 Fire Module,它主要从网络结构优化的角度出发,使用了如下 3 点策略来减少网络参数,提升网络性能:
- (1)使用
卷积来替代部分的
卷积,可以将参数减少为原来的
,同时减少输入通道的数量
- (2)利用
卷积来减少输入通道的数量
- (3)在减少通道数之后,使用多个尺寸的卷积核进行计算,以保留更多的信息,提升分类的准确率
均代表卷积层输出的通道数,Fire Module 默认
import torch
from torch import nn
class Fire(nn.Module):
def __init__(self, inplanes, squeeze_planes, expand_planes):
# 不改变输入特征图的宽高,只改变通道数
# 输入通道数为 inplanes,压缩通道数为 squeeze_planes,输出通道数为 2 * expand_planes
super(Fire, self).__init__()
# Squeeze 层
self.conv1 = nn.Conv2d(inplanes, squeeze_planes, kernel_size=1, stride=1)
self.bn1 = nn.BatchNorm2d(squeeze_planes)
self.relu1 = nn.ReLU(inplace=True)
# Expand 层
self.conv2 = nn.Conv2d(squeeze_planes, expand_planes, kernel_size=1, stride=1)
self.bn2 = nn.BatchNorm2d(expand_planes)
self.conv3 = nn.Conv2d(squeeze_planes, expand_planes, kernel_size=3, stride=1, padding=1)
self.bn3 = nn.BatchNorm2d(expand_planes)
self.relu2 = nn.ReLU(inplace=True)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu1(x)
out1 = self.conv2(x)
out1 = self.bn2(out1)
out2 = self.conv3(x)
out2 = self.bn3(out2)
out = torch.cat([out1, out2], 1)
out = self.relu2(out)
return out
SqueezeNet
- 输入图像首先送入 Conv 1,得到通道数为 96 的特征图,然后依次使用 8 个 Fire Module,中间还有两个步长为 2 的最大池化层,通道数也逐渐增加。最后一个卷积为 Conv 10,输出通道数为
的特征图 (
代表类别数),经过全局池化层后送入 Softmax
SqueezeNet 总结
- SqueezeNet 是一个精心设计的轻量化网络,其性能与 AlexNet 相近,而模型参数仅有AlexNet的
。使用常见的模型压缩技术,如 SVD、剪枝和量化等,可以进一步压缩该模型的大小。例如,使用 Deep Compresion 技术对其进行压缩时,在几乎不损失性能的前提下,模型大小可以压缩到 0.5MB
- 基于其轻量化的特性,SqueezeNet 可以广泛地应用到移动端,促进了物体检测技术在移动端的部署与应用
MobileNet
深度可分离卷积 (Depthwise Separable Convolution)
- SqueezeNet 虽在一定程度上减少了卷积计算量,但仍然使用传统的卷积计算方式,而在其后的 MobileNet 利用了更为高效的深度可分离卷积的方式,进一步加速了卷积网络在移动端的应用
标准卷积
- 假设当前特征图大小为
,需要输出的特征图大小为
,卷积核大小为
,Padding 为 1,那么总的计算量为
输出特征图上的每个点同时结合空间信息和通道信息
深度可分离卷积
- depthwise separable convolution 将空间信息的融合和通道信息的融合分开,将卷积过程分为两步:逐通道卷积(融合空间信息)和逐点
卷积(融合通道)信息),大大减少了计算量
- (1)逐通道卷积: 对于一个通道的输入特征
,利用一个
卷积核进行点乘求和,得到一个通道的输出
。然后,对于所有的输入通道
,使用
个
卷积核即可得到
大小的输出
逐通道卷积的卷积核参数个数为
,远小于标准卷积的个数
。同时,各通道相互独立,各通道之间没有特征融合。总计算量为:
- (2)逐点
卷积: 逐点
卷积就是用
卷积层融合不同通道间的特征,同时也可以改变特征图的通道。由于这里
卷积的输入特征图大小为
,输出特征图大小为
,因此这一步的总计算量为:
- 结合以上两步,我们可以得到depthwise separable convolution 和standard convolution 的计算量之比:
可以看出depthwise separable convolution的整体计算量约等于标准卷积的
,大大减少了卷积过程的计算量。
MobileNet v1
深度可分离卷积模块
- 值得注意的是,在此使用了 ReLU6 来替代原始的 ReLU 激活函数,将 ReLU 的最大输出限制在 6 以下。这主要是为了满足移动端部署的需求。移动端通常使用 Float16 或者 Int8 等较低精度的模型,如果不对激活函数的输出进行限制的话,激活值的分布范围会很大,而低精度的模型很难精确地覆盖如此大范围的输出,这样会带来精度损失
MobileNet v1 整体结构
代表逐通道卷积 (深度分解卷积),其后需要跟一个
卷积;
代表步长为 2 的卷积,用于缩小特征图尺寸,起到与 Pooling 层一样的作用
from torch import nn
class MobileNet(nn.Module):
def __init__(self):
super(MobileNet, self).__init__()
# 3 x 3 standard convolution
def conv_bn(dim_in, dim_out, stride):
return nn.Sequential(
nn.Conv2d(dim_in, dim_out, 3, stride, 1, bias=False),
nn.BatchNorm2d(dim_out),
nn.ReLU(inplace=True)
)
# depthwise separable convolution
def conv_dw(dim_in, dim_out, stride):
return nn.Sequential(
# use group convolution in PyTorch to implement channel-wise convolution
nn.Conv2d(dim_in, dim_in, 3, stride, 1, groups=dim_in, bias=False),
nn.BatchNorm2d(dim_in),
nn.ReLU(inplace=True),
nn.Conv2d(dim_in, dim_out, 1, 1, 0, bias=False),
nn.BatchNorm2d(dim_out),
nn.ReLU(inplace=True),
)
self.model = nn.Sequential(
# input: 3 x 224 x 224
conv_bn( 3, 32, 2), # 32 x 112 x 112
conv_dw( 32, 64, 1), # 64 x 112 x 112
conv_dw( 64, 128, 2), # 128 x 56 x 56
conv_dw(128, 128, 1), # 128 x 56 x 56
conv_dw(128, 256, 2), # 256 x 28 x 28
conv_dw(256, 256, 1), # 256 x 28 x 28
conv_dw(256, 512, 2), # 512 x 14 x 14
conv_dw(512, 512, 1), # 512 x 14 x 14
conv_dw(512, 512, 1), # 512 x 14 x 14
conv_dw(512, 512, 1), # 512 x 14 x 14
conv_dw(512, 512, 1), # 512 x 14 x 14
conv_dw(512, 512, 1), # 512 x 14 x 14
conv_dw(512, 1024, 2), # 1024 x 7 x 7
conv_dw(1024, 1024, 1), # 1024 x 7 x 7
nn.AvgPool2d(7), # 1024 x 1 x 1
)
self.fc = nn.Linear(1024, 1000)
def forward(self, x):
x = self.model(x)
x = x.view(-1, 1024)
x = self.fc(x)
return x
MobileNet v1 总结
- 总体上,MobileNet v1 利用深度可分离的结构牺牲了较小的精度,带来了计算量与网络层参数的大幅降低,从而也减小了模型的大小,方便应用于移动端
- 但 MobileNet v1 也有其自身结构带来的缺陷,主要有以下两点:
- (1) 模型结构较为复古,采用了与 VGGNet 类似的卷积简单堆叠,没有采用残差、特征融合等先进的结构
- (2)深度分解卷积 (i.e. 逐通道卷积) 中各通道相互独立,卷积核维度较小,输出特征中只有较少的输入特征,再加上 ReLU 激活函数,使得输出很容易变为 0,难以恢复正常训练,因此在训练时部分卷积核容易被训练废掉
MobileNet v2
Inverted Residual Block
- MobileNet v2 利用残差结构取代了原始的卷积堆叠方式,提出了 Inverted Residual Block 。在标准的 ResNet中,由于
卷积处的计算量较大,因此通常先使用
卷积进行特征降维,减少通道数,再送入
卷积,最后再利用
卷积升维。这种结构从形状上看中间窄两边宽,类似于沙漏形状;而在 MobileNet 中,由于使用了深度可分离卷积来逐通道计算,本身计算量就比较少,因此在此可以使用
卷积来升维,在计算量增加不大的基础上获取更好的效果,最后再用
卷积降维。这种结构中间宽两边窄,类似于柳叶,该结构也因此被称为 Inverted Residual Block
- 依据卷积的步长,该结构可分为两种情形,在步长为 1 时使用了残差连接,融合的方式为逐元素相加;而步长为 2 时不使用残差连接
去掉 ReLU6
- 深度可分离卷积得到的特征对应于低维空间,特征较少,如果后续接线性映射则能够保留大部分特征,而如果接非线性映射如 ReLU,则会破坏特征,造成特征的损耗,从而使得模型效果变差。针对此问题,MobileNet v2 直接去掉了每一个 Block 中最后的 ReLU6 层,减少了特征的损耗,获得了更好的检测效果
import torch.nn as nn
import math
# 标准 3 x 3 卷积
def conv_bn(inp, oup, stride):
return nn.Sequential(
nn.Conv2d(inp, oup, 3, stride, 1, bias=False),
nn.BatchNorm2d(oup),
nn.ReLU6(inplace=True)
)
# 标准 1 x 1 卷积
def conv_1x1_bn(inp, oup):
return nn.Sequential(
nn.Conv2d(inp, oup, 1, 1, 0, bias=False),
nn.BatchNorm2d(oup),
nn.ReLU6(inplace=True)
)
class InvertedResidual(nn.Module):
def __init__(self, inp, oup, stride, expand_ratio):
super(InvertedResidual, self).__init__()
self.stride = stride
assert stride in [1, 2]
hidden_dim = round(inp * expand_ratio) # 中间扩展层的通道数
self.use_res_connect = self.stride == 1 and inp == oup
if expand_ratio == 1:
# 不进行升维
self.conv = nn.Sequential(
# dw (逐通道卷积)
nn.Conv2d(hidden_dim, hidden_dim, 3, stride, 1, groups=hidden_dim, bias=False),
nn.BatchNorm2d(hidden_dim),
nn.ReLU6(inplace=True),
# pw-linear (降维)
nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
nn.BatchNorm2d(oup),
)
else:
self.conv = nn.Sequential(
# pw (升维)
nn.Conv2d(inp, hidden_dim, 1, 1, 0, bias=False),
nn.BatchNorm2d(hidden_dim),
nn.ReLU6(inplace=True),
# dw (逐通道卷积)
nn.Conv2d(hidden_dim, hidden_dim, 3, stride, 1, groups=hidden_dim, bias=False),
nn.BatchNorm2d(hidden_dim),
nn.ReLU6(inplace=True),
# pw-linear (降维)
nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
nn.BatchNorm2d(oup),
)
def forward(self, x):
if self.use_res_connect:
return x + self.conv(x)
else:
return self.conv(x)
class MobileNetV2(nn.Module):
def __init__(self, n_class=1000, input_size=224, width_mult=1.):
super(MobileNetV2, self).__init__()
block = InvertedResidual
input_channel = 32
last_channel = 1280
interverted_residual_setting = [
# t, c, n, s
[1, 16, 1, 1],
[6, 24, 2, 2],
[6, 32, 3, 2],
[6, 64, 4, 2],
[6, 96, 3, 1],
[6, 160, 3, 2],
[6, 320, 1, 1],
]
# building first layer
assert input_size % 32 == 0
input_channel = int(input_channel * width_mult)
self.last_channel = int(last_channel * width_mult) if width_mult > 1.0 else last_channel
self.features = [conv_bn(3, input_channel, 2)]
# building inverted residual blocks
for t, c, n, s in interverted_residual_setting:
output_channel = int(c * width_mult)
for i in range(n):
if i == 0:
self.features.append(block(input_channel, output_channel, s, expand_ratio=t))
else:
self.features.append(block(input_channel, output_channel, 1, expand_ratio=t))
input_channel = output_channel
# building last several layers
self.features.append(conv_1x1_bn(input_channel, self.last_channel))
# make it nn.Sequential
self.features = nn.Sequential(*self.features)
# building classifier
self.classifier = nn.Sequential(
nn.Dropout(0.2),
nn.Linear(self.last_channel, n_class),
)
self._initialize_weights()
def forward(self, x):
x = self.features(x)
x = x.mean(3).mean(2)
x = self.classifier(x)
return x
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
if m.bias is not None:
m.bias.data.zero_()
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
elif isinstance(m, nn.Linear):
n = m.weight.size(1)
m.weight.data.normal_(0, 0.01)
m.bias.data.zero_()
ShuffleNet
频道洗牌
- 目前先进的轻量级网络大多使用depthwise separable convolution或者group convolution来降低网络的计算量,但是这两种操作都不能改变特征的通道数,所以也需要使用
卷积来方便通道之间 信息的融合,将通道更改为指定维度。因此,轻量级网络中的
卷积占用了大量的计算量,且通道中充满了约束,在一定程度上降低了模型的准确率。
- 为了进一步降低计算量, ShuffleNet 提出了通道混洗来完成通道之间信息的融合:
图表示常规的两组卷积操作。可以看出,如果没有逐点
卷积或者channel shuffling,最终输出的特征只能从一部分输入通道的特征中计算出来,阻碍了操作。这减少了信息的流动,进而降低了特征的表现力。因此,我们希望经过一次组卷积后,可以融合特征图之间的通道信息,类似于
图的操作,将每组的特征分散到不同的组后,再进行下一次组卷积。 ,这样输出的特征就可以包含每一组的特征,而channel shuffle正好可以实现这个过程,如图
- 通道混洗可以通过几个常规的张量操作巧妙地实现。这里对输入通道做了 1~12 的编号,一共包含 3 个组,每个组包含 4 个通道:
def channel_shuffle(x, groups):
batchsize, num_channels, height, width = x.data.size()
channels_per_group = num_channels // groups
# Reshape
x = x.view(batchsize, groups, channels_per_group, height, width)
# Transpose
x = torch.transpose(x, 1, 2).contiguous()
# Flatten
x = x.view(batchsize, -1, height, width)
return x
ShuffleNet v1
- ShuffleNet v1 code : https://github.com/jaxony/ShuffleNet
ShuffleNet v1 基本结构单元
图是一个带有深度可分离卷积的残差模块(i.e. MobileNet v2 的基本单元)
图则是ShuffleNet 的基本单元,可以看到
卷积采用的是组卷积,然后进行通道的混洗,这两步可以取代
的逐点卷积,并且大大降低了计算量。
卷积仍然采用深度可分离的方式
图是带有降采样的 ShuffleNet 单元。由于降采样时通常要伴有通道数的增加,ShuffleNet 直接将两分支拼接在一起来实现了通道数的增加,而不是常规的逐点相加
ShuffleNet v1 网络整体结构
- 深度可分离卷积虽然可以有效降低计算量,但其存储访问效率较差,因此 ShuffleNet只在 Stage 2, 3, 4 内使用了其特殊的基本单元,这 3 个阶段的第一个 Block 的步长为 2 以完成降采样,下一个阶段的通道数是上一个的两倍
表示组卷积的组数,以控制卷积连接的稀疏性。组数越多,计算量越少,所以在计算资源相同的情况下,可以使用更多的卷积核来获得更多的通道
ShuffleNet v2
- paper : ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design
建立高性能网络的 4 个基本规则
- 原有的一些轻量化方法在衡量模型性能时,通常使用浮点运算量 FLOPs (Floating Point Operations) 作为主要指标,其中 FLOPs 是指模型在进行一次前向传播时所需的浮点计算次数。然而,ShuffleNet v2 的作者通过一系列实验发现仅仅将 FLOPs 作为评判模型复杂度的指标是有问题的,FLOPs 近似的网络会存在不同的速度,还有另外两个重要的指标:内存访问时间 (Memory Access Cost,MAC) 与网络的并行度
- 以此作为出发点,ShuffleNet v2 做了大量的实验,分析影响网络运行速度的原因,提出了建立高性能网络的 4 个基本规则:
- (1)卷积层的输入特征与输出特征通道数相等时,MAC 最小,此时模型速度最快:轻量化网络常采用深度可分离卷积,其中
卷积的计算复杂度最大,因此下面主要考虑卷积核大小为
时的情况。假设输入与输出特征图大小为
,通道数分别为
,则 FLOPs 为
,MAC 为
(两项分别为输入输出特征图的大小和
卷积核的大小)。当 FLOPs
固定时,有
,代入 MAC 表达式可得
因此 FLOPs 不变时,当且仅当
时 MAC 最小。虽然上述理论分析只适用于有足够大的 cache 能容纳输入/出特征图及卷积参数的情况,实际 MAC 可能会有所出入,但实验证明上述规律确实存在:
- (2)过多的组卷积会增加 MAC,导致模型的速度变慢:组卷积能在相同 FLOPs 的条件下输出更多通道数,但更多的通道数就意味着会增大 MAC。设组数为
,则 FLOPs
,因此
由此可见,
的增加会使得 MAC 显著增加
- (3)网络的碎片化会降低可并行度,这表明模型中分支数量越少,模型速度会越快
- (4)逐元素 (Element Wise) 操作虽然 FLOPs 值较低,但其 MAC 较高,因此也应当尽可能减少逐元素操作(逐元素操作包括 ReLU, AddTensor, AddBias…)。
ShuffleNet v1 的问题
以上述 4 个规则为基础,可以看出ShuffleNet v1 有 3 点违反了此规则:
- (1) 在 Bottleneck 中使用了
组卷积与
的逐点卷积的 bottleneck 结构,导致输入输出通道数不同,违背了规则 1 与规则 2
- (2) 整体网络中使用了大量的组卷积,造成了太多的分组,违背了规则 3
- (3) 网络中存在大量的逐点相加操作,违背了规则 4
ShuffleNet v2
- 如上图所示,(a) (b) 为 v1 的基础结构,(
) (d) 为 v2 的基础结构。v2 提出了 Channel Split 操作,如 (
) 所示,将输入特征分成两部分,一部分进行真正的深度可分离计算,将计算结果与另一部分进行通道 Concat,最后进行通道的混洗操作,完成信息的互通。同时,整个过程没有使用到
组卷积,也避免了逐点相加的操作。在需要降采样与通道翻倍时,ShuffleNet v2 去掉了 Channel Split 操作,这样最后 Concat 时通道数会翻倍
- 基于此基本单元,ShuffleNet v2 的整体结构如下表所示。与 ShuffleNet v1 相比,ShuffleNet v2在全局平均池化之前增加了一个
卷积来融合特征:
参考
- 《深度学习之 PyTorch 物体检测实战》
文章出处登录后可见!
已经登录?立即刷新