YOLOV5代码精读之损失函数(loss.py)

一、函数ComputeLoss:

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

        # Define criteria
        BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['cls_pw']], device=device))
        BCEobj = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['obj_pw']], device=device))

        # Class label smoothing https://arxiv.org/pdf/1902.04103.pdf eqn 3
        self.cp, self.cn = smooth_BCE(eps=h.get('label_smoothing', 0.0))  # positive, negative BCE targets

        # Focal loss
        g = h['fl_gamma']  # focal loss gamma
        if g > 0:
            BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g)

        m = de_parallel(model).model[-1]  # Detect() module
        self.balance = {3: [4.0, 1.0, 0.4]}.get(m.nl, [4.0, 1.0, 0.25, 0.06, 0.02])  # P3-P7
        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
        self.na = m.na  # number of anchors
        self.nc = m.nc  # number of classes
        self.nl = m.nl  # number of layers
        self.anchors = m.anchors
        self.device = device

    def __call__(self, p, targets):  # predictions, targets
        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
        tcls, tbox, indices, anchors = self.build_targets(p, targets)  # targets

        # Losses
        for i, pi in enumerate(p):  # layer index, layer predictions
            b, a, gj, gi = indices[i]  # image, anchor, gridy, gridx
            tobj = torch.zeros(pi.shape[:4], dtype=pi.dtype, device=self.device)  # target obj

            n = b.shape[0]  # number of targets
            if n:
                # pxy, pwh, _, pcls = pi[b, a, gj, gi].tensor_split((2, 4, 5), dim=1)  # faster, requires torch 1.8.0
                pxy, pwh, _, pcls = pi[b, a, gj, gi].split((2, 2, 1, self.nc), 1)  # target-subset of predictions

                # Regression
                pxy = pxy.sigmoid() * 2 - 0.5
                pwh = (pwh.sigmoid() * 2) ** 2 * anchors[i]
                pbox = torch.cat((pxy, pwh), 1)  # predicted box
                iou = bbox_iou(pbox, tbox[i], CIoU=True).squeeze()  # iou(prediction, target)
                lbox += (1.0 - iou).mean()  # iou loss

                # Objectness
                iou = iou.detach().clamp(0).type(tobj.dtype)
                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[b, a, gj, gi] = iou  # iou ratio

                # Classification
                if self.nc > 1:  # cls loss (only if multiple classes)
                    t = torch.full_like(pcls, self.cn, device=self.device)  # targets
                    t[range(n), tcls[i]] = self.cp
                    lcls += self.BCEcls(pcls, t)  # BCE

                # Append targets to text file
                # with open('targets.txt', 'a') as file:
                #     [file.write('%11.5g ' * 4 % tuple(x) + '\n') for x in torch.cat((txy[i], twh[i]), 1)]

            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

        return (lbox + lobj + lcls) * bs, torch.cat((lbox, lobj, lcls)).detach()

    def build_targets(self, p, targets):
        # Build targets for compute_loss(), input targets(image,class,x,y,w,h)
        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 = 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

        return tcls, tbox, indices, anch

 

这段代码定义了一个 ComputeLoss 类,用于在目标检测模型中计算损失。下面是对代码的逐步分解和详细解释:

1.1. 类的定义与初始化

  def __init__(self, model, autobalance=False):
        super(ComputeLoss, self).__init__()
        device = next(model.parameters()).device  # get model device
        h = model.hyp  # hyperparameters
		
		'''
		定义分类损失和置信度损失为带sigmoid的二值交叉熵损失,
		即会先将输入进行sigmoid再计算BinaryCrossEntropyLoss(BCELoss)。
		pos_weight参数是正样本损失的权重参数。
		'''
        # Define criteria
        BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['cls_pw']], device=device))
        BCEobj = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['obj_pw']], device=device))

		'''
		对标签做平滑,eps=0就代表不做标签平滑,那么默认cp=1,cn=0
        后续对正类别赋值cp,负类别赋值cn
		'''
        # Class label smoothing https://arxiv.org/pdf/1902.04103.pdf eqn 3
        self.cp, self.cn = smooth_BCE(eps=h.get('label_smoothing', 0.0))  # positive, negative BCE targets
		
		'''
		超参设置g>0则计算FocalLoss
		'''
        # Focal loss
        g = h['fl_gamma']  # focal loss gamma
        if g > 0:
            BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g)
		
		'''
		获取detect层
		'''
        det = model.module.model[-1] if is_parallel(model) else model.model[-1]  # Detect() module
        '''
        每一层预测值所占的权重比,分别代表浅层到深层,小特征到大特征,4.0对应着P3,1.0对应P4,0.4对应P5。
        如果是自己设置的输出不是3层,则返回[4.0, 1.0, 0.25, 0.06, .02],可对应1-5个输出层P3-P7的情况。
        '''
        self.balance = {3: [4.0, 1.0, 0.4]}.get(det.nl, [4.0, 1.0, 0.25, 0.06, .02])  # P3-P7
        '''
        autobalance 默认为 False,yolov5中目前也没有使用 ssi = 0即可
        '''
        self.ssi = list(det.stride).index(16) if autobalance else 0  # stride 16 index
        '''
        赋值各种参数,gr是用来设置IoU的值在objectness loss中做标签的系数, 
        使用代码如下:
		tobj[b, a, gj, gi] = (1.0 - self.gr) + self.gr * iou.detach().clamp(0).type(tobj.dtype)  # iou ratio
        train.py源码中model.gr=1,也就是说完全使用标签框与预测框的CIoU值来作为该预测框的objectness标签。
        '''
        self.BCEcls, self.BCEobj, self.gr, self.hyp, self.autobalance = BCEcls, BCEobj, model.gr, h, autobalance
        for k in 'na', 'nc', 'nl', 'anchors':
            setattr(self, k, getattr(det, k))
  • 该类初始化时获取模型的各类信息,包括设备、超参数以及损失函数的定义。
  • 使用了二元交叉熵损失和焦点损失来应对类别不平衡问题。

