【论文泛读】 ResNeXt:深度神经网络的聚合残差变换(ResNet的改进,提出了一种新的维度)

简介: 【论文泛读】 ResNeXt:深度神经网络的聚合残差变换(ResNet的改进,提出了一种新的维度)

主要思想


简单来说呢,随着很多SOTA模型的出现,从一开始的“特征工程”慢慢地转入了一些“网络工程”,引入了一些新的结构和方法。


在前面说过的网络中呢,VGG作为一种经典的模型,他提出了堆叠相同块(stacking block)的策略,在VGG中是3x3卷积核不断堆叠,并且也通过增加深度提高了准确率。


除此之外,Inception系列网络提出了split-transform-merge的策略,通过多分支卷积实现在低计算开销的前提下去接近大型密集层的表达能力。而这篇文章则是合了stacking block和split-transform-merge的策略,在ResNet的基础上提出了ResNeXt网络架构。


摘要


这里还是简单讲一下摘要,作者提出了一种简单、高度模块化的图像分类网络体系结构。我们的网络是通过重复一个构建块来构建的,该构建块聚合了一组具有相同拓扑的变换。我们的简单设计导致了一个同构的、多分支的体系结构,只需要设置几个超参数。这一策略体现了一个新的维度,我们称之为“Cardinality”(转换集的大小),作为深度和宽度维度之外的一个基本因素。在ImageNet-1K数据集上的实验表明,即使在保持复杂度的限制条件下,增加基数也能够提高分类精度。而且,当我们增加容量时,增加基数比深入或扩大更有效。我们的模型ResNeXt是我们进入ILSVRC2016分类任务的基础,我们获得了第二名。我们进一步研究了ImageNet-5K集合和COCO检测集合上的ResNeXt,也显示了比ResNet对应的更好的结果。


简单来说呢,就是ResNeXt采用 VGGs/ResNets 的网络的 depth 加深方式,同时利用 split-transform-merge 策略.


模型结构


Inception模块

首先我们可以讲一下Inception模块,Inception模块提出了split-transform-merge的策略,但是存在一个问题**网络的超参数设定的针对性比较强,当应用在别的数据集上时需要修改许多参数,因此可扩展性一般。**所以说,如果我们要用到自己的数据集,就要重新设置一下我们的众多的超参数

14c09c410ad1412db077cb242df08f33.png



split-transform-merge


作者提出了ResNeXt,同时采用 VGG 堆叠的思想和 Inception 的 split-transform-merge 思想,但是可扩展性比较强,论文中也给了一个架构图,X到xi实际上就是一个split的过程,wx就是一个transfrom的过程,最后相加就是一个merge的过程。


  1. Splitting,X分解为多个低维向量Xi;
  2. Transforming,每个Xi对应一个Wi以进行变换;
  3. Aggregating/Merge,每个Xi变换后进行累加



18ab9707abceeb44a6614967656aacf1.png



其中将Wi替换为更复杂的函数T(Xi)。在ResNeXt网络中,变换函数使用ResNet中的Bottleneck形式来替代。


分组卷积


分组卷积的思想呢,实际上在很早的AlexNet中就有出现了,在AlexNet的时候,由受限于当时硬件的限制,作者不得不将卷积操作拆分到两台GPU上运行,这两台GPU的参数是不共享的。而我们的分组卷积也是如此,如果我们组数与我们的通道数相同的话,也就是每一个通道利用一个卷积核,这就会变成我们的MobileNet的DW卷积结构了,也就是深度可分离卷积。


1c0e2e774cd612c2f5756157d2bfb9b3.png


简单来说:Grouped Convolutions:group conv 层中,输入和输出的 channels 被分为 CC 个 groups,分别对每个 group 进行 conv.

比如这个就是深度可分离卷积,每一个通道对应这一个卷积



6a63b423ec4645bc8344b2bada244aaf.png


ResNeXt模型结构

2ac26d6a2c339006d7f6cf63dd53db69.png

ResNeXt对ResNet进行了改进,采用了多分支的策略,在论文中作者提出了三种等价的模型结构,最后的ResNeXt用了©的结构来构建我们的ResNeXt、这里面和我们的Inception是不同的,在Inception中,每一部分的拓扑结构是不同的,比如一部分是1x1卷积,3x3卷积还有5x5卷积,而我们ResNeXt是用相同的拓扑结构,并在保持参数量的情况下提高了准确率。


