mmdetection最小復(fù)刻版(五):yolov5轉(zhuǎn)化內(nèi)幕
AI編輯:深度眸
0 摘要
? ?在前一篇文章mmdetection最小復(fù)刻版(四):獨(dú)家yolo轉(zhuǎn)化內(nèi)幕中,我們已經(jīng)詳細(xì)分析了darknet框架訓(xùn)練的模型如何轉(zhuǎn)化到mmdetection-mini中,這一篇文章講解最火的yolov5如何轉(zhuǎn)化到mmdetection-mini中。
? ?這個(gè)轉(zhuǎn)化就相對(duì)容易很多了,畢竟都是pytorch框架寫的,但是由于他的代碼比較亂,整個(gè)代碼組織結(jié)構(gòu)也比較亂,實(shí)在是不好用,所以我將其模型移植到mmdetection中。目前僅僅支持推理,后續(xù)會(huì)支持會(huì)模仿yolov5訓(xùn)練過程,支持到mmdetection-mini中。
? ? 通過本文你可以學(xué)會(huì):
????(1) yolov5整個(gè)結(jié)構(gòu)的構(gòu)建細(xì)節(jié)
????(2) yolov5的前向推理流程
????(3) 如何將yolov5模型遷移到mmdetection中
? ? 在閱讀本文前,我建議你閱讀進(jìn)擊的后浪yolov5深度可視化解析,該文對(duì)yolov5進(jìn)行深入分析,包括模型設(shè)計(jì)、loss設(shè)計(jì)原則和正樣本可視化等等非常詳細(xì),我相信你看完就一定能夠理解yolov5了,然后在結(jié)合本文將可以了解到y(tǒng)olov5的每個(gè)細(xì)節(jié)。
github:
https://github.com/hhaAndroid/mmdetection-mini
歡迎star和提供改進(jìn)意見
1 yolov5簡(jiǎn)要介紹
? ?整個(gè)yolov5可以簡(jiǎn)單概況為:通過應(yīng)用類似EfficientNet的channel和layer控制因子來靈活配置不同復(fù)雜度的模型,并且在正負(fù)樣本定義階段采用了跨鄰域網(wǎng)格的匹配策略,從而得到更多的正樣本anchor,加速收斂。
? yolov5的結(jié)構(gòu)設(shè)計(jì)是參考yolov4來的,也是包括backbone+pan+spp+yolov3 head。在深入淺出YOLOv5中有繪制的非常好看的結(jié)構(gòu)圖,我從里面copy出來,方便看:

? ? 一目了然,yolov5現(xiàn)在已經(jīng)發(fā)展到第3個(gè)版本了,其說明見鏈接:
https://github.com/ultralytics/yolov5/releases/tag/v3.0 。相比第2版本,主要是將大部分激活函數(shù)全部換成mobilenetv3里面的nn.Hardswish(),大概在coco數(shù)據(jù)上可以提高1個(gè)點(diǎn)的mAP,特別是yolov5s小模型,提升很大。其余沒有啥改變。不同大小模型通過depth_multiple和width_multiple兩個(gè)參數(shù)控制,width_multiple是用來控制全局的通道數(shù)的,depth_multiple是用來控制BottleneckCSP模塊的個(gè)數(shù)。
? ? yolov5的模型構(gòu)建仿照了darknet中采用的cfg模式,即通過配置文件來構(gòu)建網(wǎng)絡(luò),但是考慮到darknet中的cfg文件細(xì)粒度過高,對(duì)于重新構(gòu)建網(wǎng)絡(luò)來說是很累人的,可讀性比較差,本文作者借鑒了cfg思想,但是進(jìn)行了適當(dāng)改進(jìn)即不再細(xì)分到conv+bn+act層,而最細(xì)粒度是模塊,為后續(xù)模型構(gòu)建、結(jié)構(gòu)理解有很大好處,但是這種寫法缺點(diǎn)是不再能直接采用第三方工具例如netron進(jìn)行網(wǎng)絡(luò)模型可視化了。
? ? 如果你看了前一篇文章,熟悉了darknet里面的cfg組織格式,那么yolov5網(wǎng)絡(luò)構(gòu)建模式應(yīng)該很容易就理解了,這里就不說了。
2 yolov5轉(zhuǎn)化為mmdetection
2.1 mmdetection中構(gòu)建模型
? 首先yolov5中涉及到的幾個(gè)模塊都比較簡(jiǎn)單,基本上就是BottleneckCSP、Focus、SPP和卷積模塊,而且本身就是pytorch寫的,故我直接copy過來了。
? 在構(gòu)建具體模型時(shí)候,為了后面簡(jiǎn)單(待會(huì)會(huì)說為啥),我也是按照配置文件格式來構(gòu)建模型,例如yolov5骨架構(gòu)建如下:

