RetinaNet的Anchor生成策略

全卷积网络的动态anchor boxes.

RetinaNet的Anchor生成策略

封面图片:Jed Dela Cruz on Unsplash

Keras官方提供了一份RetianNet示例代码,地址如下:

keras-team/keras-io
Keras documentation, hosted live at keras.io. Contribute to keras-team/keras-io development by creating an account on GitHub.

这份代码与文章配合使用可以获得最佳的理解效果。本篇着重根据代码分析其Anchor生成策略。

与生成有关的参数

Anchor类的初始化函数如下:

def __init__(self):
    self.aspect_ratios = [0.5, 1.0, 2.0]
    self.scales = [2 ** x for x in [0, 1 / 3, 2 / 3]]

    self._num_anchors = len(self.aspect_ratios) * len(self.scales)
    self._strides = [2 ** i for i in range(3, 8)]
    self._areas = [x ** 2 for x in [32.0, 64.0, 128.0, 256.0, 512.0]]
    self._anchor_dims = self._compute_dims()

其中一共涉及到了6个参数:

  1. aspect_ratio: anchor的宽高比
    三个比例:0.5、1.0、2,对应高、方、扁。
  2. scales: anchor的缩放比例
    同样是三个值,换算成浮点数为1,、1.26、1.59,都是大于1的数。
  3. _num_anchors: anchor的数量
    这里是指特征图上每个像素所对应的anchor数量。很明显是长宽比与缩放比数量的乘积,本例中为3×3=9。
  4. _strides: 采样间隔
    隔多远采样一次。当前值为[8, 16, 32, 64, 128]。其实对应了特征图与原始图像的尺寸比例。
  5. _areas: anchor基础面积
    生成不同形状anchor的依据。目前是边长为32、64、128、256与512像素的正方形面积。
  6. _anchor_dims: anchor的尺寸
    是由类方法 _compute_dims 来计算得到的。

类方法

除了 __init__ 外,整个Anchor类实现了3个方法。

计算anchor尺寸

方法 _compute_dims 计算所有特征金字塔层对应的anchor维度。代码如下:

def _compute_dims(self):
    """Computes anchor box dimensions for all ratios and scales at all levels
    of the feature pyramid.
    """
    anchor_dims_all = []
    for area in self._areas:
        anchor_dims = []
        for ratio in self.aspect_ratios:
            anchor_height = tf.math.sqrt(area / ratio)
            anchor_width = area / anchor_height
            dims = tf.reshape(
                tf.stack([anchor_width, anchor_height], axis=-1), [1, 1, 2]
            )
            for scale in self.scales:
                anchor_dims.append(scale * dims)
        anchor_dims_all.append(tf.stack(anchor_dims, axis=-2))
    return anchor_dims_all

其逻辑大致为:

  1. 对于每个预定义anchor面积,根据宽高比获得与当前面积对应的anchor的宽度与高度。由于宽高比有三个数值,因此获得3个尺寸的anchor boxes。
  2. 将上一步获得的anchor boxes与三个不同的anchor缩放比例相乘,获得9个不同尺度的anchor boxes。
  3. 预定义的anchor面积数量为5,因此最终得到一个长度为5的list。每个list成员为与该面积对应的9个anchor boxe,形状为(1, 1, 9, 2)。

该函数在初始化时被调用,结果赋值给类属性 _anchor_dims

生成单张特征图的anchor

对应方法 _get_anchors 。该方法需要三个参数:特征图高度、宽度以及层级。代码如下:

def _get_anchors(self, feature_height, feature_width, level):
    """Generates anchor boxes for a given feature map size and level

    Arguments:
      feature_height: An integer representing the height of the feature map.
      feature_width: An integer representing the width of the feature map.
      level: An integer representing the level of the feature map in the
        feature pyramid.

    Returns:
      anchor boxes with the shape
      `(feature_height * feature_width * num_anchors, 4)`
    """
    rx = tf.range(feature_width, dtype=tf.float32) + 0.5
    ry = tf.range(feature_height, dtype=tf.float32) + 0.5
    centers = tf.stack(tf.meshgrid(rx, ry), axis=-1) * \
        self._strides[level - 3]
    centers = tf.expand_dims(centers, axis=-2)
    centers = tf.tile(centers, [1, 1, self._num_anchors, 1])
    dims = tf.tile(
        self._anchor_dims[level - 3], [feature_height, feature_width, 1, 1]
    )
    anchors = tf.concat([centers, dims], axis=-1)
    return tf.reshape(
        anchors, [feature_height * feature_width * self._num_anchors, 4]
    )

该函数首先使用 tf.range 生成与特征图尺度对应的网格点x与y坐标。之后使用 tf.meshgrid 将x、y组装成网格点。注意这时候 _strides 参数发挥作用了。组装后的网格点需要与当前层级对应的stride值相乘,以便与真实的图像尺寸相匹配。需要注意层级的总数为5,但是起始值为3,这是leve-3的原因。到这一步获得的只是一类anchor的中心点,而每个特征图对应9类anchor,所以需要使用 tf.tile 函数将其扩增,为每一类anchor生成中心网格点。同理,也要为特征图上的每个像素生成anchor的宽度与高度。之后,将中心点[cx, cy] 与 尺寸 [anchor_width, anchor_height] 拼接为anchor的标准形式并保证形状,便得到了与该层特征图对应的anchor boxes。

生成全部anchor

方法 get_anchors 需要参数为图像高度与宽度。代码如下:

def get_anchors(self, image_height, image_width):
    """Generates anchor boxes for all the feature maps of the feature pyramid.

    Arguments:
      image_height: Height of the input image.
      image_width: Width of the input image.

    Returns:
      anchor boxes for all the feature maps, stacked as a single tensor
        with shape `(total_anchors, 4)`
    """
    anchors = [
        self._get_anchors(
            tf.math.ceil(image_height / 2 ** i),
            tf.math.ceil(image_width / 2 ** i),
            i,
        )
        for i in range(3, 8)
    ]
    return tf.concat(anchors, axis=0)

这段代码就简单多了。在已知图像宽度与高度的前提下,计算出每一张特征图的尺寸,并与该特征图的层级一起传入 _get_anchors 方法得到对应anchor。最后堆叠所有anchor即可。

Anchor的规律

从anchor生成的过程可以归纳出一些规律。

Anchor的数量取决于输入图像的分辨率

在生成每一层的anchor时,其数量取决于特征图的大小。而5张特征图的大小依次是输入图像的 1/8、1/16、1/32、1/64、1/128。所以输入图像越大,anchor的数量越多。另外在 anchor 模块中如果图像尺寸无法被整除,则向上取整。实际训练中生成训练数据的代码对原始图像做了padding。

Anchor大小与特征层级数正相关

特征层层级越高,anchor的尺寸越大。每一层中anchor最小面积为 areas 中定义的基础面积,最大面积为基础面积的 max(scales) 倍,代码中为1.59倍。

总结

RetinaNet是一只全卷积神经网络,可以接受可变大小的输入。其anchor数量取决于特征图的尺寸,继而取决于输入图像。Anchor生成的逻辑与特征图的生成逻辑关联,也就是说FPN的设计会影响到anchor。在下一篇文章中,我会继续解读FPN的原理。敬请期待!