前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >我卷我自己——cvpr2021:Involution

我卷我自己——cvpr2021:Involution

作者头像
BBuf
发布2021-03-25 12:03:00
1.8K1
发布2021-03-25 12:03:00
举报
文章被收录于专栏:GiantPandaCVGiantPandaCV

引言

本文重新回顾了常规卷积的设计,其具有两个重要性质,一个是空间无关性,比如3x3大小的卷积核是以滑窗的形式,滑过特征图每一个像素(即我们所说的参数共享)。另外一个是频域特殊性,体现在卷积核在每个通道上的权重是不同的

我们对以上的设计原则进行了"反转",设计了一种 involution(内卷???)的操作,一方面能降低模型的参数量,另一方面也能提升模型性能,还能和最近很火的自注意力机制联系起来。该模块在各大图像任务上都有不错的性能提升。

简单回顾卷积

最初的神经网络都是由一层层全连接层网络叠加起来,对于简单的任务来说参数量还好。但是对于图像任务,动辄几百上千的像素,则全连接层的参数量会十分巨大。如果是全连接层处理二维图像,那么大致形式如下

全连接层

而卷积神经网络考虑了局部连接性,只考虑了局部的像素,从而让参数量大大减少,形式如下

卷积

由于常规卷积核是对所有输入通道进行计算,在起初的一些低算力设备上计算损耗还是很大,Alexnet提出分组卷积,对输入通道进行分组,然后单独卷积,形式如下

Group Conv

而谷歌提出的Depthwise Conv则将分组卷积推向了极端——分组数是输入通道数目,即每个输入通道单独卷积,形式如下

Depthwise

卷积核形式的演进还是基于通道做的,最基础的两个性质空间无关性和频域特殊性依旧没有改变。而Involution操作给出了一个不同的思路。

Involution的设计原则

Involution的设计原则就是颠倒常规卷积核的两个设计原则,即从空间无关性,频域特殊性转变成空间特殊性频域无关性

在这里插入图片描述

卷积神经网络存在下采样层,导致各个阶段的特征图长宽会变化。既然要与空间域联系起来,那么第一个问题是如何参数化一个Invotion的卷积核。一个很自然的想法就是设置一个函数

\phi

,让他根据输入的张量,输出一个跟特征图长宽相关的张量,再把它作为卷积核。

该函数公式写为

H_{i, j} = \phi(X_i, j) = W_1\sigma(W_0(X_{i, j}))

在实际的代码中,作者用一个类似BottleNeck的形式,可以通过控制缩放比例调整参数量,用两个1x1卷积对通道进行缩放,最后一个卷积输出通道数为(K * K * Groups),其中K代表后续involution卷积核大小,Groups代表involution操作的分组数。(如果遇到需要下采样的情况,则接一个步长为2的平均池化层。),最后我们可以得到一个形状为N*(K * K * Groups)HW的张量,下面是这部分操作的代码

代码语言:javascript
复制
 ...
    reduction_ratio = 4
    self.group_channels = 16
    self.groups = self.channels // self.group_channels
    self.conv1 = ConvModule(
            in_channels=channels,
            out_channels=channels // reduction_ratio,
            kernel_size=1,
            conv_cfg=None,
            norm_cfg=dict(type='BN'),
            act_cfg=dict(type='ReLU'))
    self.conv2 = ConvModule(
            in_channels=channels // reduction_ratio,
            out_channels=kernel_size**2 * self.groups,
            kernel_size=1,
            stride=1,
            conv_cfg=None,
            norm_cfg=None,
            act_cfg=None)
def forward(self, x): 
    weight = self.conv2(self.conv1(x if self.stride == 1 else self.avgpool(x)))
    ...

下面就会拿这个weight来做当作一个卷积核,对x卷积。

读到这里可能会比较奇怪,为什么卷积核形状长这样,我们常见的卷积核应该是(C_in, C_out, K, K)。这其实也是这篇工作的关键之处,上面我们提到他这里注重的是频域无关性,空间特殊性。因此它分组卷积的做法是 每一组内的特征图共享一个卷积核的参数,但是 同一组内,不同空间位置,使用的是不同的卷积核

