一文详解轻量化卷积神经网络ShuffleNet V1

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

beginning

今天给小伙伴介绍一个轻量化的卷积神经网络------ShuffleNet V1 。ShuffleNet是国内AI四小龙之一(其他三个分别是商汤、依图、云从)的旷视科技提出的最具代表性算法,特别设计用于在计算资源受限的设备上进行高效的图像分类任务 。它采用了一些创新的设计来减少模型的计算量和参数数量 ,同时由于不错的性能,被广泛应用到移动设备和应用上,比如人脸解锁、短视频特效、测体温、重识别等等。废话不多说啦,如果你也对轻量化网络感兴趣,想了解ShuffleNet的独特之处,请跟我一起愉快的学习叭🎈🎈🎈

1.ShuffleNet的两个创新点

之前在介绍知识蒸馏时,我们是要把冗余大模型抽取成轻量化小模型,这其实就对应着轻量化网络其中的一个技术路线(总结的四个路线如下):

  1. 压缩已经训练好的模型:知识蒸馏,权值量化,剪枝,注意力迁移等
  2. 直接训练轻量化网络:SqueezeNet、MobileNet、Xception、ShuffleNet等
  3. 加速卷积运算:低秩分解,im2col+GEMM等
  4. 硬件部署:TensorRT、Tensorflow-slim、FPGA、集成电路等

其实我们今天要学的ShuffleNet就属于第二个路线------直接设计并训练出一个轻量化的网络。与现有网络模型相比,ShuffleNet呢有两个创新点------分组1×1卷积(Group Point Convolution)和通道重排(Channel Shuffle)。下面就让我们一个个来学习理解吧🌈🌈🌈

1.1分组1×1卷积

咱们先不看分组1×1卷积,先来了解一下分组卷积是怎么回事🧐🧐🧐

标准的卷积是一个卷积核在输入上滑动,输入有多少个通道,卷积核就有多少个通道。就是说,一个卷积核是处理所有通道的,一个卷积核就对应一个二维的feature map(或者说是channel)。把卷积核的权重和输入对应位置的像素相乘求和(即向量的点乘),依次卷积生成一个标量填在输出的对应位置。

如上图所示,输入是三通道,那么卷积核也得是三通道的,一个卷积核对应一个二维的特征图。如果有128个卷积核就会生成128个特征图,我们把这128张"纸"摞起来,就变成最终的卷积结果。把这结果喂到下一层,那么下一层的卷积核就会有128个通道✨✨✨

与标准卷积不同,在分组卷积中,我们让每个卷积核只处理一部分通道,如下图所示。比如说我用三个卷积核,红色的卷积核只处理红色输入的两个通道,绿色只处理绿色输入通道,黄色只处理黄色通道。这时候仍然是两个卷积核通道在这两个输入里面滑动卷积,仍然是一个卷积核生成一个feature map✨✨✨

下面这张图(来自Condensenet这篇论文)可能会更清楚地说明分组卷积。左侧原始的卷积是一个卷积核处理所有通道,这里用了6个卷积核生成了6个feature map,每一个卷积核都是12个通道。现在呢,我把输入的这12个通道分成黄绿黄三组,每个卷积核只处理4个通道。那么红色的卷积核有两个,只处理4个红色通道;绿色的卷积核有两个,只处理4个绿色通道;黄色的卷积核也有两个,只处理4个黄色通道。最后就会生成红绿黄的channel各两个✨✨✨

为什么要分组呢?我们算一下参数数量就明白啦 :在常规卷积里,假设卷积核尺寸是3×3,通道数是12,一共有6个卷积核,那么它的参数量是3×3×12×6=648 。现在分组卷积的话,卷积核尺寸仍然是3×3,但它的通道数变为原来的三分之一,卷积核个数为2,共有三组,那么参数量为3×3×(12/3)×(6/3)×3=216 。这时候小伙伴们就看明白了叭,分成三组的话,它的参数量就降为原来的三分之一,当然计算量也会相应的降低。这就是分组卷积的好处,可以显著地降低参数量和计算量✨✨✨

