一些常见的CNN模型

最近闲着无聊在家敲了一些基本的CNN模型,这里对网上资料做一个简要的整理总结,供自己学习使用。

一、VGG

​ VGG模型是2014年ILSVRC竞赛的第二名,第一名是GoogLeNet。但是VGG模型在多个迁移学习任务中的表现要优于googLeNet。而且,从图像中提取CNN特征,VGG模型是首选算法。它的缺点在于,参数量有140M之多,需要更大的存储空间。但是这个模型很有研究价值。

VGG有多种网络结构,如VGG-11,VGG-13,VGG-16,VGG-19等。以吴恩达老师视频中讲到的VGG-16为例,

一些常见的CNN模型_第1张图片

VGG网络的特点:(1)采用了小卷积核。作者将卷积核全部替换为3x3。(2)作者采用了小池化核。相比AlexNet的3x3的池化核,VGG全部为2x2的池化核。(3)层数更深,特征图更宽。基于前两点外,由于卷积核专注于扩大通道数、池化专注于缩小宽和高,使得模型架构上更深更宽的同时,计算量的增加放缓。

​ VGG的提出,使人们得到了这样一个结论:卷积神经网络的深度增加和小卷积核的使用对网络的最终分类识别效果有很大的作用。

二、GoogLeNet

文章中提出优化模型有两种途径:提高深度或是宽度。但是这两种方法都存在局限性:1.参数太多,容易过拟合,若训练数据集有限;2.网络越大计算复杂度越大,难以应用;3.网络越深,梯度越往后穿越容易消失,难以优化模型。

GoogLeNet正是围绕着这两个思路提出的,其通过构建密集的块结构——Inception(如下图)来近似最优的稀疏结构,从而达到提高性能而又不大量增加计算量的目的。

一些常见的CNN模型_第2张图片

GoogLeNet增加了多种核1x1,3x3,5x5,还有直接max pooling的,但是如果简单的将这些应用到feature map上的话,concat起来的feature map厚度将会很大,所以在googlenet中为了避免这一现象提出的inception具有如下结构,在3x3前,5x5前,max pooling后分别加上了1x1的卷积核起到了降低feature map厚度的作用。5*5的卷积核会带来巨大的计算量,所以采用1 * 1的卷积核进行降维。(以下是一个Inception的代码)

class Inception(nn.Module):
    def __init__(self,in_channels, n1x1, n3x3_reduce, n3x3, n5x5_reduce, n5x5,         npool):
        super().__init__()
        self.brance1 = nn.Sequential(
            nn.Conv2d(in_channels,n1x1,kernel_size=1),
            nn.BatchNorm2d(n1x1),
            nn.ReLU(inplace=True)
        )
        
        self.brance2 = nn.Sequential(
            nn.Conv2d(in_channels,n3x3_reduce,kernel_size=1),
            nn.BatchNorm2d(n3x3_reduce),
            nn.ReLU(inplace=True),
            nn.Conv2d(n3x3_reduce,n3x3,kernel_size=3,padding=1),
            nn.BatchNorm2d(n3x3),
            nn.ReLU(inplace=True)
        )
        
        self.brance3 = nn.Sequential(
            nn.Conv2d(in_channels,n5x5_reduce,kernel_size=1),
            nn.BatchNorm2d(n5x5_reduce),
            nn.ReLU(inplace=True),
            nn.Conv2d(n5x5_reduce,n5x5,kernel_size=5,padding=2),
            nn.BatchNorm2d(n5x5),
            nn.ReLU(inplace=True)
        )
        
        self.brance4 = nn.Sequential(
            nn.MaxPool2d(3,stride=1,padding=1),
            nn.Conv2d(in_channels,npool,kernel_size=1),
            nn.BatchNorm2d(npool),
            nn.ReLU(inplace=True)
        )
        
    def forward(self,x):
        out = [self.brance1(x),self.brance2(x),self.brance3(x),self.brance4(x)]
        out = torch.cat(out,1)
        return out

为了解决深度问题,GoogLeNet巧妙的在不同深度处增加了两个loss来保证梯度回传消失的现象。总的网络结构如下:

一些常见的CNN模型_第3张图片

三、ResNet

ResNet的提出背景是网络越深效果越好,因为CNN越深越能提取到更丰富更抽象的特征,这些特征有更高更鲁棒的语义信息,网络越深对输入图像的变化越不敏感。但是单纯的深模型会遇到问题:模型深,易造成梯度消失,继续训练时,很难再更新参数,很难学到特征。