原文是 an involution kernel located at the corresponding coordinate (i, j), but shared over the channels.

这段比较费解,我画了一个简单的示意图

Involution

为了方便演示,这里设置N为1,特征图通道为16个,分组数为4,ksize=3

首先输入特征图被分为四组,每组有4个特征图 之前经过两次1x1卷积,我们得到了involution所需的权重,形状为(N, Groups, ksize * ksize, H, W), 在该例子中为(1, 4, 3 * 3, H, W) ,那么分配给每个组的,就是一个(1, 3 * 3, H, W),不考虑Batchsize的话,那么每组就有H * W个3x3的卷积核。

在通道维上,每组的特征图共享一个卷积核,而在同一组的不同空间位置,使用不同的卷积核。

处理完后,再把各组的结果拼接回来,下面是完整的involution操作代码

代码语言:javascript
复制
import torch.nn as nn
from mmcv.cnn import ConvModule


class involution(nn.Module):

    def __init__(self,
                 channels,
                 kernel_size,
                 stride):
        super(involution, self).__init__()
        self.kernel_size = kernel_size
        self.stride = stride
        self.channels = channels
        reduction_ratio = 4
        self.group_channels = 16
        self.groups = self.channels // self.group_channels
        self.conv1 = ConvModule(
            in_channels=channels,
            out_channels=channels // reduction_ratio, # 通过reduction_ratio控制参数量
            kernel_size=1,
            conv_cfg=None,
            norm_cfg=dict(type='BN'),
            act_cfg=dict(type='ReLU'))
        self.conv2 = ConvModule(
            in_channels=channels // reduction_ratio,
            out_channels=kernel_size**2 * self.groups,
            kernel_size=1,
            stride=1,
            conv_cfg=None,
            norm_cfg=None,
            act_cfg=None)
        if stride > 1:
         # 如果步长大于1,则加入一个平均池化
            self.avgpool = nn.AvgPool2d(stride, stride)
        self.unfold = nn.Unfold(kernel_size, 1, (kernel_size-1)//2, stride)

    def forward(self, x):
        weight = self.conv2(self.conv1(x if self.stride == 1 else self.avgpool(x))) # 得到involution所需权重
        b, c, h, w = weight.shape
        weight = weight.view(b, self.groups, self.kernel_size**2, h, w).unsqueeze(2) # 将权重reshape成 (B, Groups, 1, kernelsize*kernelsize, h, w)
        out = self.unfold(x).view(b, self.groups, self.group_channels, self.kernel_size**2, h, w) # 将输入reshape
        out = (weight * out).sum(dim=3).view(b, self.channels, h, w) # 求和,reshape回NCHW形式
        return out

实验结果

作者基于ResNet模型,将Bottleneck模块的中间卷积块,替换成7x7大小的involution操作。改进后的模型称为RedNet

实验表格

可以看到实验结果还是很不错的,不仅压缩了网络参数,在中小网络也能提升模型精度。(但我更好奇的是实际运行的速度,如每秒能处理多少图片),在其他图像任务上也有提升,这里就不放出来了,有兴趣的读者可以去读下原文。

对于Involution操作的分组数,Kernel大小,作者也做了相关消融实验

消融实验

可以看到从3x3到7x7,精度是稳定提高的,但是加到9x9以后提升有限。为了平衡参数量和精度,作者选择了7x7大小的Kernel,分组通道数为16,生成Kernel的卷积模块里,reduction参数设为4。

总结

这篇论文还是挺有意思的,作者阵容也很豪华,其中包括SENet的作者HuJie。现在的卷积核改进基本都是从通道维度去做,而这篇工作颠覆了这种思想,跟常规卷积反着来,做了一个自己卷自己的内卷操作。

论文还提到了这个操作和自注意力机制的关系,但是笔者并没有读太懂,就没有阐述(还望相关作者解答下)。作者还留了一些坑,我未来也很期待NAS在该模块上更多的探索。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-03-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 GiantPandaCV 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • 简单回顾卷积
  • Involution的设计原则
  • 实验结果
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档