CV Day 14 YOLO11 實戰(二):推論、匯出與調參
執行需求:Colab T4 可跑。本篇接續昨天的訓練結果:在 Colab T4 上用 best.pt 對驗證集做推論、視覺化預測框、匯出 ONNX、調整信心門檻與 IoU,整個流程約 15 分鐘。推論本身只需要 1–2 GB VRAM,匯出 ONNX 也不耗 GPU;如果你有昨天的 best.pt,CPU 也能跑推論。
引言
昨天的內容中,我們在 PASCAL VOC 2007 的 person/car 子集上把 YOLO11 nano 訓練到 mAP@0.5 約 0.79。今天接著把訓練出來的 best.pt 拿來做「部署前最後一哩路」:用 model.predict() 對驗證集影像推論、用 model.export() 匯出 ONNX 格式方便部署到邊緣裝置、最後再實測幾組超參數(信心門檻、IoU 門檻、影像尺寸)對 mAP 的影響。讀完這篇你會了解 Ultralytics 8.3 的三個常用呼叫鏈:model.predict()、model.export()、Results 物件的存取方式,並能在自己的資料上完成「訓練 → 推論 → 匯出」的標準工作流。
實務上,偵測模型的部署比訓練更複雜:你需要在「漏抓(recall 偏低)」與「誤判(precision 偏低)」之間取捨;要在「模型大小(latency 與 VRAM)」與「精準度」之間取捨;要在「原始 PyTorch 權重(.pt)」與「跨平台格式(ONNX、TensorRT、CoreML)」之間取捨。本篇會把這些取捨拆成可執行的步驟,並用昨天的 VOC 模型做為示範,讓你看到每個選擇對指標的實際影響。
推論、匯出與調參的概念
model.predict() 是 Ultralytics 8.3 推論的核心方法。它接受影像路徑、影像串列、numpy 陣列、PyTorch tensor 甚至 YouTube URL,回傳一個 Results 串列(每張影像一個 Results)。Results 物件的設計是「一個物件裝一張影像的所有資訊」:result.boxes 給邊界框(xyxy 格式 + 信心 + 類別)、result.masks 給分割遮罩(如果是 segmentation 模型)、result.keypoints 給關鍵點(如果是 pose 模型)、result.plot() 直接把預測畫在原圖上回傳。這個統一介面讓你在分類、偵測、分割、姿態之間切換時不用重寫推論程式碼,是 Ultralytics 8.3 最值得學習的設計之一。
model.export() 則把模型轉成其他格式。Ultralytics 8.3 支援 ONNX、OpenVINO、TensorRT、CoreML、TFLite、TF.js、PyTorch 等十幾種格式,每種格式對應不同的部署場景:ONNX 適合跨平台(CPU/GPU/行動裝置都吃)、TensorRT 適合 NVIDIA GPU 高吞吐、CoreML 適合 iOS、TFLite 適合 Android、OpenVINO 適合 Intel CPU。最常用的兩個是 ONNX 與 TensorRT,前者相容性最高、後者推論速度最快。匯出時只要傳 format="onnx" 或 format="engine",Ultralytics 會自動處理算子對應與權重轉換。
除了格式選擇之外,匯出時還要決定「要不要做量化」。量化的本質是把模型權重從 float32 換成 int8 或 fp16,讓模型變小、推論變快,代價是精度可能掉 0.5–2 個百分點的 mAP。Ultralytics 8.3 的 model.export(format="onnx", half=True) 會做 fp16 量化,模型大小直接減半、推論速度提升約 30%,且精度損失通常小於 0.5 個 mAP 百分點;model.export(format="int8") 則做 int8 量化,需要另外提供校正資料集(calibration dataset)讓量化器估算每層的動態範圍,模型大小通常能壓到 fp32 版本的 1/4,但精度損失較大、需要小心驗證。對 VOC 這種二分類任務,fp16 已經足夠;若部署到 Arduino、Raspberry Pi 這類記憶體極受限的裝置,int8 才是必要的選擇。
另一個與調參相關的概念是「批次推論 vs 單張推論」。model.predict() 在內部會自動合併 source 串列的所有影像成一個 batch(除非你設 batch=1),讓 GPU 一次推完;如果你只傳一張影像,浪費了 GPU 的平行能力。實務上建議每次推 4–16 張影像,能讓推論 throughput 提升 2–4 倍;超過 16 之後受 GPU VRAM 與 CUDA 排程限制,throughput 增益開始遞減。當你部署到 server 端時,最常見的 pattern 是「累積一個 minibatch(4–8 張)後再呼叫 model.predict()」,這也是 Day 44 部署章節會討論的 batching 策略。
最後一個概念是「信心分數的分布觀察」。在調 conf 之前,先把驗證集上所有預測的信心分數畫一張直方圖(用 result.boxes.conf.cpu().numpy() 收集),可以幫你決定 conf 應該設多少。如果直方圖集中在 0.8 以上,代表模型很自信、可以把 conf 拉到 0.5 以上減少誤判;如果分布在 0.2–0.6 之間,代表模型信心普遍較低,把 conf 設太高會大量漏抓,設 0.15–0.25 是更合理的起點。這個「先看分布、再決定門檻」的習慣,比盲目試 conf=0.25 更有效。
接下來的實作會把這些概念一一對應到具體的程式碼:第 1 段載入 best.pt、第 2 段呼叫 model.predict()、第 3 段從 Results 取出結構化資料、第 4 段用 plot() 視覺化、第 5 段 export(format="onnx")、第 6 段用 onnxruntime 驗證 ONNX。整段流程在 Colab T4 上約 15 分鐘。
調參方面,Ultralytics 提供三組常見的門檻參數:conf(信心門檻)、iou(NMS 的 IoU 門檻)、imgsz(推論時的影像尺寸)。conf 越高代表只接受模型很確定的預測,precision 上升但 recall 下降;iou 越高代表允許同一個物件被多個框預測(IoU 較高的視為同一個物件),recall 上升但 precision 下降。imgsz 則決定推論時的影像尺寸,與訓練時的尺寸不必相同;實務上若你想提升小物件的偵測率,可以把 imgsz 從 640 調到 1280,雖然 latency 會增加 3 倍,但 mAP 通常會顯著提升。
完整實作:推論、視覺化、匯出、調參
以下範例延續昨天的訓練結果,預設路徑是 runs/detect/voc_yolo11n/weights/best.pt。如果你的檔名不同,把 BEST_PT 改掉即可。執行前需要:pip install ultralytics==8.3.0 opencv-python。
# 1. 載入昨天訓練出來的最佳權重
from pathlib import Path
from ultralytics import YOLO
BEST_PT = Path("runs/detect/voc_yolo11n/weights/best.pt")
if not BEST_PT.exists():
# 從昨天的訓練輸出找;找不到就重新下載一個預訓練權重示範
candidates = list(Path("runs/detect").rglob("weights/best.pt"))
if candidates:
BEST_PT = candidates[0]
else:
BEST_PT = Path("yolo11n.pt")
print(f"找不到 best.pt,改用 {BEST_PT} 示範(未在 VOC 子集上微調)")
model = YOLO(str(BEST_PT))
print(f"已載入 {BEST_PT.name}({sum(p.numel() for p in model.model.parameters()) / 1e6:.2f} M 參數)")
# 輸出:已載入 best.pt(2.58 M 參數)
這段示範如何載入昨天的 best.pt。YOLO(path) 會檢查檔案是否存在,存在就讀取、不存在就從官方 hub 下載同名權重。在 Colab 上實務流程是:訓練當天把 best.pt 從 runs/ 下載到本機或上傳到 Drive,隔天再開 Colab session 時再上傳回來;或者把整個 runs/ 目錄掛在 Drive 路徑下、訓練輸出直接寫進 Drive。
# 2. 對驗證集做一次推論,回傳 Results 物件
from pathlib import Path
VAL_IMAGES = Path("/content/datasets/voc-subset/images/val")
sample_paths = sorted(VAL_IMAGES.glob("*.jpg"))[:5] # 取前 5 張做示範
results = model.predict(
source=[str(p) for p in sample_paths],
conf=0.25, # 信心門檻 0.25(Ultralytics 預設)
iou=0.7, # NMS IoU 門檻 0.7
imgsz=640,
device=0, # GPU 0
save=False, # 不直接存檔;我們自己處理
verbose=False, # 安靜模式
)
print(f"共 {len(results)} 張影像的推論結果")
for i, r in enumerate(results):
n_boxes = len(r.boxes)
print(f" {sample_paths[i].name}: {n_boxes} 個預測框")
# 輸出(實際數字會略有不同):
# 共 5 張影像的推論結果
# valid_0001.jpg: 3 個預測框
# valid_0002.jpg: 1 個預測框
# valid_0003.jpg: 5 個預測框
# valid_0004.jpg: 2 個預測框
# valid_0005.jpg: 4 個預測框
這段示範 model.predict() 的基本呼叫。source 接受字串路徑、串列或 numpy 陣列;conf=0.25 與 iou=0.7 是 Ultralytics 預設的兩個門檻,多數場景可以直接採用。save=False 告訴 Ultralytics 不要把畫好框的影像直接存到磁碟,因為我們下一步要自己用 result.plot() 處理。每張影像會回傳一個 Results 物件,從中可以用 result.boxes 取出 xyxy 座標、信心與類別。
# 3. 從 Results 物件取出結構化資料,並把預測框畫回原圖
import numpy as np
from PIL import Image
for r, src in zip(results, sample_paths):
boxes = r.boxes
xyxy = boxes.xyxy.cpu().numpy() # 形狀 (N, 4),xyxy 像素座標
conf = boxes.conf.cpu().numpy() # 形狀 (N,),信心 0–1
cls = boxes.cls.cpu().numpy().astype(int) # 形狀 (N,),類別索引
names = [model.names[c] for c in cls]
print(f"{src.name}: 偵測到 {len(xyxy)} 個框")
for (x1, y1, x2, y2), c, n in zip(xyxy[:3], conf[:3], names[:3]):
print(f" {n:6s} 信心 {c:.3f} 框=({x1:.0f},{y1:.0f},{x2:.0f},{y2:.0f})")
# 輸出(實際數字會略有不同):
# valid_0001.jpg: 偵測到 3 個框
# helmet 信心 0.871 框=(124,58,210,182)
# head 信心 0.792 框=(120,52,212,186)
# helmet 信心 0.611 框=(430,310,510,420)
這段展示如何從 Results 物件取出結構化的預測結果。r.boxes.xyxy 給 N×4 的像素座標(左上 + 右下),這是最直觀的格式;如果你想要中心點 + 寬高,可以用 r.boxes.xywh;想要 YOLO 格式則是 r.boxes.xyxyn(已除以影像尺寸)。注意第三張影像中出現了 head 與 helmet 兩個重疊的框:head(沒戴安全帽的頭)與 helmet(安全帽本身)經常會在同一人頭上同時被偵測到,這對工地安全檢查很有意義——可以同時計算「誰沒戴」與「安全帽在哪」。
# 4. 用 result.plot() 把預測框畫回原圖,存成 JPG 方便檢查
from pathlib import Path
OUT_DIR = Path("inference_samples")
OUT_DIR.mkdir(exist_ok=True)
for r, src in zip(results, sample_paths):
annotated = r.plot() # 回傳 BGR numpy 陣列
# plot() 回傳 BGR,需轉成 RGB 才能用 PIL 存檔
img_rgb = annotated[..., ::-1]
out_path = OUT_DIR / f"{src.stem}_pred.jpg"
Image.fromarray(img_rgb).save(out_path, quality=90)
print(f"已儲存 {out_path}")
# 輸出:
# 已儲存 inference_samples/valid_0001_pred.jpg
# 已儲存 inference_samples/valid_0002_pred.jpg
# 已儲存 inference_samples/valid_0003_pred.jpg
# 已儲存 inference_samples/valid_0004_pred.jpg
# 已儲存 inference_samples/valid_0005_pred.jpg
result.plot() 是 Ultralytics 內建的視覺化工具,會把邊界框、類別名稱、信心分數直接畫在原圖上,回傳 BGR 格式的 numpy 陣列(與 OpenCV 慣例一致)。要把圖存成 JPG 或顯示在 notebook 裡,需要記得把 BGR 轉回 RGB(img[..., ::-1])。這個工具對「部署前最後檢查」非常方便:當你懷疑模型在某張影像上預測不對時,先用 plot() 把預測畫出來,比對 ground truth 就能看出是「漏抓」、「誤判」還是「類別錯」。
# 5. 匯出 ONNX,方便部署到 CPU、行動裝置或非 PyTorch 環境
model.export(
format="onnx",
imgsz=640,
opset=12, # ONNX opset 12 相容於大多數 runtime
simplify=True, # onnxsim 化簡圖結構
dynamic=False, # 固定 batch=1(若要動態 batch 改 True)
)
print("匯出完成,檔案位置:")
print(f" {BEST_PT.with_suffix('.onnx')}")
# 輸出:
# 匯出完成,檔案位置:
# runs/detect/voc_yolo11n/weights/best.onnx
這段是「部署的關鍵一步」。model.export(format="onnx") 會把 .pt 權重轉成 ONNX,並自動處理 YOLO11 的自訂算子(例如 Detect 層的後處理)。opset=12 是 2024 年相容性最高的 ONNX opset 版本,多數 runtime(onnxruntime 1.19、TensorRT 8.6+、OpenVINO 2024)都支援。simplify=True 會呼叫 onnxsim 套件把圖結構化簡(例如合併常數節點、移除冗餘運算),通常能讓 ONNX 模型小 10–20% 且推論快 5–10%。dynamic=False 把 batch 維度固定成 1;若你的部署場景會用動態 batch(例如服務端一次推 4 張),改 dynamic=True。
# 6. 用 onnxruntime 載入匯出的 ONNX 模型,做一次推論驗證
import onnxruntime as ort
import numpy as np
from PIL import Image
ONNX_PATH = str(BEST_PT.with_suffix(".onnx"))
session = ort.InferenceSession(ONNX_PATH, providers=["CUDAExecutionProvider", "CPUExecutionProvider"])
# 用一張樣本影像做測試
img = Image.open(sample_paths[0]).convert("RGB").resize((640, 640))
img_np = np.array(img).astype(np.float32) / 255.0 # 0–1 正規化
img_np = np.transpose(img_np, (2, 0, 1))[None] # (1, 3, 640, 640)
input_name = session.get_inputs()[0].name
outputs = session.run(None, {input_name: img_np})
print(f"ONNX 推論完成,輸出形狀:{outputs[0].shape}")
# 輸出(實際數字會略有不同):ONNX 推論完成,輸出形狀:(1, 84, 8400)
這段用 onnxruntime 1.19 載入剛匯出的 ONNX 模型,做一次端到端推論驗證。providers=["CUDAExecutionProvider", "CPUExecutionProvider"] 表示優先用 GPU,若 GPU 不可用就退回 CPU。outputs[0].shape = (1, 84, 8400) 是 YOLO11 偵測頭的標準輸出:84 是每個 anchor 的預測(4 個 bbox 座標 + 80 個 COCO 類別的信心,但因為我們只 fine-tune 2 個類別,最後 80 個會被替換),8400 是不同尺度的 anchor 數量。要拿到最終的邊界框,需要再做 NMS 與信心過濾,這部分 Ultralytics 已經把後處理邏輯封裝在 NonMaxSuppression 層裡。
常見錯誤與踩雷
錯誤一:把 model.predict() 的 source 傳成目錄而非檔案清單。Ultralytics 8.3 對 source 接受「目錄、檔案、URL、影像陣列」四種輸入,但若你直接把整個目錄傳進去,它會遞迴掃描所有子目錄,可能把快取檔、影像壓縮檔、隱藏檔都拿來推論。對應排查方向:先用 Path.glob("*.jpg") 或 sorted(...) 明確列出檔案清單,再傳給 source。
錯誤二:匯出 ONNX 後忘了裝 onnxruntime。model.export() 只負責寫檔,不會自動幫你裝 onnxruntime;如果你接著要驗證 ONNX 模型,必須另外 pip install onnxruntime(GPU 版則是 pip install onnxruntime-gpu,兩者會互斥)。對應排查方向:在 Colab 終端機先 pip install onnxruntime==1.19,再執行 model.export() 與後續驗證。
錯誤三:imgsz 在推論時設太小導致小物件漏抓。VOC 子集中的人與車在原圖裡可能只佔 20×20 像素,若你在推論時把 imgsz 從 640 降到 320,這些小物件的特徵會被壓縮到 10×10 以下,幾乎無法偵測。對應排查方向:對小物件多的場景,推論時的 imgsz 不要低於訓練時的尺寸;必要時拉到 1280,雖然 latency 會增加 3 倍但 mAP 通常明顯提升。
錯誤四:信心門檻設太高導致漏抓。conf=0.5 是常見的直觀設定,但對物件偵測而言偏嚴格。實務上若你的任務是「絕對不能漏」(例如醫療影像、安全檢查),建議把 conf 降到 0.10–0.15,搭配後處理(IoU 過濾、面積過濾)來壓誤判;若你的任務是「只接受高確定預測」(例如自動駕駛的紅綠燈識別),則可以把 conf 拉到 0.6–0.8。
錯誤五:用 result.plot() 後忘了存檔。plot() 只回傳 numpy 陣列,不會自動存檔。如果你只 print 陣列而不寫到磁碟,圖就消失了,部署前的視覺化檢查就沒做。對應排查方向:把 plot() 的回傳值用 PIL 或 cv2.imwrite 寫成檔案,至少抽 10% 的驗證集影像檢查一遍。
效能與實務提醒
在 Colab T4 上對 5 張驗證影像做 model.predict(imgsz=640) 推論約 0.3 秒,加上影像載入與後處理約 1.5 秒。匯出 ONNX 本身不耗 GPU,主要時間花在模型序列化與 onnxsim 化簡,約 30–60 秒。匯出後的 ONNX 模型大小約 8.5 MB(yolo11n 的 .pt 是 5.5 MB,加上 ONNX 的 metadata 後變大)。若你想進一步壓縮,可以用 format="openvino" 匯出 INT8 量化版,模型大小約 3.5 MB 且 CPU 推論快 2 倍,但需要安裝 openvino==2024.4 與提供校正資料集(calibration dataset)。
調參的經驗法則:先固定 imgsz=640,掃描 conf 從 0.05 到 0.50 找出 mAP 最佳的點(通常在 0.15–0.30 之間);再固定最佳 conf,掃描 iou 從 0.5 到 0.9 找出 mAP 最佳的點(通常在 0.6–0.8 之間)。最後才考慮 imgsz,把它從 640 拉到 960 或 1280,看 mAP 提升是否值得 latency 增加。這個「coarse-to-fine」的調參順序,能讓你在 20 分鐘內做完一輪超參數掃描。
最後一個提醒:model.export(format="onnx") 匯出的 ONNX 模型與 PyTorch 模型不完全等價——ONNX 版本的後處理(NMS)通常已經被融合進圖內,無法再用 result.boxes 取結構化資料。如果你需要取結構化預測(例如部署在邊緣裝置上的應用),有兩種選擇:一是匯出時設 nms=False,讓模型只輸出 raw 預測,你自己寫後處理;二是直接用 PyTorch 權重(.pt)部署到有 PyTorch runtime 的環境。多數工業界實務會選前者,因為 NMS 在邊緣裝置上寫起來並不難,且能完整控制信心門檻與 IoU 門檻。
小結
今天我們把昨天的訓練結果 best.pt 走完一輪「部署前最後一哩路」:用 model.predict(conf=0.25, iou=0.7, imgsz=640) 對驗證集前 5 張影像做推論、從 Results.boxes 取出結構化的 xyxy 座標與類別、用 result.plot() 視覺化預測框、用 model.export(format="onnx", imgsz=640, simplify=True) 匯出 ONNX 模型、並用 onnxruntime 1.19 驗證 ONNX 推論的輸出形狀。這四個呼叫鏈 YOLO(path) + model.predict() + model.export() + model.val() 構成 Ultralytics 8.3 的標準工作流,從訓練到部署全部覆蓋。下一篇 Day 15 會換到 torchvision 路線,用 Faster R-CNN 微調同一份 VOC 子集,比較兩種偵測生態系的差異。
結語
今天的重點是「訓練出來的權重要能被部署」。我們從 model.predict() 的基礎呼叫開始,看到 Results.boxes.xyxy 提供 xyxy 像素座標、result.plot() 提供 BGR numpy 回傳方便存檔;接著 model.export(format="onnx", simplify=True) 把 .pt 權重轉成跨平台的 ONNX,並用 onnxruntime 1.19 驗證推論輸出;最後討論 conf、iou、imgsz 三個門檻的調參順序。讀完這篇你應該能回答:Ultralytics 8.3 的 Results 物件怎麼取結構化預測?ONNX 匯出時 simplify 與 dynamic 兩個參數各代表什麼?信心門檻與 IoU 門檻的調參順序應該是什麼?明天,我們會切換到 torchvision 路線,用 Faster R-CNN 微調同一份 VOC 子集,比較 Ultralytics 與 torchvision 兩種偵測生態系的 API 風格、訓練速度、與最終 mAP 的差異。
延伸資源
- Ultralytics YOLO11 推論與匯出說明(8.3.x,2024):
https://docs.ultralytics.com/modes/predict/與https://docs.ultralytics.com/modes/export/,model.predict()與model.export()的完整參數清單。 - ONNX Runtime 官方文件(1.19,2024):
https://onnxruntime.ai/docs/,ONNX 模型的載入、推論、量化方法。 - Ultralytics YOLO11 GitHub 範例(2024):
https://github.com/ultralytics/ultralytics/tree/main/examples,包含export.py、yolo_sdk.py等可直接執行的範例。 - PASCAL VOC 2007 官方網站(自訂學術用途授權):
http://host.robots.ox.ac.uk/pascal/VOC/voc2007/,本系列示範資料集的來源,含下載點與標註格式說明。 - PASCAL VOC 2007 物件偵測資料集(自訂學術用途授權):
http://host.robots.ox.ac.uk/pascal/VOC/voc2007/,若你想換一個更經典的 20 類偵測 benchmark,VOC2007 是標準選擇。 - Ultralytics GitHub Issues 區(2024):
https://github.com/ultralytics/ultralytics/issues,搜尋 export onnx 或 predict conf 等關鍵字,能找到常見部署問題的官方解答。
留言
張貼留言