经典网络结构 (八):轻量化网络 (SqueezeNet, MobileNet, ShuffleNet)

SqueezeNet

Fire Module: Squeeze and Expand

  • SqueezeNet 的主要模块为 Fire Module,它主要从网络结构优化的角度出发,使用了如下 3 点策略来减少网络参数,提升网络性能:
  • (1)使用1%C3%971卷积来替代部分的3%C3%973卷积,可以将参数减少为原来的1/9,同时减少输入通道的数量
  • (2)利用1%C3%971卷积来减少输入通道的数量
  • (3)在减少通道数之后,使用多个尺寸的卷积核进行计算,以保留更多的信息,提升分类的准确率

经典网络结构 (八):轻量化网络 (SqueezeNet, MobileNet, ShuffleNet)

S_1%2Ce_2%2Ce_2均代表卷积层输出的通道数,Fire Module 默认e_1%3De_2%3D4%C3%97S_1

import torch
from torch import nn

class Fire(nn.Module):

    def __init__(self, inplanes, squeeze_planes, expand_planes):
    	# 不改变输入特征图的宽高,只改变通道数
    	# 输入通道数为 inplanes,压缩通道数为 squeeze_planes,输出通道数为 2 * expand_planes
        super(Fire, self).__init__()
        # Squeeze 层
        self.conv1 = nn.Conv2d(inplanes, squeeze_planes, kernel_size=1, stride=1)
        self.bn1 = nn.BatchNorm2d(squeeze_planes)
        self.relu1 = nn.ReLU(inplace=True)
        
        # Expand 层
        self.conv2 = nn.Conv2d(squeeze_planes, expand_planes, kernel_size=1, stride=1)
        self.bn2 = nn.BatchNorm2d(expand_planes)
        self.conv3 = nn.Conv2d(squeeze_planes, expand_planes, kernel_size=3, stride=1, padding=1)
        self.bn3 = nn.BatchNorm2d(expand_planes)
        self.relu2 = nn.ReLU(inplace=True)
    
    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu1(x)
        
        out1 = self.conv2(x)
        out1 = self.bn2(out1)
        out2 = self.conv3(x)
        out2 = self.bn3(out2)
        out = torch.cat([out1, out2], 1)
        out = self.relu2(out)
        return out

SqueezeNet

  • 输入图像首先送入 Conv 1,得到通道数为 96 的特征图,然后依次使用 8 个 Fire Module,中间还有两个步长为 2 的最大池化层,通道数也逐渐增加。最后一个卷积为 Conv 10,输出通道数为N的特征图 (N代表类别数),经过全局池化层后送入 Softmax
    经典网络结构 (八):轻量化网络 (SqueezeNet, MobileNet, ShuffleNet)

SqueezeNet 总结

  • SqueezeNet 是一个精心设计的轻量化网络,其性能与 AlexNet 相近,而模型参数仅有AlexNet的1/50。使用常见的模型压缩技术,如 SVD、剪枝和量化等,可以进一步压缩该模型的大小。例如,使用 Deep Compresion 技术对其进行压缩时,在几乎不损失性能的前提下,模型大小可以压缩到 0.5MB
  • 基于其轻量化的特性,SqueezeNet 可以广泛地应用到移动端,促进了物体检测技术在移动端的部署与应用

MobileNet

深度可分离卷积 (Depthwise Separable Convolution)

  • SqueezeNet 虽在一定程度上减少了卷积计算量,但仍然使用传统的卷积计算方式,而在其后的 MobileNet 利用了更为高效的深度可分离卷积的方式,进一步加速了卷积网络在移动端的应用

标准卷积

  • 假设当前特征图大小为C_i%C3%97H%C3%97W,需要输出的特征图大小为C_o%C3%97H%C3%97W,卷积核大小为3%C3%973,Padding 为 1,那么总的计算量为
    F_s%3DC_o%5Ctimes%20H%5Ctimes%20W%5Ctimes%20C_i%5Ctimes%203%5Ctimes%203 输出特征图上的每个点同时结合空间信息和通道信息

