目标检测的损失往往由三个部分组成:分类损失Lcls,置信度损失Lobj与边界框的iou损失Lbox。Lcls与Lbox仅由正样本产生,而Lobj则由所有样本产生。
不同于DETR这种端到端的目标检测算法,YOLO会产生大量的预测框,每一个预测框称之为一个样本。那么对于产生的这些预测框,哪些应该作为正样本去与gt(ground truth)计算Lbox与Lcls,哪些又应该作为负样本仅仅贡献Lobj呢?这就取决于所定义的样本分配方法。
YOLOv3&v5样本分配策略
在开始讲样本分配之前,首先要弄清楚两点:
- YOLOv3与YOLOv5都采用特征金字塔的结构来实现多尺度,最后输出三个不同下采样倍数的特征图
- YOLOv3与YOLOv5都是基于锚框(anchor)的目标检测方法,每层特征图上的每个点都对应三个anchor
样本分配是在网络最后输出的三个不同下采样倍数的特征图上逐层进行的:
- 首先将归一化的gt映射到特征图对应的大小;
- 分别计算gt与该尺度特征图上预设的三个不同大小的anchor的宽高比并判断是否满足:
1/thr < ratio <thr
,如果满足说明这个gt与anchor尺寸匹配,接下来会进一步为其分配正样本;不满足则说明这个gt与这个anchor尺寸不匹配,不会为其匹配对应anchor的正样本。假设我们有m个标注的真实边界框gt,那么一层特征图上理论最多会有3*m
对匹配成功的gt-anchor
(因为YOLOv3&v5中每个格点对应3个anchor);核心代码解析:
t = targets * gain # gt映射到对应特征图上
if nt:
# Matches 这一步完成target与3个anchor的匹配
r = t[..., 4:6] / anchors[:, None] # wh ratio
j = torch.max(r, 1 / r).max(2)[0] < self.hyp['anchor_t'] # compare (3, n)
t = t[j] # filter
- 对于YOLOv3来说,最后一步只需要判断gt的中心点落在哪个网格中,这个网格的左上角的格点就会负责预测这个gt,也就是说作为其的正样本:
- 对于YOLOv5来说,每个gt不止其中心所在的格点负责预测它,还会根据其是位于网格中的左上角、左下角、右上角还是右下角为其额外分配两个正样本:
对于YOLOv5中落在每个格点的不同区域的分配情况我画了个图穷举如下:
在相同的情况下,YOLOv5中分配给每个gt的正样本是YOLOv3中的3倍。核心代码解析:
t = targets * gain # gt映射到对应特征图上
if nt:
# Matches 这一步完成target与3个anchor的匹配
r = t[..., 4:6] / anchors[:, None] # wh ratio
j = torch.max(r, 1 / r).max(2)[0] < self.hyp['anchor_t'] # compare (3, n)
t = t[j] # filter
# Offsets
gxy = t[:, 2:4] # grid xy 相对于左上角的距离
gxi = gain[[2, 3]] - gxy # inverse 相对于右下角的距离
# 根据gt中心点是落在网格中的左上、左下、右上、右下制定一个mask
# 除了第一行、第一列、最后一行、最后一列的网格,j与l、k与m是互补的
# 对于第一行、第一列、最后一行、最后一列的网格需要特殊处理:
# 比如如果gt落在最左上角网格的左上区域,那么对于这个gt只会有一个点负责预测它,那就是所处网格的左上格点
j, k = ((gxy % 1 < g) & (gxy > 1)).T
l, m = ((gxi % 1 < g) & (gxi > 1)).T
# 这里为什么前面要加一个torch.ones_like(j),是因为对于每个gt有三个格点负责预测它(见上面画的那个网格图),torch.ones_like(j)代表中心点本身所处的网格,jklm中有两个为true,代表偏移之后的网格
j = torch.stack((torch.ones_like(j), j, k, l, m)) # (5, 10)
# 最终分配到了正样本的gt,以及其对应的偏移量
t = t.repeat((5, 1, 1))[j]
offsets = (torch.zeros_like(gxy)[None] + off[:, None])[j]