简介:面向计算机视觉研究者和目标检测开发者的 YOLO-World 学习与复现资料包,汇集 PyTorch 实现、预训练权重以及预训练/微调代码,重点解决开放词汇检测与 grounding 场景下的模型复现和二次开发问题。资料围绕 prompt-then-detect 推理范式组织,支持用户自定义词汇的高效检测实验,适合具备一定深度学习基础、希望深入理解开放词汇目标检测流程的读者。压缩包共 161 个文件,整体约 2.35MB,以 110 个 Python 源码和 20 个 Markdown 文档为主体,另含 txt 配置说明、json 类别文本、ipynb 推理示例、Dockerfile 与 shell 脚本等,覆盖训练、推理和部署参考。已有 883 人学习下载,读者可据此梳理模型结构、数据配置、权重加载与推理入口,理解预训练及微调脚本的组织方式,并借助容器配置线索搭建实验环境,为开放词汇检测实验、工程集成和后续二次开发提供较完整的参考。
1. 从「模型不认新类别」开始:YOLO-World 解决的是什么麻烦
去年帮一个做仓储视觉的朋友看需求,货架上的 SKU 每周都在变,用传统检测器,每加一个品类就要重新标几千张图、重训一轮,标注排期比开发还长,项目直接卡住。YOLO-World 就是用来解决这个问题的:它把 CLIP 文本编码器和 YOLOv8 检测器拼在一起,给几个自然语言词(person、forklift、pallet),模型就按这些词去图里找目标,不用重新训练就能换词表。这份资料包含源码和文档,核心是开放词汇检测的完整链路:推理、微调、重参数化、导出。适合两类人:一类是要在几天内验证「这个类别能不能被检测出来」的算法同学,另一类是接手检测项目、被新增类别反复拖慢节奏的工程同学。它不是万能方案,精度天花板低于针对性微调的专用模型,但在快速验证阶段能省掉大量标注开销,这个账很划算。
2. 环境与依赖:把 PyTorch 这套跑起来
2.1 目录结构与各文件定位
资料包解压后,我按功能分成四类来看:权重、配置、数据、脚本。先不管文件名的细节,得先搞清楚每块负责什么,否则后面改配置时改错地方,模型会不报错但就是没有效果。
yolo-world/ ├── weights/ # 预训练权重 │ ├── yolov8s-world.pt # s/m/l/x 几档,按你的算力选 │ └── clip-vit-base-patch32/ # 文本编码器权重,离线环境要手动放 ├── configs/ │ ├── yolov8s-world.yaml # 模型结构:backbone + RepVL-PAN + head │ └── lvis.yaml # 数据集描述 ├── datasets/ │ └── lvis/ │ ├── images/ # train2017 / val2017 │ ├── labels/ # YOLO txt 格式 │ └── lvis.yaml # names 映射,这是开放词汇的关键 ├── scripts/ │ ├── convert_lvis.py # 官方标注转 YOLO txt │ ├── train_world.py # 训练入口 │ └── export_world.py # 导出 ONNX / engine └── docs/ ├── quickstart.md └── finetune.md职责划分很清楚:configs/*.yaml决定网络有多少层、融合模块接在哪;datasets/*/lvis.yaml决定文本编码器看到哪些词;weights/决定初始参数。改动最多的两个地方是lvis.yaml的names和训练脚本里的超参,这两个地方一旦弄错,损失曲线会变得很难解释,后面第 5 章会展开。
提示:不同小版本的 ultralytics 目录名会变(比如模型配置可能放在包内的
cfg/models/v8/下),以你手上那份文档里的实际路径为准;先别急着改,先执行一次 import 并跑通一条推理,再考虑改结构。
2.2 依赖安装与 GPU 环境自检
这套代码对 PyTorch 版本比较挑,CUDA 驱动、torch、torchvision 三者版本对不上,最常见的表现是能 import 但一跑到 forward 就报CUDA error: no kernel image is available。我的习惯是先建独立环境,再逐项验证,不要一口气把 requirements 全装上然后靠运气跑起来。
# 1. 建独立环境,Python 3.10 是兼容性最稳的一档 conda create -n yoloworld python=3.10 -y conda activate yoloworld # 2. 先装 PyTorch,单独指定 CUDA 版本,别让 pip 自动给你挑 pip install torch==2.1.0 torchvision==0.16.0 --index-url https://download.pytorch.org/whl/cu118 # 3. 再装检测框架和其余依赖 pip install ultralytics opencv-python pyyaml tqdm # 4. 文本编码器依赖,部分版本需要单独装 CLIP 分支 # 具体包名和地址以你那份文档的 requirements 为准 pip install -r requirements.txt装完之后不要急着跑训练,先做三行自检,把 GPU 状态和框架版本打出来:
import torch import ultralytics # 显存和算力档位决定了你能开多大 batch print("torch:", torch.__version__) print("cuda available:", torch.cuda.is_available()) print("device:", torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU") print("ultralytics:", ultralytics.__version__) # 跑一次小矩阵乘,确认 kernel 真的能执行,不只是 is_available 为真 x = torch.randn(1024, 1024, device="cuda") print("matmul ok:", (x @ x).shape)torch.cuda.is_available()返回 True 但矩阵乘报错,基本都是 CUDA 版本和 torch 编译版本不匹配,这时重装 torch 比查其他原因快得多。这一步只要五秒钟,却能省掉后面半小时的困惑。
权重这块要注意:yolov8s-world.pt里已经固化了 CLIP 文本编码器的结构信息,但文本权重本身可能是单独一份。文档里如果提到需要额外下载clip-vit-base-patch32,必须放到指定目录,否则set_classes会直接抛找不到模型的异常。离线环境要提前把这两份文件拷进去,在线下载在训练机上是常见问题。
2.3 两条推理入口:命令行与 Python API
跑通的第一条命令,建议用命令行,参数少、报错直观:
# 直接用预训练权重推理,先确认模型能出框 yolo predict model=weights/yolov8s-world.pt source=assets/test.jpg conf=0.25 imgsz=640 save=Trueconf=0.25是置信度阈值,开放词汇模型对没见过的类别分数普遍偏低,一开始可以放到 0.1 观察召回,再往上收紧;imgsz=640和训练分辨率保持一致,改大能提升小目标但显存线性上涨。
真正接入业务用的是 Python API,因为要在推理前动态改词表:
from ultralytics import YOLOWorld # 加载预训练权重,模型此时带的是默认词表(LVIS 常见类别) model = YOLOWorld("weights/yolov8s-world.pt") # 核心一步:用自然语言改词表,触发文本编码器重新生成嵌入 # 词越具体、越接近日常描述,召回越稳;缩写和内部代号效果通常很差 model.set_classes(["person", "forklift", "pallet", "cardboard box"]) # 推理:conf 控制置信度,iou 控制 NMS 合并强度 results = model.predict( source="assets/warehouse.jpg", conf=0.2, iou=0.5, imgsz=640, save=True, save_txt=True, ) # 逐条拿框,results 里每个元素对应一张图 for r in results: for box in r.boxes: cls_id = int(box.cls) print(model.names[cls_id], float(box.conf), box.xyxy.tolist())这里有一个容易忽略的点:set_classes不是零成本的,每调用一次都会跑一遍文本编码器,把 N 个类别的词向量算出来存到模型里。如果业务里每张图都换词表,耗时会很明显。常见做法是把词表固定下来,启动时调一次,之后复用同一个模型实例。
save_txt=True输出的是 YOLO 格式标签,方便后面和标注工具对齐;box.cls是整数索引,映射到model.names才是人看得懂的类别名。别拿索引当类别去写业务逻辑,词表一变索引就全乱。
3. 架构拆解:文本和视觉在哪里汇合
3.1 一次前向的完整数据流
把模型的执行顺序摊开看,YOLO-World 的前向分成三条支路,最后在 neck 处合并:
- 图像走 YOLOv8 的 backbone(CSPDarknet 结构),输出 P3、P4、P5 三个尺度的特征图;
- 词表经过 tokenizer 和冻结的 CLIP 文本编码器,输出形状为
(N, D)的类别嵌入,N 是类别数,D 是嵌入维度; - 两者在 RepVL-PAN 里做跨模态融合,检测头拿到融合后的特征,输出
(N + 4)维的预测,N 个分类分数加 4 个框坐标。
和普通 YOLO 最大的区别在检测头的分类通道数:普通 YOLO 的类别数在训练时固定,卷积权重形状写死;YOLO-World 的分类权重由文本嵌入动态决定,所以类别数是可以运行时变化的。它本质上是把「分类」从固定分类器换成了「特征与词向量的相似度匹配」,这个思路和开放词汇分割、CLIP 零样本分类是同一套。
3.2 文本侧:CLIP 编码器为什么必须冻结
文本编码器在整个训练过程中是冻结的,这属于设计层面的取舍,并非省事。开放词汇能力来自 CLIP 在图文对上预训练出来的语义空间,一旦在检测数据上更新文本侧参数,语义空间就会向训练集类别倾斜,模型对没见过的词立刻失去泛化能力,开放词汇这个前提就不成立了。
所以训练时实际更新的是视觉侧和融合模块,文本嵌入对模型来说更像「外部输入」。这也解释了一个现象:换个说法召回率变化很大。比如person和human在 CLIP 空间里很近,但worker可能就差一些;cardboard box比carton更常见于图文语料,效果通常更好。
我一般会准备一份同义词表,在set_classes之前做一次映射,把业务内部叫法翻译成语料里高频的自然语言描述:
# 业务词 -> 模型友好的自然语言描述 ALIAS = { "SKU_A": "cardboard box", "worker": "person", "AGV": "forklift", } raw_classes = ["SKU_A", "worker", "AGV"] classes = [ALIAS.get(c, c) for c in raw_classes] model.set_classes(classes)映射表是要进版本管理的,改一次就要跑一次回归,不然哪天某个类别的召回掉了都找不到原因。这部分后面第 6 章还会说到怎么用配置驱动。
3.3 视觉侧:RepVL-PAN 的跨模态注入
RepVL-PAN 可以理解为在 PANet 的每个融合节点上多接了一条来自文本的通道。具体有两个模块值得注意:
- T-CSPLayer(Text-guided CSPLayer):把文本嵌入当作 query,对视觉特征做注意力加权,让特征图里和当前类别相关的区域被强化;
- I-Pooling Attention:反过来,把图像特征池化成若干 token,去更新文本嵌入,让词向量带上一点当前图像的上下文信息。
这两个方向合起来是双向的,视觉特征被文本引导,文本嵌入也被图像微调。训练时这两个模块的参数会更新,推理时可以保留,也可以在重参数化阶段合并进卷积。资料里的configs/yolov8s-world.yaml能看到这些模块挂载的位置,比如:
# 结构示意,字段名以你手上版本的配置为准 backbone: - [-1, 1, Conv, [64, 3, 2]] # P1 # ... 中间省略 head: - [-1, 1, RepVL_PAN, [512, 1024]] # 跨模态融合层,接文本嵌入 - [[16, 19, 22], 1, Detect, [nc]] # 解耦检测头,nc 由词表决定读配置的时候重点关注RepVL_PAN那一行的输入输出通道,以及Detect层的nc从哪来。很多「训练 loss 正常但 mAP 是 0」的情况,根源就是这里的通道数和文本嵌入维度没有对齐,或者nc被硬编码成了固定值。
3.4 重参数化:把词表固化进权重
这是 YOLO-World 在工程上最有价值的一环。训练完或者确定词表之后,可以把文本嵌入直接融合进检测头的卷积权重,得到一个「普通」的检测器:推理时不再需要 CLIP,不再需要 tokenizer,速度回到常规 YOLO 水平。
from ultralytics import YOLOWorld # 词表一旦固定,就把它写进权重 WAREHOUSE_CLASSES = ["person", "forklift", "pallet", "cardboard box"] model = YOLOWorld("weights/yolov8s-world.pt") model.set_classes(WAREHOUSE_CLASSES) # 保存后得到的权重不再具备开放词汇能力,但推理链路最短 model.save("weights/yolov8s-world-warehouse.pt") # 验证:重新加载后 names 应该就是这四类 reloaded = YOLOWorld("weights/yolov8s-world-warehouse.pt") print(reloaded.names) # {0: 'person', 1: 'forklift', ...}这里需要说清楚代价:固化之后,这个权重就只认这四个类别了,set_classes再调用会覆盖掉原来的嵌入,等于把之前那份权重废掉。所以我的做法是永远保留一份未固化的原始权重,固化产物单独命名、单独归档,不要互相覆盖。这个习惯救过我一次,某次业务临时要加一个类别,手上只剩固化后的权重,只能从头再跑一遍。
4. 训练与微调:从 LVIS 到自己的数据集
4.1 数据格式与 names 字段的写法
YOLO-World 的训练数据最终都整理成 YOLO 格式:一张图对应一个 txt,每行class_id cx cy w h,坐标归一化到 0~1。难点在标注转换,LVIS 的 JSON 标注要转成这种格式,资料里的convert_lvis.py就是干这个的。核心逻辑简化一下大概是这样:
import json from pathlib import Path # LVIS 标注里 categories 是 [{"id": 1, "name": "person"}, ...] lvis = json.load(open("annotations/lvis_v1_train.json")) cat_map = {c["id"]: i for i, c in enumerate(lvis["categories"])} # 原始 id -> 连续 id names = {i: c["name"] for i, c in enumerate(lvis["categories"])} # images 里存宽高,用来把绝对坐标归一化 img_info = {im["id"]: im for im in lvis["images"]} out_dir = Path("labels/train2017") out_dir.mkdir(parents=True, exist_ok=True) for ann in lvis["annotations"]: if ann.get("iscrowd", 0): continue # 拥挤标注直接跳过,转出来是脏数据 im = img_info[ann["image_id"]] x, y, w, h = ann["bbox"] # LVIS 是左上角 + 宽高 cx = (x + w / 2) / im["width"] cy = (y + h / 2) / im["height"] nw, nh = w / im["width"], h / im["height"] cid = cat_map[ann["category_id"]] with open(out_dir / f"{im['id']:012d}.txt", "a") as f: f.write(f"{cid} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}\n")这段代码有三个细节值得注意:iscrowd必须过滤,否则大量重叠框会把 NMS 搞得很难看;LVIS 的 bbox 是左上角加宽高,不是中心点;类别 id 要重新映射成从 0 开始的连续整数,直接拿原始 id 会得到一个几万维的分类头。
数据集 yaml 里的names是开放词汇效果的关键,因为文本嵌入就是按这里逐字编码的。所以不要写cls_0、cls_1,要写真实短语:
path: datasets/lvis train: images/train2017 val: images/val2017 nc: 4 names: 0: person 1: forklift 2: pallet 3: cardboard box注意:
names改完之后,训练启动时会重新生成文本嵌入缓存。如果缓存目录里还留着上一次的旧文件,模型会拿旧词向量训新类别,loss 看起来在降但验证集 mAP 上不去,这个问题后面第 5 章单独说。
4.2 训练脚本与关键超参
训练入口用框架封装的train就行,重点是几个不那么直观的参数:
from ultralytics import YOLOWorld # 从预训练权重起步,不要从零训,开放词汇能力是预训练带过来的 model = YOLOWorld("weights/yolov8s-world.pt") model.train( data="datasets/lvis/lvis.yaml", epochs=100, imgsz=640, batch=32, # 显存不够先降到 16,再开 accumulate 补等效 batch device="0,1", # 多卡时这里是列表字符串 workers=8, # 数据加载线程,和 CPU 核数匹配 optimizer="AdamW", lr0=2e-4, # 开放词汇微调的学习率通常比常规检测低一个量级 lrf=0.01, # 余弦退火终点比例 warmup_epochs=3, close_mosaic=10, # 最后 10 轮关掉 mosaic,让模型收敛到真实分布 cache="ram", # 数据集能塞进内存就开,省掉大量 IO 等待 patience=30, # 30 轮不涨就早停,省钱 project="runs/world", name="warehouse-v1", )lr0=2e-4是这套配置里最需要谨慎的参数。文本编码器冻结,但融合模块和检测头在学习,学习率太大会把预训练学到的跨模态对齐破坏掉,表现为前几轮 loss 剧烈震荡;太小又挪不动,一百轮下来 mAP 几乎不动。我的经验是从 2e-4 起,观察前 20 轮曲线再决定是否调。
close_mosaic=10这个参数很多人忽略。mosaic 增强会把四张图拼一起,对定位和小目标有好处,但它引入的分布偏移在训练末期会拖慢收敛。开放词汇任务本身就有跨类别的分布差异,末期关掉增强能明显改善最终指标。
4.3 验证与指标解读
训练完第一件事是跑验证,不是直接上线:
yolo val model=runs/world/warehouse-v1/weights/best.pt data=datasets/lvis/lvis.yaml imgsz=640看三个数:mAP50、mAP50-95,以及各类别的 AP 明细。开放词汇场景下,指标的特点是「大类高、小类崩」,比如person能到 0.6 而pallet只有 0.1。这不是训练失败,是 CLIP 语义空间里这两个词与视觉概念的贴合程度不同。解决办法通常不是加数据,而是改词表描述——wooden pallet可能比pallet好不少。
还有一个容易误解的点:验证集的类别必须和训练集names一致,开放词汇不等于验证时随便换词。想在验证阶段测试「没见过的类别」,只能用单独准备好的 few-shot 子集,用预训练权重直接推理,走的是零样本路径,不经过训练。
5. 常见问题排查:这类任务的几个常见问题
5.1 现象:换个词召回掉一半,指标像随机数
现象:同一张图,词表从["person", "box"]改成["human", "carton"],检测结果天差地别,甚至一个框都没有。
原因:CLIP 文本嵌入对不同表述的敏感度远高于人的直觉。常见词、具体名词、日常语料里高频的组合效果好;缩写、内部代号、罕见复合词效果差。这不是模型不稳定,是语义空间本身的分布问题。
解决:建立业务词到自然语言描述的同义词表,在set_classes前统一映射;每次改词表都跑同一批固定图片做对比,只看指标不看单张图的肉眼效果。
5.2 现象:模型保存后重新加载,类别全变成默认值
现象:model.set_classes([...])之后用model.save()保存,重新YOLOWorld(path)加载,model.names又回到了 LVIS 的默认类别。
原因:权重里保存的类别信息和配置的解析顺序有关,部分版本加载时会用配置里的nc覆盖权重中的 names。
解决:加载后立刻重新set_classes一次,用统一的词表配置文件驱动,不要依赖权重自带的 names。固化权重的场景下,在加载后就打印model.names做断言检查,早发现比上线后发现好。
5.3 现象:训练 loss 在降,验证 mAP 一直是 0
现象:训练日志里 box_loss、cls_loss 都在正常下降,但yolo val跑出来 mAP50 接近 0,混淆矩阵全空。
原因:文本嵌入缓存没有随数据集更新。框架会把文本嵌入缓存到磁盘,换了数据集但缓存目录没清理,模型拿的是旧词向量,预测出来的类别 id 和验证集对不上。
解决:换数据集或改names之后,手动清掉缓存目录(通常在数据集同级或~/.cache下),强制重新生成;也可以在训练脚本开头加一行删除缓存的逻辑。这件事没有后悔药,只能每次改完数据先清一遍。
5.4 现象:显存爆炸,batch 开到 8 就 OOM
现象:24G 显存,batch 开到 8 就报 out of memory,但同尺寸的普通 YOLOv8 能开到 32。
原因:RepVL-PAN 的跨模态注意力额外占用显存,文本嵌入和视觉特征做注意力时会产生中间张量;另外开放词汇训练时类别数远大于常规检测(LVIS 上千类),检测头的分类分支显存占用随类别数线性增长。
解决:先把imgsz从 640 降到 512 试;再考虑冻结 backbone 前几层;如果类别数确实很大,分阶段训练——先用全量类别训融合模块,再裁词表精调。别硬扛,OOM 反复重试很耗时间。
5.5 现象:导出 ONNX 之后检测框全乱
现象:PyTorch 下正常,导出 ONNX 后用 onnxruntime 推理,框的数量和位置都对不上。
原因:没有做重参数化就导出。开放词汇模型的前向里有动态的文本嵌入输入,ONNX 对这种动态形状支持有限,导出时文本分支被静态化或者直接丢失。
解决:导出前必须先set_classes固化词表,让模型变成固定类别数的普通检测器,再执行导出。顺序不能反,反了就只能重导。
6. 二次开发:把开放词汇能力接进业务管线
真正落地的时候,set_classes那几行代码只是开头,麻烦在后面:词表要跟着业务变、模型要能热更新、改了词表得知道有没有变差。我在项目里一般会把词表从代码里抽出来,做成配置驱动。
import yaml from ultralytics import YOLOWorld # 词表配置单独一个文件,改词表不用改代码 cfg = yaml.safe_load(open("configs/classes.yaml")) classes = cfg["classes"] # ["person", "forklift", "pallet"] alias = cfg.get("alias", {}) # 业务别名映射 model = YOLOWorld("weights/yolov8s-world.pt") model.set_classes([alias.get(c, c) for c in classes]) # 保存一份带版本号的固化权重,方便回滚 version = cfg["version"] model.save(f"weights/warehouse-{version}.pt")配置里带version字段是血泪经验。词表改动在业务侧看起来是小事,但对模型来说等于换了一个任务,没有版本号就没法回滚,出问题只能靠记忆猜上次是哪份权重。配置和权重同名归档,出问题时切回去只要改一行路径。
导出这一环,顺序必须固定:先固化词表,再导出。导出后的模型走标准检测管线,速度能回到常规 YOLO 水平,适合放到边缘设备上跑。导出脚本我一般写成幂等的,每次全量重新导出,不做增量覆盖,这样权重和导出产物永远对得上。
验证这块,别只看整体 mAP。开放词汇模型的风险在于「某一类悄悄失效」,整体指标被大类撑着看不出来。我的做法是维护一个 200 张左右的小回归集,覆盖每个类别至少 20 个正样本,每次改词表或换权重都跑一遍,逐类看 AP。这个集子不用大,但必须稳定,图片和标注不能随便动。
| 验证项 | 关注点 | 触发条件 |
|---|---|---|
| 逐类 AP | 单类是否掉超过 5 个点 | 每次词表变更 |
| 推理耗时 | 固化前后对比,确认重参数化生效 | 每次导出 |
| 空框率 | 某些类别是否完全不输出 | 每次换权重 |
| 词表覆盖 | 业务新词是否已进配置 | 每周巡检 |
去年有一次,我在没有跑回归集的情况下直接换了一版权重,上线两天后发现某个次要类别的检测框全没了,偏偏这个类别在整体指标里权重很低,从 mAP 上根本看不出来。从那以后我每次改词表、换权重、重新导出,都强制走一遍那份 200 张的小回归集,逐类对比,掉点就停下来查。这个习惯看起来笨,但比事后回滚便宜太多。希望这套流程帮到你,少走几次我走过的弯路。
本文还有配套的精品资源,点击获取