【卷积神经网络系列】十五、ShuffleNet-V1


参考资料:

论文:

  ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices

博客:

  ShuffleNet 详解与复现

  ShuffleNet算法详解

  ShuffleNet详解

  ShuffleNet轻量级网络解读


一、前言

 在最先进的基础网络结构中,像 XceptionResNeXt 在小网络中效率会降低,因为密集的 1×1卷积 代价很高 ,基于此作者提出了 pointwise group convolution 以减少1×1卷积的计算复杂度,为克制 pointwise group convolution 带来的副作用,提出了 channel shuffle 的操作,用于实现信息在特征通道之间流动 。

ShuffleNet 使用是一种计算效率极高的CNN架构,它是专门为计算能力非常有限的移动设备设计 :通过 逐点分组卷积(Pointwise Group Convolution)通道洗牌(Channel Shuffle) 两种新运算,在保持精度的同时大大降低了计算成本 ;

  • ShuffleNet 比最近的 MobileNet 在 ImageNet 分类任务上的 top-1误差更低 (绝对7.8%) ;
  • 在基于ARM的移动设备上,ShuffleNetAlexNet 实现了约13倍的实际加速,并保持了相当的精度 ;

二、模型结构

2.1 Depthwise Sparable Convolutions

 它的核心思想是将一个完整的卷积运算分解为两步进行,分别为逐深度卷积(Depthwise Convolution)与逐点卷积(Pointwise Convolution)。

(1)常规卷积

 假设输入层为一个大小为64×64像素、3通道彩色图片。经过一个包含4个Filter的卷积层,最终输出4个Feature Map,且尺寸与输入层相同。整个过程可以用下图来概括:

在这里插入图片描述

 此时,卷积层共4个Filter,每个Filter包含了3个Kernel,每个Kernel的大小为3×3。因此卷积层的参数数量可以用如下公式来计算:N_std = 4 × 3 × 3 × 3 = 108

(2)逐深度卷积(Depthwise Convolution)

 同样是上述例子,一个大小为64×64像素、3通道彩色图片首先经过第一次卷积运算,不同之处在于此次的卷积完全是在二维平面内进行,且Filter的数量与上一层的Depth相同。所以一个三通道的图像经过运算后生成了3个Feature map,如下图所示。

在这里插入图片描述

 其中一个Filter只包含一个大小为3×3的Kernel,卷积部分的参数个数计算如下:N_depthwise = 3 × 3 × 3 = 27

 Depthwise Convolution完成后的Feature map数量与输入层的depth相同,但是这种运算对输入层的每个channel独立进行卷积运算后就结束了,没有有效的利用不同map在相同空间位置上的信息。因此需要增加另外一步操作来将这些map进行组合生成新的Feature map,即接下来的Pointwise Convolution。

(3)逐点卷积(Pointwise Convolution)

 Pointwise Convolution的运算与常规卷积运算非常相似,不同之处在于卷积核的尺寸为 1×1×M × NM为上一层的depth,N为新生成的Feature map的个数。所以这里的卷积运算会将上一步的map在深度方向上进行加权组合,生成新的Feature map。有几个Filter就有几个Feature map。如下图所示。

在这里插入图片描述

 由于采用的是1×1卷积的方式,此步中卷积涉及到的参数个数可以计算为:N_pointwise = 1 × 1 × 3 × 4 = 12


2.2 Group Convolution

分组卷积(Group Convolution) 起源于2012年的 AlexNet。由于当时硬件资源的限制,因为作者将Feature Maps分给多个GPU进行处理,最后把多个GPU的结果进行融合。如下图:

在这里插入图片描述

