当前位置: 首页 > news >正文

基于transformer的目标检测——匈牙利匹配算法

        目前目标检测系列分为基于卷积神经网络(CNN)系列和基于transformer方法系列。对于卷积系列我个人也学习了很多,而对于基于transformer系列的也开始在了解。所以我打算起一个专栏,专门对自己知识的盲区进行扫盲。所以这个系列的文章,也是想到什么总结什么。

        最近在使用C++复现DEIM的代码。同时也是了解到Hungarian Matching算法。这一篇就从匈牙利匹配开始吧。注意我这里只介绍与DEIM中用到的代码相关的逻辑。至于更深层次的或者其他的我暂时不做了解。

           匈牙利匹配(Hungarian Matching)在深度学习目标检测中主要用于解决目标检测框(bounding box)与真实标注框(Ground truth)之间的最优匹配问题,尤其是在多目标场景下(如DETR等模型)。我会先简单的了解一下用到的基础原理和深度学习的理论。

1,匈牙利匹配算法原理

        匈牙利匹配算法(Hungarian Algorithm)是一种解决二分图最小权匹配问题的多项式时间算法。其时间复杂度是O(N**3),在目标检测中:

  • 1,将预测框和真实框视为二分图的两部分
  • 2,需要找到预测框和真实框之间的最优一对一匹配

数字形式化:对于N个预测框和M个真实框(M≤N),定义代价矩阵C∈ℝ^(N×M),其中Cij表示第i个预测框与第j个真实框的匹配代价。目标是找到排列σ∈𝔖_N使得总代价最小:

        概念太抽象的话,我这个来一个图。假设你有三个预测框(P1, P2, P3)和三个真实框(G1, G2, G3)。如下:

        匈牙利算法会找到一个最优的分配方式(比如P1->G2, P2->G1, P3->G3),使得总代价最小。我们计算了预测和真实之间的代价矩阵,默认情况下匈牙利算法返回的是最优解,即最小代价。

下面来以一个距离的例子来说一下其理论原理。

1.1 问题背景:任务分配问题

假设我们有:

  • 3名工人:A,B,C

  • 3项任务:T1, T2, T3

  • 每个工人完成一项任务的成本(花费)如下:

        目标:我们希望每个工人只做一项任务,并且任务每个只能被一个人做,使得总成本最低。

1.2 目标:找出最优分配

        对于上面的3*3的表,问题就是找出一种一一对应的分配,使得总成本最低

        最简单的方法就是暴力法:暴力法会尝试所有3 !=6种组合。匈牙利算法可以在线性规划中高效的求解出最优组合,时间复杂度为O(n^3)。

1.3 匈牙利算法原理简化解释

        如果我们使用匈牙利算法,如何做呢? 下面是其步骤

step1:行/列减操作(简化问题)
  • 每一行减去最小值:让每行都有一个0

  • 然后每一列再减去最小值:让每列也都有一个0

(这一步不会改变最优匹配的位置,但是方便之后找0)

step2:寻找覆盖所有0的最少行/列数
  • 找出矩阵中所有的0

  • 用尽量少的横线/竖线覆盖所有0 (如果能用n条线覆盖所有0(即方针维度),说明最优匹配已找到)。否则:

  • 在未覆盖区域中找最小值

  • 修改矩阵值(调整0的布局)

  • 重复该步骤直到找到最优匹配

step3:进行匹配

        在只含0的位置进行分配(不能同行或同列),最后得到最优分配方案。

step4:数学推理        

        原始代价矩阵,如下(就是上面的数字)

按照步骤1:第一步行减:(每行减去最小值,让每行都有一个0)

第二步列减:(让每一列再减去最小值,这样每列也有一个0)

然后尝试选择不冲突的0来匹配每个人,最终得到一个最优匹配,比如:

1.4 匈牙利匹配算法的python实现-linear_sum_assignment

        python中的SciPy库提供了linear_sum_assignment函数,用于解决线性分类问题(Linear Sum Assigment Problem, LSAP),即经典的二分图最小权匹配问题。他实现了匈牙利算法(Hungarian Algorithm),用于在给定的代价矩阵中找到最优的一对一匹配,使得总代价最小。

        Scipy的linear_sum_assignment的函数签名:

