参考见图片水印。很早之前的一个笔记,当时看了比较多的博客整理的,最近重温轻量化网络结构设计就把笔记迁到语雀上。

一、理论梳理

轻量化网络:MobileNet-V2

1.1 mobileNet v1

参考文献:

最主要的贡献在于深度可分离卷积和点卷积(即1*1的卷积)

深度可分离卷积干的活是:把标准卷积分解成深度卷积(depthwise convolution)和逐点卷积(pointwise convolution)。这么做的好处是可以大幅度降低参数量和计算量。
分解过程示意图如下:

图1.1
image.png
对这个图进行一波分析。输入是D_F*D_F*M时,M表示通道数。
传统卷积的卷积核会设为D_K*D_K*M,一共有N个这样的卷积核,得到最终输出是D_F*D_F*N。计算时也是直接逐个通道对应元素累乘再累加,再将每个通道对应位置进行累加(有时还需加上偏置项)。如图1.2所示。

图1.2
image.png
而深度可分离卷积是这样的,它分成了两个卷积步骤,首先深度卷积,卷积核设置为D_K*D_K*1,一共有M个这样的卷积核,即每个通道对应一个卷积核,此时得到的输出是D_F*D_F*M。再用点卷积,即1*1*M的卷积核,共N个,得到最终输出D_F*D_F*N。如图1.3所示。

图1.3
image.png
再放些好图

图1.4
image.png
这里就是a和b的区别了。3和4是v2的改进点

下面来一波计算。
假设:

  1. 输入特征图大小为 D_F*D_F*M
  2. 输出特征图大小为 D_F*D_F*N
  3. 卷积核大小为 D_K*D_K

那么我们可以计算得到

  1. 传统卷积的计算量为:D_F * D_F * N * D_K * D_K * N
    (道理是:一共有D_F * D_F * N个结果,每个结果需要经过D_K * D_K * N次计算)
    参数量:D_F*D_F*N
  2. 深度可分离卷积第一次卷积的计算量是D_F * D_F * M * D_K * D_K
    第二次卷积的计算量是D_F * D_F * N * M
    总计算量是D_F * D_F * M * D_K * D_K + D_F * D_F * N * M
    参数量:D_K*D_K*1*M + 1*1*M*N

两种卷积方法的计算量差别是:

[MobileNet]MobileNet理解 - 图5

参数量的差别是:

[MobileNet]MobileNet理解 - 图6

以作者论文中 28×28×192 的输入,28×28×256 的输出为例,卷积核大小为 3×3,两者的计算量之比为:

[MobileNet]MobileNet理解 - 图7

深度可分离卷积的计算量缩减为传统卷积的 1/9 左右。

1.2 mobileNet v2

参考文献

v2的创新点:

  1. Inverted residuals,通常的residuals block是先经过一个1*1的Conv layer,把feature map的通道数“压”下来,再经过3*3Conv layer,最后经过一个1*1的Conv layer,将feature map通道数再“扩张”回去。即先“压缩”,最后“扩张”回去。
    (而 inverted residuals就是 先“扩张”,最后“压缩”。为什么这么做呢?请往下看。)
  2. Linear bottlenecks,为了避免Relu对特征的破坏,在residual block的Eltwise sum之前的那个 1*1 Conv 不再采用Relu。(为什么?请往下看。)

用一图说明这两个创新。

图1.5
image.png
MobileNet-V1 最大的特点就是采用depth-wise separable convolution来减少运算量以及参数量,而在网络结构上,没有采用shortcut的方式。
当时Resnet及Densenet等一系列采用shortcut的网络的成功,表明了shortcut是个非常好的东西,于是MobileNet-V2就将这个好东西拿来用。

