目录
前景概述
(1)mobilenetV1主要的贡献简单来说,就是提出了深度可分离卷积,即将普通卷积操作,拆分成两步,深度卷积depthConv (卷积核通道数时1)和逐点卷积(卷积核大小是1x1),即先空间卷积,再通道卷积。作用是显著降低计算复杂度。
(2)mobilenetV2主要贡献简单来说,就是结合了残差,提出了InvertedResidual层(正经的残差层是沙漏型的,这个是梭型的),提高了精度。
mobilenetV2论文
https://arxiv.org/pdf/1801.04381.pdf
mobilenetV2网络结构:
网络结构逐层分析
第1层是普通的conv2d+bn+relu6. mmclassification实现层层封装,不好看,这里贴简单实现。
ef conv_bn(inp, oup, stride):
return nn.Sequential(
nn.Conv2d(inp, oup, 3, stride, 1, bias=False),
nn.BatchNorm2d(oup),
nn.ReLU6(inplace=True)
)
后面7层是bottleneck层。其中一个bottleneck实现:
class InvertedResidual(nn.Module):
def __init__(self, inp, oup, stride, expand_ratio):
# inp, oup分别是输入和输出通道数。expand_ratio是通道放大倍数,用于中间隐藏层。
super(InvertedResidual, self).__init__()
self.stride = stride
assert stride in [1, 2]
hidden_dim = int(inp * expand_ratio) # 内部的隐藏层通道都是这个数。
# 只有步长为1且输入和输出通道数一样,才使用残差层,即有shortcut.
# 即残差层不会改变特征图大小和通道数。
self.use_res_connect = self.stride == 1 and inp == oup
if expand_ratio == 1: # dw -> pw_linear
self.conv = nn.Sequential(
# dw
nn.Conv2d(hidden_dim, hidden_dim, 3, stride, 1, groups=hidden_dim, bias=False), # ksize=3, padding=1, 注意group=input_channel,即输入的各个通道分别为一组
nn.BatchNorm2d(hidden_dim),
nn.ReLU6(inplace=True),
# pw-linear
nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
nn.BatchNorm2d(oup),
)
else: # pw -> dw -> pw-linear.
self.conv = nn.Sequential(
# pw
nn.Conv2d(inp, hidden_dim, 1, 1, 0, bias=False), # 先用1x1卷积即pw,放大通道。
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), # 再用1x1线性卷积,缩小通道。
nn.BatchNorm2d(oup),
)
def forward(self, x):
if self.use_res_connect:
return x + self.conv(x)
else:
return self.conv(x)
(1)上面的pw是论中的逐点卷积pointwise Conv,即1x1的卷积;
(2)dw是深度卷积depthwise Conv,即卷积核的通道数是1,计算时是与输入逐通道卷积。
(3)pw_linear是论文中的linear bottleneck,其实就是pw 1x1的卷积后,不用激活函数(非线性),mmclassification也是如此实现。
注意:这个反转残差InvertedResidual结构有多种。
(1)使用隐藏层通道数扩张倍率expand_ratio控制,即是否先用pw,即dw -> pw_linear 或者是pw -> dw -> pw-linear;
(2)只有步长为1且输入和输出通道数一样,才使用残差层,即有shortcut,如下所示,注意输入在底部。当repeated n times,只有第一个往往步长是2,用作下采样,其他必须是1,即后面都是不改变特征图大小和通道数的残差层。这样7层bottlenect每一层都只可能下采样一次。
调用上面的InvertedResidual,实现7层bottleneck.
# building inverted residual blocks
for t, c, n, s in interverted_residual_setting:
output_channel = make_divisible(c * width_mult) if t > 1 else c # 重复t>1次,则通道需先乘以放大倍数,且能被8整除。
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 # 重复多次时,内部隐藏层通道是固定的
在后面3层,分别是conv2d 1x1卷积(conv2d1x1+bn+relu6)、avgpooling、conv2d 1x1卷积分类输出层。
如果mmclassification,用作backbone时,去掉了后面两层。且支持多层输出,输出层可以时bottleneck7层和最后conv2d1x1中的任何一层或者多层。mmclassification代码如下所示。
def forward(self, x):
x = self.conv1(x) # 第一个普通卷积。输出层,不是从这里算起。
outs = [] # 输出层从第一个bottleNeck算起,一共有7个,还有1个conv1x1. 0, 1, 2, 3, 4, 5, 6, 7. 7对应的是conv1x1.
for i, layer_name in enumerate(self.layers):
layer = getattr(self, layer_name)
x = layer(x) # 依次通过每一层。
if i in self.out_indices: # 记录需要输出的层。out_indices: (None or Sequence[int])
outs.append(x)
if len(outs) == 1:
return outs[0]
else:
return tuple(outs)