檢測(cè)實(shí)戰(zhàn):從原理到自制數(shù)據(jù)集訓(xùn)練全流程)
SSDSingle Shot MultiBox Detector目標(biāo)檢測(cè)領(lǐng)域里繞不過(guò)去的一個(gè)經(jīng)典模型。它把目標(biāo)定位和分類(lèi)統(tǒng)一在一個(gè)前向過(guò)程中一次推理直接輸出所有候選框和類(lèi)別概率不需要兩階段方法那樣的先提候選區(qū)域再逐區(qū)域分類(lèi)的繁瑣流程。我在實(shí)際項(xiàng)目里用SSD做過(guò)工業(yè)零件檢測(cè)、桌面場(chǎng)景識(shí)別、還有一些邊緣設(shè)備上的輕量級(jí)檢測(cè)整體感受是原理清晰、訓(xùn)練可控、對(duì)環(huán)境要求也不苛刻非常適合作為你上手訓(xùn)練自己的目標(biāo)檢測(cè)數(shù)據(jù)集的第一套完整方案。這篇文章我會(huì)從模型設(shè)計(jì)思路講起再一步步帶你走完環(huán)境搭建、自制數(shù)據(jù)集、訓(xùn)練調(diào)參、推理評(píng)估、踩坑排錯(cuò)的完整流程。適合有Python基礎(chǔ)、了解基本卷積網(wǎng)絡(luò)概念、但還沒(méi)真正在自己的數(shù)據(jù)上跑通過(guò)完整目標(biāo)檢測(cè)流程的學(xué)習(xí)者。你不需要頂級(jí)GPU也不需要海量數(shù)據(jù)只要按著步驟來(lái)就能在自制數(shù)據(jù)集上完成SSD的端到端訓(xùn)練和檢測(cè)。1. SSD的設(shè)計(jì)思路與核心機(jī)制拆解1.1 單階段檢測(cè)到底是什么要理解SSD得先分清目標(biāo)檢測(cè)的兩條技術(shù)路線。兩階段檢測(cè)的典型代表是Faster R-CNN系列思路是先粗篩再細(xì)查第一步用區(qū)域建議網(wǎng)絡(luò)RPN在整張圖上找出幾千個(gè)可能存在目標(biāo)的區(qū)域第二步再對(duì)每個(gè)區(qū)域做分類(lèi)和邊界框回歸。好處是精度高壞處是慢兩個(gè)階段各跑一遍很難做到實(shí)時(shí)。SSD走的是另一條路一步到位。它把整張圖分成多個(gè)網(wǎng)格每個(gè)網(wǎng)格點(diǎn)上預(yù)先放置一組不同尺寸和長(zhǎng)寬比的默認(rèn)框default box網(wǎng)絡(luò)同時(shí)預(yù)測(cè)每個(gè)默認(rèn)框里有沒(méi)有目標(biāo)、是什么類(lèi)別、以及框的位置該往哪個(gè)方向調(diào)整多少。所有預(yù)測(cè)在一次前向過(guò)程中完成所以叫Single Shot。我經(jīng)常用一個(gè)類(lèi)比來(lái)解釋這件事兩階段檢測(cè)像你先用望遠(yuǎn)鏡掃一遍整個(gè)草原發(fā)現(xiàn)可疑目標(biāo)然后換單筒放大鏡一個(gè)個(gè)仔細(xì)確認(rèn)單階段檢測(cè)則是你直接站在瞭望塔上一次性用不同倍數(shù)的鏡頭同時(shí)掃過(guò)全場(chǎng)景誰(shuí)是什么、在哪里一次看清。SSD屬于后者速度優(yōu)勢(shì)非常明顯。這里還有一個(gè)MultiBox的概念。它指的不是預(yù)測(cè)一個(gè)框而是預(yù)測(cè)一大批框。訓(xùn)練階段會(huì)把默認(rèn)框和真實(shí)標(biāo)注框做匹配匹配好的默認(rèn)框負(fù)責(zé)學(xué)習(xí)我該往哪里偏移、這個(gè)位置是什么目標(biāo)沒(méi)匹配上的默認(rèn)框則負(fù)責(zé)學(xué)習(xí)這里沒(méi)有目標(biāo)。網(wǎng)絡(luò)本質(zhì)上是在做多任務(wù)學(xué)習(xí)一邊學(xué)分類(lèi)一邊學(xué)框回歸。1.2 多尺度特征圖預(yù)測(cè)SSD的看家本領(lǐng)SSD最核心的設(shè)計(jì)創(chuàng)新是它不在單一層特征圖上做預(yù)測(cè)而是在多個(gè)不同分辨率的特征圖并行做預(yù)測(cè)。這個(gè)設(shè)計(jì)直接解決了一個(gè)老大難問(wèn)題——小目標(biāo)檢測(cè)。早期的檢測(cè)模型習(xí)慣只在網(wǎng)絡(luò)最后一層特征圖上接檢測(cè)頭但最后一層特征圖經(jīng)過(guò)多次池化和卷積下采樣分辨率很低一個(gè)小目標(biāo)可能只占據(jù)幾個(gè)像素信息已經(jīng)嚴(yán)重丟失檢測(cè)效果自然差。SSD的做法是既然不同層特征圖保留了不同粒度的信息那就在特征金字塔的多個(gè)層級(jí)上各接一組檢測(cè)器。淺層特征圖分辨率高、感受野小適合檢測(cè)小目標(biāo)深層特征圖分辨率低、感受野大適合檢測(cè)大目標(biāo)。你可以把它理解成不同高度的觀察者在看同一個(gè)場(chǎng)景站在二樓只能看到局部細(xì)節(jié)站在二十樓能看到全景輪廓SSD是讓兩者同時(shí)工作再匯總結(jié)果。這就是它名字里MultiBox之外的另一個(gè)關(guān)鍵——多尺度預(yù)測(cè)。具體到300x300輸入SSD會(huì)在6層特征圖上做預(yù)測(cè)38x38、19x19、10x10、5x5、3x3、1x1。每層給不同尺度的默認(rèn)框尺度按線性遞增公式分配s_k s_min (s_max - s_min) * (k - 1) / (m - 1)對(duì)一個(gè)m6層的網(wǎng)絡(luò)結(jié)構(gòu)s_min取0.2、s_max取0.9算出來(lái)6層的相對(duì)尺度約為0.2、0.34、0.48、0.62、0.76、0.9對(duì)應(yīng)300輸入就是約60、102、144、186、228、270像素。再配合多種長(zhǎng)寬比比如1:1、1:2、2:1、1:3、3:1一個(gè)位置最多可以匹配6個(gè)不同形狀的默認(rèn)框。整個(gè)SSD300一張圖會(huì)產(chǎn)生8732個(gè)候選框SSD512則是24564個(gè)。這個(gè)設(shè)計(jì)帶來(lái)一個(gè)非常實(shí)際的收益檢測(cè)各種尺寸的目標(biāo)時(shí)模型天然具備了每個(gè)尺度都有各自專(zhuān)門(mén)的預(yù)測(cè)頭的歸納偏置訓(xùn)練起來(lái)比單純依賴最后一層特征圖容易收斂得多。1.3 訓(xùn)練階段不容忽視的配套機(jī)制多尺度預(yù)測(cè)是骨架但真正讓SSD訓(xùn)練穩(wěn)定的是配套的匹配策略和損失設(shè)計(jì)。先看先驗(yàn)框匹配。訓(xùn)練時(shí)每個(gè)真實(shí)標(biāo)注框需要和默認(rèn)框建立對(duì)應(yīng)關(guān)系。匹配策略分兩步第一步每個(gè)真實(shí)框優(yōu)先匹配IoU交并比最高的那個(gè)默認(rèn)框保證每一個(gè)真實(shí)目標(biāo)都至少有一個(gè)正樣本負(fù)責(zé)學(xué)習(xí)第二步對(duì)剩下的默認(rèn)框只要和任意真實(shí)框的IoU大于0.5也算作正樣本。這個(gè)0.5的閾值不是拍腦袋定的它保證了正樣本在確實(shí)和某個(gè)目標(biāo)重疊得不錯(cuò)的前提下盡量多緩解了正負(fù)樣本比例天然懸殊的問(wèn)題。再看負(fù)樣本處理。一張圖里大多數(shù)默認(rèn)框都是背景如果全部參與損失計(jì)算負(fù)樣本數(shù)量可能是正樣本的幾十倍模型會(huì)被背景類(lèi)淹沒(méi)。SSD的解決辦法是難例挖掘hard negative mining按分類(lèi)置信度損失從大到小排序挑選損失最高的那些負(fù)樣本參與訓(xùn)練把正負(fù)樣本比例控制在1:3左右。換句話說(shuō)模型會(huì)把精力集中在那些讓網(wǎng)絡(luò)誤以為有目標(biāo)但實(shí)際沒(méi)有的難例上而不是簡(jiǎn)單背景框上。最后是數(shù)據(jù)增強(qiáng)。SSD訓(xùn)練時(shí)的數(shù)據(jù)增強(qiáng)是一套組合拳隨機(jī)裁剪、隨機(jī)縮放、顏色抖動(dòng)、水平翻轉(zhuǎn)。其中隨機(jī)裁剪特別重要它模擬了目標(biāo)在視野中只出現(xiàn)局部的場(chǎng)景相當(dāng)于無(wú)語(yǔ)義地制造了大量目標(biāo)尺度變化和遮擋的困難樣本。這也是SSD在VOC這類(lèi)數(shù)據(jù)集上能和小目標(biāo)斗爭(zhēng)的關(guān)鍵手段之一。我現(xiàn)在自己訓(xùn)練模型時(shí)也保留了這個(gè)習(xí)慣寧可訓(xùn)練慢一點(diǎn)也要讓模型見(jiàn)過(guò)足夠多的殘缺目標(biāo)。2. 訓(xùn)練環(huán)境搭建從零把項(xiàng)目跑起來(lái)2.1 框架選型別在舊時(shí)代踩坑SSD最早是在Caffe框架下實(shí)現(xiàn)的網(wǎng)上大量老教程也都是Caffe版本。我不推薦你走那條路因?yàn)镃affe的環(huán)境配置至今仍是個(gè)深坑依賴庫(kù)多、編譯鏈路長(zhǎng)、CUDA版本敏感很多老代碼在新驅(qū)動(dòng)上直接編譯不過(guò)去。我見(jiàn)過(guò)不少同學(xué)把時(shí)間全耗在配置環(huán)境上模型一個(gè)epoch都沒(méi)跑完。更推薦的是基于PyTorch的開(kāi)源復(fù)現(xiàn)版本。PyTorch現(xiàn)在對(duì)訓(xùn)練、調(diào)試、可視化、模型導(dǎo)出的支持都更順手處理圖像數(shù)據(jù)的接口也更符合直覺(jué)。雖然Python代碼跑起來(lái)的原始性能可能不如C后端硬核但對(duì)大多數(shù)個(gè)人項(xiàng)目和中小型業(yè)務(wù)場(chǎng)景來(lái)說(shuō)開(kāi)發(fā)效率和迭代速度遠(yuǎn)比那一點(diǎn)性能差異重要。你可以去代碼托管平臺(tái)搜SSD_PyTorch之類(lèi)的倉(cāng)庫(kù)選一個(gè)star多、維護(hù)活躍的版本作為起點(diǎn)。依賴安裝很直接核心就是PyTorch加上一些輔助庫(kù)。我的建議是# 根據(jù)自身顯卡驅(qū)動(dòng)選擇合適的CUDA與PyTorch版本 pip install torch torchvision pip install opencv-python numpy tqdm pip install pycocotools # 如果評(píng)估需要COCO API如果你用的是Linux環(huán)境建議直接用conda創(chuàng)建獨(dú)立環(huán)境避免和系統(tǒng)Python環(huán)境打架。硬件方面我用一張12GB顯存的消費(fèi)級(jí)顯卡訓(xùn)過(guò)300分辨率的SSDbatch size開(kāi)到16或者32都沒(méi)問(wèn)題。8GB顯存也能跑只要把batch size降到8左右。完全沒(méi)有GPU的話小數(shù)據(jù)集也能用CPU硬扛但訓(xùn)練時(shí)間會(huì)長(zhǎng)到讓人懷疑人生。2.2 項(xiàng)目目錄與數(shù)據(jù)目錄規(guī)劃我習(xí)慣動(dòng)手之前先把目錄結(jié)構(gòu)規(guī)劃好否則訓(xùn)練中途會(huì)發(fā)現(xiàn)亂七八糟的文件散落一地。下面是一套我驗(yàn)證過(guò)很多次的布局ssd_project/ ├── train.py ├── test.py ├── evaluate.py ├── data/ │ ├── datasets/ │ │ └── VOCdevkit/ │ │ └── MyDataset/ │ │ ├── Annotations/ # XML標(biāo)注文件 │ │ ├── JPEGImages/ # 原始圖片 │ │ └── ImageSets/ │ │ └── Main/ # train.txt, val.txt, test.txt │ └── config.py ├── checkpoints/ │ └── ssd300_epoch120.pth └── logs/這里項(xiàng)目名我用某自制數(shù)據(jù)集代指實(shí)際數(shù)據(jù)。為什么要用VOC格式而不是別的格式因?yàn)樗Y(jié)構(gòu)直觀、文件組織清晰標(biāo)注和圖片分開(kāi)管理索引文件就是一行一個(gè)文件名非常容易理解。而且大多數(shù)SSD開(kāi)源實(shí)現(xiàn)的DataLoader就是按VOC格式寫(xiě)的你只需要把自己的數(shù)據(jù)整理成這個(gè)結(jié)構(gòu)代碼基本不用大改。一個(gè)常被忽略的細(xì)節(jié)是圖片名和XML文件名必須嚴(yán)格一一對(duì)應(yīng)命名里不能有空格、中文、特殊符號(hào)。我建議統(tǒng)一用6位數(shù)字編號(hào)比如000001.jpg對(duì)應(yīng)000001.xml。文件名不規(guī)則會(huì)導(dǎo)致數(shù)據(jù)加載莫名其妙報(bào)錯(cuò)而且定位問(wèn)題時(shí)要花大量時(shí)間。2.3 預(yù)訓(xùn)練權(quán)重選擇SSD的骨干網(wǎng)絡(luò)默認(rèn)是VGG16的前幾層。為什么不自己從頭訓(xùn)練整個(gè)VGG因?yàn)槟繕?biāo)檢測(cè)的骨干網(wǎng)絡(luò)需要提取通用的視覺(jué)特征邊緣、紋理、形狀這些特征在大規(guī)模分類(lèi)數(shù)據(jù)集上已經(jīng)學(xué)得很好了。用預(yù)訓(xùn)練權(quán)重做初始化再在自己的目標(biāo)檢測(cè)數(shù)據(jù)上微調(diào)收斂速度會(huì)快非常多最終精度也更高。從零訓(xùn)練不是不行但除非你有幾十萬(wàn)張級(jí)別的數(shù)據(jù)否則通常效果不佳。一些PyTorch復(fù)現(xiàn)版本會(huì)把VGG16預(yù)訓(xùn)練權(quán)重轉(zhuǎn)換后塞進(jìn)SSD結(jié)構(gòu)里你只需要下載轉(zhuǎn)換好的權(quán)重文件放到checkpoints目錄即可。這里有一個(gè)實(shí)測(cè)過(guò)的注意事項(xiàng)不同復(fù)現(xiàn)版本對(duì)權(quán)重的組織方式略有差異下載時(shí)注意和你的代碼版本對(duì)應(yīng)起來(lái)否則加載權(quán)重時(shí)會(huì)出現(xiàn)鍵名不匹配的報(bào)錯(cuò)。萬(wàn)一報(bào)錯(cuò)也沒(méi)關(guān)系檢查一下?tīng)顟B(tài)字典的鍵名做一個(gè)簡(jiǎn)單的重映射就行后面我會(huì)在排錯(cuò)章節(jié)講具體做法。3. 制作自己的數(shù)據(jù)集比訓(xùn)練更花時(shí)間的部分3.1 數(shù)據(jù)收集原則少而精不如多而雜你在訓(xùn)練上花一天往往能在數(shù)據(jù)上花一周。數(shù)據(jù)質(zhì)量直接決定模型上限網(wǎng)絡(luò)結(jié)構(gòu)只是逼近這個(gè)上限的手段。先給個(gè)數(shù)量參考如果是單類(lèi)別目標(biāo)檢測(cè)我建議每類(lèi)至少準(zhǔn)備300到500張?jiān)紙D片多一些更好。我這里說(shuō)的不是那種同一場(chǎng)景連拍10張的水?dāng)?shù)據(jù)而是要有足夠的場(chǎng)景多樣性。拿我自己做過(guò)的三類(lèi)果蔬檢測(cè)舉例蘋(píng)果、香蕉、橙子各400張左右但我會(huì)刻意覆蓋不同光照室內(nèi)燈光、自然光、不同角度俯拍、側(cè)拍、斜拍、不同遮擋部分被其他物體遮住、堆疊擺放、不同距離近景特寫(xiě)、中景、遠(yuǎn)景甚至故意放進(jìn)一些模糊圖片做難例。模型只有在訓(xùn)練時(shí)見(jiàn)過(guò)足夠多難看的姿勢(shì)推理時(shí)面對(duì)真實(shí)場(chǎng)景才不會(huì)露怯。有一個(gè)原則特別值得強(qiáng)調(diào)寧可圖片總數(shù)少一點(diǎn)也要保證所有圖里有目標(biāo)、目標(biāo)標(biāo)注無(wú)誤。網(wǎng)上有些人為了趕進(jìn)度大量下載圖片后隨便框一下就開(kāi)訓(xùn)結(jié)果模型學(xué)到的全是錯(cuò)誤的定位模式效果還不如小數(shù)據(jù)集精心標(biāo)注。數(shù)據(jù)質(zhì)量永遠(yuǎn)是第一位的。3.2 使用開(kāi)源標(biāo)注工具標(biāo)注并整理標(biāo)注工具我推薦開(kāi)源免費(fèi)、界面簡(jiǎn)單的矩形框標(biāo)注工具操作邏輯是打開(kāi)一張圖鼠標(biāo)拖拽畫(huà)框填寫(xiě)類(lèi)別名稱(chēng)保存后就生成VOC格式的XML文件。這些工具一般會(huì)幫你自動(dòng)生成XML裝在本地即可使用不需要聯(lián)網(wǎng)配置服務(wù)。標(biāo)注時(shí)有兩個(gè)很容易被忽視的細(xì)節(jié)。第一標(biāo)注框要盡量貼合目標(biāo)邊緣不要為了省事留一圈空氣邊。第二框得太緊也不行因?yàn)榭蚧貧w的偏移量本身就包含了一定的誤差空間標(biāo)得太緊反而讓模型學(xué)不到合理的邊界。我自己通常的做法是讓框的邊緣比目標(biāo)實(shí)際輪廓稍微收縮一到兩個(gè)像素這樣模型預(yù)測(cè)的框普遍更緊湊。遮擋目標(biāo)的標(biāo)注建議遵循一個(gè)原則只要目標(biāo)還有明確的可見(jiàn)主體就標(biāo)注它的可見(jiàn)部分不要試圖去標(biāo)一個(gè)腦補(bǔ)出來(lái)的完整物體。這個(gè)細(xì)節(jié)對(duì)模型非常關(guān)鍵它教會(huì)模型物體會(huì)被遮擋但可檢測(cè)性依然存在。標(biāo)注完后一定要做二次校驗(yàn)。最有效的辦法是找一個(gè)沒(méi)參與標(biāo)注的同學(xué)隨機(jī)抽20%的圖重新過(guò)一遍看看框的位置和類(lèi)別命名是否一致。我發(fā)現(xiàn)多人標(biāo)注項(xiàng)目里類(lèi)別命名的混亂同一個(gè)物體一會(huì)兒叫apple一會(huì)兒叫Apple是導(dǎo)致訓(xùn)練loss異常的主要元兇之一。一個(gè)統(tǒng)一的命名規(guī)范能省無(wú)數(shù)麻煩。3.3 劃分訓(xùn)練集與測(cè)試集數(shù)據(jù)集做好之后第一步是把圖片和XML組織進(jìn)JPEGImages與Annotations目錄然后劃分樣本索引。我建議按8:1:1劃分訓(xùn)練、驗(yàn)證、測(cè)試集并且在程序里固定隨機(jī)種子保證每次劃分結(jié)果一致。下面給一個(gè)簡(jiǎn)單的劃分腳本你可以根據(jù)自己的情況修改import os import random from pathlib import Path random.seed(42) image_dir Path(JPEGImages) names [p.stem for p in image_dir.glob(*.jpg)] random.shuffle(names) n len(names) train_names names[:int(n * 0.8)] val_names names[int(n * 0.8):int(n * 0.9)] test_names names[int(n * 0.9):] def write_txt(path, names): with open(path, w) as f: for name in names: f.write(name \n) main_dir Path(ImageSets/Main) main_dir.mkdir(parentsTrue, exist_okTrue) write_txt(main_dir / train.txt, train_names) write_txt(main_dir / val.txt, val_names) write_txt(main_dir / test.txt, test_names) write_txt(main_dir / trainval.txt, train_names val_names)這里必須提醒一個(gè)數(shù)據(jù)泄漏問(wèn)題如果同一個(gè)物體的多張連拍圖被同時(shí)分進(jìn)訓(xùn)練集和測(cè)試集你的測(cè)試分?jǐn)?shù)會(huì)虛高因?yàn)槟P偷扔谝呀?jīng)見(jiàn)過(guò)測(cè)試目標(biāo)了。我處理這類(lèi)問(wèn)題的方法是先按場(chǎng)景分組在場(chǎng)景級(jí)別而不是圖片級(jí)別做劃分。比如同一次拍攝會(huì)話產(chǎn)生的圖片歸到同一個(gè)場(chǎng)景組然后以場(chǎng)景組為單位切分?jǐn)?shù)據(jù)集。這樣評(píng)估結(jié)果才可信。3.4 轉(zhuǎn)成目標(biāo)格式的注意事項(xiàng)如果你拿到的數(shù)據(jù)不是VOC格式而是像CSV、JSON或者某種私有標(biāo)注格式需要寫(xiě)一個(gè)轉(zhuǎn)換腳本。轉(zhuǎn)換的核心步驟無(wú)非是把標(biāo)注信息讀出來(lái)重新組織成VOC XML再把圖片統(tǒng)一命名和格式。寫(xiě)轉(zhuǎn)換腳本時(shí)有一個(gè)細(xì)節(jié)值得注意類(lèi)別名稱(chēng)建議統(tǒng)一為小寫(xiě)英文。雖然VOC格式本身對(duì)大小寫(xiě)不敏感但很多數(shù)據(jù)處理代碼、數(shù)據(jù)增強(qiáng)模塊、評(píng)估腳本都默認(rèn)按名稱(chēng)匹配如果名稱(chēng)大小寫(xiě)不一致可能在類(lèi)別統(tǒng)計(jì)時(shí)出現(xiàn)兩個(gè)不同的分類(lèi)導(dǎo)致評(píng)估時(shí)對(duì)不上號(hào)。我也遇到過(guò)因?yàn)轭?lèi)別名帶空格導(dǎo)致數(shù)據(jù)加載失敗的案例所以在轉(zhuǎn)換時(shí)做一遍清理去空格、統(tǒng)一小寫(xiě)、替換特殊符號(hào)是一個(gè)低成本高收益的動(dòng)作。另外轉(zhuǎn)換完一定要抽查。隨機(jī)打印幾張圖像的標(biāo)注框信息再配合圖像可視化畫(huà)出框來(lái)確認(rèn)坐標(biāo)沒(méi)有錯(cuò)位、類(lèi)別名稱(chēng)沒(méi)有混亂。坐標(biāo)錯(cuò)位最常見(jiàn)的原因是原數(shù)據(jù)里坐標(biāo)是相對(duì)坐標(biāo)比如0到1的小數(shù)而轉(zhuǎn)換時(shí)沒(méi)有按圖片寬高還原成絕對(duì)像素這個(gè)問(wèn)題排查起來(lái)很費(fèi)時(shí)間最好在源頭就驗(yàn)證清楚。4. 開(kāi)始訓(xùn)練配置、命令與節(jié)奏控制4.1 修改模型與訓(xùn)練配置每個(gè)參數(shù)都有講究拿到開(kāi)源代碼后第一個(gè)要改的就是類(lèi)別數(shù)。VOC原本是20類(lèi)但你的數(shù)據(jù)集可能只有3類(lèi)。SSD在計(jì)算分類(lèi)損失時(shí)類(lèi)別數(shù)等于實(shí)際類(lèi)別數(shù)加1額外一個(gè)背景類(lèi)所以num_classes需要改成實(shí)際類(lèi)別數(shù)加1。如果你檢測(cè)三類(lèi)果蔬這個(gè)值就是4。訓(xùn)練尺寸的選擇同樣重要。SSD300表示輸入300x300SSD512表示輸入512x512。分辨率更高小目標(biāo)檢測(cè)能力更強(qiáng)但顯存占用和訓(xùn)練時(shí)間會(huì)明顯增加。我的建議是先在300分辨率上把流程跑通確認(rèn)數(shù)據(jù)沒(méi)問(wèn)題后再考慮要不要換成512。我自己在做小目標(biāo)密集場(chǎng)景時(shí)才用512普通場(chǎng)景300完全夠用。batch size和初始學(xué)習(xí)率是一對(duì)好兄弟。參考常見(jiàn)的訓(xùn)練策略batch size調(diào)小時(shí)初始學(xué)習(xí)率也該等比降低。我推薦一個(gè)保守的起點(diǎn)batch size 16初始學(xué)習(xí)率0.001如果你只能開(kāi)batch size 8就把學(xué)習(xí)率降到0.0005左右。優(yōu)化器用SGD加動(dòng)量momentum0.9配合權(quán)重衰減5e-4這是SSD訓(xùn)練很經(jīng)典的一套配置。不要一上來(lái)就盲目上AdamSSD這類(lèi)帶大量錨框的模型對(duì)學(xué)習(xí)率比較敏感SGD的穩(wěn)定性更適合。還有一個(gè)容易被新手忽略的參數(shù)學(xué)習(xí)率調(diào)度策略??俥poch設(shè)多少warmup要開(kāi)多久學(xué)習(xí)率在哪個(gè)階段線性衰減這些直接影響收斂質(zhì)量。通常我會(huì)先跑一個(gè)100到200個(gè)epoch的大循環(huán)學(xué)習(xí)率在前幾個(gè)epoch做warmup從很小的值升到目標(biāo)值然后在訓(xùn)練后期幾個(gè)階段各衰減一次。如果你用的復(fù)現(xiàn)代碼默認(rèn)只寫(xiě)了VOC的120輪調(diào)度那么對(duì)自制小數(shù)據(jù)集來(lái)說(shuō)epoch要適當(dāng)延長(zhǎng)模型才能充分收斂。4.2 從Debug到正式訓(xùn)練的完整流程我強(qiáng)烈建議你正式開(kāi)訓(xùn)之前先做一個(gè)冒煙測(cè)試從每類(lèi)數(shù)據(jù)中抽10張左右組成一個(gè)迷你訓(xùn)練集跑一個(gè)epoch。這個(gè)動(dòng)作看起來(lái)簡(jiǎn)單卻能幫你避開(kāi)大量無(wú)效等待。冒煙測(cè)試通過(guò)的標(biāo)準(zhǔn)很簡(jiǎn)單程序不崩、loss穩(wěn)定下降、每個(gè)batch都能正常反傳。如果冒煙測(cè)試中l(wèi)oss卡住不動(dòng)先別急著調(diào)學(xué)習(xí)率檢查數(shù)據(jù)標(biāo)簽是不是有問(wèn)題例如某些XML里類(lèi)別名對(duì)不上配置列表、某些圖里標(biāo)注框是空的。一次batch里如果大部分目標(biāo)都有標(biāo)注問(wèn)題loss會(huì)異常波動(dòng)甚至不降。把數(shù)據(jù)問(wèn)題排查干凈模型訓(xùn)練才談得上收斂。冒煙測(cè)試通過(guò)后就可以開(kāi)始正式訓(xùn)練了。命令行跑起來(lái)時(shí)記得把日志輸出到文件方便回溯。通常在訓(xùn)練過(guò)程中我會(huì)同時(shí)開(kāi)一個(gè)tensorboard類(lèi)工具實(shí)時(shí)看四個(gè)曲線總loss、分類(lèi)loss、定位loss、學(xué)習(xí)率變化。這四個(gè)曲線是判斷訓(xùn)練是否健康的核心儀表盤(pán)。訓(xùn)練中還有一個(gè)容易被低估的細(xì)節(jié)定期保存checkpoint。很多代碼默認(rèn)只保存最后一個(gè)epoch的模型但訓(xùn)練后期如果出現(xiàn)震蕩或過(guò)擬合你會(huì)希望回到一個(gè)相對(duì)穩(wěn)定的中間狀態(tài)。我更推薦每5到20個(gè)epoch保存一次同時(shí)始終保留驗(yàn)證集mAP最高的那次權(quán)重作為best模型。我的習(xí)慣是checkpoint按epoch存best按指標(biāo)存這樣既不會(huì)丟失訓(xùn)練進(jìn)度又總能拿最優(yōu)模型出來(lái)推理。4.3 訓(xùn)練中的性能觀察與調(diào)參建議訓(xùn)練過(guò)程中你觀察到的曲線應(yīng)該有這樣的變化前20個(gè)epoch里總loss快速下降尤其是分類(lèi)loss中間階段loss下降變緩定位loss開(kāi)始起作用后期loss緩慢震蕩下降進(jìn)入細(xì)調(diào)階段。如果某個(gè)階段出現(xiàn)了loss完全不變或者驗(yàn)證集mAP驟降就要警覺(jué)了。我用一個(gè)實(shí)際案例說(shuō)明之前某次訓(xùn)練做的是三類(lèi)果蔬檢測(cè)1200張訓(xùn)練圖跑SSD300VGG骨干單卡12GB顯存一共訓(xùn)練200個(gè)epoch。前10個(gè)epoch熱身后驗(yàn)證集上mAP已經(jīng)能到0.7左右之后在0.75到0.8之間緩慢爬升最終穩(wěn)定在0.83附近。整個(gè)訓(xùn)練過(guò)程大約幾個(gè)到十幾個(gè)小時(shí)完全在可接受范圍。如果訓(xùn)練結(jié)果顯示mAP只有0.3甚至更低大概率不是網(wǎng)絡(luò)結(jié)構(gòu)問(wèn)題而是以下某個(gè)環(huán)節(jié)出了問(wèn)題標(biāo)注錯(cuò)位、類(lèi)別不均衡、訓(xùn)練集劃分包含重復(fù)圖、學(xué)習(xí)率過(guò)大導(dǎo)致震蕩。我建議按順序排查先可視化一批訓(xùn)練樣本檢查標(biāo)注框是否正確對(duì)齊到目標(biāo)再統(tǒng)計(jì)每個(gè)類(lèi)別的訓(xùn)練樣本數(shù)量看是否存在嚴(yán)重的類(lèi)別不平衡最后再回頭看學(xué)習(xí)率是否合適。關(guān)于loss出現(xiàn)nan最常見(jiàn)的原因是學(xué)習(xí)率太大導(dǎo)致梯度爆炸數(shù)據(jù)處理時(shí)出現(xiàn)除零或者是輸入圖像中出現(xiàn)異常像素值。我的排查順序是先降低學(xué)習(xí)率十倍看看nan是否消失如果還不行再檢查數(shù)據(jù)增強(qiáng)模塊里會(huì)不會(huì)產(chǎn)生非法邊界框。訴諸于開(kāi)梯度裁剪或者調(diào)小batch是最后的手段。很多同學(xué)喜歡一上來(lái)就追求花哨的新模型但我想說(shuō)先把SSD用透真的足夠了。SSD的訓(xùn)練流程覆蓋了數(shù)據(jù)加載、錨框匹配、損失計(jì)算、NMS去重、評(píng)估指標(biāo)等全部核心知識(shí)點(diǎn)如果你在一個(gè)模型上把這些環(huán)節(jié)都理解透了再遷移到任何其他檢測(cè)模型都不費(fèi)勁。5. 檢測(cè)推理、評(píng)估與落地?cái)U(kuò)展5.1 推理管線與核心代碼訓(xùn)練完模型后離檢測(cè)自己的數(shù)據(jù)集只差最后一步推理。推理流程和訓(xùn)練時(shí)不一樣不需要匹配真實(shí)框也不需要難例挖掘核心只有預(yù)處理、前向、解碼、NMS四步。預(yù)處理的關(guān)鍵是把輸入圖片縮放到訓(xùn)練時(shí)的尺寸并做同樣的歸一化。很多推理報(bào)錯(cuò)都源于這里你訓(xùn)練時(shí)用了一張300x300的圖推理時(shí)卻塞進(jìn)去一張?jiān)几咔宕髨D模型當(dāng)然輸出不對(duì)。SSD的輸入是固定尺寸的這也是它和全卷積網(wǎng)絡(luò)的一個(gè)區(qū)別。下面是一段基于PyTorch的推理核心代碼可以直接參考import torch import torch.nn.functional as F import cv2 import numpy as np def detect_one_image(model, img_path, class_names, conf_thresh0.5, nms_thresh0.45): model.eval() img cv2.imread(img_path) # 縮放并歸一化 img_resized cv2.resize(img, (300, 300)).astype(np.float32) # 按訓(xùn)練配置做減均值、乘縮放 img_tensor torch.from_numpy(img_resized).permute(2, 0, 1).unsqueeze(0) img_tensor (img_tensor / 255.0) with torch.no_grad(): # 前向返回分類(lèi)置信度和預(yù)測(cè)框偏移 confidences, locations model(img_tensor) # 解碼根據(jù)默認(rèn)框和偏移計(jì)算真實(shí)坐標(biāo) boxes decode(locations, default_boxes) # 由具體實(shí)現(xiàn)提供 # 置信度過(guò)濾 keep confidences.squeeze(0).max(dim1).values conf_thresh boxes boxes.squeeze(0)[keep] scores confidences.squeeze(0)[keep] # NMS去重 final_boxes, final_scores, final_labels nms(boxes, scores, nms_thresh) return final_boxes, final_scores, final_labels這里的decode函數(shù)做的是把網(wǎng)絡(luò)預(yù)測(cè)的偏移量加回默認(rèn)框上。SSD在訓(xùn)練時(shí)不是直接回歸框的絕對(duì)坐標(biāo)而是回歸相對(duì)于默認(rèn)框的編碼偏移量所以推理時(shí)也必須用同樣的編碼方式解碼。這一步很容易出錯(cuò)如果你的推理框位置完全對(duì)不上目標(biāo)先懷疑解碼邏輯有沒(méi)有和數(shù)據(jù)預(yù)處理對(duì)齊。NMS非極大值抑制的目標(biāo)是去掉重復(fù)檢測(cè)同一目標(biāo)的框。它的邏輯很直白先把所有候選框按置信度從高到低排序然后取置信度最高的框把所有和它IoU大于閾值的框都刪掉再取下一個(gè)最高分的框重復(fù)直到處理完。nms_thresh一般設(shè)0.45太高會(huì)導(dǎo)致多個(gè)框重疊在一起太低會(huì)把同一目標(biāo)的不同部位拆成多個(gè)目標(biāo)??梢暬评頃r(shí)conf_thresh可以設(shè)0.5甚至更高減少誤檢框的干擾。5.2 mAP評(píng)估與結(jié)果可視化跑完測(cè)試集后你需要一個(gè)客觀的量化指標(biāo)來(lái)衡量模型好壞。標(biāo)準(zhǔn)做法是計(jì)算mAPmean Average Precision。具體來(lái)說(shuō)對(duì)每個(gè)類(lèi)別繪制精度-召回率曲線曲線下的面積就是該類(lèi)別的AP所有類(lèi)別AP取平均就是mAP。mAP這個(gè)數(shù)字很有用但它會(huì)掩蓋一些信息。一個(gè)常見(jiàn)陷阱是如果某個(gè)類(lèi)別的訓(xùn)練樣本特別少它的AP會(huì)非常不穩(wěn)定而mAP把這個(gè)不穩(wěn)定平均掉了。所以我評(píng)估時(shí)不僅看mAP總數(shù)值還會(huì)逐個(gè)類(lèi)別看AP并打印每個(gè)類(lèi)別的召回率。如果發(fā)現(xiàn)某個(gè)類(lèi)檢測(cè)得特別差多半是它的樣本太少或者場(chǎng)景和訓(xùn)練數(shù)據(jù)差異過(guò)大這時(shí)候優(yōu)先補(bǔ)數(shù)據(jù)而不是調(diào)模型。除了數(shù)字指標(biāo)可視化評(píng)估也必不可少。把測(cè)試圖片跑一遍檢測(cè)把預(yù)測(cè)框畫(huà)在原圖上并保存成圖片然后人工掃一遍這些結(jié)果圖。這一步能讓你發(fā)現(xiàn)數(shù)字指標(biāo)發(fā)現(xiàn)不了的問(wèn)題模型是不是總在某個(gè)固定位置誤檢是不是對(duì)某一類(lèi)特定顏色特別敏感可視化結(jié)果是和業(yè)務(wù)方溝通時(shí)最有說(shuō)服力的材料也是判斷模型是否真的可用的最后一關(guān)。5.3 擴(kuò)展模型導(dǎo)出與部署思路如果你只想在自己的電腦上跑推理直接加載pth文件就行。但如果你要把它集成到服務(wù)或者另一個(gè)程序里可以導(dǎo)出成標(biāo)準(zhǔn)格式。PyTorch模型導(dǎo)出最常用的方式是ONNX導(dǎo)出后用ONNX Runtime推理可以明顯提升部署時(shí)的加載速度和推理性能。導(dǎo)出前有幾點(diǎn)要注意模型結(jié)構(gòu)固定成訓(xùn)練時(shí)的狀態(tài)輸入尺寸也固定下來(lái)導(dǎo)出后一定要在ONNX Runtime里跑一張測(cè)試圖和PyTorch的推理結(jié)果做數(shù)值對(duì)比偏差應(yīng)該在極小范圍。如果差異大基本可以確定是預(yù)處理環(huán)節(jié)在兩邊寫(xiě)得不一致。部署到CPU時(shí)還要注意有些在GPU上天然支持的操作在CPU上會(huì)退化為較慢的實(shí)現(xiàn)所以導(dǎo)出后實(shí)測(cè)CPU推理時(shí)間是很必要的。業(yè)務(wù)場(chǎng)景如果對(duì)實(shí)時(shí)性要求很高還可以考慮把SSD模型轉(zhuǎn)成更輕量的版本但這已經(jīng)屬于優(yōu)化范疇不在跑通流程的范圍內(nèi)。先把標(biāo)準(zhǔn)流程跑通再考慮優(yōu)化是我反復(fù)強(qiáng)調(diào)的做事節(jié)奏。6. 常見(jiàn)問(wèn)題、避坑實(shí)錄與個(gè)人心得6.1 高頻錯(cuò)誤排查速查表下面這張表是我在多次指導(dǎo)和自用過(guò)程中總結(jié)出來(lái)的高頻問(wèn)題清單。你訓(xùn)練時(shí)遇到報(bào)錯(cuò)和異常先來(lái)這里對(duì)照一下。問(wèn)題現(xiàn)象最常見(jiàn)原因解決建議loss一直不降標(biāo)注錯(cuò)位 / 類(lèi)別錯(cuò)亂 / 學(xué)習(xí)率太小可視化訓(xùn)練樣本、檢查標(biāo)簽映射、適當(dāng)調(diào)大學(xué)習(xí)率loss變成nan學(xué)習(xí)率太大 / 數(shù)據(jù)增強(qiáng)產(chǎn)生非法框 / 梯度爆炸降低學(xué)習(xí)率10倍、檢查增強(qiáng)代碼、加梯度裁剪顯存不足OOMbatch太大 / 輸入分辨率太高減小batch、降低分辨率、使用梯度累積檢測(cè)不到任何目標(biāo)conf_thresh太高 / 模型欠擬合 / 解碼邏輯錯(cuò)誤調(diào)低閾值、延長(zhǎng)訓(xùn)練、重點(diǎn)檢查decode函數(shù)小目標(biāo)漏檢嚴(yán)重300分辨率信息丟失 / 小尺寸默認(rèn)框偏少換512輸入、檢查小尺度特征圖預(yù)測(cè)頭預(yù)測(cè)框位置偏移解碼偏移量計(jì)算錯(cuò)誤 / 預(yù)處理不一致核對(duì)編碼解碼公式、統(tǒng)一輸入歸一化方式評(píng)估m(xù)AP虛高數(shù)據(jù)泄漏 / 測(cè)試集包含訓(xùn)練圖像按場(chǎng)景分組劃分?jǐn)?shù)據(jù)集重新生成索引加載權(quán)重報(bào)錯(cuò)預(yù)訓(xùn)練權(quán)重結(jié)構(gòu)不匹配比對(duì)狀態(tài)字典鍵名做鍵名重映射補(bǔ)充一點(diǎn)訓(xùn)練環(huán)境報(bào)錯(cuò)時(shí)不要一上來(lái)就懷疑代碼庫(kù)。先看自己有沒(méi)有改壞配置再看數(shù)據(jù)路徑是否正確最后才考慮代碼本身的bug。大部分人的問(wèn)題都出在前兩類(lèi)。6.2 幾個(gè)很少有人提醒的隱性坑有個(gè)坑是我自己踩過(guò)多次的訓(xùn)練集和驗(yàn)證集的數(shù)據(jù)增強(qiáng)不一致。很多代碼默認(rèn)訓(xùn)練集做全套數(shù)據(jù)增強(qiáng)驗(yàn)證集和測(cè)試集只做簡(jiǎn)單歸一化。這本身沒(méi)錯(cuò)但如果你為了省事在劃分?jǐn)?shù)據(jù)集時(shí)把驗(yàn)證集圖片也扔進(jìn)訓(xùn)練數(shù)據(jù)增強(qiáng)管道里驗(yàn)證指標(biāo)就會(huì)異常波動(dòng)而且你很難意識(shí)到是這里出了問(wèn)題。我建議每一個(gè)需要改數(shù)據(jù)處理的環(huán)節(jié)都單獨(dú)寫(xiě)清楚注釋不弄混訓(xùn)練和評(píng)估兩套流程。另一個(gè)隱性坑是訓(xùn)練集里有些圖的標(biāo)注框完全在圖像外。出現(xiàn)這種情況多半是轉(zhuǎn)換腳本處理坐標(biāo)時(shí)出了問(wèn)題或者圖片被裁剪縮放后標(biāo)注沒(méi)跟著變。SSD的代碼一般會(huì)跳過(guò)或者報(bào)錯(cuò)但如果你用的是那種隱式忽略的版本模型就會(huì)在不知不覺(jué)中學(xué)到錯(cuò)誤信息。我發(fā)現(xiàn)這個(gè)問(wèn)題的方法是統(tǒng)計(jì)每個(gè)圖片的標(biāo)注框位置看看有沒(méi)有框的中心點(diǎn)坐標(biāo)異常超出圖像尺寸的情況一旦發(fā)現(xiàn)就回到轉(zhuǎn)換腳本修正。還有一個(gè)值得養(yǎng)成的好習(xí)慣訓(xùn)練過(guò)程中每過(guò)一定輪次就抽樣可視化一次驗(yàn)證集的檢測(cè)結(jié)果保存成帶有epoch編號(hào)的圖片。這樣你能直接看到模型從亂框到有模有樣的變化過(guò)程也能及時(shí)發(fā)現(xiàn)模型開(kāi)始過(guò)擬合的征兆比如訓(xùn)練loss下降但驗(yàn)證集AP開(kāi)始下降。我把這個(gè)習(xí)慣保留到了現(xiàn)在每次訓(xùn)練都會(huì)定期翻開(kāi)那些可視化圖片直觀感受模型學(xué)到了什么這比單純盯loss曲線更能建立對(duì)模型行為的直覺(jué)。最后說(shuō)一個(gè)我從實(shí)踐里得到的總經(jīng)驗(yàn)?zāi)繕?biāo)檢測(cè)項(xiàng)目里最珍貴的東西是數(shù)據(jù)質(zhì)量和數(shù)據(jù)處理的一致性而不是模型結(jié)構(gòu)有多酷炫。SSD給了你一套非常成熟可靠的框架你把數(shù)據(jù)管好、把訓(xùn)練流程走穩(wěn)效果就能超過(guò)很多數(shù)據(jù)一團(tuán)糟但模型很新的項(xiàng)目。先用SSD跑通自己的完整流程等你對(duì)錨框、匹配、NMS、mAP這些概念都有了真實(shí)手感再去看任何更復(fù)雜的模型都會(huì)輕松很多。