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

SqueezeNet

Fire Module: Squeeze and Expand

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

在这里插入图片描述

S 1 , e 2 , e 2 S_1,e_2,e_2 S1,e2,e2 均代表卷积层输出的通道数,Fire Module 默认 e 1 = e 2 = 4 × S 1 e_1=e_2=4×S_1 e1=e2=4×S1

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 N 的特征图 ( N N N 代表类别数),经过全局池化层后送入 Softmax
    在这里插入图片描述

SqueezeNet 总结

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

MobileNet

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

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

标准卷积

  • 假设当前特征图大小为 C i × H × W C_i×H×W Ci×H×W,需要输出的特征图大小为 C o × H × W C_o×H×W Co×H×W,卷积核大小为 3 × 3 3×3 3×3,Padding 为 1,那么总的计算量
    F s = C o × H × W × C i × 3 × 3 F_s=C_o\times H\times W\times C_i\times 3\times 3 Fs=Co×H×W×Ci×3×3输出特征图上的每一个点都同时融合了空间信息和通道信息

深度可分离卷积

  • 深度可分离卷积将空间信息的融合和通道信息的融合分离开,将卷积的过程分为逐通道卷积 (融合空间信息) 与逐点 1 × 1 1×1 1×1 卷积 (融合通道信息) 两步,大大降低了计算量
  • (1) 逐通道卷积: 对于一个通道的输入特征 H × W H×W H×W,利用一个 3 × 3 3×3 3×3 卷积核进行点乘求和,得到一个通道的输出 H × W H×W H×W。然后,对于所有的输入通道 C i C_i Ci,使用 C i C_i Ci 3 × 3 3×3 3×3 卷积核即可得到 C i × H × W C_i×H×W Ci×H×W 大小的输出
    在这里插入图片描述逐通道卷积的卷积核参数量 C i × 3 × 3 C_i×3×3 Ci×3×3,远少于标准卷积 C i × 3 × 3 × C o C_i×3×3×C_o Ci×3×3×Co 的数量。同时通道之间相互独立,没有各通道间的特征融合,总计算量为:
    F d = C i × H × W × 3 × 3 F_d=C_i×H×W\times3\times3 Fd=Ci×H×W×3×3
  • (2) 逐点 1 × 1 1×1 1×1 卷积: 逐点 1 × 1 1×1 1×1 卷积就是用 1 × 1 1\times1 1×1 卷积层融合不同通道间的特征,同时也可以改变特征图的通道。由于这里 1 × 1 1×1 1×1 卷积的输入特征图大小为 C i × H × W C_i×H×W Ci×H×W,输出特征图大小为 C o × H × W C_o×H×W Co×H×W,因此这一步的总计算量为:
    F 1 = C o × H × W × C i × 1 × 1 F_1=C_o×H×W×C_i×1×1 F1=Co×H×W×Ci×1×1
  • 综合上述两步,可以得到深度可分离卷积与标准卷积的计算量之比
    r = F d + F 1 F s = 1 C o + 1 9 ≈ 1 9 r=\frac{F_d+F_1}{F_s}=\frac{1}{C_o}+\frac{1}{9}\approx\frac{1}{9} r=FsFd+F1=Co1+9191可以看到,深度可分离卷积总体计算量约等于标准卷积的 1 / 9 1/9 1/9极大地减少了卷积过程的计算量

MobileNet v1

深度可分离卷积模块

在这里插入图片描述

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

MobileNet v1 整体结构

在这里插入图片描述

D w D_w Dw 代表逐通道卷积 (深度分解卷积),其后需要跟一个 1 × 1 1×1 1×1 卷积; s 2 s_2 s2 代表步长为 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 × 3 3×3 3×3 卷积处的计算量较大,因此通常先使用 1 × 1 1×1 1×1 卷积进行特征降维,减少通道数,再送入 3 × 3 3×3 3×3 卷积,最后再利用 1 × 1 1×1 1×1 卷积升维。这种结构从形状上看中间窄两边宽,类似于沙漏形状;而在 MobileNet 中,由于使用了深度可分离卷积来逐通道计算,本身计算量就比较少,因此在此可以使用 1 × 1 1×1 1×1 卷积来升维,在计算量增加不大的基础上获取更好的效果,最后再用 1 × 1 1×1 1×1 卷积降维。这种结构中间宽两边窄,类似于柳叶,该结构也因此被称为 Inverted Residual Block
  • 依据卷积的步长,该结构可分为两种情形,在步长为 1 时使用了残差连接,融合的方式为逐元素相加;而步长为 2 时不使用残差连接
    在这里插入图片描述

去掉 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