通過append方式構(gòu)建,然后全部轉(zhuǎn)化為Sequential對(duì)象。
? ??按照規(guī)范的結(jié)構(gòu)拆分原則,此處應(yīng)該有neck模塊,用于存放pan+spp模塊,但是作者直接放置在head部分了,所以我也暫時(shí)按照他的寫法構(gòu)建,后面可能會(huì)更改。head部分的代碼構(gòu)建也是類似,如下所示:

就是這么簡(jiǎn)單就把模型構(gòu)造好了。
? ? 還有一個(gè)細(xì)節(jié):pytorch1.6內(nèi)部自帶了nn.Hardswish()和nn.Identity()算子,而pytorch1.3是沒有的,所以為了兼容,我重寫了這兩個(gè)類,效果是一樣的,但是可能效率不如原生的。
2.2 yolov5模型轉(zhuǎn)化
(1) 自動(dòng)下載權(quán)重
? ? 要轉(zhuǎn)化的前提應(yīng)該是下載權(quán)重,你可以自己去官方地址下載,當(dāng)然也可以去瀏覽器上下載,作者寫的attempt_download函數(shù)可以自動(dòng)下載權(quán)重。下載后你可以發(fā)現(xiàn)權(quán)重長這樣:

(2) 模型轉(zhuǎn)換為pytorch1.3可讀權(quán)重
? ? 作為對(duì)比,yolov3的后綴是pth,但是yolov5s是pt,這是因?yàn)閥olov5采用的pytorch版本是1.6,其采用全新的存儲(chǔ)方式,你如果采用pytorch1.3讀取是會(huì)報(bào)錯(cuò)的,必須也是pytorch1.6及其以上才行。
? ? 還有一個(gè)比較坑的,作者存儲(chǔ)的模型里面包括了模型對(duì)象,而不僅僅是狀態(tài)字典,即使你采用pytorch1.6讀取權(quán)重,但是一旦你讀取的代碼不是放在yolov5對(duì)應(yīng)的工程路徑下也是會(huì)報(bào)錯(cuò)的,內(nèi)部會(huì)報(bào)pickle對(duì)象無法Load的錯(cuò)誤。所以你只能把我寫的tools/darknet/convert_yolov5_weights_step1.py代碼放在yolov5路徑下運(yùn)行,為了后面mmdetection能采用pytorch1.3進(jìn)行讀取,需要采用:
torch.save(data, save_name, _use_new_zipfile_serialization=False)方式保存,這樣就可以向前兼容了。
? ??注意:yolov5訓(xùn)練好的pt文件里面存儲(chǔ)了大量有用信息,而不僅僅是權(quán)重,包括anchor等等信息。為啥要保存呢?因?yàn)?/span>yolov5代碼中有自動(dòng)計(jì)算anchor和參數(shù)搜索的操作,如果他不保存起來,那么程序停止后就沒有了,只保存狀態(tài)字典無法在前向時(shí)候使用。這是一個(gè)不錯(cuò)的方式,即使代碼修改了,參數(shù)也不會(huì)丟。
(3) 轉(zhuǎn)化權(quán)重
? ?前面說了模型為啥要采用append的模式構(gòu)建,是為了這一個(gè)步驟方便。因?yàn)閥olov5里面是按照順序解析配置,然后轉(zhuǎn)化為Sequential的,其狀態(tài)字典中各層參數(shù)名稱是按照0,1,2...這種方式存儲(chǔ)的。如果我不也這樣寫,那么我的權(quán)重轉(zhuǎn)化過程會(huì)比較累,這樣做可以節(jié)省一些工作量。如果他后續(xù)模型改了,我這邊改動(dòng)也不大。
? ?轉(zhuǎn)換腳本在tools/darknet/convert_yolov5_weights_step2.py中,其需要輸入前面轉(zhuǎn)換得到的pytorch1.3模型。并且需要注意key和anchor這些字段,我們是不要的,如下所示:

