yolov5 loss部分理解,菜鸡复刻版

    科技2024-01-05  113

    YOLOV5 loss部分理解,菜鸡复刻版(不一定对,互相交流)

    参考了https://zhuanlan.zhihu.com/p/183838757的理解进行学习,写的很好,菜鸡复刻,也记录一下我的理解。

    一.build_targets部分理解

    def build_targets(p, targets, model):

    build_targets输入三个部分,分别是预测值,真实框值,和网络模型:

    1.其中p为网络的预测值,通过网络的前向传播计算得来:

    pred = model(imgs) # forward

    p的大小分别是:

    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]

    准备工作,取出锚框的数量,gt框的数量,以及定义变量

    # number of anchors, number of targets na, nt = det.na, targets.shape[0] tcls, tbox, indices, anch = [], [], [], [] gain = torch.ones(7, device=targets.device) # normalized to gridspace gain ai = torch.arange(na, device=targets.device).float().view(na, 1).repeat(1, nt) # same as .repeat_interleave(nt) g = 0.5 # bias # off定义偏移量 off = torch.tensor([[0, 0],[1, 0], [0, 1], [-1, 0], [0, -1], # j,k,l,m # [1, 1], [1, -1], [-1, 1], [-1, -1], # jk,jm,lk,lm], device=targets.device).float() * g # offsets

    依次对yolov5的三个不同尺度的特征层进行检测

    for i in range(det.nl): anchors = det.anchors[i]

    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个格子高三个不同长宽比的锚框。

    获取特征图的格子数

    gain[2:6] = torch.tensor(p[i].shape)[[3, 2, 3, 2]] # xyxy gain # 分别是80x80,40x40,20x20

    将gt框映射到特征图上,同样的,单位也是格子数量

    t = targets * gain

    根据gt框的shape寻找在当前尺寸可能存在正样本的bbox,过滤负样本

    r = t[:, :, 4:6] / anchors[:, None] # wh ratio j = torch.max(r, 1. / r).max(2)[0] < model.hyp['anchor_t'] # compare # j = wh_iou(anchors, t[:, 4:6]) > model.hyp['iou_t'] # iou(3,n)=wh_iou(anchors(3,2), gwh(n,2)) t = t[j] # filter

    因为此时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 indices

    b代表一个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

    学习也不是很透彻,很多个人理解,若有错误欢迎指正。

    Processed: 0.014, SQL: 8