YOLO v3
Last updated
Last updated
Yolo v3 总体思想和 Yolo v2 没有任何区别,只不过引入了最新提升性能的组件,从而将 Yolo v2 提升到一个非常强的高度,属于 Yolo v2 的改进版本,只有在彻底理解了 Yolo v2 后才好理解 Yolo v3 。其相比 v2 主要改进可以简单归纳为:
基于 RetinaNet 算法引入了 FPN 和多尺度预测
基于主流 ResNet 残差设计思想提出了新的骨架网络 darknet53
不再将所有 loss 都认为是回归问题,而是分为分类和回归 loss,更加符合主流设计思想
虽然 Yolo v3 是前 Yolo 系列的改进,但是由于其以下突出特性,使其实际应用程度远远大于前 Yolo 系列。可以说目前提到 Yolo ,基本都是指的 Yolo v3 了。
提供高速高精度的 Yolo v3 模型和极速版本的 tiny-Yolo v3 模型,可以适用于复杂和简单场景
全部代码采用 darknet 所写,纯 c 构建,容易训练和部署
更加符合现代目标检测算法思想,大家接受程度比较高
由于其突出的重要性,故本文会结合 mmdet 中 Yolo v3 代码进行讲解,力图在熟悉算法原理的同时能够深入理解代码。而且 mmdet 目前还在快速发展,故解读的 mmdet 版本是 2.5.0,截止时间是 2020.10.31。
整个 Yolo v3 网络包括标准的 backbone、neck 和 head 三个部分。
CBL:Yolo v3 网络结构中的最小组件,由 Conv + Bn + Leaky-Relu 三者组成 Res unit:借鉴 Resnet 网络中的残差结构,让网络可以构建的更深 ResX:由一个 CBL 和 X 个 Res unit 构成,是 Yolo v3 中的大组件。每个 Res unit 前面的 CBL 都起到下采样的作用,因此经过 5 次 Res 模块后,得到的特征图是 608->304->152->76->38->19 大小
(1) backbone
相比其余主流网络,性能如下:
代码层面 darknet53 配置为:
darknet53 和标准的 resnet 一样,也是分为 stem+stage+head 三个部分
stem 就是一层 CBL 模块
然后经过 5 个 stage,每个 stage 都包括 1+2*X 个卷积层,X 的配置是(1, 2, 8, 8, 4),而(32, 64), (64, 128)表示每个 stage 的输入和输出通道,其 stride 为 2、4、8、16 和 32
head 部分就是常说的 avg pool+fc 分类模块。
Yolo v3 只需要最后三个 stage 的输出,故 backbone 输出是三个不同大小的特征图,其 stride=8,16,32。整个 darknet53 配置如下:
out_indices 表示最后三个 stage 的特征图输出索引。
(2) neck
这里的 neck 模块是指 FPN 层,其输入是 backbone 的三个尺度输出特征图,输出也是和输入 wh 一样的三个尺度,只不过内部进行了从高层特征和相邻低层特征进行上采样后 concat 进行融合操作,从而对输入特征图进行增强。其配置为:
其结构简图如下:
假设输入的三个特征图命名为 P3~P5,stride 分别是 8/16/32。
对 P5 直接采用 5 个 CBL 模块得到 O5 输出,输出通道是通过 out_channels 指定的
对 O5 先进行 1x1 卷积将通道变换成和 P4 一样,然后进行 2 倍最近邻上采样,然后和 P4 特征进行 concat 融合,最后也是经过 5 个 CBL 模块得到 O4 输出
对 O4 先进行 1x1 卷积将通道变换成和 P3 一样,然后进行 2 倍最近邻上采样,然后和 P3 特征进行 concat 融合,最后也是经过 5 个 CBL 模块得到 O3 输出
此时就得到三个不同尺度的经过融合后的特征图 O3~O5,stride 分别是 8/16/32。
(3) head
对 neck 模块输出的 O3 ~ O5 都采用 3x3 的 CBL 模块+1x1 的卷积进行通道变换即可,此时就可以得到三个尺度预测层了。每一层输出特征图 shape 为(b,num_anchor*(xywh+conf+cls_num))。Yolo v3 每个预测层都是 3 个 anchor,是通过 kmean 聚类算出来的,按照从大到小 anchor 尺度排列,分别检测大物体和小物体。
可以发现相比 yolov2,其将单尺度预测修改为了多尺度输出,并且额外引入了 FPN 进行特征融合增强。
其规则和 yolov2 完全相同,只不过任何一个 gt bbox 和 anchor 计算 iou 的时候是会考虑三个预测层的 anchor,而不是将 gt bbox 和每个预测层单独匹配。假设某个 gt bbox 和第二个输出层的某个 anchor 是最大 iou,那么就仅仅该 anchor 负责对应 gt bbox,其余所有 anchor 都是负样本。同样的 Yolo v3 也需要基于预测值计算忽略样本。
在 coco 数据集上通过 kmean 算法聚类得到的 9 个 anchor 如下所示:
可以看出其是原图尺度,每个输出层的每个特征图位置都包括三个 anchor,并且按照预测从大到小物体的输出层顺序排列,对应特征图 stride 是从大到小即 O5 ~ O3。假设输入图片大小是 608x608,那么一共包括 19x19x3+38x38x3+76x76x3=22743 个 anchor。
为了详细说明 Yolo v3 的正负样本定义规则,需要先回顾下 yolov2 的规则,其可以简要归纳为:
遍历每个 gt bbox,首先判断其中心点落在哪个网格,然后和那个网格内的所有 anchor 计算 iou,iou 最大的 anchor 就负责匹配即正样本
考虑额外规则:当每个 gt bbox 和最大 iou 的 anchor 匹配完成后,对剩下的 anchor 再次和对应网格内的 gt bbox 进行匹配(必须限制在对应网格内,否则 xy 预测范围变了),当该 anchor 和 gt bbox 的 iou 大于一定阈值例如 0.7 后,也算作正样本,即该 anchor 也负责预测 gt bbox
遍历每个负 anchor 的预测值,如果 anchor 预测值和其余所有 gt bbox 的所有 iou 值中(不需要网格限制),只要有一个 iou 值大于阈值,则该 anchor 预测值忽略,不计算 loss
这样就得到了每个 anchor 的正负和忽略样本属性,其中正样本用于训练分类和回归分支,正负样本用于训练置信度分支,忽略样本忽略。
现在从 yolov2 的单尺度预测变成了 Yolo v3 多尺度预测,其匹配规则为:
遍历每个 gt bbox,首先判断其中心点落在哪 3 个网格(三个输出层上都存在),然后和那 3 个网格内的所有 anchor(一共 9 个 anchor)计算 iou,iou 最大的 anchor 就负责匹配即正样本,其余 8 个 anchor 都暂时是负样本。可以看出其不允许 gt bbox 在多个输出层上都预测
考虑额外规则:当每个 gt bbox 和最大 iou 的 anchor 匹配完成后,对剩下的 anchor 再次和对应网格内的 gt bbox 进行匹配(必须限制在对应网格内,否则 xy 预测范围变了),当该 anchor 和 gt bbox 的 iou 大于一定阈值例如 0.7 后,也算作正样本,即该 anchor 也负责预测 gt bbox
遍历每个负 anchor 的预测值,如果 anchor 预测值和其余所有 gt bbox 的所有 iou 值中(不需要网格限制),只要有一个 iou 值大于阈值,则该 anchor 预测值忽略,不计算 loss
可以发现相比 yolov2,仅仅是第一步有一点点区别。但是需要特别注意的是 mmdet 中的 Yolo v3 的正负样本分配策略和原始论文不太一样,具体是:
初始化所有 anchor 的标志都是-1,表示全部是忽略样本
遍历每个 anchor,计算每个 anchor 和所有 gt 的 iou,如果其中最大 iou 小于 neg_iou_thr 则表示是该 anchor 是负样本(背景样本),因为他和任何 gt bbox 的重合率都比较低
遍历每个 anchor 和其对应的网格索引,首先判断是否有 gt bbox 落在该网格内部,如果没有直接跳过,如果有 gt bbox,则计算该 anchor 和所有落在对应网格的 gt bbox 的 iou,找出最大 iou 值,如果其最大 iou 大于等于 pos_iou_thr,则设置该 anchor 标记为正样本,且该 anchor 负责预测对应 gt bbox
遍历每个 gt bbox,首先计算其落在哪 3 个网格上(3 个输出层),然后和那 3 个网格内的所有 anchor(一共 9 个 anchor)计算 iou,iou 最大的 anchor 就负责匹配即正样本
看起来比较复杂,我们可以仔细分析下
假设设置 pos_iou_thr>1
,neg_iou_thr>1
,那么第三步其实就失效了,且第二步中所有 anchor 都是负样本,此时的分配策略就是 Yolo v3 规则中的第 1 个步骤了
假设设置 pos_iou_thr=0.5
,neg_iou_thr>1
,那么此时分配规则就是 Yolo v3 中的第 1 和 2 步骤了
如果按照原始配置 pos_iou_thr=0.5
,neg_iou_thr=0.5
,则不存在忽略样本,并且分配规则也是 Yolo v3 中的第 1 和 2 步骤
通过上面的分析,我们可以知道 mmdet 中的实现始终缺少了第 3 步:忽略样本定义。我猜测原因是为了复用 mmdet 的 max iou assigner 策略。
这个规则和 yolov2 完全相同。正样本的 xy 预测值是相对当前网格左上角的偏移,而 wh 预测值是 gt bbox 的 wh 除以 anchor 的 wh(注意 wh 是在特征图尺度算还是原图尺度算是一样的,解码还原时候注意下就行),然后取 log 得到。
和 yolov2 不同的是,对于分类和置信度预测值,其采用的不是 l2 loss,而是 bce loss,而回归分支依然是 l2 loss。
疑问:为啥要将分类和置信度预测值的 loss 将 l2 loss 换成 bce loss?
解答:首先这是主流做法,分类问题一般都是用 ce 或者 bce loss,而在 retinanet 里面提到采用 bce loss 进行多分类可以避免类间竞争,对于 coco 这种数据集是很有好处的;其次在机器学习中知道对于逻辑回归问题采用 bce loss 是一个凸优化问题,理论上优化速度比 l2 loss 快;而且分类输出就是一个概率分布,采用分类常用 loss 是最常规做法,没必要统一成回归问题
和 yolov2 一样,置信度分支的 label 可以设置为 1 或者 iou 值。但是在标准 Yolo v3 配置中,默认是 1。通过第三方用户对比实验发现原因可能是:在训练时,有些预测框与真实框的 iou 极限值很难达到 1,假设置信度以 0.7 作为标签,最后学到的数值是 0.5,0.6,那么假设推理时的分值阈值就不能设置的比较高,否则对于 coco 这类存在大量小物体的数据很容易出现漏检。而如果设置 label 设置为 1 可以提高召回率,此时置信度就不具有反映预测的准确率的作用了,有利有弊吧!在 mmdet 的 Yolo v3 代码中,置信度分支的 label 为 1。
到目前为止,我们可以发现相比原始 Yolo v3,mmdet 中的复现少了以下操作:
置信度分支的所有负样本全部算是背景,不存在忽略样本
对于正样本 anchor,其回归 loss 没有大小物体自适应 wh 加权
按我个人理解,这两个策略还是有用的,特别是第二个对于小物体的 mAP 应该有很大提升。
Yolo v3 的训练过程和 v2 完全相同。
推理流程为:
遍历每个输出层,对 xy 预测值采用 sigmoid 变成 0~1,然后进行解码,具体是对遍历每个网格,xy 预测值加上当前网格坐标,然后乘上 stride 即可得到预测 bbox 中心坐标,对于 wh 预测值先进行指数计算,然后直接乘上 anchor 的 wh 即可,此时就可以还原得到最终的 bbox
对置信度分支采用 sigmoid 变成 0~1,分类分支由于是 bce loss 故也需要采用 sigmoid 操作
利用置信度预测将置信度值低于预测的预测值过滤掉
如果剩下的预测 bbox 数目多于设置的 nms 前阈值个数 1000,则直接对置信度值进行从大到小 topk 排序,提取前 1000 个,三个输出层最多构成 3000 个预测 bbox
对所有预测框进行 nms 即可
可以看出,Yolo v3 性能强于 ssd 系列,但是比 faster rcnn 差,这是正常的,yolo 系列算法的主打是高速,在如此快的检测速度下依然有不错的 mAP。
Yolo v3 可以认为是当前目标检测算法思想的集大成者,其通过引入主流的残差设计、FPN 和多尺度预测,将 one-stage 目标检测算法推到了一个速度和精度平衡的新高度。由于其高速高精度的特性,在实际应用中通常都是首选算法。
Yolo v3: An Incremental Improvement
每个 ResX 中包含 1+2*X 个卷积层,因此整个主干网络 Backbone 中一共包含,再加上一个 FC 全连接层,即可以组成一个 Darknet53 分类网络。将 avgpool+fc 部分去掉就是常说的 darknet53 骨架网络。