简介:这是一份基于Pytorch实现OW-DETR开放世界Transformer目标检测算法的完整项目包,主要面向具备深度学习与目标检测基础、希望将模型扩展到开放类别场景的算法工程师与研究生。算法利用Transformer自注意力机制,在无预设分类目标条件下完成检测,适用于自动驾驶、智能监控等动态复杂场景。包内不仅提供流程教程与可直接运行的源码,还按工具模块划分出tools训练脚本、OWDETR核心模型、data数据处理、configs实验配置、util通用函数等目录,并包含main_open_world.py、engine.py等重要入口;借助configs中的实验配置可灵活调整参数,便于从数据准备、模型训练到评估全流程复现。资源共74个文件,以38个py源码文件为主,辅以18个txt配置说明、9个sh运行脚本、README文档及少量C++/CUDA扩展,整体仅1.51MB,轻量且结构清晰。项目已有406人学习,工程组织清晰,适合深入研究开放世界目标检测原理,并在此基础上进行二次开发、实验对比,也可作为课程设计或研究基线。
1. OW-DETR 如何用 Transformer 在未知类别上直接出框
一个反直觉的事实:绝大多数目标检测器在训练集里只见过几十个类别时,遇到一个从未标注过的新物体,会带着很高的置信度把它识别成“背景”。这不是标注漏了,而是模型设计里就没有“我不知道这是什么,但那里有个东西”这个选项。OW-DETR(Open World Detection Transformer)要做的就是把这条路打通:沿用 DETR 的端到端 Transformer 结构,利用解码器交叉注意力图去标注“未知名目标”。它在已知类上正常分类,在未知类上只给框和物体度,不猜名字。
标题里的“基于 PyTorch 实现”点明了落地方式:可以用 PyTorch 2.x 配合标准 DETR 权重做增量扩展,数据集也不需要大规模人工重标。本文按“原理 → 代码结构 → 训练调参 → 迁移自己数据”的顺序展开,中间会给出可复现的命令和代码片段。适合两类人:一类是刚接触开放世界检测、想找个能跑通基线的新手;另一类是做过 YOLO 或 DETR 系列检测器、想评估开放世界设置下精度与召回边界的老手。
2. OW-DETR 的开放世界机制:注意力伪标签与三分量物体度
2.1 从闭集到开放世界:三种检测范式的分界在哪
先明确一个容易混淆的概念,开放世界检测(Open World Detection)不等于开放集检测(Open Set Detection),更不等于传统的闭集检测。三者对“未见过类别”的处理方式完全不同,见下表。
| 范式 | 训练时可知类别 | 测试时遇到未见类别 | 典型方法 |
|---|---|---|---|
| 闭集检测 Closed-set | 全部可见 | 直接归为背景 | YOLO、Faster R-CNN |
| 开放集检测 Open-set | 全部可见但允许声明未知 | 输出“未知”标签但不给精确位置 | OpenMax、OLTR |
| 开放世界检测 Open-world | 部分可见 | 检测为未知目标,并保留后续增量学习空间 | ORE、OW-DETR |
YOLO 这类把检测建模成密集网格回归的流程,天然躲不开这个问题:每个网格要么是已知类,要么是背景,没有第三种状态。OW-DETR 走的是另一条路,它把“这是不是一个目标”和“这个目标叫什么”拆成两个独立判断,未知类只在第一个判断上做文章。这也是它能支撑开放世界场景的根源。
2.2 DETR 的集合预测天然具备“先找框、后命名”的条件
Transformer 架构及其工作原理在这里体现得很直接。DETR 不使用锚框和后处理 NMS,而是把检测看成集合预测问题:解码器里固定一组 object query,每个 query 经过自注意力和交叉注意力后,输出一个目标框和一个类别概率。
关键在类别输出和框输出是解耦的。object query 在解码器中和图像特征的交叉注意力,决定它最终落在图像哪个区域;分类头只是在这个区域上做一个判断。也就是说,DETR 本身就允许一个 query“找到了东西但说不出名字”。OW-DETR 抓的正是这个间隙。
复现时要注意 DETR 的位置编码使用的是正弦绝对位置编码,OW-DETR 基本保留这一设定,没有改成可学习的相对位置编码。这个细节影响不大,但如果你在迁移到自己的数据集时发现收敛慢,可以优先怀疑位置编码的归一化方式,而不是模型结构。
2.3 OW-DETR 如何让未知类变得可见
2.3.1 注意力驱动的伪标签(ADPL)在做什么
训练集里只有已知类标注,模型怎么知道哪里可能存在未知目标?OW-DETR 给出的做法是:用训练过程中编码器产生的注意力图去发现“看起来像物体但没被标注”的区域。
具体来说,ADPL 模块对编码器最后一层输出计算注意力权重,经过一个可学习的门控机制,把注意力分数突显出来。然后在注意力图上采样高响应区域,聚合成候选框,作为伪标签。这些伪标签不携带类别名,只携带位置信息,用于监督未知类分支。
这里的核心区别在于伪标签不来自最终的预测头,而是来自注意力图。注意力图从定位信号上学出来,不会像分类头那样被“背景”主导。
2.3.2 通过对比聚类把未知类彼此分开
伪标签有了,但这堆未知目标里可能混杂着多个新类别。没有类别名字,怎么让它们彼此区分?OW-DETR 使用对比聚类:让同一张图片内部属于相似区域的未知特征互相靠近,属于不同区域的互相推开;不同图片之间的特征进行负向对比。
这是一种无监督的类别分离方式。它在训练时不生成具体未知类 id,而是把特征空间掰成若干个簇。测试时,当物体度分支判定某个框是未知目标,就把它映射到最近的特征簇,从而实现“知道是不同种新东西”但“不知道叫什么名字”的效果。
2.3.3 物体度头的三分量结构
普通 DETR 的分类头输出维度是known_classes + 1,这个 1 就是背景。OW-DETR 把它改成三个分量:已知类、未知目标、背景。背景只负责纯无目标区域;已知类和未知目标互不侵占。
三个分量共享特征,但分类头分别监督。已知框用真实标注,未知目标区域用 ADPL 生成的伪标签,其余区域用背景。推理时如果未知分量分数超过阈值,就直接输出为 unknown,不需要先通过某个已知类别假设。这个三分量结构是 OW-DETR 和 DETR 之间最直接的代码差异。
3. 基于 PyTorch 搭建 OW-DETR:环境、数据与训练流程
3.1 PyTorch 环境搭建:版本组合怎么选不踩坑
OW-DETR 对 PyTorch 的版本没有硬性依赖,常见实现大多基于 DETR 官方代码改造,兼容 PyTorch 1.13 到 2.8 都能跑。但建议直接使用新版本,省去很多算子兼容麻烦。下面是我在 Ubuntu 上常用的组合:
conda create -n owdetr python=3.10 -y conda activate owdetr pip install torch==2.5.1 torchvision==0.20.1 --index-url https://download.pytorch.org/whl/cu121 pip install matplotlib pycocotools scipy einops版本对应的逻辑很简单:PyTorch 2.5.1 对应 torchvision 0.20.1,CUDA 12.1 的预编译包直接指定 cu121。如果显卡驱动支持更高版本,也可以把 cu121 换成 cu128,但不要单独拆开混装 torch 和 torchvision,它们之间有编译期绑定关系。安装完务必跑一下 CUDA 是否可用:
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"输出里cuda.is_available()返回 True 再继续。False 的情况优先检查驱动和 PyTorch 的 CUDA 版本是否配套,而不是重装 Anaconda。
3.2 数据集准备:VOC 的已知类/未知类划分与 COCO JSON 格式
开放世界检测的标准评测设置,是把 PASCAL VOC 的 20 个类划分成 15 个已知类加 5 个未知类。训练时未知类图片照常参与训练,但它们的标注会被剔除。模型需要在训练时从注意力图中“自己发现”未标注目标,然后再在测试时召回它们。
常用做法是先把 VOC 的 XML 标注转成 COCO 格式,因为 DETR 系列评估代码依赖 COCO API。转换脚本的关键是 categories 部分要保留完整类别名:
# voc_to_coco.py import json, xml.etree.ElementTree as ET KNOWN_CLASSES = ["aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike", "person"] ALL_CLASSES = KNOWN_CLASSES + ["train", "tvmonitor", "sofa", "pottedplant"] def convert_annotation(xml_path, img_id, images, annotations): tree = ET.parse(xml_path) root = tree.getroot() img_info = {"id": img_id, "file_name": root.find("filename").text} images.append(img_info) for obj in root.findall("object"): name = obj.find("name").text if name not in ALL_CLASSES: continue bbox = obj.find("bndbox") x1 = float(bbox.find("xmin").text) y1 = float(bbox.find("ymin").text) w = float(bbox.find("xmax").text) - x1 h = float(bbox.find("ymax").text) - y1 annotations.append({ "id": len(annotations), "image_id": img_id, "category_id": ALL_CLASSES.index(name) + 1, "bbox": [x1, y1, w, h], "area": w * h, "iscrowd": 0 })训练时切换未知类有两种方式:一种是在转换阶段直接丢弃未知类标注,另一种是在数据加载器里动态 mask。建议使用第二种,这样同一个标注文件可以反复实验不同的已知/未知划分,不用为每组划分重新转换一遍数据。
3.3 模型代码组织:把 DETR 的 forward 扩成三头输出
OW-DETR 的常见实现是把 DETR 的DETR类继承下来,在原有分类头和框回归头之外,新增物体度头和未知类特征头。文件结构可以组织成这样:
owdetr/ ├── models/ │ ├── detr.py # 基础 DETR 模型 │ ├── owdetr.py # OW-DETR 继承扩展 │ ├── adpl.py # 注意力驱动的伪标签模块 │ └── matcher.py ├── datasets/ │ └── voc_ow.py # 开放世界 VOC Dataset ├── engine.py └── main.py核心的 forward 伪代码如下:
# models/owdetr.py import torch from torch import nn from models.detr import DETR class OWDetr(DETR): def __init__(self, num_classes=15, num_unknown_slots=5, **kwargs): super().__init__(**kwargs) # 原分类头:已知类 + 背景 self.class_embed = nn.Linear(256, num_classes + 1) # 物体度头:已知 / 未知 / 背景 三个分量 self.objness_embed = nn.Linear(256, 3) # 未知特征对比头 self.unknown_proj = nn.Linear(256, 128) def forward(self, samples, targets=None): outputs = super().forward(samples, targets) # features 来自解码器输出的 object query 特征 features = outputs["last_hidden_state"] outputs["objness_logits"] = self.objness_embed(features) outputs["unknown_feat"] = F.normalize(self.unknown_proj(features)) return outputs需要注意objness_embed和class_embed的监督目标不同:objness 是一个三分量交叉熵,class_embed 只在已知类分量上计算损失。训练时 backgrounds 由 DETR 原匹配器决定,unknown 的标签则来自 ADPL 生成的伪框,整体构成一个多任务损失。
3.4 训练启动:命令、参数与日志观察点
完成数据准备和模型改造后,训练命令基本围绕main.py展开。以 VOC 15/5 划分为例:
python main.py \ --dataset_file voc_ow \ --data_path ./data/VOCdevkit \ --known_classes 15 \ --unknown_classes 5 \ --batch_size 4 \ --epochs 50 \ --lr 1e-4 \ --lr_drop 40 \ --output_dir ./runs/owdetr_voc15_5参数含义:known_classes控制分类头维度,unknown_classes只影响伪标签聚类时预留的簇数量;batch_size 4是参数量不大但显存有限的保守选择;lr 1e-4是 DETR 系列常见的初始学习率,Transformer 部分的收敛对学习率比 CNN 更敏感,不建议直接调成 1e-3。训练日志里重点观察两个值:loss_objness和unknown_recall,前者下降说明三分量物体度在起作用,后者在验证集上逐步上升说明 ADPL 的伪标签质量在改善。
提示:前 5 个 epoch 如果
loss_class掉得很快而loss_objness几乎不动,不用慌,因为 DETR 的匹配器先稳定已知类匹配,未知类的信号需要一个预热过程。
4. OW-DETR 参数与损失:伪标签阈值、未知类别数量上限和控制点
4.1 伪标签阈值怎么定:门控分数、NMS 与训练稳定性的关系
ADPL 生成的伪标签不是直接可用的,需要过一道筛选。门控注意力分数是一个介于 0 到 1 之间的标量,分数越高,说明该区域被注意力“关注”得越充分。常见实现里会设定一个阈值 gate_threshold,只有得分超过阈值的区域才能生成候选框。
阈值设大了,漏掉大量未知目标,未知类召回率上不去;阈值设小了,大量背景区域混进来,把未知类特征空间搞脏。在 VOC 15/5 设置下,建议从 0.3 开始试,同时配合 NMS 阈值 0.5 消除重叠框。判断阈值是否合适,可以对比开合伪标签时训练集上未知类召回的差异,差异过大说明伪标签量过多或噪声过高。
具体操作时,在 ADPL 加入一个简单的门控控制:
# adpl.py gate = torch.sigmoid(gate_logits) mask = (gate > 0.3).float() attn_masked = attn * mask boxes = generate_boxes_from_peaks(attn_masked, nms_threshold=0.5)这里gate_logits是门控网络的原始输出,generate_boxes_from_peaks把注意力峰值区域转换成伪框。0.3 是经验起始值,如果你的数据集物体的尺度偏小,可以适当降到 0.25。注意,伪标签生成的随机性会影响训练稳定性,建议固定随机种子,并让 ADPL 模块不参与梯度更新,只作为离线信号源使用。
4.2 未知类别数量上限 K 的作用与动态更新策略
训练集中的未知类实际上只有 5 个,但真实场景下模型无法预知。OW-DETR 在对比聚类时需要预先设定未知类特征簇的数量,这就是 K。K 设的大小直接影响特征空间的划分粒度。
- K 太小:多个真实新类别被并进同一个簇,召回高但精度差。
- K 太大:特征空间过度碎片化,一个类被拆成好几个簇,物体度头的“未知”分会被分裂。
常见实现里 K 是硬编码的,有的项目支持动态 K 调整,做法是:在验证集上计算未知类伪标签的聚簇紧凑度,如果平均簇内距离显著高于历史均值,说明 K 偏大,自动调小。我在复现时更推荐一种简单方法:把 K 设成已知类数量的一半左右,VOC 场景下 7 到 8 的结果比 5 更平滑,因为真实未知类在特征空间里不一定正好是 5 个簇。
4.3 损失权重与已知类精度的博弈
OW-DETR 的总损失由三部分构成:传统检测损失(框回归 + 已知类分类)、ADPL 伪标签监督损失、未知物体度损失。默认权重通常都是 1:1:1,但这个等权重并不是最优解。
| 损失分量 | 默认权重 | 作用 | 调节方向 |
|---|---|---|---|
loss_ce已知类分类 | 1.0 | 维持已知类精度 | 已知类 mAP 掉太狠时增大 |
loss_boxes框回归 | 5.0 | 定位质量 | 基本固定,不要频繁动 |
loss_objness物体度 | 1.0 | 区分已知/未知/背景 | 未知召回不足时增加到 1.5-2.0 |
loss_contrast对比聚类 | 0.5 | 未知类特征分离 | 未知类经聚类混淆时增大到 1.0 |
框回归的损失权重保持 DETR 原始设定即可,它同时服务已知类和未知类,降低它会拖累一切。loss_contrast是最值得调的项,它的作用是把未知类特征空间掰开。权重太大时模型会把已知类特征也推散,导致已知类 mAP 骤降;太小则未知类之间的区分度不够。
4.4 训练中如何判断未知分支在收敛
只盯着 loss 是不够的,因为伪标签本身可能有噪声,loss 下降可能只是模型在拟合噪声。必须在训练过程中定期计算验证集上的 unknown recall。可以每 5 个 epoch 跑一次轻量评估,重点看两个指标的组合:
- unknown recall 提升但 known mAP 几乎不变,健康的收敛。
- unknown recall 提升但 known mAP 下降超过 2 个点,伪标签噪声超标,优先调低 gate_threshold,其次调低
loss_contrast。 - unknown recall 在后期剧烈震荡,大概率是 K 值过大导致聚类簇切换,考虑固定 K 并降低学习率。
5. 把 OW-DETR 用到自己的数据上:迁移检查与未知类验证技巧
5.1 从闭集标注快速转成开放世界格式
自有数据通常只有已知类标注,转成开放世界训练集不需要额外标注未知类。做法是保留全部原始标注作为已知类,在数据加载器里维护一个 ignore 列表,把想要测试的新类从监督中剔除。
# voc_ow.py class OWDataset(Dataset): def __init__(self, ann_file, known_ids, exclude_ids): self.known_ids = set(known_ids) self.exclude_ids = set(exclude_ids) # 加载标注时,只保留 known_ids self.filtered_anns = [a for a in all_anns if a["category_id"] in self.known_ids]关键在exclude_ids的设计上。这些类别的真实标注在训练时被排除,但 ADPL 的注意力图仍然能从图片上感知它们的存在,这正好模拟了真实场景里“模型看到了新东西,但没人告诉它这是什么”。评估时再用完整标注计算 unknown recall,就能看出模型到底有没有学会发现新类。
5.2 用已知精度和未知召回率联合选择 checkpoint
开放世界检测的最终落地点不在于单个 mAP,而在于已知与未知的平衡。评估时同时跑两个指标:
python eval.py --dataset_file voc_ow --data_path ./data/VOCdevkit \ --resume ./runs/owdetr_voc15_5/checkpoint0020.pth \ --known_classes 15 --unknown_classes 5评估输出会包含 known mAP、unknown recall 和 wildness impact 三项。选择 checkpoint 时优先看 unknown recall 与 known mAP 的比值,而不是单看某一边。一个常见的落地策略是:取 unknown recall 超过 60% 且 known mAP 不低于封闭集基线 95% 的 checkpoint。实际项目中很少追求最高的 unknown recall,因为那通常意味着大量背景被误报为未知。
5.3 按未知类别拆开评估,定位模型盲区
整体 unknown recall 只反映平均水平,实际使用里要关注模型漏掉的是哪些类别。逐类别计算 unknown recall 是定位盲区的最直接方法。
假设验证集中第 19 类是train但训练时被排除,遍历该类的所有标注,统计被物体度分支判定为 unknown 的框占比。如果train类召回率明显低于其他未知类,通常不是因为物体度分支不好,而是因为 ADPL 的伪标签在训练时没有覆盖类似外观的区域,比如大尺度目标和低纹理目标。此时针对该类提高门控分数的容差范围,或增加数据增强中随机缩放的幅度,能显著改善。
另外一个容易忽视的细节是评估时 IoU 阈值的选择。开放世界检测场景中,未知类框不需要和 GT 完全对齐,IoU 0.5 或更宽松的 0.4 都比 COCO 标准的 0.75 更符合“发现新目标”的语义。把所有未知类按召回率从低到高排序后,优先处理排在最后的那个类别,再重新训练迭代,比盲目调整整体权重更高效。
本文还有配套的精品资源,点击获取