基于这种背景,何凯明团队于2015年提出了ResNet(Residual Networks 残差网络)。残差网络(如下)通过在一个浅层网络基础上叠加y=x层,可以让网络随深度增加而不退化。将浅层的输入值直接连接到端部位置,这样避免了层层映射过程中,由于权重小于1而导致的梯度消失。它一方面使得模型可以更深,这样网络表达的能力更好了,另一方面也使得训练速度加快。

一些常见的CNN模型_第4张图片

假设某段神经网络的输入是x,期望输出是H(x),如果直接把x传递到输出端,那么我们要学习的目标是F(x)=H(x)-x。此时目标相当于发生变更,称之为残差。(以下是一个残差模块的代码)

class BottleNeck(nn.Module):
    expansion = 4
    def __init__(self,in_channels,out_channels,stride=1):
        super().__init__()
        self.residual_function = nn.Sequential(
            nn.Conv2d(in_channels,out_channels,stride=1,bias=False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),
            nn.Conv2d(out_channels,out_channels,stride=stride,padding=1,bias=False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),
            nn.Conv2d(out_channels,out_channels*BottleNeck.expansion,\
                      kernel_size=1,bias=False),
            nn.BatchNorm2d(out_channels*expansion)
        )
        self.shortcut = nn.Sequential()
        if stride!=1 or in_channels!=out_channels*BottleNeck.expansion:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels,out_channels*BottleNeck.expansion,\
                          kernel_size=1,bias=False),
                nn.BatchNorm2d(out_channels*BottleNeck.expansion))
    def forward(self,x):
        return nn.ReLU(inplace=True)(self.residual_function(x)+self.shortcut(x))

残差网络可以设计18、34、50、101、152层,用的都是1x1和3x3的小卷积核。

四、RIR

RIR模型的全称是ResNet in ResNet,这是一种深度dual-stream架构,它对ResNets和标准的CNN进行了推广,并且很容易实现(没有额外的计算开销)。论文中指出当前的ResNet使用identity连接会导致不同级别的特征在每一层积聚,即使在一个深度网络,前面的一些层学习到的一些特征可能在后面的层不再提供有用的信息。因此RIR网络被提出,它保留了两个流:residual stream(残差流)和transient stream(原始卷积流)。它的结构如下:

一些常见的CNN模型_第5张图片

从图中可以看出一个2层的RIR模块由2个ResNet Init组成。ResNet Init结构如图b所示,dual stream通过自身卷积和交叉卷积相加得到结果,代码如下:

class ResnetInit(nn.Module):
    def __init__(self,in_channels,out_channels,stride):
        super().__init__()
        
        self.residual_stream_conv = nn.Conv2d(in_channels,out_channels,kernel_size=3,
                                              padding=1,stride=stride)
        self.transient_stream_conv = nn.Conv2d(in_channels,out_channels,kernel_size=3,
                                               padding=1,stride=stride)
        self.residual_stream_conv_across = nn.Conv2d(in_channels,out_channels,
                                                   kernel_size=3,padding=1,stride=stride)
        self.transient_stream_conv_across = nn.Conv2d(in_channels,out_channels,
                                                   kernel_size=3,padding=1,stride=stride)
        self.residual_bn_relu = nn.Sequential(
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True)
        )
        self.transient_bn_relu = nn.Sequential(
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True)
        )
        self.shortcut = nn.Sequential()
        
        if stride!=1 or in_channels!=out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels,out_channels,kernel_size=1,stride=stride)
            )
        
    def forward(self,x):
        x_residual,x_transient = x,x
        residual_r_r = self.residual_stream_conv(x_residual)
        residual_r_t = self.residual_stream_conv_across(x_transient)
        transient_t_t = self.transient_stream_conv(x_transient)
        transient_t_r = self.transient_stream_conv_across(x_residual)
        residual_shortcut = self.shortcut(x_residual)
        
        x_residual = self.residual_bn_relu(residual_r_r+residual_r_t+residual_shortcut)
        x_transient = self.transient_bn_relu(transient_t_r+transient_t_t)
        
        return x_residual,x_transient

五、InceptionResNet

谷歌于2016年提出了InceptionResNetV1和InceptionResNetV2。二者都是将Inception同ResNet相融合,加上了shortcut的分支。Inception提出的目的是使得模型更宽,ResNet提出的目的是使得模型更深,将二者结合可以使得模型又宽又深。模型结构如下:

一些常见的CNN模型_第6张图片