到這里為止就完成了所有模型方面的轉(zhuǎn)化,m/l/x模型也是一樣的流程。
2.3 mmdetection新增bbox解碼函數(shù)
? ?看過yolov5解析的朋友,應(yīng)該知道yolov5的編解碼方式和其余yolo系列不一樣,因?yàn)槠淇缇W(wǎng)格預(yù)測(cè)了,故新增了
mmdet/det_core/bbox/coder/yolov5_bbox_coder.py編解碼類,其解碼過程為:

注意中心點(diǎn)預(yù)測(cè)范圍變了,不是0-1,而是-0.5到1.5,wh預(yù)測(cè)也改變了,沒有exp操作,而僅僅是尺度縮放了而已。作為對(duì)比,yolov3是如下:

到這里就全部完成了,下面就是測(cè)試下代碼對(duì)不對(duì)了。
2.4 模型驗(yàn)證
? ? 第一次運(yùn)行就能成功也是奇怪了,也蠻心酸的,一個(gè)人慢慢檢查嘍。
(1) 中心點(diǎn)還原代碼沒寫對(duì)
? ? 在第一次寫中心點(diǎn)解碼時(shí)候?qū)懛ㄊ牵?/span>
x_center_pred = (pred_bboxes[..., 0]*2 - 0.5) * stride + x_centery_center_pred = (pred_bboxes[..., 1]*2 - 0.5) * stride + y_center
預(yù)測(cè)現(xiàn)象就是中心點(diǎn)預(yù)測(cè)完全不對(duì)勁,總感覺偏掉了。后面仔細(xì)思考,發(fā)現(xiàn)2不能乘到里面,而是外面。因?yàn)閙mdetection中yolo生成的anchor其實(shí)是有0.5的偏移的,而不是0的,此時(shí)預(yù)測(cè)的中心點(diǎn)是正確的,但是還是有錯(cuò)誤。
(2) 有一個(gè)anchor寫錯(cuò)了參數(shù)
? ?這個(gè)低級(jí)錯(cuò)誤花費(fèi)我一個(gè)下午才發(fā)現(xiàn)。前面說明yolov5權(quán)重里面會(huì)保存anchor的,我把a(bǔ)nchor打印了然后復(fù)制過來,我靠,居然沒有發(fā)現(xiàn)正好中間的一個(gè)anchor的w寫錯(cuò)了,我檢查了幾遍都沒有發(fā)現(xiàn),尷尬啊!
? ?我來說下如何找出的吧!當(dāng)其中一個(gè)anchor寫錯(cuò)的時(shí)候,現(xiàn)象是有些bbox預(yù)測(cè)是正確的,而有些是錯(cuò)誤的。我當(dāng)時(shí)首先就懷疑是不是我的bbox解碼過程寫錯(cuò)了,思考了很久都感覺沒有錯(cuò)誤。又看了一遍模型代碼也沒有問題,為了確定bbox解碼過程是否正確,我徹底拋棄了mmdetection里面的anchor,而是采用yolo系列中常規(guī)的解碼方式,類似v5中如下所示:

所以我重寫了一個(gè)yolov5_bbox_coder.py,仿照上述寫法來進(jìn)行解碼,結(jié)果發(fā)現(xiàn)改完了測(cè)試效果一模一樣,我真是瘋了,說明問題根本就不在解碼這部分。
? ?既然找不出問題,那就只能采用終結(jié)大招了。我把mmdetection-mini中的yolov5模型不包括解碼部分移植到y(tǒng)olov5工程中,然后把他的模型代碼替換為我自己的,類似于如下所示:

? ? 這樣就可以保存輸入、解碼過程完全一致。接下來我要做的就是選中一張圖片,分別運(yùn)行yolov5模型和我的模型,保存各層輸出tensor,然后比對(duì)數(shù)值是否完全相同,如果有哪一層不一樣,那就說明這一層代碼寫錯(cuò)了。
? ?結(jié)果發(fā)現(xiàn)居然所有層tensor完全相同,除了最后的bbox預(yù)測(cè)不一樣外,此時(shí)我就知道模型肯定沒有錯(cuò)誤,問題在最后的解碼層。然后仔細(xì)檢查發(fā)現(xiàn)不一樣的解碼輸出就是在某一層而已,其余層相同,那么所有問題肯定就是anchor了,然后我再看一眼才發(fā)現(xiàn):
(116, 90), (156, 198), (373, 326)寫成了:
(116, 90), (90, 198), (373, 326)使出了我的終結(jié)大招才解決問題,心累啊,如果當(dāng)時(shí)有個(gè)人幫我檢查下anchor,就沒有這個(gè)問題了。說句題外話:通過這些模型轉(zhuǎn)換過程,我總結(jié)學(xué)到的最多就是如何找出Bug,如何解決一個(gè)看起來很難解決的Bug,不管你是啥bug,我總有辦法解決你,雖然有些辦法有點(diǎn)笨。有好幾次我都快放棄了,然后突然又想到一種調(diào)試方法,然后接著干,最終就解決了。
(3) 其余細(xì)節(jié)
? ?BN的兩個(gè)參數(shù)不是默認(rèn)值,而是
self.bn = nn.BatchNorm2d(2 * c_, eps=0.001, momentum=0.01)雖然對(duì)推理沒有啥影響,但是還是需要知道。
(4) 圖片處理邏輯不一樣
? ? 到這里就可以測(cè)試了。以yolov5為例,下載608x608訓(xùn)練的權(quán)重,采用yolov5s測(cè)試val2017,配置參數(shù)如下:
yolov5參數(shù):conf_thres=0.001 iou_thres=0.65mmdetection:test_cfg = dict(nms_pre=1000,min_bbox_size=0,score_thr=0.05,conf_thr=0.001,nms=dict(type='nms', iou_thr=0.65),max_per_img=100)
結(jié)果如下:
orig yolov5s: [email protected] 56.2@mAP0.mmdetection:[email protected][email protected]
發(fā)現(xiàn)居然少了一個(gè)點(diǎn),這你可以忍?我首先猜測(cè)原因可能有:
????1. 我實(shí)現(xiàn)的nn.Hardswish()效果不一樣
????2. 圖片處理邏輯不一樣
? ? 首先我在yolov5中把官方的寫的hardswish替換,發(fā)現(xiàn)mAP一樣,說明不是這個(gè)問題。那可能就是第2個(gè)問題了,然后我去研究了下yolov5的前向處理邏輯。我選擇bus.jpg這張圖片進(jìn)行單張圖片測(cè)試來驗(yàn)證的。也就是利用這張圖片分別在mmdetection(image_demo.py)和yolov5(detect.py)中運(yùn)行一遍,保存預(yù)測(cè)結(jié)果,看下是否相同。由于前處理邏輯不一樣,所以雖然預(yù)測(cè)的框差不多,但是其實(shí)score值不一樣,這說明前處理邏輯確實(shí)不一樣。
? ? 在yolov5的detect.py中采用的是letterbox方式對(duì)圖片進(jìn)行處理,其邏輯為:
????1. 計(jì)算縮放比例,假設(shè)input_shape = (181, 110, 3),輸出shape=201,先計(jì)算縮放比例1.11和1.9,選擇小比例。這個(gè)是常規(guī)操作,保證縮放后最長邊不超過設(shè)定值
????2. 計(jì)算pad像素,前面resize后會(huì)變成(201,122,3),理論上應(yīng)該pad=(0,79),但是內(nèi)部采用最小pad原則,設(shè)置最多不能pad超過64像素,故對(duì)79采用取模操作,變成79%64=15,然后對(duì)15進(jìn)行/2,然后左右pad即可
? ? 和常說的letterbox操作稍微有點(diǎn)區(qū)別,一般的letterbox操作輸出都是通過pad操作變成正方形的。早期yolov5也是變成正方形進(jìn)行推理,后來提出了矩形推理方式也就是上面的做法,輸出是矩形,而不是正方形,在推理階段可以加快速度。最小pad原則的目的是加快推理時(shí)間,細(xì)節(jié)可以參考 https://github.com/ultralytics/yolov3/issues/232

