关于深度学习:Pytorch之SpatialShiftOperation的5种实现策略

22次阅读

共计 17321 个字符,预计需要花费 44 分钟才能阅读完成。

Pytorch 之 Spatial-Shift-Operation 的 5 种实现策略

本文已受权极市平台, 并首发于极市平台公众号. 未经容许不得二次转载.

原始文档(可能会进一步更新): https://www.yuque.com/lart/ug…

前言

之前看了一些应用空间偏移操作来代替区域卷积运算的论文:

  • 粗看: https://www.yuque.com/lart/ar…

    • (CVPR 2018) [Grouped Shift] Shift: A Zero FLOP, Zero Parameter Alternative to Spatial Convolutions:
    • (ICCV 2019) 4-Connected Shift Residual Networks
    • (NIPS 2018) [Active Shift] Constructing Fast Network through Deconstruction of Convolution
    • (CVPR 2019) [Sparse Shift] All You Need Is a Few Shifts: Designing Efficient Convolutional Neural Networks for Image Classification
  • 细看:

    • Vision MLP 之 Hire-MLP Vision MLP via Hierarchical Rearrangement

      • Hire-MLP: Vision MLP via Hierarchical Rearrangement:https://www.yuque.com/lart/pa…
    • Visoin MLP 之 CycleMLP A MLP-like Architecture for Dense Prediction

      • CycleMLP: A MLP-like Architecture for Dense Prediction:https://www.yuque.com/lart/pa…
    • Vision MLP 之 S2-MLP V1&V2 Spatial-Shift MLP Architecture for Vision

      • S2-MLP: Spatial-Shift MLP Architecture for Vision:https://www.yuque.com/lart/pa…
      • S2-MLPv2: Improved Spatial-Shift MLP Architecture for Vision:https://www.yuque.com/lart/pa…

看完这些论文后, 通过参考他们提供的外围代码 (次要是前面那些 MLP 办法), 让我对于实现空间偏移有了一些想法.
通过整合现有的常识, 我演绎总结了五种实现策略.
因为我集体应用 pytorch, 所以这里的展现也可能会用到 pytorch 本身提供的一些有用的函数.

问题形容

在提供实现之前, 咱们应该先明确目标以便于后续的实现.
这些现有的工作都能够简化为:

给定 tensor $X \in \mathbb{R}^{1 \times 8 \times 5 \times 5}$, 这里遵循 pytorch 默认的数据格式, 即 B, C, H, W .

通过变换操作 $\mathcal{T}: x \rightarrow \tilde{x}$, 将 $X$ 转换为 $\tilde{X}$.

这里 tensor $\tilde{X} \in \mathbb{R}^{1 \times 8 \times 5 \times 5}$, 为了提供正当的比照, 这里对立应用前面章节中基于 ” 切片索引 ” 策略的后果作为 $\tilde{X}$ 的值.

import torch

xs = torch.meshgrid(torch.arange(5), torch.arange(5))
x = torch.stack(xs, dim=0)
x = x.unsqueeze(0).repeat(1, 4, 1, 1).float()
print(x)

'''
tensor([[[[0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4.]],

         [[0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.]],

         [[0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4.]],

         [[0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.]],

         [[0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4.]],

         [[0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.]],

         [[0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4.]],

         [[0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.]]]])
'''

办法 1: 切片索引

这是最间接和简略的策略了. 这也是 S2-MLP 系列中应用的策略.
咱们将其作为其余所有策略的参考对象. 后续的实现中同样会失去这个后果.

direct_shift = torch.clone(x)
direct_shift[:, 0:2, :, 1:] = torch.clone(direct_shift[:, 0:2, :, :4])
direct_shift[:, 2:4, :, :4] = torch.clone(direct_shift[:, 2:4, :, 1:])
direct_shift[:, 4:6, 1:, :] = torch.clone(direct_shift[:, 4:6, :4, :])
direct_shift[:, 6:8, :4, :] = torch.clone(direct_shift[:, 6:8, 1:, :])
print(direct_shift)

