参考了https://zhuanlan.zhihu.com/p/183838757的理解进行学习,写的很好,菜鸡复刻,也记录一下我的理解。
build_targets输入三个部分,分别是预测值,真实框值,和网络模型:
1.其中p为网络的预测值,通过网络的前向传播计算得来:
pred = model(imgs) # forwardp的大小分别是:
p[0] = 1x3x80x80x85 p[1] = 1x3x40x40x85 p[2] = 1x3x20x20x85因为yolo系都是对原始特征图进行划格操作,分别划分为80x80,40x40,20x20个格子,每个格子又负责预测3个不同比例的anchors,因此会产生上述数量个bbox。
2.target为真是框的标注值,因为送进网络的图像经过了数据增强,所以里面的坐标会相应的进行改变,即原坐标设计相对于原单独图片的相对位置,而经过数据增强后,几幅图拼成一副新的图片,因此,target内的gt框不同与标签内数值,会相对于拼接而成的新图重新计算相对百分比。
我的target为:
tensor([[ 0.00000, 45.00000, 0.49938, 0.76329, 0.99876, 0.46894], [ 0.00000, 45.00000, 0.24456, 0.41555, 0.48911, 0.37517], [ 0.00000, 50.00000, 0.33163, 0.79807, 0.51882, 0.40207], [ 0.00000, 45.00000, 0.64387, 0.55076, 0.71227, 0.61542], [ 0.00000, 49.00000, 0.32137, 0.32527, 0.12395, 0.07634], [ 0.00000, 49.00000, 0.18875, 0.32311, 0.09527, 0.07657], [ 0.00000, 49.00000, 0.29884, 0.39958, 0.13784, 0.11568], [ 0.00000, 49.00000, 0.32555, 0.28328, 0.15546, 0.11660]])一共8个目标。 3.model即为定义好的网络模型
yolov5将三个det层写在同一个模块中了,这点不同于v3/v4。 因此在调用时,仅取出model的最后一层即可。
det = model.module.model[-1] if is_parallel(model) else model.model[-1]anchors为锚,锚框大小为:
- [10,13, 16,30, 33,23] # P3/8 - [30,61, 62,45, 59,119] # P4/16 - [116,90, 156,198, 373,326] # P5/32配置文件中的锚框大小个人理解为真实尺寸的大小,即像素,而锚框的真实尺寸需要映射到相应的特征图大小,因此需要分别处以8/16/32的步幅以转为特征图尺寸。转换后的锚框大小为: [ 1.25000, 1.62500], [ 2.00000, 3.75000], [ 4.12500, 2.87500] [ 1.25000, 1.62500], [ 2.00000, 3.75000], [ 4.12500, 2.87500] [ 1.25000, 1.62500], [ 2.00000, 3.75000], [ 4.12500, 2.87500] 注意,此时的单位并不是像素,而是格子数量,如何理解呢,就是说,特征图已经被划分为80x80个格子了,80x80是用于检测最小物体尺寸的特征图,而此时的锚框检测1.25个格子宽,1.625个格子高和2.00000个格子宽,3.75000格子高和4.125个格子宽 和2.875个格子高三个不同长宽比的锚框。
因为此时gt框和anchor都是以格子为基本单位,所以可以直接除,求出比例,当shape超出一定范围,就认定在当前尺寸为负样本,尺寸的上限定义在anchor_t处,默认大小为4。 感觉此处设计的真的挺巧妙地,之前的代码将target进行了三次复制。
targets = torch.cat((targets.repeat(na, 1, 1), ai[:, :, None]), 2) # append anchor indices经过复制后的target一步就把当前尺度中三个不同shape的anchor进行了负样本的过滤。 运行之后的j为(我跑的结果,8个target在80x80尺度下的三个shape匹配情况)
tensor([[False, False, False], [False, False, False], [False, False, False], [False, False, False], [False, False, True], [False, True, True], [False, False, True], [False, False, True]], device='cuda:0')之前写到,我的target是8个,所以经过这一步就初步在80x80尺寸上保留了5个可能存在的正样本框,过滤掉了大部分负样本框。 即过滤后的结果为:
tensor([[ 0.00000, 49.00000, 15.09995, 25.84866, 7.62146, 6.12527, 1.00000], [ 0.00000, 49.00000, 25.70989, 26.02190, 9.91570, 6.10687, 2.00000], [ 0.00000, 49.00000, 15.09995, 25.84866, 7.62146, 6.12527, 2.00000], [ 0.00000, 49.00000, 23.90721, 31.96605, 11.02732, 9.25421, 2.00000], [ 0.00000, 49.00000, 26.04394, 22.66201, 12.43697, 9.32767, 2.00000]],每行最后面的数字代表不同的shape。
取出x,y坐标:
gxy = t[:, 2:4] # grid xy # gxy(tensor([[15.09995, 25.84866], # [25.70989, 26.02190], # [15.09995, 25.84866], # [23.90721, 31.96605], # [26.04394, 22.66201]], device='cuda:0'))对t复制5份,即本身点外加上下左右四个候选区共五个区域,选出三份,具体选出哪三份?由torch.stack后的j决定,第一项是torch.ones_like,即全1矩阵,说明本身是必选中状态的。剩下的4项中,由于是inverse操作,所以j和l,k和m是两两互斥的。这样就确保了只选出三项,但是到现在为止,还并没有产生偏移。offset是对off中选出与t相对应位置操作。
gxi = gain[[2, 3]] - gxy # inverse # tensor([[64.90005, 54.15134], # [54.29012, 53.97810], # [64.90005, 54.15134], # [56.09279, 48.03395], # [53.95606, 57.33799]], device='cuda:0') j, k = ((gxy % 1. < g) & (gxy > 1.)).T l, m = ((gxi % 1. < g) & (gxi > 1.)).T j = torch.stack((torch.ones_like(j), j, k, l, m)) t = t.repeat((5, 1, 1))[j] offsets = (torch.zeros_like(gxy)[None] + off[:, None])[j]取出对应的信息
b, c = t[:, :2].long().T # image, class gxy = t[:, 2:4] # grid xy gwh = t[:, 4:6] # grid wh gij = (gxy - offsets).long() gi, gj = gij.T # grid xy indicesb代表一个batch中的image的index(猜的,这个不确定),c就是标注的类别新,gxy-offset就是修改增加两个正样本的坐标,gwh并没有额外操作,说明增加的两个正样本仅仅是坐标发生了偏移,并没有修改形状。最后通过.long取整,找出index。
剩下的就是入栈操作了。
其实在build_target函数中,虽然传入了pred,但是并没有太大的用处。 对pred也是多尺度,每个尺度进行训练,
for i, pi in enumerate(p): # layer index, layer predictions b, a, gj, gi = indices[i]根据build_target后的信息对pred的信息取出
ps = pi[b, a, gj, gi] # prediction subset corresponding to targets在pred取出对应信息,剩下的就是利用交叉熵进行loss计算了,其实深度学习训练到最后训练了个啥?无非就是训练了个损失函数,深度学习的目的就是使loss降低,方法是反向传播调整卷积的参数使得loss降低。
loss = lbox + lobj + lcls学习也不是很透彻,很多个人理解,若有错误欢迎指正。