看了 ECCV 这篇 Oral 工作后对动态卷积非常感兴趣,下面梳理一下有关的几个工作。

[16 NIPS] Dynamic Filter Networks

这个工作是动态卷积的鼻祖,用一个生成网络接收 一个条件输入 为另外一个输入生成特异性的卷积核。这个思路好棒。
截屏2020-09-01 上午11.28.07.png

Filter-generating network

接受一个视图的输入生成 n 个卷积核 [20 ECCV] Conditional Convolutions for Instance Segmenation - 图2
其中 s 是卷积核的大小,cB 是 Input B 的 Channel,
对于 dynamic convolution 来说 d = 1
对于 dynamic local filtering 来说 d = h ✖️ w

Dynamic Filtering Layer

Dynamic convolution

截屏2020-09-01 上午11.51.42.png
对于 Dynamic convolution 来说,Filter-generating network 就生成一个普通的卷积核作用于 Input B。

Dynamic local filtering layer

截屏2020-09-01 上午11.57.44.png
为每个位置生成一个卷积核,这样就做到了 not only sample specific but also position specific。

Demo on vedio

下面使用在视频上的一个例子:
截屏2020-09-01 下午1.42.53.png

[19 NIPS] CondConv: Conditionally Parameterized Convolutions for Efficient Inference

上面的工作是动态生成卷积核的,这个工作呢是生成了待选的卷积核然后进行加权组合的。
在一个层中常见的是进行多次卷积,然后整合,如下图:
截屏2020-09-01 下午2.42.08.png
这种可以表示为 [20 ECCV] Conditional Convolutions for Instance Segmenation - 图7
可以看到,这需要做多次卷积,在进行整合。

不过在没有非线性层的情况下,上述式子其实可以等价成 [20 ECCV] Conditional Convolutions for Instance Segmenation - 图8
这样就只要做一次卷积就可以了,减少了计算量。
截屏2020-09-01 下午3.38.09.png

ROUTE FN 用来生成加权的权重,操作是 GAP 后,过一个 FC 层, 然后再 Sigmoid。

[20 CVPR] Dynamic Convolution: Attention over Convolution Kernels

这个和 CondConv 差不多,画的图好看多了🌚。
这个工作和 CondConv 应该是同期工作,出发点不一样, CondConv 从集成的思路解释, DynamicConv 从 Attention 角度解释。两个角度可以欣赏一下。
作者在文中也加入了和 CondConv 的对比,训练上的差异可以看看。
截屏2020-09-01 下午3.50.32.png
也可以化成这样:
截屏2020-09-01 下午3.54.15.png

形式化出来如下:
传统卷积[20 ECCV] Conditional Convolutions for Instance Segmenation - 图12

Dynamic Convolution:
[20 ECCV] Conditional Convolutions for Instance Segmenation - 图13

和 CondConv 的区别是用 Softmax 替代了 Sigmoid。

[20 arXiv] DyNet:Dynamic Convolution for Accelerating Convolutional Neural

和上面两个也是类似的。直接看图就好了。本文作者又换了一个角度行文,也可以一看。
截屏2020-09-01 下午4.28.52.png
截屏2020-09-01 下午4.29.56.png

[19 ICCV] Dynamic Multi-scale Filters for Semantic Segmentation

Dynamic Filtering Network 用在生成多尺度卷积核上。
截屏2020-09-01 下午6.18.50.png

[20 ECCV] Hierarchical Dynamic Filtering Network for RGB-D SOD

代码:Github

Dynamic Filtering Network 在 RGB-D SOD 任务上的应用,这个看图就能明白了,和上一篇做语义分割的也比较类似。
截屏2020-09-01 下午5.28.36.png
截屏2020-09-01 下午5.36.27.png

[20 ECCV] Conditional Convolutions for Instance Segmenation

代码:Github

相关讲解:知乎视频(20:00开始)

知乎

截屏2020-09-01 下午7.47.23.png

Dynamic Filtering 的代码实现

Batch 中每一个 sample 的每个位置都动态生成 Filter

这个实现来自 Hierarchical Dynamic Filtering Network for RGB-D SOD。策略是学习参数手动进行卷积。

  1. '''
  2. https://github.com/lartpang/HDFNet/blob/49455f1215952ef4a11918b61ff391b923ce23ca/module/MyModules.py#L13
  3. '''
  4. class DepthDC3x3_1(nn.Module):
  5. def __init__(self, in_xC, in_yC, out_C, down_factor=4):
  6. """DepthDC3x3_1,利用nn.Unfold实现的动态卷积模块
  7. Args:
  8. in_xC (int): 第一个输入的通道数
  9. in_yC (int): 第二个输入的通道数
  10. out_C (int): 最终输出的通道数
  11. down_factor (int): 用来降低卷积核生成过程中的参数量的一个降低通道数的参数
  12. """
  13. super(DepthDC3x3_1, self).__init__()
  14. self.kernel_size = 3
  15. self.fuse = nn.Conv2d(in_xC, out_C, 3, 1, 1)
  16. self.gernerate_kernel = nn.Sequential(
  17. nn.Conv2d(in_yC, in_yC, 3, 1, 1),
  18. DenseLayer(in_yC, in_yC, k=down_factor),
  19. nn.Conv2d(in_yC, in_xC * self.kernel_size ** 2, 1),
  20. )
  21. self.unfold = nn.Unfold(kernel_size=3, dilation=1, padding=1, stride=1)
  22. def forward(self, x, y):
  23. N, xC, xH, xW = x.size()
  24. kernel = self.gernerate_kernel(y).reshape([N, xC, self.kernel_size ** 2, xH, xW])
  25. unfold_x = self.unfold(x).reshape([N, xC, -1, xH, xW])
  26. result = (unfold_x * kernel).sum(2)
  27. return self.fuse(result)