'''
tensor([[[[0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4.]],

         [[0., 0., 1., 2., 3.],
          [0., 0., 1., 2., 3.],
          [0., 0., 1., 2., 3.],
          [0., 0., 1., 2., 3.],
          [0., 0., 1., 2., 3.]],

         [[0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4.]],

         [[1., 2., 3., 4., 4.],
          [1., 2., 3., 4., 4.],
          [1., 2., 3., 4., 4.],
          [1., 2., 3., 4., 4.],
          [1., 2., 3., 4., 4.]],

         [[0., 0., 0., 0., 0.],
          [0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.]],

         [[0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.]],

         [[1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4.],
          [4., 4., 4., 4., 4.]],

         [[0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.]]]])
'''

办法 2: 特色图偏移—— torch.roll

pytorch 提供了一个间接对特色图进行偏移的函数, 即 torch.roll . 这一操作在最近的 transformer 论文和 mlp 中有一些工作曾经开始应用, 例如 SwinTransformer 和 AS-MLP.

这里展现下 AS-MLP 论文中提供的伪代码:

其次要作用就是将特色图沿着某个轴向进行偏移, 并反对同时沿着多个轴向偏移, 从而结构更多样的偏移方向.
为了实现与后面雷同的后果, 咱们须要首先对输出进行 padding.
因为间接切片索引有个特点就是边界值是会反复呈现的, 而若是间接 roll 操作, 会导致所有的值整体挪动.
所以为了实现相似的成果, 先对周围各 padding 一个网格的数据.
留神这里抉择应用反复模式 (replicate) 以实现最终的边界反复值的成果.

import torch.nn.functional as F

pad_x = F.pad(x, pad=[1, 1, 1, 1], mode="replicate")  # 这里须要借助 padding 来保留边界的数据

接下来开始解决, 沿着四个方向各偏移一个单位的长度:

roll_shift = torch.cat(
    [torch.roll(pad_x[:, c * 2 : (c + 1) * 2, ...], shifts=(shift_h, shift_w), dims=(2, 3))
        for c, (shift_h, shift_w) in enumerate([(0, 1), (0, -1), (1, 0), (-1, 0)])
    ],
    dim=1,
)

'''
tensor([[[[0., 0., 0., 0., 0., 0., 0.],
          [0., 0., 0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4., 4., 4.],
          [4., 4., 4., 4., 4., 4., 4.]],

         [[4., 0., 0., 1., 2., 3., 4.],
          [4., 0., 0., 1., 2., 3., 4.],
          [4., 0., 0., 1., 2., 3., 4.],
          [4., 0., 0., 1., 2., 3., 4.],
          [4., 0., 0., 1., 2., 3., 4.],
          [4., 0., 0., 1., 2., 3., 4.],
          [4., 0., 0., 1., 2., 3., 4.]],

         [[0., 0., 0., 0., 0., 0., 0.],
          [0., 0., 0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4., 4., 4.],
          [4., 4., 4., 4., 4., 4., 4.]],

         [[0., 1., 2., 3., 4., 4., 0.],
          [0., 1., 2., 3., 4., 4., 0.],
          [0., 1., 2., 3., 4., 4., 0.],
          [0., 1., 2., 3., 4., 4., 0.],
          [0., 1., 2., 3., 4., 4., 0.],
          [0., 1., 2., 3., 4., 4., 0.],
          [0., 1., 2., 3., 4., 4., 0.]],

         [[4., 4., 4., 4., 4., 4., 4.],
          [0., 0., 0., 0., 0., 0., 0.],
          [0., 0., 0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4., 4., 4.]],

         [[0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.]],

         [[0., 0., 0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4., 4., 4.],
          [4., 4., 4., 4., 4., 4., 4.],
          [0., 0., 0., 0., 0., 0., 0.]],

         [[0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.]]]])
'''

接下来只须要剪裁一下即可:

roll_shift = roll_shift[..., 1:6, 1:6]
print(roll_shift)

'''
tensor([[[[0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4.]],

         [[0., 0., 1., 2., 3.],
          [0., 0., 1., 2., 3.],
          [0., 0., 1., 2., 3.],
          [0., 0., 1., 2., 3.],
          [0., 0., 1., 2., 3.]],

         [[0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4.]],

         [[1., 2., 3., 4., 4.],
          [1., 2., 3., 4., 4.],
          [1., 2., 3., 4., 4.],
          [1., 2., 3., 4., 4.],
          [1., 2., 3., 4., 4.]],

         [[0., 0., 0., 0., 0.],
          [0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.]],

         [[0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.]],

         [[1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4.],
          [4., 4., 4., 4., 4.]],

         [[0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.]]]])
'''