小伙伴们大开一下脑洞,想象一下,若这时候我们来做个特例------把分组卷积的组数进一步地增多,有多少个输入的channel我们就分为多少个组,也就是说每个卷积核只处理一个输入的channel,那么这个时候就变成了深度可分离卷积Depthwise Convolution 。通俗讲,输入是一个薄片,卷积核也是一个薄片,这两个薄片来做卷积,然后把得到的通道摞起来,再用1×1卷积,即先Depthwise,再Pointwise,这其实就是MobileNet的原理,如下图所示(一文了解多种卷积原理,真不戳)😁😁😁

明白了分组卷积之后,咱们再来看分组1×1卷积就简单多了。1×1卷积是非常密集的,且目前算法都没有考虑到1×1卷积的优化加速,但就是这个1×1卷积它需要相当的复杂度和算力,绝大部分计算都消耗在1×1卷积上。针对这个问题,ShuffleNet对1×1卷积做出了优化,即把分组用在了1×1卷积上(如上文所讲)。

1.2通道重排

分组卷积固然好,但是小伙伴们有没有发现其中存在的问题腻🧐🧐🧐(仔细想一想)没戳,就是"近亲繁殖"问题。你可能一头雾水,且听我慢慢道来。

大家看下图中的(a),卷积呢分成了红绿蓝三组,红的跟红的卷积,绿的跟绿的卷积,蓝的跟蓝的卷积,生成了红绿蓝三组,再卷积再生成了三个组......你会发现,红色永远都在跟红色卷积,绿色蓝色也是一样,这三个组之间是没有任何信息交融的,每一个组都是"近亲繁殖" 。在大自然中我们都知道,近亲繁殖是不好的,它会丧失掉基因的多样性。基于此,ShuffleNet引入了通道重排(channel shuffle)操作 。现在我们把红绿蓝每个"村子"等分成三份,把每个村子的第一份收集起来,作为下一个组;第二份收集起来,作为下一个组;第三份收集起来,作为下一个组。正如图中(b)和(c)所示(b图和c图是等价的)。这样就实现了跨组、跨group的信息交流,从原来的"近亲繁殖"变成了"混血儿"✨✨✨

那在ShuffleNet里面具体是怎么进行通道重排的呢 ?过程如下图所示。把原来的分成了三个组,每一个组我们等分成四份,每一个圆圈都表示一个或多个channel。首先把它Reshape成g行n列的矩阵,其中,g表示分组卷积的组数(这里红蓝绿是3),n表示等分成几份(这里为4)。然后把这个矩阵进行线性代数中的转置操作,即第一行变成第一列,第二行变成第二列......最后进行Flatten操作,以上就是通道重排的过程。经过Reshape、Transpose和Flatten之后,每一个组里面都包含了原来三个组的信息,所以这可以直接调用pytorch的API来实现,非常高效,它也是可微分可导的,能端到端的训练;并且也没有引入额外的计算量✨✨✨

2.ShuffleNet网络结构

明白了分组卷积和通道重排之后,咱们再来看一看ShuffleNet的基本设计单元。

下图中的(a)是由ResNet中的bottleneck模块改进的,把原来的3×3标准卷积改成了3×3的Depthwise卷积。先1×1卷积降维,然后再3×3Depthwise卷积 ,接着再1×1卷积升维,最后恒等映射来个逐元素相加再ReLU激活✨✨✨

图中的(b)是在ShuffleNet里面,我们把1×1卷积降维和升维都改成了组卷积GConv ,可以有效的降低参数量和计算量;为了防止"近亲繁殖",在降维之后引入了通道重排,升维之后就不再引入了,因为我们发现目前这个效果就挺好的。(b)就是一个标准的ShuffleNet V1模块✨✨✨

如果要下采样的话,就变成了图中的(c)。在featue map的长宽方向要缩减为原来的一半,通道数要加倍,那么就在shortcut那一路引入了一个步长为2的平均池化,把这两路摞起来,就实现通道数加倍了。(a)和(b)都是逐元素的相加求和,而(c)是把两路摞起来,换成了Concat操作。下采样就用(c),不下采样就用(b)✨✨✨

了解了基本单元之后,ShuffleNet网络结构其实就是由若干个这样的基本单元堆叠起来的。不同分组数的ShuffleNet网络结构如下图所示。

