论文链接:https://arxiv.org/abs/1904.01169
一、Res2Net Module
本论文所提出的结构如上图中的右侧
与现有的增强cnn多层多尺度表示强度的方法不同,本论文在更细粒度的级别上改进了多尺度表示能力。与一些并行工作利用不同分辨率的特征提高多尺度能力不同,本论文提出的多尺度是指更细粒度的多个可用接受域。为了实现这一目标,用一组更小的卷积,取代的n通道的3×3 卷积(为了不失去普遍性,使用n = s×w),如上图所示,这些更小的卷积组连接在一个分层的类残差结构中来增加可以代表的尺度输出特性。具体来说,将输入特征图分成几组。一组卷积首先从一组输入特征图中提取特征。前一组的输出特征和另一组输入特征图一起发送到下一组卷积。这个过程重复几次,直到所有的输入特征图都得到处理。最后,将所有组的feature map串接并发送到另一组1 × 1卷积中,进行信息融合。随着输入特征转换为输出特征的任何可能路径,当它通过3 × 3滤波器时,等效接受域增加,由于组合效应,产生了许多等效特征尺度。
Res2Net策略公开了一个新的维度,即规模(Res2Net块中特征组的数量),作为深度、宽度和基数(cardinality)等现有维度之外的一个基本因素。注意,所提出的方法在更细粒度的级别上利用了多尺度的潜力,这与现有的利用分层操作的方法是正交的。
如上图(a)所示的瓶颈结构是许多现代骨干cnn架构的基本构建块,如ResNet、ResNeXt和DLA。与bottleneck block中使用一组3 × 3卷积核提取特征不同,作者寻求具有更强的多尺度特征提取能力的替代架构,同时保持类似的计算负荷。具体来说,作者将一组3 × 3卷积替换为更小的分组卷积,同时以一种类似残差的结构样式连接不同的卷积组。由于文中提出的神经网络模块涉及单个残留块中的类残差连接,作者将其命名为Res2Net。上图显示了bottleneck block和提议的Res2Net模块之间的差异。经过1 × 1卷积后,作者将特征映射平均分割为s个特征映射子集,用xi表示,其中i∈{1,2,…,s}。与输入特征图相比,每个特征子集 x i x_i xi具有相同的空间大小,但通道数为1/s。除 x 1 x_1 x1外,每个 x i x_i xi都有对应的3 × 3卷积,用 K i ( ) Ki() Ki()表示。用 y i y_i yi表示 k i ( ) k_i() ki()的输出。将特征子集 x i x_i xi与 K i − 1 ( ) K_i-1() Ki−1()的输出相加,然后输入 k i ( ) k_i() ki()。为了在增加s的同时减少参数,作者省略了 x 1 x_1 x1的3 × 3卷积。因此, y i y_i yi可以写成:
每个3 × 3卷积算子 K i ( ) K_i() Ki()都有可能从所有的特征分割{ X j X_j Xj, j≤i}中接收到特征信息。每次特征分割 X j X_j Xj经过一个3 × 3卷积算子,输出结果可以有一个比 X j X_j Xj更大的接受域。由于组合爆炸激增,Res2Net模块的输出包含不同的数量和不同的接受域大小/尺度组合。
与ResNeXt不同的是,在Res2Net模块中,splits采用了多尺度的方式,这有利于全局和局部信息的提取。为了更好地融合不同尺度的信息,作者将所有的分割串接起来,并通过1 × 1的卷积将它们传递出去。splits和concatenation策略可以强制卷积更有效地处理特征。为了减少参数的数量,本文省略了第一次分割的卷积,这也可以被视为特征重用的一种形式。在这项工作中,作者使用s作为尺度尺寸的控制参数。更大的s可能允许学习具有更丰富的接受域大小的功能,而通过连接引入的计算/内存开销可以忽略不计。
上图为Res2Net模块与维基数c(将conv替换为group conv)和SE块进行集成。
(一)、维度基数
维度基数C是我上一篇博客的思想。ResNet
维度基数表示分组卷积中组的数量。这个维度使过卷积从单分支变为多分支,提高了CNN模型的表示能力。在设计中,可以用3 × 3分组组卷积来代替3 × 3组卷积,其中c表示组数。
(二)、SE block.
SE块通过自适应通道之间的相互依赖关系,自适应地重新校准通道特征响应。作者在Res2Net模块的跳连接之前添加SE块。Res2Net模块可以从SE块的集成中受益。
二、消融实验
表中显示了ImageNet数据集上top-1和top-5的测试错误。为了简单起见,表1中所有Res2Net模型的比例都是s = 4。Res2Net-50在top-1错误上比ResNet-50有1.84%的改进。在top-1错误方面,resnext -50比ResNeXt50提高了0.85%。此外,Res2Net-DLA-60在top-1错误方面比DLA-60高出1.27%。Res2Net-DLA-60的最高错误比DLA-X-60高出0.64%。SE-Res2Net-50比SENet50有1.68%的改进。在top-1错误方面,bLRes2Net-50比bLResNet-50提高了0.73%。Res2Net模块在粒度级别上进一步增强了bLResNet的多尺度能力,即使bLResNet是为了利用不同尺度的特征而设计的。请注意ResNet、ResNeXt、SE-Net、bLResNet和DLA是最先进的CNN模型。与这些强大的基线相比,与Res2Net模块集成的模型仍然有一致的性能增益。
更深层次的网络已被证明具有更强的视觉任务表示能力。为了更深入地验证本文的模型,作者比较了Res2Net和ResNet的分类性能,两者都有101层。如表2所示,Res2Net-101比ResNet-101获得了显著的性能提升,在 top-1 error方面为1.82%。请注意,在top-1 error中 Res2Net-50的性能比ResNet-50高出了1.84%。这些结果表明,所提出的具有额外维度尺度的模块可以与更深层次的模型集成,从而获得更好的性能。作者还将方法与DenseNet进行了比较。与官方提供的DenseNet族中性能最好的DenseNet-161模型相比,Res2Net-101的top-1误差提高了1.54%。
上表中说明,随着规模的增加,在top-1 error带有14w×8s的Res2Net-50比ResNet-50有提升了1.99%。注意,在保持复杂度的情况下, K i ( ) K_i() Ki()的宽度随着尺度的增加而减小。我们进一步评估了随着模型复杂性的增加而增加规模的性能增益。带有26w×8s的Res2Net-50比ResNet-50获得了显著的性能提升,在top-1 error方面为3.05%。一个有18w×4s的Res2Net-50也比ResNet-50的表现好0.93%,在top-1 error方面只有69%的失败。上表显示了不同尺度下的运行时间,这是平均推断推理尺寸为224 × 224的ImageNet验证集的时间。尽管由于层次连接的关系,分割{yi}的特性需要顺序计算,但Res2Net模块引入的额外运行时通常可以被忽略。由于GPU中可用张量的数量是有限的,对于Res2Net的典型设置,在单个GPU时钟周期内通常会有足够的并行计算,即s = 4。
表4显示了CIFAR-100数据集上的top-1 error和模型大小。实验结果表明,该方法在参数较少的情况下优于baseline和其他方法。作者提议的Res2NeXt-29, 6c×24w×6s比baseline高出1.11%。Res2NeXt29, 6c×24w×4s甚至优于ResNeXt-29, 16c×64w只有35%的参数。与DenseNet-BC (k = 40)相比,用更少的参数实现了更好的性能。与Res2NeXt-29相比,6c×24w×4s、Res2NeXt29、8c×25w×4s获得了更好的结果,具有更大的宽度和基数,说明维度尺度与维度宽度和基数是正交的。本文还将最近提出的SE块集成到结构中。使用更少的参数,该方法仍然优于ResNeXt-29, 8c×64w-SE的baseline。
作者通过增加CNN的不同维数来评估baseline模型的测试性能,包括scale(式(1))、cardinality和depth。当使用一个维度增加模型容量时,固定了所有其他维度。一系列的网络在这些变化下进行了训练和评估。由于已经表明增加基数比增加宽度更有效,所以我们只将建议的维度尺度与基数和深度进行比较。
上图为CIFAR-100数据集对模型大小的测试精度。baseline model的深度、基数和规模分别为29、6和1。实验结果表明,scale是提高模型性能的有效维度,这与之前在mageNet数据集上的观察结果一致。此外,增加规模比其他维度更有效,从而更快地获得性能。如式(1)和图1所示,对于尺度s = 2的情况,作者仅通过增加1 × 1卷积的参数来增加模型容量。因此,s = 2时的模型性能略差于增加基数时的模型性能。对于s = 3,4,分层类残差结构的组合效应产生了一组丰富的等效尺度,从而显著提高了性能。然而,尺度5和尺度6的模型性能提高有限,我们假设CIFAR数据集中的图像太小(32×32),不能有很多尺度。
在CAM方面
为了理解Res2Net的多尺度能力,我们使用通常用于定位图像分类的判别区域的Grad-CAM来可视化类激活映射(CAM)。在上图所示的可视化示例中,较强的CAM区域被较浅的颜色覆盖。与ResNet相比,基于Res2Net的CAM结果在小对象上有更集中的激活映射,如“baseball”和“penguin”。这两种方法在中等大小的物体上都有类似的激活映射,比如“ice cream”。由于Res2Net具有更强的多尺度能力,Res2Net的激活图倾向于覆盖大对象上的整个对象,如“bulbul”、“mountain dog”、“ballpoint”和“mosque”,而ResNet的激活图只覆盖对象的部分。这种精确定位CAM区域的能力使得Res2Net对于弱监督语义分割任务中的对象区域挖掘具有潜在价值。
相关代码如下:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import torch
import torch.nn as nn
__all__ = ['ImageNetRes2Net', 'res2net50', 'res2net101',
'res2net152', 'res2next50_32x4d', 'se_res2net50',
'CifarRes2Net', 'res2next29_6cx24wx4scale',
'res2next29_8cx25wx4scale', 'res2next29_6cx24wx6scale',
'res2next29_6cx24wx4scale_se', 'res2next29_8cx25wx4scale_se',
'res2next29_6cx24wx6scale_se']
def conv3x3(in_planes, out_planes, stride=1, groups=1): #3*3卷积组
"""3x3 convolution with padding"""
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
padding=1, groups=groups, bias=False)
def conv1x1(in_planes, out_planes, stride=1):
"""1x1 convolution"""
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)
class SEModule(nn.Module): #SE block
def __init__(self, channels, reduction=16):
super(SEModule, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.fc1 = nn.Conv2d(channels, channels // reduction, kernel_size=1, padding=0)
self.relu = nn.ReLU(inplace=True)
self.fc2 = nn.Conv2d(channels // reduction, channels, kernel_size=1, padding=0)
self.sigmoid = nn.Sigmoid()
def forward(self, input):
x = self.avg_pool(input)
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
x = self.sigmoid(x)
return input * x
class Res2NetBottleneck(nn.Module):
expansion = 4
def __init__(self, inplanes, planes, width, downsample=None, stride=1, scales=4, groups=1, se=False, norm_layer=None):
super(Res2NetBottleneck, self).__init__()
# if planes % scales != 0:
# raise ValueError('Planes must be divisible by scales')
if norm_layer is None:
norm_layer = nn.BatchNorm2d
bottleneck_planes = int(width * scales * groups)
self.conv1 = conv1x1(inplanes, bottleneck_planes)#C*D
self.bn1 = norm_layer(bottleneck_planes)
self.conv2 = nn.ModuleList([conv3x3(bottleneck_planes // scales, bottleneck_planes // scales, stride, groups=groups) for _ in range(scales-1)]) #k1,k2,k3后接3×3卷积,分组卷积
self.bn2 = nn.ModuleList([norm_layer(bottleneck_planes // scales) for _ in range(scales-1)])
self.conv3 = conv1x1(bottleneck_planes, planes * self.expansion)#输出concat操作
self.bn3 = norm_layer(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.se = SEModule(planes * self.expansion) if se else None
self.downsample = downsample
self.stride = stride
self.scales = scales
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
xs = torch.chunk(out, self.scales, 1)#将输入张量进行分块
ys = []
# 原文中的公式
if self.stride == 1:
for s in range(self.scales):
if s == 0:
ys.append(xs[s])
elif s == 1:
ys.append(self.relu(self.bn2[s-1](self.conv2[s-1](xs[s]))))
else:
ys.append(self.relu(self.bn2[s-1](self.conv2[s-1](xs[s] + ys[-1]))))
else:
for s in range(self.scales):
if s == 0:
ys.append(self.maxpool(xs[s]))
else:
ys.append(self.relu(self.bn2[s-1](self.conv2[s-1](xs[s]))))
out = torch.cat(ys, 1)#concat操作
out = self.conv3(out)
out = self.bn3(out)
if self.se is not None:
out = self.se(out)
if self.downsample is not None:
identity = self.downsample(identity)
out += identity
out = self.relu(out)
return out