境下復(fù)現(xiàn)Faster R-CNN完整教程:TensorFlow+PyCharm實(shí)戰(zhàn))
很多人一聽到Faster R-CNN第一反應(yīng)就是“必須得有GPU”否則根本跑不起來。實(shí)際上我在Windows上用CPU完整復(fù)現(xiàn)過TensorFlow版本的Faster R-CNN配合PyCharm做調(diào)試不僅代碼能跑通訓(xùn)練和推理也都能正常出結(jié)果只是速度慢一些而已。這篇帖子就圍繞Faster-RCNN-tf-windows-python-pycharm這條技術(shù)路線把我在CPU環(huán)境下復(fù)現(xiàn)代碼的完整過程講清楚包括環(huán)境怎么搭、源碼怎么讀、參數(shù)怎么調(diào)、坑在哪里希望能給你省下幾天的排查時(shí)間。1. 復(fù)現(xiàn)之前必須先弄明白這個(gè)模型到底在做什么1.1 Faster R-CNN的三大網(wǎng)絡(luò)塊和各自職責(zé)Faster R-CNN是一個(gè)典型的兩階段目標(biāo)檢測(cè)模型但它的“兩階段”不是指兩個(gè)獨(dú)立的網(wǎng)絡(luò)在串聯(lián)跑而是同一個(gè)骨干網(wǎng)絡(luò)提取特征后分叉出兩條分支。我用一個(gè)容易理解的方式來拆解骨干網(wǎng)絡(luò)負(fù)責(zé)從原始圖片里抽取特征。當(dāng)年比較流行的選擇是VGG16輸入圖片會(huì)被歸一化到固定尺寸例如短邊600像素、長(zhǎng)邊1000像素以內(nèi)經(jīng)過一系列卷積和池化后得到一個(gè)大為縮小但通道加深的特征圖。這個(gè)特征圖可以看作一種“濃縮版畫面”保留了物體的空間位置信息和語義信息。RPN區(qū)域提議網(wǎng)絡(luò)在這張?zhí)卣鲌D上滑動(dòng)小窗口為每個(gè)位置生成9個(gè)不同尺寸和比例的錨框然后判斷這些錨框里是否包含前景物體并粗調(diào)錨框的位置。它輸出的是一批可能包含物體的候選區(qū)域通常有幾百到兩千個(gè)。檢測(cè)頭負(fù)責(zé)對(duì)RPN給出的候選區(qū)域做最終分類和坐標(biāo)精修。由于候選區(qū)域的尺寸各不相同ROI Pooling層會(huì)把每個(gè)候選區(qū)域內(nèi)的特征統(tǒng)一池化成相同尺寸比如7×7然后送入全連接層輸出具體的類別概率和精確的邊框坐標(biāo)。這三個(gè)模塊合在一起才是完整的Faster R-CNN。訓(xùn)練時(shí)損失函數(shù)也分為RPN的分類損失、RPN的回歸損失、檢測(cè)頭的分類損失、檢測(cè)頭的回歸損失四部分。理解了這條鏈路你后面讀源碼時(shí)就不會(huì)迷失在文件堆里。1.2 為什么TensorFlow實(shí)現(xiàn)特別適合作為復(fù)現(xiàn)目標(biāo)深度學(xué)習(xí)框架層出不窮但TensorFlow 1.x時(shí)代積累下來的Faster R-CNN工程代碼對(duì)學(xué)習(xí)而言仍然有不可替代的價(jià)值因?yàn)樗墓こ探Y(jié)構(gòu)非常完整數(shù)據(jù)集處理、網(wǎng)絡(luò)定義、訓(xùn)練循環(huán)、測(cè)試腳本、可視化工具都分得清清楚楚。你跟著代碼走一遍等于把目標(biāo)檢測(cè)的完整工程鏈學(xué)了一遍。相比PyTorch風(fēng)格的實(shí)現(xiàn)TensorFlow 1.x的靜態(tài)圖模式有一個(gè)特點(diǎn)先構(gòu)建計(jì)算圖再在Session里喂數(shù)據(jù)執(zhí)行。這個(gè)特點(diǎn)在CPU平臺(tái)上反而有一個(gè)好處——每一步算子執(zhí)行情況非常明確計(jì)算圖的每個(gè)節(jié)點(diǎn)都能通過TensorBoard可視化檢查你甚至能清楚地看到每個(gè)張量在哪個(gè)環(huán)節(jié)被創(chuàng)建、在哪個(gè)環(huán)節(jié)被消耗。另外社區(qū)里傳播很廣的那份經(jīng)典TensorFlow實(shí)現(xiàn)對(duì)CPU環(huán)境沒有太多性能上的惡意要求默認(rèn)配置的batch size就是1這意味著顯存和內(nèi)存壓力都很小CPU機(jī)器完全扛得住。1.3 這種方式適合誰、不適合誰我的結(jié)論很明確如果你是想快速刷榜、處理海量數(shù)據(jù)或者參加比賽CPU復(fù)現(xiàn)Faster R-CNN不是好選擇訓(xùn)練速度會(huì)讓你崩潰。但如果你是為了理解目標(biāo)檢測(cè)的原理、完成課程設(shè)計(jì)、做畢業(yè)設(shè)計(jì)或者要在小規(guī)模自定義數(shù)據(jù)集上驗(yàn)證算法可行性這套方案完全可以應(yīng)付。CPU復(fù)現(xiàn)的另一個(gè)合適場(chǎng)景是代碼級(jí)調(diào)試。在GPU環(huán)境下每一步操作都很快反而沒有耐心去逐行追蹤數(shù)據(jù)流在CPU上你被迫等待節(jié)奏放慢你可以用PyCharm的斷點(diǎn)功能在每一個(gè)關(guān)鍵環(huán)節(jié)停下來觀察張量形狀和數(shù)值這種學(xué)習(xí)效果是GPU跑一千次也換不來的。2. Windows環(huán)境搭建版本組合是成敗的關(guān)鍵2.1 一份經(jīng)過驗(yàn)證的依賴清單與版本組合復(fù)現(xiàn)老代碼最頭疼的事情是版本匹配。TensorFlow 1.x的代碼在TensorFlow 2.x下有一堆API會(huì)被移除直接跑必然報(bào)錯(cuò)例如tf.Session、tf.placeholder等。所以我的第一原則是代碼用什么時(shí)代的API環(huán)境就匹配什么時(shí)代。以下是一套我在Windows 10 64位系統(tǒng)上實(shí)際驗(yàn)證過的組合軟件推薦版本說明Python3.6.8TensorFlow 1.13.1在Python 3.6下支持最穩(wěn)定TensorFlow1.13.1 CPU版帶MKL加速CPU推理速度比早期版本好一些PyCharmCommunity版即可調(diào)試功能夠用numpy1.16.4與TensorFlow 1.13兼容良好scipy1.2.1太高版本會(huì)找不到部分模塊opencv-python4.1.x用于圖片讀取和結(jié)果可視化cython0.29.x編譯部分C擴(kuò)展代碼easydict1.9訪問配置字典用點(diǎn)號(hào)方式pillow6.2.0圖片處理matplotlib3.x可視化loss曲線重點(diǎn)提醒一下不要為了“追求新版本”去裝最新的Python或TensorFlow這會(huì)踩進(jìn)一堆莫名其妙的坑里。很多老項(xiàng)目代碼里的寫法只認(rèn)這些老接口。如果你的某個(gè)基礎(chǔ)庫版本不得不上調(diào)一定要逐個(gè)測(cè)試兼容性不能想當(dāng)然。2.2 PyCharm項(xiàng)目結(jié)構(gòu)、解釋器設(shè)置與編譯擴(kuò)展拿到代碼后我是這樣組織目錄的Faster-RCNN-TF/ ├── experiments/ # 訓(xùn)練腳本和日志輸出 ├── lib/ # 核心代碼庫 │ ├── datasets/ # 數(shù)據(jù)集讀取 │ ├── networks/ # 網(wǎng)絡(luò)結(jié)構(gòu)定義 │ ├── fast_rcnn/ # 訓(xùn)練配置、測(cè)試接口 │ ├── utils/ # 工具函數(shù) ├── data/ # 數(shù)據(jù)和預(yù)訓(xùn)練權(quán)重 │ ├── VOCdevkit2007/ # 自定義數(shù)據(jù)集 │ ├── pretrain_model/ # VGG16預(yù)訓(xùn)練權(quán)重 ├── tools/ # 訓(xùn)練、測(cè)試、demo入口 ├── output/ # 訓(xùn)練日志與模型保存在PyCharm里打開這個(gè)目錄然后進(jìn)入Settings里的Project Interpreter選擇你安裝好的Python 3.6環(huán)境。這里有一個(gè)關(guān)鍵點(diǎn)代碼里有幾個(gè)Cython擴(kuò)展模塊第一次使用前必須編譯否則運(yùn)行時(shí)會(huì)提示找不到某些子模塊。編譯的方式是在終端里進(jìn)入對(duì)應(yīng)目錄執(zhí)行python setup.py build_ext --inplace這一步需要系統(tǒng)里有可用的C編譯工具。Windows上通常要裝Visual C Build Tools或者Visual Studio的C組件。裝完之后如果環(huán)境變量沒刷新生效重新打開一個(gè)終端再試。編譯完成以后其他代碼就能直接import這些擴(kuò)展模塊了。如果這一步編譯失敗99%的情況是C編譯工具沒裝全而不會(huì)是你的Python環(huán)境有問題。2.3 準(zhǔn)備訓(xùn)練數(shù)據(jù)最省心的方式是跑通一個(gè)小數(shù)據(jù)集很多人在復(fù)現(xiàn)項(xiàng)目時(shí)喜歡直接拿完整的大型數(shù)據(jù)集去訓(xùn)練結(jié)果一下就跑好幾個(gè)小時(shí)期間問題還不斷。我的建議是先構(gòu)造一個(gè)極小的自定義數(shù)據(jù)集只包含200到300張圖片、兩三個(gè)類別把整個(gè)流程徹底跑通再考慮用更多數(shù)據(jù)。數(shù)據(jù)集的組織方式按照PASCAL VOC的目錄結(jié)構(gòu)來data/VOCdevkit2007/ ├── VOC2007/ │ ├── Annotations/ # 存放XML標(biāo)注文件 │ ├── JPEGImages/ # 存放圖片 │ ├── ImageSets/ │ │ └── Main/ # 存放train.txt、val.txt等列表文件XML標(biāo)注文件的格式也必須符合VOC規(guī)范里面要包含object節(jié)點(diǎn)每個(gè)object節(jié)點(diǎn)里有name、bndbox坐標(biāo)等信息。如果你的原始標(biāo)注是其他格式可以用腳本轉(zhuǎn)換這個(gè)是常規(guī)操作網(wǎng)上有大量參考腳本按自己的文件格式改一改就行。生成train.txt、val.txt時(shí)每行只寫圖片的文件名前綴不包含擴(kuò)展名。例如lane_001 lane_002 ...2.4 預(yù)訓(xùn)練權(quán)重沒有它在CPU上從頭訓(xùn)會(huì)非常痛苦Faster R-CNN的骨干網(wǎng)絡(luò)按慣例使用ImageNet上預(yù)訓(xùn)練過的VGG16權(quán)重初始化。沒有這個(gè)預(yù)訓(xùn)練權(quán)重在CPU上從零開始訓(xùn)練收斂速度會(huì)慢到讓你懷疑人生。預(yù)訓(xùn)練權(quán)重文件的放置路徑一般在data/pretrain_model/vgg_16.ckpt注意下載時(shí)要確認(rèn)文件格式是ckpt格式而不是別的格式。下載好之后放到指定目錄并在配置文件中確認(rèn)權(quán)重路徑指向正確。實(shí)際操作時(shí)我建議訓(xùn)練前先跑一次代碼流程確認(rèn)權(quán)重真的被加載了而不是加載了空白參數(shù)。怎么確認(rèn)呢可以在初始化Session的地方打印一兩層卷積層的變量名和值范圍看到數(shù)值接近ImageNet訓(xùn)練出的合理范圍就沒問題了。3. 源碼閱讀先用放大鏡看數(shù)據(jù)流再按啟動(dòng)鍵3.1 訓(xùn)練入口腳本train.py里到底發(fā)生了什么tools目錄下通常會(huì)有一個(gè)train_net.py或train.py命令行參數(shù)包括GPU設(shè)備選擇、訓(xùn)練權(quán)重路徑、數(shù)據(jù)集名稱、最大迭代次數(shù)等。即便你是CPU環(huán)境GPU相關(guān)參數(shù)也要留在代碼里不動(dòng)因?yàn)槌绦騼?nèi)部統(tǒng)一走配置文件讀取沒有GPU也不會(huì)主動(dòng)調(diào)用CUDA。訓(xùn)練入口做的事情按順序拆開看是這樣加載配置文件包括學(xué)習(xí)率、步長(zhǎng)、batch size等超參數(shù)。構(gòu)建數(shù)據(jù)集對(duì)象讀取標(biāo)注信息生成一個(gè)包含每張圖片路徑和標(biāo)注框的roidb列表。構(gòu)建網(wǎng)絡(luò)結(jié)構(gòu)加載預(yù)訓(xùn)練權(quán)重。創(chuàng)建訓(xùn)練Session往數(shù)據(jù)讀取隊(duì)列里喂數(shù)據(jù)。循環(huán)調(diào)用sess.run執(zhí)行訓(xùn)練和定期保存模型。你可以在讀取標(biāo)注信息之后、訓(xùn)練循環(huán)之前先打印一下roidb的長(zhǎng)度和第一條內(nèi)容確認(rèn)數(shù)據(jù)讀對(duì)了再往后走。3.2 數(shù)據(jù)集與roidb的創(chuàng)建過程標(biāo)注信息如何變成訓(xùn)練信號(hào)這部分是代碼閱讀的重點(diǎn)。lib/datasets目錄下的pascal_voc.py文件里有一個(gè)類它的任務(wù)是解析XML標(biāo)注文件把標(biāo)簽名稱映射成類別編號(hào)并將所有標(biāo)注框信息整理成一張“信息表”也叫做roidb。roidb里的每一條記錄包含圖片路徑、圖片尺寸、標(biāo)注框坐標(biāo)、標(biāo)注框?qū)?yīng)的類別標(biāo)簽以及一個(gè)非常重要的字段是否包含困難樣本。訓(xùn)練過程中的很多預(yù)處理邏輯都是圍繞這個(gè)表進(jìn)行的。接下來訓(xùn)練管線會(huì)循環(huán)從roidb里取數(shù)據(jù)對(duì)圖片做隨機(jī)裁剪、翻轉(zhuǎn)、亮度調(diào)整等操作同時(shí)對(duì)應(yīng)地調(diào)整標(biāo)注框坐標(biāo)。理解了這個(gè)流程你就理解了一個(gè)關(guān)鍵點(diǎn)數(shù)據(jù)增強(qiáng)不是隨便亂做而是要把圖片的變換同步應(yīng)用到標(biāo)注框上否則你訓(xùn)練出的模型就是“訓(xùn)練集上有效、測(cè)試集上失靈”的廢模型。3.3 網(wǎng)絡(luò)核心VGGnet_train與VGGnet_test的使用邏輯在lib/networks目錄下你會(huì)看到訓(xùn)練網(wǎng)絡(luò)和測(cè)試網(wǎng)絡(luò)兩個(gè)類。它們共享相同的卷積層但尾部分支不同訓(xùn)練版網(wǎng)絡(luò)會(huì)額外組裝損失層把RPN和檢測(cè)頭兩部分損失算出來測(cè)試版網(wǎng)絡(luò)則只輸出候選區(qū)域、類別概率和框坐標(biāo)不計(jì)算損失。讀這兩個(gè)文件時(shí)我最在意的部分是Anchor的生成邏輯。特征圖上的每個(gè)像素點(diǎn)對(duì)應(yīng)9個(gè)Anchor這些Anchor的坐標(biāo)映射回原圖時(shí)可能超出圖片邊界因此代碼里有一個(gè)過濾邏輯把所有超出邊界的Anchor剔除。留下來的Anchor和真實(shí)標(biāo)注框做IoU計(jì)算高于正樣本閾值的作為前景低于負(fù)樣本閾值的作為背景中間模棱兩可的直接忽略。這里有一個(gè)很多人想不明白的細(xì)節(jié)為什么RPN訓(xùn)練時(shí)只隨機(jī)采樣一部分Anchor參與損失計(jì)算答案是前景框數(shù)量遠(yuǎn)少于背景框如果全部參與計(jì)算模型會(huì)嚴(yán)重偏向預(yù)測(cè)“沒有物體”。代碼里的標(biāo)準(zhǔn)做法是從前景Anchor中采樣一小批從背景Anchor中采樣一小批湊成一個(gè)小batch這樣正負(fù)樣本比例不至于太失衡。3.4 為什么必須先理解數(shù)據(jù)流再跑訓(xùn)練在CPU上一次完整的迭代可能就要零點(diǎn)幾秒到一兩秒跑幾萬步等于熬幾個(gè)小時(shí)。如果你對(duì)數(shù)據(jù)流和損失函數(shù)的組裝邏輯不清楚訓(xùn)練中途發(fā)現(xiàn)loss不降或者激增根本無從排查。我的經(jīng)驗(yàn)是正式訓(xùn)練前先用PyCharm的調(diào)試模式運(yùn)行20次迭代每迭代斷點(diǎn)查看一次輸入圖片的shape、標(biāo)簽值、RPN loss值、檢測(cè)頭loss值??吹竭@些數(shù)值都是合理的有限數(shù)字再放手讓它整夜跑。這一步最多消耗你半小時(shí)但能讓你后面少走幾十小時(shí)的彎路。4. CPU訓(xùn)練實(shí)操配置、跑批與日志判讀4.1 動(dòng)手改參數(shù)哪些可以動(dòng)哪些最好別動(dòng)開始訓(xùn)練前我會(huì)打開lib/fast_rcnn/config.py文件逐個(gè)核對(duì)這些配置項(xiàng)MAX_ITERS總迭代次數(shù)。默認(rèn)可能是70000CPU環(huán)境建議先改成2000跑通流程確認(rèn)沒問題后再改回來。LEARNING_RATE初始學(xué)習(xí)率默認(rèn)0.001不用動(dòng)。BATCH_SIZE默認(rèn)1保留。CPU想提高吞吐可以試2但相應(yīng)地?cái)?shù)據(jù)結(jié)構(gòu)調(diào)整會(huì)比較復(fù)雜新手不建議動(dòng)。DISPLAY每多少步打印一次日志默認(rèn)20可以改成1方便觀察。SNAPSHOT_ITERS每多少步保存一次模型默認(rèn)10000CPU環(huán)境建議改成1000防止中途意外中斷導(dǎo)致白跑。還有一個(gè)容易被忽略的參數(shù)是NUM_CLASSES它表示類別數(shù)加背景如果你的數(shù)據(jù)只有兩類物體那這里就填3。這個(gè)數(shù)字和數(shù)據(jù)集配置、網(wǎng)絡(luò)輸出維度是一一對(duì)應(yīng)的填錯(cuò)會(huì)直接導(dǎo)致維度不匹配或者訓(xùn)練結(jié)果詭異。訓(xùn)練命令參考python tools/train_net.py --weight data/pretrain_model/vgg_16.ckpt --imdb voc_2007_trainval --iters 2000 --cfg experiments/cfgs/faster_rcnn_end2end.yml第一次跑看到命令行輸出中每隔一段出現(xiàn)如下這樣的日志就說明流程是通的Step 20, image: 1, total_loss: 1.845, rpn_loss_cls: 0.512, rpn_loss_box: 0.731, fast_rcnn_loss_cls: 0.412, fast_rcnn_loss_box: 0.1904.2 日志判讀怎么算正常怎么算有問題判斷訓(xùn)練是否正常的標(biāo)志不是單看total_loss降不降而是要看四個(gè)分量是否都在合理范圍內(nèi)波動(dòng)。RPN分類損失正常會(huì)在0.2到0.7之間浮動(dòng)最終往0.1附近收斂RPN回歸損失通常在1.0以內(nèi)緩慢下降檢測(cè)頭分類損失開始會(huì)比較高接近類別數(shù)的對(duì)數(shù)級(jí)別后期會(huì)降到0.2以下檢測(cè)頭回歸損失同樣呈下降趨勢(shì)。如果某個(gè)損失長(zhǎng)期不變或者直接爆炸到NaN優(yōu)先檢查三樣?xùn)|西學(xué)習(xí)率是否過大把數(shù)值頂?shù)揭绯鰳?biāo)注數(shù)據(jù)里是否混入了空框或者坐標(biāo)異常例如框?qū)捀邽?圖片歸一化時(shí)是否出現(xiàn)了除以零的情況。CPU上訓(xùn)練還有一個(gè)特點(diǎn)因?yàn)槊看蔚?jì)算時(shí)間較長(zhǎng)loss曲線的“瞬時(shí)抖動(dòng)”會(huì)更明顯。不用看單步數(shù)值而是要看幾十步的平均趨勢(shì)。建議每100步記錄一次loss保存到txt里之后用matplotlib畫個(gè)平滑曲線比盯著終端日志有效得多。4.3 讓CPU跑得更聰明線程數(shù)與指令集優(yōu)化TensorFlow CPU版本如果安裝的是帶MKL加速的版本會(huì)自動(dòng)利用CPU的指令集優(yōu)化。不過線程管理上可以稍微干預(yù)一下。在啟動(dòng)訓(xùn)練前我習(xí)慣在同一終端里先設(shè)置幾個(gè)環(huán)境變量再運(yùn)行訓(xùn)練腳本set OMP_NUM_THREADS8 set KMP_BLOCKTIME1 set KMP_AFFINITYgranularityfine,compact,1,0 set KMP_SETTINGS1如果不知道自己的CPU是幾核心先按物理核心數(shù)設(shè)置不要超過物理核心數(shù)。除了環(huán)境變量我還會(huì)在代碼里固定一下TensorFlow的線程配置config tf.ConfigProto( intra_op_parallelism_threads8, inter_op_parallelism_threads8, allow_soft_placementTrue )這樣TensorFlow在做矩陣運(yùn)算時(shí)才會(huì)真正利用多核心。默認(rèn)設(shè)置可能只占滿一部分核心CPU利用率上去了步數(shù)時(shí)間能縮短40%以上。4.4 收斂檢查用驗(yàn)證集而不是訓(xùn)練集給自己交代訓(xùn)練時(shí)定期保存出來的model.ckpt文件是你在測(cè)試階段用來做推理的憑據(jù)。每5000步可以把模型保存一份然后用tools里的測(cè)試腳本對(duì)驗(yàn)證集圖片做一次評(píng)估。測(cè)試命令參考python tools/test_net.py --weights output/vgg16/voc_2007_trainval/model.ckpt --imdb voc_2007_test測(cè)試腳本會(huì)對(duì)每張圖片推薦候選框并計(jì)算各類別的平均精確度。觀察這個(gè)mAP值的變化趨勢(shì)比盯訓(xùn)練loss有用得多。你會(huì)發(fā)現(xiàn)一個(gè)規(guī)律訓(xùn)練loss還在平穩(wěn)下降mAP可能已經(jīng)在某個(gè)水平橫盤了這就是過擬合的前兆這時(shí)候就需要考慮數(shù)據(jù)增強(qiáng)或增加訓(xùn)練數(shù)據(jù)。5. 踩坑實(shí)錄我在CPU復(fù)現(xiàn)過程中遇到的高頻問題5.1 版本類問題編譯器版本導(dǎo)致的Cython擴(kuò)展報(bào)錯(cuò)第一次運(yùn)行demo腳本時(shí)我遇到了找不到lib.utils.cython_nms模塊的錯(cuò)誤。編譯的時(shí)候Cython是裝上了但編譯產(chǎn)物沒有被正確放到位。排查過程是這樣的第一步確認(rèn)是否已經(jīng)執(zhí)行過build_ext --inplace。第二步檢查編譯產(chǎn)物文件是否存在例如在lib/utils目錄下應(yīng)該有一個(gè)cython_nms.pyd文件。第三步確認(rèn)pyd文件名和import語句里的名字是否完全一致包括大小寫和末尾下劃線。這類問題大多在Windows上才會(huì)遇到因?yàn)長(zhǎng)inux下的編譯產(chǎn)物是.so文件名稱匹配規(guī)則會(huì)寬松一些。解決方法很干脆重新編譯把生成的pyd文件放在正確目錄下不要改文件名。5.2 pycocotools裝不上怎么辦很多Faster R-CNN實(shí)現(xiàn)的評(píng)估腳本依賴pycocotools這個(gè)包。在Windows上安裝它需要編譯C擴(kuò)展如果你沒有完整的開發(fā)工具鏈pip install pycocotools會(huì)直接報(bào)錯(cuò)一大片紅字。我當(dāng)時(shí)沒有專門去裝編譯鏈而是繞開了這個(gè)依賴。具體做法是在代碼中找到import pycocotools的地方然后把它替換成等價(jià)的簡(jiǎn)單評(píng)估邏輯。因?yàn)槲业尿?yàn)證集中類別數(shù)量很少直接用自定義函數(shù)計(jì)算IoU和平均精確度就可以了。這個(gè)評(píng)估邏輯只影響最終評(píng)估指標(biāo)的計(jì)算不參與訓(xùn)練和推理完全可以替換。如果你不打算繞開它那就在安裝Visual C Build Tools之后重新打開終端再安裝。注意是重開終端不是直接在舊終端里繼續(xù)否則編譯器路徑可能沒有及時(shí)生效。5.3 圖片尺寸問題導(dǎo)致的內(nèi)存崩潰CPU環(huán)境下內(nèi)存相對(duì)充足但如果輸入圖片分辨率特別大比如超過2000×2000預(yù)處理階段放縮后特征圖依然很大加上RPN要為每個(gè)位置生成9個(gè)Anchor內(nèi)存占用會(huì)迅速攀升程序不會(huì)報(bào)錯(cuò)而是直接卡死或者被系統(tǒng)直接結(jié)束掉。排查辦法是在配置里明確設(shè)置最長(zhǎng)邊像素值測(cè)試階段不要使用超大尺寸圖片推理。另外demo代碼里有時(shí)會(huì)寫一個(gè)長(zhǎng)邊上限例如1000但我發(fā)現(xiàn)具體實(shí)現(xiàn)里存在一次預(yù)處理和一次后處理兩處尺寸控制只改一處是不夠的兩處要同時(shí)修改。5.4 訓(xùn)練不收斂的一個(gè)隱蔽元兇Anchor標(biāo)定異常有一次我訓(xùn)練了3000步RPN分類損失始終在0.7以上徘徊檢測(cè)頭分類損失也沒降多少??磁渲?、看學(xué)習(xí)率都沒問題后來逐條檢查roidb發(fā)現(xiàn)問題出在標(biāo)注框上。我的數(shù)據(jù)里有一小部分圖片的標(biāo)注框因?yàn)闃?biāo)注工具精度問題寬度或高度只有1像素。這樣的框和Anchor計(jì)算IoU時(shí)幾乎永遠(yuǎn)匹配不上正面標(biāo)簽導(dǎo)致模型學(xué)不到任何有效信息。它們是“沉默的壞數(shù)據(jù)”不會(huì)讓你報(bào)錯(cuò)只會(huì)悄悄拉低訓(xùn)練質(zhì)量。解決辦法是訓(xùn)練前加一個(gè)預(yù)處理步驟把所有面積過小的標(biāo)注框過濾掉或者直接把這些標(biāo)注同步縮放后再重新生成標(biāo)注文件。清洗數(shù)據(jù)之后loss曲線肉眼可見地下去了。5.5 顯存錯(cuò)誤在CPU上也會(huì)出現(xiàn)雖然你沒有GPU顯存但TensorFlow 1.x代碼里很多默認(rèn)配置包含per_process_gpu_memory_fraction之類與顯存相關(guān)的字段。如果這個(gè)字段的值配置不合法即使在沒有GPU的機(jī)器上也會(huì)拋出與顯存相關(guān)的異常。遇到這類錯(cuò)誤不要慌定位到config文件里和GPU顯存相關(guān)的設(shè)置把它改小或者改為自動(dòng)分配即可。另外所有device_count相關(guān)的配置也可以顯式設(shè)置為0明確告訴TensorFlow我們不使用GPU設(shè)備。6. 復(fù)現(xiàn)成功之后從跑通到能二次開發(fā)6.1 換骨干網(wǎng)絡(luò)從VGG16到自定義網(wǎng)絡(luò)跑通VGG16版本后你可以把這套工程擴(kuò)展到更輕量的骨干網(wǎng)絡(luò)比如MobileNet或自己設(shè)計(jì)的小型卷積網(wǎng)絡(luò)。修改時(shí)有三處必須同步改骨干網(wǎng)絡(luò)的構(gòu)建函數(shù)、RPN輸入特征圖的通道數(shù)、ROI Pooling后全連接層的輸入維度。很多人改第一個(gè)地方忘了后兩處導(dǎo)致維度不匹配的報(bào)錯(cuò)。我的建議是先不動(dòng)完整工程而是在lib/networks下新建一個(gè)網(wǎng)絡(luò)文件復(fù)制原來的訓(xùn)練網(wǎng)絡(luò)類只替換卷積池化部分然后讓RPN部分直接復(fù)用原邏輯。這樣改動(dòng)范圍可控出了問題也容易回退。6.2 封裝一個(gè)批量推斷腳本tools里的demo.py一次只能處理單張圖片后續(xù)做驗(yàn)證時(shí)會(huì)覺得很不方便。我后來寫了一個(gè)批量鏡像腳本讀入一個(gè)文件夾下所有圖片依次調(diào)用detect接口把每個(gè)目標(biāo)的類別、置信度、坐標(biāo)寫進(jìn)CSV文件。這個(gè)腳本不涉及網(wǎng)絡(luò)內(nèi)部結(jié)構(gòu)只調(diào)用現(xiàn)成的檢測(cè)函數(shù)因此不依賴具體網(wǎng)絡(luò)實(shí)現(xiàn)以后換模型腳本依然能復(fù)用。批量推斷時(shí)要注意圖片讀取方式保持一致盡量都用cv2的BGR通道讀取預(yù)測(cè)之后再把結(jié)果輸出出來避免RGB和BGR通道順序顛倒造成的檢測(cè)效果驟降。6.3 把模型導(dǎo)出成推理格式TensorFlow 1.x訓(xùn)練好的ckpt文件只能在配套的Python環(huán)境下使用。如果你想把它部署到服務(wù)端或嵌入式平臺(tái)需要額外做格式的轉(zhuǎn)換與整理。這個(gè)步驟在我復(fù)現(xiàn)時(shí)沒有做但對(duì)有部署需求的人值得提前了解把模型里的權(quán)重和計(jì)算圖固化到一個(gè)自包含文件里部署時(shí)就不需要重新依賴訓(xùn)練代碼環(huán)境。實(shí)際操作手法上先加載訓(xùn)練好的Session然后把需要輸出的張量節(jié)點(diǎn)類別、坐標(biāo)、候選框標(biāo)記為輸出節(jié)點(diǎn)最后調(diào)用凍結(jié)工具導(dǎo)出即可。凍結(jié)過程比想象中簡(jiǎn)單難點(diǎn)在于搞清楚哪些節(jié)點(diǎn)是真正的輸出否則導(dǎo)出的模型可能在服務(wù)端GraphDef里缺了關(guān)鍵分支?;氐轿业膹?fù)現(xiàn)過程我最想強(qiáng)調(diào)的是用CPU跑Faster R-CNN圖的不是速度而是把整條管線徹底讀懂。每一步都慢反而每一步都有充足時(shí)間確認(rèn)Solidity。等你把網(wǎng)絡(luò)結(jié)構(gòu)、數(shù)據(jù)流、Anchor機(jī)制、損失函數(shù)這四個(gè)點(diǎn)都吃透再用GPU跑別的模型會(huì)明顯感覺學(xué)習(xí)曲線平滑很多。如果你也打算在本機(jī)復(fù)現(xiàn)一次我的最后一條實(shí)操建議是不要一開始就追求跑滿7萬步。先配好環(huán)境、跑通數(shù)據(jù)、調(diào)整完參數(shù)、用demo腳本跑出一張帶框的檢測(cè)圖完成這一步再?zèng)Q定要不要熬長(zhǎng)訓(xùn)。前置流程全部驗(yàn)證無誤之后長(zhǎng)訓(xùn)只是時(shí)間問題不會(huì)再有方向性問題。