办法 3: 1×1 Deformable Convolution—— ops.deform_conv2d

在浏览 Cycle FC 的过程中, 理解到了 Deformable Convolution 在实现空间偏移操作上的妙用.
因为 torchvision 最新版曾经集成了这一操作, 所以咱们只须要导入函数即可:

from torchvision.ops import deform_conv2d

为了应用它实现空间偏移, 我在对 Cycle FC 的解读中, 对相干代码增加了一些正文信息:

要想了解这一函数的操作, 须要首先了解前面应用的 deform_conv2d_tv 的具体用法.

具体可见:https://pytorch.org/vision/0….
这里对于 offset 参数的要求是:

offset (Tensor[batch_size, 2 offset_groups kernel_height * kernel_width, out_height, out_width])

offsets to be applied for each position in the convolution kernel.

也就是说, 对于样本 s 的输入特色图的通道 c 中的地位 (x, y) , 这个函数会从 offset 中取出, 形态为 kernel_height*kernel_width 的卷积核所对应的偏移参数, 其为 offset[s, 0:2*offset_groups*kernel_height*kernel_width, x, y] . 也就是这一系列参数都是对应样本 s 的单个地位 (x, y) 的.

针对不同的地位能够有不同的 offset , 也能够有雷同的 (上面的实现就是后者).

对于这 2*offset_groups*kernel_height*kernel_width 个数, 波及到对于输出特色通道的分组.

将其分成 offset_groups 组, 每份独自领有一组对应于卷积核核心地位的绝对偏移量, 共 2*kernel_height*kernel_width 个数.

对于每个核参数, 应用两个量来形容偏移, 即 h 方向和 w 方向绝对核心地位的偏移, 即对应于前面代码中的减去 kernel_height//2 或者 kernel_width//2 .

须要留神的是, 当偏移地位位于 padding 后的 tensor 的边界之外, 则是将网格应用 0 补齐. 如果网格上有边界值, 则应用边界值和用 0 补齐的网格顶点来计算双线性插值的后果.

该策略须要咱们去结构特定的绝对偏移值 offset 来对 1 ×1 卷积核在不同通道的采样地位进行调整.

咱们先结构咱们须要的 offset $\Delta \in \mathbb{R}^{1 \times 2C_iK_hK_w \times 1 \times 1}$. 这里之所以将 out_height & out_width 两个维度设置为 1, 是因为咱们对整个空间的偏移是统一的, 所以只须要简略的反复数值即可.

offset = torch.empty(1, 2 * 8 * 1 * 1, 1, 1)
for c, (rel_offset_h, rel_offset_w) in enumerate([(0, -1), (0, -1), (0, 1), (0, 1), (-1, 0), (-1, 0), (1, 0), (1, 0)]):
    offset[0, c * 2 + 0, 0, 0] = rel_offset_h
    offset[0, c * 2 + 1, 0, 0] = rel_offset_w
offset = offset.repeat(1, 1, 7, 7).float()  # 针对空间偏移反复偏移量

在结构 offset 的时候, 咱们要明确, 其通道中的数据都是两两一组的, 每一组蕴含着沿着 H 轴和 W 轴的绝对偏移量 (这一绝对偏移量应该是以其作用的卷积权重地位为核心 —— 这一论断我并没有验证, 只是集体的推理, 因为这样可能在源码中实现起来更加不便, 能够间接作用权重对应地位的坐标. 在不读源码的前提下了解函数的性能, 那就须要自行结构数据来验证性的了解了).

为了更好的了解 offset 的作用的原理, 咱们能够设想对于采样地位 $(h, w)$, 应用绝对偏移量 $(\delta_h, \delta_w)$ 作用后, 采样地位变成了 $(h+\delta_h, w+\delta_w)$. 即原来作用于 $(h, w)$ 的权重, 偏移后间接作用到了地位 $(h+\delta_h, w+\delta_w)$ 上.

对于咱们的后面形容的沿着四个轴向各自一个单位偏移, 能够通过对 $\delta_h$ 和 $\delta_w$ 别离赋予 $\{-1, 0, 1\}$ 中的值即可实现.

因为这里仅须要体现通道特定的空间偏移作用, 而并不需要 Deformable Convolution 的卷积性能, 咱们须要将卷积核设置为单位矩阵, 并转换为分组卷积对应的卷积核的模式:

weight = torch.eye(8).reshape(8, 8, 1, 1).float()
# 输出 8 通道,输入 8 通道,每个输出通道只和一个对应的输入通道有映射权值 1 

接下来将权重和偏移送入导入的函数中.
因为该函数对于偏移超出边界的地位是应用 0 补齐的网格计算的, 所以为了实现后面边界上的反复值的成果, 这里同样须要应用反复模式下的 padding 后的输出.
并对后果进行一下修剪:

deconv_shift = deform_conv2d(pad_x, offset=offset, weight=weight)
deconv_shift = deconv_shift[..., 1:6, 1:6]
print(deconv_shift)

'''
tensor([[[[0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4.]],

         [[0., 0., 1., 2., 3.],
          [0., 0., 1., 2., 3.],
          [0., 0., 1., 2., 3.],
          [0., 0., 1., 2., 3.],
          [0., 0., 1., 2., 3.]],

         [[0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4.]],

         [[1., 2., 3., 4., 4.],
          [1., 2., 3., 4., 4.],
          [1., 2., 3., 4., 4.],
          [1., 2., 3., 4., 4.],
          [1., 2., 3., 4., 4.]],

         [[0., 0., 0., 0., 0.],
          [0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.]],

         [[0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.]],

         [[1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4.],
          [4., 4., 4., 4., 4.]],

         [[0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.]]]])
'''

办法 4: 3×3 Depthwise Convolution—— F.conv2d

在 S2MLP 中提到了空间偏移操作能够通过应用非凡结构的 3 ×3 Depthwise Convolution 来实现.
因为基于 3 ×3 卷积操作, 所以为了实现边界值的反复成果依然须要对输出进行反复 padding.
首先结构对应四个方向的卷积核:

k1 = torch.FloatTensor([[0, 0, 0], [1, 0, 0], [0, 0, 0]]).reshape(1, 1, 3, 3)
k2 = torch.FloatTensor([[0, 0, 0], [0, 0, 1], [0, 0, 0]]).reshape(1, 1, 3, 3)
k3 = torch.FloatTensor([[0, 1, 0], [0, 0, 0], [0, 0, 0]]).reshape(1, 1, 3, 3)
k4 = torch.FloatTensor([[0, 0, 0], [0, 0, 0], [0, 1, 0]]).reshape(1, 1, 3, 3)
weight = torch.cat([k1, k1, k2, k2, k3, k3, k4, k4], dim=0)  # 每个输入通道对应一个输出通道

接下来将卷积核和数据送入 F.conv2d 中计算即可, 输出在四边各 padding 了 1 个单位, 所以输入形态不变:

conv_shift = F.conv2d(pad_x, weight=weight, groups=8)
print(conv_shift)

'''
tensor([[[[0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4.]],

         [[0., 0., 1., 2., 3.],
          [0., 0., 1., 2., 3.],
          [0., 0., 1., 2., 3.],
          [0., 0., 1., 2., 3.],
          [0., 0., 1., 2., 3.]],

         [[0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4.]],

         [[1., 2., 3., 4., 4.],
          [1., 2., 3., 4., 4.],
          [1., 2., 3., 4., 4.],
          [1., 2., 3., 4., 4.],
          [1., 2., 3., 4., 4.]],

         [[0., 0., 0., 0., 0.],
          [0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.]],

         [[0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.]],

         [[1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4.],
          [4., 4., 4., 4., 4.]],

         [[0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.]]]])
'''

办法 5: 网格采样—— F.grid_sample

最初这里提到的基于 F.grid_sample , 该操作是 pytorch 提供的用于构建 STN 的一个函数, 然而其在光流预测工作以及最近的一些宰割工作中开始呈现:

  • AlignSeg: Feature-Aligned Segmentation Networks
  • Semantic Flow for Fast and Accurate Scene Parsing

针对 4Dtensor, 其次要作用就是依据给定的网格采样图 grid$\Gamma = \mathbb{R}^{B \times H_o \times W_o \times 2}$ 来对数据点 $(\gamma_h, \gamma_w)$ 进行采样以搁置到输入的地位 $(h, w)$ 中.
要留神的是, 该函数对限度了采样图 grid 的取值范畴是对输出的尺寸归一化后的后果, 并且 $\Gamma$ 的最初一维度别离是在索引 W 轴、H 轴. 即对于输出 tensor 的布局 B, C, H, W 的四个维度从后往前索引. 实际上, 这一规定在 pytorch 的其余函数的设计中宽泛遵循. 例如 pytorch 中的 pad 函数的规定也是一样的.
首先依据需要结构基于输出数据的原始坐标数组 (左上角为 $(h_{coord}[0, 0], w_{coord}[0, 0])$, 右上角为 $(h_{coord}[0, 5], w_{coord}[0, 5])$):

