【卷积神经网络系列】十三、MobileNetV3

目录

  • 参考资料:
  • 一、简介
  • 二、模型优化细节
    • 2.1 引入SE模块
    • 2.2 重新设计激活函数
    • 2.3 重新设计耗时层结构
  • 三、网络结构
  • 四、论文复现
    • (1)确保Channel个数能被8整除
    • (2)定义h-swish激活函数:
    • (3)定义SE模块:
    • (4)引入SE模块后的倒残差模块bneck:
    • (5)主体网络结构:(MobileNetV3-Large)


参考资料:

论文:

  MobileNetV3:Searching for MobileNetV3

博客:

  MobileNetv3网络详解

  MobileNetV3网络结构详解

  MobileNetV3论文理解,以及tensorflow、pytorch相关代码

  MobileNetV3论文翻译

  MobileNetV3论文讲解


一、简介

整体来说MobileNetV3有两大创新点:

(1)互补搜索技术组合
 使用Platform-Aware Neural Architecture Search(NAS)来形成网络结构,并利用NetAdapt技术进一步筛选网络层结构。

(2)网络结构改进

  • block结构发生改变,引入了Squeeze-and-Excitation block(SENet的机制)
  • 算法内部微结构变化,把部分ReLU6使用hard-swish替换,把全部sigmoid使用hard-sigmoid替换;

如下图所示是MobieNetV2的Block:

  • 首先会通过一个1x1卷积层来进行升维处理,在卷积后会跟有BNReLU6激活函数;
  • 紧接着是一个3x3大小DW卷积,卷积后面依旧会跟有BNReLU6激活函数;
  • 最后一个卷积层是1x1卷积,起到降维作用,注意卷积后只跟了BN结构,并没有使用ReLU6激活函数;
  • 网络还存在一个捷径分支shotcut,将我们输入特征矩阵与输出特征矩阵在相同维度的数值上进行相加操作。并且只有DW卷积的步距为1,且input_channel==output_channel,才有shotcut连接。

【卷积神经网络系列】十三、MobileNetV3_第1张图片

下图是MobieNetV3的Block:

  • 加入了SE模块(注意力机制)
  • 更新了激活函数

【卷积神经网络系列】十三、MobileNetV3_第2张图片


二、模型优化细节

2.1 引入SE模块

SE(Squeeze-and-Excitation) 模块类似于一个注意力模块,MobileNetV3版本中SE模块加在了bottleneck结构的内部,在深度卷积后增加SE块,scale操作后再做逐点卷积,如下图所示。

 MobileNetV3版本的SERadio系数为0.25。

【卷积神经网络系列】十三、MobileNetV3_第3张图片

 对得到的特征矩阵,对每个channel进行池化处理,接下来通过两个全连接层得到输出的向量,其中第一个全连接层,它的全连接层节点数是等于输入特征矩阵channel乘以SERadio系数(0.25),第二个全连接层的channel是与我们特征矩阵的channel保持一致的。
经过平均池化+两个全连接层,输出的特征向量可以理解为是对SE之前的特征矩阵的每一个channel分析出了一个权重关系,它认为比较重要的channel会赋予一个更大的权重,对于不是那么重要的channel维度上对应一个比较小的权重

【卷积神经网络系列】十三、MobileNetV3_第4张图片


2.2 重新设计激活函数

 之前在v2版本我们基本都是使用ReLU6激活函数,ReLu6激活函数如下图所示,相当于限制最大值为6。
R e L U 6 ( x ) = m i n ( m a x ( x , 0 ) , 6 ) ReLU6(x)=min(max(x,0),6) ReLU6(x)=min(max(x,0),6)
【卷积神经网络系列】十三、MobileNetV3_第5张图片

 现在比较常用的激活函数叫swish激活函数:
s w i t h x = x σ ( x ) swithx=xσ(x) swithx=xσ(x)
 其中 σ \sigma σ 的计算公式如下:
σ ( x ) = 1 1 + e − x σ(x)={\frac{1}{1+e^{−x}}} σ(x)=1+ex1
 使用switch激活函数之后,确实能够提高网络的准确率,但是也存在2个问题:

  • 计算、求导复杂;
  • 对量化过程不友好(对移动端设备,基本上为了加速都会对它进行量化操作);

 由于存在这个问题,作者就提出了h-switch激活函数,在讲h-switch激活函数之前我们来讲一下h-sigmoid激活函数,h-sigmoid激活函数是在ReLU6激活函数上进行修改的:
