从 YOLOv3 浅度理解目标检测
在前段时间,我们刚理了一遍 RetinaNet 是如何实现目标检测的。不过简单从一个方法上了解目标检测全貌有失偏颇,所以接下来我们继续看另一个 anchor-based 方法 YOLOv3,从两个方法上继续片面理解目标检测。
不过 YOLOv1 呢?因为 v3 是在其上的改进,就不再单独解释 v1,下文可能会以对比的形式提及。若没有提到版本,下文所有的 YOLO 均指 YOLOv3,代码可以参考a、b。
这两份代码多少有点相似,不知道是不是我的错觉。
网络概览
YOLO 网络的主要部分是作者自己用 C 和 CUDA 实现的 Darknet-53。
YOLO 网络结构较为线性,没有 RetinaNet 那种复杂的 U 型结构。借 Darknet 框架(它真的是个框架,不是库)和简单的网络结构,YOLO 的速度可是说一骑绝尘。不过对我们来讲想要看明白网络的结构就多少有点头疼了,不但需要额外学习这个框架,而且它的配置文件格式实在是有些……
所以我现在并不打算深入观察 YOLOv3 网络细节。Darknet 的主要组成仍为 1x1 和 3x3 的带残差连接卷积层叠成的深层网络。从最高层、次高层、次次高层三个 stage 上,YOLO 拿到 3 个不同尺度上的图像特征,通过上采样后,直接前后相连来生成 3 组特征,并使用这些特征分别生成 bbox。
Anchor
YOLO 同样是一个 anchor-based 方法。其 anchor 的设置从 v1 后经过改进,现在与 RetinaNet 等方法的 anchor 格式是基本一致的,同样为 ,并且
其中 分别为图像的宽和高, 则是在特征坐标系和图像坐标系间的转换函数。有重大变化的是 的计算中多出来的 。YOLO 将图像划分成为 的网格,为不同网格设置多个 anchor。作为对比,RetinaNet 没有网格,它的 anchor 是直接设置在每个像素点上的。
虽然设计上像是额外多出了「网格」的概念,落实到网络上其实就是特征尺寸和原图间的大小比例。卷积提取特征的网络一般都遵循相同的特点,也就是多层、层数越高通道越多、尺寸逐渐减小的金字塔式结构。那么一个 大小的特征,每个像素的特征应该和原图的局部区域有一定的对应关系,也就是对应了原图里的一个格。这对 YOLO 来说是个很自然的设计。
YOLO 确定 anchor scale 和 ratio 的方法相比 RetinaNet 有些赖皮:它将数据集中所有的框拉出来聚类,取聚类结果作为 anchor。我不评价这种方法,它在方法的落地应用上一定是有意义的,实践中预先的聚类相比指望网络经 IoU 指导从零学习预测框要好很多。
另外,YOLO 对于类别置信度的预测有很大不同。首先它对每个 anchor 输出一个置信度 ,表示该框中有「任何物体」的概率,其次再输出 ,表示框中物体的类别。所以如果有 的网格和 个 anchor,网络的输出将会是 ,其中 为类别集合[1]。配合这一设计,YOLO 不会受到正负类别失衡的影响,「Focal Loss 在 YOLO 上没有意义」。
训练
build targets
RetinaNet 中与 gound truth 的 IoU 超过 0.5 的任何 anchor 都会设为 positive。 YOLO 是如何确定 anchor 与 gound truth bbox 之间的关系的呢。我们需要谨慎地理解这一部分设计,必要时我会贴对应的代码实现,里面有很多作者的实践经验。
…YOLOv3 predicts an objectness score for each bounding box using logistic regression. This should be 1 if the bounding box prior overlaps a ground truth object by more than any other bounding box prior. If the bounding box prior is not the best but does overlap a ground truth object by more than some threshold we ignore the prediction…
- YOLO 中每个 anchor 仅对应一个 ground truth,且每个 ground truth 仅对应一个最匹配()的 anchor。
- 与 anchor 「足够」重合()但因第一点而不是 positive 的 anchor 在 loss 中会被忽略。
- 剩余 anchor 才是 negative。
听着就很合理。不过看了两份代码,实现上好像不是那么回事,他们都使用了另一种策略。我们来简单看一下。
这部分代码是在生成 target 和 anchor 编号 的笛卡尔积。
1 |
|
此后就是计算 的关系了。前文我们提到,YOLO 在网络的多层上检测目标,所以此处是对所有层都计算一次。能看到这里的实现并没有按照 0.5 的阈值筛选。
1 |
|
如果去看看另一份代码,还能发现更多的东西。这份代码里甚至保留了被注释的 IoU 筛选代码,转而采取了这种边长比值的办法来筛选。
1 |
|
至少,两份代码的作者打开始就没解释这样做的原因。在浪费了大量时间踩坑之后我发现此处修改有可能是因为 IoU 的训练难度。IoU 在训练过程中容易把框给训飞了,也就是发散。像是这两份代码里使用的 GIoU、DIoU、CIoU,如果预测框没有包围真实的物体框,在训练中常见的现象是预测框先扩大去包围物体框,再逐步被 IoU 收缩框的大小。结果在预测框边长变大的时候,IoU 直接小小小,0.5 的阈值最后什么也选不到,在中途 loss 就就歇菜了。
在拿到那组 之后,剩下的就是计算 loss。
Focal Loss
说好的不用 Focal Loss 呢……能发现,两份实现都使用了 Focal Loss。算了,其实我觉得这两份代码之间应该是有什么不可言说的秘密,代码长得都快一样了。下文之后,统称「这份实现」。
这份实现里同时在 obj 和 cls 上都使用了 Focal Loss。在实践中可以发现,即使 可以不用 Focal Loss, 也一定要用什么方式来平衡一下正负类别的。Focal Loss 就是个不错的办法。
Focal Loss 已经在 RetinaNet 一文中介绍了,此处不赘述。
IoU 及其变种
IoU 如上次 RetinaNet 所说,就是下面这个东西。
IoU 本身在使用上有很多坑。最麻烦的是当两个框完全无交时 IoU 没有梯度。没有梯度就没有优化,就没得可学。当然,IoU 还有别的一些问题,比如它不太能反映预测框的真实效果。
为了弥补 IoU 在设计上的这样那样的问题,有很多其他变种被逐步提出。例如GIoU。区别在于 GIoU 除了计算交并比外,多出了一项并集与凸包(包围两个框的最小凸多边形 )面积的比值。
这一比值使得即使框间没有交集,GIoU 仍然能提供梯度,让 和 两个框逐步重合。但实际使用时,GIoU 也有问题:可能会出现 的情况,此时凸包比值是 (并集和凸包都是 )。如果只是单纯退化到 IoU 倒也好,但因为多出一个优化目标,我在实际使用时发现更容易出现发散和震荡,如果学习率不够玄学,收敛相比之下更困难。
DIoU。入手方向略有不同,它试图把两个框的中心点推到一起,添加了一项框中心点距离和凸包直径距离的比值。这直观理解就没有什么问题了。实际使用时也比 GIoU 要好收敛得多。
我认为 DIoU 算是合格的完成了任务,该有的都有了。至于 CIoU 则是比 DIoU 额外考虑了长宽比,我没有太多研究。如有机会,另行补充。
Non-maximum Suppression
上次的 RetinaNet 没有说到这个东西。网络会对于每一个网格都输出若干个预测框,就会有同一个物体与多个预测框相交,我们显然只需要里面最合适的那一个就够了。Non-maximum Supression 就是一个用来筛选网络预测框的算法。
总的说来一句话就能讲清楚:将置信度最高的框加入最终结果,顺便踢掉与之过于重合的框,然后不断重复这个过程直到没有框。……我个人是感觉这「算法」相比其他算法来说份量略显得轻了一点,没什么意思。这只是个暴力的贪心算法,本身设计上没什么效率与证明可言,放经典算法设计里根本谈不上起新名字。
实现起来多少还是有需谨慎的地方。不过摊上 GPU 并行之后优化代码不是每个人都能做的,容易弄巧成拙。torchvision 里提供了一个 NMS 算法的实现,如果没什么特殊需求建议用那个。
下方是网上一个 python+pytorch 的简单实现。效率很差,只是放在这展示一下算法过程,不要用。想要优化这东西恐怕还是写 CUDA 来得方便,pytorch 的封装太高阶了。
1 |
|
说些有的没的
从理论上来讲,YOLO 的 anchor 是少于 RetinaNet 的,它的效果也应该不会更好。实验印证了这一想法。如果又快准确率还又高,RetinaNet 就可以打回去重造了。
YOLO 的作者 Joseph Redmon 也是个很有意思的家伙,不管是从自己实现 Darknet 上来看,还是从他网站和论文上看,或者是他的简历。前段时间他因无法容忍自己研究成果在军事等领域造成的负面影响,退出了 CV 研究。
- v1 有所不同,它对每个网格输出类别概率,而不是 anchor。 ↩