论文地址:https://mftp.mmcheng.net/Papers/20cvprSCNet.pdf
代码地址:https://github.com/MCG-NKU/SCNet
SCNet是一种卷积神经网络,它使用自校准卷积(Self-Calibrated Convolutions)来增强子任务之间的关系,包括分类、检测和分割。不同于标准卷积采用小尺寸核同时融合空域与通道信息,所设计的SCConv可以通过自矫正操作自适应构建long-range空域与通道间相关性。SCConv的这种特性可以帮助CNN生成更具判别能力的特征表达,因其具有更丰富的信息。作者所设计的SCConv极为简单且通用,可以轻易嵌入到现有CNN架构中,而不会导致参数量增加与计算复杂度提升。
传统卷积:存在输入x,卷积核k,输出z,则传统卷积操作的公式:
存在的问题;提取到的特征图没有很强的区分性
1. 每个输出的特征图都是通过所有通道求和来计算的,所有的特征图都是通过重复同一公式得到。
2. 每个空间位置的感受野主要由预定义的卷积核大小控制。
所提出了一种由多个卷积注意力组合的自校准模块,用于替换基本的卷积结构,在不增加额外参数和计算量的情况下,该模块能够产生全局的感受野。相比于标准卷积,该模块产生的特征图更具有区分度。
该模块的优势所在:
1、传统卷积只能对小区域进行卷积操作,而自校准卷积模块使每个空间位置可以自适应的编码来自长范围区域的相关信息。
2、自校准卷积是普遍适用的,能够轻易地应用到标准的卷积层中,而不需要引入任何参数和复杂的头部或改变超参数。
自校正卷积具体步骤如上图所示:
第一步,输入特征图X为C X H X W大小,拆分为两个C/2 X H X W大小的X1,X2;
第二步,卷积核K的维度为C X C X H X W,将K分为4个部分,每份的作用各不相同,分别记为K1,K2,K3,K4,其维度均为C/2 X C/2 X H X W;
为了有效地收集每个空间位置的丰富的上下文信息,作者提出在两个不同的尺度空间中进行卷积特征转换:原始尺度空间中的特征图(输入共享相同的分辨率)和下采样后的具有较小分辨率的潜在空间(用于自校正) 。利用下采样后特征具有较大的感受野,因此在较小的潜在空间中进行变换后的嵌入将用作参考,以指导原始特征空间中的特征变换过程。
第三步,对自校正尺度空间进行处理(Self-Calibration)
对T使用卷积核组进行特征变换︰
其中Up(⋅)表示线性插值操作,得到中间参考量从小尺度空间到原始特征空间的映射,则自校准操作可以表示为:
其中,σ表示sigmoid函数,符号“.”表示逐元素乘运算,X’被用作残差项,建立权重,用于自校准。自校准后的最终输出可以写作:
自校正卷积SCConv
class SCConv(nn.Module):
def __init__(self, inplanes, planes, stride, padding, dilation, groups, pooling_r, norm_layer):
super(SCConv, self).__init__()
# k2(Self-Calibration上半分支):先下采样,再通过卷积K2
self.k2 = nn.Sequential(
nn.AvgPool2d(kernel_size=pooling_r, stride=pooling_r),
nn.Conv2d(inplanes, planes, kernel_size=3, stride=1,
padding=padding, dilation=dilation,
groups=groups, bias=False),
norm_layer(planes),
)
# k3(Self-Calibration下半分支):通过卷积K3
self.k3 = nn.Sequential(
nn.Conv2d(inplanes, planes, kernel_size=3, stride=1,
padding=padding, dilation=dilation,
groups=groups, bias=False),
norm_layer(planes),
)
# k4(Self-Calibration下半分支):通过卷积K4
self.k4 = nn.Sequential(
nn.Conv2d(inplanes, planes, kernel_size=3, stride=stride,
padding=padding, dilation=dilation,
groups=groups, bias=False),
norm_layer(planes),
)
def forward(self, x):
identity = x
# Self-Calibration上半分支:输入特征x通过k2后,上采样到和输入特征的大小一样,再与输入特征进行残差连接,再通过sigmoid函数
out = torch.sigmoid(torch.add(identity, F.interpolate(self.k2(x), identity.size()[2:]))) # sigmoid(identity + k2)
# Self-Calibration下半分支:输入特征x通过k3后,与Self-Calibration上半分支输出进行矩阵乘法
out = torch.mul(self.k3(x), out) # k3 * sigmoid(identity + k2)
# 最后,将输出out通过k4
out = self.k4(out) # k4
return out
将自校正卷积融入到BottleNeck模块中:SCBottleNeck
class SCBottleneck(nn.Module):
"""SCNet SCBottleneck
将SCConv放入BottleNeck中
"""
expansion = 4
# 平均池化的下采样率为4
pooling_r = 4 # down-sampling rate of the avg pooling layer in the K3 path of SC-Conv.
def __init__(self, inplanes, planes, stride=1, downsample=None,
cardinality=1, bottleneck_width=32,
avd=False, dilation=1, is_first=False,
norm_layer=None):
super(SCBottleneck, self).__init__()
group_width = int(planes * (bottleneck_width / 64.)) * cardinality # int(planes * (32 /64)) * 1 = int(0.5 * planes)
self.conv1_a = nn.Conv2d(inplanes, group_width, kernel_size=1, bias=False)
self.bn1_a = norm_layer(group_width)
self.conv1_b = nn.Conv2d(inplanes, group_width, kernel_size=1, bias=False)
self.bn1_b = norm_layer(group_width)
self.avd = avd and (stride > 1 or is_first)
if self.avd:
self.avd_layer = nn.AvgPool2d(3, stride, padding=1)
stride = 1
# k1:通过卷积K1
self.k1 = nn.Sequential(
nn.Conv2d(
group_width, group_width, kernel_size=3, stride=stride,
padding=dilation, dilation=dilation,
groups=cardinality, bias=False),
norm_layer(group_width),
)
self.scconv = SCConv(
group_width, group_width, stride=stride,
padding=dilation, dilation=dilation,
groups=cardinality, pooling_r=self.pooling_r, norm_layer=norm_layer)
self.conv3 = nn.Conv2d(
group_width * 2, planes * 4, kernel_size=1, bias=False)
self.bn3 = norm_layer(planes*4)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.dilation = dilation
self.stride = stride
def forward(self, x):
residual = x
# 通过卷积分别得到两个通道数为输入特征通道数一半的特征out_a和out_b
out_a = self.conv1_a(x)
out_a = self.bn1_a(out_a)
out_b = self.conv1_b(x)
out_b = self.bn1_b(out_b)
out_a = self.relu(out_a)
out_b = self.relu(out_b)
# out_a通过k1,out_b通过scconv
out_a = self.k1(out_a)
out_b = self.scconv(out_b)
out_a = self.relu(out_a)
out_b = self.relu(out_b)
if self.avd:
out_a = self.avd_layer(out_a)
out_b = self.avd_layer(out_b)
# 沿着dim=1(channel)进行拼接,再通过conv3
out = self.conv3(torch.cat([out_a, out_b], dim=1))
out = self.bn3(out)
if self.downsample is not None:
residual = self.downsample(x)
# 对输出out进行残差连接
out += residual
out = self.relu(out)
return out
参考:南开大学程明明团队新作 | ResNet的又一改进:SCNet
2D关键点检测之SCNet:Improving Convolutional Networks with Self-Calibrated Convolutions