2017 Deformable ConvNets V1介绍及分析(包括代码)

最近Deformable Conv V2比较火, Deformable Conv的概念是MSRA的戴季峰等人提出来的, 挺不错的一个东西, 对于有形变的物体的识别效果很好, 今天先把其前身Deformable ConvNets V1做个总结, 以便打好学习Deformable ConvNets V2的基础. 发现了一个特别好的博客 imperfect00——deformable convolution, 既有文章说明, 又辅有代码解释. 非常直观, 本博客在此基础上, 对其进行更具体的代码分析和基本思路提炼.

论文亮点:

  • Enabling effective modeling of spatial transformation in ConvNets
  • No additional supervision for learning spatial transformation
  • Significant accuracy improvements on sophisticated vision tasks

论文概述:

CNN由于固定的几何结构,导致其对几何形变的建模受到限制。为了加强CNN对形变的建模能力,戴季峰等的”deformable convolution network的”提出了deformable convolution 和 deformable RoI pooling两种网络结构单元。deformable convolution 和 deformable RoI pooling都是基于通过学习一个额外的偏移量(offset),使卷积核对输入feature map的采样的产生偏移,集中于感兴趣的目标区域。可以将deformable convolution , deformable RoI pooling加入现有的CNN中,进行端到端(end to end)训练。

在这里插入图片描述

可以看出, a)对应的是正常的卷积采样区域 (以3x3为例, 绿色表示). b)则是变形后的采样区域 (以蓝色表示), c) 和 d)是b)的特殊形式.
这表明deformable conv可以生成各种各样的变换形式(各向异性anisotropic)-----规模, 角度和旋转.

这里, b),c),d)均为deformable conv,箭头表示卷积核权重的偏移。图 c) 为可变形卷积学到了平移尺度形变,图 d)为旋转形变

deformable conv网络结构如下:
在这里插入图片描述

简单来讲: Deformable Conv的作用是: "通过一个卷积层,对输入feaure map学习偏移量offset,再通过插值(spine resize) ,得到输出feature map"

  • 标准卷积:
    以3×3卷积核为例,首先生成卷积核采样网格点:

  • 变形卷积:
    在这里插入图片描述
    在这里插入图片描述

代码梳理:

我梳理的代码是基于Keras和Tensorflow实现的: https://github.com/kastnerkyle/deform-conv/tree/master/deform_conv

  • ① 作者专门设计了这个ConvOffset2D的层如下(此实现要求输入的特征图 h = w):