h − s i g m o i d = R e L U 6 ( x + 3 ) 6 h−sigmoid={\frac{ReLU6(x+3)}{6}} hsigmoid=6ReLU6(x+3)
【卷积神经网络系列】十三、MobileNetV3_第6张图片

 从图中可以看出h-sigmoid与sigmoid激活函数比较接近,所以在很多场景中会使用h-sigmoid激活函数去替代我们的sigmoid激活函数。因此h-switch中 σ \sigma σ 替换为h-sigmoid之后,函数的形式如下:
h − s w i t c h [ x ] = x R e L U 6 ( x + 3 ) 6 h−switch[x]=x{\frac{ReLU6(x+3)}{6}} hswitch[x]=x6ReLU6(x+3)


2.3 重新设计耗时层结构

 论文首先使用了神经网络搜索功能(NAS)来构建全局的网络结构,随后利用了NetAdapt算法来对每层的核数量进行优化,在尽量优化模型延时的同时保持精度,减小扩充层和每一层中瓶颈的大小。

(1)减少第一个卷积层的卷积个数(32 -> 16)

 在v1,v2版本第一层卷积核个数都是32的,在v3版本中我们只使用了16个,作者说将卷积核Filter个数从32变为16之后,它的准确率是和32是一样的,既然准确率没有影响,使用更少的卷积核计算量就变得更小了。这里节省了大概2ms的运算时间。

(2)精简 Last Stage

 在使用NAS搜索出来的网络结构的最后一部分,叫做Original last Stage,它的网络结构如下:

【卷积神经网络系列】十三、MobileNetV3_第7张图片

 该网络是主要是通过堆叠卷积而来的,作者在使用过程中发现这个Original Last Stage是一个比较耗时的部分,作者就针对该结构进行了精简,提出了一个Efficient Last Stage

【卷积神经网络系列】十三、MobileNetV3_第8张图片

Efficient Last Stage相比之前的Original Last Stage,少了很多卷积层,作者发现更新网络后准确率是几乎没有变化的,但是节省了7ms的执行时间。这7ms占据了推理11%的时间,因此使用Efficient Last Stage之后,对我们速度提升还是比较明显的。


三、网络结构

 作者针对不同需求,通过NAS得到两种结构,一个是MobilenetV3-Large,结构如下图:

【卷积神经网络系列】十三、MobileNetV3_第9张图片

简单看下表中各参数的含义:

  • input 表示输入层特征矩阵的维度;

  • out代表的输出特征矩阵的channel,我们说过在v3版本中第一个卷积核使用的是16个卷积核;

  • SE表示是否使用注意力机制,只要表格中标所对应的bneck结构才会使用我们的注意力机制;

  • NL代表的是激活函数,其中HS代表的是hard switch激活函数,RE代表的是ReLU6激活函数;

  • s代表的DW卷积的步距stride

  • NBN 表示这卷积不使用BN结构,最后两个卷积相conv2d 1x1当于全连接的作用;

  • exp size代表的是第一个升维的卷积,我们要将维度升到多少,exp size多少,我们就用第一层1x1卷积升到多少维;

第一个bneck结构,这里有一点比较特殊,它的exp size和输入的特征矩阵channel是一样的,本来bneck中第一个卷积起到升维的作用,但这里并没有升维。所以在实现过程中,第一个bneck结构是没有1x1卷积层的,它是直接对我们特征矩阵进行DW卷积处理。

对于shortcut捷径分支,必须是DW卷积的步距为1,且bneckinput_channel=output_channel才有shortcut连接。

 另一个是MobilenetV3-Small,结构如下图:

【卷积神经网络系列】十三、MobileNetV3_第10张图片


四、论文复现

参考

  【MobileNetV3】MobileNetV3网络结构详解

  PyTorch深度可分离卷积与MobileNet-v3


(1)确保Channel个数能被8整除

