專案使用 YOLOv8 物件檢測模型部屬在邊緣裝置上,在了解模型與測試後發現有可以優化地方存在,而優化的關鍵在於 YOLOv8 在 Decode bounding box 的過程中是將每一個特徵網格 (grid) 的每一個 bbox 都進行解碼,而這部分能夠將其移除,並且移動到邊緣端進行實作,進而減少 decode box 時間,而減少時間關鍵在於可以先判斷信心分數,把分數較低的 Bounding Box 移除,再進行積分操作,進而優化程式碼執行時間
在將影像輸入模型並進行後續操作後,針對不同部署模型格式 (如 ONNX、TensorRT、TFLite 等) 進行 export 時,後處理操作可能會影響計算圖 (computational graph) 的生成,而 decode bbox 過程也包含在裡面,因此我們需要將其拔除並另外實現在邊緣裝置上
我們從 YOLOv8 (現行為 YOLOv11 了) 來進行部分程式碼的解讀,參考 ultralytics
的 github 開源程式碼 ultralytics/nn/modules/head.py
在 Detect
類別中負責了以下的任務
雖然 YOLOv8 是 anchor-free 的模型,但實際上訓練與 inference 也都需要 anchor,但意義上是不同的
self.cv2
self.cv3
self.reg_max
在 YOLOv8 的架構中,forward
函數需要處理來自不同尺度的特徵圖,而這些特徵圖被儲存在一個列表 x
中。這些特徵圖分過 self.cv2
和 self.cv3
層進行向前傳播,可以看出 YOLOv8 在輸出預測上進行了 解偶 (decoupling) 的
特徵圖 x[i]
經過 self.cv2[i]
和 self.cv3[i]
的處理後,tensor 的形狀如下:
self.cv2[i](x[i])
的 tensor shape 為 ,其中:
self.cv3[i](x[i])
的 tensor shape 為 ,其中:
最終,特徵圖 x[i]
的 tensor shape 會是 ,即將回歸框和分類信息合併到同一個 tensor 中。
這樣,我們就完成了第一階段的 tensor 處理。由於這過程涉及許多 tensor 操作,對初學者來說可能較難理解,但基本上是將不同的預測任務(如邊界框和分類)進行分離後,再合併回輸出中。
_inference
在 _inference
函數中,負責處理 decode bbox與 anchor 處理,我們將核心的程式碼來進行解讀
x_cat
make_anchors(feats, strides, grid_cell_offset)
feat[0][0]
生成的話就會是在特徵點的中心有一個 anchor,strides
則是相對於原圖其特徵圖縮小的倍率,通常就是 就是輸出到 Detect
的特徵圖相對於原圖的縮放比例self.dlf(x)
reshape() -> permute()
積分操作藉由 Conv 1 x 1
來完成,等價於積分操作
藉由 NCNN 將模型部屬,並使用 C++ 進行撰寫
model.export(format="ncnn", dynamic=True, simplify=True)
將模型轉換在export後會得到4個檔案,我們需要的是.bin
與.param
預定義的框,用於協助模型學習,讓模型學習的目標是如何調整預定義的框,以去符合標籤所標記的框
YOLOv8 Anchor-Free 模型預測偏移量()和網格中心點(),通過縮放因子 轉換為最終邊界框座標()。
模型輸出參數:
邊界框計算公式:
由於 YOLOv8 是密集地在一張影像上進行 bbox 的預測,因此需要透過 NMS 來消除分數較低以及重複預測的bbox
在不更改原始程式碼的情況下,我們可以透過 export.py
來將 forward
函數進行改寫後,替換掉原生 Detect
的類別,這樣我們便可以在匯出模型權重檔案時,將 Bounding Box Decode 部分移除
Export model時不包含decode box的功能
python .\export_ncnn.py --path best.pt --patch
Export model時包含decode box的功能
python .\export_ncnn.py --path best.pt