You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

锚框检测器损失函数异常排查:分类损失无变化

锚框检测器损失函数问题

问题背景

我正在梳理带锚框(anchors)的检测器模型的学习过程,目前遇到了损失函数相关问题。模型的分类头、回归头以及锚框生成模块均已验证正常,但尝试多种损失函数变体后仍未解决问题。模型采用MobileNet作为特征骨干网络,连接两个适配头以输出回归和分类结果。

初始损失类代码

class Losses(nn.Module):
    def __init__(self, cls_loss=nn.BCEWithLogitsLoss(), reg_loss=nn.SmoothL1Loss(), threshold=0.1, alpha=1.0, beta=1.0):
        super().__init__()
        self.threshold = threshold
        self.cls_loss = cls_loss
        self.reg_loss = reg_loss
        self.alpha = alpha
        self.beta = beta

    def forward(self, bboxes, pred_offsets, predictions, groundtruth):
        """
        bboxes - bounding boxes, shape [bs, X, 4]
        pred_offsets - offset values, shape [bs, X, 4],
        predictions - prediction of target object in bbox, shape [bs, X], 
        groundtruth - real boxes, shape [bs, y, 4]. The structure is [xc, yc, w, h]
        """
        assert bboxes.shape[0] == groundtruth.shape[0], "Mismatch in batch sizes"

        distance, probs, giou_losses = [], [], []
        for i in range(bboxes.shape[0]):
            dist, prob, giou = self._overlap(bboxes[i], groundtruth[i])
            distance.append(dist)
            probs.append(prob)
            giou_losses.append(giou)

        distance = torch.stack(distance)
        probs = torch.stack(probs)
        giou_loss = torch.stack(giou_losses).mean()  

        # Classification loss
        cls_loss = self.cls_loss(predictions, probs)
        # Regression loss
        reg_loss = self.reg_loss(distance, torch.zeros_like(predictions[0]))
        # Weighted total loss
        total_loss = self.alpha * cls_loss + self.beta * reg_loss + giou_loss
        return total_loss, cls_loss, reg_loss, giou_loss

    def _overlap(self, bboxes, groundtruth):
        distance = torch.zeros(len(bboxes)).to(bboxes.device)
        probs = torch.zeros(len(bboxes)).to(bboxes.device)
        giou_loss = torch.zeros(len(bboxes)).to(bboxes.device)

        for gt in groundtruth:
            if torch.any(gt[:4] != 0):
                x1, y1, x2, y2 = to_corners(gt[:4])
                bx1, by1, bx2, by2 = to_corners_batch(bboxes)
                iou, dist = self._intersect(x1, y1, x2, y2, bx1, by1, bx2, by2)
                giou = self.generalized_iou(
                    torch.stack([bx1, by1, bx2, by2], dim=-1),
                    torch.tensor([x1, y1, x2, y2], device=bboxes.device)
                )
                mask = (iou >= self.threshold)
                probs[mask] = iou[mask]  # Use IoU as probabilities
                distance[mask] = dist[mask]
                giou_loss[mask] = (1 - giou[mask])  # GIoU values

        probs[probs == 0] = 0.01  # small noise
        return distance, probs, giou_loss

    @staticmethod
    def generalized_iou(bboxes1, bboxes2):
        x1_min = torch.min(bboxes1[:, 0], bboxes2[0])
        y1_min = torch.min(bboxes1[:, 1], bboxes2[1])
        x2_max = torch.max(bboxes1[:, 2], bboxes2[2])
        y2_max = torch.max(bboxes1[:, 3], bboxes2[3])

        inter_x1 = torch.max(bboxes1[:, 0], bboxes2[0])
        inter_y1 = torch.max(bboxes1[:, 1], bboxes2[1])
        inter_x2 = torch.min(bboxes1[:, 2], bboxes2[2])
        inter_y2 = torch.min(bboxes1[:, 3], bboxes2[3])

        inter_area = torch.clamp(inter_x2 - inter_x1, min=0) * torch.clamp(inter_y2 - inter_y1, min=0)
        bbox1_area = (bboxes1[:, 2] - bboxes1[:, 0]) * (bboxes1[:, 3] - bboxes1[:, 1])
        bbox2_area = (bboxes2[2] - bboxes2[0]) * (bboxes2[3] - bboxes2[1])

        union_area = bbox1_area + bbox2_area - inter_area
        iou = inter_area / (union_area + 1e-6)

        # Enclosing box area
        enclosing_area = (x2_max - x1_min) * (y2_max - y1_min)
        giou = iou - (enclosing_area - union_area) / (enclosing_area + 1e-6)
        return giou

    def _intersect(self, x1, y1, x2, y2, bx1, by1, bx2, by2):
        x_inter_min = torch.max(x1, bx1)
        y_inter_min = torch.max(y1, by1)
        x_inter_max = torch.min(x2, bx2)
        y_inter_max = torch.min(y2, by2)

        inter_width = torch.clamp(x_inter_max - x_inter_min, min=0)
        inter_height = torch.clamp(y_inter_max - y_inter_min, min=0)
        intersection_area = inter_width * inter_height

        union_area = ((x2 - x1) * (y2 - y1)) + ((bx2 - bx1) * (by2 - by1)) - intersection_area
        iou = intersection_area / (union_area + 1e-6)

        dist = torch.sqrt((bx2 - x2) ** 2 + (by2 - y2) ** 2) + torch.sqrt((bx1 - x1) ** 2 + (by1 - y1) ** 2)
        return iou, dist