MobileNet的特点就是depth-wise separable convolution,但是直接把depth-wise separable convolution应用到 residual block中,会碰到如下问题:

  1. DWConv layer层提取得到的特征受限于输入的通道数,若是采用以往的residual block,先“压缩”,再卷积提特征,那么DWConv layer可提取得特征就太少了,因此一开始不“压缩”,MobileNetV2反其道而行,一开始先“扩张”,本文实验“扩张”倍数为6。 通常residual block里面是 “压缩”→“卷积提特征”→“扩张”,MobileNetV2就变成了 “扩张”→“卷积提特征”→ “压缩”,因此称为Inverted residuals
  2. 当采用“扩张”→“卷积提特征”→ “压缩”时,在“压缩”之后会碰到一个问题,那就是Relu会破坏特征。为什么这里的Relu会破坏特征呢?这得从Relu的性质说起,Relu对于负的输入,输出全为零;而本来特征就已经被“压缩”,再经过Relu的话,又要“损失”一部分特征,因此这里不采用Relu,实验结果表明这样做是正确的,这就称为Linear bottlenecks

这时候需要重新把一张图翻出来讲了。
图1.4
image.png

  • 图(a):普通模型架构使用标准卷积将空间和通道信息一起映射到下一层,参数和计算量会比较大
  • 图(b),MobileNetv1中将标准卷积拆分为深度卷积和逐点卷积,深度卷积负责逐通道的过滤空间信息,逐点卷积负责映射通道。将空间和通道分开了
  • 图(c)和图(d)是MobileNetv2的结构(d是c的下一个连接状态),同样是将标准卷积拆分为深度卷积和逐点卷积,在逐点卷积后使用了接1×1卷积,该卷积使用线性变换,总称为一层低维linear bottleneck,其作用是将输入映射回低维空间

映射完成以后使用shortcut,即残差网络
图1.5.1
image.png
一个正经的bottleneck由下图构成

图1.6
image.png

  • 首先是1×1 conv2d变换通道,后接ReLU6激活(ReLU6即最高输出为6,超过了会clip下来)
  • 中间是深度卷积,后接ReLU
  • 最后的1×1 conv2d后面不接ReLU了,而是论文提出的linear bottleneck

针对stride=1和2,有不同的处理方法。为了与shortcut的维度匹配,stride=2时,不采用shortcut。

图1.7
image.png

二、代码梳理

2.1 深度可分离卷积

用pytorch实现比较简单,conv2d中有个参数叫groups,将它设置为通道数即可

2.2 源码

Randl/MobileNetV2-pytorch我的shuffleNet就是看他的代码
tonylins/pytorch-mobilenet-v2

看几个关键block

InvertedResidual

图2.1 网络结构图
image.png
其中:t表示“扩张”倍数,c表示输出通道数,n表示重复次数,s表示步长stride。
先说两点有误之处吧:

  1. 第五行,也就是第7~10个bottleneck,stride=2,分辨率应该从28降低到14;如果不是分辨率出错,那就应该是stride=1;
  2. 文中提到共计采用19个bottleneck,但是这里只有17个。
  1. class InvertedResidual(nn.Module):
  2. # inputSize, outputSize, stride和扩张倍数
  3. def __init__(self, inp, oup, stride, expand_ratio):
  4. super(InvertedResidual, self).__init__()
  5. self.stride = stride
  6. assert stride in [1, 2]
  7. hidden_dim = round(inp * expand_ratio)
  8. # 判断是否需要shortcut
  9. self.use_res_connect = self.stride == 1 and inp == oup
  10. # 如果不需要扩张, 则步骤为:3*3的DWconv->BN->Relu6->1*1的PWConv->BN
  11. if expand_ratio == 1:
  12. self.conv = nn.Sequential(
  13. # dw 深度可分离卷积
  14. # torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)
  15. nn.Conv2d(hidden_dim, hidden_dim, 3, stride, 1, groups=hidden_dim, bias=False),
  16. nn.BatchNorm2d(hidden_dim),
  17. nn.ReLU6(inplace=True),
  18. # pw-linear 点卷积,实现线性变换
  19. # 点卷积不需要padding
  20. nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
  21. nn.BatchNorm2d(oup),
  22. )
  23. # 如果需要扩张,则步骤为图1.7所示
  24. # 1*1 PWconv->BN->Relu6->3*3 DWconv->BN->Relu6->1*1 PWconv->BN
  25. else:
  26. self.conv = nn.Sequential(
  27. # pw
  28. nn.Conv2d(inp, hidden_dim, 1, 1, 0, bias=False),
  29. nn.BatchNorm2d(hidden_dim),
  30. nn.ReLU6(inplace=True),
  31. # dw
  32. nn.Conv2d(hidden_dim, hidden_dim, 3, stride, 1, groups=hidden_dim, bias=False),
  33. nn.BatchNorm2d(hidden_dim),
  34. nn.ReLU6(inplace=True),
  35. # pw-linear
  36. nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
  37. nn.BatchNorm2d(oup),
  38. )
  39. def forward(self, x):
  40. if self.use_res_connect:
  41. return x + self.conv(x)
  42. else:
  43. return self.conv(x)