卷积操作可以分解成 1)取出局部的数值并展开 2)矩阵乘法 3)reshape 回去相应的维度。Conv = unfold + matmul + fold (or view to output shape)。

首先看一下 torch.nn.Unfold 这个类,这个类的作用是从一批输入张量中提取滑动局部块的内容。
以 2D 为例,输入是 (N, C, H, W), 那么输出就是 (N, C × kh × kw, L) ,L是可以滑动的次数
一个简单的例子

  1. a = torch.randn(1, 3, 10, 12)
  2. a_unf = torch.nn.functional.unfold(input=a, kernel_size=(4, 5),dilation=1, padding=0, stride=1)
  3. print(a_unf.shape)
  4. # 输出为:torch.Size([1, 60, 56])
  5. # 60 = 3 * 4 * 5
  6. # 56 = (10 - 4 + 1) * (12 - 5 + 1)

在作者代码中 self.gernerate_kernel(y) 会生成一个 (N, xC self.kernel_size 2, xH, xW) 的 tensor,
进一步被 reshape 成 (N, xC, self.kernel_size
2, xH, xW)
self.unfold(x) 会生成一个 (N, xC
self.kernel_size 2, xH, xW) 的 tensor,
进一步被 reshape 成 (N, xC, self.kernel_size
2, xH, xW)
result = (unfold_x * kernel).sum(2) 得到 (N, xC, xH, xW) 的 tensor,完成动态卷积。

Batch 中每一个 sample 都动态生成 Filter,但每个位置共享卷积

实现一:

这个的实现方式只需要让上面的 self.gernerate_kernel(y)生成 (N, xC self.kernel_size * 2, 1, 1)的 tensor 就可以了。

  1. # self.unfold = nn.Unfold(kernel_size=3, dilation=1, padding=1, stride=1)
  2. [B, tC, tH, tW] = feat.size()
  3. unfold_feat = self.unfold(feat).reshape([B, -1, tH*tW])
  4. induced_feat = dynamic_kernel.matmul(unfold_feat)
  5. induced_feat = induced_feat.reshape(B, tC, tH, tW)

实现二:

这个实现方式来自 Conditional Convolutions for Instance Segmenation

看 torch.nn.functional.conv2d 中的几个参数,_weight_, _bias_, _groups_
weight – filters of shape (out_channels, [20 ECCV] Conditional Convolutions for Instance Segmenation - 图20, kH, kW)
bias – optional bias tensor of shape (out_channels). Default: None
groups – split input into groups, in_channels should be divisible by the number of groups. Default: 1

用 groups 来实现动态卷积。

下面是分离参数的参考操作:

  1. '''
  2. https://github.com/aim-uofa/AdelaiDet/blob/f9103480b351330774205ecd6376a0683437bcfa/adet/modeling/condinst/dynamic_mask_head.py
  3. '''
  4. def parse_dynamic_params(params, channels, weight_nums, bias_nums):
  5. assert params.dim() == 2
  6. assert len(weight_nums) == len(bias_nums)
  7. assert params.size(1) == sum(weight_nums) + sum(bias_nums)
  8. num_insts = params.size(0)
  9. num_layers = len(weight_nums)
  10. params_splits = list(torch.split_with_sizes(
  11. params, weight_nums + bias_nums, dim=1
  12. ))
  13. weight_splits = params_splits[:num_layers]
  14. bias_splits = params_splits[num_layers:]
  15. for l in range(num_layers):
  16. if l < num_layers - 1:
  17. # out_channels x in_channels x 1 x 1
  18. weight_splits[l] = weight_splits[l].reshape(num_insts * channels, -1, 1, 1)
  19. bias_splits[l] = bias_splits[l].reshape(num_insts * channels)
  20. else:
  21. # out_channels x in_channels x 1 x 1
  22. weight_splits[l] = weight_splits[l].reshape(num_insts * 1, -1, 1, 1)
  23. bias_splits[l] = bias_splits[l].reshape(num_insts)
  24. return weight_splits, bias_splits

下面这个便是作者写动态卷积参数加载的部分,作者为每个 instance 动态生成了权重,并用分组卷积的方法

  1. def mask_heads_forward(self, features, weights, biases, num_insts):
  2. '''
  3. :param features
  4. :param weights: [w0, w1, ...] 长度为层数
  5. :param bias: [b0, b1, ...]
  6. :return:
  7. '''
  8. assert features.dim() == 4
  9. n_layers = len(weights)
  10. x = features
  11. for i, (w, b) in enumerate(zip(weights, biases)):
  12. x = F.conv2d(
  13. x, w, bias=b,
  14. stride=1, padding=0,
  15. groups=num_insts
  16. )
  17. if i < n_layers - 1:
  18. x = F.relu(x)
  19. return x

那么其实如果为每个 Batch 的每个图片生成的话, F.conv2d(x, weight = w, bias=b, ..., groups=N)