标准的2D卷积:步骤如下图所示,输入特征为 (H × W × C) ,然后应用 C’ 个filters(每个filter的大小为 (h × w × c),输入层被转换为大小为 (H’ × W’ × C’) 的输出特征。

在这里插入图片描述

分组卷积的表示如下图(下图表示的是被拆分为 2 个filters组的分组卷积) :

  • 首先每个filters组,包含 C’/2个 数量的filter, 每个filter 的通道数为传统2D-卷积filter的一半。
  • 每个filters组作用于原来 W × H × C 对应通道数的一半,也就是 W × H × C/2。
  • 最终每个filters组对应输出输出 C’ / 2 个通道的特征。
  • 最后将通道堆叠得到了最终的 C’个通道,实现了和上述标准2D 卷积一样的效果。

在这里插入图片描述

分组卷积的优势

(1)降低参数量

  • 标准2D卷积:w × h × C × C’;
  • 分组卷积:w × h × C 2 \frac{C}{2} 2C × C ′ 2 \frac{C'}{2} 2C + w × h × C 2 \frac{C}{2} 2C × C ′ 2 \frac{C'}{2} 2C = w × h × C × C ′ 2 \frac{C'}{2} 2C

参数量减少到原来的 1 2 \frac{1}{2} 21!当Group为4的时候,参数量减少到原来的 1 4 \frac{1}{4} 41,所以参数量为原来的 1 g r o u p \frac{1}{group} group1

(2)增加相邻层filter之间的对角相关性

​ 在某些情况下,分组卷积能带来的模型效果确实要优于标准的2D 卷积,是因为组卷积的方式能够增加相邻层filter之间的对角相关性,而且能够减少训练参数,不容易过拟合,这类似于正则的效果。

在这里插入图片描述


2.3 Channel Shuffle

分组卷积的概念最先于 AlexNet中提出,当时受制于计算机硬件算力的约束,使用分布式GPU进行训练。后来被 ResNeXt 证实了分组卷积的高效实用性。

Depthwise Separable Convolution深度可分离卷积最先于 Xception提出,并且在 InceptionNet系列得到中推广。最近的一些研究,MobileNet使用深度可分离卷积,在轻量级网络中达到了SOTA的表现。

然而在小型网络中,昂贵的逐点卷积会导致满足复杂度约束的通道数量有限,从而严重的影响精度 。

 最直接的解决方案是:采用通道稀疏连接( channel sparse connections ),例如分组卷积可以大大降低计算成本 。

这样就会出现一个 问题 :某个通道的输出只能来自一小部分输入通道,这样阻止了通道之间的信息流,也就削弱了神经网络表达能力 ;


作者通过 通道洗牌(Channel Shuffle) 允许分组卷积从不同的组中获取输入数据,从而实现输入通道和输出通道相关联。

在这里插入图片描述

 先从group操作说起,一般卷积操作中比如输入feature map的数量是N,该卷积层的filter数量是M,那么M个filter中的每一个filter都要和N个feature map的某个区域做卷积,然后相加作为一个卷积的结果。假设你引入group操作,设group为g,那么N个输入feature map就被分成g个group,M个filter就被分成g个group,然后在做卷积操作的时候,第一个group的M/g个filter中的每一个都和第一个group的N/g个输入feature map做卷积得到结果,第二个group同理,直到最后一个group,如Figure1(a)。不同的颜色代表不同的group,图中有三个group。这种操作可以大大减少计算量,因为你每个filter不再是和输入的全部feature map做卷积,而是和一个group的feature map做卷积。

 但是如果多个group操作叠加在一起,如Figure1(a)的两个卷积层都有group操作,显然就会产生边界效应,什么意思呢?就是某个输出channel仅仅来自输入channel的一小部分这样肯定是不行的的,学出来的特征会非常局限。于是就有了channel shuffle来解决这个问题

 再看Figure1(b),在进行GConv2之前,对其输入feature map做一个分配,也就是每个group分成几个subgroup,然后将不同group的subgroup作为GConv2的一个group的输入,使得GConv2的每一个group都能卷积输入的所有group的feature map,这和Figure1(c)的channel shuffle的思想是一样的。

具体方法为:假设有G组,每组有N个通道,将通道Reshape为(G,N)并进行转转置,转置结束后对通道做Flatten操作,这样一来,就可以实现channel shuffle操作了。

在这里插入图片描述


2.4 ShuffleNet Unit

 基于残差块(residual block)和 通道洗牌(channel shuffle)设计的 ShuffleNet Unit

  • (a)深度卷积;
  • (b)逐点分组卷积;
  • (c)逐点分组卷积 ( stride=2 );

在这里插入图片描述

 第二个 1×1 GConv 的作用是:改变通道维数,实现与旁路的 shutcut 的维度相同 ;

 图(c)用通道级联Concat代替逐元素加法Add,这样可以很容易地扩大通道尺寸,而只需很少的额外计算开销。


2.5 网络结构

 所提出的网络由分成三个阶段ShuffleNet Unit分组而成:

  • 在每个阶段中的第一个块的步长设置为2,其余保持一致。
  • 下个阶段输出通道翻倍
  • 与ResNet一样,对于每个Shuffle Unit的瓶颈结构的通道数量设置为输出通道的 1/4
  • 在ShuffleNet Unit中,分组数量 g 控制着点卷积连接稀疏性。

在这里插入图片描述


三、实验结果

(1)评估Gconv的分组数g和网络宽度w对网络的影响:

 网络宽度w:1×表示正常,0.5×表示filters数量减少到0.5倍;

 分组数g:ShuffleNet中分组数量 g 设置为从1到8。如果分组数量为1,那么ShuffelNet中就不存在分组点卷积了;

结论:

  • 当分组数量 g>1时,要比没有点卷积(g=1)的表现要好。
  • 小型网络在分组数量较多时,会表现优秀。
  • 例如,ShuffleNet 1x 变现最好的是当g=8是,比g=1时,性能提升了1.2%。ShuffleNet0.5x 和ShuffleNet0.25x 分别提升了3.5%和4.4%。
  • 在给定计算复杂度限制下,组卷积可以获得更多的特征图通道数量,这个性能方面的提升可能来自于宽度更大的特征图,从而更好的编码信息。此外,小型网络通常包括更"薄"的特征图,所以它获得信息来自于变宽之后的特征图。
  • 同时也证实了对于一些网络 ShuffleNet 0.5x,当分组卷积数量增加之后性能会逐渐饱和甚至退化。
  • 对于ShuffleNet 0.25x这个小型网络,随着分组数量越来越多,性能是逐渐变好,这意味着更宽的特征图对小型网络有性能促进作;

在这里插入图片描述


(2)评估Channel Shuffle操作:

 通道打乱的目的在于能都使多个分组卷积进行跨组之间信息交流,下表是在通道打乱有无情况下的性能对比。

  • 在不同的环境变量设置下,channel shuffle提高了模型性能,也就是评价指标错误率是越来越低的。
  • 当g=8时,所降低的错误率比没有设置打乱那组提高了一大截。
  • 这个实验证实跨组之间的信息交互非常重要。

在这里插入图片描述


四、复现

参考

  ShuffleNet轻量级网络解读

  ShuffleNet 详解与复现


(1)定义channel_shuffle操作:

  • 假设有G组,每组有N个通道,将通道Reshape为(G,N)并进行转转置,转置结束后对通道做Flatten操作,这样一来,就可以实现channel shuffle操作了。
    在这里插入图片描述
def channel_shuffle(x, groups):
    # (B, C, H, W)
    batchsize, num_channels, height, width = x.size()
    
    # 每个组有多少个channel
    channels_per_group = num_channels // groups

    # reshape: b, num_channels, h, w  -->  b, groups, channels_per_group, h, w
    x = x.view(batchsize, groups, channels_per_group, height, width)

    # channelshuffle
    # torch.transpose(x,dim_1,dim_2)表示将x这个tensor的dim_1和dim_2进行交换
    # 而contiguous方法就类似深拷贝,使得上面这些操作不会改变元数据
    x = torch.transpose(x, 1, 2).contiguous()

    # flatten
    x = x.view(batchsize, -1, height, width)

    return x

(2)定义ShuffleNet Unit:

  • 如果stride==2,此时ShuffleNet Unit输出的维度=输入维度+直连边输出的维度
    在这里插入图片描述
class shuffleNet_unit(nn.Module):
    expansion = 1

    def __init__(self, in_channels, out_channels, stride, groups):
        super(shuffleNet_unit, self).__init__()

        # 与ResNet一样,对于每个Shuffle Unit的瓶颈结构的通道数量设置为输出通道的1/4
        mid_channels = out_channels//4
        self.stride = stride
        
        if in_channels == 24:
            self.groups = 1
        else:
            self.groups = groups
            
        # 1x1 GConv1
        self.GConv1 = nn.Sequential(
            nn.Conv2d(in_channels, mid_channels, 
                      kernel_size=1, stride=1, groups=self.groups, bias=False),
            nn.BatchNorm2d(mid_channels),
            nn.ReLU(inplace=True)
        )

        # 3x3 DWConv
        self.DWConv = nn.Sequential(
            nn.Conv2d(mid_channels, mid_channels, 
                      kernel_size=3, stride=self.stride, 
                      padding=1, groups=self.groups, bias=False),
            nn.BatchNorm2d(mid_channels)
        )

        # 1x1 GConv2
        self.GConv2 = nn.Sequential(
            nn.Conv2d(mid_channels, out_channels, 
                      kernel_size=1, stride=1, groups=self.groups, bias=False),
            nn.BatchNorm2d(out_channels)
        )

        # 如果步长为2,in_channels!=out_channels,此时直连边需要变维
        if self.stride == 2:
            self.shortcut = nn.Sequential(
                nn.AvgPool2d(kernel_size=3, stride=2, padding=1)
            )
        else:
            self.shortcut = nn.Sequential()

    def forward(self, x):
        out = self.GConv1(x)                            # GConv1
        out = channel_shuffle(out, self.groups)         # channel_shuffle
        out = self.DWConv(out)                          # DWConv
        out = self.GConv2(out)                          # GConv2
        short = self.shortcut(x)                        # shortcut
        
        if self.stride == 2:
            out = F.relu(torch.cat([out, short], dim=1))
        else:
            out += short
            out = F.relu(out)
        return out

(3)主体结构:

在这里插入图片描述

class ShuffleNet(nn.Module):
    def __init__(self, groups, num_layers, num_channels, num_classes=1000):
        super(ShuffleNet, self).__init__()
        self.groups = groups
        
        # first layer
        self.conv1 = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=24, 
                      kernel_size=3, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(24),
            nn.ReLU(inplace=True),
        )
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        # 迭代shuffleNet_unit
        self.stage2 = self.make_layers(24, num_channels[0], num_layers[0], groups)
        self.stage3 = self.make_layers(num_channels[0], num_channels[1], num_layers[1], groups)
        self.stage4 = self.make_layers(num_channels[1], num_channels[2], num_layers[2], groups)

        # last layers
        self.globalpool = nn.AvgPool2d(kernel_size=7, stride=1)
        self.fc = nn.Linear(num_channels[2], num_classes)

    def make_layers(self, in_channels, out_channels, num_layers, groups):
        layers = []
        # 每个stage的第一个shuffleNet_unit的stride=2,且只重复1次
        # 经过这层之后,维度变为out_channels - in_channels + 直连边的维度(in_channels) = out_channels
        layers.append(shuffleNet_unit(in_channels, out_channels - in_channels,  
                                      stride=2, groups=groups))
        # 更新此时的in_channels
        in_channels = out_channels
        for i in range(num_layers - 1):
            layers.append(shuffleNet_unit(in_channels, out_channels, 
                                          stride=1, groups=groups))
        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.conv1(x)
        x = self.maxpool(x)
        x = self.stage2(x)
        x = self.stage3(x)
        x = self.stage4(x)
        x = self.globalpool(x)
        x = x.view(x.size(0), -1)
        out = self.fc(x)
        return out

(4)针对不同的g,设计不同的网络结构

在这里插入图片描述

def ShuffleNet_g1(**kwargs):
    num_layers = [4, 8, 4]
    num_channels = [144, 288, 576]
    model = ShuffleNet(1, num_layers, num_channels, **kwargs)
    return model


def ShuffleNet_g2(**kwargs):
    num_layers = [4, 8, 4]
    num_channels = [200, 400, 800]
    model = ShuffleNet(2, num_layers, num_channels, **kwargs)
    return model


def ShuffleNet_g3(**kwargs):
    num_layers = [4, 8, 4]
    num_channels = [240, 480, 960]
    model = ShuffleNet(3, num_layers, num_channels, **kwargs)
    return model


def ShuffleNet_g4(**kwargs):
    num_layers = [4, 8, 4]
    num_channels = [272, 544, 1088]
    model = ShuffleNet(4, num_layers, num_channels, **kwargs)
    return model


def ShuffleNet_g8(**kwargs):
    num_layers = [4, 8, 4]
    num_channels = [384, 768, 1536]
    model = ShuffleNet(8, num_layers, num_channels, **kwargs)
    return model
def test():
    net = ShuffleNet_g1()
    #创建模型,部署gpu
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    net.to(device)
    summary(net, (3, 224, 224))


if __name__ == '__main__':
    test()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

travellerss

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值