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