这三个结构实际上是等价的,对于选c的原因,其实是因为c结构比较简洁而且速度更快。


63b0e5ec03857f57464e3018f0723635.png


这个构建基于两个准则


  1. 同stage中的block使用相同的width和filter size;
  2. spatial size减小时,增加channel的数量。


除此之外,作者还提出了一个新的维度,也就是基数(Cardinality)也就是转换集的大小,原文解释是the size of the set of transformations,并且论文中说,这是比深度和宽度更有效的一个维度


除此之外,ResNeXt 只能在 block 的 depth>3时使用. 如果 block 的 depth=2,则会得到宽而密集的模块,所以这也是为什么不在ResNet18和34进行修改的原因


95f5cfe8dd076328eeec9a21eee7015b.png


ResNeXt模型评估及结论


基于上述准则,文章在ResNet-50模型的基础上,提出了ResNeXt-50模型。

我们可以看到,这两者模型的参数是差不多的,复杂度也是差不多的,但是最后在经典数据集ImageNet1k中得到的准确率,ResNeXt更胜一筹。



8f3e86cf91fb6dad6e8a8d58955eb7c9.png


探究Cardinality


这个表主要列举了一些参数,来探究我们的Cardiality,也用来说明模型的结构和参数也是差不多的,第二行的d表示每个path的中间channels数量,最后一行则表示整个block的宽度,是第一行C和第二行d的乘积。


0c688f9e2c55a7d3992441e15789f57f.png


以下就是我们的实验结果,可以看到ResNeXt-50是优于ResNet50的

153fc348144e4098e88bc367e546d737.png


主要说明增加Cardinality和增加深度或宽度的区别,增加宽度就是简单地增加filter channels。第一个是基准模型,增加深度和宽度的分别是第三和第四个,可以看到误差分别降低了0.3%和0.7%。但是第五个加倍了Cardinality,则降低了1.3%,第六个Cardinality加到64,则降低了1.6%。显然增加Cardianlity比增加深度或宽度更有效。结果表明,增加基数可以提高模型的性能,且要比增加宽度和深度更有效。


f378aa0cb5a154c008d60aec1bbdba89.png


此外,模型还对基数(分支数)进行了对比试验。


ca8099e81daaff72f0069fe79923552f.png


和当前最好的模型的对比


对我们会发现,虽然说ResNeXt与Inception差不多,但是有一个点很重要,计算效率和复杂性是大大高于我们的Inceptionv3的

a61afb9482cca2eb7780a1ba0e768fa0.png


论文还在简单的CIFAR10数据集作了研究,模型还对比验证了残差结构的有效性,实验表明,在深层网络中引入残差结构,可以明显提高模型的性能



79b8f29c3e32518e60c7703b28ad033d.png


总结和感想


在这篇论文中,我觉得比较好的就是,ResNeXt虽然并没有提出一种很奇特的结构,而是在ResNet的基础上增加了分组卷积的方式。并且在保持模型参数变化不大的基础下进行的,最后的ResNeXt50与ResNet50参数相似,但是性能却是更好的。最后也拿了当时2016的ILSVRC的图像分类的第二名。


最后总结来说,作者的核心创新点就在于提出了 aggregrated transformations,用一种平行堆叠相同拓扑结构的blocks代替原来 ResNet 的三层卷积的block,在不明显增加参数量级的情况下提升了模型的准确率,同时由于拓扑结构相同,超参数也减少了,便于模型移植。


Pytorch代码实现


最后附一下他的ResNeXt的pytorch实现吧,这是基于ImageNet的,并且是在ResNet上进行修改。

对于CIFAR10的是网络分类的实现,可以到时候关注我们图像分类篇进行一个添加学习


'''
ResNeXt in PyTorch.
See the paper "Aggregated Residual Transformations for Deep Neural Networks" for more details.
这一部分借鉴官方的实现方式,改ResNet进行训练
'''
from numpy import pad
import torch
import torch.nn as nn
import torch.nn.functional as F
class BasicBlock(nn.Module):
    # 最基础的Block的压缩为1,在ResNet18,34有效
    expansion = 1
    # downsample代表:是否进行下采样,或者说我们是否需要一个shortcut,也就是我们的下采样
    def __init__(self, in_channel, out_channel, stride=1, downsample=None, **kwargs):
        super(BasicBlock, self).__init__()
        # 首先是一个3x3的卷积层,这里的stride是我们设置的,因为对于我们的第一个卷积层,stride为2
        self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
                               kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channel)
        self.relu = nn.ReLU()
        self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel,
                               kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channel)
        self.downsample = downsample
    def forward(self, x):
        identity = x
        if self.downsample is not None:
            identity = self.downsample(x)
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out)
        out += identity # 进行残差连接
        out = self.relu(out)
        return out