? ? 第一行是常規(guī)的正方形padding,第二行是上面介紹的最小pad原則得到的矩形圖片。
? ? 然而在mmdetection中采用的是Resize函數(shù),其直接保持長寬比進(jìn)行resize,沒有pad操作,效果應(yīng)該說類似吧。注意letterbox和mmdetection中的Resize函數(shù)輸出都不一定是指定size,也就是說即使你指定608x608,計(jì)算完成后也不一樣的是608x608輸出。目前mmdetection中也集成了letterbox操作。
? ? 基于這個(gè)設(shè)定,我也對(duì)mmdetection的推理流程進(jìn)行修改,采用了letterbox模式,配置如下:

在采用demo/image_demo.py腳本進(jìn)行運(yùn)行,同樣的bus.jpg圖片,運(yùn)行結(jié)果可視化,可以發(fā)現(xiàn)和yolov5完全一樣了。說明推理時(shí)候確實(shí)如此,如下所示(左邊是yolov5結(jié)果,右邊是轉(zhuǎn)化后mmdetection-mini結(jié)果):

? ? 當(dāng)我滿懷歡喜,將這個(gè)改動(dòng)應(yīng)用于test(對(duì)應(yīng)mmdetecion中的test.py和yolov5中的test.py),重新測(cè)試mAP時(shí)候,發(fā)現(xiàn)居然沒有啥變化,說明其實(shí)LetterResize和Resize應(yīng)用于val2017沒啥區(qū)別。
? ? 然后我再次審視了配置文件,發(fā)現(xiàn)yolov5里面沒有score_thr這個(gè)參數(shù),在mmdetection中這個(gè)參數(shù)的作用是應(yīng)用conf_thr,然后應(yīng)用score_thr參數(shù)刪除預(yù)測(cè)對(duì)應(yīng)類別的score小于預(yù)測(cè)的bbox,最后才是nms操作。但是yolov5中沒有score_thr這個(gè)步驟,這會(huì)導(dǎo)致yolov5預(yù)測(cè)的框超級(jí)多,但是對(duì)mAP計(jì)算有利。我于是把這個(gè)參數(shù)值設(shè)置的超級(jí)小,相當(dāng)于沒有再次測(cè)試,如下所示:
test_cfg = dict(nms_pre=1000,min_bbox_size=0,score_thr=0.0000001,conf_thr=0.001,nms=dict(type='nms', iou_thr=0.6),max_per_img=300)
這個(gè)配置就是和yolov5里面完全相同了。mAP再次測(cè)試結(jié)果如下:
orig yolov5s: 37.0@mAP0.5...0.9 56.2@mAP0.mmdetection: 36.6@mAP0.5...0.9 56.6@mAP0.5
? ? 此時(shí)可以發(fā)現(xiàn)mAP就沒有差那么多了,但是還差了0.4個(gè)點(diǎn)?,F(xiàn)在的差距就又要說到letterresize函數(shù)了,因?yàn)?strong>我在單張圖片測(cè)試時(shí)候明顯預(yù)測(cè)值完全相同,理論上mAP肯定是完全相同,現(xiàn)在居然不一樣,說明哪里還是有不同?我檢查了下yolov3的測(cè)試邏輯和單張圖推理邏輯的區(qū)別,發(fā)現(xiàn)差別在于dataset。
? ? 后來檢查發(fā)現(xiàn):yolov5中l(wèi)etterresize雖然是用了,但是其輸入shape是自適應(yīng)的,其保證了訓(xùn)練和測(cè)試的數(shù)據(jù)處理邏輯一樣(除了mosaic邏輯外),也就是說yolov5測(cè)試模式下,每個(gè)batch內(nèi)部shape是一樣的,但是不同batch之間的shape是不一樣的,這會(huì)造成最終結(jié)果有差異。雖然他是指定的608x608進(jìn)行推理,但是其內(nèi)部還是相當(dāng)于有個(gè)基于當(dāng)前數(shù)據(jù)集進(jìn)行自適應(yīng)操作。而在detertor代碼里面,是直接調(diào)用letterresize,而輸入shape是指定的,所以才會(huì)出現(xiàn)在對(duì)某一張圖進(jìn)行demo測(cè)試時(shí)候,結(jié)果完全相同但是test代碼時(shí)候mAP不一致。
? ? 總結(jié)來說,yolov5采用dataloader進(jìn)行測(cè)試時(shí)候,實(shí)際上是有自適應(yīng)的,雖然你設(shè)置的是608x608的輸入,其流程是:
? ?
1. 遍歷所有驗(yàn)證集圖片的shape,保存起來
2. 開啟Rectangular模式,對(duì)所有shape按照h/w比例從小到大排序
3. 計(jì)算所有驗(yàn)證集,一共可以構(gòu)成多少個(gè)batch,然后對(duì)前面排序后的shape進(jìn)行連續(xù)截取操作,并且考慮h/w大于1和小于1的場(chǎng)景,因?yàn)閔/w不同,pad的方向也不同,保存每個(gè)batch內(nèi)部的shape比例都差不多
4. 將每個(gè)batch內(nèi)部的shape值轉(zhuǎn)化為指定的圖片大小比例,例如打算網(wǎng)絡(luò)預(yù)測(cè)最大不超過608,那么所有shape都要不大于608
5. 對(duì)batch內(nèi)部圖片進(jìn)行l(wèi)etterbox操作,測(cè)試或者訓(xùn)練時(shí)候,不開啟minimum rectangle操作,也就是輸出shape一定等于指定的shape。這樣可以保證每個(gè)batch內(nèi)部輸出的圖片shape完全相同? ?
而mmdetection中test時(shí)候?qū)崿F(xiàn)的邏輯是:
1. 將每張圖片LetterResize到640x640(輸出不一定是640x640)
2. 將圖片shape pad到32的整數(shù)倍,右下pad
3. 在collate函數(shù)中將一個(gè)batch內(nèi)部的圖片全部右下pad到當(dāng)前batch最大的w和h,變成相同shape
可以看出yolov5這種設(shè)置會(huì)更好一點(diǎn),應(yīng)該就是這個(gè)差異導(dǎo)致的mAP不一樣,后面我把這個(gè)策略應(yīng)用到mmdetection中。
3 總結(jié)
? ? 本文一步一步,從0開始講解如何將yolov5模型轉(zhuǎn)化到mmdetection中,其中對(duì)于我踩得每一個(gè)坑,我都詳細(xì)說明了,希望下次其他朋友碰到同樣問題可以快速跳過。
??
github:
https://github.com/hhaAndroid/mmdetection-mini
歡迎star和提供改進(jìn)意見
推薦閱讀
機(jī)器學(xué)習(xí)算法工程師
? ??? ? ? ? ? ? ? ? ? ? ? ??????????????????一個(gè)用心的公眾號(hào)
?