首先输入一个224×224的彩色图像,对这个彩色图像先进行一次普通卷积和最大池化 ,得到的是24个通道,这24个通道就有了Stage2、Stage3和Stage4这三个阶段。这三个阶段用的ShuffleNet模块分别是4、8、4 。在每个Stage的第一个模块要用到下采样模块(即c模块),其他的模块都用的普通模块(即b模块),跨Stage的时候会下采样一次,通道数会加倍 ,然后得到的是一个7×7大小的feature map,最后再用一个全局平均池化(比如把7×7×960变成1×1×960),再接一个1000个神经元的全连接层作为分类,输出logits,对logits进行softmax,得到1000个类别的概率🌟🌟🌟

3.ShuffleNet代码实战

旷视自己开源了一个ShuffleNet代码,其中blocks.py是它的基本模块,network.py是由这些模块堆叠而成的整个ShuffleNet V1的网络,train.py是训练ImageNet图像分类,utils.py是一些常用的函数。小伙伴们可以直接按照它的文档来复现。学完原理之后,咱们一起来看看在代码中是怎么体现的叭🌞🌞🌞

基本模块单元重点代码

python 复制代码
class ShuffleV1Block(nn.Module):
    def __init__(self, inp, oup, *, group, first_group, mid_channels, ksize, stride):
        super(ShuffleV1Block, self).__init__()
        self.stride = stride
        assert stride in [1, 2]

        self.mid_channels = mid_channels
        self.ksize = ksize
        pad = ksize // 2
        self.pad = pad
        self.inp = inp
        self.group = group

        if stride == 2:
            outputs = oup - inp
        else:
            outputs = oup

        branch_main_1 = [
            # pw
            nn.Conv2d(inp, mid_channels, 1, 1, 0, groups=1 if first_group else group, bias=False),
            nn.BatchNorm2d(mid_channels),
            nn.ReLU(inplace=True),
            # dw
            nn.Conv2d(mid_channels, mid_channels, ksize, stride, pad, groups=mid_channels, bias=False),
            nn.BatchNorm2d(mid_channels),
        ]
        branch_main_2 = [
            # pw-linear
            nn.Conv2d(mid_channels, outputs, 1, 1, 0, groups=group, bias=False),
            nn.BatchNorm2d(outputs),
        ]
        self.branch_main_1 = nn.Sequential(*branch_main_1)
        self.branch_main_2 = nn.Sequential(*branch_main_2)

        if stride == 2:
            self.branch_proj = nn.AvgPool2d(kernel_size=3, stride=2, padding=1)
python 复制代码
def forward(self, old_x):
        x = old_x
        x_proj = old_x
        x = self.branch_main_1(x)
        if self.group > 1:
            x = self.channel_shuffle(x)
        x = self.branch_main_2(x)
        if self.stride == 1:
            return F.relu(x + x_proj)
        elif self.stride == 2:
            return torch.cat((self.branch_proj(x_proj), F.relu(x)), 1)
python 复制代码
def channel_shuffle(self, x):
        batchsize, num_channels, height, width = x.data.size()
        assert num_channels % self.group == 0
        group_channels = num_channels // self.group
        
        x = x.reshape(batchsize, group_channels, self.group, height, width)
        x = x.permute(0, 2, 1, 3, 4)
        x = x.reshape(batchsize, num_channels, height, width)

        return x

代码是用pytorch实现的。在定义的ShuffleV1Block()类中,branch_main_1和branch_main_2依次进行了1×1分组卷积降维、3×3Depthwise卷积、1×1分组卷积升维操作,如果是下采样的话,还要执行下采样-concat拼接操作。在函数channel_shuffle()中定义了通道重排。最后在函数forward()执行这些定义好的模块。代码不难,小伙伴们照着上文给的图和表仔细看看叭🌈🌈🌈

搭建ShuffleNet V1网络结构

