目录
参考资料:
论文:
MobileNetV1: Efficient Convolutional Neural Networks for Mobile Vision Applications
博客:
一、简介
传统卷积神经网络,内存需求大、运算量大导致无法在移动设备以及嵌入式设备上运行。
MobileNet网络是由google团队在2017年提出的,专注于移动端或者嵌入式设备中的轻量级CNN网络。相比传统卷积神经网络,在准确率小幅降低的前提下大大减少模型参数与运算量。(相比VGG16准确率减少了0.9%,但模型参数只有VGG的1/32)
, 网络的两点主要体现下两方面:
- Depthwise Convolution(深度可分离卷积),大大减少运算量和参数数量;
- 增加了两个超参数 α \alpha α, β \beta β ,其中 α \alpha α控制卷积核个数的超参数, β \beta β 控制输入图像大小,这两个参数是人为设定的,并不是网络学习到的;
二、深度可分离卷积
它的核心思想是将一个完整的卷积运算分解为两步进行,分别为逐深度卷积(Depthwise Convolution)与逐点卷积(Pointwise Convolution)。
(1)常规卷积
假设输入层为一个大小为64×64像素、3通道彩色图片。经过一个包含4个Filter的卷积层,最终输出4个Feature Map,且尺寸与输入层相同。整个过程可以用下图来概括:
此时,卷积层共4个Filter,每个Filter包含了3个Kernel,每个Kernel的大小为3×3。因此卷积层的参数数量可以用如下公式来计算:N_std = 4 × 3 × 3 × 3 = 108
(2)逐深度卷积(Depthwise Convolution)
同样是上述例子,一个大小为64×64像素、3通道彩色图片首先经过第一次卷积运算,不同之处在于此次的卷积完全是在二维平面内进行,且Filter的数量与上一层的Depth相同。所以一个三通道的图像经过运算后生成了3个Feature map,如下图所示。
其中一个Filter只包含一个大小为3×3的Kernel,卷积部分的参数个数计算如下:N_depthwise = 3 × 3 × 3 = 27
Depthwise Convolution完成后的Feature map数量与输入层的depth相同,但是这种运算对输入层的每个channel独立进行卷积运算后就结束了,没有有效的利用不同map在相同空间位置上的信息。因此需要增加另外一步操作来将这些map进行组合生成新的Feature map,即接下来的Pointwise Convolution。
(3)逐点卷积(Pointwise Convolution)
Pointwise Convolution的运算与常规卷积运算非常相似,不同之处在于卷积核的尺寸为 1×1×M × N,M为上一层的depth,N为新生成的Feature map的个数。所以这里的卷积运算会将上一步的map在深度方向上进行加权组合,生成新的Feature map。有几个Filter就有几个Feature map。如下图所示。
由于采用的是1×1卷积的方式,此步中卷积涉及到的参数个数可以计算为:N_pointwise = 1 × 1 × 3 × 4 = 12
(4)参数数量对比
参考<<卷积参数量计算(标准卷积,分组卷积,深度可分离开)>>
假设输入特征图为( H × W × M H × W × M H×W×M),卷积核大小为( D K × D K × N D_K × D_K × N DK×DK×N), M M M 和 N N N 分别为输入和输出通道数量,则对应的参数量 为:
- 标准卷积 : D K × D K × M × N D_K × D_K × M × N DK×DK×M×N
- 深度卷积 : D K × D K × M × 1 D_K × D_K × M × 1 DK×DK×M×1
- 1 × 1 1×1 1×1点卷积 : 1 × 1 × M × N 1 × 1 × M × N 1×1×M×N
- 深度可分离卷积 :( D K × D K × M × 1 D_K × D_K × M × 1 DK×DK×M×1 ) + ( 1 × 1 × M × N 1 × 1 × M × N 1×1×M×N )
Separable Convolution的参数个数是常规卷积的约1/3。因此,在参数量相同的前提下,采用Separable Convolution的神经网络层数可以做的更深。
(5)计算量对比
假设输入特征图为( D F × D F × M D_F × D_F × M DF×DF×M),卷积核大小为( D K × D K × N D_K × D_K × N DK×DK×N), M M M 和 N N N 分别为输入和输出通道数量,则对应的 计算量 为:
- 标准卷积 : D F × D F × M × D K × D K × N D_F × D_F × M × D_K × D_K × N DF×DF×M×DK×DK×N
- 深度卷积 : D F × D F × M × D K × D K D_F × D_F × M × D_K × D_K DF×DF×M×DK×DK
- 1×1点卷积 : D F × D F × M × N × 1 × 1 D_F × D_F × M × N × 1 × 1 DF×DF×M×N×1×1
- 深度可分离卷积 :( D F × D F × M × D K × D K D_F × D_F × M × D_K × D_K DF×DF×M×DK×DK ) + ($ D_F × D_F × M × N × 1 × 1 $)
减少的 计算量 为:
三、网络结构
MobileNet 结构建立在深度可分离卷积基础之上,在 深度卷积
和 逐点卷积
之后加入归一化和激活层(BN and ReLU):
- 表1
Mobienet v1
网络结构中第一行Conv/s2
表示普通卷积,步距为2。Filter Shape
为3x3x3x32
表示卷积核大小为3x3
,输入为彩色图片3
通道,输出为32
通道; Conv dw/s1
表示DW
卷积,步距为1
,Filter Shape
为3 x 3 x 32
表示卷积核大小3x3
,dw卷积的channel为1
,卷积核的个数为32;- 由于可分离卷积是Mobienet v1基本组件,可分离卷积表示为
dw +pw
,因此dw 和1x1的pw是成对出现的 - Mobienet v1的模型结构有点类似于VGG结构,简单的将一系列卷积进行串行链接。
训练模型过程中,与训练大型模型策略相反 :
- 使用较少的
正则化
和数据增强
技术,这是由于小模型出现过拟合
现象的可能性很低 ; - 模型将几乎所有的计算都放在密集的 1×1卷积 ;
(1)宽度倍增器
为构造更小且计算成本更低的模型,引入了一个非常简单的参数 α
(宽度倍增器 ( Width Multiplier ) );
它可以在每一层均匀地细化网络,输入通道数 M
变为 αM
,输出通道数 N
变为 αN
;
基于此得到的模型计算量为( α ∈ (0,1) ):
- 深度可分离卷积 :( D F × D F × D K × D K × α M × 1 D_F × D_F × D_K × D_K × αM × 1 DF×DF×DK×DK×αM×1 ) + ( D F × D F × 1 × 1 × α M × α N D_F × D_F × 1 × 1 × αM × αN DF×DF×1×1×αM×αN )
典型值设置为 1、0.75、0.5
和 0.25
。 α=1 是基准 MobileNet 模型, α<1 是缩小版的 MobileNets
。宽度乘数的作用是将计算量和参数数量大约减少
α
2
α^2
α2倍,从而降低了网络计算成本( computational cost of a neural network)。 宽度乘数可以应用于任何模型结构,以定义新的较小模型,且具有合理的准确性、网络延迟 latency
和模型大小之间的权衡。 它用于定义新的精简结构,需要从头开始进行训练模型。
(2)分辨率倍增器
降低神经网络计算代价的第二个超参数为 ρ
(分辨率倍增器 ( Resolution Multiplier ) )
它可以减小输入特征图的尺寸 H × W H × W H×W,由 DF 变为 ρDF ;
实际上,论文通过设置输入分辨率来隐式设置 ρ 。 将网络核心层的计算成本表示为具有宽度乘数 α 和分辨率乘数 ρ 的深度可分离卷积的公式如下( α ∈ (0,1) ,ρ ∈ (0,1] ):
- 深度可分离卷积 :( ρ D F × ρ D F × D K × D K × α M × 1 ρDF × ρDF × D_K × D_K × αM × 1 ρDF×ρDF×DK×DK×αM×1 ) + ( ρ D F × ρ D F × 1 × 1 × α M × α N ρDF × ρDF × 1 × 1 × αM × αN ρDF×ρDF×1×1×αM×αN )
上图为 ρ = 1,6/7,5/7,4/7 时分别对应的结果 ;通常是隐式设置的,因此网络的输入分辨率为 224、192、160
或 128
。 ρ=1 时是基准(baseline
) MobilNet, ρ<1 时缩小版 MobileNets
。分辨率乘数的作用是将计算量减少
ρ
2
ρ^2
ρ2 。
在Mobienet v1网络的实际使用中,很多人发现dw卷积,它在训练完之后部分卷积核容易废掉,即卷积核参数大部分为0。因为你观察DW卷积的参数时,你会发现它的大部分参数都是等于0的,DW卷积核是没有起到作用的。针对这个问题再我们mobienet v2中会得到改善。
四、论文复现
(1)定义带BN的卷积:卷积+BN+ReLu
class ConvBNReLU(nn.Module):
def __init__(self, in_channels, out_channels, **kwargs):
super(ConvBNReLU, self).__init__()
self.conv = nn.Conv2d(in_channels, out_channels, **kwargs)
self.bn = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU6(inplace=True)
def forward(self, x):
x = self.conv(x)
x = self.bn(x)
x = self.relu(x)
return x
(2)深度可分离卷积:
-
groups
就是实现depthwise conv的关键,默认为1,意思是将输入分为一组,此时是常规卷积,当将其设为in_channels时,意思是将输入的每一个通道作为一组,然后分别对其卷积,输出通道数为k,最后再将每组的输出串联,最后通道数为K。 -
最后总结下,要实现depthwise conv,就将groups设为in_channels,同时out_channels也设为与in_channels相同,然后再加上1X1卷积。
class Depthwise_Separable(nn.Module):
def __init__(self, in_channels, out_channels, stride, padding):
super(Depthwise_Separable, self).__init__()
self.conv = nn.Sequential(
# 3 × 3 深度卷积
# groups=in_channels
ConvBNReLU(in_channels, in_channels, kernel_size=3,
stride=stride, padding=padding, groups=in_channels),
# 1 × 1 点卷积
ConvBNReLU(in_channels, out_channels, kernel_size=1, stride=1, bias=False)
)
def forward(self, x):
x = self.conv(x)
return x
(3)主干网络:
-
表1
Mobienet v1
网络结构中第一行Conv/s2
表示普通卷积,步距为2。Filter Shape
为3x3x3x32
表示卷积核大小为3x3
,输入为彩色图片3
通道,输出为32
通道; -
Conv dw/s1
表示DW
卷积,步距为1
,Filter Shape
为3 x 3 x 32
表示卷积核大小3x3
,dw卷积的channel为1
,卷积核的个数为32; -
由于可分离卷积是Mobienet v1基本组件,可分离卷积表示为
dw +pw
,因此dw 和1x1的pw是成对出现的
class MobileNet(nn.Module):
def __init__(self, num_classes=1000, width=1):
super(MobileNet, self).__init__()
# 第一个卷积核
self.first_conv = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=32, kernel_size=3, stride=2, padding=1, bias=False),
nn.BatchNorm2d(32),
nn.ReLU6(inplace=True),
)
# 主干网络
self.layers = nn.Sequential(
# width是宽度倍增器的系数α
# 它可以在每一层均匀地细化网络,输入通道数 `M` 变为 `αM`,输出通道数 `N` 变为 `αN` ;
Depthwise_Separable(width*32, width*64, 1, 1),
Depthwise_Separable(width*64, width*128, 2, 1),
Depthwise_Separable(width*128, width*128, 1, 1),
Depthwise_Separable(width*128, width*256, 2, 1),
Depthwise_Separable(width*256, width*256, 1, 1),
Depthwise_Separable(width*256, width*512, 2, 1),
Depthwise_Separable(width*512, width*512, 1, 1),
Depthwise_Separable(width*512, width*512, 1, 1),
Depthwise_Separable(width*512, width*512, 1, 1),
Depthwise_Separable(width*512, width*512, 1, 1),
Depthwise_Separable(width*512, width*512, 1, 1),
Depthwise_Separable(width*512, width*1024, 2, 1),
Depthwise_Separable(width*1024, width*1024, 2, 4),
)
self.pool = nn.AvgPool2d(kernel_size=7, stride=1)
self.classifier = nn.Linear(width*1024, num_classes)
def forward(self, x):
x = self.first_conv(x)
x = self.layers(x)
x = self.pool(x)
x = torch.flatten(x, 1)
out = self.classifier(x)
return out
def test():
net = MobileNet()
#创建模型,部署gpu
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
net.to(device)
summary(net, (3, 224, 224))
if __name__ == '__main__':
test()