这段代码是在定义一个损失计算类时的一部分,其主要功能是设置和初始化与目标检测模型训练相关的损失函数和超参数(hyperparameters)配置。这段代码可以逐步分解如下:

  1. 获取模型设备:

    device = next(model.parameters()).device  # get model device
    

    这行代码通过访问模型的参数来获取其所在的设备(例如 CPU 或 GPU),用于之后的张量计算。

  2. 获取超参数:

    h = model.hyp  # hyperparameters
    

    从模型中提取超参数(hyperparameters),这些参数通常包括学习率、损失函数权重等。

  3. 定义损失函数:

    # Define criteria 定义分类损失和置信度损失
    # BCEcls = BCEBlurWithLogitsLoss()
    # BCEobj = BCEBlurWithLogitsLoss()
    # h['cls_pw']=1  BCEWithLogitsLoss默认的正样本权重也是1
    BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['cls_pw']], device=device))
    BCEobj = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['obj_pw']], device=device))
    

    这两行代码分别定义了分类损失(BCEcls)和目标物体损失(BCEobj,使用的是带有权重的二元交叉熵损失(Binary Cross Entropy Loss with Logits)。pos_weight参数用于调整正样本和负样本的权重,帮助处理类别不平衡的问题。

  4. 类标签平滑:

    # 标签平滑  eps=0代表不做标签平滑-> cp=1 cn=0  eps!=0代表做标签平滑 cp代表positive的标签值 cn代表negative的标签值
    self.cp, self.cn = smooth_BCE(eps=h.get('label_smoothing', 0.0))  # positive, negative BCE targets
    

    这行代码通过调用 smooth_BCE 函数来实现标签平滑(label smoothing),使得模型对标签的学习更加平滑,减少模型对训练样本的过拟合。

  5. 焦点损失:

    # Focal loss  g=0 代表不用focal loss
    g = h['fl_gamma']  # focal loss gamma
    if g > 0:
        BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g)
    

    这段代码检索焦点损失(Focal Loss)的参数 gamma,如果大于0,则对分类及目标物体损失进行焦点损失包装,以抵消对简单样本的过度关注,从而使模型更加关注难以分类的样本。

  6. 获取检测模块:

    # det: 返回的是模型的检测头 Detector 3个 分别对应产生三个输出feature map
    m = de_parallel(model).model[-1]  # Detect() module
    

    通过调用 de_parallel 方法获取模型的最后一层(检测模块),它用于执行目标检测。

  7. 定义平衡因子:

    # balance用来设置三个feature map对应输出的置信度损失系数(平衡三个feature map的置信度损失)
    # 从左到右分别对应大feature map(检测小目标)到小feature map(检测大目标)
    # 思路:  It seems that larger output layers may overfit earlier, so those numbers may need a bit of adjustment
    #       一般来说,检测小物体的难度大一点,所以会增加大特征图的损失系数,让模型更加侧重小物体的检测
    # 如果det.nl=3就返回[4.0, 1.0, 0.4]否则返回[4.0, 1.0, 0.25, 0.06, .02]
    # self.balance = {3: [4.0, 1.0, 0.4], 4: [4.0, 1.0, 0.25, 0.06], 5: [4.0, 1.0, 0.25, 0.06, .02]}[det.nl]
    self.balance = {3: [4.0, 1.0, 0.4]}.get(m.nl, [4.0, 1.0, 0.25, 0.06, 0.02])  # P3-P7
    

    根据模型的层数决定损失的平衡因子,以调整不同层次的损失计算,帮助达到更好的训练效果。

  8. 确定索引:

     # 三个预测头的下采样率det.stride: [8, 16, 32]  .index(16): 求出下采样率stride=16的索引
     # 这个参数会用来自动计算更新3个feature map的置信度损失系数self.balance
    self.ssi = list(m.stride).index(16) if autobalance else 0  # stride 16 index
    

    这行代码确定了一个用于平衡计算的索引 ssi,具体依赖于模型的步幅(stride)和是否启用自动平衡。

  9. 设置其他参数:

     # self.BCEcls: 类别损失函数   self.BCEobj: 置信度损失函数   self.hyp: 超参数
    # self.gr: 计算真实框的置信度标准的iou ratio    self.autobalance: 是否自动更新各feature map的置信度损失平衡系数  默认False
    # na: number of anchors  每个grid_cell的anchor数量 = 3
    # nc: number of classes  数据集的总类别 = 80
    # nl: number of detection layers   Detect的个数 = 3
    # anchors: [3, 3, 2]  3个feature map 每个feature map上有3个anchor(w,h) 这里的anchor尺寸是相对feature map的
    self.BCEcls, self.BCEobj, self.gr, self.hyp, self.autobalance = BCEcls, BCEobj, 1.0, h, autobalance
    self.na = m.na  # number of anchors
    self.nc = m.nc  # number of classes
    self.nl = m.nl  # number of layers
    self.anchors = m.anchors
    self.device = device
    

    最后几行代码将之前定义的各种损失函数、超参数、锚框和设备设置为类的属性,方便在后续计算损失时调用。

这段代码主要功能是初始化和配置深度学习模型训练过程中的损失函数和相关超参数。它旨在处理目标检测的特定需求,包括类别不平衡、目标物体的检测误差处理以及训练过程中损失的平衡因子配置。整体来看,它为后续的损失计算做好了必要的准备。

1.2. 损失计算

def __call__(self, p, targets):  # predictions, targets, model
        device = targets.device
        lcls, lbox, lobj = torch.zeros(1, device=device), torch.zeros(1, device=device), torch.zeros(1, device=device)
        '''
        从build_targets函数中构建目标标签,获取标签中的tcls, tbox, indices, anchors
        tcls = [[cls1,cls2,...],[cls1,cls2,...],[cls1,cls2,...]]
        tcls.shape = [nl,N]
        tbox = [[[gx1,gy1,gw1,gh1],[gx2,gy2,gw2,gh2],...],
        
        indices = [[image indices1,anchor indices1,gridj1,gridi1],
        		   [image indices2,anchor indices2,gridj2,gridi2],
        		   ...]]
        anchors = [[aw1,ah1],[aw2,ah2],...]		  
        '''
        tcls, tbox, indices, anchors = self.build_targets(p, targets)  # targets

        # Losses
        '''
		p.shape = [nl,bs,na,nx,ny,no]
		nl 为 预测层数,一般为3
		na 为 每层预测层的anchor数,一般为3
		nx,ny 为 grid的w和h
		no 为 输出数,为5 + nc (5:x,y,w,h,obj,nc:分类数)
		'''
        for i, pi in enumerate(p):  # layer index, layer predictions
            '''
            a:所有anchor的索引
            b:标签所属image的索引
            gridy:标签所在grid的y,在0到ny-1之间
            gridy:标签所在grid的x,在0到nx-1之间
            '''
            b, a, gj, gi = indices[i]  # image, anchor, gridy, gridx
            '''
            pi.shape = [bs,na,nx,ny,no]
            tobj.shape = [bs,na,nx,ny]
            '''
            tobj = torch.zeros_like(pi[..., 0], device=device)  # target obj

            n = b.shape[0]  # number of targets
            if n:
            	'''
            	ps为batch中第b个图像第a个anchor的第gj行第gi列的output
            	ps.shape = [N,5+nc],N = a[0].shape,即符合anchor大小的所有标签数
            	'''
                ps = pi[b, a, gj, gi]  # prediction subset corresponding to targets

				'''
				xy的预测范围为-0.5~1.5
                wh的预测范围是0~4倍anchor的w和h,
                原理在代码后讲述。
				'''
                # Regression
                pxy = ps[:, :2].sigmoid() * 2. - 0.5
                pwh = (ps[:, 2:4].sigmoid() * 2) ** 2 * anchors[i]
                pbox = torch.cat((pxy, pwh), 1)  # predicted box
                '''
                只有当CIOU=True时,才计算CIOU,否则默认为GIOU
                '''
                iou = bbox_iou(pbox.T, tbox[i], x1y1x2y2=False, CIoU=True)  # iou(prediction, target)
                lbox += (1.0 - iou).mean()  # iou loss

                # Objectness
                '''
                通过gr用来设置IoU的值在objectness loss中做标签的比重, 
                '''
                tobj[b, a, gj, gi] = (1.0 - self.gr) + self.gr * iou.detach().clamp(0).type(tobj.dtype)  # iou ratio

                # Classification
                if self.nc > 1:  # cls loss (only if multiple classes)
                    '''
               		ps[:, 5:].shape = [N,nc],用 self.cn 来填充型为[N,nc]得Tensor。
               		self.cn通过smooth_BCE平滑标签得到的,使得负样本不再是0,而是0.5 * eps
                	'''
                    t = torch.full_like(ps[:, 5:], self.cn, device=device)  # targets
                    '''
                    self.cp 是通过smooth_BCE平滑标签得到的,使得正样本不再是1,而是1.0 - 0.5 * eps
                    '''
                    t[range(n), tcls[i]] = self.cp 
                    '''
                    计算用sigmoid+BCE分类损失
                    '''
                    lcls += self.BCEcls(ps[:, 5:], t)  # BCE

                # Append targets to text file
                # with open('targets.txt', 'a') as file:
                #     [file.write('%11.5g ' * 4 % tuple(x) + '\n') for x in torch.cat((txy[i], twh[i]), 1)]
			'''
			pi[..., 4]所存储的是预测的obj
			'''
            obji = self.BCEobj(c, tobj)
            '''
			self.balance[i]为第i层输出层所占的权重,在init函数中已介绍
			将每层的损失乘上权重计算得到obj损失
			'''
            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]
        '''
        hyp.yaml中设置了每种损失所占比重,分别对应相乘
        '''
        lbox *= self.hyp['box']
        lobj *= self.hyp['obj']
        lcls *= self.hyp['cls']
        bs = tobj.shape[0]  # batch size

        loss = lbox + lobj + lcls
        return loss * bs, torch.cat((lbox, lobj, lcls, loss)).detach()
  • 该方法是调用时计算损失的主体。
  • 它会计算类别损失、边框损失和物体损失,并根据需要进行自适应平衡。
  • 预测值和目标之间的损失通过多种形式进行计算(BCE、IoU等)。

这段代码是一个__call__方法的实现,属于一个损失计算类,通过预测结果和真实目标计算损失值。这个函数相当于forward函数,在这个函数中进行损失函数的前向传播。