# ------------------------------------------------------#
#   这个函数的目的是确保Channel个数能被8整除。
#	很多嵌入式设备做优化时都采用这个准则
# ------------------------------------------------------#
def _make_divisible(v, divisor, min_value=None):
    if min_value is None:
        min_value = divisor
    # int(v + divisor / 2) // divisor * divisor:四舍五入到8
    new_v = max(min_value, int(v + divisor / 2) // divisor * divisor)
    # Make sure that round down does not go down by more than 10%.
    if new_v < 0.9 * v:
        new_v += divisor
    return new_v

(2)定义h-swish激活函数:

h − s w i t c h [ x ] = x R e L U 6 ( x + 3 ) 6 h−switch[x]=x{\frac{ReLU6(x+3)}{6}} hswitch[x]=x6ReLU6(x+3)

class Hswish(nn.Module):
    def __init__(self):
        super(Hswish, self).__init__()
        self.relu6=nn.ReLU6(inplace=True)
        
    def forward(self, x):
        return x*self.relu6(x+3)/6

(3)定义SE模块:

  • 首先对特征图进行平均池化(无论此时的特征图大小是多少,都池化为1x1的大小);

  • 然后接两个全连接层(conv2d 1x1),第一个全连接层的激活函数为ReLU6,输出通道采用输入通道数的1/4,第二个全连接层的激活函数采用H-swish,输出通道采用设定的输出通道大小

【卷积神经网络系列】十三、MobileNetV3_第11张图片

class SE_Block(nn.Module):                         # Squeeze-and-Excitation block
    def __init__(self, in_channels):
        super(SE_Block, self).__init__()
        # 保证通道数是 8 的倍数,原因是:适配于硬件优化加速
        mid_channel = _make_divisible(in_channels // 4, 8)
        
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))     # GAP
        self.conv1 = nn.Conv2d(in_channels, mid_channel, kernel_size=1)   # 1x1的卷积核充当FC
        self.relu = nn.ReLU6()
        self.conv2 = nn.Conv2d(mid_channel, in_channels, kernel_size=1)   # 1x1的卷积核充当FC
        self.hswish = Hswish()

    def forward(self, x):
        x = self.avgpool(x)
        x = self.conv1(x)
        x = self.relu(x)
        x = self.conv2(x)
        out = self.hswish(x)
        return out

(4)引入SE模块后的倒残差模块bneck:

