【论文笔记】轻量级网络MobileNet


文中代码可能是错误示范,如需代码及论文,移步gitee

1. 论文

V1

主要思想:深度可分离卷积

V2

论文第3节

3.1Depthwise Separable Convolutions,讲述了深度可分离卷积由两部分组成(1)3x3 depthwise convolution (2)1x1 convolution。如果输入为h*w*d的张量,卷积核为k*k*d*n,产生的输出为h*w*n。标准卷积层的计算量为h*w*k*k*d*n,而深度可分离卷积的计算量为h*w*k*k*d+h*w*1*1*d*n=h*w*d*(k2+n);两者相差k2倍,k通常取3,因此计算量约为之前的1/9。

3.2 Linear Bottlenecks,发现当维度n很小的时候,后面接ReLU非线性变换的话会导致很多信息的丢失。使用linear bottleneck(即不使用ReLU激活,做了线性变换)的来代替原本的非线性激活变换。

3.3 Inverted residuals,借鉴resnet,先用1x1卷积,然后3x3卷积,再1x1卷积,同时输入输出shortcut连接。不用的是,mobilenet的3x3卷积是dw卷积,且1x1卷积时,是升维的。如下图所以,经过1x1卷积后,维度是之前的t倍。1x1卷积和dw卷积后都接relu,最后的1x1降维时,接线性变换。

V3

5.1 Redesigning Expensive Layers

重新设计两个地方花费比较大的地方:

1)V2最后用了1x1的卷积来增加维度,为了拥有更多的特征,但是会导致额外的延迟。(说的是bottleneck中3x3之后的那个1x1)将其放在avg pooling的后面,首先利用avg pooling将特征图大小由7x7降到了1x1,然后再利用1x1提高维度,这样就减少了7x7=49倍的计算量。删掉3x3以及1x1后,精度并没有得到损失。

2)最开始的地方用了32个3x3的卷积核来做最初的边缘检测。我们改为用16个,并且用hard swish的非线性层。

5.2 修改了swish非线性层

 swish提高了精度,但对于移动设备sigmoid带来的计算量多很多。对于这个问题, 采取了以下两个操作:

  • 用ReLU6(x+3)/6替换了sigmoid:

1)ReLU6可以在任何软硬件平台进行计算,

2)量化的时候,它消除了由于近似sigmoid的不同实现而导致的潜在数值精度损失。

3)在实践中,h-swish可以作为一个分段函数来实现,以减少内存访问的数量,从而大幅降低延迟成本。

  • 只在下半部分使用h-swish

随着我们深入网络,应用非线性的成本会降低,因为每层激活内存通常在分辨率下降时减半。我们发现swish的大部分好处都是通过在更深层次上使用它们来实现的。因此,在我们的架构中,我们只在下半部分使用h-swish。

5.3 引入squeeze-and-excite bottleneck

因为SE结构会消耗一定的时间,所以作者在含有SE的结构中,将expansion layer的channel变为原来的1/4,这样既提高了精度,同时还没有增加时间消耗。

5.4. MobileNetV3 Definitions

MobileNetV3被定义为两种型号:MobileNetV3-large和MobileNetV3-small,分别针对高资源和低资源用例。这些模型是通过应用支持平台的NAS和NetAdapt进行网络搜索并结合本节中定义的网络改进而创建的。

2. V1代码实现:

2.1 深度可分离卷积

class Block(nn.Module):
    '''Depthwise conv + Pointwise conv'''
    def __init__(self,input_channels,output_channels,stride=1):
        super(Block,self).__init__()
        self.conv1 = nn.Conv2d\
            (input_channels,input_channels,kernel_size=3,stride=stride,
             padding=1,groups=input_channels,bias=False)
        self.bn1 = nn.BatchNorm2d(input_channels)
        self.conv2=nn.Conv2d(input_channels,output_channels,kernel_size=1,
                             stride=1,padding=0,bias=False)
        self.bn2=nn.BatchNorm2d(output_channels)
    def forward(self,x):
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        return x
  • Depthwise conv不改变通道数,outout_channels = input_channels, groups = input_channels。相当于对卷积的通道数进行分组,然后对每组的特征图分别进行卷积,是组卷积(group convolution)的一种扩展,每组只有一个特征图。

2.2 13个blocks组合:

