19. YOLOv5源码loss.py解读

783 阅读9分钟

声明:

1.私人数据集,有3种类别 0/1/2

2.训练 batchsize=16

思路

build_targets:找到与target匹配的Anchors,并记录下这些Anchors应该的target类别,target转到feature map的位置尺寸信息,这些Anchors的索引,这些Anchors的尺寸。

  • tcls:与target匹配的Anchors应该的targets类别 0/1/2

  • tbox:target从原图转到特征图的归一化坐标和尺寸[x_center,y_center,w,h]

  • indices:[target所属image index,target匹配到的Anchor index,target所在grid 左上角y,target所在grid 左上角x]

indices记录了与target相匹配的Anchors的索引,后面计算位置回归损失和类别损失则通过indices找到的Anchor与target相应的信息计算损失值。

  • anch:target匹配到的Anchors的尺寸(从原图转到特征图的Anchor归一化尺寸[w,h])

yolov5.yaml中的Anchor预设尺寸是从原图转到特征图的Anchor尺寸[w,h],不过没有进行过归一化,那些Anchor的尺寸是输入图像640×640分辨率预设的。

ComputeLoss

位置回归损失(IOU):tbox与匹配Anchor归一化尺寸[x_center,y_center,w,h]计算iou。

类别损失:tcls与匹配Anchor的类别信息计算损失。

置信度损失:位置回归损失(IOU)与所有Anchor的置信度计算损失。

(train.py) 初始化损失对象:

#初始化损失计算对象
compute_loss = ComputeLoss(model)  # init loss class

(loss.py) 进入ComputeLoss的__init__函数初始化计算损失的变量:

class ComputeLoss:
    sort_obj_iou = False

    # Compute losses
    def __init__(self, model, autobalance=False):
        # 设备号
        device = next(model.parameters()).device  # get model device

        # 模型参数
        h = model.hyp  # hyperparameters

        # BCE loss:
            # 分类损失对象和置信度损失对象
        BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['cls_pw']], device=device))
        BCEobj = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['obj_pw']], device=device))

        # smooth_BCE 标签平滑策略:
            # self.cp是1.0 cp代表正样本标签值
            # self.cn是0.0 cn代表负样本标签值
        self.cp, self.cn = smooth_BCE(eps=h.get('label_smoothing', 0.0))  # positive, negative BCE targets

        # Focal loss:
            # g作为开关:当g大于0时,将分类和置信度的BCE损失封装成FocalLoss损失,g作为gamma系数控制难易样本的训练。
        g = h['fl_gamma']  # focal loss gamma
        if g > 0:
            BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g)

        # m是Detect类型,即模型的3个检测头
        m = de_parallel(model).model[-1]  # Detect() module

        # self.balance用来设置三个feature map对应输出的置信度损失系数(分别平衡三个feature map的置信度损失)
            # 这句话是指:如果key=m.nl存在,则获取value=[4.0, 1.0, 0.4]
            # 如果如果key=m.nl不存在,则返回[4.0, 1.0, 0.25, 0.06, 0.02]
        self.balance = {3: [4.0, 1.0, 0.4]}.get(m.nl, [4.0, 1.0, 0.25, 0.06, 0.02])
        # 往下找找:self.balance = [x / self.balance[self.ssi] for x in self.balance]
        # 所以说self.ssi是autobalance为True时,用来更新self.balance用的
        self.ssi = list(m.stride).index(16) if autobalance else 0  # stride 16 index

        self.BCEcls, self.BCEobj, self.gr, self.hyp, self.autobalance = BCEcls, BCEobj, 1.0, h, autobalance
        # 模型中得到:每个检测头专有的Anchor的种类 3
        self.na = m.na  # number of anchors
        # 模型中得到:类别数量 3
        self.nc = m.nc  # number of classes
        # 模型中得到:检测头的个数 3
        self.nl = m.nl  # number of layers
        # 模型中得到:self.anchors.shape=(3,3,2)  3个检测头,每个检测头有3种尺寸的Anchor,从原图转到特征图的Anchor归一化尺寸[长,宽]
        self.anchors = m.anchors 
        self.device = device

(train.py) 每个epoch中,开始迭代每个batch的数据并获取一个batch的损失:

for epoch in range(start_epoch, epochs):
    ...
    for i, (imgs, targets, paths, _) in pbar:
        ...
        pred = model(imgs)  # forward
        # compute_loss-->return (lbox + lobj + lcls) * bs, torch.cat((lbox, lobj, lcls)).detach()
        # compute_loss中最后计算的 lbox、lobj、lcls是一个平均batch的各项loss(具体而言就是用BCE去计算的),因此(lbox + lobj + lcls) * bs看做是一个batch的数据的总loss。
        # loss是一个batch的数据的总loss(loss=[(lbox + lobj + lcls) * bs]),loss_items是一个平均batch的各项loss(loss=[位置损失,置信度损失,类别损失])。
        loss, loss_items = compute_loss(pred, targets.to(device)) 
        ...
        #mloss=(上个batch的各项loss+当前batch的各项loss)/(batch索引+1)
        mloss = (mloss * i + loss_items) / (i + 1)
        ...

(loss.py)进入ComputeLoss的__call__函数计算损失:

build_targets:将targets匹配到对应的Anchors

# build_targets作用:将targets匹配到对应的Anchors。
    # tcls为 与targets匹配的Anchors应该的targets类别 0/1/2  
    # tbox为 target从原图转到特征图的归一化坐标和尺寸[x_center,y_center,w,h]  
    # indices为 [targets所属image index,targets匹配到的Anchors index,target所在grid 左上角y,target所在grid 左上角x] grid是feature map  
    # anch为 target匹配到的Anchors的尺寸(从原图转到特征图的Anchor归一化尺寸[w,h])

    # 因此返回的例如len(tcls[0])=39482并不是真实目标数量,也不是Anchors总数(并不是88*88*3),而是targets匹配的Anchors数量。  

    def build_targets(self, p, targets):
        # Build targets for compute_loss(), input targets(image,class,x,y,w,h)
        # na是anchor的种类  nt是真实目标个数
        na, nt = self.na, targets.shape[0]  # number of anchors, targets
        tcls, tbox, indices, anch = [], [], [], []
        # 空间网格增益
        gain = torch.ones(7, device=self.device)  # normalized to gridspace gain
        # ai.shape=(3,一个batch图片目标数之和) 每个目标3种anchor [[0...0],[1...1],[2...2]]
        ai = torch.arange(na, device=self.device).float().view(na, 1).repeat(1, nt)  # same as .repeat_interleave(nt)

        targets = torch.cat((targets.repeat(na, 1, 1), ai[..., None]), 2)  # append anchor indices

        g = 0.5  # bias
        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=self.device).float() * g  # offsets

        # 对每一个检测头进行迭代
        for i in range(self.nl):
            anchors, shape = self.anchors[i], p[i].shape
            gain[2:6] = torch.tensor(shape)[[3, 2, 3, 2]]  # xyxy gain

            # Match targets to anchors
            t = targets * gain  # shape(3,n,7)
            if nt:
                # Matches
                r = t[..., 4:6] / anchors[:, None]  # wh ratio
                j = torch.max(r, 1 / r).max(2)[0] < self.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

                # Offsets
                gxy = t[:, 2:4]  # grid xy
                gxi = gain[[2, 3]] - gxy  # inverse
                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]
            else:
                t = targets[0]
                offsets = 0

            # Define
            bc, gxy, gwh, a = t.chunk(4, 1)  # (image, class), grid xy, grid wh, anchors
            a, (b, c) = a.long().view(-1), bc.long().T  # anchors, image, class
            gij = (gxy - offsets).long()
            gi, gj = gij.T  # grid indices

            # Append
            indices.append((b, a, gj.clamp_(0, shape[2] - 1), gi.clamp_(0, shape[3] - 1)))  # image, anchor, grid
            tbox.append(torch.cat((gxy - gij, gwh), 1))  # box
            anch.append(anchors[a])  # anchors
            tcls.append(c)  # class
            print(tbox)
        return tcls, tbox, indices, anch

__call__:计算targets与匹配到的Anchors的损失