深度可分离卷积

  • depthwise separable convolution 将空间信息的融合和通道信息的融合分开,将卷积过程分为两步:逐通道卷积(融合空间信息)和逐点1%C3%971卷积(融合通道)信息),大大减少了计算量
  • (1)逐通道卷积: 对于一个通道的输入特征H%C3%97W,利用一个3%C3%973卷积核进行点乘求和,得到一个通道的输出H%C3%97W。然后,对于所有的输入通道C_i,使用C_i3%C3%973卷积核即可得到C_i%C3%97H%C3%97W大小的输出
    经典网络结构 (八):轻量化网络 (SqueezeNet, MobileNet, ShuffleNet)逐通道卷积的卷积核参数个数为C_i%C3%973%C3%973,远小于标准卷积的个数C_i%C3%973%C3%973%C3%97C_o。同时,各通道相互独立,各通道之间没有特征融合。总计算量为:
    F_d%3DC_i%C3%97H%C3%97W%5Ctimes3%5Ctimes3
  • (2)逐点1%C3%971卷积: 逐点1%C3%971卷积就是用1%5Ctimes1卷积层融合不同通道间的特征,同时也可以改变特征图的通道。由于这里1%C3%971卷积的输入特征图大小为C_i%C3%97H%C3%97W,输出特征图大小为C_o%C3%97H%C3%97W,因此这一步的总计算量为:
    F_1%3DC_o%C3%97H%C3%97W%C3%97C_i%C3%971%C3%971
  • 结合以上两步,我们可以得到depthwise separable convolution 和standard convolution 的计算量之比:
    r%3D%5Cfrac%7BF_d%2BF_1%7D%7BF_s%7D%3D%5Cfrac%7B1%7D%7BC_o%7D%2B%5Cfrac%7B1%7D%7B9%7D%5Capprox%5Cfrac%7B1%7D%7B9%7D 可以看出depthwise separable convolution的整体计算量约等于标准卷积的1/9,大大减少了卷积过程的计算量。

MobileNet v1

深度可分离卷积模块

经典网络结构 (八):轻量化网络 (SqueezeNet, MobileNet, ShuffleNet)

  • 值得注意的是,在此使用了 ReLU6 来替代原始的 ReLU 激活函数,将 ReLU 的最大输出限制在 6 以下。这主要是为了满足移动端部署的需求。移动端通常使用 Float16 或者 Int8 等较低精度的模型,如果不对激活函数的输出进行限制的话,激活值的分布范围会很大,而低精度的模型很难精确地覆盖如此大范围的输出,这样会带来精度损失

MobileNet v1 整体结构

经典网络结构 (八):轻量化网络 (SqueezeNet, MobileNet, ShuffleNet)

D_w代表逐通道卷积 (深度分解卷积),其后需要跟一个1%C3%971卷积;s_2代表步长为 2 的卷积,用于缩小特征图尺寸,起到与 Pooling 层一样的作用

from torch import nn

