文章目录
前言
MobileNet网络是由google团队在2017年提出的,专注于移动端或者嵌入 式设备中的轻量级CNN网络。相比传统卷积神经网络,在准确率小幅降低的前提下大大减少模型参数与运算量。(相比VGG16准确率减少了0.9%, 但模型参数只有VGG的1/32)。主要创新点在于深度可分离卷积,而整个网络实际上也是深度可分离模块的堆叠。
mobilenetv1论文地址:https://arxiv.org/abs/1704.04861
mobilenetv2论文地址:https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=8578572
1.mobilenetV1解析
网络结构
DW(Depthwise Convolution)卷积:对输入特征层,每个通道对应一个卷积核且每个卷积核的通道数仅为1个通道。这样对每一通道进行逐通道卷积的操作,就得到了和输入特征图通道数一致的输出特征图。
PW(Pointwise Conv)卷积:对输入特征层,进行每个像素点卷积(1X1)卷积。卷积核大小为1X1,通道数为输出所需的通道数。
普通卷积修改为深度可分离卷积
为什么深度可分离卷积会比普通的卷积推理时间要快。
我们看看普通卷积的计算量:
对于输入为SxSxM大小的特征层,使用KxK大小的卷积核,要使输出变为PxPxN。计算量为:
计算量大小:KxKxMxNxPxP
再看看使用深度分离卷积的计算量:
使用DW:KxKx1xMxPxP # 仅使用一个卷积核通道,并且进行DW卷积输出通道数不变
使用PW:1x1XMxNxPxP # 仅使用卷积核大小为1X1的卷积,并且进行PW卷积输出通道数会变
DW+PW:k*k*M*P*P+M*N*P*P
比较两者的关系
(k*k*M*P*P+M*N*P*P)/K*K*M*N*P*P=1/N+1/k^2
所以如果卷积核大小为3x3,那么理论上普通卷积计算量是DW+PW的8到9倍。
这就是为什么深度可分离卷积会比普通的卷积运行时间要快
mobilenetv1还设置了两个超参数α和β来控制网络结构的宽度和输入分辨率大小
通道数和分辨率的不同选择对应的结果。
3.mobilenetV2解析
网络结构
t是通道扩展因子(升维和降维几倍) ,c是输出特征矩阵通道数channel ,n是bottleneck的重复次数 ,s是步距(针对第一个残差层,其他为1)
主要提出了倒残差结构
Resnet里的残差结构是先1x1卷积降维后,3X3卷积,再1X1卷积升维,两边大,中间小。这里是先升维,DW卷积后,1X1卷积降维。直接1X1卷积降维再经过深度可分离卷积(卷积核通道仅为1)后,提取的特征会更少,所以采用先升维丰富特征信息的思想。这里每层的激活函数由原来的BN+Relu变成了Relu6,最后由高维变低维输出没有用relu6了的是线性层。如下图所示,并且只有输入和输出分辨率和通道一致的时候才采用残差连接。
为什么选择使用有限制的relu6作为激活函数。
因为mobilenet主要是用在移动端设备,如果限制权值范围,使得整个区间被好好的利用起来,这样的话权值能够被更加均匀的映射到0-255区间上,使得权值信息更多地被保留,从而减少了量化误差。
https://blog.csdn.net/tangshopping/article/details/112979152
为什么最后输出不用relu激活函数
因为作者发现,relu激活函数在变为低纬度的输出会损失很多信息,变为高维用可以。因为最后输出是由高维变为低维,就直接用1X1线性层了(这里我们在压缩通道操作的时候要不要考虑下呢)。
代码解析
mobilenetv2的forward函数
def forward(self, x):
"""Forward function."""
x = self.conv1(x)
outs = []
for i, layer_name in enumerate(self.layers):
layer = getattr(self, layer_name)
x = layer(x)
if i in self.out_indices:
outs.append(x)
return tuple(outs)
conv1:是一个卷积核k=3,stride = 2,padding=1的3X3卷积。
layer层
for i, layer_cfg in enumerate(self.arch_settings):
expand_ratio, channel, num_blocks, stride = layer_cfg # 每个layer层的配置
out_channels = make_divisible(channel * widen_factor, 8) # 输出通道调整为8的整数倍
inverted_res_layer = self.make_layer( # 创建倒残差块
out_channels=out_channels,
num_blocks=num_blocks,
stride=stride,
expand_ratio=expand_ratio)
layer_name = f'layer{i + 1}'
self.add_module(layer_name, inverted_res_layer)
self.layers.append(layer_name)
if widen_factor > 1.0:
self.out_channel = int(1280 * widen_factor)
else:
self.out_channel = 1280
make_divisible
def make_divisible(value, divisor, min_value=None, min_ratio=0.9):
if min_value is None:
min_value = divisor
new_value = max(min_value, int(value + divisor / 2) // divisor * divisor) # 注意// divisor * divisor不会抵消,是向下取整后再乘
# Make sure that round down does not go down by more than (1-min_ratio).
if new_value < min_ratio * value: # 确保不会减少10%
new_value += divisor
return new_value
构建倒残差块
def make_layer(self, out_channels, num_blocks, stride, expand_ratio):
"""Stack InvertedResidual blocks to build a layer for MobileNetV2.
Args:
out_channels (int): out_channels of block.
num_blocks (int): number of blocks.
stride (int): stride of the first block. Default: 1
expand_ratio (int): Expand the number of channels of the
hidden layer in InvertedResidual by this ratio. Default: 6.
"""
layers = []
for i in range(num_blocks):
if i >= 1:
stride = 1 # stride 只在堆叠的残差块的第一个残差块会变化,其他的都为1
layers.append(
InvertedResidual(
self.in_channels,
out_channels,
mid_channels=int(round(self.in_channels * expand_ratio)), # 中间升维后的通带数
stride=stride,
with_expand_conv=expand_ratio != 1,
conv_cfg=self.conv_cfg,
norm_cfg=self.norm_cfg,
act_cfg=self.act_cfg,
with_cp=self.with_cp))
self.in_channels = out_channels # 最后输出通道数等于输入通道数
return nn.Sequential(*layers)