在InceptionResNet网络中,作者采用了三种不同的Inception-ResNet模块(Inception-resnet-A,Inception-resnet-B,inception-resnet-C)。三种模块结构如下:

一些常见的CNN模型_第7张图片

除此之外,作者还提出了Reduction模块(如下),分别接在Inception-resnet-A后(ReductionA)和Inception-resnet-B后(ReductionB)。Reduction的提出是为了补偿因Inception块引起的降维。k、l、m、n表示filter个数,可以在论文的表中进行查找。

一些常见的CNN模型_第8张图片

InceptionResNet-v1/v2将Inception和ResNet结合到一起,使得Inception减少了冗余度和复杂度,使它能够更快的收敛,更加容易训练。

六、PreActResNet

鉴于ResNet在非常深的网络结构中也会遇到优化问题(尽管我们一般不会使用非常深的网络),作者对ResNet进行改进,提出了PreAct的残差结构,如下图所示

一些常见的CNN模型_第9张图片

左图是原始的ResNet残差结构,右图是作者提出的PreActResNet残差结构。从图中我们可以看出,PreAct残差结构在进行卷积之前加入了BatchNorm和ReLU。通过实验我们得到这样的结论:不管网络有多深,整个网络中的梯度流都不会产生弥散问题。

class PreActBasic(nn.Module):

    expansion = 1
    
    def __init__(self, in_channels, out_channels, stride):
        super().__init__()
        self.residual = nn.Sequential(
            nn.BatchNorm2d(in_channels),
            nn.ReLU(inplace=True),
            nn.Conv2d(in_channels, out_channels, kernel_size=3, \
                      stride=stride, padding=1),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),
            nn.Conv2d(out_channels, out_channels * PreActBasic.expansion, \
                      kernel_size=3, padding=1)
        )

        self.shortcut = nn.Sequential()
        if stride != 1 or in_channels != out_channels * PreActBasic.expansion:
            self.shortcut = nn.Conv2d(in_channels, out_channels * PreActBasic.expansion,\                                       kernel_size=1, stride=stride)
    def forward(self, x):

        res = self.residual(x)
        shortcut = self.shortcut(x)

        return res + shortcut

七、ResNeXt

ResNeXt网络结构于2017年提出,在ResNet的基础上作出了进一步的改进。与InceptionResNet类似,作者提出这个模型的思想也是基于将模型做的更深及更宽。本文提出的 ResNeXt 结构可以在不增加参数复杂度的前提下提高准确率,还减少了超参数的数量。

在ResNeXt提出之前,有VGG系列和Inception系列网络模型。VGG系列模型主要是通过堆叠网络(即深度)来提高网络性能,Inception系列网络模型提出了split-transform-merge策略(即提高宽度)来提高网络性能。而ResNeXt将二者结合起来。下面是一个ResNeXt模块的结构:

一些常见的CNN模型_第10张图片

论文中提出了cardinality(the size of the set of transformations),图示是cardinality=32时,即存在32个并联的模块,在代码中是通过组卷积实现的,就是卷积中的group size,令group=cardinality。实验证明,增加 cardinality 比增加深度和宽度更有效。

class ResNeXtBlcok(nn.Module):
    def __init__(self,in_channels,out_channels,stride):
        super().__init__()
        
        C = CARDINALITY
        D = int(out_channels*DEPTH)//BASEWIDTH
        
        self.split_transform = nn.Sequential(
            nn.Conv2d(in_channels,C*D,kernel_size=1,groups=C,bias=False),
            nn.BatchNorm2d(C*D),
            nn.ReLU(inplace=True),
            nn.Conv2d(C*D,C*D,kernel_size=3,stride=stride,padding=1,groups=C),
            nn.BatchNorm2d(C*D),
            nn.ReLU(inplace=True),
            nn.Conv2d(C*D,out_channels*expansion,kernel_size=1,bias=False),
            nn.BatchNorm2d(out_channels*expansion)
        )
        
        self.shortcut = nn.Sequential()
        
        if stride!=1 or in_channels!=out_channels*4:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels,out_channels*expansion,kernel_size=1,\
                          stride=stride,bias=False),
                nn.BatchNorm2d(out_channels*expansion)
            )
            
        self.relu = nn.ReLU(inplace=True)
        
    def forward(self,x):
        shortcut = self.shortcut(x)
        split_transform = self.split_transform(x)
        out = shortcut+split_transform
        out = self.relu(out)
        
        return out

八、WideResNet