...//cfg存放(输出通道数,stride)
cfg = [64, (128,2), 128, (256,2), 256, (512,2),
           512, 512, 512, 512, 512, (1024,2), 1024]
...
def _make_layers(self,input_channels):
  layers = []
  for x in self.cfg:
    output_channels = x if isinstance(x,int) else x[0]
    stride = 1 if isinstance(x,int) else x[1]
    layers.append(Block(input_channels,output_channels,stride))
    input_channels=output_channels
    return nn.Sequential(*layers)        
  • isinstance的用法:https://www.runoob.com/python/python-func-isinstance.html
  • 对于使用深度可分离卷积后的计算速度:https://zhuanlan.zhihu.com/p/80177088

 3. V2代码实现

3.1 bottleneck

class Bottleneck(nn.Module):
    def __init__(self,input_channels,output_channels,stride=1,expansion_ratio=1):
        super(Bottleneck,self).__init__()
        self.stride = stride
        temp_channels = input_channels*expansion_ratio
        self.conv1 = nn.Conv2d(input_channels,temp_channels,kernel_size=1,bias=False)
        self.bn1 = nn.BatchNorm2d(temp_channels)
        self.conv2 = nn.Conv2d(temp_channels,temp_channels,padding=1,groups=temp_channels,kernel_size=3,stride=stride,bias=False)
        self.conv3 = nn.Conv2d(temp_channels,output_channels,kernel_size=1,bias=False)
        self.bn2 = nn.BatchNorm2d(output_channels)
        if(stride==1 and input_channels!=output_channels):
            self.shortcut = nn.Conv2d(in_channels=input_channels,out_channels=output_channels,kernel_size=1)
        if(stride==1 and input_channels==output_channels):
            self.shortcut = nn.Sequential()
    def forward(self,x):
        output = F.relu(self.bn1(self.conv1(x)))
        output = F.relu(self.bn1(self.conv2(output)))
        output = self.bn2(self.conv3(output))
        if(self.stride==1):
            output = output+self.shortcut(x)
        return output

Tips:

1. 在stride=2时,不需要将输入与输出shortcut

2. 输入和输出的维数不同,无法叠加,可以用一个卷积改变输入通道的维数。(这是公认的方法吗?

3. 上面每一个卷积后都要加BN层(BN层添加在激活函数前,对输入激活函数的输入进行归一化。这样解决了输入数据发生偏移和增大的影响。

4. 关于nn.Conv2d的bias参数是false还是true?(卷积之后,如果要接BN操作,最好是不设置偏置,因为不起作用,而且占显卡内存。

3.2 多个bottleneck叠加

 Tips:表中的n是该bottleneck重复的次数,如果重复多次,且stride=2的话,只有第一次的stride为2,以后都为1。

class MobileNetV2(BasicModule):
    cfg =  [(24,2),24,
            (32,2),32,32,
            (64,2),64,64,64,
            96,96,96,
            (160,2),160,160,
            320]
    def __init__(self,num_classes):
        super(MobileNetV2, self).__init__()
        self.model_name = "MobileNetV2"
        self.conv1 = nn.Conv2d(in_channels=1,out_channels=32,kernel_size=3,stride=2,padding=1)
        self.bottle = Bottleneck(input_channels=32,output_channels=16)
        self.layer = self._make_layers(input_channels=16)
        self.conv2 = nn.Conv2d(in_channels=320,out_channels=1280,kernel_size=1)
        self.conv3 = nn.Conv2d(in_channels=1280,out_channels=256,kernel_size=1)
        self.linear = nn.Linear(256,num_classes)
        self.bn1 = nn.BatchNorm2d(32)
    def _make_layers(self,input_channels):
        layers = []
        for x in self.cfg:
            output_channels = x if isinstance(x,int) else x[0]
            stride = 1 if isinstance(x,int) else x[1]
            layers.append(Bottleneck(input_channels,output_channels,stride=stride,expansion_ratio=6))
            input_channels = output_channels
        return nn.Sequential(*layers)
    def forward(self,x):
        x = F.relu(self.bn1(self.conv1(x)))
        x = self.bottle(x)
        x = self.layer(x)
        x = F.relu(self.conv2(x))
        x = F.avg_pool2d(x,7)
        x = self.conv3(x)
        x = x.view(x.size(0), -1)
        x = self.linear(x)
        return x

参考文献:

1. 

2. 轻量级模型:MobileNet V2 详解MobileNetV2