python 复制代码
class ShuffleNetV1(nn.Module):
    def __init__(self, input_size=224, n_class=1000, model_size='2.0x', group=None):
        super(ShuffleNetV1, self).__init__()
        print('model size is ', model_size)

        assert group is not None

        self.stage_repeats = [4, 8, 4]
        self.model_size = model_size
        if group == 3:
            if model_size == '0.5x':
                self.stage_out_channels = [-1, 12, 120, 240, 480]
            elif model_size == '1.0x':
                self.stage_out_channels = [-1, 24, 240, 480, 960]
            elif model_size == '1.5x':
                self.stage_out_channels = [-1, 24, 360, 720, 1440]
            elif model_size == '2.0x':
                self.stage_out_channels = [-1, 48, 480, 960, 1920]
            else:
                raise NotImplementedError
        elif group == 8:
            if model_size == '0.5x':
                self.stage_out_channels = [-1, 16, 192, 384, 768]
            elif model_size == '1.0x':
                self.stage_out_channels = [-1, 24, 384, 768, 1536]
            elif model_size == '1.5x':
                self.stage_out_channels = [-1, 24, 576, 1152, 2304]
            elif model_size == '2.0x':
                self.stage_out_channels = [-1, 48, 768, 1536, 3072]
            else:
                raise NotImplementedError
python 复制代码
 # building first layer
        input_channel = self.stage_out_channels[1]
        self.first_conv = nn.Sequential(
            nn.Conv2d(3, input_channel, 3, 2, 1, bias=False),
            nn.BatchNorm2d(input_channel),
            nn.ReLU(inplace=True),
        )
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        self.features = []
        for idxstage in range(len(self.stage_repeats)):
            numrepeat = self.stage_repeats[idxstage]
            output_channel = self.stage_out_channels[idxstage+2]

            for i in range(numrepeat):
                stride = 2 if i == 0 else 1
                first_group = idxstage == 0 and i == 0
                self.features.append(ShuffleV1Block(input_channel, output_channel,
                                            group=group, first_group=first_group,
                                            mid_channels=output_channel // 4, ksize=3, stride=stride))
                input_channel = output_channel

        self.features = nn.Sequential(*self.features)

        self.globalpool = nn.AvgPool2d(7)

        self.classifier = nn.Sequential(nn.Linear(self.stage_out_channels[-1], n_class, bias=False))
        self._initialize_weights()

定义好基本单元之后就可以搭建出整个网络结构了。self.stage_repeats = [4, 8, 4]指的是Stage2为4,Stage3为8,Stage4为4。最基础的分组数group=3,它的各层channel个数是240、480和960。如果group=8的话,就变为了384、768和1536。在定义它的每一个层的代码中,依次进行了普通卷积和池化、遍历每一个stage、遍历每一个Block、全局平均池化、输出分类🌈🌈🌈


ending

看到这里相信盆友们都对轻量化卷积神经网络ShuffleNet有了一个全面深入的了解啦🌴🌴🌴很开心能把学到的知识以文章的形式分享给大家。如果你也觉得我的分享对你有所帮助,please一键三连嗷!!!下期见

相关推荐
GIOTTO情9 分钟前
媒介宣发的技术革命:Infoseek如何用AI重构企业传播全链路
大数据·人工智能·重构
阿里云大数据AI技术17 分钟前
云栖实录 | 从多模态数据到 Physical AI,PAI 助力客户快速启动 Physical AI 实践
人工智能
小关会打代码25 分钟前
计算机视觉进阶教学之颜色识别
人工智能·计算机视觉
IT小哥哥呀31 分钟前
基于深度学习的数字图像分类实验与分析
人工智能·深度学习·分类
机器之心1 小时前
VAE时代终结?谢赛宁团队「RAE」登场,表征自编码器或成DiT训练新基石
人工智能·openai
机器之心1 小时前
Sutton判定「LLM是死胡同」后,新访谈揭示AI困境
人工智能·openai
大模型真好玩1 小时前
低代码Agent开发框架使用指南(四)—Coze大模型和插件参数配置最佳实践
人工智能·agent·coze
jerryinwuhan1 小时前
基于大语言模型(LLM)的城市时间、空间与情感交织分析:面向智能城市的情感动态预测与空间优化
人工智能·语言模型·自然语言处理
落雪财神意1 小时前
股指10月想法
大数据·人工智能·金融·区块链·期股
中杯可乐多加冰1 小时前
无代码开发实践|基于业务流能力快速开发市场监管系统,实现投诉处理快速响应
人工智能·低代码