终于把yolo v3框架写好了。支持多模型、多数据集、任意输出层数量、任意anchor数量、模型剪枝还适配k210.不要太好用~

这里记录一下我之前的实现的问题出在哪里。

错误地计算了ignore mask

yolo v2开始就会计算正确box与预测box直接iou关系,如果iou score大于阈值,那么说明这个预测box是成功预测到了这个对象的,极大的提高了模型的recall。 但是我在开源的yolo v2中使用Boolean mask函数时忽略了一点,比如batch为16,那么输出label的尺寸为\([16,7,10,3,25]\),直接使用Boolean mask会得到正确box尺寸为\([?,4]\)。然后我把这个\([?,4]\)与预测出来的box\([16,7,10,3,4]\)计算iou score

乍一看还以为没什么毛病,其实这里最大的毛病就是整个batch的正确box都与整个batch的预测box都做了iou score,如果这时候计算最优iou score,很有可能这个最优的预测box不属于这张图片!数据直接出现了混合,这就是根源问题。

在新的写的代码中,我用了map的方式处理每张图片,既提高了效率,又避免了错误。

题外话一句。。我现在都惊讶我之前的yolo v2为啥效果还行,很有可能误打误撞搞了个数据集mix的效果。。。

更新: 通过对比腾讯优图所开源的yolo3的代码,我发现这个ignore mask不但需要每张图像单独计算,还需要单一输出层与全局的目标进行计算,因为我用的是tf.keras,所以没办法在不使用hack的方式下传入整张图像的bbox数组,所以我在label中多加了一维,标记全局的对象位置.

以下代码为我目前的标签制作代码:

  • 避免了inf
  • 避免了对象重叠(原版yolo也没有考虑到这一点)
  • 添加了全局的对象标记.

这些问题消除之后,我的yolo所计算出的loss与腾讯优图所开源的yolo完全一致.终于完美复现出yolo的效果了~

labels = [np.zeros((self.out_hw[i][0], self.out_hw[i][1], len(self.anchors[i]),
5 + self.class_num + 1), dtype='float32') for i in range(self.output_number)]

layer_idx, anchor_idx = self._get_anchor_index(ann[:, 3:5])
for box, l, n in zip(ann, layer_idx, anchor_idx):
# NOTE box [x y w h] are relative to the size of the entire image [0~1]
# clip box avoid width or heigh == 0 ====> loss = inf
bb = np.clip(box[1:5], 1e-8, 0.99999999)
cnt = np.zeros(self.output_number, np.bool) # assigned flag
for i in range(len(l)):
x, y = self._xy_grid_index(bb[0:2], l[i]) # [x index , y index]
if cnt[l[i]] or labels[l[i]][y, x, n[i], 4] == 1.:
# 1. when this output layer already have ground truth, skip
# 2. when this grid already being assigned, skip
continue
labels[l[i]][y, x, n[i], 0:4] = bb
labels[l[i]][y, x, n[i], 4] = (0. if cnt.any() else 1.)
labels[l[i]][y, x, n[i], 5 + int(box[0])] = 1.
labels[l[i]][y, x, n[i], -1] = 1. # set gt flag = 1
cnt[l[i]] = True # output layer ground truth flag
if cnt.all():
# when all output layer have ground truth, exit
break

anchor的尺度

前面我有个文章也写了,anchor的作用就是让预测wh与真实wh直接的比例接近与1,那么细细想来,anchor的尺度是对应图片尺度\([224,320]\)还是对应栅格的尺度,还是对应全局的0-1都没有什么关系,只不过anchor的尺度就代表做标签的时候label要转换的尺度。所以为了方便起见,直接把anchor尺度设置为全局的0-1就完事了,还减少运算量。

loss出现NaN

问题原因在于图片标签的widthheight出现了0,导致log(0)=-inf的问题. 解决起来很简单,在制作标签的时候限制widthheight范围即可.

label中的极端情况的考虑

bbox到达边界值

bbox的中心点位于边界值最大值时,如下图所示. \[\begin{aligned} index&=floor(x*w) \\ \because w&=3,x=1 \Rightarrow floor(1*3)=3 \end{aligned} \] 但使用3进行索引就会报错,所以我们需要限制一下bbox的中心坐标不能大于等于\(1\).

+-------+-------+-------+
|       |       |       |
|       |       |       |
|       |       |  +---------+
+-------+-------+--|----+    |
|       |       |  |    |    |
|       |       |  |  center |
|       |       |  |    |    |
+-------+-------+--|----+    |
|       |       |  +---------+
|       |       |       |
|       |       |       |
+-------+-------+-------+
            

当两个目标的label相同时

如下图所示,当两个bbox真的非常靠近时,就会出现他们的label所在的位置都是相同的,就会出现label被覆盖的问题了.目前我将相同label时,后面的label分配给次优的anchor.

+---------------+-------+
|  +---------+  |       |
| +|--------+|  |       |
| ||    |   ||  |       |
+-||--------||----------+
| ||    |   ||  |       |
| ||    |   ||  |       |
| ||    |   ||  |       |
+-|+---------+----------+
| +---------+   |       |
|       |       |       |
|       |       |       |
+-------+-------+-------+

数据增强

数据增强我使用gluoncv的方式,首先是图像cropresize,使用的是ssd所提出的带iou约束的crop方式,resize之后结合imgaug库进行数据增强,效果不错。如果可以再进一步,可以使用谷歌提出的autoaugment策略。我这里暂时还没用mixupgluoncv里面应该是有使用的。

IOULoss

推荐使用ciou loss,我测试之后map提高了4个点,效果相当不错。几个iou loss的实现方式我总结在这里