def to_corners_batch(batch):
    cx, cy, w, h = batch[..., 0], batch[..., 1], batch[..., 2], batch[..., 3]
    bx1 = cx - w / 2
    by1 = cy - h / 2
    bx2 = cx + w / 2
    by2 = cy + h / 2
    return bx1, by1, bx2, by2


def to_corners(box):
    cx, cy, w, h = box[0], box[1], box[2], box[3]
    bx1 = cx - w / 2
    by1 = cy - h / 2
    bx2 = cx + w / 2
    by2 = cy + h / 2
    return bx1, by1, bx2, by2

修改后的损失函数代码

def forward(self, anchors, preds, gt_bboxes, offset):
    probs = preds['probs']  
    bboxes = preds['bboxes']  
    device = anchors.device

    cls_pos_targets = torch.zeros_like(probs, device=device)  
    cls_neg_targets = torch.zeros_like(probs, device=device)  
    reg_targets = torch.zeros_like(bboxes, device=device)   
    reg_mask = torch.zeros_like(probs, device=device)  

    iou_loss = 0
    reg_loss = 0
    cls_loss = 0
    
    bs = anchors.shape[0]
    for b in range(bs):
        gt = gt_bboxes[b]  
        gt = gt[~torch.all(gt == 0, dim=-1)]  
        if len(gt) == 0:
            continue  # if we do not have groundtruth
            
        ious = self.compute_iou(bboxes[b], gt)  # calc IoU between bboxes and groundtruth

        max_ious, gt_indices = ious.max(dim=1) 
        
        # Positive boxes
        pos_mask = max_ious >= self.pos_trh
        iou_loss += (1 - max_ious[pos_mask].sum()/pos_mask.sum()) if pos_mask.sum() > 0 else 0
        neg_mask = max_ious < self.neg_trh
        # update pos and neg targets
        cls_pos_targets[b, pos_mask] = 1
        cls_neg_targets[b, neg_mask] = 1

        # regression targets (use only positive anchors, boxes and groundtruth)
        if pos_mask.sum() > 0:
            pos_anchors = anchors[b][pos_mask]
            pos_gt = gt[gt_indices[pos_mask]]
            reg_targets[b, pos_mask] = self.box_to_deltas(pos_anchors, pos_gt) # calculate the required offset between the anchors and their groundtruth
            pred_delta = self.box_to_deltas(pos_anchors, bboxes[b, pos_mask]) # predict offset between the anchors and bboxes
            reg_loss += F.smooth_l1_loss(pred_delta, reg_targets[b, pos_mask], reduction="sum")/max(1, pos_mask.sum()) # calculate regression loss

    # classification losses. Most probably the error is here
    total = cls_pos_targets.sum() + cls_neg_targets.sum()
    pos_count = cls_pos_targets.sum().clamp(min=1)
    neg_count = cls_neg_targets.sum().clamp(min=1)
    total_count = pos_count + neg_count
    cls_pos_loss = focal(probs, cls_pos_targets).sum()
    cls_neg_loss = focal(-probs, cls_neg_targets).sum()/neg_count
    cls_loss = cls_pos_loss + cls_neg_loss
    
    # Total loss
    total_loss = cls_loss/total + self.lambda_reg * reg_loss 
    # print(f'classification loss {cls_loss/total} and regression loss {reg_loss}, iou loss = {iou_loss}')
    return total_loss

当前现象与疑问

分类损失几乎没有变化,而回归损失如预期下降,同时IoU损失略有下降。分类部分输出的prob logits在训练过程中没有变化,但各层梯度正常更新,推测问题出在分类损失计算环节。

内容的提问来源于stack exchange,提问作者Johan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 03:30:55