class ConvOffset2D(Conv2D):
    """ConvOffset2D

    Convolutional layer responsible for learning the 2D offsets and output the
    deformed feature map using bilinear interpolation

    Note that this layer does not perform convolution on the deformed feature
    map. See get_deform_cnn in cnn.py for usage
    """

    def __init__(self, filters, init_normal_stddev=0.01, **kwargs):
        """Init

        Parameters
        ----------
        filters : int
            Number of channel of the input feature map
        init_normal_stddev : float
            Normal kernel initialization
        **kwargs:
            Pass to superclass. See Con2D layer in Keras
        """

        self.filters = filters
        super(ConvOffset2D, self).__init__(
            self.filters * 2, (3, 3), padding='same', use_bias=False,
            kernel_initializer=RandomNormal(0, init_normal_stddev),
            **kwargs
        )

    def call(self, x):
        """Return the deformed featured map"""
        # x_shape 应该是 b x h x w x c (TF)
        x_shape = x.get_shape()
        # 因为ConvOffset2D的父类是Conv2D, 所以这里是用一层Conv层来计算offset.
        # 这里的策略是padding='same', filters = 2*filters, 所以offsets的shape为:
        #  b x h x w x 2c (TF)    
        offsets = super(ConvOffset2D, self).call(x)

        # offsets: (b x c, h, w, 2)
        offsets = self._to_bc_h_w_2(offsets, x_shape)

        # x: (b x c, h, w)
        x = self._to_bc_h_w(x, x_shape)

        # X_offset: (b x c, h, w)
        # 对输入特征x & offset, 采用双线性插值,计算可变形卷积结果:
        x_offset = tf_batch_map_offsets(x, offsets)

        # x_offset: (b, h, w, c)
        x_offset = self._to_b_h_w_c(x_offset, x_shape)

        return x_offset

    def compute_output_shape(self, input_shape):
        """Output shape is the same as input shape
        Because this layer does only the deformation part.
        Offset层的输出输入结构还没发生变化. 2019.1.22
        """
        return input_shape

    @staticmethod
    def _to_bc_h_w_2(x, x_shape):
        """(b, h, w, 2c) -> (b*c, h, w, 2)"""
        """为什么是2c, 因为filters设置的是2c!!!"""
        x = tf.transpose(x, [0, 3, 1, 2])
        x = tf.reshape(x, (-1, int(x_shape[1]), int(x_shape[2]), 2))
        return x

    @staticmethod
    def _to_bc_h_w(x, x_shape):
        """(b, h, w, c) -> (b*c, h, w)"""
        x = tf.transpose(x, [0, 3, 1, 2])
        x = tf.reshape(x, (-1, int(x_shape[1]), int(x_shape[2])))
        return x

    @staticmethod
    def _to_b_h_w_c(x, x_shape):
        """(b*c, h, w) -> (b, h, w, c)"""
        x = tf.reshape(
            x, (-1, int(x_shape[3]), int(x_shape[1]), int(x_shape[2]))
        )
        x = tf.transpose(x, [0, 2, 3, 1])
        return x
  • ② 我们着重分析这个层的成员函数call(self, x)内的实现即可:
    1) 通过offsets = super(ConvOffset2D, self).call(x) 调用一个padding=‘same’, filters数目为传入特征图channel个数的2倍的卷积层, 得到offsets.
    2) 对输入特征图xoffsets进行一系列乱78糟的操作, 包括reshape和spine interpolation.
    3) 这里着重对经过reshape后的xoffsets进行插值得到输出的特征图结果x_offsettf_batch_map_offsets的实现进行强调, 代码如下所示, 经过tf_batch_map_offsets处理后, x_offsetb x c, h, w的3D形式, 为了保持形式一致, 最后需再通过x_offset = self._to_b_h_w_c(...)reshape成b, c, h, w的4D形式.

关于tf_batch_map_offsets中的详细剖析见额外说明部分.

def tf_batch_map_offsets(input, offsets, order=1):
    """Batch map offsets into input

    Parameters
    ---------
    input : tf.Tensor. shape = (b x c, h, w)
    offsets: tf.Tensor. shape = (b x c), h, w, 2)
    修正我的错误, 我之前一直误以为offsets的channel仍为c, 然后看到了
    self.filters * 2, 所以, 2c -> c就理所当然了~.

    Returns
    -------
    输出:
    tf.Tensor. shape = (b*c, h, w)
    """

    input_shape = tf.shape(input)
    batch_size = input_shape[0] # batch_size = b x c
    input_size = input_shape[1] # input_size = h
    
    # offsets经过整理过后, shape为: (batch_size, h*w, 2)
    offsets = tf.reshape(offsets, (batch_size, -, 2))
    # grid的shape: 2d
    grid = tf.meshgrid(
        tf.range(input_size), tf.range(input_size), indexing='ij'
    ) # meshgrid用法 https://tensorflow.google.cn/api_docs/python/tf/meshgrid
    grid = tf.stack(grid, axis=-1) # tf.stack用法 https://blog.csdn.net/loseinvain/article/details/79638183
    grid = tf.cast(grid, 'float32')
    grid = tf.reshape(grid, (-1, 2))
    # 经过tf.repeat_2d后的 grid的shape为(batch_size, input_size^2, 2)
    grid = tf_repeat_2d(grid, batch_size)
    # 这里是对h=w的情况来进行的, 
    # 所以, grid 和 offsets的shape都是: (batch_size, input_size^2, 2)
    coords = offsets + grid
    
    # scipy返回的mapped_vals 的shape: (b x c, h^2)
    # tf返回的为(b x c, h, w)
    mapped_vals = tf_batch_map_coordinates(input, coords)
    return mapped_vals

