mmdetection最小復(fù)刻版(四):獨家yolo轉(zhuǎn)化內(nèi)幕——#mmdetection學習
本文轉(zhuǎn)載自
https://www.zybuluo.com/huanghaian/note/1744915
github: https://github.com/hhaAndroid/mmdetection-mini
歡迎star
部分內(nèi)容有刪改
1 darknet系列轉(zhuǎn)化過程
由于yolov2比較老了,性能和速度都無法和yolov3、tiny-yolov3相比,故這里不包括yolov2,具體是yolov3/yolov4以及tiny-yolo3/tiny-yolov4,一共4個模型。
剛開始我對darknet不熟悉,只是看cfg而已,darknet源碼是完全沒看過(當然現(xiàn)在也沒有看過)。如果你想找pytorch版本的yolo系列,github當然也是不計其數(shù)。搜索yolov3,帶有pytorch的star最多的是eriklindernoren/PyTorch-YOLOv3庫,但打開model.py,如下所示:
很明顯,他也是直接解析yolov3.cfg來構(gòu)建模型的。而前面說過我比較不喜歡這個做法,所以這種方式我直接就放棄了。
然后我找到了騰訊優(yōu)圖的目標檢測庫,在當時他就已經(jīng)實現(xiàn)了yolov2和yolov3系列了,他的模型構(gòu)建就是標準的pytorch寫法,通俗易懂,所以我一直沿用了這種做法。
通過netron可視化網(wǎng)絡(luò)結(jié)構(gòu),可以非常容易看出網(wǎng)絡(luò)結(jié)構(gòu)以及檢查自己寫的代碼是否正確,起到了極大的幫助。
我首先把ObjectDetection-OneStageDet代碼看了一遍并且跑了下,基本上了解了細節(jié)。考慮到y(tǒng)olov3模型太大了,不好搞,所以先拿tiny-yolov3開刀,一旦整個流程通了,那么新增yolov3那還不是手到擒來。
1.1 第一步:構(gòu)建模型
構(gòu)建tiny-yolov3模型,那實在是太簡單了,因為結(jié)構(gòu)非常簡單,為了快速實現(xiàn),我們可以模仿騰訊優(yōu)圖做法,如下所示,地址為:
https://github.com/Tencent/ObjectDetection-OneStageDet/blob/master/vedanet/network/backbone/_tiny_yolov3.py:
我開始也是按照這個寫法寫的,對應(yīng)框架代碼里面的mmdet/models/backbones/rr_tiny_yolov3_backbone.py。tiny-yolov3是兩個輸出尺度,而不是三個,下采樣率為32和16。為了檢查代碼是否正確,我還使用了darknet的cfg文件進行可視化。
顯示backbone已經(jīng)寫好了,下面就是構(gòu)建head了。head部分也非常簡單,就是對stride=32的分支采用上采樣層然后和stride=16的特征層進行concat操作即可:
1.2 第二步:權(quán)重轉(zhuǎn)化
構(gòu)建模型是非常容易的,可以直接抄騰訊優(yōu)圖的代碼,但是權(quán)重轉(zhuǎn)化就沒那么容易了,需要自己搞定。要將tiny-yolov3權(quán)重轉(zhuǎn)化到mmdetection-mini中,需要做以下幾件事情:
(1) 熟悉darknet權(quán)重保存格式.weights
(2) 權(quán)重轉(zhuǎn)化為pytorch結(jié)構(gòu)
對于darknet保存的weigts的格式解析,在網(wǎng)上有很多,我梳理了下大概就理解了。對應(yīng)代碼在mmdet/models/utils/brick.py里面的WeightLoader類中。整個權(quán)重文件其實是一個numpy矩陣,前幾個字節(jié)是一些版本以及head信息,后面開始才是權(quán)重,其格式為:
如果僅僅是卷積層,那么存儲格式是先bias,然后采用卷積權(quán)重參數(shù)
如果是conv+bn+激活,那么存儲格式是先bn的bias,weights,然后采用卷積的bias,weights
明白這兩個就可以了,解析時候必須要按照這個格式來讀取,否則后果非常嚴重,后面會細說。并且必須以conv+bn+激活的組合格式來解析,因為darkent的最小配置單位就是這個,如果你在pytorch構(gòu)建模型時候,把conv和bn分開寫,那么解析難度會增加非常多,注意看我的yolo系列的最小模型單位是Conv2dBatchLeaky或者Conv2,目的就是為了解析方便。
理解了上面的流程,就大概知道咋弄了,但是為了轉(zhuǎn)換方便以及轉(zhuǎn)換本身就是僅僅運行一次即可,不需要每次跑訓練都轉(zhuǎn)換一次,故我把tiny-yolov3在tools/darknet/tiny_yolov3.py里面copy了一遍,模型是一樣的,主要目的是:任何人先用這個腳本轉(zhuǎn)換一下模型,保存為mmdetection-mini能夠直接讀取的格式,然后在訓練和測試時候就直接用轉(zhuǎn)換后的模型即可,而不再需要讀取darknet原始weights權(quán)重。
tools/darknet/tiny_yolov3.py里面采用了WeightLoader類來加載darknet權(quán)重:
注意必須實現(xiàn)__modules_recurse方法,該方法的作用是遍歷復(fù)雜模型的每個子模塊,一定要遍歷到最小單位即conv+bn+激活或者conv,因為WeightLoader類的輸入必須是這兩個,否則無法解析。
下載darknet的tiny-yolov3權(quán)重,替換tiny_yolov3.py的權(quán)重路徑,運行即可。
打印信息如下,跑到這里,你需要檢查兩個部分:
(1) 最后一行[8858734/8858734 weights],如果兩者不相等,說明你代碼寫錯了,因為權(quán)重居然沒有全部導(dǎo)入
(2) 檢查Layer skipped的層是否是沒有參數(shù)的層,如果跳過了帶參數(shù)層,那么你一定寫錯了
到目前為止,一切都搞定了,tiny-yolov3的所有權(quán)重全部導(dǎo)入了,美好的一比,如果你這樣認為,那你就打錯特錯了。
還有一個收尾的工作要做,這里保存的pth模型和mmdetection-mini里面的算法雖然一樣,但是模型的key是不一樣的,為了能夠在mmdetection-mini中直接跑,你還需要替換key,對于tiny-yolov3來說,稍微比較簡單,如下:
如果你想說我咋知道哪些key要替換?我的做法是:先去mmdetection-mini中把模型的所有key保存下來,然后和這里的key比對下就行了。工作雖然繁瑣一些,但是還是能接受的。
到這里為止,保存的pth就可以真的在mmdetection-mini中跑起來了。美好的一比,如果你這樣認為,那你又打錯特錯了。
1.3 第三步: 結(jié)果檢查
前面都是準備工作,模型也得到了,最核心的是驗證你轉(zhuǎn)化的模型進行推理時候效果對不對,可以從單張圖預(yù)測可視化和驗證集計算mAP兩個方面驗證。
先對單張圖進行推理,看下預(yù)測效果對不對吧。此時你可以發(fā)現(xiàn),基本上啥都預(yù)測不出來。這一步才是最要命的,搞了半天,各種確認,最后發(fā)現(xiàn)沒有效果?這個地方坑很多,我開始被坑了一天。出現(xiàn)這個原因的主要問題是還是對darknet框架不熟悉。下面我說下錯誤原因,有好幾個。
(1) 模型構(gòu)建順序不能錯
因為darknet里面的權(quán)重是沒有開始和結(jié)尾的numpy數(shù)組,你只能自己去切割數(shù)組,這就會帶來一個問題:假設(shè)原始模式是ConvBN_1+ConvBN_2,但是你在pytorch里面寫成了ConvBN_2+ConvBN_1,那么上面寫的整個過程都是不會報錯而且是正常的,但是實際上你寫錯了。
例如你可以在tools/darknet/tiny-yolov3.py里面把:
self.layer0 = nn.ModuleList([nn.Sequential(layer_dict) for layer_dict in layer0]) self.head0 = nn.ModuleList([nn.Sequential(layer_dict) for layer_dict in head0])
順序調(diào)換,寫成:
self.head0 = nn.ModuleList([nn.Sequential(layer_dict) for layer_dict in head0]) self.layer0 = nn.ModuleList([nn.Sequential(layer_dict) for layer_dict in layer0])
就是這么簡單,你再跑腳本,打印也是完全正確的,但是實際你錯的非常嚴重,因為權(quán)重切割錯了。
總的來說就是darkent里面的權(quán)重,保存順序完全按照cfg從上到下的順序,而不是網(wǎng)絡(luò)實際運行順序。如果你的解析順序不正確,那么即使網(wǎng)絡(luò)結(jié)構(gòu)寫對了,導(dǎo)入的權(quán)重也可能是錯誤的,因為不會報錯。
(2) head模型順序不能錯
要說明這個問題,tiny-yolov3說明不了問題,需要上大模型yolov3。yolov3是典型的darknet53+fpn+head的結(jié)構(gòu),一般我們在構(gòu)建模型時候,都是按照mmdetection-mini的做法來寫,例如head部分我們的寫法是:
這種寫法本身沒有問題,但是對于導(dǎo)入darknet權(quán)重來說就不行了,因為yolov3里面的配置是:
yolo層其實就是輸出層,他的三個輸出層其實不是放在最后,而是插在中間的,也就是說他的權(quán)重保存模式是backbone+卷積+輸出1+fpn融合+輸出2+fpn融合+輸出3,而是通常的backbone+fpn+輸出1+輸出2+輸出3。
這種結(jié)構(gòu)導(dǎo)致我們的pytorch模型構(gòu)建瞬間復(fù)雜很多,大家仔細看tools/darknet/yolov3.py里面的代碼拆分非常細,原因就是如此:
self.layers1 = nn.ModuleList([nn.Sequential(layer_dict) for layer_dict in layer_list1]) self.head1 = nn.ModuleList([nn.Sequential(layer_dict) for layer_dict in head_1]) self.layers2 = nn.ModuleList([nn.Sequential(layer_dict) for layer_dict in layer_list2]) self.head2 = nn.ModuleList([nn.Sequential(layer_dict) for layer_dict in head_2]) self.layers3 = nn.ModuleList([nn.Sequential(layer_dict) for layer_dict in layer_list3]) self.head3 = nn.ModuleList([nn.Sequential(layer_dict) for layer_dict in head_3])
順序是layers1+head1+layers2+head2+layers3+head3,任何一個順序都不能亂,否則都是錯誤的。
(3) concat一定要注意順序
即使前面的你都弄好了,你依然可能出錯,因為concat不管你是a+b,還是b+a,代碼都不會報錯,但是實際你錯了,這個要非常小心,一定要執(zhí)行檢查,不要concat順序反了。
我開始沒有想到這個問題,在測試tiny-yolov3時候,總是發(fā)現(xiàn)有一層的預(yù)測是正確的,但是另一個層是錯誤的,當時想了很久都搞不懂,最后才想到可能是concat的問題,最后調(diào)換下順序就好了。
這個問題出現(xiàn)的原因依然是沒有仔細研究cfg,參數(shù)如下:
[route] layers = -1, 8
這個就表示是將最后一層輸出和第8個輸出層結(jié)果進行concat,順序是[-1,8]。
(4) tiny-yolov3里面MaxPool2d寫錯了
這個問題也被坑了很久,注意是因為騰訊優(yōu)圖里面寫錯了(他現(xiàn)在也是錯誤的),我當時沒有想到會錯,所以一直沒有懷疑這個,查了大概1天才發(fā)現(xiàn),被坑的很慘。tiny-yolov3的cfg中maxpool有一個地方非常奇怪:
注意看這兩個maxpool參數(shù),第一個maxpool是標準寫法,但是第二個maxpool的stride=1,而不是2,騰訊優(yōu)圖里面寫的是:
('11_max', nn.MaxPool2d(3, 1, 1)),
這個明顯不是cfg里面的寫法。對于騰訊優(yōu)圖復(fù)現(xiàn)的來說,可能影響很小,因為他根本就沒有導(dǎo)入tiny-yolov3權(quán)重,所以改了下沒有問題,但是我就不能這么搞了。正確的寫法應(yīng)該是:
('10_zero_pad', nn.ZeroPad2d((0, 1, 0, 1))), ('11_max', nn.MaxPool2d(2, 1)),
先pad,然后在maxpool。當時寫錯情況下預(yù)測現(xiàn)象是預(yù)測的bbox會存在偏移即預(yù)測正確,但是bbox偏掉了。我開始一直懷疑是我bbox還原代碼寫錯了,因為解碼錯誤也很出現(xiàn)Bbox偏移,最后才發(fā)現(xiàn)是這個參數(shù),好慘啊!
這個坑,被坑了很久。
(5) bn和激活參數(shù)設(shè)置不一致
這個也要非常小心,一定要執(zhí)行看下bn和激活函數(shù)參數(shù)設(shè)置,否則影響很大。例如LeakyReLU的激活參數(shù)是0.1,而不是默認的0.01。你最好先檢查這個參數(shù)。
(6) 圖片輸入處理流程不一致
假設(shè)輸入是416x416,其測試流程是先進行pad為正方形,然后resize,但是需要注意其resize圖片是采用最近鄰,而框架中是線性插值。實際測試表明差距還是比較大,會導(dǎo)致一些框丟失,可能darknent訓練時候采用的是最近鄰。
而且,我原來寫的代碼一直都是bgr輸入,測試時候發(fā)現(xiàn)效果總是不太對,后來才發(fā)現(xiàn)darknet網(wǎng)絡(luò)輸入是rgb,且直接除以255進行歸一化。
注意:我在mmdetection-mini中測試依然用的是線性插值模式,沒有修改,而且數(shù)據(jù)前處理邏輯也沒有完全按照darknet來做,而且沿用mmdetection的處理邏輯,如果你是用權(quán)重來微調(diào),我認為影響很小的。
以上就是全部坑了,我踩過的坑全部記錄下了,希望對大家有點幫助。下面針對各個模型說明下一些細節(jié)。
2 yolov3特殊說明
按照前面的流程可以轉(zhuǎn)換darknet中的所有模型。對于yolov3而言,有些細節(jié)說明下:
2.1 __modules_recurse函數(shù)
前面說過darknet權(quán)重的最小單位是ConvBnAct,故需要通過自己遞歸模塊,直到符合條件為止。故對于自定義模塊例如HeadBody,你需要在Yolov3類開頭定義custom_layers,然后在__modules_recurse遞歸。如果子模塊里面還包括非最小模塊,則內(nèi)部模塊也要加入,否則依然無法導(dǎo)入:
custom_layers = (vn_layer.Stage, vn_layer.Stage.custom_layers, vn_layer.HeadBody, vn_layer.Transition, vn_layer.Head)
例如上面的Stage模塊,內(nèi)部還有定制模塊StageBlock,那么也要加入。
如果你不加入,那么運行時候會看到很多conv層都被跳過了。
并且由于yolov3的head寫法,導(dǎo)致在模型的key替換時候會稍微麻煩一些,這種問題只需要細心點就可以解決,不難。
對于我來說,只要搞定了tiny-yolov3和yolov3,那么其余模型都可以很快就轉(zhuǎn)換成功。
3 tiny-yolov4特殊說明
在tiny-yolov4中引入了一種新的配置:
[route] layers=-1 groups=2 group_id=1
groups=2表示將該特征圖平均分成兩組,group_id=1表示取第1組特征。實際上就是:
x0 = x[:, channel // 2:, ...]
其他地方就沒啥要注意的了,按照cfg可視化寫就行。
4 轉(zhuǎn)化性能說明
對應(yīng)的文檔在docs/model_zoo.md中,這里寫下詳情。
在給出指標前,有一個地方一定要理清楚,否則各種指標你會看起來很懵。
coco數(shù)據(jù)集劃分方法有兩種:coco2014和coco2017,在https://cocodataset.org/#download里面有說明,大概就是
MSCOCO2014數(shù)據(jù)集:
訓練集: 82783張,13.5GB, 驗證集:40504張,6.6GB,共計123287張
MSCOCO2017數(shù)據(jù)集:
訓練集:118287張,19.3GB,驗證集: 5000張,1814.7M,共計123287張
數(shù)據(jù)集應(yīng)該還是那些,但是劃分原則改變了。在coco2014中驗證集數(shù)據(jù)太多了,很多大佬做實驗時候其實不是這樣搞的,參考retinanet論文里面的說法:訓練集通常都是train2014+val2014-minival2014,也就是從val2014中隨機挑選5000張圖片構(gòu)成minival數(shù)據(jù)集,其余數(shù)據(jù)集全部當做訓練集。論文中貼的指標都是test_dev2014數(shù)據(jù),是本地生成json,然后發(fā)送給coco官方服務(wù)器得到mAP值,本地是沒有l(wèi)abel的。
基于大家通常的做法,coco官方在2017年開始也采用了這種切分方式,也就是原來是大家各種隨機切分val得到minival,但是現(xiàn)在官方統(tǒng)一了minival,讓大家數(shù)據(jù)完全一樣,對比更加公平。故在2017年開始,劃分就變成train2014+val2014-minival2014=train2017,minival2014=val2017,測試集應(yīng)該沒咋變。
也就是是說yolov3論文中的指標都是test_dev2014的結(jié)果,訓練是train2014+val2014-minival2014。一定要注意minival2014是各自隨機切分的,也就是minival2014雖然圖片數(shù)和val2017一樣,但是有可能val2017的數(shù)據(jù)有部分數(shù)據(jù)其實在train2014+val2014-minival2014中。
4.1 測試前置說明
(1) mmdetection中貼的指標都是在val2017上面測試的,而darknet中提供的指標都是test_dev2014上面的,不具有非常強的對比性,因為val2017可能出現(xiàn)在darknet的訓練集中,而且val數(shù)據(jù)集一般會比test_dev簡單一些
(2) mmdetection中數(shù)據(jù)處理流程還是沿用mmdetection的,而不是darknet一樣的處理流程,會有點點差距,而且閾值也不太一樣,但是對于最終結(jié)果不會差距很大或者說沒有關(guān)系(只要證明我的代碼沒有問題就行)。在后面的yolov5中我進行了深入分析,可以保證處理流程完全一致,這里就暫時不寫了。
4.2 yolov3指標
權(quán)重下載鏈接: https://github.com/AlexeyAB/darknet
對應(yīng)配置: https://raw.githubusercontent.com/AlexeyAB/darknet/master/cfg/yolov3.cfg
darknet(test_dev2014): 416x416 55.3% mAP@0.5 (31.0 mAP@0.5:0.95) - 66? FPS - 65.9 BFlops - 236 MB
darknet(val2017): 416x416 65.9 mAP@0.5
mmdetection(val2017): 416x416 66.8 mAP@0.5 (37.4 mAP@0.5:0.95) -248 MB
darknet(test_dev2014)是官方論文指標,而darknet(val2017)是我直接用darknet框架測試val2017數(shù)據(jù)得到的指標,而mmdetection(val2017)是mini框架轉(zhuǎn)換后進行測試的指標。可以發(fā)現(xiàn)指標會更高一些,原因應(yīng)該是后處理閾值不一樣。
這里就可以完全確定代碼肯定沒有問題了。
4.3 yolov4
權(quán)重下載鏈接: https://github.com/AlexeyAB/darknet
對應(yīng)配置: https://raw.githubusercontent.com/AlexeyAB/darknet/master/cfg/yolov4.cfg
darknet(test_dev2014): 416x416 62.8% mAP@0.5 (41.2% AP@0.5:0.95) - 55? FPS / 96(V) FPS - 60.1 BFlops 245 MB
darknet(test_dev2014): 608x608 65.7% mAP@0.5 (43.5% AP@0.5:0.95) - 55? FPS / 96(V) FPS - 60.1 BFlops 245 MB
mmdetection(val2017): 416x416 65.7% mAP@0.5 (41.7% AP@0.5:0.95) -257. MB
mmdetection(val2017): 608x608 72.9% mAP@0.5 (48.1% AP@0.5:0.95) -257.7 MB
注意: yolov4的anchor尺寸變了,我開始沒有發(fā)現(xiàn)導(dǎo)致測試mAP比較低,不同于yolov3,下載的權(quán)重是608x608訓練過的,測試用了兩種尺度而已
4.4 tiny-yolov3
權(quán)重下載鏈接: https://github.com/AlexeyAB/darknet
對應(yīng)配置: https://raw.githubusercontent.com/AlexeyAB/darknet/master/cfg/yolov3-tiny.cfg
darknet(test_dev2014): 416x416 33.1% mAP@0.5 - 345? FPS - 5.6 BFlops - 33.7 MB
mmdetection(val2017): 416x416 36.5% mAP@0.5 -35.4 MB
注意: yolov3-tiny.cfg中最后一個[yolo]節(jié)點,mask應(yīng)該是 1,2,3,而不是github里面的0,1,2
4.5 tiny-yolov4
權(quán)重下載鏈接: https://github.com/AlexeyAB/darknet
對應(yīng)配置: https://raw.githubusercontent.com/AlexeyAB/darknet/master/cfg/yolov4-tiny.cfg
darknet(test_dev2014): 416x416 40.2% mAP@0.5 - 371(1080Ti) FPS / 330(RTX2070) FPS - 6.9 BFlops - 23.1 MB
mmdetection(val2017): 416x416 37.9% mAP@0.5 (19.2 mAP@0.5:0.95) -24.3 MB
注意:更低的原因應(yīng)該是該配置里面有一個scale_x_y = 1.05參數(shù)不一樣,目前沒有利用到,我后面會解決這個問題。
由于我的測試集是val2017,而不是test_dev2014,所以我提供的mAP會高一些,這是非常正常的。
5 總結(jié)
我提供的代碼應(yīng)該沒有大問題,但是有些細節(jié)可能有點問題,我后面會慢慢完善。希望大家看完這篇文章能夠?qū)W會:
(1) darknet的cfg可視化方式;權(quán)重存儲方式
(2) 對于以后任何darknet模型,都可以僅僅用1個小時就轉(zhuǎn)換到mmdetection中進行微調(diào)訓練
(3) darknet訓練、測試方式和mmdetection中的區(qū)別
我后面會去研究darknet的具體實現(xiàn)過程,爭取理解的更加透徹。還有一些不完善的地方,歡迎提出改進意見。下一篇對yolov5轉(zhuǎn)換過程進行深度講解。
6 附加
其實后續(xù)我還想完成一件事情即模仿大部分pytorch復(fù)現(xiàn)yolo系列寫法,提供一個通用的pytorch模型,可以直接加載darknet權(quán)重,輸入也是cfg模式,內(nèi)部自動解析cfg來構(gòu)建模型。這樣就可以實現(xiàn)兩種目的:
(1) 如果想了解所有模型細節(jié),可以按照原來的流程,先轉(zhuǎn)化再導(dǎo)入pth權(quán)重
(2) 如果想快速將darknet模型應(yīng)用到mmdetection中,而不寫一行代碼,那么就可以用這個通用的pytorch模型了。
但是其需要做如下工作:
(1) 這個pytorch模型要能夠適應(yīng)所有cfg,也就是必須實時了解darknet里面有沒有新增一些騷操作,這邊也要實時兼容
(2) 對于微調(diào)訓練,也要支持不同輸出層的權(quán)重不導(dǎo)入
圖像處理 深度學習 神經(jīng)網(wǎng)絡(luò)
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔相應(yīng)法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內(nèi)刪除侵權(quán)內(nèi)容。