image.png

前言

这是一篇改进卷积操作的论文,实际上是一种仍然是一种卷积参数与特征相关的动态卷积网络(Dynamic Convolution Networks)。

具体实现

由于卷积参数动态生成自特征,而且也不再是标准卷积那种局部处理的策略,所以在实现起来需要有些额外的技巧。
本文的实现中,作者提供了两种手段,一种是基于pytorch自身的unfold方法(https://github.com/d-li14/involution/blob/main/cls/mmcls/models/utils/involution_naive.py)和相乘求和,而另一种是直接使用cuda手动编写特征与生成的卷积核之间的整合过程(https://github.com/d-li14/involution/blob/main/cls/mmcls/models/utils/involution_cuda.py)。
而基于unfold的实现方法实际上理解起来非常简单,这里直接从代码入手,先看看这份工作到底怎么做的:

  1. import torch.nn as nn
  2. from mmcv.cnn import ConvModule
  3. class involution(nn.Module):
  4. def __init__(self, channels, kernel_size, stride):
  5. super(involution, self).__init__()
  6. self.kernel_size = kernel_size
  7. self.stride = stride
  8. self.channels = channels
  9. reduction_ratio = 4
  10. self.group_channels = 16
  11. self.groups = self.channels // self.group_channels
  12. self.conv1 = ConvModule(in_channels=channels,
  13. out_channels=channels // reduction_ratio, kernel_size=1,
  14. conv_cfg=None, norm_cfg=dict(type="BN"), act_cfg=dict(type="ReLU"))
  15. self.conv2 = ConvModule(in_channels=channels // reduction_ratio,
  16. out_channels=kernel_size ** 2 * self.groups,
  17. kernel_size=1, stride=1, conv_cfg=None, norm_cfg=None, act_cfg=None)
  18. if stride > 1:
  19. self.avgpool = nn.AvgPool2d(stride, stride)
  20. self.unfold = nn.Unfold(kernel_size, 1, (kernel_size - 1) // 2, stride)
  21. def forward(self, x):
  22. weight = self.conv2(self.conv1(x if self.stride == 1 else self.avgpool(x)))
  23. b, c, h, w = weight.shape
  24. weight = weight.view(b, self.groups, self.kernel_size ** 2, h, w).unsqueeze(2)
  25. out = self.unfold(x).view(b, self.groups, self.group_channels, self.kernel_size ** 2, h, w) # 组内共享卷积核
  26. out = (weight * out).sum(dim=3).view(b, self.channels, h, w)
  27. return out

这里的实现中依赖了pytorch的一个非常重要的方法,就是 unfold ,该方法的主要功能就是实现了卷积的滑窗操作,每一次窗口中的数据会被收集到并堆叠到通道维度上,即dim=1上。

实际上的它的主要参数和卷积也基本一致,就是 kernel_size, stride, padding, dilation 这些。关于 unfold 的简单介绍可见后文。

这里可以看到,对于生成卷积权重的过程使用了一个简单的 Conv-BN-ReLU-Conv 的结构,生成权重tensor后对其进行reshape操作,按照Line26和Line27的内容可以理解,这里实际上构造了一种动态的分组卷积。针对输入特征x,在 unfold 收集滑窗数据并堆叠到通道维度上后(就是 b, self.groups, self.group_channels, self.kernel_size ** 2, h, w 这里的 self.groups, self.group_channels, self.kernel_size ** 2 这部分所指代的数据),这里堆叠数据进行了拆分,将原始的通道中的数据进行分组,对于组里每一通道的滑窗数据共享同一个卷积核(即weight在 self.group_channels 对应的维度上是使用相同的广播得到的数据)。
对权重和调整后的x进行乘法(这里用的是元素乘法后累和,和后面介绍 unfold 中提到的矩阵乘法在思想上是一致的,都是在进行卷积中的加权求和)。
总体而言,这里的involution操作可以被称为动态的(空间不共享)、分组(组内共享)的卷积。(感觉这里应该整理下不同方法的差异了,其实已经有一些论文出现了类似的构造,例如CARAFE中实际上是一种动态的深度分离的卷积)。
既然已经知道了核心操作的过程,那么接下来我们需要了解的是,这么一个概念,作者是怎么思考或者展示的。

文章动机

传统卷积

从传统图像滤波方法中可以了解到,卷积核具有两个引人注目的特性,这些特性有助于其吸引力(magnetism)和流行性(popularity),即,与空间无关(spatial-agnostic)和特定于通道(channel-specific)。 :::info

  • 在空间范围内,前一属性通过在不同位置之间重用卷积核来保证卷积核的效率,并且追求变换等效性[translation equivalence: Making convolutional networks shift-invariant again]。
  • 在通道域中,一系列(a spectrum of)卷积核负责收集在不同通道中编码的各种信息,满足着后一特性。 ::: 自开创性的VGG问世以来,神经网络通过将卷积核的空间跨度限制为不超过3x3的区域,从而追求卷积核的紧凑性。但是这也带来了一些问题:

  • 一方面,尽管spatial-annostic以及spatial-compact的性质在提高效率和解释translation equivalence方面是有意义的,但它剥夺了卷积核适应于不同空间位置的各种视觉模式的能力。此外,局部性(locality)限制了卷积的感受野,这给在单次滑窗处理中就想要捕获长距离空间交互带来了挑战。

  • 另一方面,众所周知,卷积滤波器内部的通道间(inter-channel)冗余在许多成功的深度神经网络中都非常突出[Speeding up convolutional neural networks with low rank expansions],这使得卷积核的较大灵活性(对于不同通道而言)受到质疑。

    Involution的提出

    为了克服以上局限,这里提出了involution,这种实现正好是相对于卷积的两种属性的各自的反面,即实现了spatial-specific和channel-agnostic。具体而言,involution核对于空间各个位置是不同的,但是在通道上确是共享的。

  • 受spatial-specific这一属性的限制,如果involution核被参数化为固定大小的矩阵(例如卷积核那样)并使用反向传播算法进行更新,则将阻止学习到的卷积核在具有可变分辨率的输入图像之间的迁移使用。在处理可变特征分辨率时,可以将一个对应于某一空间位置的involution kernel仅以相应位置本身上的输入特征向量为条件来生成,作为直观而有效的实例化手段。

  • 此外,通过沿通道维度共享involution kernel来减轻kernel的冗余。

综合以上两个因素,involution操作的计算复杂度随特征通道数量而线性放缩,基于此,动态参数化的involution kernel在空间维度上具有广泛的覆盖范围(更广的感受野)。
由于这种反转的设计策略,提出的involution相较于convolution有着两个好处:

  • involution could summarize the context in a wider spatial arrangement, thus overcome the difficulty of modeling long-range interactions well
  • involution could adaptively allocate the weights over different positions, so as to prioritize the most informative visual elements in the spatial domain (这种全局不共享的卷积核,实际上在一定程度上特别类似空间attention的操作,这不过这里针对每个位置实际上进一步包含了一个局部的整合,以及通道上的共享策略略有不同)

类似地,最近的方法开始尝试使用自注意力来替换卷积操作,以捕获远程依赖关系[Stand-alone self-attention in vision models, Exploring self-attention for image recognition]。在这些作品中,纯粹的自注意力机制可以被用来构建具有良好性能的独立模型。有趣的是(intriguingly),文章揭示了自注意力通过涉及内核构造的复杂公式化来具体化了我们一般化定义的involution操作。相比之下,这项工作中采用的involution kernel根据单个像素而不是依据相邻像素的关系生成的。更进一步,在实验中证明,即使使用令人尴尬(embarrassingly)的简单版本,involution也可以实现相较于self-attention在accuracy-cost的权衡。充分意识到在self-attention中,通过将查询与每个键进行比较而获得的亲和度矩阵(affinity matrix)也是involution kernel一种实例,在这里作者们开始质疑组合query和key特征以生成这样一个kernel的必要性,因为作者们简化了的involution kernel可以在避免key内容的冗余使用的同时还可以获得不错的性能。至于self-attention中的专用的位置编码就更不用说了(可能更不是有必要的了)。
提出的involution运算通过以相当轻量级的方式将可扩展(extendable)和可切换(switchable)的空间模型嵌入到表示学习范式中,轻松地促进了视觉识别。
在重新设计的视觉原语(visual primitive)的基础上,建立了一个被称为RedNet的主干架构,该架构可以实现优于基于卷积的ResNet和基于自注意力的图像分类模型的性能。在包括检测和分割在内的下游任务上,我们全面进行了逐步研究,以检验involution对检测器和分割器的不同组件(例如其backbone和neck)的有效性。 事实证明,对每个考虑的组件而言,involution都是有帮助的,并且将它们组合在一起可带来最高的效率。
说了这么多,看看作者是如何总结贡献的:

  • We rethink the inherent properties of convolution, associated with the spatial and channel scope. This motivates our advocate of other potential operators embodied with discrimination capability and expressiveness for visual recognition as an alternative, breaking through existing inductive biases of convolution. 我们重新思考卷积的固有特性,它与空间和通道范围有关。这促使我们提倡使用其他具有辨别能力和表达能力的潜在算子作为视觉识别的替代,突破了卷积现有的归纳偏见。(这是从involution的动机出发)
  • We bridge the emerging philosophy of incorporating self-attention into the learning procedure of visual representation. In this context, the desiderata of composing pixel pairs for relation modeling is challenged. Furthermore, we unify the view of self-attention and convolution through the lens of our involution. 我们在把自注意融入视觉表征的学习过程这一新兴哲学(应该是指代架构设计思路)上架起了桥梁。在此背景下,关系建模中对像素对的组合要求受到了挑战。此外,我们通过involution的镜头统一了自注意和卷积的观点。(这是从involution对于架构设计的意义出发)
  • The involution-powered architectures work universally well across a wide array of vision tasks, including image classification, object detection, instance and semantic segmentation, offering significantly better performance than the convolution-based counterparts. 基于involution的架构在广泛的视觉任务中都能很好地工作,包括图像分类、目标检测、实例和语义分割,并且比基于卷积的架构提供了更好的性能。(从involution的实际效果出发)

    Involution与传统卷积

    最直接是按照他们的计算方式来进行表示。
    假设对于仅包含卷积操作的单一卷积层,输入特征为Involution: Inverting the Inherence of Convolution for Visual Recognition - 图2,输出特征为Involution: Inverting the Inherence of Convolution for Visual Recognition - 图3

  • 标准卷积

Involution: Inverting the Inherence of Convolution for Visual Recognition - 图4

  • 使用的卷积核参数整体可以表示为Involution: Inverting the Inherence of Convolution for Visual Recognition - 图5,这里的Involution: Inverting the Inherence of Convolution for Visual Recognition - 图6表示卷积核的大小。
    • 一般两个K都为奇数以区分中心和周围像素,当然,对于池化等操作可能是偶数。
    • 由于涉及到对于卷积核内参数和对应的tensor输入的参数的索引,为了方便,这里使用相对卷积核中心的偏移值来索引,即使用Involution: Inverting the Inherence of Convolution for Visual Recognition - 图7,这里后面的乘积是笛卡尔乘积,二者共同合成一个新的二维空间坐标偏移集合。
  • 这里的Involution: Inverting the Inherence of Convolution for Visual Recognition - 图8实际上对应的是每一个卷积核,它有着Involution: Inverting the Inherence of Convolution for Visual Recognition - 图9大小。
    • 分组卷积

Involution: Inverting the Inherence of Convolution for Visual Recognition - 图10

  • 此时需要引入额外的通道分组数Involution: Inverting the Inherence of Convolution for Visual Recognition - 图11,且其必须可以整除Involution: Inverting the Inherence of Convolution for Visual Recognition - 图12。卷积核由于分组的设定,大小变成了Involution: Inverting the Inherence of Convolution for Visual Recognition - 图13,即Involution: Inverting the Inherence of Convolution for Visual Recognition - 图14
  • 另外这里为了方便索引,直接应用了组内索引,即Involution: Inverting the Inherence of Convolution for Visual Recognition - 图15,分别表示输入通道分组和输出通道分组组内的索引。
    • 分组卷积的极致,即深度分离卷积

Involution: Inverting the Inherence of Convolution for Visual Recognition - 图16

  • 此时分组数等于输入通道数。一般而言,此时会设置Involution: Inverting the Inherence of Convolution for Visual Recognition - 图17,这里我们考虑更一般的情况。卷积核参数可以表示为Involution: Inverting the Inherence of Convolution for Visual Recognition - 图18
    • Involution

Involution: Inverting the Inherence of Convolution for Visual Recognition - 图19

  • 卷积核和形式分组卷积类似,但是空间不共享,组内特征通道共享的,即Involution: Inverting the Inherence of Convolution for Visual Recognition - 图20
  • 这里由于是组内共享的卷积核,所以需要用到组内通道数这个量,设为Involution: Inverting the Inherence of Convolution for Visual Recognition - 图21
  • 这里的卷积核依赖于输入特征图Involution: Inverting the Inherence of Convolution for Visual Recognition - 图22,即由其生成。实际代码中使用了两层卷积实现。

    RedNet

    为了通过渐进式构建整个网络,我们通过堆叠残差快来模仿ResNet的设计,因为ResNet的优雅架构使其易于尝试新思想并进行比较。我们对ResNet的stem中(使用3x3或7x7 involution进行分类或密集预测)和trunk(对所有任务使用7x7 involution)位置中的所有bottleneck位置的3x3卷积进行替换,但保留所有1x1卷积用于通道投影和融合。这些经过精心设计的实体联合起来,形成了一种称为RedNet的新型高效backbone。
    一旦空间和通道信息交织在一起,神经网络内部就会出现大量的冗余。 但是,信息交互在RedNet中巧妙地解耦,朝着有利的精度与效率的权衡的方向发展。具体而言,在一个像素的通道维度中编码的信息隐式分散在其空间中 核生成步骤中的邻近区域,此后,由于具有庞大且动态的involution kernel,因此可以收集到丰富的感受野中的信息。必不可少的是,线性变换(通过1x1卷积实现)用于信道信息交换。综上所述,channel-spatial,spatial-alone和channel-alone的交互,交替且独立地作用于信息传播流,在确保表征能力的同时,协同促进了网络体系结构的小型化。

    相关内容

    动态卷积

  • Hypernetworks. ICLR 2017
  • Dynamic filter networks. NIPS, 2016
  • Condconv: Conditionally parameterized convolu-tions for efficient inference. NeurIPS 2019
  • Carafe: Content-aware reassembly of features. ICCV, 2019
  • HDFNet: Hierarchical Dynamic Filtering Network for RGB-D Salient Object Detection. ECCV, 2020 动态卷积+深度分离+扩张卷积的思想
  • Deformable convolutional networks. ICCV, 2017
  • Deformable convnets v2: More deformable, better results. CVPR, 2019
  • Active convolution: Learning the shape of convolution for image classification. CVPR, 2017
  • Deepface: Closing the gap to human-level perfor-mance in face verification. CVPR, 2014
  • DeepID: Deep learning face representation from predicting 10,000 classes. CVPR, 2014
  • Pixel-adaptive convolutional neural networks. CVPR, 2019
  • Adaptive convolutional kernels. ICCV Workshops, 2019

    Unfold

    简单的例子——单通道输入

    1. >>> a = torch.randn(1, 1, 3, 3)
    2. >>> unfold = nn.Unfold(kernel_size=(2, 3), stride=(1, 1), padding=(1, 1), dilation=1)
    3. >>> unfold(a).shape
    4. torch.Size([1, 6, 12])
    5. >>> unfold(a)
    6. tensor([[[ 0.0000, 0.0000, 0.0000, 0.0000, 0.0286, 0.6680, 0.0000, -0.8924, -0.8030, 0.0000, -2.2951, -0.0457],
    7. [ 0.0000, 0.0000, 0.0000, 0.0286, 0.6680, -0.2763, -0.8924, -0.8030, 1.0248, -2.2951, -0.0457, -0.4175],
    8. [ 0.0000, 0.0000, 0.0000, 0.6680, -0.2763, 0.0000, -0.8030, 1.0248, 0.0000, -0.0457, -0.4175, 0.0000],
    9. [ 0.0000, 0.0286, 0.6680, 0.0000, -0.8924, -0.8030, 0.0000, -2.2951, -0.0457, 0.0000, 0.0000, 0.0000],
    10. [ 0.0286, 0.6680, -0.2763, -0.8924, -0.8030, 1.0248, -2.2951, -0.0457, -0.4175, 0.0000, 0.0000, 0.0000],
    11. [ 0.6680, -0.2763, 0.0000, -0.8030, 1.0248, 0.0000, -0.0457, -0.4175, 0.0000, 0.0000, 0.0000, 0.0000]]])
    12. >>> a
    13. tensor([[[[ 0.0286, 0.6680, -0.2763],
    14. [-0.8924, -0.8030, 1.0248],
    15. [-2.2951, -0.0457, -0.4175]]]])

    这里做的就是对tensor a沿着h和w滑动滑窗,滑窗的h=2,w=3,沿着两个方向的移动的步长都为1,对tensor a的padding也是沿着h方向对上下各补1行0和沿着w方向左右各补1列0。

    1. tensor([[[[ 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
    2. [ 0.0000, 0.0286, 0.6680, -0.2763, 0.0000],
    3. [ 0.0000, -0.8924, -0.8030, 1.0248, 0.0000],
    4. [ 0.0000, -2.2951, -0.0457, -0.4175, 0.0000],
    5. [ 0.0000, 0.0000, 0.0000, 0.0000, 0.0000]]]])

    例如对于第一个窗口,即对上面这个已经padding之后的tensor处理时,窗口数据为 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0286, 0.6680 ,则它们会被收集起来堆叠到通道维度。也就是 unfold(a) 中的一列(因为unfold会将滑窗处理后的h和w维度以类似flatten的规则,按照从左至右从上到下、从低维度到高维度的过程,堆叠到一起)。

    稍微复杂一点的例子——多通道输入

    不过这里展示的将窗口中的数据堆叠到c上的过程是基于单通的tensor进行的演示,如果是多通道会如何?

    1. a = torch.randn(1, 2, 3, 3)
    2. a
    3. tensor([[[[-1.4588, -0.7465, 1.4516],
    4. [-1.2399, -0.9225, -2.0265],
    5. [-1.1223, -0.4368, -0.4471]],
    6. [[ 0.0395, -0.8731, -1.0842],
    7. [-0.0681, -0.8138, -0.3849],
    8. [ 0.9828, -0.0866, -0.7849]]]])
    9. unfold(a)
    10. tensor([[[ 0.0000, 0.0000, 0.0000, 0.0000, -1.4588, -0.7465, 0.0000, -1.2399, -0.9225, 0.0000, -1.1223, -0.4368],
    11. [ 0.0000, 0.0000, 0.0000, -1.4588, -0.7465, 1.4516, -1.2399, -0.9225, -2.0265, -1.1223, -0.4368, -0.4471],
    12. [ 0.0000, 0.0000, 0.0000, -0.7465, 1.4516, 0.0000, -0.9225, -2.0265, 0.0000, -0.4368, -0.4471, 0.0000],
    13. [ 0.0000, -1.4588, -0.7465, 0.0000, -1.2399, -0.9225, 0.0000, -1.1223, -0.4368, 0.0000, 0.0000, 0.0000],
    14. [-1.4588, -0.7465, 1.4516, -1.2399, -0.9225, -2.0265, -1.1223, -0.4368, -0.4471, 0.0000, 0.0000, 0.0000],
    15. [-0.7465, 1.4516, 0.0000, -0.9225, -2.0265, 0.0000, -0.4368, -0.4471, 0.0000, 0.0000, 0.0000, 0.0000],
    16. [ 0.0000, 0.0000, 0.0000, 0.0000, 0.0395, -0.8731, 0.0000, -0.0681, -0.8138, 0.0000, 0.9828, -0.0866],
    17. [ 0.0000, 0.0000, 0.0000, 0.0395, -0.8731, -1.0842, -0.0681, -0.8138, -0.3849, 0.9828, -0.0866, -0.7849],
    18. [ 0.0000, 0.0000, 0.0000, -0.8731, -1.0842, 0.0000, -0.8138, -0.3849, 0.0000, -0.0866, -0.7849, 0.0000],
    19. [ 0.0000, 0.0395, -0.8731, 0.0000, -0.0681, -0.8138, 0.0000, 0.9828, -0.0866, 0.0000, 0.0000, 0.0000],
    20. [ 0.0395, -0.8731, -1.0842, -0.0681, -0.8138, -0.3849, 0.9828, -0.0866, -0.7849, 0.0000, 0.0000, 0.0000],
    21. [-0.8731, -1.0842, 0.0000, -0.8138, -0.3849, 0.0000, -0.0866, -0.7849, 0.0000, 0.0000, 0.0000, 0.0000]]])

    从上面的例子来看, unfold 操作实际上也是按照着从低维到高维的过程进行的处理,即对c、h、w三个维度,按照w、h、c的先后顺序将滑窗内(要注意,这里的“滑窗要想象成三维的block,它通道数和输入特征一致)的数据收集起来,进而放到c维度上。

    与卷积的关系

    unfold 操作实现了卷积中最基本的滑窗收集数据的操作,可以用来构造更复杂的卷积。
    通过前面的那个多通道tensor的例子,可以知道,实际上对于卷积运算而言,如果我们已经得到了 unfold(a) 这样的将每个卷积滑窗内的数据收集到整个通道上的中间tensor,那么接下来只需要将卷积核变形,直接对这个中间tensor的通道维度进行矩阵相乘即可。而动态与标准卷积最大的差异就在于这里的各个通道是否共享这个卷积核。
    nn.Unfold 的文档中给出了一个构造标准卷积的例子:

    1. >>> # Convolution is equivalent with Unfold + Matrix Multiplication + Fold (or view to output shape)
    2. >>> inp = torch.randn(1, 3, 10, 12)
    3. >>> w = torch.randn(2, 3, 4, 5) # 这可以来自模型的特征,即动态卷积,亦或是一个parameter,即标准卷积
    4. >>> inp_unf = torch.nn.functional.unfold(inp, (4, 5)) # 这里用的是nn.Unfold的函数形式
    5. >>> inp_unf.shape
    6. torch.Size([1, 60, 56])
    7. >>> reshaped_w = w.view(w.size(0), -1).t()
    8. >>> reshaped_w.shape
    9. torch.Size([60, 2])
    10. >>> out_unf = inp_unf.transpose(1, 2).matmul(reshaped_w).transpose(1, 2)
    11. # 60 -> 2 这个过程既有卷积窗口的整合亦有通道的调整
    12. >>> out_unf.shape
    13. torch.Size([1, 2, 56])
    14. >>> out = torch.nn.functional.fold(out_unf, (7, 8), (1, 1)) # fold是和unfold相反的操作,是将堆叠起来的数据展开成窗口
    15. >>> # or equivalently (and avoiding a copy),
    16. >>> # out = out_unf.view(1, 2, 7, 8)
    17. >>> (torch.nn.functional.conv2d(inp, w) - out).abs().max() # 使用unfold算出的out和使用conv2d算出的结果基本一致
    18. tensor(1.9073e-06)

    但是需要注意到的一点是,一般而言,使用 unfold 构造的卷积速度会慢不少。我个人感觉,这主要还是因为 unfold 构造卷积时,中间的形状转化造成了不必要的时间消耗。

    链接

  • 论文:https://arxiv.org/abs/2103.06255

  • 代码:https://github.com/d-li14/involution
  • 解析:https://blog.csdn.net/amusi1994/article/details/114697821