class Bottleneck(nn.Module):
    """
    注意:原论文中,在虚线残差结构的主分支上,第一个1x1卷积层的步距是2,第二个3x3卷积层步距是1。
    但在pytorch官方实现过程中是第一个1x1卷积层的步距是1,第二个3x3卷积层步距是2,
    实验结果证明,这么做的好处是能够在top1上提升大概0.5%的准确率。
    可参考Resnet v1.5 https://ngc.nvidia.com/catalog/model-scripts/nvidia:resnet_50_v1_5_for_pytorch
    """
    expansion = 4
    # 这里相对于简单的残差网络中多增加了两个参数,一个是groups和width_per_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)  # squeeze channels
        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)  # unsqueeze channels
        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 = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)
        out = self.conv3(out)
        out = self.bn3(out)
        out += identity # 残差连接
        out = self.relu(out)
        return out
class ResNet(nn.Module):
    def __init__(self,
                 block, # 表示block的类型
                 blocks_num, # 表示的是每一层block的个数
                 num_classes=1000, # 表示类别
                 include_top=True, # 表示是否含有分类层(可做迁移学习)
                 groups=1, # 表示组卷积的数
                 width_per_group=64):
        super(ResNet, self).__init__()
        self.include_top = include_top
        self.in_channel = 64
        self.groups = groups
        self.width_per_group = width_per_group
        self.conv1 = nn.Conv2d(3, self.in_channel, kernel_size=7, stride=2,
                               padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(self.in_channel)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.layer1 = self._make_layer(block, 64, blocks_num[0])
        self.layer2 = self._make_layer(block, 128, blocks_num[1], stride=2)
        self.layer3 = self._make_layer(block, 256, blocks_num[2], stride=2)
        self.layer4 = self._make_layer(block, 512, blocks_num[3], stride=2)
        if self.include_top:
            self.avgpool = nn.AdaptiveAvgPool2d((1, 1))  # output size = (1, 1)
            self.fc = nn.Linear(512 * block.expansion, num_classes)
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
    def _make_layer(self, block, channel, block_num, stride=1):
        downsample = None
        if stride != 1 or self.in_channel != channel * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.in_channel, channel * block.expansion, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(channel * block.expansion))
        layers = []
        layers.append(block(self.in_channel,
                            channel,
                            downsample=downsample,
                            stride=stride,
                            groups=self.groups,
                            width_per_group=self.width_per_group))
        self.in_channel = channel * block.expansion # 得到最后的输出
        for _ in range(1, block_num):
            layers.append(block(self.in_channel,
                            channel,
                            groups=self.groups,
                            width_per_group=self.width_per_group))
        return nn.Sequential(*layers)
    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        if self.include_top:
            x = self.avgpool(x)
            x = torch.flatten(x, 1)
            x = self.fc(x)
        return x
def ResNet34(num_classes=1000, include_top=True):
    # https://download.pytorch.org/models/resnet34-333f7ec4.pth
    return ResNet(BasicBlock, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)
def ResNet50(num_classes=1000, include_top=True):
    # https://download.pytorch.org/models/resnet50-19c8e357.pth
    return ResNet(Bottleneck, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)
def ResNet101(num_classes=1000, include_top=True):
    # https://download.pytorch.org/models/resnet101-5d3b4d8f.pth
    return ResNet(Bottleneck, [3, 4, 23, 3], num_classes=num_classes, include_top=include_top)
# 论文中的ResNeXt50_32x4d
def ResNeXt50_32x4d(num_classes=1000, include_top=True):
    # https://download.pytorch.org/models/resnext50_32x4d-7cdf4587.pth
    groups = 32
    width_per_group = 4
    return ResNet(Bottleneck, [3, 4, 6, 3],
                  num_classes=num_classes,
                  include_top=include_top,
                  groups=groups,
                  width_per_group=width_per_group)
def ResNeXt101_32x8d(num_classes=1000, include_top=True):
    # https://download.pytorch.org/models/resnext101_32x8d-8ba56ff5.pth
    groups = 32
    width_per_group = 8
    return ResNet(Bottleneck, [3, 4, 23, 3],
                  num_classes=num_classes,
                  include_top=include_top,
                  groups=groups,
                  width_per_group=width_per_group)