WideResNet的提出是因为ResNet的跳跃连接导致只有少量的残差块学习到特征。于是作者尝试着利用另一种思路:减少深度,增加宽度,就提出了WideResNet。

一些常见的CNN模型_第11张图片

图c就是在图a的基础上,拓宽了跳跃连接的宽度。图d在图c的基础上增加了dropout,以提高模型的泛化能力。

模型参数如下:

一些常见的CNN模型_第12张图片

在搭建好BasicBlock和BottleNeck之后,模型的代码如下:

class WideResNet(nn.Module):
    def __init__(self,block,block_num,wfactor,class_num=100):
        super().__init__()
        self.in_channel=16
        self.pre = nn.Sequential(
            nn.Conv2d(3,16,kernel_size=3,stride=1,padding=1),
            nn.BatchNorm2d(16),
            nn.ReLU(inplace=True)
        )
        self.stage1 = self._make_stage(block,block_num[0],16*wfactor,1)
        self.stage2 = self._make_stage(block,block_num[1],32*wfactor,2)
        self.stage3 = self._make_stage(block,block_num[2],64*wfactor,2)
        self.avgpool = nn.AvgPool2d(8,1)
        self.fc = nn.Linear(64*wfactor*block.expansion,class_num)
        
    def _make_stage(self,block,block_num,out_channels,stride):
        strides = [stride] + [1]*(block_num-1)
        layers = []
        for stride in strides:
            layers.append(block(self.in_channel,out_channels,stride=stride))
            self.in_channel = out_channels
        return nn.Sequential(*layers)
    
    def forward(self,x):
        x = self.pre(x)
        x = self.stage1(x)
        x = self.stage2(x)
        x = self.stage3(x)
        x = self.avgpool(x)
        x = x.view(x.size(0),-1)
        x = self.fc(x)
        return x 

九、Xception

在Xception模型中,作者提出了“depth-wise separable convolution”的结构。depthwise separable convolution就是先用M个3x3卷积核一对一卷积输入的M个feature map,不求和,生成M个结果;然后用N个1x1的卷积核正常卷积前面生成的M个结果,求和,最后生成N个结果。因此文章中将depthwise separable convolution分成两步,一步叫depthwise convolution,另一步是pointwise convolution。

class SeperableConv(nn.Module):
    def __init__(self,in_channels,out_channels,kernel_size,**kwargs):
        super().__init__()
        
        self.depthwise = nn.Conv2d(in_channels,in_channels,kernel_size,\
                                   groups=in_channels,bias=False,**kwargs)
        
        self.pointwise = nn.Conv2d(in_channels,out_channels,kernel_size=1,bias=False)
        
    def forward(self,x):
        x = self.depthwise(x)
        x = self.pointwise(x)
        return x

下图中为Xception结构的表示。它就是由Inception v3直接演变而来。其中引入了Residual learning的结构。

一些常见的CNN模型_第13张图片

Xception中引入了Entry/Middle/Exit三个flow,每个flow内部使用不同的重复模块。Entry flow主要是用来不断下采样,减小空间维度;中间的Middle flow是最核心的部分,不断学习关联关系,优化特征;最终则是通过Exit flow汇总、整理特征,用于交由FC来进行表达。

十、DenseNet

DenseNet于2017年提出。我们知道ResNet的提出,是通过shortcut来避免出现梯度消失/爆炸。因为当梯度消失/爆炸时,输入的特征不易于学习。DenseNet的核心思想就是在每一层上都加上一个单独的shortcut,使得任意两层都能直接连通。DenseNet的模型如下:

一些常见的CNN模型_第14张图片

在代码实现中,是通过torch.cat操作实现任意两层互联。各层的参数如下:

一些常见的CNN模型_第15张图片

其中,k表示growth_rate,即每个Dense Block中每层输出的feather map数量。BottleNeck是由1x1和3x3两层卷积层组成,目的是降维,减少计算量。Transition Layer层中包含了卷积层和池化层,使得输出通道比输入通道减少一半,并减少计算量。

实验结果证明,DenseNet在与ResNet同等复杂度和参数的情况下,效果要优于ResNet。

十一、SENet

SENet的全称是Squeeze and Excitation Networks,顾名思义,就是在网络中加入了Squeeze和Excitation操作。作者提出这个网络的目的是显式地建模卷积特征通道之间的相互依赖性来提高网络的表达能力。网络结构如下:

一些常见的CNN模型_第16张图片