class MobileNet(nn.Module):
    def __init__(self):
        super(MobileNet, self).__init__()

		# 3 x 3 standard convolution
        def conv_bn(dim_in, dim_out, stride):
            return nn.Sequential(
                nn.Conv2d(dim_in, dim_out, 3, stride, 1, bias=False),
                nn.BatchNorm2d(dim_out),
                nn.ReLU(inplace=True)
            )

		# depthwise separable convolution
        def conv_dw(dim_in, dim_out, stride):
            return nn.Sequential(
            	# use group convolution in PyTorch to implement channel-wise convolution
                nn.Conv2d(dim_in, dim_in, 3, stride, 1, groups=dim_in, bias=False),
                nn.BatchNorm2d(dim_in),
                nn.ReLU(inplace=True),
                nn.Conv2d(dim_in, dim_out, 1, 1, 0, bias=False),
                nn.BatchNorm2d(dim_out),
                nn.ReLU(inplace=True),
            )
           
        self.model = nn.Sequential(
        	# input: 3 x 224 x 224
            conv_bn(  3,  32, 2), 	# 32 x 112 x 112
            conv_dw( 32,  64, 1), 	# 64 x 112 x 112
            conv_dw( 64, 128, 2), 	# 128 x 56 x 56
            conv_dw(128, 128, 1), 	# 128 x 56 x 56
            conv_dw(128, 256, 2), 	# 256 x 28 x 28
            conv_dw(256, 256, 1), 	# 256 x 28 x 28
            conv_dw(256, 512, 2), 	# 512 x 14 x 14
            conv_dw(512, 512, 1), 	# 512 x 14 x 14
            conv_dw(512, 512, 1), 	# 512 x 14 x 14
            conv_dw(512, 512, 1), 	# 512 x 14 x 14
            conv_dw(512, 512, 1), 	# 512 x 14 x 14
            conv_dw(512, 512, 1), 	# 512 x 14 x 14
            conv_dw(512, 1024, 2), 	# 1024 x 7 x 7
            conv_dw(1024, 1024, 1), # 1024 x 7 x 7
            nn.AvgPool2d(7),		# 1024 x 1 x 1
        )
        self.fc = nn.Linear(1024, 1000)

    def forward(self, x):
        x = self.model(x)
        x = x.view(-1, 1024)
        x = self.fc(x)
        return x

MobileNet v1 总结

  • 总体上,MobileNet v1 利用深度可分离的结构牺牲了较小的精度,带来了计算量与网络层参数的大幅降低,从而也减小了模型的大小,方便应用于移动端
  • 但 MobileNet v1 也有其自身结构带来的缺陷,主要有以下两点:
  • (1) 模型结构较为复古,采用了与 VGGNet 类似的卷积简单堆叠,没有采用残差、特征融合等先进的结构
  • (2)深度分解卷积 (i.e. 逐通道卷积) 中各通道相互独立,卷积核维度较小,输出特征中只有较少的输入特征,再加上 ReLU 激活函数,使得输出很容易变为 0,难以恢复正常训练,因此在训练时部分卷积核容易被训练废掉

MobileNet v2

Inverted Residual Block

  • MobileNet v2 利用残差结构取代了原始的卷积堆叠方式,提出了 Inverted Residual Block 。在标准的 ResNet中,由于3%C3%973卷积处的计算量较大,因此通常先使用1%C3%971卷积进行特征降维,减少通道数,再送入3%C3%973卷积,最后再利用1%C3%971卷积升维。这种结构从形状上看中间窄两边宽,类似于沙漏形状;而在 MobileNet 中,由于使用了深度可分离卷积来逐通道计算,本身计算量就比较少,因此在此可以使用1%C3%971卷积来升维,在计算量增加不大的基础上获取更好的效果,最后再用1%C3%971卷积降维。这种结构中间宽两边窄,类似于柳叶,该结构也因此被称为 Inverted Residual Block
  • 依据卷积的步长,该结构可分为两种情形,在步长为 1 时使用了残差连接,融合的方式为逐元素相加;而步长为 2 时不使用残差连接
    经典网络结构 (八):轻量化网络 (SqueezeNet, MobileNet, ShuffleNet)

去掉 ReLU6

  • 深度可分离卷积得到的特征对应于低维空间,特征较少,如果后续接线性映射则能够保留大部分特征,而如果接非线性映射如 ReLU,则会破坏特征,造成特征的损耗,从而使得模型效果变差。针对此问题,MobileNet v2 直接去掉了每一个 Block 中最后的 ReLU6 层,减少了特征的损耗,获得了更好的检测效果
import torch.nn as nn
import math

# 标准 3 x 3 卷积
def conv_bn(inp, oup, stride):
    return nn.Sequential(
        nn.Conv2d(inp, oup, 3, stride, 1, bias=False),
        nn.BatchNorm2d(oup),
        nn.ReLU6(inplace=True)
    )

