YOLOv5 anchor 编码(label assign)方式详解

喜欢就关注 AIZOO 吧!

YOLOv5 凭借其不错的性能,以及傻瓜式的使用方法,让其在社区的确比较火🔥。YOLOv5 在其 anchor 编码(也就是label assign)方式上,与YOLOv2~v4 有很大不同,其代码又比较难懂,本文来写一下笔者对其的理解。

个人觉得,YOLOv5 这个仓库的代码,在追求性能的同时,并没有遵守良好的代码风格,导致有些地方的代码,非常难以看懂,大量类似 b,a,j,k 这种单字母的变量,让人很容易摸不着头脑。

YOLOv5 label assign 方式,看似只有短短的 53 行,但其实不是特别容易让人弄懂,笔者也是断断续续的花了好几天的时间,才把它彻底搞明白。下面,结合个人理解,对它做一下讲解,如有错误,欢迎指正。

YOLOv5 label assign 的代码,在这里:https://github.com/ultralytics/yolov5/blob/4c409332667477560200958b513b958bb8fdef71/utils/loss.py#L169

也就是这个 def build_targets(self, p, targets) 函数。其实,想搞明白这个函数的核心在于搞明白,一个物体,也就是 Ground truth (以下简称GT),是如何分配到几千上万个 anchor 上面去的。关于 anchor 是什么,笔者之前写个一篇通俗易懂的文章,对 anchor 不熟悉的朋友可以看看这一篇:

新手也能彻底搞懂的目标检测Anchor是什么?怎么科学设置?(附代码)

不同于 YOLOv2~v4,一个 GT 只会分配给一个 anchor,YOLOv5中,一个 anchor 可以被分配给多个anchor,还有可能被分配到三个不同的检测层中的两个甚至三个检测层。

让我们看一段源码:

r = t[:, :, 4:6] / anchors[:, None]  # 目标的 w 和 h,与 anchor 相除
j = torch.max(r, 1 / r).max(2)[0] < self.hyp['anchor_t']  # 保留 1/4~4 之间的 GT
t = t[j]  # filter

其中 t 就是GT数组,取 4:6 就是取出来 GT 的 width 和 hight,然后与当前检测层的三个 anchor 匹配,这三个 anchor 是这样的:

[[10,13],
[16,30],
[33,23]]

self.hyp['anchor_t'] 默认是4,也就是目标 GT 与 anchor 相除,只要其比值在 1/4~4 之间,这个 就说明这个 GT 可以被分配到这个 anchor 上面。t = t[j] ,其实就是多个 GT 被过滤了一下,只留下了那些可以被分配到三组 anchor 其中 1~3个 的 GT。

你以为到这里就结束了吗,no,难理解的还在后面。让我们再来看看这一段

gxy = t[:, 2:4]  # grid xy
gxi = gain[[2, 3]] - gxy  # 反转一下,用 [feature_w, feature_h] 减去 GT 的 x 和 y 坐标。
j, k = ((gxy % 1 < g) & (gxy > 1)).T  # 看余数是不是小于 g, gxy>1 是为了判断是不是在边界grid,防止扩张的时候溢出范围。
l, m = ((gxi % 1 < g) & (gxi > 1)).T  # 看余数是不是大于 g
j = torch.stack((torch.ones_like(j), j, k, l, m))
t = t.repeat((5, 1, 1))[j] # 把目标复制 5份,然后再根据条件过滤一下
offsets = (torch.zeros_like(gxy)[None] + off[:, None])[j]

其中 gain[[2, 3]] 就是当前检测层特征图的长宽,例如[80,80],gxy 就是 GT 的 x 和 y 坐标(请注意,这个 x 和 y 不是 GT 在原图的大小,而是在当前特征图上的大小,例如特征图大小是 80x80,x 和 y 的范围就是 0~80 之间,是归一化值乘以特征图大小的结果)。

最难理解的,就是第三第四行,其中 g 默认是 0.5, gxy 对 1 取余数,也就是只保留小数部分,其实,这两行代码的本意,就是为了判断 GT,到底在一个 grid 的四个象限的哪个位置。

c271c68901a66301d4eca13b9b9c0c3e.png因为一个 GT 的中心,总得落入某个 Grid 中吧(包括grid 左边和上面的边界线),上面代码第三和第四行,就是为了确认物体落在一个 grid 中四个象限中的哪一个。

后面,还有一个比较难理解的,就是代码最后一行,其中 off 就是

(Pdb) off
tensor([[ 0.00000,  0.00000],
        [ 0.50000,  0.00000],
        [ 0.00000,  0.50000],
        [-0.50000,  0.00000],
        [ 0.00000, -0.50000]])

,将 off 复制成跟 t.repeat((5, 1, 1)) 一样多份,然后通过 j 来过滤一下,保留对应位置的数值,那么它的作用是什么呢?且看代码

gxy = t[:, 2:4]  # GT 中心点 xy
gwh = t[:, 4:6]  # GT 的 w h
gij = (gxy - offsets).long()

将 gxy 与 offset 相减,因为 offset 有的是正 0.5, 有的是 -0.5,有的是 0,其实就是将每个 gxy,向两个方向扩张,将一个 grid,扩张成三个。也就是下面这个图中,每个 GT,根据所处的位置,向两个方向扩张,所以,YOLOv5,并不是只有一个 grid 中的某个 anchor 为正样本,而是一般会扩张两个 grid 来预测。

根据GT 中心点在 grid 中的位置,将会向两个方向扩张,不同位置扩张的 Grid,笔者用不同颜色的小球来表示了,一目了然。

eba9aec820aca2f8bf8a5288687b82d0.png所以,才会导致某个 grid 中预测的值,会出现负值,例如下面,如果 GT 在左下角,那下面的 grid 中的某个 anchor,预测的 y 值为负数,因为实际的物体中心 y 值,相比下面grid 的y 坐标,为负值。

3469fafc1b2cb862a65561f9324c3e69.png

这也是,为什么下面代码,pxy 的值会是 -0.5 ~ 1.5。

pxy = ps[:, :2].sigmoid() * 2 - 0.5 # pxy 范围是 -0.5 ~ 1.5
pwh = (ps[:, 2:4].sigmoid() * 2) ** 2 * anchors[i] # 值的范围是anchor 的 0~4 倍

最后补充一句,YOLOv5 预测的,是相对于 grid 左上角点的 x 和 y 的偏移。,具体可以看下面代码的最后一行,xy 是gxy - gij

gij = (gxy - offsets).long() # 向两个方向扩张两个grid
  gi, gj = gij.T  # grid xy indices
  
  a = t[:, 6].long()  # anchor indices
  indices.append((b, a, gj.clamp_(0, gain[3] - 1), gi.clamp_(0, gain[2] - 1)))  # image, anchor, grid indices
  tbox.append(torch.cat((gxy - gij, gwh), 1))  # box

到这里,YOLOv5 label assign 核心部分算是讲完了,你应该大致知道 YOLOv5 anchor 是怎么分配的了。但是,你应该还是云里雾里,你应该多看看代码,pdb 调试一下,多看几遍文章,实在不懂,可以加群讨论。

62944220042ee5c8c19ae8ba07054e32.png

欢迎扫描下方的二维码添加小助手微信,邀请您加入我们的微信交流群。

群里有多位清北复交、BAT、AI独角兽大牛和众多深度学习er在一起愉快的交流技术,有任何问题,都可以咨询大家,欢迎你的加入哦。

05fe2dbca9965d42e7df370ee6b4e923.png

 添加小助手微信,邀您进 AIZOO圈 技术交流群

听说点个在看的人运气都很好~

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值