总结:

在这里插入图片描述
最后, 可以知道Deformable Conv层的作用是通过学习offset, 加之特定的规则, 从特征图的每个channel对应的H*W的2D结构中, reorgnize这个H*W的结构, 相较于常规的卷积正常传给后面的层, Deformable Conv层对H*W里面的值进行调整, 有些位置的值会多次出现, 而有些位置的值则会被丢弃. 如下:
在这里插入图片描述
原H*W的结构就相当于inputs[0]: map_coordinates(…)的结果就相当于经过了Deformabel Conv层后的结果. 显然, 0, 1, 4, 5 等值都被丢弃, 而7, 11, 15等值多次出现. 当然, Deformable Conv的理想情况是每一层的所有信息尽量不丢失的前提下, 通过offset的学习对其进行重新排列, 突出重要的区域, 增强模型对形变的识别效果.

额外说明

因为tf_batch_map_offsets的实现中用到了很多tensorflow的算子, 在这里把这些用到的算子举例进行说明:

    1. tf.meshgrid

notice: indexing='ij’是有用的, 如果不带indexing=‘ij’, 则结果为 [[[0, 1, 2, 3, 4], …, [0, 1, 2, 3, 4]], [[0, 0, 0, 0, 0], … , [4, 4, 4, 4, 4]]], 结果不一样的.

在这里插入图片描述

    1. tf.stack

可以看出, axis=-1表示从后向前对数组进行拼接. 若默认情况下,结果应为[[0, 0], [1, 0], [2, 0], [3, 0], [4, 0]] …

在这里插入图片描述

    1. tf_repeat_2d

这个算子是作者自定义的, 效果跟np.repeat一样.

def tf_repeat_2d(a, repeats):
    """Tensorflow version of np.repeat for 2D"""

    assert len(a.get_shape()) == 2
    a = tf.expand_dims(a, 0)
    a = tf.tile(a, [repeats, 1, 1])
    return a

需要说明的是: 过了tf_repeat_2d的shape为: (batchsize, h^2 , 2), 注意: 这里的batchsize实际上
是上面的b x c的结果. 以batchsize=2为例, tf_repeat_2d返回的这两个batch对应的结果是一模一样的:
在这里插入图片描述

    1. tf_batch_map_coordinates

最后, 来看看函数tf_batch_map_coordinates是怎么实现的. 因为tf版本的不容易看懂, 作者为了方便起见, 另外用scipy写了一版, 以便理解:

from scipy.ndimage.interpolation import map_coordinates as sp_map_coordinates

def sp_batch_map_coordinates(inputs, coords):
    """
        Reference implementation for batch_map_coordinates
        # 本代码只对于h = w的情况生效.
        inputs: (b x c, h, w)
        coords: (b x c, h^2, 2)    
    """
    # 将coords里的所有值都限制在(0, h^2-1)之间, 如果超出h^2-1, 则变成h^2-1, 若小于0, 则变成0. 
    coords = coords.clip(0, inputs.shape[1] - 1)
    mapped_vals = np.array([
        sp_map_coordinates(input, coord.T, mode='nearest', order=1)
        for input, coord in zip(inputs, coords)
    ])
    # mapped_vals 的shape: (b x c, h^2)
    return mapped_vals

举个例子如下图, inputs[0]就对应sp_batch_map_coordinates中的input, 同理对coords[0] <—> coord.

coords[0],也就是coord中存放的每个channel对应的H x W的矩形图的每个点的插值坐标.(通过offsets + grid重新算得的新的插值位置coord. 顺序是从左上到右下.)

显然, 对插值的坐标[7, 7], x方向超过了界限(上界为3, 下界为0, 因为是4x4的input), 所以, 返回最后一行, y方向同理, 返回最后一列, 所以对应的值是15.
在这里插入图片描述

参考资料

[1] Deformable Convolutional Networks_戴季峰官方tutorial
[2] deformable convolution
[3] scipy中map_coordinates的说明
[4] Stackoverflow上关于类似问题的提问:

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页