放詞匯檢測(cè)實(shí)戰(zhàn):CLIP融合、微調(diào)與重參數(shù)化)
簡(jiǎn)介面向計(jì)算機(jī)視覺(jué)研究者和目標(biāo)檢測(cè)開(kāi)發(fā)者的 YOLO-World 學(xué)習(xí)與復(fù)現(xiàn)資料包匯集 PyTorch 實(shí)現(xiàn)、預(yù)訓(xùn)練權(quán)重以及預(yù)訓(xùn)練/微調(diào)代碼重點(diǎn)解決開(kāi)放詞匯檢測(cè)與 grounding 場(chǎng)景下的模型復(fù)現(xiàn)和二次開(kāi)發(fā)問(wèn)題。資料圍繞 prompt-then-detect 推理范式組織支持用戶自定義詞匯的高效檢測(cè)實(shí)驗(yàn)適合具備一定深度學(xué)習(xí)基礎(chǔ)、希望深入理解開(kāi)放詞匯目標(biāo)檢測(cè)流程的讀者。壓縮包共 161 個(gè)文件整體約 2.35MB以 110 個(gè) Python 源碼和 20 個(gè) Markdown 文檔為主體另含 txt 配置說(shuō)明、json 類別文本、ipynb 推理示例、Dockerfile 與 shell 腳本等覆蓋訓(xùn)練、推理和部署參考。已有 883 人學(xué)習(xí)下載讀者可據(jù)此梳理模型結(jié)構(gòu)、數(shù)據(jù)配置、權(quán)重加載與推理入口理解預(yù)訓(xùn)練及微調(diào)腳本的組織方式并借助容器配置線索搭建實(shí)驗(yàn)環(huán)境為開(kāi)放詞匯檢測(cè)實(shí)驗(yàn)、工程集成和后續(xù)二次開(kāi)發(fā)提供較完整的參考。1. 從「模型不認(rèn)新類別」開(kāi)始YOLO-World 解決的是什么麻煩去年幫一個(gè)做倉(cāng)儲(chǔ)視覺(jué)的朋友看需求貨架上的 SKU 每周都在變用傳統(tǒng)檢測(cè)器每加一個(gè)品類就要重新標(biāo)幾千張圖、重訓(xùn)一輪標(biāo)注排期比開(kāi)發(fā)還長(zhǎng)項(xiàng)目直接卡住。YOLO-World 就是用來(lái)解決這個(gè)問(wèn)題的它把 CLIP 文本編碼器和 YOLOv8 檢測(cè)器拼在一起給幾個(gè)自然語(yǔ)言詞person、forklift、pallet模型就按這些詞去圖里找目標(biāo)不用重新訓(xùn)練就能換詞表。這份資料包含源碼和文檔核心是開(kāi)放詞匯檢測(cè)的完整鏈路推理、微調(diào)、重參數(shù)化、導(dǎo)出。適合兩類人一類是要在幾天內(nèi)驗(yàn)證「這個(gè)類別能不能被檢測(cè)出來(lái)」的算法同學(xué)另一類是接手檢測(cè)項(xiàng)目、被新增類別反復(fù)拖慢節(jié)奏的工程同學(xué)。它不是萬(wàn)能方案精度天花板低于針對(duì)性微調(diào)的專用模型但在快速驗(yàn)證階段能省掉大量標(biāo)注開(kāi)銷這個(gè)賬很劃算。2. 環(huán)境與依賴把 PyTorch 這套跑起來(lái)2.1 目錄結(jié)構(gòu)與各文件定位資料包解壓后我按功能分成四類來(lái)看權(quán)重、配置、數(shù)據(jù)、腳本。先不管文件名的細(xì)節(jié)得先搞清楚每塊負(fù)責(zé)什么否則后面改配置時(shí)改錯(cuò)地方模型會(huì)不報(bào)錯(cuò)但就是沒(méi)有效果。yolo-world/ ├── weights/ # 預(yù)訓(xùn)練權(quán)重 │ ├── yolov8s-world.pt # s/m/l/x 幾檔按你的算力選 │ └── clip-vit-base-patch32/ # 文本編碼器權(quán)重離線環(huán)境要手動(dòng)放 ├── configs/ │ ├── yolov8s-world.yaml # 模型結(jié)構(gòu)backbone RepVL-PAN head │ └── lvis.yaml # 數(shù)據(jù)集描述 ├── datasets/ │ └── lvis/ │ ├── images/ # train2017 / val2017 │ ├── labels/ # YOLO txt 格式 │ └── lvis.yaml # names 映射這是開(kāi)放詞匯的關(guān)鍵 ├── scripts/ │ ├── convert_lvis.py # 官方標(biāo)注轉(zhuǎn) YOLO txt │ ├── train_world.py # 訓(xùn)練入口 │ └── export_world.py # 導(dǎo)出 ONNX / engine └── docs/ ├── quickstart.md └── finetune.md職責(zé)劃分很清楚configs/*.yaml決定網(wǎng)絡(luò)有多少層、融合模塊接在哪datasets/*/lvis.yaml決定文本編碼器看到哪些詞weights/決定初始參數(shù)。改動(dòng)最多的兩個(gè)地方是lvis.yaml的names和訓(xùn)練腳本里的超參這兩個(gè)地方一旦弄錯(cuò)損失曲線會(huì)變得很難解釋后面第 5 章會(huì)展開(kāi)。提示不同小版本的 ultralytics 目錄名會(huì)變比如模型配置可能放在包內(nèi)的cfg/models/v8/下以你手上那份文檔里的實(shí)際路徑為準(zhǔn)先別急著改先執(zhí)行一次 import 并跑通一條推理再考慮改結(jié)構(gòu)。2.2 依賴安裝與 GPU 環(huán)境自檢這套代碼對(duì) PyTorch 版本比較挑CUDA 驅(qū)動(dòng)、torch、torchvision 三者版本對(duì)不上最常見(jiàn)的表現(xiàn)是能 import 但一跑到 forward 就報(bào)CUDA error: no kernel image is available。我的習(xí)慣是先建獨(dú)立環(huán)境再逐項(xiàng)驗(yàn)證不要一口氣把 requirements 全裝上然后靠運(yùn)氣跑起來(lái)。# 1. 建獨(dú)立環(huán)境Python 3.10 是兼容性最穩(wěn)的一檔 conda create -n yoloworld python3.10 -y conda activate yoloworld # 2. 先裝 PyTorch單獨(dú)指定 CUDA 版本別讓 pip 自動(dòng)給你挑 pip install torch2.1.0 torchvision0.16.0 --index-url https://download.pytorch.org/whl/cu118 # 3. 再裝檢測(cè)框架和其余依賴 pip install ultralytics opencv-python pyyaml tqdm # 4. 文本編碼器依賴部分版本需要單獨(dú)裝 CLIP 分支 # 具體包名和地址以你那份文檔的 requirements 為準(zhǔn) pip install -r requirements.txt裝完之后不要急著跑訓(xùn)練先做三行自檢把 GPU 狀態(tài)和框架版本打出來(lái)import torch import ultralytics # 顯存和算力檔位決定了你能開(kāi)多大 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__) # 跑一次小矩陣乘確認(rèn) kernel 真的能執(zhí)行不只是 is_available 為真 x torch.randn(1024, 1024, devicecuda) print(matmul ok:, (x x).shape)torch.cuda.is_available()返回 True 但矩陣乘報(bào)錯(cuò)基本都是 CUDA 版本和 torch 編譯版本不匹配這時(shí)重裝 torch 比查其他原因快得多。這一步只要五秒鐘卻能省掉后面半小時(shí)的困惑。權(quán)重這塊要注意yolov8s-world.pt里已經(jīng)固化了 CLIP 文本編碼器的結(jié)構(gòu)信息但文本權(quán)重本身可能是單獨(dú)一份。文檔里如果提到需要額外下載clip-vit-base-patch32必須放到指定目錄否則set_classes會(huì)直接拋找不到模型的異常。離線環(huán)境要提前把這兩份文件拷進(jìn)去在線下載在訓(xùn)練機(jī)上是常見(jiàn)問(wèn)題。2.3 兩條推理入口命令行與 Python API跑通的第一條命令建議用命令行參數(shù)少、報(bào)錯(cuò)直觀# 直接用預(yù)訓(xùn)練權(quán)重推理先確認(rèn)模型能出框 yolo predict modelweights/yolov8s-world.pt sourceassets/test.jpg conf0.25 imgsz640 saveTrueconf0.25是置信度閾值開(kāi)放詞匯模型對(duì)沒(méi)見(jiàn)過(guò)的類別分?jǐn)?shù)普遍偏低一開(kāi)始可以放到 0.1 觀察召回再往上收緊imgsz640和訓(xùn)練分辨率保持一致改大能提升小目標(biāo)但顯存線性上漲。真正接入業(yè)務(wù)用的是 Python API因?yàn)橐谕评砬皠?dòng)態(tài)改詞表from ultralytics import YOLOWorld # 加載預(yù)訓(xùn)練權(quán)重模型此時(shí)帶的是默認(rèn)詞表LVIS 常見(jiàn)類別 model YOLOWorld(weights/yolov8s-world.pt) # 核心一步用自然語(yǔ)言改詞表觸發(fā)文本編碼器重新生成嵌入 # 詞越具體、越接近日常描述召回越穩(wěn)縮寫(xiě)和內(nèi)部代號(hào)效果通常很差 model.set_classes([person, forklift, pallet, cardboard box]) # 推理conf 控制置信度iou 控制 NMS 合并強(qiáng)度 results model.predict( sourceassets/warehouse.jpg, conf0.2, iou0.5, imgsz640, saveTrue, save_txtTrue, ) # 逐條拿框results 里每個(gè)元素對(duì)應(yīng)一張圖 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())這里有一個(gè)容易忽略的點(diǎn)set_classes不是零成本的每調(diào)用一次都會(huì)跑一遍文本編碼器把 N 個(gè)類別的詞向量算出來(lái)存到模型里。如果業(yè)務(wù)里每張圖都換詞表耗時(shí)會(huì)很明顯。常見(jiàn)做法是把詞表固定下來(lái)啟動(dòng)時(shí)調(diào)一次之后復(fù)用同一個(gè)模型實(shí)例。save_txtTrue輸出的是 YOLO 格式標(biāo)簽方便后面和標(biāo)注工具對(duì)齊box.cls是整數(shù)索引映射到model.names才是人看得懂的類別名。別拿索引當(dāng)類別去寫(xiě)業(yè)務(wù)邏輯詞表一變索引就全亂。3. 架構(gòu)拆解文本和視覺(jué)在哪里匯合3.1 一次前向的完整數(shù)據(jù)流把模型的執(zhí)行順序攤開(kāi)看YOLO-World 的前向分成三條支路最后在 neck 處合并圖像走 YOLOv8 的 backboneCSPDarknet 結(jié)構(gòu)輸出 P3、P4、P5 三個(gè)尺度的特征圖詞表經(jīng)過(guò) tokenizer 和凍結(jié)的 CLIP 文本編碼器輸出形狀為(N, D)的類別嵌入N 是類別數(shù)D 是嵌入維度兩者在 RepVL-PAN 里做跨模態(tài)融合檢測(cè)頭拿到融合后的特征輸出(N 4)維的預(yù)測(cè)N 個(gè)分類分?jǐn)?shù)加 4 個(gè)框坐標(biāo)。和普通 YOLO 最大的區(qū)別在檢測(cè)頭的分類通道數(shù)普通 YOLO 的類別數(shù)在訓(xùn)練時(shí)固定卷積權(quán)重形狀寫(xiě)死YOLO-World 的分類權(quán)重由文本嵌入動(dòng)態(tài)決定所以類別數(shù)是可以運(yùn)行時(shí)變化的。它本質(zhì)上是把「分類」從固定分類器換成了「特征與詞向量的相似度匹配」這個(gè)思路和開(kāi)放詞匯分割、CLIP 零樣本分類是同一套。3.2 文本側(cè)CLIP 編碼器為什么必須凍結(jié)文本編碼器在整個(gè)訓(xùn)練過(guò)程中是凍結(jié)的這屬于設(shè)計(jì)層面的取舍并非省事。開(kāi)放詞匯能力來(lái)自 CLIP 在圖文對(duì)上預(yù)訓(xùn)練出來(lái)的語(yǔ)義空間一旦在檢測(cè)數(shù)據(jù)上更新文本側(cè)參數(shù)語(yǔ)義空間就會(huì)向訓(xùn)練集類別傾斜模型對(duì)沒(méi)見(jiàn)過(guò)的詞立刻失去泛化能力開(kāi)放詞匯這個(gè)前提就不成立了。所以訓(xùn)練時(shí)實(shí)際更新的是視覺(jué)側(cè)和融合模塊文本嵌入對(duì)模型來(lái)說(shuō)更像「外部輸入」。這也解釋了一個(gè)現(xiàn)象換個(gè)說(shuō)法召回率變化很大。比如person和human在 CLIP 空間里很近但worker可能就差一些cardboard box比carton更常見(jiàn)于圖文語(yǔ)料效果通常更好。我一般會(huì)準(zhǔn)備一份同義詞表在set_classes之前做一次映射把業(yè)務(wù)內(nèi)部叫法翻譯成語(yǔ)料里高頻的自然語(yǔ)言描述# 業(yè)務(wù)詞 - 模型友好的自然語(yǔ)言描述 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)映射表是要進(jìn)版本管理的改一次就要跑一次回歸不然哪天某個(gè)類別的召回掉了都找不到原因。這部分后面第 6 章還會(huì)說(shuō)到怎么用配置驅(qū)動(dòng)。3.3 視覺(jué)側(cè)RepVL-PAN 的跨模態(tài)注入RepVL-PAN 可以理解為在 PANet 的每個(gè)融合節(jié)點(diǎn)上多接了一條來(lái)自文本的通道。具體有兩個(gè)模塊值得注意T-CSPLayerText-guided CSPLayer把文本嵌入當(dāng)作 query對(duì)視覺(jué)特征做注意力加權(quán)讓特征圖里和當(dāng)前類別相關(guān)的區(qū)域被強(qiáng)化I-Pooling Attention反過(guò)來(lái)把圖像特征池化成若干 token去更新文本嵌入讓詞向量帶上一點(diǎn)當(dāng)前圖像的上下文信息。這兩個(gè)方向合起來(lái)是雙向的視覺(jué)特征被文本引導(dǎo)文本嵌入也被圖像微調(diào)。訓(xùn)練時(shí)這兩個(gè)模塊的參數(shù)會(huì)更新推理時(shí)可以保留也可以在重參數(shù)化階段合并進(jìn)卷積。資料里的configs/yolov8s-world.yaml能看到這些模塊掛載的位置比如# 結(jié)構(gòu)示意字段名以你手上版本的配置為準(zhǔn) backbone: - [-1, 1, Conv, [64, 3, 2]] # P1 # ... 中間省略 head: - [-1, 1, RepVL_PAN, [512, 1024]] # 跨模態(tài)融合層接文本嵌入 - [[16, 19, 22], 1, Detect, [nc]] # 解耦檢測(cè)頭nc 由詞表決定讀配置的時(shí)候重點(diǎn)關(guān)注RepVL_PAN那一行的輸入輸出通道以及Detect層的nc從哪來(lái)。很多「訓(xùn)練 loss 正常但 mAP 是 0」的情況根源就是這里的通道數(shù)和文本嵌入維度沒(méi)有對(duì)齊或者nc被硬編碼成了固定值。3.4 重參數(shù)化把詞表固化進(jìn)權(quán)重這是 YOLO-World 在工程上最有價(jià)值的一環(huán)。訓(xùn)練完或者確定詞表之后可以把文本嵌入直接融合進(jìn)檢測(cè)頭的卷積權(quán)重得到一個(gè)「普通」的檢測(cè)器推理時(shí)不再需要 CLIP不再需要 tokenizer速度回到常規(guī) YOLO 水平。from ultralytics import YOLOWorld # 詞表一旦固定就把它寫(xiě)進(jìn)權(quán)重 WAREHOUSE_CLASSES [person, forklift, pallet, cardboard box] model YOLOWorld(weights/yolov8s-world.pt) model.set_classes(WAREHOUSE_CLASSES) # 保存后得到的權(quán)重不再具備開(kāi)放詞匯能力但推理鏈路最短 model.save(weights/yolov8s-world-warehouse.pt) # 驗(yàn)證重新加載后 names 應(yīng)該就是這四類 reloaded YOLOWorld(weights/yolov8s-world-warehouse.pt) print(reloaded.names) # {0: person, 1: forklift, ...}這里需要說(shuō)清楚代價(jià)固化之后這個(gè)權(quán)重就只認(rèn)這四個(gè)類別了set_classes再調(diào)用會(huì)覆蓋掉原來(lái)的嵌入等于把之前那份權(quán)重廢掉。所以我的做法是永遠(yuǎn)保留一份未固化的原始權(quán)重固化產(chǎn)物單獨(dú)命名、單獨(dú)歸檔不要互相覆蓋。這個(gè)習(xí)慣救過(guò)我一次某次業(yè)務(wù)臨時(shí)要加一個(gè)類別手上只剩固化后的權(quán)重只能從頭再跑一遍。4. 訓(xùn)練與微調(diào)從 LVIS 到自己的數(shù)據(jù)集4.1 數(shù)據(jù)格式與 names 字段的寫(xiě)法YOLO-World 的訓(xùn)練數(shù)據(jù)最終都整理成 YOLO 格式一張圖對(duì)應(yīng)一個(gè) txt每行class_id cx cy w h坐標(biāo)歸一化到 0~1。難點(diǎn)在標(biāo)注轉(zhuǎn)換LVIS 的 JSON 標(biāo)注要轉(zhuǎn)成這種格式資料里的convert_lvis.py就是干這個(gè)的。核心邏輯簡(jiǎn)化一下大概是這樣import json from pathlib import Path # LVIS 標(biāo)注里 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 - 連續(xù) id names {i: c[name] for i, c in enumerate(lvis[categories])} # images 里存寬高用來(lái)把絕對(duì)坐標(biāo)歸一化 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 # 擁擠標(biāo)注直接跳過(guò)轉(zhuǎn)出來(lái)是臟數(shù)據(jù) 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)這段代碼有三個(gè)細(xì)節(jié)值得注意iscrowd必須過(guò)濾否則大量重疊框會(huì)把 NMS 搞得很難看LVIS 的 bbox 是左上角加寬高不是中心點(diǎn)類別 id 要重新映射成從 0 開(kāi)始的連續(xù)整數(shù)直接拿原始 id 會(huì)得到一個(gè)幾萬(wàn)維的分類頭。數(shù)據(jù)集 yaml 里的names是開(kāi)放詞匯效果的關(guān)鍵因?yàn)槲谋厩度刖褪前催@里逐字編碼的。所以不要寫(xiě)cls_0、cls_1要寫(xiě)真實(shí)短語(yǔ)path: datasets/lvis train: images/train2017 val: images/val2017 nc: 4 names: 0: person 1: forklift 2: pallet 3: cardboard box注意names改完之后訓(xùn)練啟動(dòng)時(shí)會(huì)重新生成文本嵌入緩存。如果緩存目錄里還留著上一次的舊文件模型會(huì)拿舊詞向量訓(xùn)新類別loss 看起來(lái)在降但驗(yàn)證集 mAP 上不去這個(gè)問(wèn)題后面第 5 章單獨(dú)說(shuō)。4.2 訓(xùn)練腳本與關(guān)鍵超參訓(xùn)練入口用框架封裝的train就行重點(diǎn)是幾個(gè)不那么直觀的參數(shù)from ultralytics import YOLOWorld # 從預(yù)訓(xùn)練權(quán)重起步不要從零訓(xùn)開(kāi)放詞匯能力是預(yù)訓(xùn)練帶過(guò)來(lái)的 model YOLOWorld(weights/yolov8s-world.pt) model.train( datadatasets/lvis/lvis.yaml, epochs100, imgsz640, batch32, # 顯存不夠先降到 16再開(kāi) accumulate 補(bǔ)等效 batch device0,1, # 多卡時(shí)這里是列表字符串 workers8, # 數(shù)據(jù)加載線程和 CPU 核數(shù)匹配 optimizerAdamW, lr02e-4, # 開(kāi)放詞匯微調(diào)的學(xué)習(xí)率通常比常規(guī)檢測(cè)低一個(gè)量級(jí) lrf0.01, # 余弦退火終點(diǎn)比例 warmup_epochs3, close_mosaic10, # 最后 10 輪關(guān)掉 mosaic讓模型收斂到真實(shí)分布 cacheram, # 數(shù)據(jù)集能塞進(jìn)內(nèi)存就開(kāi)省掉大量 IO 等待 patience30, # 30 輪不漲就早停省錢 projectruns/world, namewarehouse-v1, )lr02e-4是這套配置里最需要謹(jǐn)慎的參數(shù)。文本編碼器凍結(jié)但融合模塊和檢測(cè)頭在學(xué)習(xí)學(xué)習(xí)率太大會(huì)把預(yù)訓(xùn)練學(xué)到的跨模態(tài)對(duì)齊破壞掉表現(xiàn)為前幾輪 loss 劇烈震蕩太小又挪不動(dòng)一百輪下來(lái) mAP 幾乎不動(dòng)。我的經(jīng)驗(yàn)是從 2e-4 起觀察前 20 輪曲線再?zèng)Q定是否調(diào)。close_mosaic10這個(gè)參數(shù)很多人忽略。mosaic 增強(qiáng)會(huì)把四張圖拼一起對(duì)定位和小目標(biāo)有好處但它引入的分布偏移在訓(xùn)練末期會(huì)拖慢收斂。開(kāi)放詞匯任務(wù)本身就有跨類別的分布差異末期關(guān)掉增強(qiáng)能明顯改善最終指標(biāo)。4.3 驗(yàn)證與指標(biāo)解讀訓(xùn)練完第一件事是跑驗(yàn)證不是直接上線yolo val modelruns/world/warehouse-v1/weights/best.pt datadatasets/lvis/lvis.yaml imgsz640看三個(gè)數(shù)mAP50、mAP50-95以及各類別的 AP 明細(xì)。開(kāi)放詞匯場(chǎng)景下指標(biāo)的特點(diǎn)是「大類高、小類崩」比如person能到 0.6 而pallet只有 0.1。這不是訓(xùn)練失敗是 CLIP 語(yǔ)義空間里這兩個(gè)詞與視覺(jué)概念的貼合程度不同。解決辦法通常不是加數(shù)據(jù)而是改詞表描述——wooden pallet可能比pallet好不少。還有一個(gè)容易誤解的點(diǎn)驗(yàn)證集的類別必須和訓(xùn)練集names一致開(kāi)放詞匯不等于驗(yàn)證時(shí)隨便換詞。想在驗(yàn)證階段測(cè)試「沒(méi)見(jiàn)過(guò)的類別」只能用單獨(dú)準(zhǔn)備好的 few-shot 子集用預(yù)訓(xùn)練權(quán)重直接推理走的是零樣本路徑不經(jīng)過(guò)訓(xùn)練。5. 常見(jiàn)問(wèn)題排查這類任務(wù)的幾個(gè)常見(jiàn)問(wèn)題5.1 現(xiàn)象換個(gè)詞召回掉一半指標(biāo)像隨機(jī)數(shù)現(xiàn)象同一張圖詞表從[person, box]改成[human, carton]檢測(cè)結(jié)果天差地別甚至一個(gè)框都沒(méi)有。原因CLIP 文本嵌入對(duì)不同表述的敏感度遠(yuǎn)高于人的直覺(jué)。常見(jiàn)詞、具體名詞、日常語(yǔ)料里高頻的組合效果好縮寫(xiě)、內(nèi)部代號(hào)、罕見(jiàn)復(fù)合詞效果差。這不是模型不穩(wěn)定是語(yǔ)義空間本身的分布問(wèn)題。解決建立業(yè)務(wù)詞到自然語(yǔ)言描述的同義詞表在set_classes前統(tǒng)一映射每次改詞表都跑同一批固定圖片做對(duì)比只看指標(biāo)不看單張圖的肉眼效果。5.2 現(xiàn)象模型保存后重新加載類別全變成默認(rèn)值現(xiàn)象model.set_classes([...])之后用model.save()保存重新YOLOWorld(path)加載model.names又回到了 LVIS 的默認(rèn)類別。原因權(quán)重里保存的類別信息和配置的解析順序有關(guān)部分版本加載時(shí)會(huì)用配置里的nc覆蓋權(quán)重中的 names。解決加載后立刻重新set_classes一次用統(tǒng)一的詞表配置文件驅(qū)動(dòng)不要依賴權(quán)重自帶的 names。固化權(quán)重的場(chǎng)景下在加載后就打印model.names做斷言檢查早發(fā)現(xiàn)比上線后發(fā)現(xiàn)好。5.3 現(xiàn)象訓(xùn)練 loss 在降驗(yàn)證 mAP 一直是 0現(xiàn)象訓(xùn)練日志里 box_loss、cls_loss 都在正常下降但yolo val跑出來(lái) mAP50 接近 0混淆矩陣全空。原因文本嵌入緩存沒(méi)有隨數(shù)據(jù)集更新??蚣軙?huì)把文本嵌入緩存到磁盤換了數(shù)據(jù)集但緩存目錄沒(méi)清理模型拿的是舊詞向量預(yù)測(cè)出來(lái)的類別 id 和驗(yàn)證集對(duì)不上。解決換數(shù)據(jù)集或改names之后手動(dòng)清掉緩存目錄通常在數(shù)據(jù)集同級(jí)或~/.cache下強(qiáng)制重新生成也可以在訓(xùn)練腳本開(kāi)頭加一行刪除緩存的邏輯。這件事沒(méi)有后悔藥只能每次改完數(shù)據(jù)先清一遍。5.4 現(xiàn)象顯存爆炸batch 開(kāi)到 8 就 OOM現(xiàn)象24G 顯存batch 開(kāi)到 8 就報(bào) out of memory但同尺寸的普通 YOLOv8 能開(kāi)到 32。原因RepVL-PAN 的跨模態(tài)注意力額外占用顯存文本嵌入和視覺(jué)特征做注意力時(shí)會(huì)產(chǎn)生中間張量另外開(kāi)放詞匯訓(xùn)練時(shí)類別數(shù)遠(yuǎn)大于常規(guī)檢測(cè)LVIS 上千類檢測(cè)頭的分類分支顯存占用隨類別數(shù)線性增長(zhǎng)。解決先把imgsz從 640 降到 512 試再考慮凍結(jié) backbone 前幾層如果類別數(shù)確實(shí)很大分階段訓(xùn)練——先用全量類別訓(xùn)融合模塊再裁詞表精調(diào)。別硬扛OOM 反復(fù)重試很耗時(shí)間。5.5 現(xiàn)象導(dǎo)出 ONNX 之后檢測(cè)框全亂現(xiàn)象PyTorch 下正常導(dǎo)出 ONNX 后用 onnxruntime 推理框的數(shù)量和位置都對(duì)不上。原因沒(méi)有做重參數(shù)化就導(dǎo)出。開(kāi)放詞匯模型的前向里有動(dòng)態(tài)的文本嵌入輸入ONNX 對(duì)這種動(dòng)態(tài)形狀支持有限導(dǎo)出時(shí)文本分支被靜態(tài)化或者直接丟失。解決導(dǎo)出前必須先set_classes固化詞表讓模型變成固定類別數(shù)的普通檢測(cè)器再執(zhí)行導(dǎo)出。順序不能反反了就只能重導(dǎo)。6. 二次開(kāi)發(fā)把開(kāi)放詞匯能力接進(jìn)業(yè)務(wù)管線真正落地的時(shí)候set_classes那幾行代碼只是開(kāi)頭麻煩在后面詞表要跟著業(yè)務(wù)變、模型要能熱更新、改了詞表得知道有沒(méi)有變差。我在項(xiàng)目里一般會(huì)把詞表從代碼里抽出來(lái)做成配置驅(qū)動(dòng)。import yaml from ultralytics import YOLOWorld # 詞表配置單獨(dú)一個(gè)文件改詞表不用改代碼 cfg yaml.safe_load(open(configs/classes.yaml)) classes cfg[classes] # [person, forklift, pallet] alias cfg.get(alias, {}) # 業(yè)務(wù)別名映射 model YOLOWorld(weights/yolov8s-world.pt) model.set_classes([alias.get(c, c) for c in classes]) # 保存一份帶版本號(hào)的固化權(quán)重方便回滾 version cfg[version] model.save(fweights/warehouse-{version}.pt)配置里帶version字段是血淚經(jīng)驗(yàn)。詞表改動(dòng)在業(yè)務(wù)側(cè)看起來(lái)是小事但對(duì)模型來(lái)說(shuō)等于換了一個(gè)任務(wù)沒(méi)有版本號(hào)就沒(méi)法回滾出問(wèn)題只能靠記憶猜上次是哪份權(quán)重。配置和權(quán)重同名歸檔出問(wèn)題時(shí)切回去只要改一行路徑。導(dǎo)出這一環(huán)順序必須固定先固化詞表再導(dǎo)出。導(dǎo)出后的模型走標(biāo)準(zhǔn)檢測(cè)管線速度能回到常規(guī) YOLO 水平適合放到邊緣設(shè)備上跑。導(dǎo)出腳本我一般寫(xiě)成冪等的每次全量重新導(dǎo)出不做增量覆蓋這樣權(quán)重和導(dǎo)出產(chǎn)物永遠(yuǎn)對(duì)得上。驗(yàn)證這塊別只看整體 mAP。開(kāi)放詞匯模型的風(fēng)險(xiǎn)在于「某一類悄悄失效」整體指標(biāo)被大類撐著看不出來(lái)。我的做法是維護(hù)一個(gè) 200 張左右的小回歸集覆蓋每個(gè)類別至少 20 個(gè)正樣本每次改詞表或換權(quán)重都跑一遍逐類看 AP。這個(gè)集子不用大但必須穩(wěn)定圖片和標(biāo)注不能隨便動(dòng)。驗(yàn)證項(xiàng)關(guān)注點(diǎn)觸發(fā)條件逐類 AP單類是否掉超過(guò) 5 個(gè)點(diǎn)每次詞表變更推理耗時(shí)固化前后對(duì)比確認(rèn)重參數(shù)化生效每次導(dǎo)出空框率某些類別是否完全不輸出每次換權(quán)重詞表覆蓋業(yè)務(wù)新詞是否已進(jìn)配置每周巡檢去年有一次我在沒(méi)有跑回歸集的情況下直接換了一版權(quán)重上線兩天后發(fā)現(xiàn)某個(gè)次要類別的檢測(cè)框全沒(méi)了偏偏這個(gè)類別在整體指標(biāo)里權(quán)重很低從 mAP 上根本看不出來(lái)。從那以后我每次改詞表、換權(quán)重、重新導(dǎo)出都強(qiáng)制走一遍那份 200 張的小回歸集逐類對(duì)比掉點(diǎn)就停下來(lái)查。這個(gè)習(xí)慣看起來(lái)笨但比事后回滾便宜太多。希望這套流程幫到你少走幾次我走過(guò)的彎路。本文還有配套的精品資源點(diǎn)擊獲取