RetinaNet的Anchor生成策略
全卷积网络的动态anchor boxes.
封面图片:Jed Dela Cruz on Unsplash
Keras官方提供了一份RetianNet示例代码,地址如下:
这份代码与文章配合使用可以获得最佳的理解效果。本篇着重根据代码分析其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个参数:
aspect_ratio
: anchor的宽高比
三个比例:0.5、1.0、2,对应高、方、扁。scales
: anchor的缩放比例
同样是三个值,换算成浮点数为1,、1.26、1.59,都是大于1的数。_num_anchors
: anchor的数量
这里是指特征图上每个像素所对应的anchor数量。很明显是长宽比与缩放比数量的乘积,本例中为3×3=9。_strides
: 采样间隔
隔多远采样一次。当前值为[8, 16, 32, 64, 128]。其实对应了特征图与原始图像的尺寸比例。_areas
: anchor基础面积
生成不同形状anchor的依据。目前是边长为32、64、128、256与512像素的正方形面积。_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
其逻辑大致为:
- 对于每个预定义anchor面积,根据宽高比获得与当前面积对应的anchor的宽度与高度。由于宽高比有三个数值,因此获得3个尺寸的anchor boxes。
- 将上一步获得的anchor boxes与三个不同的anchor缩放比例相乘,获得9个不同尺度的anchor boxes。
- 预定义的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的原理。敬请期待!
Comments ()