在计算机视觉领域,深度卷积神经网络(CNN)一直是解决视觉任务的主流模型。它们在多个基准测试中取得了最先进的性能,并在过去几年中主导了视觉领域。随着自注意力模型在自然语言处理领域的巨大成功,它们在视觉任务中的应用也越来越多。在这样的背景下,2021年4月11日,由香港科技大学、字节跳动AI实验室和北京大学的研究人员提出的新型神经网络原语操作——Involution,为带来了新的视角。本文将深入探讨Involution的概念和应用。
卷积是信号处理中最重要的操作之一,它通过结合两个信号来生成第三个信号。由于图像本质上是二维信号,卷积在视觉任务中找到了应用。从数学上讲,卷积可以表示为:
(f * w)
其中f是输入图像,w是另一个被称为核或滤波器的二维矩阵(大小为(2a,2b))。在探索w的内容及其通过卷积对输入图像的影响之前,让看看如何计算给定矩阵的卷积("*"是卷积操作符)。
伪代码如下:
for each pixel in image row:
set accumulator to zero
for each kernel row in kernel:
for each element in kernel row:
if element position corresponding* to pixel position then
multiply element value corresponding* to pixel value
add result to accumulator
endif
set output image pixel to accumulator
核通常比图像小,它被滑动覆盖图像以生成每个输出像素值。可以通过控制核滑动的速度(步长)和输入图像周围的填充来控制输出形状。
核可以是任何东西,这取决于希望卷积的输出是什么。为了更好地理解,让看一些示例核及其对图像的影响。
卷积之所以流行,是因为它的形状不可知和通道特定的行为。让揭开这些行为的神秘面纱。
卷积描述了线性时不变(LTI)类操作的输出。当谈论二维图像时,时间不变性转化为空间不变性。这使能够将具有平移等价性的系统建模为卷积。
图像的不同通道包含不同的信息。因此,对每个图像通道使用不同的卷积核允许提取这些不同的信息。
ResNet 18变体使用核来提取每一层的特徵图。提取的特徵图数量等于核或滤波器的数量。输出形状取决于步长和填充。这些特徵图被认为是下一步的输入通道。
卷积的最大缺点是其固有的局部性限制。卷积在提取不同空间位置的视觉模式方面的能力有限。每个像素的感受野狭窄,使得很难模拟像素之间的长距离多跳依赖关系。
注意力是另一项革命性的神经网络原语,它作为Transformer架构的一部分,改变了自然语言处理。其变体正在以前所未有的速度被采用于不同的领域。然而,其二次计算复杂度限制了其在图像相关任务中的使用。自注意力的主要思想是使用基于像素相似性的权重对图像进行非局部过滤。
Involution的概念是反转卷积的特性。Involution的计算与卷积相同,除了核。与卷积不同,不是在整个图像上滑动单个学习到的核。核是动态实例化的,每个像素根据像素的值和学习到的参数生成核。这很好,因为类似于注意力,输入控制核的权重。Involution使用以下方式计算每个输出像素:
On the channel dimension, the kernel is shared among groups of channels. The kernel is generated using the following linear network.
class Involution(nn.Module):
def __init__(self, channels, kernel_size=3, reduction_ratio=4, channels_per_group=16):
super(Involution, self).__init__()
self.channels = channels
self.channels_per_group = channels_per_group
self.reduction_ratio = reduction_ratio
self.groups = channels // channels_per_group
self.kernel_size = kernel_size
self.weights0 = nn.Conv2d(self.channels, self.channels // self.reduction_ratio, 1)
self.BN = nn.BatchNorm2d(self.channels // self.reduction_ratio)
self.relu = nn.ReLU(True)
self.weights1 = nn.Conv2d(self.channels // self.reduction_ratio, self.groups * self.kernel_size**2, 1)
self.kernel_generator = nn.Sequential(self.weights0, self.BN, self.relu, self.weights1)
self.patch_maker = nn.Unfold(self.kernel_size, 1, (self.kernel_size - 1) // 2, 1)
def forward(self, x):
b, c, h, w = x.shape
kernel_weights = self.kernel_generator(x)
kernels = kernel_weights.view(b, self.groups, 1, self.kernel_size**2, h, w)
patches = self.patch_maker(x).view(b, self.groups, self.channels_per_group, self.kernel_size**2, h, w)
return (patches * kernels).sum(dim=3).view(b, self.channels, h, w)