# 标准 1 x 1 卷积
def conv_1x1_bn(inp, oup):
    return nn.Sequential(
        nn.Conv2d(inp, oup, 1, 1, 0, bias=False),
        nn.BatchNorm2d(oup),
        nn.ReLU6(inplace=True)
    )
class InvertedResidual(nn.Module):
    def __init__(self, inp, oup, stride, expand_ratio):
        super(InvertedResidual, self).__init__()
        self.stride = stride
        assert stride in [1, 2]

        hidden_dim = round(inp * expand_ratio)	# 中间扩展层的通道数
        self.use_res_connect = self.stride == 1 and inp == oup

        if expand_ratio == 1:
        	# 不进行升维
            self.conv = nn.Sequential(
                # 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),
                nn.BatchNorm2d(oup),
            )
        else:
            self.conv = nn.Sequential(
                # pw (升维)
                nn.Conv2d(inp, hidden_dim, 1, 1, 0, bias=False),
                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),
                nn.BatchNorm2d(oup),
            )

    def forward(self, x):
        if self.use_res_connect:
            return x + self.conv(x)
        else:
            return self.conv(x)
class MobileNetV2(nn.Module):
    def __init__(self, n_class=1000, input_size=224, width_mult=1.):
        super(MobileNetV2, self).__init__()
        block = InvertedResidual
        input_channel = 32
        last_channel = 1280
        interverted_residual_setting = [
            # t, c, n, s
            [1, 16, 1, 1],
            [6, 24, 2, 2],
            [6, 32, 3, 2],
            [6, 64, 4, 2],
            [6, 96, 3, 1],
            [6, 160, 3, 2],
            [6, 320, 1, 1],
        ]

        # building first layer
        assert input_size % 32 == 0
        input_channel = int(input_channel * width_mult)
        self.last_channel = int(last_channel * width_mult) if width_mult > 1.0 else last_channel
        self.features = [conv_bn(3, input_channel, 2)]
        # building inverted residual blocks
        for t, c, n, s in interverted_residual_setting:
            output_channel = int(c * width_mult)
            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
        # building last several layers
        self.features.append(conv_1x1_bn(input_channel, self.last_channel))
        # make it nn.Sequential
        self.features = nn.Sequential(*self.features)

        # building classifier
        self.classifier = nn.Sequential(
            nn.Dropout(0.2),
            nn.Linear(self.last_channel, n_class),
        )

        self._initialize_weights()

    def forward(self, x):
        x = self.features(x)
        x = x.mean(3).mean(2)
        x = self.classifier(x)
        return x

    def _initialize_weights(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
                m.weight.data.normal_(0, math.sqrt(2. / n))
                if m.bias is not None:
                    m.bias.data.zero_()
            elif isinstance(m, nn.BatchNorm2d):
                m.weight.data.fill_(1)
                m.bias.data.zero_()
            elif isinstance(m, nn.Linear):
                n = m.weight.size(1)
                m.weight.data.normal_(0, 0.01)
                m.bias.data.zero_()

ShuffleNet

频道洗牌

  • 目前先进的轻量级网络大多使用depthwise separable convolution或者group convolution来降低网络的计算量,但是这两种操作都不能改变特征的通道数,所以也需要使用1%C3%971卷积来方便通道之间 信息的融合,将通道更改为指定维度。因此,轻量级网络中的1%C3%971卷积占用了大量的计算量,且通道中充满了约束,在一定程度上降低了模型的准确率。
  • 为了进一步降低计算量, ShuffleNet 提出了通道混洗来完成通道之间信息的融合:
    经典网络结构 (八):轻量化网络 (SqueezeNet, MobileNet, ShuffleNet)a表示常规的两组卷积操作。可以看出,如果没有逐点1%C3%971卷积或者channel shuffling,最终输出的特征只能从一部分输入通道的特征中计算出来,阻碍了操作。这减少了信息的流动,进而降低了特征的表现力。因此,我们希望经过一次组卷积后,可以融合特征图之间的通道信息,类似于b图的操作,将每组的特征分散到不同的组后,再进行下一次组卷积。 ,这样输出的特征就可以包含每一组的特征,而channel shuffle正好可以实现这个过程,如图c
  • 通道混洗可以通过几个常规的张量操作巧妙地实现。这里对输入通道做了 1~12 的编号,一共包含 3 个组,每个组包含 4 个通道:
    经典网络结构 (八):轻量化网络 (SqueezeNet, MobileNet, ShuffleNet)
def channel_shuffle(x, groups):
	batchsize, num_channels, height, width = x.data.size()  
	channels_per_group = num_channels // groups
	# Reshape
	x = x.view(batchsize, groups, channels_per_group, height, width)  
	# Transpose
	x = torch.transpose(x, 1, 2).contiguous()
	# Flatten
	x = x.view(batchsize, -1, height, width)
	return x

ShuffleNet v1

  • ShuffleNet v1 code : https://github.com/jaxony/ShuffleNet

ShuffleNet v1 基本结构单元

经典网络结构 (八):轻量化网络 (SqueezeNet, MobileNet, ShuffleNet)

  • a图是一个带有深度可分离卷积的残差模块(i.e. MobileNet v2 的基本单元)
  • b图则是ShuffleNet 的基本单元,可以看到1%C3%971卷积采用的是组卷积,然后进行通道的混洗,这两步可以取代1%C3%971的逐点卷积,并且大大降低了计算量。3%C3%973卷积仍然采用深度可分离的方式
  • c图是带有降采样的 ShuffleNet 单元。由于降采样时通常要伴有通道数的增加,ShuffleNet 直接将两分支拼接在一起来实现了通道数的增加,而不是常规的逐点相加

ShuffleNet v1 网络整体结构

经典网络结构 (八):轻量化网络 (SqueezeNet, MobileNet, ShuffleNet)

  • 深度可分离卷积虽然可以有效降低计算量,但其存储访问效率较差,因此 ShuffleNet只在 Stage 2, 3, 4 内使用了其特殊的基本单元,这 3 个阶段的第一个 Block 的步长为 2 以完成降采样,下一个阶段的通道数是上一个的两倍
  • g%5Cin%5B1%2C8%5D表示组卷积的组数,以控制卷积连接的稀疏性。组数越多,计算量越少,所以在计算资源相同的情况下,可以使用更多的卷积核来获得更多的通道

ShuffleNet v2

  • paper : ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design

建立高性能网络的 4 个基本规则

  • 原有的一些轻量化方法在衡量模型性能时,通常使用浮点运算量 FLOPs (Floating Point Operations) 作为主要指标,其中 FLOPs 是指模型在进行一次前向传播时所需的浮点计算次数。然而,ShuffleNet v2 的作者通过一系列实验发现仅仅将 FLOPs 作为评判模型复杂度的指标是有问题的,FLOPs 近似的网络会存在不同的速度,还有另外两个重要的指标:内存访问时间 (Memory Access Cost,MAC) 与网络的并行度
  • 以此作为出发点,ShuffleNet v2 做了大量的实验,分析影响网络运行速度的原因,提出了建立高性能网络的 4 个基本规则:
  • (1)卷积层的输入特征与输出特征通道数相等时,MAC 最小,此时模型速度最快:轻量化网络常采用深度可分离卷积,其中1%5Ctimes1卷积的计算复杂度最大,因此下面主要考虑卷积核大小为1%5Ctimes1时的情况。假设输入与输出特征图大小为h%5Ctimes%20w,通道数分别为c_1%2Cc_2,则 FLOPs 为B%3Dhwc_1c_2,MAC 为hw%28c_1%2Bc_2%29%2Bc_1c_2(两项分别为输入输出特征图的大小和1%5Ctimes%201卷积核的大小)。当 FLOPsB固定时,有c_1c_2%3D%5Cfrac%7BB%7D%7Bhw%7D,代入 MAC 表达式可得
    %5Cmathrm%7BMAC%7D%20%5Cgeq%202%20%5Csqrt%7Bh%20w%20B%7D%2B%5Cfrac%7BB%7D%7Bh%20w%7D因此 FLOPs 不变时,当且仅当c_1%3Dc_2时 MAC 最小。虽然上述理论分析只适用于有足够大的 cache 能容纳输入/出特征图及卷积参数的情况,实际 MAC 可能会有所出入,但实验证明上述规律确实存在:
    经典网络结构 (八):轻量化网络 (SqueezeNet, MobileNet, ShuffleNet)
  • (2)过多的组卷积会增加 MAC,导致模型的速度变慢:组卷积能在相同 FLOPs 的条件下输出更多通道数,但更多的通道数就意味着会增大 MAC。设组数为g,则 FLOPsB%3Dhwc_1c_2/g,因此
    %5Cbegin%7Baligned%7D%20%5Cmathrm%7BMAC%7D%20%26%3Dh%20w%5Cleft%28c_%7B1%7D%2Bc_%7B2%7D%5Cright%29%2B%5Cfrac%7Bc_%7B1%7D%20c_%7B2%7D%7D%7Bg%7D%20%5C%5C%20%26%3Dh%20w%20c_%7B1%7D%2B%5Cfrac%7BB%20g%7D%7Bc_%7B1%7D%7D%2B%5Cfrac%7BB%7D%7Bh%20w%7D%20%5Cend%7Baligned%7D由此可见,g的增加会使得 MAC 显著增加
    经典网络结构 (八):轻量化网络 (SqueezeNet, MobileNet, ShuffleNet)
  • (3)网络的碎片化会降低可并行度,这表明模型中分支数量越少,模型速度会越快
    经典网络结构 (八):轻量化网络 (SqueezeNet, MobileNet, ShuffleNet)
  • (4)逐元素 (Element Wise) 操作虽然 FLOPs 值较低,但其 MAC 较高,因此也应当尽可能减少逐元素操作(逐元素操作包括 ReLU, AddTensor, AddBias…)。
    经典网络结构 (八):轻量化网络 (SqueezeNet, MobileNet, ShuffleNet)

ShuffleNet v1 的问题

以上述 4 个规则为基础,可以看出ShuffleNet v1 有 3 点违反了此规则:

  • (1) 在 Bottleneck 中使用了1%C3%971组卷积与1%C3%971的逐点卷积的 bottleneck 结构,导致输入输出通道数不同,违背了规则 1 与规则 2
  • (2) 整体网络中使用了大量的组卷积,造成了太多的分组,违背了规则 3
  • (3) 网络中存在大量的逐点相加操作,违背了规则 4

ShuffleNet v2

经典网络结构 (八):轻量化网络 (SqueezeNet, MobileNet, ShuffleNet)

  • 如上图所示,(a) (b) 为 v1 的基础结构,(c) (d) 为 v2 的基础结构。v2 提出了 Channel Split 操作,如 (c) 所示,将输入特征分成两部分,一部分进行真正的深度可分离计算,将计算结果与另一部分进行通道 Concat,最后进行通道的混洗操作,完成信息的互通。同时,整个过程没有使用到1%C3%971组卷积,也避免了逐点相加的操作。在需要降采样与通道翻倍时,ShuffleNet v2 去掉了 Channel Split 操作,这样最后 Concat 时通道数会翻倍
  • 基于此基本单元,ShuffleNet v2 的整体结构如下表所示。与 ShuffleNet v1 相比,ShuffleNet v2在全局平均池化之前增加了一个1%C3%971卷积来融合特征:
    经典网络结构 (八):轻量化网络 (SqueezeNet, MobileNet, ShuffleNet)

参考

  • 《深度学习之 PyTorch 物体检测实战》

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
扎眼的阳光的头像扎眼的阳光普通用户
上一篇 2022年3月15日 下午6:56
下一篇 2022年3月15日 下午7:18

相关推荐