ResNet改进之ResNext学习笔记 (附代码)

本文介绍了何恺明的ResNeXt模型,它是VGG、ResNet和Inception的结合体,核心创新是用平行堆叠相同拓扑结构的blocks代替ResNet三层卷积block。文中阐述了设计原因,介绍了模型设计原则、聚合变换等内容,还给出了BasicBlock、Bottleneck模块等代码实现。

前言

论文地址:https://arxiv.org/abs/1611.05431

代码地址:https://gitcode.net/mirrors/facebookresearch/ResNeXt?utm_source=csdn_github_accelerator

1.是什么

何恺明大神的又一经典之作: ResNeXt(《Aggregated Residual Transformations for Deep Neural Networks》)。这个网络可以被解释为 VGG、ResNet 和 Inception 的结合体,它通过重复多个block(如在 VGG 中)块组成,每个block块聚合了多种转换(如 Inception),同时考虑到跨层连接(来自 ResNet)。

ResNeXt就是一种典型的混合模型,由基础的Inception+ResNet组合而成,本质在gruops分组卷积,核心创新点就是用一种平行堆叠相同拓扑结构的blocks代替原来 ResNet 的三层卷积的block,在不明显增加参数量级的情况下提升了模型的准确率,同时由于拓扑结构相同,超参数也减少了,便于模型移植。
 

2.为什么?

设计架构越来越困难,越来越多的超参数(宽度2,过滤尺寸,步幅等),尤其是当有许多层时。 VGG-Net [36]展示了构建非常深网络的简单而有效的策略:堆叠相同shape的组件。该策略继承于Resnet[14],Resnet堆叠了相同拓扑的模块。这个简单的规则减少了超参数的自由选择,深度揭示了神经网络中的基本维度。此外,我们认为这条规则的简单性可能会降低过度调整超参数到特定数据集的风险。通过各种视觉识别任务[7,10,9,28,31,14]并通过涉及语音[42,30]和语言[4,41,20]的非视觉任务已经被证明的VGGNET和RESNET的鲁棒性。
与VGG-nets不同,Inception模型系列[38,17,39,37]已经证明,精心设计的有着较低理论复杂性的拓扑结构足以实现令人信服的精确率。随着时间的推移,Inception模型已经进化[38,39],但重要的常见属性是分裂变换合并策略。在Inception模块中,输入被分成几维低维的输入(按1×1卷积),由一组专用过滤器(3×3,5×5等)转换,并通过连接合并。可以表明,这种架构的解决方案是在高维输入的单个大网络层(例如,5×5)的解空间的严格子空间。预计Inception模块的分流变换合并行为将接近大而致密层的代表性,但是具有相当较低的计算复杂性。

尽管准确性良好,但Inception模型的实现已经伴随着一系列复杂的因素——过滤器数量和大小是为每个单独的变换定制的并且模块是分阶段自定义的。虽然这些组件的仔细组合产生了优秀的神经网络结构,但通常不清楚如何将初始化架构调整到新的数据集/任务,特别是当有许多因素和超参数时要设计。

本文提出了一个简单架构,采用 VGG/ResNets 重复相同网络层的策略,以一种简单可扩展的方式延续Split-Transform-Merge 策略。将ResNet中高维特征图分组为多个相同的低维特征图,然后在卷积操作之后,将多组结构进行求和,最后得到ResNeXt模型。

Inception系列:采用多分支结构Split- Transform-Merge(分割-变换-聚合)

1) Split:将向量x分成低维嵌入表示;(由1x1卷积降维)

2) Transform:每个低维特征经过一个线性变换;(再由3x3或者5x5的卷积进一步提取特征)

3) Merge:通过单位加合成最后的输出;(最后拼接各分支的特征)

        不足:但是每个映射变换要量身定制卷积核数量、尺寸,模块在每一阶段都要改变。尤其将 Inception 模型用于新的数据或者任务时如何修改并不清晰。

3.怎么样?

3.1模型