另一种写法。可以发现两者是有区别的。上面的方法单独考虑了扩张倍数是1,上文的做法是3*3的DWconv->BN->Relu6->1*1的PWConv->BN
而这里的做法没考虑,还是普遍做法,1*1 PWconv->BN->Relu6->3*3 DWconv->BN->Relu6->1*1 PWconv->BN
原因是因为在MobileNet的主函数里面,一个会单独建Module,另一个把它作为通用模块

  1. class LinearBottleneck(nn.Module):
  2. # inputSize, outputSize, stride, t是扩张倍数,激活函数
  3. def __init__(self, inplanes, outplanes, stride=1, t=6, activation=nn.ReLU6):
  4. super(LinearBottleneck, self).__init__()
  5. self.conv1 = nn.Conv2d(inplanes, inplanes * t, kernel_size=1, bias=False)
  6. self.bn1 = nn.BatchNorm2d(inplanes * t)
  7. self.conv2 = nn.Conv2d(inplanes * t, inplanes * t, kernel_size=3, stride=stride, padding=1, bias=False,
  8. groups=inplanes * t)
  9. self.bn2 = nn.BatchNorm2d(inplanes * t)
  10. self.conv3 = nn.Conv2d(inplanes * t, outplanes, kernel_size=1, bias=False)
  11. self.bn3 = nn.BatchNorm2d(outplanes)
  12. self.activation = activation(inplace=True)
  13. self.stride = stride
  14. self.t = t
  15. self.inplanes = inplanes
  16. self.outplanes = outplanes
  17. def forward(self, x):
  18. residual = x
  19. # 1*1 PWconv->BN->Relu6
  20. out = self.conv1(x)
  21. out = self.bn1(out)
  22. out = self.activation(out)
  23. # 3*3 DWconv->BN->Relu6
  24. out = self.conv2(out)
  25. out = self.bn2(out)
  26. out = self.activation(out)
  27. # 1*1 PWconv->BN
  28. out = self.conv3(out)
  29. out = self.bn3(out)
  30. # 根据stride决定是否需要残差
  31. if self.stride == 1 and self.inplanes == self.outplanes:
  32. out += residual
  33. return out

MobileNetV2

