前言
在这里我们将回顾一下2020年的顶刊论文 《Dynamic Convolution: Attention over Convolution Kernels》 在本文里将详细介绍一种新型的卷积神经网络操作,即动态卷积(Dynamic Convolution)。动态卷积是一种基于注意力机制的卷积操作,它可以自适应地调整卷积核的采样点,从而更好地适应输入数据的空间结构。本文将首先介绍传统的卷积操作的缺点,然后详细介绍动态卷积的原理和实现方式。
传统卷积的缺点
传统的卷积运算使用一个固定的内核来扫描输入数据。然而,这个固定的核可能并不总是与输入的空间结构很好地对齐。传统卷积结构相对于动态卷积存在以下缺点:
- 固定采样点:传统卷积结构使用固定的采样点来对输入进行卷积操作,这种方式可能无法适应输入数据的空间结构变化,导致模型无法充分提取特征。
- 固定核大小:传统卷积结构使用固定的核大小来卷积输入数据,这种方式可能无法适应不同空间位置的特征差异,从而影响模型性能。
- 计算量大:传统卷积结构的计算量较大,尤其是在高维数据上时,需要消耗大量的计算资源,导致模型的训练和推理速度较慢。
- 特征冗余:传统卷积结构使用相同的核对整个特征图进行卷积操作,这种方式可能会产生特征冗余,导致模型无法充分利用输入数据的特征。
- 空间不变性:传统卷积结构具有空间不变性,即无论输入数据中的特征在图像中的位置如何,卷积核始终可以从输入中提取出相同的特征,这可能会导致模型忽略一些空间信息,从而影响模型的性能。
针对传统卷积的缺点,我们提出了一种动态卷积:动态卷积通过引入一组额外的可学习参数来解决这个问题,称为偏移量,允许内核根据输入动态调整。动态卷积通过引入可学习的偏移量来解决传统卷积结构的缺点,从而提高模型的性能。
动态卷积
动态卷积不是每层使用一个卷积核,而是根据它们的注意力(依赖于输入)动态聚合多个并行卷积核。装配多个ker不仅由于内核尺寸小而计算效率高,而且由于这些内核通过注意以非线性方式聚合,因此具有更强的表示能力。
原理
动态卷积的核心思想是通过引入可学习的偏移量来调整卷积核的采样点,从而更好地适应输入数据的空间结构。具体来说,动态卷积首先在输入特征图上应用一个类似于注意力机制的模块,该模块通过学习输入特征图中每个空间位置的重要性来调整卷积核的采样点。然后,动态卷积使用调整后的卷积核对输入数据进行卷积操作。
实现
动态卷积的实现方式包括以下步骤:
(1) 定义偏移量:定义一个偏移量矩阵M∈RH×W×K2,其中H和W分别表示输入特征图的高度和宽度,KKK表示卷积核的大小。偏移量矩阵M的每个元素表示对应空间位置上卷积核中心点的偏移量。
(2) 计算卷积核采样点:使用偏移量矩阵M计算每个空间位置上卷积核的采样点,即将卷积核中心点的坐标(i,j)加上对应空间位置上的偏移量,得到新的采样点坐标(i′,j′)。
(3) 应用卷积核:使用新的采样点坐标(i′,j′)在输入特征图上应用卷积核,得到输出特征图。
(4) 训练模型:训练模型时,需要将偏移量矩阵M作为可训练参数加入模型中,并使用反向传播算法来更新偏移量矩阵M,从而使得模型能够自适应地调整卷积核的采样点,更好地适应输入数据的空间结构。
代码实现:
python
复制代码
import torch import torch.nn as nn import torch.nn.functional as F class attention2d(nn.Module): def __init__(self, in_planes, ratios, K, temperature, init_weight=True): super(attention2d, self).__init__() assert temperature%3==1 self.avgpool = nn.AdaptiveAvgPool2d(1) if in_planes!=3: hidden_planes = int(in_planes*ratios)+1 else: hidden_planes = K self.fc1 = nn.Conv2d(in_planes, hidden_planes, 1, bias=False) # self.bn = nn.BatchNorm2d(hidden_planes) self.fc2 = nn.Conv2d(hidden_planes, K, 1, bias=True) self.temperature = temperature if init_weight: self._initialize_weights() def _initialize_weights(self): for m in self.modules(): if isinstance(m, nn.Conv2d): nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') if m.bias is not None: nn.init.constant_(m.bias, 0) if isinstance(m ,nn.BatchNorm2d): nn.init.constant_(m.weight, 1) nn.init.constant_(m.bias, 0) def updata_temperature(self): if self.temperature!=1: self.temperature -=3 print('Change temperature to:', str(self.temperature)) def forward(self, x): x = self.avgpool(x) x = self.fc1(x) x = F.relu(x) x = self.fc2(x).view(x.size(0), -1) return F.softmax(x/self.temperature, 1) class DYConv2d(nn.Module): def __init__(self, in_planes, out_planes, kernel_size, ratio=0.25, stride=1, padding=0, dilation=1, groups=1, bias=True, K=4,temperature=34, init_weight=True): super(DYConv2d, self).__init__() assert in_planes%groups==0 self.in_planes = in_planes self.out_planes = out_planes self.kernel_size = kernel_size self.stride = stride self.padding = padding self.dilation = dilation self.groups = groups self.bias = bias self.K = K self.attention = attention2d(in_planes, ratio, K, temperature) self.weight = nn.Parameter(torch.randn(K, out_planes, in_planes//groups, kernel_size, kernel_size), requires_grad=True) if bias: self.bias = nn.Parameter(torch.zeros(K, out_planes)) else: self.bias = None if init_weight: self._initialize_weights() #TODO 初始化 def _initialize_weights(self): for i in range(self.K): nn.init.kaiming_uniform_(self.weight[i]) def update_temperature(self): self.attention.updata_temperature() def forward(self, x):#将batch视作维度变量,进行组卷积,因为组卷积的权重是不同的,动态卷积的权重也是不同的 softmax_attention = self.attention(x) batch_size, in_planes, height, width = x.size() x = x.view(1, -1, height, width)# 变化成一个维度进行组卷积 weight = self.weight.view(self.K, -1) # 动态卷积的权重的生成, 生成的是batch_size个卷积参数(每个参数不同) aggregate_weight = torch.mm(softmax_attention, weight).view(batch_size*self.out_planes, self.in_planes//self.groups, self.kernel_size, self.kernel_size) if self.bias is not None: aggregate_bias = torch.mm(softmax_attention, self.bias).view(-1) output = F.conv2d(x, weight=aggregate_weight, bias=aggregate_bias, stride=self.stride, padding=self.padding, dilation=self.dilation, groups=self.groups*batch_size) else: output = F.conv2d(x, weight=aggregate_weight, bias=None, stride=self.stride, padding=self.padding, dilation=self.dilation, groups=self.groups * batch_size) output = output.view(batch_size, self.out_planes, output.size(-2), output.size(-1)) return output if __name__ == '__main__': x = torch.randn(1, 3, 256, 256) model = DYConv2d(in_planes=3, out_planes=16, kernel_size=3, ratio=0.25, padding=1,) print(model(x).shape)
吐槽
在网上看见有好多人吐槽这篇文章够水,尤其是吐槽这篇文章是仿照SENet写的,网友给出的论证如下:将se的sigmoid改成sofmax,把原来se用在feature map上的attention用在卷积核上 。就这样法了一篇顶刊文章,属实水到家了。
当然也有人感觉和Google的 cond conv区别不大,也有人感觉这篇文章【撞衫】 SK Conv,这个也是自适应卷积,融合多尺度,CVPR2019的文章。