from scipy.optimize import linear_sum_assignment
row_ind, col_ind = linear_sum_assignment(cost_matrix, maximize=False)

输入

  • cost_matrixn x m 的代价矩阵(n 任务,m 工人)。

  • maximize:若为 True,则求解最大权匹配(如最大化IoU)。

输出

  • row_ind:匹配的行索引(任务ID)。

  • col_ind:匹配的列索引(工人ID)。

示例:

import numpy as np
from scipy.optimize import linear_sum_assignment# 代价矩阵:3个预测框 vs 2个真实框(行列可不等)
cost_matrix = np.array([[1, 2],  # 预测框1与真实框1/2的代价[3, 4],  # 预测框2与真实框1/2的代价[5, 6]   # 预测框3与真实框1/2的代价
])# 找到最小代价匹配
row_ind, col_ind = linear_sum_assignment(cost_matrix)
print("匹配结果:", row_ind, col_ind)  # 输出:[0, 1], [0, 1] → 预测框1←→真实框1, 预测框2←→真实框2
print("总代价:", cost_matrix[row_ind, col_ind].sum())  # 输出:1 + 4 = 5

2,匈牙利匹配在目标检测的应用

我们知道目标检测模型会生成大量预测框(如YOLO的Anchor Boxes或DETR的预测集合),但需要确定那些预测框对应哪些真实标注框。所以匹配的目标就是:最小化预测与真实值之间的整体差异(如位置误差,分类误差)。

        而匈牙利匹配的核心思想就是二分图匹配:将预测框和真实框视为二分图的两部分,通过计算他们之间的代价(cost),找到最优的一对一匹配。

2.1 实例:深度学习演示匈牙利匹配算法过程

        在深度学习目标检测中,比如每张图预测了多个目标:pred_boxes(模型预测的框),gt_boxes(标注的真实框)。我们使用匈牙利匹配,找出哪个预测最匹配哪个真实框(一对一)

        当然匹配标注中可能包含:分类损失,L1框差异,GIOU框重叠程度。然后利用这些匹配关系来监督训练。

        如上流程:其内容包括:

  • 模拟预测框(pred_logits和pred_boxes)和目标框(targets)

  • 构建 cost matrix(分类cost + L1 box cost + GIOU cost)

  • 使用匈牙利算法找出最优匹配对

step1:模拟数据构建

        假设有一张图(batch=1),模型输出了3个预测框,Ground Truth有两个目标。模拟预测(outputs):