通道混洗

  • 当前先进的轻量化网络大都使用深度可分离卷积或者组卷积,以降低网络的计算量,但这两种操作都无法改变特征的通道数,因此还需要使用 1 × 1 1×1 1×1 卷积来促进通道之间信息的融合并改变通道至指定维度。因此,轻量化网络中 1 × 1 1×1 1×1 卷积占据了大量的计算,并且致使通道之间充满约束,一定程度上降低了模型的精度
  • 为了进一步降低计算量,ShuffleNet 提出了通道混洗来完成通道之间信息的融合:
    在这里插入图片描述 a a a代表了常规的两个组卷积操作,可以看到,如果没有逐点的 1 × 1 1×1 1×1 卷积或者通道混洗,最终输出的特征仅由一部分输入通道的特征计算得出,这种操作阻碍了信息的流通,进而降低了特征的表达能力。因此,我们希望在一个组卷积之后,能够将特征图之间的通道信息进行融合,类似于 b b b的操作,将每一个组的特征分散到不同的组之后,再进行下一个组卷积,这样输出的特征就能够包含每一个组的特征,而通道混洗恰好可以实现这个过程,如 c c c 图所示
  • 通道混洗可以通过几个常规的张量操作巧妙地实现。这里对输入通道做了 1~12 的编号,一共包含 3 个组,每个组包含 4 个通道:
    在这里插入图片描述
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 基本结构单元

在这里插入图片描述

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

ShuffleNet v1 网络整体结构

在这里插入图片描述

  • 深度可分离卷积虽然可以有效降低计算量,但其存储访问效率较差,因此 ShuffleNet 只在 Stage 2, 3, 4 内使用了其特殊的基本单元,这 3 个阶段的第一个 Block 的步长为 2 以完成降采样,下一个阶段的通道数是上一个的两倍
  • g ∈ [ 1 , 8 ] g\in[1,8] g[1,8] 代表组卷积的组数,以控制卷积连接的稀疏性。组数越多,计算量越少,因此在相同计算资源的条件下,可以使用更多的卷积核以获取更多的通道数

ShuffleNet v2


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

  • 原有的一些轻量化方法在衡量模型性能时,通常使用浮点运算量 FLOPs (Floating Point Operations) 作为主要指标,其中 FLOPs 是指模型在进行一次前向传播时所需的浮点计算次数。然而,ShuffleNet v2 的作者通过一系列实验发现仅仅将 FLOPs 作为评判模型复杂度的指标是有问题的,FLOPs 近似的网络会存在不同的速度,还有另外两个重要的指标:内存访问时间 (Memory Access Cost,MAC) 与网络的并行度
  • 以此作为出发点,ShuffleNet v2 做了大量的实验,分析影响网络运行速度的原因,提出了建立高性能网络的 4 个基本规则
    • (1) 卷积层的输入特征与输出特征通道数相等时,MAC 最小,此时模型速度最快:轻量化网络常采用深度可分离卷积,其中 1 × 1 1\times1 1×1 卷积的计算复杂度最大,因此下面主要考虑卷积核大小为 1 × 1 1\times1 1×1 时的情况。假设输入与输出特征图大小为 h × w h\times w h×w,通道数分别为 c 1 , c 2 c_1,c_2 c1,c2,则 FLOPs 为 B = h w c 1 c 2 B=hwc_1c_2 B=hwc1c2,MAC 为 h w ( c 1 + c 2 ) + c 1 c 2 hw(c_1+c_2)+c_1c_2 hw(c1+c2)+c1c2 (两项分别为输入输出特征图的大小和 1 × 1 1\times 1 1×1 卷积核的大小)。当 FLOPs B B B 固定时,有 c 1 c 2 = B h w c_1c_2=\frac{B}{hw} c1c2=hwB,代入 MAC 表达式可得
      M A C ≥ 2 h w B + B h w \mathrm{MAC} \geq 2 \sqrt{h w B}+\frac{B}{h w} MAC2hwB +hwB因此 FLOPs 不变时,当且仅当 c 1 = c 2 c_1=c_2 c1=c2 时 MAC 最小。虽然上述理论分析只适用于有足够大的 cache 能容纳输入/出特征图及卷积参数的情况,实际 MAC 可能会有所出入,但实验证明上述规律确实存在:
      在这里插入图片描述
    • (2) 过多的组卷积会增加 MAC,导致模型的速度变慢:组卷积能在相同 FLOPs 的条件下输出更多通道数,但更多的通道数就意味着会增大 MAC。设组数为 g g g,则 FLOPs B = h w c 1 c 2 / g B=hwc_1c_2/g B=hwc1c2/g,因此
      M A C = h w ( c 1 + c 2 ) + c 1 c 2 g = h w c 1 + B g c 1 + B h w \begin{aligned} \mathrm{MAC} &=h w\left(c_{1}+c_{2}\right)+\frac{c_{1} c_{2}}{g} \\ &=h w c_{1}+\frac{B g}{c_{1}}+\frac{B}{h w} \end{aligned} MAC=hw(c1+c2)+gc1c2=hwc1+c1Bg+hwB由此可见, g g g 的增加会使得 MAC 显著增加
      在这里插入图片描述
    • (3) 网络的碎片化会降低可并行度,这表明模型中分支数量越少,模型速度会越快
      在这里插入图片描述
    • (4) 逐元素 (Element Wise) 操作虽然 FLOPs 值较低,但其 MAC 较高,因此也应当尽可能减少逐元素操作 (逐元素操作包括 ReLU, AddTensor, AddBias…)。
      在这里插入图片描述

ShuffleNet v1 的问题

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

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

ShuffleNet v2

在这里插入图片描述

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

参考文献

  • 《深度学习之 PyTorch 物体检测实战》
  • 41
    点赞
  • 148
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值