class SEInvertedBlock(nn.Module):
    def __init__(self,in_channels, exp_size, out_channels, 
                 kernel_size, stride, activate=1, use_se=1):
        super(SEInvertedBlock, self).__init__()
        self.use_se = use_se
        self.stride = stride
        self.NL = nn.ReLU6(inplace=True) if activate else Hswish()
        
        # 1x1 升维
        self.conv1=nn.Sequential(
            nn.Conv2d(in_channels, exp_size, kernel_size=1, stride=1),
            nn.BatchNorm2d(exp_size),
            self.NL
        )
        
        # dw卷积
        self.dwconv=nn.Sequential(
            nn.Conv2d(in_channels=exp_size, out_channels=exp_size, 
                      kernel_size=kernel_size, stride=stride,
                      padding=kernel_size//2, groups=exp_size),
            nn.BatchNorm2d(exp_size),
            self.NL
        )
        
        # 引入SE模块
        if self.use_se:
            self.SE=SE_Block(exp_size)

        # 1x1 降维(不激活)
        self.conv3=nn.Sequential(
            nn.Conv2d(exp_size, out_channels,kernel_size=1, stride=1),
            nn.BatchNorm2d(out_channels)
        )
        
        # 只有同时满足两个条件时,才使用短连接
        self.use_res_connect = (self.stride == 1 and in_channels == out_channels)
        
    def forward(self,x):
        out=self.conv1(x)       # 1x1卷积核升维     
        out=self.dwconv(out)    # 3x3dw卷积       
        if self.use_se:         # 是否引入SE模块,引入的话需要计算SE-Block分支的输出
            SE_out=self.SE(out)
            out = out * SE_out  # 对位相乘,重新加权   
        out=self.conv3(out)     # 1x1卷积核降维        
        if self.use_res_connect:# 是否需要残差连接(stride==1&&in==out时)
            return x + out
        else:
            return out    

(5)主体网络结构:(MobileNetV3-Large)

  • input 表示输入层特征矩阵的维度;
  • out代表的输出特征矩阵的channel,我们说过在v3版本中第一个卷积核使用的是16个卷积核;
  • SE表示是否使用注意力机制,只要表格中标所对应的bneck结构才会使用我们的注意力机制;
  • NL代表的是激活函数,其中HS代表的是hard switch激活函数,RE代表的是ReLU6激活函数;
  • s代表的DW卷积的步距stride
  • NBN 表示这卷积不使用BN结构,最后两个卷积相conv2d 1x1当于全连接的作用;
  • exp size代表的是第一个升维的卷积,我们要将维度升到多少,exp size多少,我们就用第一层1x1卷积升到多少维;

【卷积神经网络系列】十三、MobileNetV3_第12张图片

class MobileNetV3(nn.Module):
    def __init__(self, num_classes=1000, width_mult=1.0, 
                 inverted_residual_setting=None, round_nearest=8):
        """
        MobileNet V3 main class
        Args:
            num_classes (int): Number of classes
            width_mult (float): Width multiplier - adjusts number of channels in each layer by this amount
            inverted_residual_setting: Network structure
            round_nearest (int): Round the number of channels in each layer to be a multiple of this number
                                 Set to 1 to turn off rounding
        """
        super(MobileNetV3, self).__init__()
        
        # 保证通道数是 8 的倍数,原因是:适配于硬件优化加速
        input_channel = _make_divisible(16 * width_mult, round_nearest)
        last_channel = _make_divisible(960 * width_mult, round_nearest)

        if inverted_residual_setting is None:
            inverted_residual_setting = [
                # kernel_size, exp_size, out, SE, NL, s
                [3, 16, 16, 0, 0, 1],   # 112x112x16 -> 112x112x16
                [3, 64, 24, 0, 0, 2],   # 112x112x16 -> 56x56x24
                [3, 72, 24, 0, 0, 1],   # 56x56x24 -> 56x56x24
                [5, 72, 40, 1, 0, 2],   # 56x56x24 -> 28x28x40
                [5, 120, 40, 1, 0, 1],  # 28x28x40 -> 28x28x40
                [5, 120, 40, 1, 0, 1],  # 28x28x40 -> 28x28x40
                [3, 240, 80, 0, 1, 2],  # 28x28x40 -> 14x14x80
                [3, 200, 80, 0, 1, 1],  # 14x14x80 -> 14x14x80
                [3, 184, 80, 0, 1, 1],  # 14x14x80 -> 14x14x80
                [3, 184, 80, 0, 1, 1],  # 14x14x80 -> 14x14x80
                [3, 480, 112, 1, 1, 1], # 14x14x80 -> 14x14x112
                [3, 672, 112, 1, 1, 1], # 14x14x112 -> 14x14x112
                [5, 672, 160, 1, 1, 2], # 14x14x112 -> 7x7x160
                [5, 960, 160, 1, 1, 1], # 7x7x160 -> 7x7x160
                [5, 960, 160, 1, 1, 1]  # 7x7x160 -> 7x7x160
            ]

        # 检查传入的配置是否正确
        if len(inverted_residual_setting) == 0 or len(inverted_residual_setting[0]) != 6:
            raise ValueError("inverted_residual_setting should be non-empty "
                             "or a 6-element list, got {}".format(inverted_residual_setting))

		# conv_first layer
        # 224,224,3 -> 112,112,16
        self.conv_first = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=input_channel,
                      kernel_size=3, stride=2, padding=1),
            nn.BatchNorm2d(input_channel),
            Hswish()
        )
        # 加入features列表
        features = [self.conv_first]
        
        # building inverted residual blocks
        # kernel_size:代表卷积核大小
        # exp_size:代表的是第一个1x1升维的卷积,我们要将维度升到多少;
        # out:代表该层输出特征矩阵的channel;
        # SE:表示是否使用注意力机制,只要表格中标`√`所对应的`bneck`结构才会使用我们的注意力机制;
        # NL:代表的是激活函数,其中`HS`代表的是`hard switch`激活函数,`RE`代表的是`ReLU6`激活函数;
        # s:代表的`DW`卷积的步距`stride`;
        for kernel_size, exp_size, out, SE, NL, s in inverted_residual_setting:
            # 各层的输出通道数
            output_channel = _make_divisible(out * width_mult, round_nearest)
            
            # SEInvertedBlock函数
            features.append(SEInvertedBlock(in_channels=input_channel, exp_size=exp_size,
                                  out_channels=out, kernel_size=kernel_size,
                                  stride=s, activate=NL, use_se=SE))
            
            # 这一层的输出通道数作为下一层的输入通道数
            input_channel = output_channel	

        # conv_last layer
        self.conv_last = nn.Sequential(
            nn.Conv2d(in_channels=input_channel, out_channels=last_channel, kernel_size=1),
            nn.BatchNorm2d(last_channel),
            Hswish()
        )
        # 加入features列表
        features.append(self.conv_last)
        
        # *features表示位置信息,将特征层利用nn.Sequential打包成一个整体
        self.features = nn.Sequential(*features)

        # building classifier
        # 自适应平均池化下采样层,输出矩阵高和宽均为1
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))	
        self.classifier = nn.Sequential(
            nn.Linear(in_features=last_channel, out_features=1280),
            Hswish(), 
            nn.Linear(in_features=1280, out_features=num_classes)
            # 以下方式会出错,1维不能用2d卷积?
            # nn.Conv2d(in_channels=last_channel, out_channels=1280, kernel_size=1),
            # Hswish(), 
            # nn.Conv2d(in_channels=1280, out_channels=num_classes, kernel_size=1),
        )

    def forward(self, x):
        x = self.features(x)
        x = self.avgpool(x)
        x = torch.flatten(x, 1)		# 展平处理
        x = self.classifier(x)
        return x
def test():
    # ------------------------------------#
    # 方法1 获取计算量与参数量
    # ------------------------------------#
    net = MobileNetV3()
    #创建模型,部署gpu
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    net.to(device)
    summary(net, (3, 224, 224))

if __name__ == '__main__':
    test()

你可能感兴趣的:(#,卷积神经网络,深度学习,人工智能,神经网络)