YOLO-World开放词汇检测实战:CLIP融合、微调与重参数化
简介面向计算机视觉研究者和目标检测开发者的 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 python3.10 -y conda activate yoloworld # 2. 先装 PyTorch单独指定 CUDA 版本别让 pip 自动给你挑 pip install torch2.1.0 torchvision0.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, devicecuda) 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 modelweights/yolov8s-world.pt sourceassets/test.jpg conf0.25 imgsz640 saveTrueconf0.25是置信度阈值开放词汇模型对没见过的类别分数普遍偏低一开始可以放到 0.1 观察召回再往上收紧imgsz640和训练分辨率保持一致改大能提升小目标但显存线性上涨。真正接入业务用的是 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( sourceassets/warehouse.jpg, conf0.2, iou0.5, imgsz640, saveTrue, save_txtTrue, ) # 逐条拿框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_txtTrue输出的是 YOLO 格式标签方便后面和标注工具对齐box.cls是整数索引映射到model.names才是人看得懂的类别名。别拿索引当类别去写业务逻辑词表一变索引就全乱。3. 架构拆解文本和视觉在哪里汇合3.1 一次前向的完整数据流把模型的执行顺序摊开看YOLO-World 的前向分成三条支路最后在 neck 处合并图像走 YOLOv8 的 backboneCSPDarknet 结构输出 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-CSPLayerText-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(parentsTrue, exist_okTrue) 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( datadatasets/lvis/lvis.yaml, epochs100, imgsz640, batch32, # 显存不够先降到 16再开 accumulate 补等效 batch device0,1, # 多卡时这里是列表字符串 workers8, # 数据加载线程和 CPU 核数匹配 optimizerAdamW, lr02e-4, # 开放词汇微调的学习率通常比常规检测低一个量级 lrf0.01, # 余弦退火终点比例 warmup_epochs3, close_mosaic10, # 最后 10 轮关掉 mosaic让模型收敛到真实分布 cacheram, # 数据集能塞进内存就开省掉大量 IO 等待 patience30, # 30 轮不涨就早停省钱 projectruns/world, namewarehouse-v1, )lr02e-4是这套配置里最需要谨慎的参数。文本编码器冻结但融合模块和检测头在学习学习率太大会把预训练学到的跨模态对齐破坏掉表现为前几轮 loss 剧烈震荡太小又挪不动一百轮下来 mAP 几乎不动。我的经验是从 2e-4 起观察前 20 轮曲线再决定是否调。close_mosaic10这个参数很多人忽略。mosaic 增强会把四张图拼一起对定位和小目标有好处但它引入的分布偏移在训练末期会拖慢收敛。开放词汇任务本身就有跨类别的分布差异末期关掉增强能明显改善最终指标。4.3 验证与指标解读训练完第一件事是跑验证不是直接上线yolo val modelruns/world/warehouse-v1/weights/best.pt datadatasets/lvis/lvis.yaml imgsz640看三个数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(fweights/warehouse-{version}.pt)配置里带version字段是血泪经验。词表改动在业务侧看起来是小事但对模型来说等于换了一个任务没有版本号就没法回滚出问题只能靠记忆猜上次是哪份权重。配置和权重同名归档出问题时切回去只要改一行路径。导出这一环顺序必须固定先固化词表再导出。导出后的模型走标准检测管线速度能回到常规 YOLO 水平适合放到边缘设备上跑。导出脚本我一般写成幂等的每次全量重新导出不做增量覆盖这样权重和导出产物永远对得上。验证这块别只看整体 mAP。开放词汇模型的风险在于「某一类悄悄失效」整体指标被大类撑着看不出来。我的做法是维护一个 200 张左右的小回归集覆盖每个类别至少 20 个正样本每次改词表或换权重都跑一遍逐类看 AP。这个集子不用大但必须稳定图片和标注不能随便动。验证项关注点触发条件逐类 AP单类是否掉超过 5 个点每次词表变更推理耗时固化前后对比确认重参数化生效每次导出空框率某些类别是否完全不输出每次换权重词表覆盖业务新词是否已进配置每周巡检去年有一次我在没有跑回归集的情况下直接换了一版权重上线两天后发现某个次要类别的检测框全没了偏偏这个类别在整体指标里权重很低从 mAP 上根本看不出来。从那以后我每次改词表、换权重、重新导出都强制走一遍那份 200 张的小回归集逐类对比掉点就停下来查。这个习惯看起来笨但比事后回滚便宜太多。希望这套流程帮到你少走几次我走过的弯路。本文还有配套的精品资源点击获取