h_coord, w_coord = torch.meshgrid(torch.arange(5), torch.arange(5))
print(h_coord)
print(w_coord)
h_coord = h_coord.reshape(1, 5, 5, 1)
w_coord = w_coord.reshape(1, 5, 5, 1)

'''
tensor([[0, 0, 0, 0, 0],
        [1, 1, 1, 1, 1],
        [2, 2, 2, 2, 2],
        [3, 3, 3, 3, 3],
        [4, 4, 4, 4, 4]])
tensor([[0, 1, 2, 3, 4],
        [0, 1, 2, 3, 4],
        [0, 1, 2, 3, 4],
        [0, 1, 2, 3, 4],
        [0, 1, 2, 3, 4]])
'''

针对每一个输入 $\tilde{x}$, 计算对应的输出 $x$ 的的坐标 (即采样地位):

            torch.cat(
                [  # 请留神这里的重叠程序,先放靠后的轴的坐标
                    2 * torch.clamp(w_coord + w, 0, 4) / (5 - 1) - 1,
                    2 * torch.clamp(h_coord + h, 0, 4) / (5 - 1) - 1,
                ],
                dim=-1,
            )

这里的参数 $w\&h$ 示意基于原始坐标系的偏移量.
因为这里间接应用 clamp 限度了采样区间, 凑近边界的局部会重复使用, 所以后续间接应用原始的输出即可.
将新坐标送入函数的时候, 须要将其转换为 $[-1, 1]$ 范畴内的值, 即针对输出的形态 W 和 H 进行归一化计算.

        F.grid_sample(
            x,
            torch.cat(
                [2 * torch.clamp(w_coord + w, 0, 4) / (5 - 1) - 1,
                    2 * torch.clamp(h_coord + h, 0, 4) / (5 - 1) - 1,
                ],
                dim=-1,
            ),
            mode="bilinear",
            align_corners=True,
        )

要留神, 这里应用的是 align_corners=True , 对于 pytorch 中该参数的介绍能够查看 https://www.yuque.com/lart/id….
True :

False :

所以能够看到, 这里前者更合乎咱们的需要, 因为这里提到的波及双线性插值的算法 (例如后面的 Deformable Convolution) 的实现都是将像素放到网格顶点上的 (依照这一思路了解比拟合乎试验景象, 我就权且这样形容).

grid_sampled_shift = torch.cat(
    [
        F.grid_sample(
            x,
            torch.cat(
                [2 * torch.clamp(w_coord + w, 0, 4) / (5 - 1) - 1,
                    2 * torch.clamp(h_coord + h, 0, 4) / (5 - 1) - 1,
                ],
                dim=-1,
            ),
            mode="bilinear",
            align_corners=True,
        )
        for x, (h, w) in zip(x.chunk(4, dim=1), [(0, -1), (0, 1), (-1, 0), (1, 0)])
    ],
    dim=1,
)
print(grid_sampled_shift)

'''
tensor([[[[0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4.]],

         [[0., 0., 1., 2., 3.],
          [0., 0., 1., 2., 3.],
          [0., 0., 1., 2., 3.],
          [0., 0., 1., 2., 3.],
          [0., 0., 1., 2., 3.]],

         [[0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4.]],

         [[1., 2., 3., 4., 4.],
          [1., 2., 3., 4., 4.],
          [1., 2., 3., 4., 4.],
          [1., 2., 3., 4., 4.],
          [1., 2., 3., 4., 4.]],

         [[0., 0., 0., 0., 0.],
          [0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.]],

         [[0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.]],

         [[1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4.],
          [4., 4., 4., 4., 4.]],

         [[0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.],
          [0., 1., 2., 3., 4.]]]])
'''

另外的一些思考

对于 F.grid_sample 的误差问题

因为 F.grid_sample 波及到归一化操作, 自然而然存在精度损失.
所以实际上如果想要实现准确管制的话, 不太倡议应用这个办法.
如果地位恰好在但单元格角点上, 倒是能够应用最近邻插值的模式来取得一个更加参差的后果.
上面是一个例子:

