:從訓練到INT8量化優(yōu)化)
YOLO-FastestV2這個名字最近在邊緣設備部署圈子里出現(xiàn)的頻率越來越高。我最早接觸它是在一個智能貨柜項目上甲方給的控制板算力低得離譜——沒有獨立GPU內存只有2G還得同時跑攝像頭采集和UI。當時把YOLOv5、YOLOX甚至YOLOv8按個試了一圈壓縮到極限還是卡得沒法用最后換到YOLO-FastestV2才真正跑起來幀率穩(wěn)定在30FPS以上。這篇文章就圍繞我從零部署到移動端推理的完整過程展開包括模型訓練、格式轉換、框架集成、性能優(yōu)化和踩坑記錄給準備在手機、嵌入式板卡上落地目標檢測的朋友一份可以直接照著抄的實戰(zhàn)手冊。1. 為什么是YOLO-FastestV2一個“小模型”的生存哲學1.1 從YOLO家族里挑一個“能跑得動”的做移動端目標檢測最繞不開的問題就是模型體積和推理速度。YOLOv5s部署到手機雖然精度不錯但FP32權重就14MB左右內存占用和首幀延遲在低端機上都不太好看。YOLOX-Nano稍微小一點但對于我的項目來說還是不夠“極端”。YOLO-FastestV2正是沖著這個痛點來的。它在YOLOX的基礎上做了大量瘦身骨干網(wǎng)絡換成ShuffleNetV2檢測頭精簡成anchor-free結構FP32權重只有1.3MB左右INT8量化后能壓到0.5MB上下。輸入分辨率默認352x352在驍龍865級別手機上用NCNN部署純推理時間可以跑到6到9毫秒放到樹莓派4B這種板子上也能維持15FPS左右。這個體積帶來的直接好處不只是快而且省內存。移動端App或者嵌入式程序系統(tǒng)分配給模型推理的內存往往只有幾十MB一個動輒幾十MB的大模型加上中間張量稍微不注意就OOM。YOLO-FastestV2這種“一個安裝包大小”的模型給前后處理留出了大量余量。所以如果你判斷目標檢測任務對精度要求中等偏上、但對端側資源卡得很死這個模型基本是最穩(wěn)妥的選擇。1.2 移動端部署的現(xiàn)實算力、內存、功耗三者博弈部署不是“模型能跑”就行而是要在算力、內存、功耗三者之間找到平衡點。移動端設備CPU是高通驍龍、聯(lián)發(fā)科天璣、蘋果A系列這類ARM架構和桌面端的x86 NVIDIA GPU完全不同。CPU沒有CUDAGPU是Adreno/Mali/Apple GPU還有獨立的NPU/DSP單元但各個平臺的加速庫差異極大。在移動端跑推理首選一般不是直接用PyTorch而是NCNN、TFLite、MNN這類推理框架。它們會針對ARM架構做指令集優(yōu)化比如ARMv8.2的FP16、SDOT/UDOT指令并把模型計算圖做算子融合和緩存優(yōu)化最終性能比裸跑PyTorch Mobile高出數(shù)倍。我剛做移動端項目時犯過一個典型錯誤就是先拿大模型在PC上驗證功能再轉頭想辦法塞進手機結果發(fā)現(xiàn)精度是夠了但幀率掉到個位數(shù)功耗也壓不住。后來學乖了移動端部署的第一步不是調參而是先劃定資源上限——確定目標設備、可用內存、期望幀率再反向選擇模型架構和量化方案。YOLO-FastestV2能成為這個平衡點上的常青樹就是因為它的體型注定它很難“爆內存”同時推理耗時可控。1.3 完整項目技術棧與上手路線一套完整的YOLO-FastestV2移動端部署鏈路大致由三部分組成訓練、轉換、推理集成。訓練端使用PyTorch YOLOX框架YOLO-FastestV2官方訓練代碼基于YOLOX改造轉換端通過導出ONNX中間格式再轉成NCNN/TFLite/MNN等端側格式推理端則在自己項目的C或Java/Swift代碼中調用推理框架API。入門路線我認為應該這樣走先跑通官方Demo用PyTorch在COCO或者VOC數(shù)據(jù)集上訓練一個能用的模型導出ONNX后交給NCNN跑通PC端C推理確認結果沒問題再移植到Android/iOS工程最后做量化和其他移動端優(yōu)化。整套流程走一遍你對模型結構、部署格式、底層算子調度都會有一個遠超“調包”層面的理解。2. 環(huán)境準備與模型訓練先把“腦子”養(yǎng)出來2.1 訓練環(huán)境與數(shù)據(jù)集準備我先說明一下YOLO-FastestV2不是一個開箱即用的“預訓練模型庫”官方提供了在COCO和VOC上的預訓練權重但真實業(yè)務通常要用自己的數(shù)據(jù)集微調。訓練環(huán)境建議直接用官方倉庫的依賴列表核心是PyTorch 1.8以上版本加上torchvision、opencv-python、numpy、pycocotools、tqdm等常用庫。數(shù)據(jù)集這塊有兩種準備方式。如果做通用物體檢測直接用COCO或者VOC數(shù)據(jù)集的子集即可如果做業(yè)務場景比如檢測安全帽、螺絲缺陷、寵物那就得自己標注。常用的標注格式是VOC的XML和COCO的JSON。YOLO-FastestV2倉庫里自帶VOC格式的數(shù)據(jù)加載器所以我個人建議自建數(shù)據(jù)集優(yōu)先轉成VOC格式省去改數(shù)據(jù)加載代碼的時間。訓練目錄結構大致如下datasets/ ├── VOCdevkit/ │ ├── VOC2007/ │ │ ├── Annotations/ # XML標注 │ │ ├── JPEGImages/ # 原始圖片 │ │ └── ImageSets/ │ │ └── Main/ # train.txt / val.txt準備好數(shù)據(jù)后運行訓練腳本前修改兩個地方一個是data/voc.yaml里的類別數(shù)一個是yolo_fastest_v2.py里的num_classes。這兩個如果不一致訓練直接報維度錯誤。我在第一次訓練時就是只改了yaml忘了改模型文件結果Loss算到一半崩了排查了好一陣子。2.2 訓練參數(shù)與實驗記錄默認訓練參數(shù)里最需要關注的是batch_size、base_lr、epochs、輸入尺寸和Anchor設置。YOLO-FastestV2雖然是anchor-free結構但它在訓練階段仍然參考了YOLOX的標簽分配策略輸入尺寸統(tǒng)一縮放到352x352。參考參數(shù)如下參數(shù)推薦值說明input_size352x352也可用320或416但對速度影響明顯batch_size16/32視GPU顯存而定越小越容易過擬合base_lr0.01batch 64線性縮放batch減半則lr減半epochs100-300自建數(shù)據(jù)集建議至少100輪num_classes按業(yè)務定COCO是80VOC是20weight_decay5e-4防止過擬合warmup_epochs5學習率預熱的輪數(shù)訓練時我習慣每10個epoch手動驗證一次看mAP是否還在上漲。因為移動端模型容量有限訓練到后期mAP很容易飽和甚至震蕩提前保存最優(yōu)權重比盲目跑滿300輪更有意義。官方代碼會自動保存best_ckpt.pth和last_ckpt.pth我用的是best那個。這里有個容易被忽略的點訓練時的數(shù)據(jù)增強強不強直接影響模型在移動端的泛化能力。YOLOX默認帶了Mosaic、隨機翻轉、顏色抖動等增強如果訓練集小可以適當增強但如果增強太猛模型在真實場景的檢測反而可能退化。我的經驗是自建數(shù)據(jù)集最好保留Mosaic因為它對小目標很友好。2.3 訓練結果評估與模型導出訓練結束后先用eval.py跑一遍驗證集mAP。COCO標準下YOLO-FastestV2原版大概在27%到30% mAP0.5:0.95的范圍如果只看mAP0.5會到45%以上。業(yè)務場景下單類檢測任務通常能比通用模型高出很多因為背景混淆少。需要提醒的是不要在PC端的mAP上過于自信移動端經過量化和算子替換后精度會下降。FP32轉INT8之后mAP掉2到5個點是常態(tài)。所以訓練階段最好設置一個“余量目標”——業(yè)務對精度要求是mAP0.5大于80%那訓練階段至少做到85%再收手。模型導出這一步官方倉庫提供了converter/yolox_export_onnx.py腳本。執(zhí)行后會生成一個yolo_fastest_v2.onnx文件。導出時注意設置opset_version建議固定為11太高或太低都可能造成后續(xù)轉換工具的兼容性問題。我自己在ONNX opset 11下轉NCNN和TFLite都沒遇到大的算子報錯整體比較順。3. 從PyTorch到移動端模型轉換全流程3.1 導出ONNX格式與兼容性ONNX在這里的作用是中間交換格式。它的價值在于把PyTorch的動態(tài)圖計算圖固化成一個靜態(tài)圖后續(xù)轉NCNN、TFLite、MNN、CoreML都有統(tǒng)一的起點。導出的核心代碼在官方倉庫里已經寫好但我想補充幾個容易踩的坑。第一導出前一定要把模型切到eval()模式model.eval()這行不能漏否則BatchNorm層運行統(tǒng)計不一致推理結果會漂移。第二確認輸入shape是[1, 3, 352, 352]RGB且歸一化到0到1。第三導出后先用onnxruntime在PC上跑一遍對比一下和PyTorch原始輸出是否一致這一步可以篩掉很多模型結構層面的問題。我用onnxruntime驗證的代碼大致這樣import onnxruntime as ort import numpy as np import torch from yolo_fastest_v2 import YoloFastestV2 model YoloFastestV2(num_classes80) ckpt torch.load(best_ckpt.pth, map_locationcpu) model.load_state_dict(ckpt[model]) model.eval() dummy torch.randn(1, 3, 352, 352) with torch.no_grad(): torch_out model(dummy) sess ort.InferenceSession(yolo_fastest_v2.onnx, providers[CPUExecutionProvider]) onnx_out sess.run(None, {images: dummy.numpy()}) for i, (a, b) in enumerate(zip(torch_out, onnx_out)): print(output, i, max diff:, np.abs(a.numpy() - b).max())如果max diff在1e-4以下說明ONNX導出基本沒問題。3.2 ONNX到NCNN/TFLite的轉換轉NCNN官方推薦用onnx2ncnn工具。這個工具在NCNN倉庫的build/tools/onnx/目錄下。命令行如下onnx2ncnn yolo_fastest_v2.onnx yolo_fastest_v2.param yolo_fastest_v2.bin轉完后有一步很多人會忽略用ncnnoptimize做圖優(yōu)化再生成一個適用于真實部署的精簡模型。ncnnoptimize yolo_fastest_v2.param yolo_fastest_v2.bin yolo_fastest_v2_opt.param yolo_fastest_v2_opt.bin 1最后那個數(shù)字表示模型類型1代表FP32。如果想輸出FP16模型可以傳0不過NCNN在ARM上默認會啟用FP16加速param標記成FP32跑起來速度差別不大主要看CPU是否支持FP16指令。轉TFLite則稍微麻煩一點不推薦直接ONNX轉TFLite因為當時ONNX-TFLite轉換器的算子覆蓋還不完整。更穩(wěn)的路徑是先轉成TensorFlow SavedModel再用TFLite Converter轉。具體流程是ONNX - TF用onnx-tf庫- TFLite。雖然有點繞但成功率更高。如果目標平臺是Android且啟用NNAPITFLite是更通用的選擇。3.3 模型輸出解析三個尺度下的解碼邏輯這是整個部署鏈路里最需要吃透的一步。YOLO-FastestV2有3個檢測頭輸出特征圖的步長分別是8、16、32。以352x352輸入為例三個輸出層的空間維度分別是stride 844x44stride 1622x22stride 3211x11每個網(wǎng)格預測的特征通道數(shù)是4 1 num_classes。4代表目標的中心點偏移和寬高x, y, w, h1代表objectness置信度num_classes是類別數(shù)。以COCO為例就是85通道。解碼邏輯看起來復雜其實拆開只是兩步。第一步根據(jù)網(wǎng)格坐標還原目標中心在整圖中的位置center_x (grid_x sigmoid(tx)) * stride center_y (grid_y sigmoid(ty)) * stride第二步結合寬高得到檢測框box_w exp(tw) * anchor_w box_h exp(th) * anchor_h不過YOLO-FastestV2的設計里已經簡化了訓練代碼里用的是全卷積直接回歸xywh所以部署端的解碼代碼也會跟著對應倉庫里的解碼模塊寫。如果自己寫解碼最容易錯的地方就是寬高沒有除以輸入尺寸導致坐標全部溢出到幾百甚至上千。我建議先在PC端把C或Python解碼跑通再用同樣的邏輯移植到移動端減少調試成本。4. 移動端推理框架集成實戰(zhàn)4.1 NCNN集成步驟與CMake配置這里以Android NCNN為例因為NCNN對Android的適配最成熟資料也多。集成方式有兩種一種是從源碼編譯NCNN一種是下載預編譯的AAR庫。對大多數(shù)項目直接用官方發(fā)布的AAR就夠了。在app/build.gradle里引入implementation com.tencent.ncnn:ncnn:20240820如果你需要自定義算子或改動NCNN源碼再從源碼編譯。Android工程里更關鍵的是CMake配置。把yolo_fastest_v2_opt.param和yolo_fastest_v2_opt.bin放到assets目錄下然后在CMakeLists里配置add_library(ncnn STATIC IMPORTED) set_target_properties(ncnn PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libncnn.a) add_library(yolodet SHARED src/main/cpp/yolo_detector.cpp) target_link_libraries(yolodet ncnn jnigraphics)需要注意NCNN的.param和.bin文件最好都從assets里拷貝到App私有目錄再加載直接讀assets也可以但某些版本NCNN對asset路徑支持有兼容問題拷貝最保險。4.2 推理代碼核心流程NCNN推理代碼的整體流程我個人把它分成五步加載模型、準備輸入、前處理、推理會話、拿到原始輸出。加載模型很簡單ncnn::Net net; net.opt.use_vulkan_compute false; // 如果走GPU再開 net.opt.num_threads 4; int ret net.load_param(env-GetStringUTFChars(paramPath...)); ret net.load_model(modelPath);輸入圖片要縮放到352x352再轉成NCNN的Mat。這里有個前處理的血淚教訓如果訓練時用的是ImageNet均值歸一化(0.485, 0.456, 0.406)和方差縮放移動端必須保持完全一致而且通道順序必須是RGB。如果輸入是RGBA攝像頭幀一定要先轉RGB再推理否則顏色通道錯亂會導致檢測結果完全不可用。ncnn::Mat in ncnn::Mat::from_pixels_resize( rgba_data, ncnn::Mat::PIXEL_RGBA2RGB, w, h, 352, 352 ); const float mean_vals[3] {0.485f, 0.456f, 0.406f}; const float norm_vals[3] {0.229f, 0.224f, 0.225f}; in.substract_mean_normalize(mean_vals, norm_vals); ncnn::Extractor ex net.create_extractor(); ex.input(images, in); ncnn::Mat out0, out1, out2; ex.extract(output0, out0); ex.extract(output1, out1); ex.extract(output2, out2);output0、output1、output2這三個名字要和param文件里的輸出層節(jié)點對應。如果你在導出ONNX時改了輸出節(jié)點名這里也要跟著改。4.3 后處理NMS與坐標還原拿到三個輸出層的原始特征圖后要做的是把它們拼成候選框列表再執(zhí)行NMS抑制重復框。這一步的代碼結構通常是三層循環(huán)遍歷每個尺度遍歷每個網(wǎng)格遍歷每個類別。解碼時建議先把objectness和類別最高分相乘得到最終置信度過濾掉小于閾值的框。置信度閾值一般取0.25到0.5之間太低會輸出太多誤檢框后處理耗時也更高。NMS的IoU閾值取0.45比較合適。坐標還原要特別注意坐標系NCNN輸出的是歸一化到[0,1]的相對坐標還是以像素為單位的絕對坐標取決于模型轉換時的解碼方式。YOLO-FastestV2官方轉換腳本里輸出的通常是歸一化到輸入尺寸的相對坐標所以還原到原圖時要除以352再乘以原圖寬高box.x1 (center_x / 352.0 - box_w / 2) * original_width box.y1 (center_y / 352.0 - box_h / 2) * original_height這一步如果漏了除以352檢測框會整體縮放異常出現(xiàn)“框特別大”或“框偏移到邊角”的現(xiàn)象。5. 移動端性能優(yōu)化讓FPS漲上去5.1 量化INT8與FP16的選擇把FP32模型轉成INT8是移動端提升速度最直接的手段。NCNN提供了ncnn2table和ncnnopt來做模型量化。前者需要輸入一批校準圖片讓量化器統(tǒng)計各層激活值的動態(tài)范圍然后生成一張量化表。校準圖片不需要太多100到500張有代表性的就行太多了反而過擬合到校準集。INT8量化流程大概是ncnn2table yolo_fastest_v2.param yolo_fastest_v2.bin imagelist.txt yolo_fastest_v2.table mean0.485,0.456,0.406 norm0.229,0.224,0.225 shape352,352,3 pixelrgb thread8 methodklncnnoptimize yolo_fastest_v2.param yolo_fastest_v2.bin yolo_fastest_v2_int8.param yolo_fastest_v2_int8.bin 2最后那個數(shù)字2表示“加載量化表并量化權重”如果寫0或1不加載表得到的就不是真正的INT8模型。這里是最容易踩的坑之一。FP16則不需要量化表支持FP16的ARM CPU基本所有中高端芯片會在推理時自動使用FP16指令模型體積直接減半精度損失可以忽略。所以如果只求快速部署優(yōu)先試FP16要極限壓測再走INT8。5.2 線程數(shù)與CPU/GPU/NPU加速NCNN默認num_threads取設備核心數(shù)但往往不是最優(yōu)值。大核少但主頻高小核多但性能弱。我測試過驍龍設備4線程比8線程在單幀延遲上更有優(yōu)勢因為線程過多帶來同步開銷而且大小核調度不穩(wěn)定。Android端可以用std::thread::hardware_concurrency()拿到邏輯核心數(shù)再根據(jù)自己的設備手動限制為4或6。實測數(shù)據(jù)如下線程數(shù)平均推理耗時351x352備注211.2ms偏慢適合省電47.6ms均衡67.1ms提升有限87.9ms同步開銷增加GPU加速方面NCNN在支持Vulkan的設備上可以開啟net.opt.use_vulkan_compute true對3x3卷積多的網(wǎng)絡提升明顯。不過YOLO-FastestV2本身已經很小GPU版本的收益有時不如CPU穩(wěn)定而且首幀初始化Vulkan上下文有額外延遲。如果目標設備GPU驅動不完善我建議CPU為主、GPU作為可選項。NPU加速是另一個方向但適配成本高各家供應商的模型轉換工具鏈不通用只有團隊打算長期投入端側AI引擎時才建議優(yōu)先做。5.3 實測優(yōu)化效果與性能數(shù)據(jù)對比我拿一臺驍龍865手機做過一組對照實驗統(tǒng)一輸入352x352單幀只測模型推理時間不含前處理和NMS結果如下模型類型推理耗時模型大小備注FP327.8ms1.3MB基線FP164.9ms0.6MB幾乎無精度損失INT8KL量化3.6ms0.5MB精度下降約2.2 mAP如果想在低端機上達到30FPSINT8是必選項。如果是中高端手機FP16已經足夠而且部署簡單、風險低。需要注意的是性能優(yōu)化不能只盯模型推理時間。前處理和NMS如果寫得不好會讓總幀率腰斬。用Neon指令優(yōu)化HWC轉CHW和resize、把NMS改成并行或剪枝版本省下2到3ms比壓模型算子更劃算。6. 避坑實錄我踩過的那些坑6.1 預處理不一致導致的精度崩壞有一次我把模型從NCNN換成TFLite在PC上用ONNX一切正常到Android上檢測結果全亂套——明明是人模型輸出卻是垃圾桶置信度還很高。排查了大半天發(fā)現(xiàn)問題出在前處理TFLite例程里用了0到255的輸入范圍而我的NCNN模型用的是0到1加上均值方差歸一化。同一份權重輸入分布換了模型輸出自然崩塌。這類問題特別隱蔽因為模型不會報錯它只是給出一堆“自信”的錯誤結果。排查方法是把PC端Python前處理的中間結果導出和移動端前處理結果逐像素對比看到底是歸一化、通道順序還是resize算法不一致。6.2 輸出尺寸對不上與anchor mismatchNCNN模型跑起來后我發(fā)現(xiàn)output0的通道數(shù)只有24。當時的模型是VOC數(shù)據(jù)集訓練的類別數(shù)20所以412025怎么算都不該是24。后來發(fā)現(xiàn)是模型導出時某個卷積層的輸出通道被onnx2ncnn錯誤優(yōu)化掉了表現(xiàn)為最后一層通道數(shù)莫名少1。解決辦法是重新導出ONNX并在onnx2ncnn時加上-o參數(shù)關閉某些重排優(yōu)化繞開算子融合的bug。如果你的輸出尺寸和預期不一致最直接的辦法是用Net的blob調試接口打印每一層的輸出shape定位是從哪一層開始偏的。不要靠猜不然很浪費時間。6.3 穩(wěn)定性問題內存抖動、發(fā)熱、黑屏移動端推理在長時間運行后還有兩類“非算法”問題。第一類是內存泄漏。如果每次推理都通過JNI創(chuàng)建新的ncnn::Mat且不釋放幾分鐘后App就開始卡頓。正確做法是復用Mat和Extractor不要每幀都重新create_extractor和申請內存。第二類是發(fā)熱降頻。連續(xù)運行十幾分鐘后SoC溫度升高CPU會主動降頻幀率從30FPS掉到18FPS體感非常明顯。緩解手段包括限制線程數(shù)、開啟Vulkan讓GPU分擔負載、在非檢測時段休眠推理線程。如果你做的是實時視頻流檢測最好加上動態(tài)幀率控制檢測跟不上就把抽幀間隔拉大避免惡性循環(huán)。最后再分享一個小技巧。如果模型在你目標設備上頻繁出錯先別急著調代碼把同一份輸入圖片分別在PC端和移動端跑導出中間feature map逐層對比確定是模型轉換問題、前處理問題還是硬件差異。我用這個辦法排查完大部分“玄學bug”比反復調參高效得多。目標檢測部署這件事說到底就是“同一套邏輯在無數(shù)種設備上保持一致”只要每一步都留好對比驗證的接口就沒什么不能解決的問題。