下文中出现了一个参数width_mult,这是什么呢~让我们重温这张图
图2.1 网络结构图
image.png
注意最下面一行的k了吗?MobileNetV1中提出了宽度缩放因子,其作用是在整体上对网络的每一层维度(特征数量)进行瘦身。MobileNetV2中,当该因子<1时,最后的那个1*1conv不进行宽度缩放;否则进行宽度缩放。

  1. class MobileNetV2(nn.Module):
  2. # 输出类别,输入尺寸,宽度缩放因子
  3. def __init__(self, n_class=1000, input_size=224, width_mult=1.):
  4. super(MobileNetV2, self).__init__()
  5. block = InvertedResidual
  6. # 这两个数据是从论文中来的,如图2.1
  7. input_channel = 32
  8. last_channel = 1280
  9. # 根据作者的论文数据来的,如图2.1
  10. interverted_residual_setting = [
  11. # t, c, n, s
  12. [1, 16, 1, 1],
  13. [6, 24, 2, 2],
  14. [6, 32, 3, 2],
  15. [6, 64, 4, 2],
  16. [6, 96, 3, 1],
  17. [6, 160, 3, 2],
  18. [6, 320, 1, 1],
  19. ]
  20. # building first layer
  21. assert input_size % 32 == 0
  22. input_channel = int(input_channel * width_mult)
  23. self.last_channel = int(last_channel * width_mult) if width_mult > 1.0 else last_channel
  24. self.features = [conv_bn(3, input_channel, 2)]
  25. # building inverted residual blocks
  26. for t, c, n, s in interverted_residual_setting:
  27. # t表示“扩张”倍数,c表示输出通道数,n表示重复次数,s表示步长stride。
  28. output_channel = int(c * width_mult)
  29. # 这一部分的意思是先按s来一个,剩下的stride都是1
  30. for i in range(n):
  31. if i == 0:
  32. self.features.append(block(input_channel, output_channel, s, expand_ratio=t))
  33. else:
  34. self.features.append(block(input_channel, output_channel, 1, expand_ratio=t))
  35. input_channel = output_channel
  36. # building last several layers
  37. # 在多个bottleneck后有一个pw conv, 具体包括1*1conv->BN->relu6
  38. self.features.append(conv_1x1_bn(input_channel, self.last_channel))
  39. # make it nn.Sequential
  40. # 之前的self.features里面存的是list,现在要把它改成Sequential
  41. # 问题:为什么要有这个*不是很理解
  42. self.features = nn.Sequential(*self.features)
  43. # building classifier
  44. # 问题:整套流程下来好像少了一个Avgpool
  45. # 问题已解决,AvgPool用自己的方式实现在forward里了
  46. self.classifier = nn.Sequential(
  47. nn.Dropout(0.2),
  48. nn.Linear(self.last_channel, n_class),
  49. )
  50. self._initialize_weights()
  51. def forward(self, x):
  52. x = self.features(x)
  53. x = x.mean(3).mean(2)
  54. x = self.classifier(x)
  55. return x
  56. def _initialize_weights(self):
  57. for m in self.modules():
  58. if isinstance(m, nn.Conv2d):
  59. n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
  60. m.weight.data.normal_(0, math.sqrt(2. / n))
  61. if m.bias is not None:
  62. m.bias.data.zero_()
  63. elif isinstance(m, nn.BatchNorm2d):
  64. m.weight.data.fill_(1)
  65. m.bias.data.zero_()
  66. elif isinstance(m, nn.Linear):
  67. n = m.weight.size(1)
  68. m.weight.data.normal_(0, 0.01)
  69. m.bias.data.zero_()
  1. class MobileNet2(nn.Module):
  2. """MobileNet2 implementation.
  3. """
  4. # scale:缩放因子,t是扩张倍数
  5. def __init__(self, scale=1.0, input_size=224, t=6, in_channels=3, num_classes=1000, activation=nn.ReLU6):
  6. """
  7. MobileNet2 constructor.
  8. :param in_channels: (int, optional): number of channels in the input tensor.
  9. Default is 3 for RGB image inputs.
  10. :param input_size:
  11. :param num_classes: number of classes to predict. Default
  12. is 1000 for ImageNet.
  13. :param scale:
  14. :param t:
  15. :param activation:
  16. """
  17. super(MobileNet2, self).__init__()
  18. self.scale = scale
  19. self.t = t
  20. self.activation_type = activation
  21. self.activation = activation(inplace=True)
  22. self.num_classes = num_classes
  23. self.num_of_channels = [32, 16, 24, 32, 64, 96, 160, 320]
  24. # assert (input_size % 32 == 0)
  25. # 这里的_make_divisible和shuffleNet V2里的辅助函数一致,都是为了让输入能被分组数整除
  26. self.c = [_make_divisible(ch * self.scale, 8) for ch in self.num_of_channels]
  27. self.n = [1, 1, 2, 3, 4, 3, 3, 1]
  28. self.s = [2, 1, 2, 2, 2, 1, 2, 1]
  29. # 这里完成了bottleNeck之前的结构:conv->BN
  30. self.conv1 = nn.Conv2d(in_channels, self.c[0], kernel_size=3, bias=False, stride=self.s[0], padding=1)
  31. self.bn1 = nn.BatchNorm2d(self.c[0])
  32. # 这里完成了bottleNeck
  33. self.bottlenecks = self._make_bottlenecks()
  34. # Last convolution has 1280 output channels for scale <= 1
  35. self.last_conv_out_ch = 1280 if self.scale <= 1 else _make_divisible(1280 * self.scale, 8)
  36. self.conv_last = nn.Conv2d(self.c[-1], self.last_conv_out_ch, kernel_size=1, bias=False)
  37. self.bn_last = nn.BatchNorm2d(self.last_conv_out_ch)
  38. # AdaptiveAvgPool2d全局池化的作用
  39. self.avgpool = nn.AdaptiveAvgPool2d(1)
  40. self.dropout = nn.Dropout(p=0.2, inplace=True) # confirmed by paper authors
  41. self.fc = nn.Linear(self.last_conv_out_ch, self.num_classes)
  42. self.init_params()
  43. def init_params(self):
  44. for m in self.modules():
  45. if isinstance(m, nn.Conv2d):
  46. init.kaiming_normal_(m.weight, mode='fan_out')
  47. if m.bias is not None:
  48. init.constant_(m.bias, 0)
  49. elif isinstance(m, nn.BatchNorm2d):
  50. init.constant_(m.weight, 1)
  51. init.constant_(m.bias, 0)
  52. elif isinstance(m, nn.Linear):
  53. init.normal_(m.weight, std=0.001)
  54. if m.bias is not None:
  55. init.constant_(m.bias, 0)
  56. def _make_stage(self, inplanes, outplanes, n, stride, t, stage):
  57. modules = OrderedDict()
  58. stage_name = "LinearBottleneck{}".format(stage)
  59. # First module is the only one utilizing stride
  60. first_module = LinearBottleneck(inplanes=inplanes, outplanes=outplanes, stride=stride, t=t,
  61. activation=self.activation_type)
  62. modules[stage_name + "_0"] = first_module
  63. # add more LinearBottleneck depending on number of repeats
  64. for i in range(n - 1):
  65. name = stage_name + "_{}".format(i + 1)
  66. module = LinearBottleneck(inplanes=outplanes, outplanes=outplanes, stride=1, t=6,
  67. activation=self.activation_type)
  68. modules[name] = module
  69. return nn.Sequential(modules)
  70. # 对于BottleNec,第一层BottleNec单独写了,之后的用了一个循环,扩张倍数默认是6
  71. def _make_bottlenecks(self):
  72. modules = OrderedDict()
  73. stage_name = "Bottlenecks"
  74. # First module is the only one with t=1
  75. bottleneck1 = self._make_stage(inplanes=self.c[0], outplanes=self.c[1], n=self.n[1], stride=self.s[1], t=1,
  76. stage=0)
  77. modules[stage_name + "_0"] = bottleneck1
  78. # add more LinearBottleneck depending on number of repeats
  79. for i in range(1, len(self.c) - 1):
  80. name = stage_name + "_{}".format(i)
  81. module = self._make_stage(inplanes=self.c[i], outplanes=self.c[i + 1], n=self.n[i + 1],
  82. stride=self.s[i + 1],
  83. t=self.t, stage=i)
  84. modules[name] = module
  85. return nn.Sequential(modules)
  86. def forward(self, x):
  87. x = self.conv1(x)
  88. x = self.bn1(x)
  89. x = self.activation(x)
  90. x = self.bottlenecks(x)
  91. x = self.conv_last(x)
  92. x = self.bn_last(x)
  93. x = self.activation(x)
  94. # average pooling layer
  95. x = self.avgpool(x)
  96. x = self.dropout(x)
  97. # flatten for input to fully-connected layer
  98. x = x.view(x.size(0), -1)
  99. x = self.fc(x)
  100. return F.log_softmax(x, dim=1) #TODO not needed(?)