其中,Squeeze通过空间维度压缩特征,在空间上做全局平均池化,每个通道的二维特征变成了一个实数,这个实数某种程度上具有全局的感受野,并且输出的维度和输入的通道数相匹配。而Excitation的作用是用两个全连接层和Sigmoid函数得到各通道的权值,第一个全连接层会压缩通道数,减少计算量。该权值与Fscale得到的feature map结合得到最后的结果。下图是将SE模块嵌入Inception和ResNet中的例子:

一些常见的CNN模型_第17张图片

与ResNet结合的代码如下:

class BasicResidualSEBlock(nn.Module):
    expansion = 1
    def __init__(self,in_channels,out_channels,stride,r=16):
        super().__init__()
        self.residual = nn.Sequential(
            nn.Conv2d(in_channels,out_channels,kernel_size=3,stride=stride,padding=1),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),
            nn.Conv2d(out_channels,out_channels*self.expansion,kernel_size=3,padding=1),
            nn.BatchNorm2d(out_channels*self.expansion),
            nn.ReLU(inplace=True)
        )
        self.shortcut = nn.Sequential()
        if stride!=1 or in_channels!=out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels,out_channels*self.expansion,\
                		  kernel_size=1,stride=stride),
                nn.BatchNorm2d(out_channels*self.expansion),
                nn.ReLU(inplace=True)
            )
        self.squeeze = nn.AdaptiveAvgPool2d(1)
        self.excitation = nn.Sequential(
            nn.Linear(out_channels*self.expansion,out_channels*self.expansion//r),
            nn.ReLU(inplace=True),
            nn.Linear(out_channels*self.expansion//r,out_channels*self.expansion),
            nn.Sigmoid()
        )
        
    def forward(self,x):
        shortcut = self.shortcut(x)
        residual = self.residual(x)
        
        squeeze = self.squeeze(residual)
        squeeze = squeeze.view(squeeze.size(0), -1)
        excitation = self.excitation(squeeze)
        excitation = excitation.view(residual.size(0),residual.size(1),1,1)
        
        x = residual*(excitation.expand_as(residual))+shortcut
        return nn.ReLU(inplace=True)(x)

十二、SqueezeNet

SqueezeNet 发表于ICLR-2017,作者分别来自Berkeley和Stanford,SqueezeNet不是模型压缩技术,而是 “design strategies for CNN architectures with few parameters”。SqueezeNet的思想与Inception非常类似,其提出了squeeze层和expand层。Squeeze层只采用了1x1的卷积核,使得卷积过后的通道尽可能小,以减少计算量。然后进入expand层,expand层的卷积核由1x1和3x3的卷积核构成(如下图),其思想就和Inception类似。

一些常见的CNN模型_第18张图片

s1x1 (squeeze convolution layer中1x1filter的个数)、e1x1(expand layer中1∗1filter的个数)、e3x3(expand layer中3x3 filter的个数)都可以进行调整。

这里给出一个子模块的代码:

class Fire(nn.Module):
    def __init__(self,in_channel,out_channel,squeeze_channel):
        super().__init__()
        
        self.squeeze = nn.Sequential(
            nn.Conv2d(in_channel,squeeze_channel,kernel_size=1),
            nn.BatchNorm2d(squeeze_channel),
            nn.ReLU(inplace=True)
        ) 
        
        self.expand1x1 = nn.Sequential(
            nn.Conv2d(squeeze_channel,int(out_channel//2),kernel_size=1),
            nn.BatchNorm2d(int(out_channel//2),),
            nn.ReLU(inplace=True)
        )
        
        self.expand3x3 = nn.Sequential(
            nn.Conv2d(squeeze_channel,int(out_channel//2),kernel_size=3,padding=1),
            nn.BatchNorm2d(int(out_channel//2)),
            nn.ReLU(inplace=True)
        )
        
    def forward(self,x):
        x = self.squeeze(x)
        return torch.cat([self.expand1x1(x),self.expand3x3(x)],1)

文中给了三种不同的网络模型:

一些常见的CNN模型_第19张图片

左边为原始的SqueezeNet,中间为包含simple bypass的改进版本,最右侧为使用complex bypass的改进版本。在下表中给出了更多的细节。关于bypass,个人理解就像shortcut,使得模型更容易优化。

十三、MobileNet

MobileNet V1,2017年Google人员发表,针对手机等嵌入式设备提出的一种轻量级的深层神经网络,采用了深度可分离的卷积。核心思想就是卷积核的巧妙分解,可以有效减少网络参数。它将标准卷积分解成一个深度卷积和一个点卷积(1 × 1卷积核),即Depthwise和Pointwise。如下图:

一些常见的CNN模型_第20张图片

Depthwise将每个卷积核应用到每一个通道,体现在代码中的group,而Pointwise中1 × 1卷积则用来组合通道卷积的输出。

class DepthSeperable(nn.Module):
    def __init__(self,in_channels,out_channels,kernel_size,**kwargs):
        super().__init__()
        self.depthwise = nn.Sequential(
            nn.Conv2d(in_channels,in_channels,kernel_size=kernel_size,\
                      groups=in_channels,**kwargs),
            nn.BatchNorm2d(in_channels),
            nn.ReLU(inplace=True)
        )
        
        self.pointwise = nn.Sequential(
            nn.Conv2d(in_channels,out_channels,kernel_size=1),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True)
        )
        
    def forward(self,x):
        x = self.depthwise(x)
        x = self.pointwise(x)
        
        return x

而后,通过一个流线型的架构(堆叠式),都是由深度可分离的卷积模块堆叠的,构成最后的模型。具体的参数如下:

一些常见的CNN模型_第21张图片

MobileNetV2则是在V1的基础上做出了改进,先进行1x1的Pointwise通道扩张,Depthwise+Pointwise通道压缩,然后Linear激活。

十四、ShuffleNet

ShuffleNetV1,2017年旷视科技的Xiangyu Zhang,Jian Sun发表,参考MobileNet和ResNet,提出了Channel Shuffle来增强分组卷积的全局信息流通。ShuffleNetV1是专门为计算能力有限的移动平台设计。采用两个新操作——逐渐群卷积(pointwise group convolution)和通道混洗(channel shuffle)在保障精确率损失不大的同时大大减少了计算成本。

ShuffleNetV1最大的创新点是在之前模型的基础上进一步加入了channel shuffle。假设引入group操作,设group为g,那么N个输入feature map就被分成了g个group,M个filter就被分成g个group。然后在做卷积操作时,第一个group的M/g个filter中的每一个都和第一个group的N/g个输入feature map做卷积得到结果。第二个group同理,直至最后一个group。这种操作大大减少了计算量。但如果多个group叠加在一起,如有两个卷积层都有group操作,就会产生边界效应。即某个输出channel仅来自输入channel的一小部分,这样学出来的特征会非常局限。于是需要channel shuffle来解决。效果如下图:

一些常见的CNN模型_第22张图片

在进行GConv之前对其输入feature map做一个分配,也就是每个group分成几个subgroup,然后将不同group的subgroup作为GConv2的一个group的输入,使得GConv2的每一个group都能卷积输入的所有group的feature map,如(c)中的channel shuffle。其代码如下:

class ChannelShuffle(nn.Module):
    def __init__(self,group):
        self.group = group
    
    def forward(self,x):
        batch_size,channels,height,width = x.data.size() 
        channel_per_group = channels/self.group
        
        x = x.view(batch_size,self.group,channel_per_group,height,width)
        x = x.transpose(1,2).contiguous()  #这一步画一下图就明白了
        x = x.view(batch_size,-1,height,width)
        
        return x

ShuffleNetV2根据提出的4则轻量化标准改进了ShuffleNet V1:通道宽度相同、减少分组卷积、减少分支、减少Element-wise。主要是在网络结构及参数上做出了改动。

一些常见的CNN模型_第23张图片

a和b是ShuffleNet v1的两种不同block结构,两者的差别在于后者对特征图尺寸做了缩小。c和d是ShuffleNet v2的两种不同block结构。从a和c的对比可以看出c在开始处增加了一个channel split操作,该操作将输入特征的通道分开(文中是对半切分),这样操作的目的是使得运行速度最快。然后c中取消了1x1卷积层中的group操作,这和同时前面的channel split其实已经算是变相的group操作了。其次,channel shuffle的操作移到了concat后面,同时也是因为第一个1x1卷积层没有group操作,所以在其后面跟channel shuffle也没有太大必要。最后是将element-wise add操作替换成concat,这是为了减少element-wise操作。多个c结构连接在一起的话,channel split、concat和channel shuffle是可以合并在一起的。b和d的对比也是同理,只不过因为d的开始处没有channel split操作,所以最后concat后特征图通道数翻倍。

关于ShuffleNetV2的详细解释可以参考https://blog.csdn.net/u014380165/article/details/81322175

你可能感兴趣的:(深度学习,python)