我们采用高度模块化的设计,遵循VGG/ResNets.我们的网络由堆叠的残差块组成。这些块具有相同的拓扑,并受到VGG / Resnets的两个简单规则的影响:(i)如果产生相同大小的空间映射,则该块共享相同的超参数(宽度和滤波器大小),以及(II )每次当空间映射下采样因子为2时,块的宽度就乘以2。第二条规则确保计算复杂性,按照浮点操作(浮点操作中,在#中乘法添加)对于所有块大致相同。
通过这两个规则,我们只需要设计模板模块,并且可以相应地确定网络中的所有模块。

模型设计两个原则:

(1)如果输出的空间尺寸一样,那么模块的超参数(宽度和卷积核尺寸)也是一样的。

(2)每当空间分辨率/2(降采样),则卷积核的宽度*2。这样保持模块计算复杂度。

作用:

这两条规则大大缩小了设计空间,让我们可以专注于几个关键因素。

3.2 回顾单个神经元

人工神经网络中最简单的神经元执行内积(加权和),这是由全连通和卷积层完成的初等变换。内积可以看作是一种聚集变换形式:


其中x = [ x1 , x2 , … , xD ]是神经元的一个具有D通道的输入向量,并且wi 是一个卷积核第i通道权重。这个操作(通常包括一些输出非线性)被称为“神经元”。参见图2。


 可以将上述操作理解为分裂、变换和聚合的组合。
(i) 分割: 将向量x切为低维输入,其中为单维子空间xi 
(ii)变换:对低维表示进行变换,并对其进行简单缩放:wixi 
(iii)聚合:所有输入的变换由公式(1)聚合

3.3 聚合变换

对于一个ResNeXt Block中的基数块输出可以表示为:

其中,参数C 代表基数块的数目,Ti 代表对应的基数块,将x投影到一个(可选的低维)集成中,然后对其进行变换。

这拓展了VGG设计原则:从重复相同大小的层,到重复相同拓扑的卷积核组。

在本文中,我们考虑一种设计变换函数的简单方法:所有的Ti都有相同的拓扑结构。

在这种情况下,每个Ti中的第一个1×1层产生低维嵌入。那么对应的残差网络输出就可以被表示为:


 

 图像表示

具体操作

Splitting: 通过1×1卷积实现低维嵌入,256个通道变成4个通道,总共32个分支(cardinality = 32)

Transforming: 每个分支进行变换(对网络层对数据操作)

Aggregating: 对32个分支得到的变换结果—特征图,进行聚合

Block的三种等效形式
(a)表示先划分,单独卷积并计算输出,最后输出相加。split-transform-merge三阶段形式

(b)表示先划分,单独卷积,然后拼接再计算输出。将各分支的最后一个1×1卷积聚合成一个卷积。

(c)就是分组卷积。将各分支的第一个1×1卷积融合成一个卷积,3×3卷积采用group(分组)卷积的形式,分组数=cardinality(基数)

(c)结构分析

(1)首先通过一个1x1的卷积层进行降维处理,将它的channel从256降低到128,

(2)然后在通过group卷积进行处理,这里group 卷积的卷积核为3x3它的groups数为32,它所输出的channel也是等于128,

(3)接着通过1x1卷积对它进行升维

(4)最后将它的输出与我们的输入进行相加得到最后输出。


三个结构为什么等价?
(1)b和c等价

第一层:

过程:首先从(b)到(c)这个过程,对于(b)中第一层通过包括32个分支,每个分支(path)卷积核个数为4的1x1卷积,对于每个path而言它的卷积核大小都是1x1,channel为256,又由于我们path的个数为32,就可以简单将他们合并在一起,变为( c)图中第一层了。

参数:

(b)第一层 256×1×1×4×32=32768

(c)第一层 256×1×1×128=32768

第二层:

过程:和group卷积其实是一样的,对于每个path可以理解为一个group,每个组的输入输出channel为原来的1/group,对于每个组采用3x3的卷积核,卷积之后将特征矩阵进行concate拼接,所以图(b)第二层也是与图(c)第二层 group为32的组卷积也是等价的。

参数:

(b)第二层 4×3×3×4×32=4608

(c)第二层 128/32×3×3×128 = 4608

(2)a和b等价

过程:在(a)中,4维特征图通过1x1卷积变为256维,然后32个256维数据求和,而在(b)中,是先将4维数据concat成128维,在利用1x1卷积,实际上也就是求和过程。

参数:

(a)第三层 4×1×1×256×32=32768

(b)第三层 128×1×1×256 = 32768
 


 分组卷积

在AlexNet时就曾提出,由受限于当时硬件的限制,作者不得不将卷积操作拆分到两台GPU上运行,这两台GPU的参数是不共享的。 两组卷积核学习两种不同的特征,一组学习纹理,另一组学习色彩。

操作:

分组卷积层中,输入和输出的 channels 被分为 C 个 groups,分别对每个 group 进行卷积操作。

优点:

(1)减少参数量,分成G组,则该层的参数量减为原来的1/G。

(2)让网络学习到不同的特征,每组卷积学习到的特征不一样,获得更丰富的信息。

(3)分组卷积可以看做是对原来的特征图进行了一个dropout,有正则的效果。

3.4 代码实现

(1)BasicBlock模块

基础Block模块,也就是对应18/34层的BasicBlock,这里实现和ResNet一样。

'''-------------一、BasicBlock模块-----------------------------'''
# 用于ResNet18和ResNet34基本残差结构块
class BasicBlock(nn.Module):
    def __init__(self, in_channel, out_channel, stride=1, downsample=None):
        super(BasicBlock, self).__init__()
        self.left = nn.Sequential(
            nn.Conv2d(in_channel, out_channel, kernel_size=3, stride=stride, padding=1, bias=False),
            nn.BatchNorm2d(out_channel),
            nn.ReLU(),
            nn.Conv2d(out_channel, out_channel, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(out_channel),
            nn.downsample(downsample)
        )
 
    def forward(self, x):
        identity = x
        if self.downsample is not None:
            identity = self.downsample(x)
 
        out = self.left(x)  # 这是由于残差块需要保留原始输入
        out += identity  # 这是ResNet的核心,在输出上叠加了输入x
        out = F.relu(out)
        return out
(2)Bottleneck模块


从表中可以看出,ResNeXt网络每一个convx的第一层和第二层卷积的卷积核个数是ResNet网络的两倍,在代码实现时,需要注意在代码中增加一下两个参数groups和width_per_group(即为group数和conv2中组卷积每个group的卷积核个数)并且根据这两个参数计算出第一层卷积的输出(为ResNet网络的两倍)。
 

'''-------------二、Bottleneck模块-----------------------------'''
class Bottleneck(nn.Module):
 
    expansion = 4
 
    # 这里相对于RseNet,在代码中增加一下两个参数groups和width_per_group(即为group数和conv2中组卷积每个group的卷积核个数)
    # 默认值就是正常的ResNet
    def __init__(self, in_channel, out_channel, stride=1, downsample=None,
                 groups=1, width_per_group=64):
        super(Bottleneck, self).__init__()
        # 这里也可以自动计算中间的通道数,也就是3x3卷积后的通道数,如果不改变就是out_channels
        # 如果groups=32,with_per_group=4,out_channels就翻倍了
        width = int(out_channel * (width_per_group / 64.)) * groups
 
        self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=width,
                               kernel_size=1, stride=1, bias=False)
        self.bn1 = nn.BatchNorm2d(width)
        # -----------------------------------------
        # 组卷积的数,需要传入参数
        self.conv2 = nn.Conv2d(in_channels=width, out_channels=width, groups=groups,
                               kernel_size=3, stride=stride, bias=False, padding=1)
        self.bn2 = nn.BatchNorm2d(width)
        # -----------------------------------------
        self.conv3 = nn.Conv2d(in_channels=width, out_channels=out_channel * self.expansion,
                               kernel_size=1, stride=1, bias=False)
        self.bn3 = nn.BatchNorm2d(out_channel * self.expansion)
        # -----------------------------------------
        self.relu = nn.ReLU(inplace=True)
        self.downsample = downsample
 
    def forward(self, x):
        identity = x
        if self.downsample is not None:
            identity = self.downsample(x)
 
        out 
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值