h_coord, w_coord = torch.meshgrid(torch.arange(7), torch.arange(7))
h_coord = h_coord.reshape(1, 7, 7, 1)
w_coord = w_coord.reshape(1, 7, 7, 1)
grid = torch.cat(
    [2 * torch.clamp(w_coord, 0, 6) / (7 - 1) - 1,
        2 * torch.clamp(h_coord, 0, 6) / (7 - 1) - 1,
    ],
    dim=-1,
)
print(grid)
print(pad_x[:, :2])

print("mode=bilinear\n", F.grid_sample(pad_x[:, :2], grid, mode="bilinear", align_corners=True))
print("mode=nearest\n", F.grid_sample(pad_x[:, :2], grid, mode="nearest", align_corners=True))

'''
tensor([[[[-1.0000, -1.0000],
          [-0.6667, -1.0000],
          [-0.3333, -1.0000],
          [0.0000, -1.0000],
          [0.3333, -1.0000],
          [0.6667, -1.0000],
          [1.0000, -1.0000]],

         [[-1.0000, -0.6667],
          [-0.6667, -0.6667],
          [-0.3333, -0.6667],
          [0.0000, -0.6667],
          [0.3333, -0.6667],
          [0.6667, -0.6667],
          [1.0000, -0.6667]],

         [[-1.0000, -0.3333],
          [-0.6667, -0.3333],
          [-0.3333, -0.3333],
          [0.0000, -0.3333],
          [0.3333, -0.3333],
          [0.6667, -0.3333],
          [1.0000, -0.3333]],

         [[-1.0000,  0.0000],
          [-0.6667,  0.0000],
          [-0.3333,  0.0000],
          [0.0000,  0.0000],
          [0.3333,  0.0000],
          [0.6667,  0.0000],
          [1.0000,  0.0000]],

         [[-1.0000,  0.3333],
          [-0.6667,  0.3333],
          [-0.3333,  0.3333],
          [0.0000,  0.3333],
          [0.3333,  0.3333],
          [0.6667,  0.3333],
          [1.0000,  0.3333]],

         [[-1.0000,  0.6667],
          [-0.6667,  0.6667],
          [-0.3333,  0.6667],
          [0.0000,  0.6667],
          [0.3333,  0.6667],
          [0.6667,  0.6667],
          [1.0000,  0.6667]],

         [[-1.0000,  1.0000],
          [-0.6667,  1.0000],
          [-0.3333,  1.0000],
          [0.0000,  1.0000],
          [0.3333,  1.0000],
          [0.6667,  1.0000],
          [1.0000,  1.0000]]]])
tensor([[[[0., 0., 0., 0., 0., 0., 0.],
          [0., 0., 0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4., 4., 4.],
          [4., 4., 4., 4., 4., 4., 4.]],

         [[0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.]]]])
mode=bilinear
 tensor([[[[0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00,
           0.0000e+00, 0.0000e+00],
          [1.1921e-07, 1.1921e-07, 1.1921e-07, 1.1921e-07, 1.1921e-07,
           1.1921e-07, 1.1921e-07],
          [1.0000e+00, 1.0000e+00, 1.0000e+00, 1.0000e+00, 1.0000e+00,
           1.0000e+00, 1.0000e+00],
          [2.0000e+00, 2.0000e+00, 2.0000e+00, 2.0000e+00, 2.0000e+00,
           2.0000e+00, 2.0000e+00],
          [3.0000e+00, 3.0000e+00, 3.0000e+00, 3.0000e+00, 3.0000e+00,
           3.0000e+00, 3.0000e+00],
          [4.0000e+00, 4.0000e+00, 4.0000e+00, 4.0000e+00, 4.0000e+00,
           4.0000e+00, 4.0000e+00],
          [4.0000e+00, 4.0000e+00, 4.0000e+00, 4.0000e+00, 4.0000e+00,
           4.0000e+00, 4.0000e+00]],

         [[0.0000e+00, 1.1921e-07, 1.0000e+00, 2.0000e+00, 3.0000e+00,
           4.0000e+00, 4.0000e+00],
          [0.0000e+00, 1.1921e-07, 1.0000e+00, 2.0000e+00, 3.0000e+00,
           4.0000e+00, 4.0000e+00],
          [0.0000e+00, 1.1921e-07, 1.0000e+00, 2.0000e+00, 3.0000e+00,
           4.0000e+00, 4.0000e+00],
          [0.0000e+00, 1.1921e-07, 1.0000e+00, 2.0000e+00, 3.0000e+00,
           4.0000e+00, 4.0000e+00],
          [0.0000e+00, 1.1921e-07, 1.0000e+00, 2.0000e+00, 3.0000e+00,
           4.0000e+00, 4.0000e+00],
          [0.0000e+00, 1.1921e-07, 1.0000e+00, 2.0000e+00, 3.0000e+00,
           4.0000e+00, 4.0000e+00],
          [0.0000e+00, 1.1921e-07, 1.0000e+00, 2.0000e+00, 3.0000e+00,
           4.0000e+00, 4.0000e+00]]]])
mode=nearest
 tensor([[[[0., 0., 0., 0., 0., 0., 0.],
          [0., 0., 0., 0., 0., 0., 0.],
          [1., 1., 1., 1., 1., 1., 1.],
          [2., 2., 2., 2., 2., 2., 2.],
          [3., 3., 3., 3., 3., 3., 3.],
          [4., 4., 4., 4., 4., 4., 4.],
          [4., 4., 4., 4., 4., 4., 4.]],

         [[0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.],
          [0., 0., 1., 2., 3., 4., 4.]]]])
'''