:params p:  预测框 由模型构建中的三个检测头Detector返回的三个yolo层的输出
                    tensor格式 list列表 存放三个tensor 对应的是三个yolo层的输出
                    如: [4, 3, 112, 112, 85]、[4, 3, 56, 56, 85]、[4, 3, 28, 28, 85]
                    [bs, anchor_num, grid_h, grid_w, xywh+class+classes]
                    可以看出来这里的预测值p是三个yolo层每个grid_cell(每个grid_cell有三个预测值)的预测值,后面肯定要进行正样本筛选
        :params targets: 数据增强后的真实框 [63, 6] [num_object,  batch_index+class+xywh]
        :params loss * bs: 整个batch的总损失  进行反向传播
        :params torch.cat((lbox, lobj, lcls, loss)).detach(): 回归损失、置信度损失、分类损失和总损失 这个参数只用来可视化参数或保存信息
        """

以下是代码的逐步分解和解释:

  1. 初始化损失变量

    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
    

    这里创建了三个损失(lclslboxlobj)并初始化为零,分别用于计算类别损失、边界框损失和目标性损失。

  2. 构建目标

    # 每一个都是append的 有feature map个 每个都是当前这个feature map中3个anchor筛选出的所有的target(3个grid_cell进行预测)
            # tcls: 表示这个target所属的class index
            # tbox: xywh 其中xy为这个target对当前grid_cell左上角的偏移量
            # indices: b: 表示这个target属于的image index
            #          a: 表示这个target使用的anchor index
            #          gj: 经过筛选后确定某个target在某个网格中进行预测(计算损失)  gj表示这个网格的左上角y坐标
            #          gi: 表示这个网格的左上角x坐标
            # anch: 表示这个target所使用anchor的尺度(相对于这个feature map)  注意可能一个target会使用大小不同anchor进行计算
            tcls, tbox, indices, anchors = self.build_targets(p, targets)  # targets
    

    通过调用build_targets方法,获取真实目标的类别(tcls)、框信息(tbox)、索引(indices)和锚框信息(anchors)。

  3. 损失计算循环

    # 依次遍历三个feature map的预测输出pi
    for i, pi in enumerate(p):  # layer index, layer predictions
    

    遍历每一层的预测(p),pi代表第i层的预测结果。

  4. 索引解压

    b, a, gj, gi = indices[i]  # image, anchor, gridy, gridx
    

    indices中解压出每个预测的图像索引、锚框索引及网格坐标。

  5. 目标对象初始化

    # 初始化target置信度(先全是负样本 后面再筛选正样本赋值)
    tobj = torch.zeros(pi.shape[:4], dtype=pi.dtype, device=self.device)  # target obj
    

    创建一个用于存放目标对象的张量。

  6. 处理目标

    n = b.shape[0]  # number of targets
    if n:
        # pxy, pwh, _, pcls = pi[b, a, gj, gi].tensor_split(...)
        # 精确得到第b张图片的第a个feature map的grid_cell(gi, gj)对应的预测值
        # 用这个预测值与我们筛选的这个grid_cell的真实框进行预测(计算损失)
        pxy, pwh, _, pcls = pi[b, a, gj, gi].split((2, 2, 1, self.nc), 1)  # target-subset of predictions
    

    如果找到目标,提取预测的框的坐标(pxypwh)及分类概率(pcls)。

  7. 回归损失计算

     # Regression loss  只计算所有正样本的回归损失
    # 新的公式:  pxy = [-0.5 + cx, 1.5 + cx]    pwh = [0, 4pw]   这个区域内都是正样本
    # Get more positive samples, accelerate convergence and be more stable
    pxy = ps[:, :2].sigmoid() * 2. - 0.5  # 一个归一化操作 和论文里不同
    # https://github.com/ultralytics/yolov3/issues/168
    pwh = (ps[:, 2:4].sigmoid() * 2) ** 2 * anchors[i]  # 和论文里不同 这里是作者自己提出的公式
    pbox = torch.cat((pxy, pwh), 1)  # predicted box
    # 这里的tbox[i]中的xy是这个target对当前grid_cell左上角的偏移量[0,1]  而pbox.T是一个归一化的# 就是要用这种方式训练 传回loss 修改梯度 让pbox越来越接近tbox(偏移量)
    iou = bbox_iou(pbox.T, tbox[i], x1y1x2y2=False, CIoU=True)  # iou(prediction, target)
    lbox += (1.0 - iou).mean()  # iou loss
    
    # Objectness loss stpe1
    # iou.detach()  不会更新iou梯度  iou并不是反向传播的参数 所以不需要反向传播梯度信息
    score_iou = iou.detach().clamp(0).type(tobj.dtype)  # .clamp(0)必须大于等于0
    # 这里对iou进行排序在做一个优化:当一个正样本出现多个GT的情况也就是同一个grid中有两个gt(密集型且形状差不多物体)
    # There maybe several GTs match the same anchor when calculate ComputeLoss in the scene with dense targets
    if self.sort_obj_iou:
       # https://github.com/ultralytics/yolov5/issues/3605
       # There maybe several GTs match the same anchor when calculate ComputeLoss in the scene with dense targets
       sort_id = torch.argsort(score_iou)  # score从小到大排序 拿到对应index
       # 排序之后 如果同一个grid出现两个gt 那么我们经过排序之后每个grid中的score_iou都能保证是最大的
       # (小的会被覆盖 因为同一个grid坐标肯定相同)那么从时间顺序的话, 最后1个总是和最大的IOU去计算LOSS, 梯度传播
       b, a, gj, gi, score_iou = b[sort_id], a[sort_id], gj[sort_id], gi[sort_id], score_iou[sort_id]
       # 预测信息有置信度 但是真实框信息是没有置信度的 所以需要我们人为的给一个标准置信度
       # self.gr是iou ratio [0, 1]  self.gr越大置信度越接近iou  self.gr越小置信度越接近1(人为加大训练难度)
        tobj[b, a, gj, gi] = (1.0 - self.gr) + self.gr * score_iou  # iou ratio
        # tobj[b, a, gj, gi] = 1  # 如果发现预测的score不高 数据集目标太小太拥挤 困难样本过多 可以试试这个
    
        # Classification loss  只计算所有正样本的分类损失
        if self.nc > 1:  # cls loss (only if multiple classes)
           # targets 原本负样本是0  这里使用smooth label 就是cn
           t = torch.full_like(ps[:, 5:], self.cn, device=device)
           t[range(n), tcls[i]] = self.cp  # 筛选到的正样本对应位置值是cp
           lcls += self.BCEcls(ps[:, 5:], t)  # BCE
    
         # Append targets to text file
         # with open('targets.txt', 'a') as file:
         # [file.write('%11.5g ' * 4 % tuple(x) + '\n') for x in torch.cat((txy[i], twh[i]), 1)]

     分割预测结果

    pxy, pwh, _, pcls = pi[b, a, gj, gi].split((2, 2, 1, self.nc), 1)  # target-subset of predictions
    

    pi 表示某一层的预测输出。

    bagjgi 是索引,代表图像、锚点、网格的行与列。

    这行代码将预测结果分割为 pxy(x、y坐标)、pwh(宽、高)、 _(未用的变量)和 pcls(分类概率)。

    split 方法将张量沿指定维度(这里是第 1 维)进行分割。

    回归损失

    pxy = pxy.sigmoid() * 2 - 0.5
    pwh = (pwh.sigmoid() * 2) ** 2 * anchors[i]
    pbox = torch.cat((pxy, pwh), 1)  # predicted box
    

    通过 sigmoid 激活函数将 pxy 和 pwh 转换为概率值。pxy 的范围调整到 [-0.5, 1.5],pwh 则是经过平方处理后再与当前锚点相乘,得到预测框的宽和高。

    将 pxy 和 pwh 连接在一起,形成一个完整的预测框 pbox

    计算 IOU(交并比):

    iou = bbox_iou(pbox, tbox[i], CIoU=True).squeeze()  # iou(prediction, target)
    lbox += (1.0 - iou).mean()  # iou loss
    

    bbox_iou 函数计算预测框 pbox 和真实框 tbox[i] 之间的 IOU 值。

    squeeze() 函数用于去掉维度为1的维度。

    通过 1.0 - iou 计算损失,并取平均值累加到 lbox 中。

    物体性评分(Objectness)

    iou = iou.detach().clamp(0).type(tobj.dtype)
    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[b, a, gj, gi] = iou  # iou ratio
    

    detach() 用于从计算图中断开,clamp(0) 用于将 IOU 值限制在0以上,确保没有负值。

    如果设置了 sort_obj_iou,则根据 IOU 的值排序索引。

    通过平滑操作调整 IOU 的计算,最终将 IOU 比率赋值给目标对象 tobj

    分类损失

    if self.nc > 1:  # cls loss (only if multiple classes)
        t = torch.full_like(pcls, self.cn, device=self.device)  # targets
        t[range(n), tcls[i]] = self.cp
        lcls += self.BCEcls(pcls, t)  # BCE
    

    如果类别数量大于1,则进行分类损失计算。

    创建一个目标张量 t,填充值为 self.cn(负标签平滑的设置),并根据真实类别设置正标签为 self.cp

    最后计算 BCE(二进制交叉熵)损失,并将其累加到 lcls

    这段代码的主要功能是处理 YOLOv5 目标检测模型的输出结果,在训练中计算回归损失、物体性评分以及分类损失。

    回归损失:计算模型预测框与真实框之间的 IOU,评价预测框的精确度。

    物体性评分:评估物体存在的程度,以及预测的可信度,调整后存储在 tobj 中。

    分类损失:计算并累加分类的二进制损失,以优化模型在多类目标检测中的性能。

    整体来看,这段代码是模型训练阶段的关键部分,确保模型能够有效学习到如何识别和定位目标。

  8. 目标性损失计算

    # Objectness loss stpe2 置信度损失是用所有样本(正样本 + 负样本)一起计算损失的
    obji = self.BCEobj(pi[..., 4], tobj)
    # 每个feature map的置信度损失权重不同  要乘以相应的权重系数self.balance[i]
    # 一般来说,检测小物体的难度大一点,所以会增加大特征图的损失系数,让模型更加侧重小物体的检测
    lobj += obji * self.balance[i]  # obj loss
    

    计算目标性损失并乘以相应的平衡因子更新总损失。

  9. 自动平衡损失

    if self.autobalance:
        self.balance[i] = self.balance[i] * 0.9999 + 0.0001 / obji.detach().item()
    

    根据当前损失动态调整损失的平衡因子,以实现更好的训练效果。

  10. 最终损失返回

    # 根据超参中的损失权重参数 对各个损失进行平衡  防止总损失被某个损失所左右
    lbox *= self.hyp['box']
    lobj *= self.hyp['obj']
    lcls *= self.hyp['cls']
    
    bs = tobj.shape[0]  # batch size
    
    loss = lbox + lobj + lcls  # 平均每张图片的总损失
    
    # loss * bs: 整个batch的总损失
     # .detach()  利用损失值进行反向传播 利用梯度信息更新的是损失函数的参数 而对于损失这个值是不需要梯度反向传播的
    return loss * bs, torch.cat((lbox, lobj, lcls, loss)).detach()
    

    最后将三种损失加和,乘以批次大小并返回损失值以及每种损失的中间结果。

该代码实现了一个损失计算的核心逻辑,主要功能是根据模型的预测结果和真实目标计算三个主要损失:类别损失(lcls)、边界框回归损失(lbox)和目标性损失(lobj)。通过有效的目标构建方法和损失计算过程,使得模型在训练过程中可以有效地优化目标检测任务。代码还包括动态平衡损失的功能,以提高训练的稳定性和效果。

在anchor回归时,对xywh进行了以下处理:

 # Regression
 pxy = ps[:, :2].sigmoid() * 2. - 0.5
 pwh = (ps[:, 2:4].sigmoid() * 2) ** 2 * anchors[i]

这和yolo.py Detect中的代码一致:

y = x[i].sigmoid()
y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i]  # xy
y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i]  # wh


可以先翻看yolov3论文中对于anchor box回归的介绍:

这里的bx∈[Cx,Cx+1],by∈[Cy,Cy+1],bw∈(0,+∞),bh∈(0,+∞)
而yolov5里这段公式变成了:

使得bx∈[Cx-0.5,Cx+1.5],by∈[Cy-0.5,Cy+1.5],bw∈[0,4pw],bh∈[0,4ph]。
这么做是为了消除网格敏感,因为sigmoid函数想取到0或1需要趋于正负无穷,这对网络训练来说是比较难取到的,所以通过往外扩大半个格子范围,是的格点边缘上的点也能取到。
这一策略可以提高召回率(因为每个grid的预测范围变大了),但会略微降低精确度,总体提升mAP。

1.3. 目标构建

def build_targets(self, p, targets):
        # Build targets for compute_loss(), input targets(image,class,x,y,w,h)
        '''
        na = 3,表示每个预测层anchors的个数
        targets 为一个batch中所有的标签,包括标签所属的image,以及class,x,y,w,h
        targets = [[image1,class1,x1,y1,w1,h1],
        		   [image2,class2,x2,y2,w2,h2],
        		   ...
        		   [imageN,classN,xN,yN,wN,hN]]
        nt为一个batch中所有标签的数量
        '''
        na, nt = self.na, targets.shape[0]  # number of anchors, targets
        tcls, tbox, indices, anch = [], [], [], []
        '''
        gain是为了最终将坐标所属grid坐标限制在坐标系内,不要超出范围,
        其中7是为了对应: image class x y w h ai,
        但后续代码只对x y w h赋值,x,y,w,h = nx,ny,nx,ny,
        nx和ny为当前输出层的grid大小。
        '''
        gain = torch.ones(7, device=targets.device)  # normalized to gridspace gain
        '''
        ai.shape = [na,nt]
        ai = [[0,0,0,.....],
        	  [1,1,1,...],
        	  [2,2,2,...]]
        这么做的目的是为了给targets增加一个属性,即当前标签所属的anchor索引
        '''
        ai = torch.arange(na, device=targets.device).float().view(na, 1).repeat(1, nt)  # same as .repeat_interleave(nt)
        '''
        targets.repeat(na, 1, 1).shape = [na,nt,6]
        ai[:, :, None].shape = [na,nt,1](None在list中的作用就是在插入维度1)
        ai[:, :, None] = [[[0],[0],[0],.....],
        	  			  [[1],[1],[1],...],
        	  	  		  [[2],[2],[2],...]]
        cat之后:
        targets.shape = [na,nt,7]
        targets = [[[image1,class1,x1,y1,w1,h1,0],
        			[image2,class2,x2,y2,w2,h2,0],
        			...
        			[imageN,classN,xN,yN,wN,hN,0]],
        			[[image1,class1,x1,y1,w1,h1,1],
        			 [image2,class2,x2,y2,w2,h2,1],
        			...],
        			[[image1,class1,x1,y1,w1,h1,2],
        			 [image2,class2,x2,y2,w2,h2,2],
        			...]]
        这么做是为了纪录每个label对应的anchor。
        '''
        targets = torch.cat((targets.repeat(na, 1, 1), ai[:, :, None]), 2)  # append anchor indices
		
		'''
		定义每个grid偏移量,会根据标签在grid中的相对位置来进行偏移
		'''
        g = 0.5  # bias
        '''
        [0, 0]代表中间,
		[1, 0] * g = [0.5, 0]代表往左偏移半个grid, [0, 1]*0.5 = [0, 0.5]代表往上偏移半个grid,与后面代码的j,k对应
		[-1, 0] * g = [-0.5, 0]代代表往右偏移半个grid, [0, -1]*0.5 = [0, -0.5]代表往下偏移半个grid,与后面代码的l,m对应
		具体原理在代码后讲述
        '''
        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

        for i in range(self.nl):
        	'''
        	原本yaml中加载的anchors.shape = [3,6],但在yolo.py的Detect中已经通过代码
        	a = torch.tensor(anchors).float().view(self.nl, -1, 2)
        	self.register_buffer('anchors', a) 
        	将anchors进行了reshape。
        	self.anchors.shape = [3,3,2]
        	anchors.shape = [3,2]
        	'''
            anchors = self.anchors[i]
            '''
            p.shape = [nl,bs,na,nx,ny,no]
            p[i].shape = [bs,na,nx,ny,no]
            gain = [1,1,nx,ny,nx,ny,1]
            '''
            gain[2:6] = torch.tensor(p[i].shape)[[3, 2, 3, 2]]  # xyxy gain

            # Match targets to anchors
            '''
            因为targets进行了归一化,默认在w = 1, h =1 的坐标系中,
            需要将其映射到当前输出层w = nx, h = ny的坐标系中。
            '''
            t = targets * gain
            if nt:
                # Matches
                '''
                t[:, :, 4:6].shape = [na,nt,2] = [3,nt,2],存放的是标签的w和h
                anchor[:,None] = [3,1,2]
                r.shape = [3,nt,2],存放的是标签和当前层anchor的长宽比
                '''
                r = t[:, :, 4:6] / anchors[:, None]  # wh ratio
                '''
                torch.max(r, 1. / r)求出最大的宽比和最大的长比,shape = [3,nt,2]
                再max(2)求出同一标签中宽比和长比较大的一个,shape = [2,3,nt],之所以第一个维度变成2,
                因为torch.max如果不是比较两个tensor的大小,而是比较1个tensor某一维度的大小,则会返回values和indices:
                	torch.return_types.max(
						values=tensor([...]),
						indices=tensor([...]))
                所以还需要加上索引0获取values,
                torch.max(r, 1. / r).max(2)[0].shape = [3,nt],
                将其和hyp.yaml中的anchor_t超参比较,小于该值则认为标签属于当前输出层的anchor
                j = [[bool,bool,....],[bool,bool,...],[bool,bool,...]]
                j.shape = [3,nt]
                '''
                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.shape = [na,nt,7] 
                j.shape = [3,nt]
                假设j中有NTrue个True值,则
                t[j].shape = [NTrue,7]
                返回的是na*nt的标签中,所有属于当前层anchor的标签。
                '''
                t = t[j]  # filter

                # Offsets
                '''
                下面这段代码和注释可以配合代码后的图片进行理解。
                t.shape = [NTrue,7] 
                7:image,class,x,y,h,w,ai
                gxy.shape = [NTrue,2] 存放的是x,y,相当于坐标到坐标系左边框和上边框的记录
                gxi.shape = [NTrue,2] 存放的是w-x,h-y,相当于测量坐标到坐标系右边框和下边框的距离
                '''
                gxy = t[:, 2:4]  # grid xy
                gxi = gain[[2, 3]] - gxy  # inverse
                '''
                因为grid单位为1,共nx*ny个gird
                gxy % 1相当于求得标签在第gxy.long()个grid中以grid左上角为原点的相对坐标,
                gxi % 1相当于求得标签在第gxy.long()个grid中以grid右下角为原点的相对坐标,
                下面这两行代码作用在于
                筛选中心坐标 左、上方偏移量小于0.5,并且中心点大于1的标签
                筛选中心坐标 右、下方偏移量小于0.5,并且中心点大于1的标签          
                j.shape = [NTrue], j = [bool,bool,...]
                k.shape = [NTrue], k = [bool,bool,...]
                l.shape = [NTrue], l = [bool,bool,...]
                m.shape = [NTrue], m = [bool,bool,...]
                '''
                j, k = ((gxy % 1. < g) & (gxy > 1.)).T 
                l, m = ((gxi % 1. < g) & (gxi > 1.)).T  
                '''
                j.shape = [5,NTrue]
                t.repeat之后shape为[5,NTrue,7], 
                通过索引j后t.shape = [NOff,7],NOff表示NTrue + (j,k,l,m中True的总数量)
                torch.zeros_like(gxy)[None].shape = [1,NTrue,2]
                off[:, None].shape = [5,1,2]
                相加之和shape = [5,NTrue,2]
                通过索引j后offsets.shape = [NOff,2]
                这段代码的表示当标签在grid左侧半部分时,会将标签往左偏移0.5个grid,上下右同理。
                '''   
                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
            
            '''
            t.shape = [NOff,7],(image,class,x,y,w,h,ai)
            '''
            b, c = t[:, :2].long().T  # image, class
            gxy = t[:, 2:4]  # grid xy
            gwh = t[:, 4:6]  # grid wh
            '''
            offsets.shape = [NOff,2]
            gxy - offsets为gxy偏移后的坐标,
            gxi通过long()得到偏移后坐标所在的grid坐标
            '''
            gij = (gxy - offsets).long()
            gi, gj = gij.T  # grid xy indices

            # Append
            '''
            a:所有anchor的索引 shape = [NOff]
            b:标签所属image的索引 shape = [NOff]
            gj.clamp_(0, gain[3] - 1)将标签所在grid的y限定在0到ny-1之间
            gi.clamp_(0, gain[2] - 1)将标签所在grid的x限定在0到nx-1之间
            indices = [image, anchor, gridy, gridx] 最终shape = [nl,4,NOff]
            tbox存放的是标签在所在grid内的相对坐标,∈[0,1] 最终shape = [nl,NOff]
            anch存放的是anchors 最终shape = [nl,NOff,2]
            tcls存放的是标签的分类 最终shape = [nl,NOff]
            '''
            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
            anch.append(anchors[a])  # anchors
            tcls.append(c)  # class

        return tcls, tbox, indices, anch

  • 构建损失计算需要的目标。根据输入目标和锚点生成对应的目标信息。
  • 该方法计算每个锚点与目标之间的匹配关系,并生成相应的索引和框的边界。

这个函数是用来为所有GT筛选相应的anchor正样本。筛选条件是比较GT和anchor的宽比和高比,大于一定的阈值就是负样本,反之正样本。筛选到的正样本信息(image_index, anchor_index, gridy, gridx),传入call函数,通过这个信息去筛选pred每个grid预测得到的信息,保留对应grid_cell上的正样本。通过build_targets筛选的GT中的正样本和pred筛选出的对应位置的预测样本进行计算损失。

补充理解:这个函数的目的是为了每个gt匹配相应的高质量anchor正样本参与损失计算,j = torch.max(r, 1. / r).max(2)[0] < self.hyp[‘anchor_t’]这步的比较是为了将gt分配到不同层上去检测(和你说的差不多),后面的步骤是为了将确定在这层检测的gt中心坐标,进而确定这个gt在这层哪个grid cell进行检测。做到这一步也就做到了为每个gt匹配anchor正样本的目的。

该代码片段是用来构建目标数据的,主要用于YOLO(You Only Look Once)目标检测模型中的损失计算。函数名为 build_targets,其输入为模型的预测 p 和真实目标 targets

"""所有GT筛选相应的anchor正样本
        Build targets for compute_loss()
        :params p: p[i]的作用只是得到每个feature map的shape
                   预测框 由模型构建中的三个检测头Detector返回的三个yolo层的输出
                   tensor格式 list列表 存放三个tensor 对应的是三个yolo层的输出
                   如: [4, 3, 112, 112, 85]、[4, 3, 56, 56, 85]、[4, 3, 28, 28, 85]
                   [bs, anchor_num, grid_h, grid_w, xywh+class+classes]
                   可以看出来这里的预测值p是三个yolo层每个grid_cell(每个grid_cell有三个预测值)的预测值,后面肯定要进行正样本筛选
        :params targets: 数据增强后的真实框 [63, 6] [num_target,  image_index+class+xywh] xywh为归一化后的框
        :return tcls: 表示这个target所属的class index
                tbox: xywh 其中xy为这个target对当前grid_cell左上角的偏移量
                indices: b: 表示这个target属于的image index
                         a: 表示这个target使用的anchor index
                        gj: 经过筛选后确定某个target在某个网格中进行预测(计算损失)  gj表示这个网格的左上角y坐标
                        gi: 表示这个网格的左上角x坐标
                anch: 表示这个target所使用anchor的尺度(相对于这个feature map)  注意可能一个target会使用大小不同anchor进行计算
        """
  1. 初始化参数

    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 = torch.arange(na, device=self.device).float().view(na, 1).repeat(1, nt)  # same as .repeat_interleave(nt)
    

    这段代码是 ComputeLoss 类中的一部分,用于构建目标(targets),以便在计算损失时使用。以下是对每一行代码的逐步分解和详细解释:

    na, nt = self.na, targets.shape[0] # number of anchors, targets

    这里获取了模型中的锚框个数 naself.na)和目标的数量 nt(用 targets 张量的第一维来表示,通常代表样本数量)。

    tcls, tbox, indices, anch = [], [], [], []

    初始化四个空列表:tcls 将用来存放目标的类别,tbox 用来存放目标的边界框(bounding box),indices 用于存放与每个目标相关的索引,anch 用来存放与目标对应的锚框。

    # gain是为了后面将targets=[na,nt,7]中的归一化了的xywh映射到相对feature map尺度上
    # 7: image_index+class+xywh+anchor_index
    gain = torch.ones(7, device=targets.device)

    创建一个大小为 7 的张量 gain,其中每个元素初始化为 1,并将其放置在模型使用的设备上(如 GPU)。这个张量将用于将目标转换为网络的网格空间(gridspace)。

    # 需要在3个anchor上都进行训练 所以将标签赋值na=3个  ai代表3个anchor上在所有的target对应的anchor索引 就是用来标记下当前这个target属于哪个anchor
    # [1, 3] -> [3, 1] -> [3, 63]=[na, nt]   三行  第一行63个0  第二行63个1  第三行63个2
    ai = torch.arange(na, device=targets.device).float().view(na, 1).repeat(1, nt)  # same as .repeat_interleave(nt)

    创建一个张量 ai,包含从 0 到 na-1 的所有锚框索引。这里使用 view 和 repeat 方法将这些索引调整为一个 na x nt 的张量,使得每一列都代表同一个锚框,便于后续的计算。

    这段代码的主要功能是为目标生成锚框索引,并准备目标数据,以便可以在计算损失时使用。它通过获取目标的类别和边界框信息,以及锚框的索引,建立了一个符合网络输入格式的张量。这是深度学习中一个重要的步骤,因为模型需要知道每个预测与哪些真实目标相对应,以便能够有效地计算损失并进行学习。

    • na: 指锚框(anchors)的数量。
    • nt: 目标的数量。
    • tcls, tbox, indices, anch: 初始化列表,用于存储目标类别、边框信息、索引和锚框。
    • gain: 创建一个长度为7的张量,值为1,用于之后的归一化操作。
    • ai: 创建一个张量,包含锚框索引,并重复 nt 次。
  2. 处理目标数据

    # [63, 6] [3, 63] -> [3, 63, 6] [3, 63, 1] -> [3, 63, 7]  7: [image_index+class+xywh+anchor_index]
    # 对每一个feature map: 这一步是将target复制三份 对应一个feature map的三个anchor
    # 先假设所有的target都由这层的三个anchor进行检测(复制三份)  再进行筛选  并将ai加进去标记当前是哪个anchor的target
    targets = torch.cat((targets.repeat(na, 1, 1), ai[:, :, None]), 2)  # append anchor indices
    • 这一行将目标张量 targets 复制 na 次,以便为每个锚框生成一个对应的目标。接着,通过 torch.cat 将复制后的 targets 和 ai 张量沿最后一个维度(即添加锚框索引)拼接在一起。这样,每个目标现在都有了对应的锚框索引,有助于后续的损失计算。
  3. 定义偏移量

    # 这两个变量是用来扩展正样本的 因为预测框预测到target有可能不止当前的格子预测到了
    # 可能周围的格子也预测到了高质量的样本 我们也要把这部分的预测信息加入正样本中
    g = 0.5  # bias  中心偏移  用来衡量target中心点离哪个格子更近
    # 以自身 + 周围左上右下4个网格 = 5个网格  用来计算offsets
    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
    • g: 偏移量的基准值。
    • off: 定义了一组偏移量,用于后续的网格匹配。
  4. 遍历每个尺度的锚框

     # 遍历三个feature 筛选gt的anchor正样本
            for i in range(self.nl):  # self.nl: number of detection layers   Detect的个数 = 3
                # anchors: 当前feature map对应的三个anchor尺寸(相对feature map)  [3, 2]
                anchors = self.anchors[i]
    
                # gain: 保存每个输出feature map的宽高 -> gain[2:6]=gain[whwh]
                # [1, 1, 1, 1, 1, 1, 1] -> [1, 1, 112, 112, 112,112, 1]=image_index+class+xywh+anchor_index
                gain[2:6] = torch.tensor(p[i].shape)[[3, 2, 3, 2]]  # xyxy gain
    
    • 遍历所有预测层,获取当前尺度的锚框和预测形状,并更新 gain 中关于尺寸的部分。
  5. 匹配目标到锚框

    # t = [3, 63, 7]  将target中的xywh的归一化尺度放缩到相对当前feature map的坐标尺度
                #     [3, 63, image_index+class+xywh+anchor_index]
                t = targets * gain
    
                if nt:  # 开始匹配  Matches
                    # t=[na, nt, 7]   t[:, :, 4:6]=[na, nt, 2]=[3, 63, 2]
                    # anchors[:, None]=[na, 1, 2]
                    # r=[na, nt, 2]=[3, 63, 2]
                    # 所有的gt与当前层的三个anchor的宽高比(w/w  h/h)
                    r = t[:, :, 4:6] / anchors[:, None]  # wh ratio (w/w  h/h)
    
                    # 筛选条件  GT与anchor的宽比或高比超过一定的阈值 就当作负样本
                    # torch.max(r, 1. / r)=[3, 63, 2] 筛选出宽比w1/w2 w2/w1 高比h1/h2 h2/h1中最大的那个
                    # .max(2)返回宽比 高比两者中较大的一个值和它的索引  [0]返回较大的一个值
                    # j: [3, 63]  False: 当前anchor是当前gt的负样本  True: 当前anchor是当前gt的正样本
                    j = torch.max(r, 1. / r).max(2)[0] < self.hyp['anchor_t']  # compare
                    # yolov3 v4的筛选方法: wh_iou  GT与anchor的wh_iou超过一定的阈值就是正样本
                    # j = wh_iou(anchors, t[:, 4:6]) > model.hyp['iou_t']  # iou(3,n)=wh_iou(anchors(3,2), gwh(n,2))
    
                    # 根据筛选条件j, 过滤负样本, 得到所有gt的anchor正样本(batch_size张图片)
                    # 知道当前gt的坐标 属于哪张图片 正样本对应的idx 也就得到了当前gt的正样本anchor
                    # t: [3, 63, 7] -> [126, 7]  [num_Positive_sample, image_index+class+xywh+anchor_index]
                    t = t[j]  # filter
    
                    # Offsets 筛选当前格子周围格子 找到2个离target中心最近的两个格子  可能周围的格子也预测到了高质量的样本 我们也要把这部分的预测信息加入正样本中
                    # 除了target所在的当前格子外, 还有2个格子对目标进行检测(计算损失) 也就是说一个目标需要3个格子去预测(计算损失)
                    # 首先当前格子是其中1个 再从当前格子的上下左右四个格子中选择2个 用这三个格子去预测这个目标(计算损失)
                    # feature map上的原点在左上角 向右为x轴正坐标 向下为y轴正坐标
                    gxy = t[:, 2:4]  # grid xy 取target中心的坐标xy(相对feature map左上角的坐标)
                    gxi = gain[[2, 3]] - gxy  # inverse  得到target中心点相对于右下角的坐标  gain[[2, 3]]为当前feature map的wh
                    # 筛选中心坐标 距离当前grid_cell的左、上方偏移小于g=0.5 且 中心坐标必须大于1(坐标不能在边上 此时就没有4个格子了)
                    # j: [126] bool 如果是True表示当前target中心点所在的格子的左边格子也对该target进行回归(后续进行计算损失)
                    # k: [126] bool 如果是True表示当前target中心点所在的格子的上边格子也对该target进行回归(后续进行计算损失)
                    j, k = ((gxy % 1. < g) & (gxy > 1.)).T
                    # 筛选中心坐标 距离当前grid_cell的右、下方偏移小于g=0.5 且 中心坐标必须大于1(坐标不能在边上 此时就没有4个格子了)
                    # l: [126] bool 如果是True表示当前target中心点所在的格子的右边格子也对该target进行回归(后续进行计算损失)
                    # m: [126] bool 如果是True表示当前target中心点所在的格子的下边格子也对该target进行回归(后续进行计算损失)
                    l, m = ((gxi % 1. < g) & (gxi > 1.)).T
                    # j: [5, 126]  torch.ones_like(j): 当前格子, 不需要筛选全是True  j, k, l, m: 左上右下格子的筛选结果
                    j = torch.stack((torch.ones_like(j), j, k, l, m))
                    # 得到筛选后所有格子的正样本 格子数<=3*126 都不在边上等号成立
                    # t: [126, 7] -> 复制5份target[5, 126, 7]  分别对应当前格子和左上右下格子5个格子
                    # j: [5, 126] + t: [5, 126, 7] => t: [378, 7] 理论上是小于等于3倍的126 当且仅当没有边界的格子等号成立
                    t = t.repeat((5, 1, 1))[j]
                    # torch.zeros_like(gxy)[None]: [1, 126, 2]   off[:, None]: [5, 1, 2]  => [5, 126, 2]
                    # j筛选后: [378, 2]  得到所有筛选后的网格的中心相对于这个要预测的真实框所在网格边界(左右上下边框)的偏移量
                    offsets = (torch.zeros_like(gxy)[None] + off[:, None])[j]
                else:
                    t = targets[0]
                    offsets = 0
    • 将 targets 乘以 gain 进行适当的尺寸调整。
    • 根据宽高比(wh ratio)过滤出与锚框匹配的目标。
  6. 准备要返回的数据

    # Define
                b, c = t[:, :2].long().T  # image_index, class
                gxy = t[:, 2:4]  # target的xy
                gwh = t[:, 4:6]  # target的wh
                gij = (gxy - offsets).long()   # 预测真实框的网格所在的左上角坐标(有左上右下的网格)    
                gi, gj = gij.T  # grid xy indices
    
    • 将目标数据分块为图像索引、类别、网格坐标、宽高和锚框索引。
    • 计算最终的网格索引。
  7. 存储匹配的目标信息

    # Append
                a = t[:, 6].long()  # anchor index
                # b: image index  a: anchor index  gj: 网格的左上角y坐标  gi: 网格的左上角x坐标
                indices.append((b, a, gj.clamp_(0, gain[3] - 1), gi.clamp_(0, gain[2] - 1)))
                # tbix: xywh 其中xy为这个target对当前grid_cell左上角的偏移量
                tbox.append(torch.cat((gxy - gij, gwh), 1))  # box
                anch.append(anchors[a])  # 对应的所有anchors
                tcls.append(c)  # class
    
  8. 返回结果

    return tcls, tbox, indices, anch
    

该函数的主要功能是为YOLO模型构建目标数据。它根据给定的真实目标(包含图像索引、类别、位置等信息)和预测,匹配锚框、计算网格位置、宽高和类别,并最终返回目标类别、目标边框、索引和锚框信息。这些数据将在后续的损失计算中使用,帮助模型优化其参数以提高预测性能。

1.4 总结

ComputeLoss 类主要用于在深度学习目标检测模型的训练过程中计算损失。它通过结合二元交叉熵损失、IoU损失和焦点损失,根据模型的预测值和实际目标来不断调整模型权重。类中的方法支持自适应损失权重调整,并且能够构建与锚点匹配的目标数据,为模型的训练提供精准的优化信息。

二、损失函数

2.1 smooth_BCE

def smooth_BCE(eps=0.1):  # https://github.com/ultralytics/yolov3/issues/238#issuecomment-598028441
    # return positive, negative label smoothing BCE targets
    return 1.0 - 0.5 * eps, 0.5 * eps

 这段代码定义了一个名为 smooth_BCE 的函数,用于实现标签平滑(label smoothing)技术,目的是在训练深度学习模型时,减轻模型对某些标签的过度自信,从而提高模型的泛化能力。

  1. 函数定义:

    def smooth_BCE(eps=0.1):
    

    smooth_BCE 是一个函数,接受一个参数 eps,默认值为 0.1eps 的值代表标签平滑的程度。

  2. 注释:

    # https://github.com/ultralytics/yolov3/issues/238#issuecomment-598028441
    

    这是一个链接注释,指向一个 GitHub 问题的详细描述,提供了更多关于标签平滑的背景信息。

  3. 函数功能描述:

    # return positive, negative label smoothing BCE targets
    

    该注释表明函数的作用是返回经过平滑处理的正样本和负样本的标签。

  4. 平滑计算:

    return 1.0 - 0.5 * eps, 0.5 * eps
    
    • 这行代码计算并返回两个值:平滑后的正样本标签和负样本标签。
      • 正样本标签:1.0 - 0.5 * eps,即预测为正类的样本,标签被平滑处理减少为 1 - 0.5 * eps 的值,目的是让模型在输出时不完全确定。
      • 负样本标签:0.5 * eps,即预测为负类的样本,标签设置为一个较小的正值 0.5 * eps,此举防止模型过于自信地将样本识别为负类。

smooth_BCE 函数主要用于实现标签平滑的机制,通过调整正类和负类的标签来降低模型对标签的过度自信。这种方法有助于提高训练过程中模型的泛化能力,减少其对训练数据的过拟合。函数返回经过平滑处理的正负标签,可以在后续计算损失时直接使用。

2.2 函数BCEBlurWithLogitsLoss

class BCEBlurWithLogitsLoss(nn.Module):
    # BCEwithLogitLoss() with reduced missing label effects.
    def __init__(self, alpha=0.05):
        super().__init__()
        self.loss_fcn = nn.BCEWithLogitsLoss(reduction='none')  # must be nn.BCEWithLogitsLoss()
        self.alpha = alpha

    def forward(self, pred, true):
        loss = self.loss_fcn(pred, true)
        pred = torch.sigmoid(pred)  # prob from logits
        dx = pred - true  # reduce only missing label effects
        # dx = (pred - true).abs()  # reduce missing label and false label effects
        alpha_factor = 1 - torch.exp((dx - 1) / (self.alpha + 1e-4))
        loss *= alpha_factor
        return loss.mean()

这段代码定义了一个名为 BCEBlurWithLogitsLoss 的损失函数类,继承自 torch.nn.Module。该类实现了一种改进的二元交叉熵损失,旨在减少在缺失标签情况下的损失影响。以下是对代码的逐步分解和详细解释:

类的构造函数 __init__

def __init__(self, alpha=0.05):
    super().__init__()
    self.loss_fcn = nn.BCEWithLogitsLoss(reduction='none')  # must be nn.BCEWithLogitsLoss()
    self.alpha = alpha
  • alpha 参数: 这是一个超参数,默认为 0.05。它在计算调整系数时使用。
  • super().__init__(): 调用父类 nn.Module 的构造函数,初始化模块。
  • self.loss_fcn: 使用 PyTorch 提供的 BCEWithLogitsLoss 函数,其特性是可以处理未经过 sigmoid 激活的 logits,并且设定 reduction='none' 以便在计算损失时不对损失值进行任何聚合。

前向传播函数 forward

def forward(self, pred, true):
    loss = self.loss_fcn(pred, true)
    pred = torch.sigmoid(pred)  # prob from logits
    dx = pred - true  # reduce only missing label effects
    # dx = (pred - true).abs()  # reduce missing label and false label effects
    alpha_factor = 1 - torch.exp((dx - 1) / (self.alpha + 1e-4))
    loss *= alpha_factor
    return loss.mean()
  1. 计算初始损失:

    loss = self.loss_fcn(pred, true)
    

    使用 self.loss_fcn(即 BCEWithLogitsLoss)计算模型预测 pred 和真实标签 true 之间的损失。

  2. 应用 Sigmoid 激活:

    pred = torch.sigmoid(pred)  # prob from logits
    

    将 logits 转换为概率值,后续计算将基于这些概率进行。

  3. 计算损失差异:

    dx = pred - true  # reduce only missing label effects
    

    计算预测值与真实值之间的差异 dx。当真实标签缺失时,该值的计算有助于调整损失。

  4. 计算调整因子 alpha_factor:

    alpha_factor = 1 - torch.exp((dx - 1) / (self.alpha + 1e-4))
    

    这个因子用于调整损失,主要目的是减少缺失标签的负面影响。当 dx 为负值时,损失因子会增大,反之减小。

  5. 调整损失值:

    loss *= alpha_factor
    

    将计算得到的 alpha_factor 应用到损失上,以获得最终损失值。

  6. 返回平均损失:

    return loss.mean()
    

    返回计算出的损失的平均值,作为此批次的最终损失。

总结

BCEBlurWithLogitsLoss 类实现了一种改进的二元交叉熵损失函数,其中主要功能是通过调整损失值来减少由于缺失标签导致的负面影响。该损失函数在处理分类任务时,能够提高模型在标签不完全情况下的鲁棒性。通过使用 alpha 超参数,用户可以控制损失函数对缺失标签的敏感程度,从而优化模型性能。

 2.3 FocalLoss

class FocalLoss(nn.Module):
    # Wraps focal loss around existing loss_fcn(), i.e. criteria = FocalLoss(nn.BCEWithLogitsLoss(), gamma=1.5)
    def __init__(self, loss_fcn, gamma=1.5, alpha=0.25):
        super().__init__()
        self.loss_fcn = loss_fcn  # must be nn.BCEWithLogitsLoss()
        self.gamma = gamma
        self.alpha = alpha
        self.reduction = loss_fcn.reduction
        self.loss_fcn.reduction = 'none'  # required to apply FL to each element

    def forward(self, pred, true):
        loss = self.loss_fcn(pred, true)
        # p_t = torch.exp(-loss)
        # loss *= self.alpha * (1.000001 - p_t) ** self.gamma  # non-zero power for gradient stability

        # TF implementation https://github.com/tensorflow/addons/blob/v0.7.1/tensorflow_addons/losses/focal_loss.py
        pred_prob = torch.sigmoid(pred)  # prob from logits
        p_t = true * pred_prob + (1 - true) * (1 - pred_prob)
        alpha_factor = true * self.alpha + (1 - true) * (1 - self.alpha)
        modulating_factor = (1.0 - p_t) ** self.gamma
        loss *= alpha_factor * modulating_factor

        if self.reduction == 'mean':
            return loss.mean()
        elif self.reduction == 'sum':
            return loss.sum()
        else:  # 'none'
            return loss

代码逐步分解与详细解释:

  1. 类定义与初始化 (__init__ 方法)

    • class FocalLoss(nn.Module)::定义了一个名为 FocalLoss 的类,该类继承自 nn.Module,这是 PyTorch 中所有神经网络模块的基类。
    • def __init__(self, loss_fcn, gamma=1.5, alpha=0.25)::构造函数接收三个参数:
      • loss_fcn:传入的损失函数,通常为 nn.BCEWithLogitsLoss()(二元交叉熵损失与 logits 结合使用)。
      • gamma:控制给定样本的损失减小程度的指数,用于强调难分类样本的损失(默认值为 1.5)。
      • alpha:用于平衡正负样本的权重(默认值为 0.25)。
    • self.loss_fcn = loss_fcn:将传入的损失函数保存为实例变量。
    • self.reduction = loss_fcn.reduction:保存传入损失函数的归约方式(例如,'mean'、'sum'或'none')。
    • self.loss_fcn.reduction = 'none':设置损失函数在计算时不进行任何自动归约,以便逐元素计算损失。
  2. 前向传播 (forward 方法)

    • def forward(self, pred, true)::定义了模型的前向传播方法,接收预测值 pred 和真实标签 true
    • loss = self.loss_fcn(pred, true):计算基础损失值,即使用传入的损失函数计算预测值与真实标签之间的损失。
    • pred_prob = torch.sigmoid(pred):将预测的值应用 sigmoid 函数,将 logits 转换为概率。
    • p_t = true * pred_prob + (1 - true) * (1 - pred_prob):计算有效的概率值,p_t 代表正确类的概率。
    • alpha_factor = true * self.alpha + (1 - true) * (1 - self.alpha):根据真实标签计算加权因子,根据正负样本应用不同的权重。
    • modulating_factor = (1.0 - p_t) ** self.gamma:计算调制因子,使用 gamma 指数调整损失,强调难以分类的样本。
    • loss *= alpha_factor * modulating_factor:将基础损失乘以加权因子和调制因子。
  3. 损失归约

    • 通过条件判断,依据原始损失函数的归约方式返回最终的损失:
      • if self.reduction == 'mean'::若设置为求平均值,返回损失的平均值。
      • elif self.reduction == 'sum'::若设置为求和,返回损失的总和。
      • else::若不进行任何归约,直接返回未归约的损失。

总结:

FocalLoss 类实现了一种焦点损失(Focal Loss),旨在处理类别不平衡问题,特别用于处理难以分类的样本。它通过调制损失函数来强调较难分类样本的权重,同时减小那些容易分类样本的影响。这种机制通过调整损失计算,以便训练过程中更关注那些具有挑战性的样本,从而提升模型的整体性能。该类还允许在使用时自定义其他损失函数,适用于特定的任务需求。

2.4 QFocalLoss

class QFocalLoss(nn.Module):
    # Wraps Quality focal loss around existing loss_fcn(), i.e. criteria = FocalLoss(nn.BCEWithLogitsLoss(), gamma=1.5)
    def __init__(self, loss_fcn, gamma=1.5, alpha=0.25):
        super().__init__()
        self.loss_fcn = loss_fcn  # must be nn.BCEWithLogitsLoss()
        self.gamma = gamma
        self.alpha = alpha
        self.reduction = loss_fcn.reduction
        self.loss_fcn.reduction = 'none'  # required to apply FL to each element

    def forward(self, pred, true):
        loss = self.loss_fcn(pred, true)

        pred_prob = torch.sigmoid(pred)  # prob from logits
        alpha_factor = true * self.alpha + (1 - true) * (1 - self.alpha)
        modulating_factor = torch.abs(true - pred_prob) ** self.gamma
        loss *= alpha_factor * modulating_factor

        if self.reduction == 'mean':
            return loss.mean()
        elif self.reduction == 'sum':
            return loss.sum()
        else:  # 'none'
            return loss

这段代码定义了一个名为QFocalLoss的类,继承自nn.Module,用于实现一种特殊的损失函数,称为质量焦点损失(Quality Focal Loss)。下面是对该代码的逐步分解和详细解释:

1. 类定义和初始化方法

class QFocalLoss(nn.Module):
    def __init__(self, loss_fcn, gamma=1.5, alpha=0.25):
        super().__init__()
        self.loss_fcn = loss_fcn  # must be nn.BCEWithLogitsLoss()
        self.gamma = gamma
        self.alpha = alpha
        self.reduction = loss_fcn.reduction
        self.loss_fcn.reduction = 'none'  # required to apply FL to each element
  • 类定义QFocalLoss类的定义,其构造函数__init__接受损失函数loss_fcn(应为nn.BCEWithLogitsLoss),焦点损失的调节因子gamma,以及平衡因子alpha
  • 初始化:在构造函数中,将传入的参数存储为类的属性。loss_fcn.reduction属性控制损失的缩减方式,但在这里设置为'none',以便于对每个元素进行独立计算。

2. 前向传播方法

def forward(self, pred, true):
    loss = self.loss_fcn(pred, true)
  • 前向传播方法forward接收预测值pred和真实值true
  • loss计算使用传入的损失函数self.loss_fcn,基于predtrue

3. 概率计算和损失调整

pred_prob = torch.sigmoid(pred)  # prob from logits
alpha_factor = true * self.alpha + (1 - true) * (1 - self.alpha)
modulating_factor = torch.abs(true - pred_prob) ** self.gamma
loss *= alpha_factor * modulating_factor
  • 概率计算:通过torch.sigmoid将模型的原始预测值(logits)转换为概率值。
  • 平衡因子alpha_factor用于调整不同类别损失的权重,真正的类乘以alpha,反之则乘以1 - alpha
  • 调制因子modulating_factor用于调整损失的影响程度,基于真实标签与预测概率之间的绝对差值进行计算,结果的gamma次方调制损失。
  • 损失调整:最终的损失loss乘以两个因子,使得分类较困难的样本获得更大的损失值,从而引导模型关注这些样本。

4. 损失返回

if self.reduction == 'mean':
    return loss.mean()
elif self.reduction == 'sum':
    return loss.sum()
else:  # 'none'
    return loss
  • 根据类属性self.reduction决定损失的返回方式:
    • 'mean':返回损失的均值。
    • 'sum':返回损失的总和。
    • 'none':返回原始的未缩减损失。

总结

QFocalLoss类实现了质量焦点损失(Quality Focal Loss),是一种对每个样本元素独立计算损失的方法。该类通过对传统的二元交叉熵损失(BCEWithLogitsLoss)进行扩展,加入了两个因子来调整损失:alpha_factor平衡不同类别的重要性,modulating_factor则根据模型的预测准确性来调节损失权重。因此,QFocalLoss的主要功能在于改善模型在类别不平衡数据集上的训练效果,增强其对困难样本的关注能力。

 

 参考:

  1. 从YOLOv5源码loss.py详细介绍Yolov5的损失函数
  2. YOLOv5系列(十) 解析损失部分loss(详尽)

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值