# 分类 logits(预测框对3个类别的分数)
pred_logits = torch.tensor([[[2.0, 0.1, 0.1],    # 第一个预测框:class 0[0.1, 2.0, 0.1],    # 第二个预测框:class 1[0.1, 0.1, 2.0]]    # 第三个预测框:class 2
])  # shape: [1, 3, 3]# 预测框(中心点坐标 cx, cy, 宽 w,高 h)格式
pred_boxes = torch.tensor([[[0.5, 0.5, 1.0, 1.0],   # box 1[2.0, 2.0, 1.0, 1.0],   # box 2[3.0, 3.0, 1.0, 1.0]]   # box 3
])  # shape: [1, 3, 4]

        模拟Ground truth:

targets = [{"labels": torch.tensor([0, 2]),                 # 第一个GT是类0,第二个GT是类2"boxes": torch.tensor([[0.6, 0.6, 1.0, 1.0],     # GT1[3.1, 3.1, 1.0, 1.0]])    # GT2
}]
step2:计算Cost matrix

1,分类cost:我们用softmax后的概率来计算 cost_class=-log(p[class]) 即负对数似然。

from torch.nn.functional import softmax
import torchprobs = softmax(pred_logits, dim=-1)[0]  # shape: [3, 3]
print("分类概率:\n", probs)
# GT标签是[0, 2],我们取每个预测框对 class 0 和 class 2 的概率
cost_class = -probs[:, [0, 2]]
print("分类代价 (负log概率):\n", cost_class)

        输出类似:

分类概率:
tensor([[0.8438, 0.0781, 0.0781],[0.0781, 0.8438, 0.0781],[0.0781, 0.0781, 0.8438]])cost_class:
tensor([[-0.1696, -2.5494],   # 预测框1与GT0、GT2[-2.5494, -2.5494],   # 预测框2[-2.5494, -0.1696]])  # 预测框3

2,L1 box cost:

from torch.cdist import cdist# 展平预测框和 GT 框
boxes_pred = pred_boxes[0]  # [3, 4]
boxes_gt = targets[0]['boxes']  # [2, 4]cost_bbox = torch.cdist(boxes_pred, boxes_gt, p=1)
print("L1 框代价:\n", cost_bbox)

        输出:

tensor([[0.2, 5.2],[2.8, 1.4],[5.2, 0.2]])

3,GIOU cost

from torchvision.ops import generalized_box_iou# 把 cxcywh → xyxy
def box_cxcywh_to_xyxy(x):x_c, y_c, w, h = x.unbind(-1)b = [(x_c - 0.5 * w), (y_c - 0.5 * h),(x_c + 0.5 * w), (y_c + 0.5 * h)]return torch.stack(b, dim=-1)box1 = box_cxcywh_to_xyxy(boxes_pred)
box2 = box_cxcywh_to_xyxy(boxes_gt)cost_giou = -generalized_box_iou(box1, box2)
print("GIoU cost:\n", cost_giou)

假设结果如下(GIOU越大越好,所以我们取负值当cost)

tensor([[ -0.7, -0.9],[ -0.9, -0.4],[ -0.6, -0.1]])

step3:最终 cost matrix + 匈牙利匹配

加权合并(比如都权重设为1)

total_cost = cost_class + cost_bbox + cost_giou
print("总代价矩阵:\n", total_cost)

例如得到如下结果(3*2)

tensor([[ 0.2, 1.75],[ 0.35, -0.55],[ 2.05, -0.07]])

使用linear_sum_assignment 找最优匹配对。

from scipy.optimize import linear_sum_assignmentcost_matrix = total_cost.detach().numpy()
row_ind, col_ind = linear_sum_assignment(cost_matrix)
print("预测框索引:", row_ind)
print("匹配的GT索引:", col_ind)

输出结果示例:

预测框索引: [0 2]
匹配的GT索引: [0 1]

说明:

  • 预测框0 → GT0(匹配上了同类)

  • 预测框2 → GT1(也匹配合理)

  • 预测框1 没有匹配上(将作为负样本)

这个过程模拟了:

  • 构造代价矩阵

  • 使用匈牙利算法匹配预测和 GT

  • 得到匹配对后,剩余未匹配

  • 的预测框作为背景负样本处理(比如计算 no-object loss)

2.2 DEIM的匈牙利匹配代码

        下面是针对DEIM匈牙利匹配代码,结合目标检测任务的需求和匈牙利算法的实现逻辑。

代码如下:

import torch
import torch.nn as nn
import torch.nn.functional as Ffrom scipy.optimize import linear_sum_assignment
from typing import Dictfrom .box_ops import box_cxcywh_to_xyxy, generalized_box_ioufrom ..core import register
import numpy as np@register()
class HungarianMatcher(nn.Module):"""This class computes an assignment between the targets and the predictions of the networkFor efficiency reasons, the targets don't include the no_object. Because of this, in general,there are more predictions than targets. In this case, we do a 1-to-1 matching of the best predictions,while the others are un-matched (and thus treated as non-objects)."""__share__ = ['use_focal_loss', ]def __init__(self, weight_dict, use_focal_loss=False, alpha=0.25, gamma=2.0):"""Creates the matcherParams:cost_class: This is the relative weight of the classification error in the matching costcost_bbox: This is the relative weight of the L1 error of the bounding box coordinates in the matching costcost_giou: This is the relative weight of the giou loss of the bounding box in the matching cost"""super().__init__()self.cost_class = weight_dict['cost_class']self.cost_bbox = weight_dict['cost_bbox']self.cost_giou = weight_dict['cost_giou']self.use_focal_loss = use_focal_lossself.alpha = alphaself.gamma = gammaassert self.cost_class != 0 or self.cost_bbox != 0 or self.cost_giou != 0, "all costs cant be 0"@torch.no_grad()def forward(self, outputs: Dict[str, torch.Tensor], targets, return_topk=False):""" Performs the matchingParams:outputs: This is a dict that contains at least these entries:"pred_logits": Tensor of dim [batch_size, num_queries, num_classes] with the classification logits"pred_boxes": Tensor of dim [batch_size, num_queries, 4] with the predicted box coordinatestargets: This is a list of targets (len(targets) = batch_size), where each target is a dict containing:"labels": Tensor of dim [num_target_boxes] (where num_target_boxes is the number of ground-truthobjects in the target) containing the class labels"boxes": Tensor of dim [num_target_boxes, 4] containing the target box coordinatesReturns:A list of size batch_size, containing tuples of (index_i, index_j) where:- index_i is the indices of the selected predictions (in order)- index_j is the indices of the corresponding selected targets (in order)For each batch element, it holds:len(index_i) = len(index_j) = min(num_queries, num_target_boxes)"""bs, num_queries = outputs["pred_logits"].shape[:2]# We flatten to compute the cost matrices in a batchif self.use_focal_loss:out_prob = F.sigmoid(outputs["pred_logits"].flatten(0, 1))else:out_prob = outputs["pred_logits"].flatten(0, 1).softmax(-1)  # [batch_size * num_queries, num_classes]out_bbox = outputs["pred_boxes"].flatten(0, 1)  # [batch_size * num_queries, 4]# Also concat the target labels and boxestgt_ids = torch.cat([v["labels"] for v in targets])tgt_bbox = torch.cat([v["boxes"] for v in targets])# Compute the classification cost. Contrary to the loss, we don't use the NLL,# but approximate it in 1 - proba[target class].# The 1 is a constant that doesn't change the matching, it can be ommitted.if self.use_focal_loss:out_prob = out_prob[:, tgt_ids]neg_cost_class = (1 - self.alpha) * (out_prob ** self.gamma) * (-(1 - out_prob + 1e-8).log())pos_cost_class = self.alpha * ((1 - out_prob) ** self.gamma) * (-(out_prob + 1e-8).log())cost_class = pos_cost_class - neg_cost_classelse:cost_class = -out_prob[:, tgt_ids]# Compute the L1 cost between boxescost_bbox = torch.cdist(out_bbox, tgt_bbox, p=1)# Compute the giou cost betwen boxescost_giou = -generalized_box_iou(box_cxcywh_to_xyxy(out_bbox), box_cxcywh_to_xyxy(tgt_bbox))# Final cost matrix 3 * self.cost_bbox + 2 * self.cost_class + self.cost_giouC = self.cost_bbox * cost_bbox + self.cost_class * cost_class + self.cost_giou * cost_giouC = C.view(bs, num_queries, -1).cpu()sizes = [len(v["boxes"]) for v in targets]# FIXME,RT-DETR, different way to set NaNC = torch.nan_to_num(C, nan=1.0)indices_pre = [linear_sum_assignment(c[i]) for i, c in enumerate(C.split(sizes, -1))]indices = [(torch.as_tensor(i, dtype=torch.int64), torch.as_tensor(j, dtype=torch.int64)) for i, j in indices_pre]# Compute topk indicesif return_topk:return {'indices_o2m': self.get_top_k_matches(C, sizes=sizes, k=return_topk, initial_indices=indices_pre)}return {'indices': indices} # , 'indices_o2m': C.min(-1)[1]}

        该Hungarian Matcher类将模型的预测框(pred_boxes)和分类分数(pred_logits)与真实标注框(targets)进行最优一对一匹配,匹配依据是综合分类误差,框坐标误差和GIOU误差的加权代价。

        下面是前向传播的流程:

  • 1,扁平化预测和真实值:将批量数据展开,便于批量计算代价矩阵。
  • 2,计算分类代价:focal loss会对难样本(低概率预测)赋予更高代价;softamx则计算目标类别的负对数概率
  • 3,计算框坐标和GIOU代价:L1距离是衡量预测框与真实框的中心点坐标和宽高差异;GIOU广义交并比(负数是因为匈牙利算法最小化代价)
  • 4,加权总代价:加权求和得到最终代价矩阵C,形状为 [batch_size, num_queries, num_targets]。
  • 5,匈牙利匹配算法:Linear_sum_assignment:对每个样本独立调用匈牙利算法,返回匹配的预测索引i和真实框索引j。

2.3  匈牙利匹配的二义性

        匈牙利匹配(Hungarian Matching)在解决指派问题(Assignment Problem)时,其二义性(Ambiguity)指的是当存在多于一种方式可以达到相同的最优总成本(或总权重)时,算法可能返回不同的匹配方案。

        简单来说,就是有不止一种“最佳答案”,他们在总分数上一样好,但具体的配对方式不同

        首先,快速回顾一下匈牙利算法:他是一种用于解决二分图的最小成本完美匹配问题(或最大权重完美匹配问题)的算法。

        问题背景:假设你有N个人和N个任务,每个人完成每个任务都有一个特定的成本(或价值)。目标是为每个人分配一个任务(且每个任务只分配给一个人),使得总成本最小(或总价值最大)。

        核心:算法会找到一个唯一的最优总成本。

匈牙利匹配的二义性示例

        二义性不是指最优总成本不唯一,而是指达到这个最优总成本的匹配方案(即那些人匹配哪些任务)可能不唯一。

考虑一个简单的2*2的成本矩阵:

比如这种情况:

现在考虑另一种情况:

在这种情况下:

        在这个例子中,总成本 20 是唯一的最小成本,但达到这个最小成本的匹配方案却有两种。这就是匈牙利匹配的二义性。当算法运行时,它可能会返回其中任意一个达到最优总成本的匹配方案。

为什么会出现二义性?

        二义性通常发生在成本矩阵中存在对称性或多个零点排列方式时,在匈牙利算法的内部步骤中,当寻找独立的零点覆盖时,如果一个零点可以通过多条路径或选择来覆盖,并且这些不同的选择最终都导向相同的最优总成本,那么就会出现二义性。

http://www.dtcms.com/a/313417.html

相关文章:

  • 深入解析HashMap:原理与性能优化
  • Vim编辑器详解:从入门到高效使用
  • 从零开始的CAD|CAE开发: LBM源码实现分享
  • 编程语言分类
  • JAVAEE--5.多线程之常见的锁策略
  • AI Competitor Intelligence Agent Team
  • 【openlayers框架学习】七:绘制线要素以及点击画线功能
  • 力扣热题100----------141.环形链表
  • 基于BiLSTM+CRF实现NER
  • 【机器人】VLN-R1 微调 | 增强训练 | 连续导航
  • Web3合约ABI,合约地址生成部署调用及创建,连接钱包,基础交易流程
  • ARPO:让LLM智能体更高效探索
  • 【Linux网络编程基础--socket地址API】
  • 多 4G 通讯模组共存时的干扰问题深度解析与解决方案
  • leecode-每日一题-2106. 摘水果
  • vmfusion启动centos6.10 一直卡到call 169.254.169.254
  • 全面解析 BGE Embedding 模型:训练方式、模型系列与实战用法
  • Redis——常用指令汇总指南(三)(哈希类型)
  • 编写xsync集群分发脚本(保姆级别)
  • Redis 数据同步机制
  • 【Linux】Makefile Cmake—基操
  • [特殊字符]字节Get!免费进楼攻略速存[特殊字符]
  • LWIP从FreeRTOS到uC/OS-III的适配性改动
  • linux 扩展未分配的磁盘空间到home下
  • SQL157 更新记录(一)
  • 代码随想录算法训练营第五十八天|动态规划part8
  • 成功解决ImportError: DLL load failed while importing _multiarray_umath: 找不到指定的模块。
  • 深度学习中的模型知识蒸馏
  • 深度学习中卷积与互相关
  • 记录使用ruoyi-flowable开发部署中出现的问题以及解决方法