F.grid_sample 与 Deformable Convolution 的关系

尽管二者都实现了对于输出与输入地位映射关系的调整, 然而二者调整的形式有着显著的差异.

  • 参考坐标系不同

    • 前者的坐标系是基于整体输出的一个归一化坐标系, 原点为输出的 HW 立体的核心地位, H 轴和 W 轴别离以向下和向右为正向. 而在坐标系 WOH 中, 输出数据的左上角为 $(-1, -1)$, 右上角为 $(1, -1)$.
    • 后者的坐标系是绝对于权重初始作用地位的绝对坐标系. 然而实际上, 这里其实了解为 沿着 H 轴和 W 轴的_绝对偏移量_更为适合. 例如, 将权重作用地位向左偏移一个单位, 实际上让其对应的偏移参数组 $(\delta_h, \delta_w)$ 取值为 $(0, -1)$ 即可, 行将作用地位绝对于原始作用地位的 $w$ 坐标加上个 $-1$.
  • 作用成果不同

    • 前者间接对整体输出进行坐标调整, 对于输出的所有通道具备雷同的调整成果.
    • 后者因为构建于卷积操作之上, 所以能够更加不便的解决不同通道(offset_groups )、不同的实际上可能有重叠的部分区域(kernel_height * kernel_width ). 所以理论性能更加灵便和可调整.

Shift 操作的第二春

尽管在之前的工作中曾经摸索了多种空间 shift 操作的模式, 然而却并没有引起太多的关注.

  • (CVPR 2018) [Grouped Shift] Shift: A Zero FLOP, Zero Parameter Alternative to Spatial Convolutions:
  • (ICCV 2019) 4-Connected Shift Residual Networks
  • (NIPS 2018) [Active Shift] Constructing Fast Network through Deconstruction of Convolution
  • (CVPR 2019) [Sparse Shift] All You Need Is a Few Shifts: Designing Efficient Convolutional Neural Networks for Image Classification

这些工作大多专一于轻量化网络的设计, 而当初的这些基于 shift 的办法, 则联合了 MLP 这一快船, 如同又激发了一些新的水花.
以后的这些办法, 往往会采纳更无效的训练设定, 这些模型之外的策略在肯定水平上也极大的晋升了模型的体现. 这其实也会让人纳闷, 如果间接迁徙之前的那些 shift 操作到这里的 MLP 框架中, 或者性能也不会差吧?

这一想法其实也实用于传统的 CNN 办法, 之前的那些构造如果应用雷同的训练策略, 相比当初, 到底能差多少? 这预计只能那些有卡有工夫有急躁的大佬们可能一探到底了.

实际上综合来看, 现有的这些基于空间偏移的 MLP 的办法, 更能够看作是 [(NIPS 2018) [Active Shift] Constructing Fast Network through Deconstruction of Convolution](https://www.yuque.com/lart/ar…) 这篇工作的特化版本.

也就是将本来这篇工作中的自适应学习的偏移参数改成了固定的偏移参数.

正文完
 0