#一个batch的数据已经到达战场啦~  
# p:预测值  
# len(pred)=3 pred指的是每个检测头的输出feature map 每个像素每个anchor的信息  
# preds[0].shape (16,3,176,176,8)  
# preds[1].shape (16,3,88,88,8)  
# preds[2].shape (16,3,21,21,8)  
# 8表示这个anchor的[x_center, y_center, w, h, 1置信度信息, 3类别条件概率信息]
# 注:1.检测头所得到的Anchor[x_center, y_center, w, h]是从原图转到特征图的归一化坐标和尺寸。
#      2.与val.py中的的pred是不同的!
  
# targets:真实值  
# targets是一batch的数据,每个目标的[图片索引, 0/1/2, x_center, y_center, w, h] 
# targets.shape=(一个batch图片目标数之和,6)
# 注:targets的[x_center, y_center, w, h]是缩放过的原图上的比例。


def __call__(self, p, targets):  # predictions, targets
    #lcls=lbox=lobj=[0.]
    lcls = torch.zeros(1, device=self.device)  # class loss
    lbox = torch.zeros(1, device=self.device)  # box loss
    lobj = torch.zeros(1, device=self.device)  # object loss
    # 一个batch的数据,真实目标在3个检测头上匹配到的Anchors:
        # tcls:len(tcls)=3 表示3个检测头上 与targets匹配的Anchors应该的targets类别 0/1/2
        # tbox:len(tbox)=3 表示3个检测头上 target从原图转到特征图的归一化坐标和尺寸[x_center, y_center, w, h]
        # indices: len(indices)=3 表示3个检测头上 target匹配到的Anchors信息。
            # 3个检测头:
                # [(b,b..),(a,a..),(gi,gi..),(gj,gj..)],
                # [(b,b..),(a,a..),(gi,gi..),(gj,gj..)],
                # [(b,b..),(a,a..),(gi,gi..),(gj,gj..)]
            # b:target所属image index
            # a:target匹配到的Anchors index。每个预测层都有3种类别的Anchor,a=0/1/2指的是该预测层3种Anchor。
            # gj:target所在grid 左上角y
            # gi:target所在grid 左上角x
        # anchors:表示3个检测头上 target匹配到的Anchors尺寸(从原图转到特征图的Anchor归一化尺寸[w,h])
    tcls, tbox, indices, anchors = self.build_targets(p, targets)  # targets

    # Losses:对每个检测头去计算损失
        # i=0,1,2
        # i=0时,pi.shape=(16,3,176,176,8) 得到了第一个检测头的所有Anchors信息。
    for i, pi in enumerate(p):  # layer index, layer predictions

        # 检测头1 i=0:
        # b.shape=(39482,)
        # a.shape=(39482,)
        # gj.shape=(39482,)
        # gi.shape=(39482,)
        b, a, gj, gi = indices[i]  # image, anchor, gridy, gridx
        # tobj.shape (16,3,176,176)
        tobj = torch.zeros(pi.shape[:4], dtype=pi.dtype, device=self.device)  # target obj
        # i=0 n=39482 targets匹配到的Anchors数量
        n = b.shape[0]  # number of targets

        # i=0 targets匹配到的Anchors数量不为0时
        if n:
            # pi.shape=(16,3,176,176,8)
            # 首先,根据索引[b, a, gj, gi]只选取targets匹配到的Anchors 的最后一个维度的数值。
            # 然后,在最后一个维度上,因为这个维度有8个数,将8个数划分成2,2,1,3的子数组。
            # 2(x_center+y_center)+2(w+h)+1(置信度信息)+3(类别条件概率信息)+
            # 得到:
                # pxy.shape=(39482,2) 与target匹配Anchor的[x_center,y_center]
                # pwh.shape=(39482,2) 与target匹配Anchor的[w,h]
                # pcls.shape=(39482,3) 与target匹配Anchor的3个概率信息 #pcls=[[-2.145,-7.889,-2.4554],...[-2.145,-7.889,-2.4554]]
            pxy, pwh, _, pcls = pi[b, a, gj, gi].split((2, 2, 1, self.nc), 1)  # target-subset of predictions

            # 1.Regression 位置回归损失:
            # 1-iou=1-(tbox--target从原图转到特征图的[x_center,y_center,w,h])与(pbox--与target匹配的Anchors[x_center,y_center,w,h])的iou
            pxy = pxy.sigmoid() * 2 - 0.5
            pwh = (pwh.sigmoid() * 2) ** 2 * anchors[i]
            # pbox.shape=(39482,4) 与target匹配的Anchors的[x_center,y_center,w,h]
            pbox = torch.cat((pxy, pwh), 1)  # predicted box
            # tbox[0].shape=(39482,4) target从原图转到特征图的[x_center,y_center,w,h]
            iou = bbox_iou(pbox, tbox[i], CIoU=True).squeeze()  # iou(prediction, target)
            lbox += (1.0 - iou).mean()  # iou loss


            # 2.Objectness 置信度损失:
            # (tobj--iou)与(pi[..., 4]--所有Anchors的置信度)的BCE损失
            iou = iou.detach().clamp(0).type(tobj.dtype)
            # 张量从计算图中分离 iou.detach(),小于0的部分截断为0 clamp(0)
            if self.sort_obj_iou:
                # 好的,不会计算这玩意
                j = iou.argsort()
                b, a, gj, gi, iou = b[j], a[j], gj[j], gi[j], iou[j]
            if self.gr < 1:
                # 好的,不会计算这玩意
                iou = (1.0 - self.gr) + self.gr * iou
            # tobj.shape=(16,3,176,176)
            # 将iou记录到对应的[b,a,gj,gi]索引处
            tobj[b, a, gj, gi] = iou  # iou ratio

            # 3.Classification 分类损失:(pcls--与target匹配Anchors的3个类别概率信息)和(t--targets匹配到的Anchors应该的targets类别)的BCE损失  
            if self.nc > 1: # cls loss (only if multiple classes)  
                # pcls.shape=(39482,3) 与target匹配Anchors的3个类别概率信息  
                # 创建一个与pcls形状相同(39482,3),值为self.cn=0.0的张量 [ [0,0,0],[0,0,0]... ]  
                t = torch.full_like(pcls, self.cn, device=self.device) # targets  
                # n是targets匹配到的Anchors数量 39482 range(n)=[0,1,2,...39481]  
                # tcls为targets匹配到的Anchors应该的targets的类别:tcls[0].shape=39482 tcls[0]=[0,1,0,1,2...0]  
                # t[range(n), tcls[i]] = self.cp :将t相应索引处赋值为self.cp=1.0  
                t[range(n), tcls[i]] = self.cp  
                # t=[[0,0,1],...[0,1,0]]  
                # pcls=[[-2.145,-7.889,-2.4554],...[-2.145,-7.889,-2.4554]]
                # (pcls--与target匹配AnchorsAnchors的3个类别概率信息)和(t--targets匹配到的Anchors标注上targets的类别)的BCE损失  
                lcls += self.BCEcls(pcls, t) # BCE
                
        # 2.Objectness 置信度损失:
        # (tobj--iou)与(pi[..., 4]--所有Anchors的置信度)的BCE损失

        # tobj.shape=(16,3,176,176) target从原图转到特征图的[x_center,y_center,w,h]与target匹配Anchors[x_center,y_center,w,h]的iou
        # pi.shape=(16,3,176,176,8) 8表示这个Anchor的[x_center, y_center, w, h, 1置信度信息, 3类别条件概率信息]
        # pi[..., 4]表示选取前面所有维度的索引,然后在最后一个维度上选择索引为4的元素,即选取所有Anchors的置信度信息

        # 注意负样本的置信度损失计算: tobj有些位置是0,说明这个位置没有目标,那么与pi对应索引处会计算置信度损失。
        obji = self.BCEobj(pi[..., 4], tobj)
        # 置信度损失=置信度损失×平衡系数
        lobj += obji * self.balance[i]  # obj loss
        if self.autobalance:
            self.balance[i] = self.balance[i] * 0.9999 + 0.0001 / obji.detach().item()

    if self.autobalance:
        self.balance = [x / self.balance[self.ssi] for x in self.balance]
    lbox *= self.hyp['box']
    lobj *= self.hyp['obj']
    lcls *= self.hyp['cls']
    bs = tobj.shape[0]  # batch size

    # 最后计算的 lbox、lobj、lcls是一个平均batch的各项loss(具体而言就是用BCE去计算的),因此(lbox + lobj + lcls) * bs看做是一个batch的数据的总loss。
    return (lbox + lobj + lcls) * bs, torch.cat((lbox, lobj, lcls)).detach()