def test():
    net = ResNeXt50_32x4d(num_classes=10)
    device = 'cuda' if torch.cuda.is_available() else 'cpu'
    net = net.to(device)
    # x = torch.randn(2,3,32,32).to(device)
    # y = net(x)
    # print(y.size())
    from torchinfo import summary
    print(summary(net,(1,3,224,224)))
test()


相关文章
|
15天前
|
负载均衡 网络虚拟化
网络技术基础(17)——以太网链路聚合
【3月更文挑战第4天】网络基础笔记(加班了几天,中途耽搁了,预计推迟6天)
|
15天前
|
机器学习/深度学习 编解码 计算机视觉
【APFN】从大佬论文中探索如何分析改进金字塔网络
【APFN】从大佬论文中探索如何分析改进金字塔网络
145 0
|
15天前
|
计算机视觉
【论文复现】经典再现:yolov4的主干网络重构(结合Slim-neck by GSConv)
【论文复现】经典再现:yolov4的主干网络重构(结合Slim-neck by GSConv)
78 0
【论文复现】经典再现:yolov4的主干网络重构(结合Slim-neck by GSConv)
|
15天前
|
机器学习/深度学习 测试技术 Ruby
YOLOv5改进 | 主干篇 | 反向残差块网络EMO一种轻量级的CNN架构(附完整代码 + 修改教程)
YOLOv5改进 | 主干篇 | 反向残差块网络EMO一种轻量级的CNN架构(附完整代码 + 修改教程)
158 2
|
6天前
|
机器学习/深度学习 监控 自动驾驶
【传知代码】从零开始搭建图像去雾神经网络-论文复现
本文介绍了基于集成学习的双分支非均匀去雾神经网络的复现,该网络由迁移学习子网和数据拟合子网组成,分别处理全局表示和数据拟合。网络使用Res2Net作为编码器,并结合通道和像素注意力模块。代码可在提供的链接下载。网络在交通监控、自动驾驶、航海和目标跟踪等领域有广泛应用,通过提升图像质量来提高系统性能。实验在O-Haze、I-Haze和NH-Haze数据集上进行,展示了网络在去除雾霾方面的效果,尽管存在细节模糊和色彩饱和度低的问题。
|
6天前
|
机器学习/深度学习 自然语言处理 搜索推荐
【传知代码】图神经网络长对话理解-论文复现
在ACL2023会议上发表的论文《使用带有辅助跨模态交互的关系时态图神经网络进行对话理解》提出了一种新方法,名为correct,用于多模态情感识别。correct框架通过全局和局部上下文信息捕捉对话情感,同时有效处理跨模态交互和时间依赖。模型利用图神经网络结构,通过构建图来表示对话中的交互和时间关系,提高了情感预测的准确性。在IEMOCAP和CMU-MOSEI数据集上的实验结果证明了correct的有效性。源码和更多细节可在文章链接提供的附件中获取。
【传知代码】图神经网络长对话理解-论文复现
|
12天前
|
机器学习/深度学习 数据挖掘 算法框架/工具
想要了解图或图神经网络?没有比看论文更好的方式,面试阿里国际站运营一般会问什么
想要了解图或图神经网络?没有比看论文更好的方式,面试阿里国际站运营一般会问什么
|
15天前
|
机器学习/深度学习 存储 自然语言处理
【威胁情报挖掘-论文阅读】学习图表绘制 基于多实例学习的网络行为提取 SeqMask: Behavior Extraction Over Cyber Threat Intelligence
【威胁情报挖掘-论文阅读】学习图表绘制 基于多实例学习的网络行为提取 SeqMask: Behavior Extraction Over Cyber Threat Intelligence
19 0
|
15天前
|
网络虚拟化 数据安全/隐私保护 数据中心
【专栏】对比了思科与华为网络设备的基本配置、接口、VLAN、路由、访问控制列表及其它关键命令
【4月更文挑战第28天】本文对比了思科与华为网络设备的基本配置、接口、VLAN、路由、访问控制列表及其它关键命令。尽管两者在很多操作上相似,如设备命名(思科:`hostname`,华为:`sysname`)、查看版本信息(思科:`show version`,华为:`display version`),但在某些方面存在差异,如接口速率设置(两者都使用`speed`和`duplex`,但命令结构略有不同)和VLAN配置(华为的`port hybrid`命令)。

热门文章

最新文章