自己做網(wǎng)站怎么優(yōu)化產(chǎn)品軟文是什么意思
精讀論文
前言
從這篇開始,我們將進入YOLO的學習。YOLO是目前比較流行的目標檢測算法,速度快且結(jié)構(gòu)簡單,其他的目標檢測算法如RCNN系列,以后有時間的話再介紹。
本文主要介紹的是YOLOV1,這是由以Joseph Redmon為首的大佬們于2015年提出的一種新的目標檢測算法。它與之前的目標檢測算法如R-CNN等不同之處在于,R-CNN等目標檢測算法是兩階段算法, 步驟為先在圖片上生成候選框,然后利用分類器對這些候選框進行逐一的判斷;而YOLOv1是一階段算法,是端到端的算法,它把目標檢測問題看作回歸問題,將圖片輸入單一的神經(jīng)網(wǎng)絡(luò),然后就輸出得到了圖片的物體邊界框,即boundingbox以及分類概率等信息。下面我們就開始學習吧。
下面是一些學習資料:
論文鏈接:[1506.02640] You Only Look Once: Unified, Real-Time Object Detection (arxiv.org)
項目地址 :YOLO: Real-Time Object Detection (pjreddie.com)
Github源碼地址:mirrors / alexeyab / darknet · GitCode
目錄
Abstract—摘要
一、Introduction—前言
二、Uni?ed Detection—統(tǒng)一檢測
2.1 Network Design—網(wǎng)絡(luò)設(shè)計
2.2 Training—訓練
2.3 Inference—推論
2.4 Limitations of YOLO—YOLO的局限性
三、Comparison to Other Detection Systems—與其他目標檢測算法的比較
四、Experiments—實驗
4.1 Comparison to Other RealTime Systems—與其他實時系統(tǒng)的比較
4.2 VOC 2007 Error Analysis—VOC 2007誤差分析
4.3 Combining Fast R-CNN and YOLO—Fast R-CNN與YOLO的結(jié)合
4.4 VOC 2012 Results—VOC 2012結(jié)果
4.5 Generalizability: Person Detection in Artwork—泛化性:圖像中的人物檢測
五、Real-Time Detection In The Wild—自然環(huán)境下的實時檢測
六、Conclusion—結(jié)論
Abstract—摘要
翻譯
我們提出的YOLO是一種新的目標檢測方法。以前的目標檢測方法通過重新利用分類器來執(zhí)行檢測。與先前的方案不同,我們將目標檢測看作回歸問題從空間上定位邊界框(bounding box)*并*預測該框的類別概率。我們使用單個神經(jīng)網(wǎng)絡(luò)**,在一次評估中直接從完整圖像上預測邊界框和類別概率。由于整個檢測流程僅用一個網(wǎng)絡(luò),所以可以直接對檢測性能進行端到端的優(yōu)化。
我們的統(tǒng)一架構(gòu)速度極快。我們的基本YOLO模型以45 fps(幀/秒)的速度實時處理圖像。該網(wǎng)絡(luò)的一個較小版本——Fast YOLO,以155 fps這樣驚人的速度運行,同時仍然達到其他實時檢測器的兩倍。與最先進的(state-of-the-art,SOTA)檢測系統(tǒng)相比,YOLO雖然產(chǎn)生了較多的定位誤差,但它幾乎不會發(fā)生把背景預測為目標這樣的假陽性(False Positive)的錯誤。最后,YOLO能學習到泛化性很強的目標表征。當從自然圖像學到的模型用于其它領(lǐng)域如藝術(shù)畫作時,它的表現(xiàn)都優(yōu)于包括DPM和R-CNN在內(nèi)的其它檢測方法。
精讀
之前的方法(RCNN系列)
(1)通過region proposal產(chǎn)生大量的可能包含待檢測物體的potential bounding box
(2)再用分類器去判斷每個bounding box里是否包含有物體,以及物體所屬類別的probability或者 confidence
(3)最后回歸預測
YOLO的簡介:
本文將檢測變?yōu)橐粋€regression problem(回歸問題),YOLO 從輸入的圖像,僅僅經(jīng)過一個神經(jīng)網(wǎng)絡(luò),直接得到一些bounding box以及每個bounding box所屬類別的概率。
因為整個的檢測過程僅僅有一個網(wǎng)絡(luò),所以它可以直接進行end-to-end的優(yōu)化。
end-to-end: 端到端,指的是輸入原始數(shù)據(jù),輸出的是最后結(jié)果,原來輸入端不是直接的原始數(shù)據(jù),而是在原始數(shù)據(jù)中提取的特征。通過縮減人工預處理和后續(xù)處理,盡可能使模型從原始輸入到最終輸出,給模型更多可以根據(jù)數(shù)據(jù)自動調(diào)節(jié)的空間,增加模型的整體契合度。在CV中具體表現(xiàn)是,神經(jīng)網(wǎng)絡(luò)的輸入為原始圖片,神經(jīng)網(wǎng)絡(luò)的輸出為(可以直接控制機器的)控制指令。
一、Introduction—前言
翻譯
人們只需瞄一眼圖像,立即知道圖像中的物體是什么,它們在哪里以及它們?nèi)绾蜗嗷プ饔?。人類的視覺系統(tǒng)是快速和準確的,使得我們在無意中就能夠執(zhí)行復雜的任務(wù),如駕駛??焖偾覝蚀_的目標檢測算法可以讓計算機在沒有專門傳感器的情況下駕駛汽車,使輔助設(shè)備能夠向人類用戶傳達實時的場景信息,并解鎖通用、響應(yīng)性的機器人系統(tǒng)的潛能。
目前的檢測系統(tǒng)通過重用分類器來執(zhí)行檢測。為了檢測目標,這些系統(tǒng)為該目標提供一個分類器,在測試圖像的不同的位置和不同的尺度上對其進行評估。像deformable parts models(DPM,可變形部分模型)這樣的系統(tǒng)使用滑動窗口方法,其分類器在整個圖像上均勻間隔的位置上運行[10]。
最近的方法,如R-CNN使用region proposal(區(qū)域候選)策略,首先在圖像中生成潛在的邊界框(bounding box),然后在這些框上運行分類器。在分類之后,執(zhí)行用于細化邊界框的后處理,消除重復的檢測,并根據(jù)場景中的其它目標為邊界框重新打分[13]。這些復雜的流程是很慢,很難優(yōu)化的,因為每個獨立的部分都必須單獨進行訓練。
**我們將目標檢測看作是一個單一的回歸問題,直接從圖像像素得到邊界框坐標和類別概率。**使用我們的系統(tǒng)——You Only Look Once(YOLO),便能得到圖像上的物體是什么和物體的具體位置。
YOLO非常簡單(見圖1),它僅用單個卷積網(wǎng)絡(luò)就能同時預測多個邊界框和它們的類別概率。YOLO在整個圖像上訓練,并能直接優(yōu)化檢測性能。與傳統(tǒng)的目標檢測方法相比,這種統(tǒng)一的模型下面所列的一些優(yōu)點
第一,YOLO速度非???/strong>。由于我們將檢測視為回歸問題,所以我們不需要復雜的流程。測試時,我們在一張新圖像上簡單的運行我們的神經(jīng)網(wǎng)絡(luò)來預測檢測結(jié)果。在Titan X GPU上不做批處理的情況下,YOLO的基礎(chǔ)版本以每秒45幀的速度運行,而快速版本運行速度超過150fps。這意味著我們可以在不到25毫秒的延遲內(nèi)實時處理流媒體視頻。此外,YOLO實現(xiàn)了其它實時系統(tǒng)兩倍以上的平均精度。關(guān)于我們的系統(tǒng)在網(wǎng)絡(luò)攝像頭上實時運行的演示,請參閱我們的項目網(wǎng)頁:YOLO: Real-Time Object Detection。
第二,YOLO是在整個圖像上進行推斷的。與基于滑動窗口和候選框的技術(shù)不同,YOLO在訓練期間和測試時都會顧及到整個圖像,所以它隱式地包含了關(guān)于類的上下文信息以及它們的外觀。Fast R-CNN是一種很好的檢測方法[14],但由于它看不到更大的上下文,會將背景塊誤檢為目標。與Fast R-CNN相比,YOLO的背景誤檢數(shù)量少了一半。
第三,YOLO能學習到****目標的泛化表征(generalizable representations of objects)。把在自然圖像上進行訓練的模型,用在藝術(shù)圖像進行測試時,YOLO大幅優(yōu)于DPM和R-CNN等頂級的檢測方法。由于YOLO具有高度泛化能力,因此在應(yīng)用于新領(lǐng)域或碰到意外的輸入時不太可能出故障。
YOLO在精度上仍然落后于目前最先進的檢測系統(tǒng)。雖然它可以快速識別圖像中的目標,但它在定位某些物體尤其是小的物體上精度不高。
我們在實驗中會進一步探討精度/時間的權(quán)衡。我們所有的訓練和測試代碼都是開源的,而且各種預訓練模型也都可以下載。
精讀
之前的研究:
DPM: 系統(tǒng)為檢測對象使用分類器,并在測試圖像的不同位置和尺度對其進行評估
**R-CNN:**SS方法提取候選框+CNN+分類+回歸。
YOLO處理步驟:
(1)將輸入圖像的大小調(diào)整為448×448,分割得到7*7網(wǎng)格;
(2)通過CNN提取特征和預測;
(3)利用非極大值抑制(NMS)進行篩選
YOLO的定義:
YOLO將目標檢測重新定義為單個回歸問題,從圖像像素直接到邊界框坐標和類概率。YOLO可以在一個圖像來預測:哪些對象是存在的?它們在哪里?
如 Figure 1:將圖像輸入單獨的一個 CNN 網(wǎng)絡(luò),就會預測出 bounding box,以及這些 bounding box 所屬類別的概率。
YOLO 用一整幅圖像來訓練,同時可以直接優(yōu)化性能檢測。
性能檢測對比:
YOLO的優(yōu)點:
(1)**YOLO的速度非??臁?*能夠達到實時的要求。在 Titan X 的 GPU 上 能夠達到 45 幀每秒。
(2)**YOLO在做預測時使用的是全局圖像。**與FastR-CNN相比,YOLO產(chǎn)生的背景錯誤數(shù)量不到一半。
(3)**YOLO 學到物體更泛化的特征表示。**因此當應(yīng)用于新域或意外輸入時,不太可能崩潰。
二、Uni?ed Detection—統(tǒng)一檢測
網(wǎng)格單元
翻譯
我們將目標檢測的獨立部分(the separate components )整合到單個神經(jīng)網(wǎng)絡(luò)中。我們的網(wǎng)絡(luò)使用整個圖像的特征來預測每個邊界框。它還可以同時預測一張圖像中的所有類別的所有邊界框。這意味著我們的網(wǎng)絡(luò)對整張圖像和圖像中的所有目標進行全局推理(reason globally)。YOLO設(shè)計可實現(xiàn)端到端訓練和實時的速度,同時保持較高的平均精度。
我們的系統(tǒng)將輸入圖像分成 S×S 的網(wǎng)格。如果目標的中心落入某個網(wǎng)格單元(grid cell)中,那么該網(wǎng)格單元就負責檢測該目標。
每個網(wǎng)格單元都會預測 B個 邊界框和這些框的置信度分數(shù)(confidence scores)。這些置信度分數(shù)反映了該模型對那個框內(nèi)是否包含目標的置信度,以及它對自己的預測的準確度的估量。在形式上,我們將置信度定義為 confidence=Pr(Object)?IOUpred truth 。如果該單元格中不存在目標(即Pr(Object)=0),則置信度分數(shù)應(yīng)為 0 。否則(即Pr(Object)=1),我們希望置信度分數(shù)等于預測框(predict box)與真實標簽框(ground truth)之間聯(lián)合部分的交集(IOU)。
每個網(wǎng)格單元還預測了C類的條件概率,Pr(Classi|Object)。這些概率是以包含目標的網(wǎng)格單元為條件的。我們只預測每個網(wǎng)格單元的一組類別概率,而不考慮框B的數(shù)量。
在測試時,我們將條件類概率和單個框的置信度預測相乘:[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-411RQXtW-1700230508536)(https://latex.csdn.net/eq?Pr%28Class_%7Bi%7D%7CObject%29Pr%28Object%29IOU%5E%7Btruth%7D_%7Bpred%7D%3DPr%28Class_%7Bi%7D%29*IOU%5E%7Btruth%7D_%7Bpred%7D)]
這給我們提供了每個框的特定類別的置信度分數(shù)。這些分數(shù)既是對該類出現(xiàn)在框里的概率的編碼,也是對預測的框與目標的匹配程度的編碼。
精讀
思想
YOLO將目標檢測問題作為回歸問題。會將輸入圖像分成S×S的網(wǎng)格,如果一個物體的中心點落入到一個cell中,那么該cell就要負責預測該物體,一個格子只能預測一個物體,會生成兩個預測框。
對于每個grid cell:
(1)預測B個邊界框,每個框都有一個置信度分數(shù)(confidence score)這些框大小尺寸等等都隨便,只有一個要求,就是生成框的中心點必須在grid cell里。
(2)每個邊界框包含5個元素:(x,y,w,h)
● x,y: 是指bounding box的預測框的中心坐標相較于該bounding box歸屬的grid cell左上角的偏移量,在0-1之間。
在上圖中,綠色虛線框代表grid cell,綠點表示該grid cell的左上角坐標,為(0,0);紅色和藍色框代表該grid cell包含的兩個bounding box,紅點和藍點表示這兩個bounding box的中心坐標。有一點很重要,bounding box的中心坐標一定在該grid cell內(nèi)部,因此,紅點和藍點的坐標可以歸一化在0-1之間。在上圖中,紅點的坐標為(0.5,0.5),即x=y=0.5,藍點的坐標為(0.9,0.9),即x=y=0.9。
● w,h: 是指該bounding box的寬和高,但也歸一化到了0-1之間,表示相較于原始圖像的寬和高(即448個像素)。比如該bounding box預測的框?qū)捠?4.8個像素,高也是44.8個像素,則w=0.1,h=0.1。
紅框的x=0.8,y=0.5,w=0.1,h=0.2。
(3)不管框 B 的數(shù)量是多少,只負責預測一個目標。
(4)預測 C 個條件概率類別(物體屬于每一種類別的可能性)
**綜上,S×S 個網(wǎng)格,每個網(wǎng)格要預測 B個bounding box (中間上圖),還要預測 C 個類(中間下圖)。**將兩圖合并,網(wǎng)絡(luò)輸出就是一個 S × S × (5×B+C)。(S x S個網(wǎng)格,每個網(wǎng)格都有B個預測框,每個框又有5個參數(shù),再加上每個網(wǎng)格都有C個預測類)
Q1:為什么每個網(wǎng)格有固定的B個bounding box?(即B=2)
在訓練的時候會在線地計算每個predictor預測的bounding box和ground truth的IOU,計算出來的IOU大的那個predictor,就會負責預測這個物體,另外一個則不預測。這么做有什么好處?我的理解是,這樣做的話,實際上有兩個predictor來一起進行預測,然后網(wǎng)絡(luò)會在線選擇預測得好的那個predictor(也就是IOU大)來進行預測。
Q2:每個網(wǎng)格預測的兩個bounding box是怎么得到的?
YOLO中兩個bounding box是人為選定的(2個不同 長寬比)的box,在訓練開始時作為超參數(shù)輸入bounding box的信息,隨著訓練次數(shù)增加,loss降低,bounding box越來越準確。Faster RCNN也是人為選定的(9個 不同長寬比和scale),YOLOv2是統(tǒng)計分析ground true box的特點得到的(5個)。
預測特征組成
最終的預測特征由邊框的位置、邊框的置信度得分以及類別概率組成,這三者的含義如下:
- 邊框位置: 對每一個邊框需要預測其中心坐標及寬、高這4個量, 兩個邊框共計8個預測值邊界框?qū)挾葁和高度h用圖像寬度和高度歸一化。因此 x,y,w,h 都在0和1之間。
- 置信度得分(box confidence score) c : 框包含一個目標的可能性以及邊界框的準確程度。類似于Faster RCNN 中是前景還是背景。由于有兩個邊框,因此會存在兩個置信度預測值。
- 類別概率: 由于PASCAL VOC數(shù)據(jù)集一共有20個物體類別,因此這里預測的是邊框?qū)儆谀囊粋€類別。
注意
-
一個cell預測的兩個邊界框共用一個類別預測, 在訓練時會選取與標簽IoU更大的一個邊框負責回歸該真實物體框,在測試時會選取置信度更高的一個邊框,另一個會被舍棄,因此7×7=49個gird cell最多只能預測49個物體。
-
因為每一個 grid cell只能有一個分類,也就是他只能預測一個物體,這也是導致YOLO對小目標物體性能比較差的原因。
如果所給圖片極其密集,導致 grid cell里可能有多個物體,但是YOLO模型只能預測出來一個,那這樣就會忽略在本grid cell內(nèi)的其他物體。
2.1 Network Design—網(wǎng)絡(luò)設(shè)計
翻譯
我們將此模型作為卷積神經(jīng)網(wǎng)絡(luò)來實現(xiàn),并在Pascal VOC檢測數(shù)據(jù)集[9]上進行評估。網(wǎng)絡(luò)的初始卷積層從圖像中提取特征,而全連接層負責預測輸出概率和坐標。
我們的網(wǎng)絡(luò)架構(gòu)受圖像分類模型GoogLeNet的啟發(fā)[34]。我們的網(wǎng)絡(luò)有24個卷積層,后面是2個全連接層。我們只使用1×1降維層,后面是3×3卷積層,這與Lin等人[22]類似,而不是GoogLeNet使用的Inception模塊。
我們還訓練了快速版本的YOLO,旨在推動快速目標檢測的界限??焖資OLO使用具有較少卷積層(9層而不是24層)的神經(jīng)網(wǎng)絡(luò),在這些層中使用較少的卷積核。除了網(wǎng)絡(luò)規(guī)模之外,基本版YOLO和快速YOLO的所有訓練和測試參數(shù)都是相同的。
我們網(wǎng)絡(luò)的最終輸出是7×7×30的預測張量。
精讀
網(wǎng)絡(luò)結(jié)構(gòu)
YOLO網(wǎng)絡(luò)結(jié)構(gòu)借鑒了 GoogLeNet (經(jīng)典神經(jīng)網(wǎng)絡(luò)論文超詳細解讀(三)——GoogLeNet InceptionV1學習筆記(翻譯+精讀+代碼復現(xiàn)))。輸入圖像的尺寸為448×448,經(jīng)過24個卷積層,2個全連接的層(FC),最后在reshape操作,輸出的特征圖大小為7×7×30。
Q:7×7×30怎么來的?
張量剖面圖
(圖片來源:YOLO v1詳細解讀_yolov1詳解_迪菲赫爾曼的博客-CSDN博客)
- 7×7: 一共劃分成7×7的網(wǎng)格。
- 30: 30包含了兩個預測框的參數(shù)和Pascal VOC的類別參數(shù):每個預測框有5個參數(shù):x,y,w,h,confidence。另外,Pascal VOC里面還有20個類別;所以最后的30實際上是由5x2+20組成的,也就是說這一個30維的向量就是一個gird cell的信息。
- 7×7×30: 總共是7 × 7個gird cell一共就是7 × 7 ×(2 × 5+ 20)= 7 × 7 × 30 tensor = 1470 outputs,正好對應(yīng)論文。
網(wǎng)絡(luò)詳解
(1)YOLO主要是建立一個CNN網(wǎng)絡(luò)生成預測7×7×1024 的張量 。
(2)然后使用兩個全連接層執(zhí)行線性回歸,以進行7×7×2 邊界框預測。將具有高置信度得分(大于0.25)的結(jié)果作為最終預測。
(3)在3×3的卷積后通常會接一個通道數(shù)更低1×1的卷積,這種方式既降低了計算量,同時也提升了模型的非線性能力。
(4)除了最后一層使用了線性激活函數(shù)外,其余層的激活函數(shù)為 Leaky ReLU 。
(5)在訓練中使用了 Dropout 與數(shù)據(jù)增強的方法來防止過擬合。
(6)對于最后一個卷積層,它輸出一個形狀為 (7, 7, 1024) 的張量。 然后張量展開。使用2個全連接層作為一種線性回歸的形式,它輸出1470個參數(shù),然后reshape為 (7, 7, 30) 。
2.2 Training—訓練
翻譯
我們在ImageNet的1000類競賽數(shù)據(jù)集[30]上預訓練我們的卷積層。對于預訓練,我們使用圖3中的前20個卷積層,接著是平均池化層和全連接層。我們對這個網(wǎng)絡(luò)進行了大約一周的訓練,并且在ImageNet 2012驗證集上獲得了單一裁剪圖像88%的top-5準確率,與Caffe模型池中的GoogLeNet模型相當。我們使用Darknet框架進行所有的訓練和推斷[26]。
然后我們轉(zhuǎn)換模型來執(zhí)行檢測訓練。Ren等人表明,預訓練網(wǎng)絡(luò)中增加卷積層和連接層可以提高性能[29]。按照他們的方法,我們添加了四個卷積層和兩個全連接層,這些層的權(quán)重都用隨機值初始化。檢測通常需要細粒度的視覺信息,因此我們將網(wǎng)絡(luò)的輸入分辨率從224×224改為448×448。
模型的最后一層預測類概率和邊界框坐標。我們通過圖像寬度和高度來規(guī)范邊界框的寬度和高度,使它們落在0和1之間。我們將邊界框x和y坐標參數(shù)化為特定網(wǎng)格單元位置的偏移量,所以它們的值被限定在在0和1之間。
模型的最后一層使用線性激活函數(shù),而所有其它的層使用下面的Leaky-ReLU:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-2xEDccXI-1700230508539)(https://latex.csdn.net/eq?%5Cphi%20%28x%29%3D%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%20x%2C%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20if%20%5C%3A%20x%3E0%20%26%20%26%20%5C%5C0.1x%2C%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20otherwise%26%20%26%20%5Cend%7Bmatrix%7D%5Cright.)][外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-B4veVrdH-1700230508540)(https://latex.csdn.net/eq?%5Cphi%20%28x%29%3D%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%20x%2C%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20if%20%5C%3A%20x%3E0%20%26%20%26%20%5C%5C0.1x%2C%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20otherwise%26%20%26%20%5Cend%7Bmatrix%7D%5Cright.)]
我們對模型輸出的平方和誤差(sum-squared error)進行優(yōu)化。我們選擇使用平方和誤差,是因為它易于優(yōu)化,但是它并不完全符合最大化平均精度(average precision)的目標。它給分類誤差與定位誤差的權(quán)重是一樣的,這點可能并不理想。另外,每個圖像都有很多網(wǎng)格單元并沒有包含任何目標,這將這些單元格的“置信度”分數(shù)推向零,通常壓制了包含目標的單元格的梯度。這可能導致模型不穩(wěn)定,從而導致訓練在早期就發(fā)散(diverge)。
為了彌補平方和誤差的缺陷,我們增加了邊界框坐標預測的損失,并減少了不包含目標的框的置信度預測的損失。 我們使用兩個參數(shù)λ c o o r d λ_{coord}λcoord和λ n o o b j λ_{noobj}λnoobj來實現(xiàn)這一點。 我們設(shè)定λ c o o r d = 5 λ_{coord}= 5λcoord=5 和 λ n o o b j = 0.5 λ_{noobj}=0.5λnoobj=0.5。
平方和誤差對大框和小框的誤差權(quán)衡是一樣的,而我們的錯誤指標(error metric)應(yīng)該要體現(xiàn)出,大框的小偏差的重要性不如小框的小偏差的重要性。為了部分解決這個問題,我們直接預測邊界框?qū)挾群透叨鹊?strong>平方根,而不是寬度和高度。
YOLO為每個網(wǎng)格單元預測多個邊界框。在訓練時,每個目標我們只需要一個邊界框預測器來負責。若某預測器的預測值與目標的實際值的IOU值最高,則這個預測器被指定為“負責”預測該目標。這導致邊界框預測器的專業(yè)化。每個預測器可以更好地預測特定大小,方向角,或目標的類別,從而改善整體召回率(recall)。
在訓練期間,我們優(yōu)化以下多部分損失函數(shù):
注意,如果目標存在于該網(wǎng)格單元中(前面討論的條件類別概率),則損失函數(shù)僅懲罰(penalizes)分類錯誤。如果預測器“負責”實際邊界框(即該網(wǎng)格單元中具有最高IOU的預測器),則它也僅懲罰邊界框坐標錯誤。
我們用Pascal VOC 2007和2012的訓練集和驗證數(shù)據(jù)集進行了大約 135個epoch 的網(wǎng)絡(luò)訓練。因為我們僅在Pascal VOC 2012上進行測試,所以我們的訓練集里包含了Pascal VOC 2007的測試數(shù)據(jù)。在整個訓練過程中,我們使用:batch size=64,momentum=0.9,decay=0.0005
。
我們的學習率(learning rate
)計劃如下:在第一個epoch中,我們將學習率從1 0 ? 3 10^{-3}10?3慢慢地提高到 1 0 ? 2 10^{-2}10?2。如果從大的學習率開始訓練,我們的模型通常會由于不穩(wěn)定的梯度而發(fā)散(diverge)。我們繼續(xù)以 1 0 ? 2 10^{-2}10?2 進行75個周期的訓練,然后以 1 0 ? 3 10^{-3}10?3 進行30個周期的訓練,最后以 1 0 ? 4 10^{-4}10?4 進行30個周期的訓練。
為避免過擬合,我們使用了Dropout和大量的數(shù)據(jù)增強。 在第一個連接層之后的dropout層的丟棄率設(shè)置為0.5,以防止層之間的相互適應(yīng)[18]。 對于數(shù)據(jù)增強(data augmentation),我們引入高達20%的原始圖像大小的隨機縮放和平移(random scaling and translations )。我們還在 HSV 色彩空間中以高達 1.5 的因子隨機調(diào)整圖像的曝光度和飽和度。
精讀
預訓練分類網(wǎng)絡(luò)
在 ImageNet 1000數(shù)據(jù)集上預訓練一個分類網(wǎng)絡(luò),這個網(wǎng)絡(luò)使用Figure3中的前20個卷積層,然后是一個平均池化層和一個全連接層。(此時網(wǎng)絡(luò)輸入是224×224)。
Q:主干結(jié)構(gòu)的輸入要求必須是448x448的固定尺寸,為什么在預訓練階段可以輸入224x224的圖像呢?
主要原因是加入了平均池化層,這樣不論輸入尺寸是多少,在和最后的全連接層連接時都可以保證相同的神經(jīng)元數(shù)目。
訓練檢測網(wǎng)絡(luò)
經(jīng)過上一步的預訓練,就已經(jīng)把主干網(wǎng)絡(luò)的前20個卷積層給訓練好了,前20層的參數(shù)已經(jīng)學到了圖片的特征。接下來的步驟本質(zhì)就是遷移學習,在訓練好的前20層卷積層后加上4層卷積層和2層全連接層,然后在目標檢測的任務(wù)上進行遷移學習。
在整個網(wǎng)絡(luò)(24+2)的訓練過程中,除最后一層采用ReLU函數(shù)外,其他層均采用leaky ReLU激活函數(shù)。leaky ReLU相對于ReLU函數(shù)可以解決在輸入為負值時的零梯度問題。YOLOv1中采用的leaky ReLU函數(shù)的表達式為:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-6j8khv2N-1700230508541)(https://latex.csdn.net/eq?%5Cphi%20%28x%29%3D%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%20x%2C%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20if%20%5C%3A%20x%3E0%20%26%20%26%20%5C%5C0.1x%2C%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20otherwise%26%20%26%20%5Cend%7Bmatrix%7D%5Cright.)]
NMS非極大值抑制
**概念:**NMS算法主要解決的是一個目標被多次檢測的問題,意義主要在于在一個區(qū)域里交疊的很多框選一個最優(yōu)的。
YOLO中具體操作
(1)對于上述的98列數(shù)據(jù),先看某一個類別,也就是只看98列的這一行所有數(shù)據(jù),先拿出最大值概率的那個框,剩下的每一個都與它做比較,如果兩者的IoU大于某個閾值,則認為這倆框重復識別了同一個物體,就將其中低概率的重置成0。
(2)最大的那個框和其他的框比完之后,再從剩下的框找最大的,繼續(xù)和其他的比,依次類推對所有類別進行操作。 注意,這里不能直接選擇最大的,因為有可能圖中有多個該類別的物體,所以IoU如果小于某個閾值,則會被保留。
(3)最后得到一個稀疏矩陣,因為里面有很多地方都被重置成0,拿出來不是0的地方拿出來概率和類別,就得到最后的目標檢測結(jié)果了。
注意: NMS只發(fā)生在預測階段,訓練階段是不能用NMS的,因為在訓練階段不管這個框是否用于預測物體的,他都和損失函數(shù)相關(guān),不能隨便重置成0。
損失函數(shù)
損失函數(shù)包括:
localization loss -> 坐標損失
confidence loss -> 置信度損失
classification loss -> 分類損失
損失函數(shù)詳解:
(1)坐標損失
- 第一行: 負責檢測物體的框中心點(x, y)定位誤差。
- 第二行: 負責檢測物體的框的高寬(w,h)定位誤差,這個根號的作用就是為了修正對大小框一視同仁的缺點,削弱大框的誤差。
Q:為啥加根號?
在上圖中,大框和小框的bounding box和ground truth都是差了一點,但對于實際預測來講,大框(大目標)差的這一點也許沒啥事兒,而小框(小目標)差的這一點可能就會導致bounding box的方框和目標差了很遠。而如果還是使用第一項那樣直接算平方和誤差,就相當于把大框和小框一視同仁了,這樣顯然不合理。而如果使用開根號處理,就會一定程度上改善這一問題 。
這樣一來,同樣是差一點,小框產(chǎn)生的誤差會更大,即對小框懲罰的更嚴重。
(2)置信度損失
- 第一行: 負責檢測物體的那個框的置信度誤差。
- 第二行: 不負責檢測物體的那個框的置信度誤差。
(3)分類損失
負責檢測物體的grid cell分類的誤差。
特殊符號的含義:
2.3 Inference—推論
翻譯
就像在訓練中一樣,預測測試圖像的檢測只需要一次網(wǎng)絡(luò)評估。在Pascal VOC上,每張圖像上網(wǎng)絡(luò)預測 98 個邊界框和每個框的類別概率。YOLO在測試時非???#xff0c;因為它只需要一次網(wǎng)絡(luò)評估(network evaluation),這與基于分類器的方法不同。
網(wǎng)格設(shè)計強化了邊界框預測中的空間多樣性。通常一個目標落在哪一個網(wǎng)格單元中是很明顯的,而網(wǎng)絡(luò)只能為每個目標預測一個邊界框。然而,一些大的目標或接近多個網(wǎng)格單元的邊界的目標能被多個網(wǎng)格單元定位。非極大值抑制(Non-maximal suppression,NMS)可以用來修正這些多重檢測。非最大抑制對于YOLO的性能的影響不像對于R-CNN或DPM那樣重要,但也能增加2?3%的mAP。
精讀
(1)預測測試圖像的檢測只需要一個網(wǎng)絡(luò)評估。
(2)測試時間快
(3)當圖像中的物體較大,或者處于 grid cells 邊界的物體,可能在多個 cells 中被定位出來。
(4)利用NMS去除重復檢測的物體,使mAP提高,但和RCNN等相比不算大。
2.4 Limitations of YOLO—YOLO的局限性
翻譯
由于每個格網(wǎng)單元只能預測兩個框,并且只能有一個類,因此YOLO對邊界框預測施加了很強的空間約束。這個空間約束限制了我們的模型可以預測的鄰近目標的數(shù)量。我們的模型難以預測群組中出現(xiàn)的小物體(比如鳥群)。
由于我們的模型學習是從數(shù)據(jù)中預測邊界框,因此它很難泛化到新的、不常見的長寬比或配置的目標。我們的模型也使用相對較粗糙的特征來預測邊界框,因為輸入圖像在我們的架構(gòu)中歷經(jīng)了多個下采樣層(downsampling layers)。
最后,我們的訓練基于一個逼近檢測性能的損失函數(shù),這個損失函數(shù)無差別地處理小邊界框與大邊界框的誤差。大邊界框的小誤差通常是無關(guān)要緊的,但小邊界框的小誤差對IOU的影響要大得多。我們的主要錯誤來自于不正確的定位。
精讀
(1)對于圖片中一些群體性小目標檢測效果比較差。因為yolov1網(wǎng)絡(luò)到后面感受野較大,小目標的特征無法再后面7×7的grid中體現(xiàn),針對這一點,yolov2已作了一定的修改,加入前層(感受野較小)的特征進行融合。
(2)原始圖片只劃分為7x7的網(wǎng)格,當兩個物體靠的很近時(挨在一起且中點都落在同一個格子上的情況),效果比較差。因為yolov1的模型決定了一個grid只能預測出一個物體,所以就會丟失目標,針對這一點,yolov2引入了anchor的概念,一個grid有多少個anchor理論上就可以預測多少個目標。
(3)每個網(wǎng)格只對應(yīng)兩個bounding box,當物體的長寬比不常見(也就是訓練數(shù)據(jù)集覆蓋不到時),效果較差。
(4)最終每個網(wǎng)格只對應(yīng)一個類別,容易出現(xiàn)漏檢(物體沒有被識別到)。
三、Comparison to Other Detection Systems—與其他目標檢測算法的比較
翻譯
目標檢測是計算機視覺中的核心問題。檢測流程通常是首先從輸入圖像上提取一組魯棒特征(Haar [25],SIFT [23],HOG [4],卷積特征[6])。然后,分類器[36,21,13,10]或定位器[1,32]被用來識別特征空間中的目標。這些分類器或定位器或在整個圖像上或在圖像中的一些子區(qū)域上以滑動窗口的方式運行[35,15,39]。我們將YOLO檢測系統(tǒng)與幾種頂級檢測框架進行比較,突出了關(guān)鍵的相似性和差異性。
Deformable parts models??勺冃尾糠帜P?#xff08;DPM)使用滑動窗口方法進行目標檢測[10]。DPM使用不相交的流程來提取靜態(tài)特征,對區(qū)域進行分類,預測高評分區(qū)域的邊界框等。我們的系統(tǒng)用單個卷積神經(jīng)網(wǎng)絡(luò)替換所有這些不同的部分。網(wǎng)絡(luò)同時進行特征提取,邊界框預測,非極大值抑制和上下文推理。網(wǎng)絡(luò)的特征feature是在**在線(in-line)**訓練出來的而不是靜態(tài),因此可以根據(jù)特定的檢測任務(wù)進行優(yōu)化。我們的統(tǒng)一架構(gòu)比DPM更快,更準確。
R-CNN。R-CNN及其變體(variants)使用區(qū)域候選而不是滑動窗口來查找圖像中的目標。選擇性搜索[35]生成潛在的邊界框(Selective Search generates potential bounding boxes),卷積網(wǎng)絡(luò)提取特征,SVM對框進行評分,線性模型調(diào)整邊界框,非最大抑制消除重復檢測(eliminates duplicate detections)。 這個復雜流水線的每個階段都必須獨立地進行精確調(diào)整(precisely tuned independently),所得到的系統(tǒng)非常緩慢,在測試時間每個圖像需要超過40秒[14]。
YOLO與R-CNN有一些相似之處。每個網(wǎng)格單元提出潛在的邊界框并使用卷積特征對這些框進行評分。然而,我們的系統(tǒng)對網(wǎng)格單元的候選框施加空間限制,這有助于緩解對同一目標的多次檢測的問題。 我們的系統(tǒng)還生成了更少的邊界框,每張圖像只有98個,而選擇性搜索則有約2000個。最后,我們的系統(tǒng)將這些單獨的組件(individual components)組合成一個單一的、共同優(yōu)化的模型。
其它快速檢測器。 Fast R-CNN 和 Faster R-CNN 通過共享計算和使用神經(jīng)網(wǎng)絡(luò)替代選擇性搜索[14],[28]來提出候選區(qū)域來加速 R-CNN 框架。雖然它們提供了比 R-CNN 更快的速度和更高的準確度,但仍然不能達到實時性能。
許多研究工作集中在加快DPM流程上[31] [38] [5]。它們加速HOG計算,使用級聯(lián)(cascades),并將計算推動到(多個)GPU上。但是,實際上只有30Hz的DPM [31]可以實時運行。
YOLO并沒有試圖優(yōu)化大型檢測流程的單個組件,相反,而是完全拋棄(throws out…entirely)了大型檢測流程,并通過設(shè)計來提高速度。
像人臉或行人等單個類別的檢測器可以高度優(yōu)化,因為他們只需處理較少的多樣性[37]。YOLO是一種通用的檢測器,它可以同時(simultaneously)檢測多個目標。
Deep MultiBox。與R-CNN不同,Szegedy等人 訓練一個卷積神經(jīng)網(wǎng)絡(luò)來預測感興趣的區(qū)域(regions of interest,ROI)[8],而不是使用選擇性搜索。 MultiBox還可以通過用單個類別預測替換置信度預測來執(zhí)行單個目標檢測。 但是,MultiBox無法執(zhí)行一般的目標檢測,并且仍然只是較大檢測流水線中的一部分,需要進一步的圖像補丁分類。 YOLO和MultiBox都使用卷積網(wǎng)絡(luò)來預測圖像中的邊界框,但YOLO是一個完整的檢測系統(tǒng)。
OverFeat。Sermanet等人訓練了一個卷積神經(jīng)網(wǎng)絡(luò)來執(zhí)行定位,并使該定位器進行檢測[32]。OverFeat高效地執(zhí)行滑動窗口檢測,但它仍然是一個不相交的系統(tǒng)(disjoint system)。OverFeat優(yōu)化了定位功能,而不是檢測性能。像DPM一樣,定位器在進行預測時只能看到局部信息。OverFeat無法推斷全局上下文,因此需要大量的后處理來產(chǎn)生連貫的檢測。
MultiGrasp。我們的系統(tǒng)在設(shè)計上類似于Redmon等[27]的抓取檢測。 我們的網(wǎng)格邊界框預測方法基于MultiGrasp系統(tǒng)進行回歸分析。 然而,抓取檢測比物體檢測要簡單得多。 MultiGrasp只需要為包含一個目標的圖像預測一個可抓取區(qū)域。 它不必估計目標的大小,位置或邊界或預測它的類別,只需找到適合抓取的區(qū)域就可以了。 而YOLO則是預測圖像中多個類的多個目標的邊界框和類概率。
精讀
DPM
用傳統(tǒng)的HOG特征方法,也用的是傳統(tǒng)的支持向量機SVM分類器,然后人工造一個模板,再用滑動窗口方法不斷的暴力搜索整個待識別圖,去套那個模板。這個方法比較大的問題就是在于設(shè)計模板,計算量巨大,而且是個靜態(tài)的,沒辦法匹配很多變化的東西,魯棒性差。
R-CNN
- 第一階段:每個圖片使用選擇性搜索SS方法提取2000個候選框。
- 第二階段:將每個候選框送入CNN網(wǎng)絡(luò)進行分類(使用的SVM)。
YOLO對比他們倆都很強,YOLO和R-CNN也有相似的地方,比如也是提取候選框,YOLO的候選框就是上面說過的那98個 bounding boxes,也是用到了NMS非極大值抑制,也用到了CNN提取特征。
Other Fast Detectors
Fast和Faster R-CNN :這倆模型都是基于R-CNN的改版,速度和精度都提升了很多,但是也沒辦法做到實時監(jiān)測,也就是說FPS到不了30,作者在這里并沒有談準確度的問題,實際上YOLO的準確度在這里是不占優(yōu)勢的,甚至于比他們低。
Deep MultiBox
訓練卷積神經(jīng)網(wǎng)絡(luò)來預測感興趣區(qū)域,而不是使用選擇性搜索。多盒也可以用單個類預測替換置信預測來執(zhí)行單個目標檢測。YOLO和MultiBox都使用卷積網(wǎng)絡(luò)來預測圖像中的邊界框,但YOLO是一個完整的檢測系統(tǒng)。
OverFeat
OverFeat有效地執(zhí)行滑動窗口檢測,優(yōu)化了定位,而不是檢測性能。與DPM一樣,定位器在進行預測時只看到本地信息。OverFeat不能推理全局環(huán)境。
MultiGrasp
YOLO在設(shè)計上與Redmon等人的抓取檢測工作相似。邊界盒預測的網(wǎng)格方法是基于多重抓取系統(tǒng)的回歸到抓取。
總之,作者就是給前人的工作都數(shù)落一遍,凸顯自己模型的厲害(學到了!)
四、Experiments—實驗
4.1 Comparison to Other RealTime Systems—與其他實時系統(tǒng)的比較
翻譯
目標檢測方面的許多研究工作都集中在使標準的檢測流程更快[5],[38],[31],[14],[17],[28]。然而,只有Sadeghi等人實際上產(chǎn)生了一個實時運行的檢測系統(tǒng)(每秒30幀或更好)[31]。我們將YOLO與DPM的GPU實現(xiàn)進行了比較,其在30Hz或100Hz下運行。雖然其它的算法沒有達到實時性的標準,我們也比較了它們的mAP和速度的關(guān)系,從而探討目標檢測系統(tǒng)中精度和性能之間的權(quán)衡。
Fast YOLO是PASCAL上最快的目標檢測方法;據(jù)我們所知,它是現(xiàn)有的最快的目標檢測器。具有52.7%的mAP,實時檢測的精度是以前的方法的兩倍以上。普通版YOLO將mAP推到63.4%的同時保持了實時性能。
我們還使用VGG-16訓練YOLO。 這個模型比普通版YOLO更精確,但也更慢。 它的作用是與依賴于VGG-16的其他檢測系統(tǒng)進行比較,但由于它比實時更慢,所以本文的其他部分將重點放在我們更快的模型上。
最快的DPM可以在不犧牲太多mAP的情況下有效加速DPM,但仍然會將實時性能降低2倍[38]。與神經(jīng)網(wǎng)絡(luò)方法相比,DPM的檢測精度相對較低,這也是限制它的原因。
減去R的R-CNN用靜態(tài)侯選邊界框取代選擇性搜索[20]。雖然速度比R-CNN更快,但它仍然無法實時,并且由于該方法無法找到好的邊界框,準確性受到了嚴重影響。
Fast R-CNN加快了R-CNN的分類階段,但它仍然依賴于選擇性搜索,每個圖像需要大約2秒才能生成邊界候選框。因此,它雖然具有較高的mAP,但的速度是0.5 fps,仍然遠未達到實時。
最近的Faster R-CNN用神經(jīng)網(wǎng)絡(luò)替代了選擇性搜索來候選邊界框,類似于Szegedy等人[8]的方法。在我們的測試中,他們最準確的模型達到了 7fps,而較小的、不太準確的模型以18 fps運行。 Faster R-CNN的VGG-16版本比YOLO高出10mAP,但比YOLO慢了6倍。 Zeiler-Fergus 版本的Faster R-CNN只比YOLO慢2.5倍,但也不如YOLO準確。
精讀
Table 1 在Pascal VOC 2007 上與其他檢測方法的對比
**結(jié)論:**實時目標檢測(FPS>30),YOLO最準,Fast YOLO最快。
4.2 VOC 2007 Error Analysis—VOC 2007誤差分析
翻譯
為了進一步研究YOLO和最先進的檢測器之間的差異,我們詳細分析了VOC 2007的分類(breakdown)結(jié)果。我們將YOLO與Fast R-CNN進行比較,因為Fast R-CNN是PASCAL上性能最高的檢測器之一并且它的檢測代碼是可公開得到的。
我們使用Hoiem等人的方法和工具[19],對于測試的每個類別,我們查看該類別的前N個預測。每個預測都或是正確的,或是根據(jù)錯誤的類型進行分類:
- Correct: correct class and IOU>0.5
- Localization: correct class, 0.1<IOU<0.5
- Similar: class is similar, IOU>0.1
- Other: class is wrong, IOU>0.1
- Background: IOU<0.1 for any object(所有目標的IOU都<0.1)
YOLO難以正確地定位目標,因此定位錯誤比YOLO的所有其他錯誤總和都要多。Fast R-CNN定位錯誤更少,但把背景誤認成目標的錯誤比較多。它的最高檢測結(jié)果中有13.6%是不包含任何目標的誤報(false positive,背景)。 Fast R-CNN把背景誤認成目標的概率比YOLO高出3倍。
精讀
本文使用HoeMm等人的方法和工具。對于測試時間的每個類別,查看該類別的N個預測。每個預測要么是正確的,要么是基于錯誤類型進行分類的:
參數(shù)含義:
- **Correct:**正確分類,且預測框與ground truth的IOU大于0.5,既預測對了類別,預測框的位置和大小也很合適。
- **Localization:**正確分類,但預測框與ground truth的IOU大于0.1小于0.5,即雖然預測對了類別,但預測框的位置不是那么的嚴絲合縫,不過也可以接受。
- Similar: 預測了相近的類別,且預測框與ground truth的IOU大于0.1。即預測的類別雖不正確但相近,預測框的位置還可以接受。
- **Other:**預測類別錯誤,預測框與ground truth的IOU大于0.1。即預測的類別不正確,但預測框還勉強把目標給框住了。
- **Background:**預測框與ground truth的IOU小于0.1,即該預測框的位置為背景,沒有目標。
Figure 4 顯示了所有20個類中每種錯誤類型的平均細分情況
**結(jié)論:**YOLO定位錯誤率高于Fast R-CNN;Fast R-CNN背景預測錯誤率高于YOLO
4.3 Combining Fast R-CNN and YOLO—Fast R-CNN與YOLO的結(jié)合
翻譯
YOLO誤認背景為目標的情況比Fast R-CNN少得多。 通過使用YOLO消除Fast R-CNN的背景檢測,我們獲得了顯著的性能提升。 對于R-CNN預測的每個邊界框,我們檢查YOLO是否預測了一個相似的框。 如果確實如此,那么我們會根據(jù)YOLO預測的概率和兩個框之間的重疊情況提高預測值。
最好的Fast R-CNN模型在VOC 2007測試集中達到了 71.8% 的mAP。 當與YOLO合并時,其mAP增加了 3.2% 至 75.0%。 我們還嘗試將頂級Fast R-CNN模型與其他幾個版本的Fast R-CNN結(jié)合起來。 這寫的結(jié)合的平均增長率在 0.3% 至 0.6% 之間。
結(jié)合YOLO后獲得的性能提高不僅僅是模型集成的副產(chǎn)品,因為結(jié)合不同版本的Fast R-CNN幾乎沒有什么益處。 相反,正是因為YOLO在測試時出現(xiàn)了各種各樣的錯誤,所以它在提高Fast R-CNN的性能方面非常有效。
不幸的是,這種組合不會從YOLO的速度中受益,因為我們分別運行每個模型,然后合并結(jié)果。 但是,由于YOLO速度如此之快,與Fast R-CNN相比,它不會增加任何顯著的計算時間。
精讀
Table2 模型組合在VOC 2007上的實驗結(jié)果對比
**結(jié)論:**因為YOLO在測試時犯了各種錯誤,所以它在提高快速R-CNN的性能方面非常有效。但是這種組合并不受益于YOLO的速度,由于YOLO很快,和Fast R-CNN相比,它不增加任何有意義的計算時間。
4.4 VOC 2012 Results—VOC 2012結(jié)果
翻譯
在VOC 2012測試集中,YOLO的mAp得分是57.9%。這比現(xiàn)有最先進的技術(shù)水平低,更接近使用VGG-16的原始的R-CNN,見表3。與其最接近的競爭對手相比,我們的系統(tǒng)很難處理小物體上(struggles with small objects)。在瓶子、羊、電視/監(jiān)視器等類別上,YOLO得分比R-CNN和Feature Edit低8-10%。然而,在其他類別,如貓和火車YOLO取得了更好的表現(xiàn)。
我們的Fast R-CNN + YOLO模型組合是性能最高的檢測方法之一。 Fast R-CNN與YOLO的組合提高了2.3%,在公共排行榜上提升了5個位置。
精讀
Table 3 在VOC2012上mAP排序
**結(jié)論:**Fast R-CNN從與YOLO的組合中得到2.3%的改進,在公共排行榜上提升了5個百分點。
4.5 Generalizability: Person Detection in Artwork—泛化性:圖像中的人物檢測
翻譯
用于目標檢測的學術(shù)數(shù)據(jù)集的訓練和測試數(shù)據(jù)是服從同一分布的。但在現(xiàn)實世界的應(yīng)用中,很難預測所有可能的用例,他的測試數(shù)據(jù)可能與系統(tǒng)已經(jīng)看到的不同[3]。我們將YOLO與其他檢測系統(tǒng)在畢加索(Picasso)數(shù)據(jù)集[12]和人物藝術(shù)(People-Art)數(shù)據(jù)集[3]上進行了比較,這兩個數(shù)據(jù)集用于測試藝術(shù)品上的人物檢測。
作為參考(for reference),我們提供了VOC 2007的人形檢測的AP,其中所有模型僅在VOC 2007數(shù)據(jù)上訓練。在Picasso數(shù)據(jù)集上測試的模型在是在VOC 2012上訓練,而People-Art數(shù)據(jù)集上的模型則在VOC 2010上訓練。
R-CNN在VOC 2007上有很高的AP值。然而,當應(yīng)用于藝術(shù)圖像時,R-CNN顯著下降。R-CNN使用選擇性搜索來調(diào)整自然圖像的候選邊界框。R-CNN在分類器階段只能看到小區(qū)域,而且需要有很好的候選框。
DPM在應(yīng)用于藝術(shù)圖像時可以很好地保持其AP。之前的研究認為DPM表現(xiàn)良好,因為它具有強大的物體形狀和布局空間模型。雖然DPM不會像R-CNN那樣退化,但它的AP本來就很低。
YOLO在VOC 2007上表現(xiàn)出色,其應(yīng)用于藝術(shù)圖像時其AP降低程度低于其他方法。與DPM一樣,YOLO模擬目標的大小和形狀,以及目標之間的關(guān)系和目標通常出現(xiàn)的位置之間的關(guān)系。藝術(shù)圖像和自然圖像在像素級別上有很大不同,但它們在物體的大小和形狀方面相似,因此YOLO仍然可以預測好的邊界框和檢測結(jié)果。
精讀
Figure 5 通用性(Picasso 數(shù)據(jù)集和 People-Art數(shù)據(jù)集)
**結(jié)論:**YOLO都具有很好的檢測結(jié)果
五、Real-Time Detection In The Wild—自然環(huán)境下的實時檢測
翻譯
YOLO是一款快速,精確的物體檢測器,非常適合計算機視覺應(yīng)用。 我們將YOLO連接到網(wǎng)絡(luò)攝像頭,并驗證它是否保持實時性能,包括從攝像頭獲取圖像并顯示檢測結(jié)果的時間。
由此產(chǎn)生的系統(tǒng)是互動的和參與的。 雖然YOLO單獨處理圖像,但當連接到網(wǎng)絡(luò)攝像頭時,它的功能類似于跟蹤系統(tǒng),可在目標移動并在外觀上發(fā)生變化時檢測目標。 系統(tǒng)演示和源代碼可在我們的項目網(wǎng)站上找到:YOLO: Real-Time Object Detection。
精讀
**結(jié)論:**將YOLO連接到一個網(wǎng)絡(luò)攝像頭上,并驗證它是否保持了實時性能,包括從攝像頭中獲取圖像和顯示檢測結(jié)果的時間。結(jié)果證明效果很好,如上圖所示,除了第二行第二個將人誤判為飛機以外,別的沒問題。
六、Conclusion—結(jié)論
翻譯
我們介紹YOLO——一種用于物體檢測的統(tǒng)一模型。 我們的模型構(gòu)造簡單,可以直接在完整圖像上訓練。 與基于分類器的方法不同,YOLO是通過與檢測性能直接對應(yīng)的損失函數(shù)進行訓練的,并且整個模型是一起訓練的。
快速YOLO是文獻中最快的通用目標檢測器,YOLO推動實時對象檢測的最新技術(shù)。 YOLO還能很好地推廣到新領(lǐng)域,使其成為快速,魯棒性強的應(yīng)用的理想選擇。
精讀
到底什么是YOLO?
- YOLO眼里目標檢測是一個回歸問題
- 一次性喂入圖片,然后給出bbox和分類概率
- 簡單來說,只看一次就知道圖中物體的類別和位置
YOLO過程總結(jié):
訓練階段:
首先將一張圖像分成 S × S個 gird cell,然后將它一股腦送入CNN,生成S × S × (B × 5 + C)個結(jié)果,最后根據(jù)結(jié)果求Loss并反向傳播梯度下降。
預測、驗證階段:
首先將一張圖像分成 S × S網(wǎng)格(gird cell),然后將它一股腦送入CNN,生成S × S × (B × 5 + C)個結(jié)果,最后用NMS選出合適的預選框。
復現(xiàn)代碼
復現(xiàn)YOLO v1 PyTorch
Paper: [1506.02640] You Only Look Once: Unified, Real-Time Object Detection (arxiv.org)
Github: EclipseR33/yolo_v1_pytorch (github.com)
數(shù)據(jù)集
VOC2007:The PASCAL Visual Object Classes Challenge 2007 (VOC2007)
VOC2012:The PASCAL Visual Object Classes Challenge 2012 (VOC2012)
PASCAL VOC 07/12的目錄結(jié)構(gòu)都是一致的,因此只需要針對VOC07編寫代碼再擴展即可。VOC2007目錄下有5個文件夾。我們需要其中的’Annotations’(存有標注信息),‘ImageSets’(存有train、val、test各類文件名), ‘JPEGImages’(存有圖像)。VOC中的圖像都是.jpg文件,ImageSets中的文件都是.txt文件,Annotations中的注釋都是.xml文件。
//這是一個xml注釋的示例,我們需要其中的<object>信息
<annotation><folder>VOC2007</folder><filename>000001.jpg</filename><source><database>The VOC2007 Database</database><annotation>PASCAL VOC2007</annotation><image>flickr</image><flickrid>341012865</flickrid></source><owner><flickrid>Fried Camels</flickrid><name>Jinky the Fruit Bat</name></owner><size><width>353</width><height>500</height><depth>3</depth></size><segmented>0</segmented><object><name>dog</name><pose>Left</pose><truncated>1</truncated><difficult>0</difficult><bndbox><xmin>48</xmin><ymin>240</ymin><xmax>195</xmax><ymax>371</ymax></bndbox></object><object><name>person</name> // name中包含的就是class信息<pose>Left</pose><truncated>1</truncated><difficult>0</difficult><bndbox><xmin>8</xmin><ymin>12</ymin><xmax>352</xmax><ymax>498</ymax></bndbox></object>
</annotation>
find_classes.py
首先我們需要獲得VOC數(shù)據(jù)集中的所有class信息并為其編號,將該信息存儲到j(luò)son文件中
# 路徑: ./dataset/find_classes.py
import xml.etree.ElementTree as ET
from tqdm import tqdm
import json
import osdef xml2dict(xml):"""使用遞歸讀取xml文件若c指向的元素是<name>person</name>,那么c.tag是name,c.text則是person"""# data初始化時就已經(jīng)將所有子元素的tag定義為keydata = {c.tag: None for c in xml}for c in xml:# add函數(shù)用于將tag與text添加到data中def add(data, tag, text):if data[tag] is None:# data中該tag為空則直接添加textdata[tag] = textelif isinstance(data[tag], list):# data中該tag為不為空且已經(jīng)創(chuàng)建了list則append(text)data[tag].append(text)else:# data中該tag不為空但是沒有創(chuàng)建list,需要先創(chuàng)建listdata[tag] = [data[tag], text]return dataif len(c) == 0:# len(c)表示c的子元素個數(shù),若為0則表示c是葉元素,沒有子元素data = add(data, c.tag, c.text)else:data = add(data, c.tag, xml2dict(c))return datajson_path = './classes.json' # json保存到的地址root = r'F:\AI\Dataset\VOC2012\VOCdevkit\VOC2012' # 數(shù)據(jù)集root(VOC2007與VOC2012的class信息一致)
# 獲取所有xml注釋地址
annotation_root = os.path.join(root, 'Annotations')
annotation_list = os.listdir(annotation_root)
annotation_list = [os.path.join(annotation_root, a) for a in annotation_list]s = set()
for annotation in tqdm(annotation_list):xml = ET.parse(os.path.join(annotation)).getroot()data = xml2dict(xml)['object']if isinstance(data, list):# 有多個objectfor d in data:s.add(d['name'])else:# 僅有一個objects.add(data['name'])s = list(s)
s.sort()# 以class名稱為key可以便于xml2label的轉(zhuǎn)換
data = {value: i for i, value in enumerate(s)}
json_str = json.dumps(data)with open(json_path, 'w') as f:f.write(json_str)
運行./dataset/find_classes.py之后,我們在指定目錄下得到一個json文件
{"aeroplane": 0, "bicycle": 1, "bird": 2, "boat": 3, "bottle": 4, "bus": 5, "car": 6, "cat": 7, "chair": 8, "cow": 9, "diningtable": 10, "dog": 11, "horse": 12, "motorbike": 13, "person": 14, "pottedplant": 15, "sheep": 16, "sofa": 17, "train": 18, "tvmonitor": 19}
接下來我們要開始寫data.py文件,主要流程:
1.通過ImageSets中train.txt, val.txt, test.txt文件的指引尋找數(shù)據(jù)集對應(yīng)的所有圖像名與其地址。
2.編寫getitem,讀取xml中的信息并轉(zhuǎn)化為label形式,讀取圖像,并將兩者都轉(zhuǎn)為tensor
這里設(shè)置的dataset傳出的label都是xmin, ymin, xmax, ymax, class的VOC形式,并且是直接的坐標數(shù)值,并沒有使用百分比表示。
data.py
# 路徑: ./dataset/data.py
from dataset.transform import * # 導入我們重寫的transform類from torch.utils.data import Dataset
import xml.etree.ElementTree as ET
from PIL import Image
import numpy as np
import json
import osdef get_file_name(root, layout_txt):with open(os.path.join(root, layout_txt)) as layout_txt:""".read() 讀取文件中的數(shù)據(jù),會得到一個str字符串.split('\n') 以\n回車符為分界將str字符串分割成list[:-1] 去除最后一個空字符串,文件末尾有\(zhòng)n,分割后會有空字符串所以要去除"""file_name = layout_txt.read().split('\n')[:-1]return file_namedef xml2dict(xml):# 這里的xml2dict與上一個文件的xml2dict一致data = {c.tag: None for c in xml}for c in xml:def add(data, tag, text):if data[tag] is None:data[tag] = textelif isinstance(data[tag], list):data[tag].append(text)else:data[tag] = [data[tag], text]return dataif len(c) == 0:data = add(data, c.tag, c.text)else:data = add(data, c.tag, xml2dict(c))return dataclass VOC0712Dataset(Dataset):def __init__(self, root, class_path, transforms, mode, data_range=None, get_info=False):# label: xmin, ymin, xmax, ymax, class# 從json文件中獲得class的信息with open(class_path, 'r') as f:json_str = f.read()self.classes = json.loads(json_str)"""如果是train模式,那么root的輸入將為一個list(長為2,分別為2007、2012兩年的數(shù)據(jù)集根目錄, main中的root0712是一個示例)。將兩個root與train、val兩種分割組合成四個layout_txt路徑,這四個路徑指向VOC07/12的所有可用訓練數(shù)據(jù)。如果是test模式那么只有VOC2007的test分割可用。這里也轉(zhuǎn)換為list形式,就可以同一兩種模式的代碼。"""layout_txt = Noneif mode == 'train':root = [root[0], root[0], root[1], root[1]]layout_txt = [r'ImageSets\Main\train.txt', r'ImageSets\Main\val.txt',r'ImageSets\Main\train.txt', r'ImageSets\Main\val.txt']elif mode == 'test':if not isinstance(root, list):root = [root]layout_txt = [r'ImageSets\Main\test.txt']assert layout_txt is not None, 'Unknown mode'self.transforms = transformsself.get_info = get_info # get_info表示在getitem時是否需要獲得圖像的名稱以及圖像大小信息 bool# 由于有多root,所以image_list與annotation_list均存儲了圖像與xml文件的絕對路徑self.image_list = []self.annotation_list = []for r, txt in zip(root, layout_txt):self.image_list += [os.path.join(r, 'JPEGImages', t + '.jpg') for t in get_file_name(r, txt)]self.annotation_list += [os.path.join(r, 'Annotations', t + '.xml') for t in get_file_name(r, txt)]# data_range是一個二元tuple,分別表示數(shù)據(jù)集需要取哪一段區(qū)間,訓練時若使用全部的數(shù)據(jù)則無需傳入data_range,默認None的取值是會選擇所有的數(shù)據(jù)的if data_range is not None:self.image_list = self.image_list[data_range[0]: data_range[1]]self.annotation_list = self.annotation_list[data_range[0]: data_range[1]]def __len__(self):# 返回數(shù)據(jù)集長度return len(self.annotation_list)def __getitem__(self, idx):image = Image.open(self.image_list[idx])image_size = image.sizelabel = self.label_process(self.annotation_list[idx])if self.transforms is not None:# 由于目標檢測中image的變換如隨機裁剪與Resize都會導致label的變化,所以需要重寫transform,添加部分的label處理代碼image, label = self.transforms(image, label)if self.get_info:return image, label, os.path.basename(self.image_list[idx]).split('.')[0], image_sizeelse:return image, labeldef label_process(self, annotation):xml = ET.parse(os.path.join(annotation)).getroot()data = xml2dict(xml)['object']# 根據(jù)data的兩種形式將其讀取到label中,并將label轉(zhuǎn)為numpy形式if isinstance(data, list):label = [[float(d['bndbox']['xmin']), float(d['bndbox']['ymin']),float(d['bndbox']['xmax']), float(d['bndbox']['ymax']),self.classes[d['name']]]for d in data]else:label = [[float(data['bndbox']['xmin']), float(data['bndbox']['ymin']),float(data['bndbox']['xmax']), float(data['bndbox']['ymax']),self.classes[data['name']]]]label = np.array(label)return labelif __name__ == "__main__":from dataset.draw_bbox import drawroot0712 = [r'F:\AI\Dataset\VOC2007\VOCdevkit\VOC2007', r'F:\AI\Dataset\VOC2012\VOCdevkit\VOC2012']transforms = Compose([ToTensor(),RandomHorizontalFlip(0.5),Resize(448)])ds = VOC0712Dataset(root0712, 'classes.json', transforms, 'train', get_info=True)print(len(ds))for i, (image, label, image_name, image_size) in enumerate(ds):if i <= 1000:continueelif i >= 1010:breakelse:print(label.dtype)print(tuple(image.size()[1:]))draw(image, label, ds.classes)print('VOC2007Dataset')
transform.py
# 路徑: ./dataset/transform.py
import torch
import torchvision
import randomclass Compose:def __init__(self, transforms):self.transforms = transformsdef __call__(self, image, label):for t in self.transforms:image, label = t(image, label)return image, labelclass ToTensor:def __init__(self):self.totensor = torchvision.transforms.ToTensor()def __call__(self, image, label):image = self.totensor(image)label = torch.tensor(label)return image, labelclass RandomHorizontalFlip:def __init__(self, p=0.5):self.p = pdef __call__(self, image, label):""":param label: xmin, ymin, xmax, ymax如果圖片被水平翻轉(zhuǎn),那么label的xmin與xmax會互換,變成 xmax, ymin, xmin, ymax由于YOLO的輸出是(center_x, center_y, w, h) ,因此label的xmin與xmax換位不會影響損失計算與訓練但是需要注意w,h計算時使用abs"""if random.random() < self.p:height, width = image.shape[-2:]image = image.flip(-1) # 水平翻轉(zhuǎn)bbox = label[:, :4]bbox[:, [0, 2]] = width - bbox[:, [0, 2]]label[:, :4] = bboxreturn image, labelclass Resize:def __init__(self, image_size, keep_ratio=True):""":param image_size: intkeep_ratio = True 保留寬高比keep_ratio = False 填充成正方形"""self.image_size = image_sizeself.keep_ratio = keep_ratiodef __call__(self, image, label):""":param in_image: tensor [3, h, w]:param label: xmin, ymin, xmax, ymax:return:"""# 將所有圖片左上角對齊構(gòu)成448*448tensor的Transformh, w = tuple(image.size()[1:])label[:, [0, 2]] = label[:, [0, 2]] / wlabel[:, [1, 3]] = label[:, [1, 3]] / hif self.keep_ratio:r_h = min(self.image_size / h, self.image_size / w)r_w = r_helse:r_h = self.image_size / hr_w = self.image_size / wh, w = int(r_h * h), int(r_w * w)h, w = min(h, self.image_size), min(w, self.image_size)label[:, [0, 2]] = label[:, [0, 2]] * wlabel[:, [1, 3]] = label[:, [1, 3]] * hT = torchvision.transforms.Resize([h, w])Padding = torch.nn.ZeroPad2d((0, self.image_size - w, 0, self.image_size - h))image = Padding(T(image))assert list(image.size()) == [3, self.image_size, self.image_size]return image, label
draw_bbox.py
# 路徑: ./dataset/draw_bbox.py
import torchvision.transforms as F
import numpy as np
from PIL import ImageDraw, ImageFont
import matplotlib.pyplot as pltcolors = ['Pink', 'Crimson', 'Magenta', 'Indigo', 'BlueViolet','Blue', 'GhostWhite', 'LightSteelBlue', 'Brown', 'SkyBlue','Tomato', 'SpringGreen', 'Green', 'Yellow', 'Olive','Gold', 'Wheat', 'Orange', 'Gray', 'Red']def draw(image, bbox, classes, show_conf=False, conf_th=0.0):""":param image: tensor:param bbox: tensor xmin, ymin, xmax, ymax"""keys = list(classes.keys())values = list(classes.values())# 設(shè)置字體(包括大小)font = ImageFont.truetype('arial.ttf', 10)transform = F.ToPILImage()image = transform(image)draw_image = ImageDraw.Draw(image)bbox = np.array(bbox.cpu())for b in bbox:print(b)if show_conf and b[-2] < conf_th:continuedraw_image.rectangle(list(b[:4]), outline=colors[int(b[-1])], width=3)if show_conf:draw_image.text(list(b[:2] + 5), keys[values.index(int(b[-1]))] + ' {:.2f}'.format(b[-2]),fill=colors[int(b[-1])], font=font)else:draw_image.text(list(b[:2] + 5), keys[values.index(int(b[-1]))],fill=colors[int(b[-1])], font=font)plt.figure()plt.imshow(image)plt.show()
模型
darknet.py
根據(jù)論文的描述,首先構(gòu)建Darknet的Backbone部分
下圖是Backbone的基本結(jié)構(gòu)請?zhí)砑訄D片描述,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳
請?zhí)砑訄D片描述
下文指出了darknet全部使用LeakyReLU并且參數(shù)為0.1,除了最后的全連接層不需要激活函數(shù)。
# 路徑: ./model/darknet.py
import torch.nn as nndef conv(in_ch, out_ch, k_size=3, stride=1, padding=1):return nn.Sequential(nn.Conv2d(in_ch, out_ch, k_size, stride, padding, bias=False),nn.LeakyReLU(0.1))def make_layer(param):layers = []if not isinstance(param[0], list):param = [param]for p in param:layers.append(conv(*p))return nn.Sequential(*layers)class Block(nn.Module):def __init__(self, param, use_pool=True):super(Block, self).__init__()self.conv = make_layer(param)self.pool = nn.MaxPool2d(2)self.use_pool = use_pooldef forward(self, x):x = self.conv(x)if self.use_pool:x = self.pool(x)return xclass DarkNet(nn.Module):def __init__(self):super(DarkNet, self).__init__()self.conv1 = Block([[3, 64, 7, 2, 3]])self.conv2 = Block([[64, 192, 3, 1, 1]])self.conv3 = Block([[192, 128, 1, 1, 0],[128, 256, 3, 1, 1],[256, 256, 1, 1, 0],[256, 512, 3, 1, 1]])self.conv4 = Block([[512, 256, 1, 1, 0],[256, 512, 3, 1, 1],[512, 256, 1, 1, 0],[256, 512, 3, 1, 1],[512, 256, 1, 1, 0],[256, 512, 3, 1, 1],[512, 256, 1, 1, 0],[256, 512, 3, 1, 1],[512, 512, 1, 1, 0],[512, 1024, 3, 1, 1]])self.conv5 = Block([[1024, 512, 1, 1, 0],[512, 1024, 3, 1, 1],[1024, 512, 1, 1, 0],[512, 1024, 3, 1, 1],[1024, 1024, 3, 1, 1],[1024, 1024, 3, 2, 1]], False)self.conv6 = Block([[1024, 1024, 3, 1, 1],[1024, 1024, 3, 1, 1]], False)for m in self.modules():if isinstance(m, nn.Conv2d):nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='leaky_relu')def forward(self, x):x = self.conv1(x)x = self.conv2(x)x = self.conv3(x)x = self.conv4(x)x = self.conv5(x)x = self.conv6(x)return xif __name__ == "__main__":import torchx = torch.randn([1, 3, 448, 448])net = DarkNet()print(net)out = net(x)print(out.size())
resnet.py
作者還說明了Darknet需要預訓練,由于在ImageNet上進行預訓練耗時過長,我選擇使用修改過的Resnet50作為Backbone并使用Pytorch官方的預訓練參數(shù)。
# 路徑: ./model/resnet.py
import torch
from torchvision.models.resnet import ResNet, Bottleneck"""
通過繼承Pytorch的ResNet代碼,重寫其中的_forward_impl來去除最后的avgpool與fc層
此外我將Resnet50原有的layer4省略并額外增加了兩個maxpool層,使得Resnet輸出的特征圖與Darknet一致
均為[1024, 7, 7]
"""
class ResNet_(ResNet):def __init__(self, block, layers):super(ResNet_, self).__init__(block=block, layers=layers)def _forward_impl(self, x):x = self.conv1(x)x = self.bn1(x)x = self.relu(x)x = self.maxpool(x)x = self.layer1(x)x = self.layer2(x)x = self.maxpool(x)x = self.layer3(x)x = self.maxpool(x)return xdef forward(self, x):return self._forward_impl(x)def _resnet(block, layers, pretrained):model = ResNet_(block, layers)if pretrained is not None:state_dict = torch.load(pretrained)model.load_state_dict(state_dict)return modeldef resnet_1024ch(pretrained=None) -> ResNet:resnet = _resnet(Bottleneck, [3, 4, 6, 3], pretrained)return resnetif __name__ == '__main__':x = torch.randn([1, 3, 448, 448])net = resnet_1024ch('resnet50-19c8e357.pth')print(net)y = net(x)print(y.size())
yolo.py
接下來復現(xiàn)yolo v1的目標檢測核心部分。
? Yolo v1的目標檢測思路是將一張圖像分割成S × S個grid cell,每個grid cell都會預測B個bbox,同時每個grid cell都會預測一個類別,作為其預測出的B個bbox的共同類別。如果一個目標的中心坐標位于某個grid cell中,那么這個grid cell就負責預測這個目標。一個bbox由x,y,w,h,conf五個參數(shù)表示,分別是中心X坐標,中心Y坐標(這里的x、y是相對于grid cell的x、y),寬,高,object置信度。其中這五個參數(shù)都是百分比的表示形式,x、y需要除以grid cell的寬、高;w、h則需要除以整張圖片的寬、高;對于object conf,如果沒有對應(yīng)bbox則label為0,如果有對應(yīng)bbox則label為預測出的bbox與對應(yīng)bbox的IoU交并比。而且在測試時最終conf=object conf * conditional class probabilities。最終結(jié)果是xywh都在0到1之間。負責預測類別的tensor長度應(yīng)為class個數(shù)。模型結(jié)構(gòu)上最后一個全連接層的大小則是S × S × ( B * 5 + C )。
? 最后預測bbox的全連接層之間有Dropout層,rate=0.5
損失函數(shù)上,YOLO v1以均方損失為為主,在部分參數(shù)的損失計算上使用了一些技巧。如w、h計算中就先開根號使得小目標的損失更為顯著,加入λcoord=5和λnoobj=0.5以增加xy的損失值(0到1中開根號會讓值變大),降低框出背景bbox的損失以適應(yīng)該類bbox較多的狀況。復現(xiàn)過程中xywhc的五個損失值都直接計算、class的損失值由CorssEntropyLoss函數(shù)直接計算。文中也定義了預測的bbox與label中的bbox匹配方法——IoU匹配。以IoU較大的一對bbox確定對應(yīng)關(guān)系,而且這個過程取走一對之后不能重復取。只有對應(yīng)的bbox才能計算x y w h conf各類損失。如果一個預測bbox沒有對應(yīng)的label bbox,那么就認為其標記到了背景,只計算conf。class的損失是有負責label bbox的grid cell需要計算的。因此其計算次數(shù)只是S2次。
這里需注意計算損失時如果沒有使用pytorch中官方定義的損失函數(shù),那么需要先通過sigmoid函數(shù)將模型輸出限制到0~1之間才能進行直接的損失計算。
yolo.py的代碼了yolo模型、yolo損失計算、yolo后處理三個部分。對前兩個模塊,yolo.py都有相應(yīng)的測試代碼,而后處理部分需要在test.py中測試,其不會被yolo.py內(nèi)部調(diào)用。
# 路徑: ./model/yolo.py
import numpy as npfrom model.darknet import DarkNet
from model.resnet import resnet_1024chimport torch
import torch.nn as nn
import torchvision# yolo模型
class yolo(nn.Module):def __init__(self, s, cell_out_ch, backbone_name, pretrain=None):"""return: [s, s, cell_out_ch]"""super(yolo, self).__init__()self.s = sself.backbone = Noneself.conv = Noneif backbone_name == 'darknet':self.backbone = DarkNet()elif backbone_name == 'resnet':self.backbone = resnet_1024ch(pretrained=pretrain)self.backbone_name = backbone_nameassert self.backbone is not None, 'Wrong backbone name'self.fc = nn.Sequential(nn.Linear(1024 * s * s, 4096),nn.LeakyReLU(0.1),nn.Dropout(0.5),nn.Linear(4096, s * s * cell_out_ch))def forward(self, x):batch_size = x.size(0)x = self.backbone(x)x = torch.flatten(x, 1)x = self.fc(x)x = x.view(batch_size, self.s ** 2, -1)return x# yolo損失計算
class yolo_loss:def __init__(self, device, s, b, image_size, num_classes):self.device = deviceself.s = sself.b = bself.image_size = image_sizeself.num_classes = num_classesself.batch_size = 0def __call__(self, input, target):""":param input: (yolo net output)tensor[s, s, b*5 + n_class] bbox: b * (c_x, c_y, w, h, obj_conf), class1_p, class2_p.. %:param target: (dataset) tensor[n_bbox] bbox: x_min, ymin, xmax, ymax, class:return: loss tensorgrid type: [[bbox, ..], [], ..] -> bbox_in_grid: c_x(%), c_y(%), w(%), h(%), class(int)target to grid typeif s = 7 -> grid idx: 1 -> 49由于沒有使用PyTorch的損失函數(shù),所以需要先分離不同的batch分別計算損失"""self.batch_size = input.size(0)# label預處理target = [self.label_direct2grid(target[i]) for i in range(self.batch_size)]# IoU 匹配predictor和label# 以Predictor為基準,每個Predictor都有且僅有一個需要負責的Target(前提是Predictor所在Grid Cell有Target中心位于此)# x, y, w, h, cmatch = []conf = []for i in range(self.batch_size):m, c = self.match_pred_target(input[i], target[i])match.append(m)conf.append(c)loss = torch.zeros([self.batch_size], dtype=torch.float, device=self.device)xy_loss = torch.zeros_like(loss)wh_loss = torch.zeros_like(loss)conf_loss = torch.zeros_like(loss)class_loss = torch.zeros_like(loss)for i in range(self.batch_size):loss[i], xy_loss[i], wh_loss[i], conf_loss[i], class_loss[i] = \self.compute_loss(input[i], target[i], match[i], conf[i])return torch.mean(loss), torch.mean(xy_loss), torch.mean(wh_loss), torch.mean(conf_loss), torch.mean(class_loss)def label_direct2grid(self, label):""":param label: dataset type: xmin, ymin, xmax, ymax, class:return: label: grid type, if the grid doesn't have object -> put None將label轉(zhuǎn)換為c_x, c_y, w, h, conf再根據(jù)不同的grid cell分類,并轉(zhuǎn)換成百分比形式若一個grid cell中沒有l(wèi)abel則都用None代替"""output = [None for _ in range(self.s ** 2)]size = self.image_size // self.s # h, wn_bbox = label.size(0)label_c = torch.zeros_like(label)label_c[:, 0] = (label[:, 0] + label[:, 2]) / 2label_c[:, 1] = (label[:, 1] + label[:, 3]) / 2label_c[:, 2] = abs(label[:, 0] - label[:, 2])label_c[:, 3] = abs(label[:, 1] - label[:, 3])label_c[:, 4] = label[:, 4]idx_x = [int(label_c[i][0]) // size for i in range(n_bbox)]idx_y = [int(label_c[i][1]) // size for i in range(n_bbox)]label_c[:, 0] = torch.div(torch.fmod(label_c[:, 0], size), size)label_c[:, 1] = torch.div(torch.fmod(label_c[:, 1], size), size)label_c[:, 2] = torch.div(label_c[:, 2], self.image_size)label_c[:, 3] = torch.div(label_c[:, 3], self.image_size)for i in range(n_bbox):idx = idx_y[i] * self.s + idx_x[i]if output[idx] is None:output[idx] = torch.unsqueeze(label_c[i], dim=0)else:output[idx] = torch.cat([output[idx], torch.unsqueeze(label_c[i], dim=0)], dim=0)return outputdef match_pred_target(self, input, target):match = []conf = []with torch.no_grad():input_bbox = input[:, :self.b * 5].reshape(-1, self.b, 5)ious = [match_get_iou(input_bbox[i], target[i], self.s, i)for i in range(self.s ** 2)]for iou in ious:if iou is None:match.append(None)conf.append(None)else:keep = np.ones([len(iou[0])], dtype=bool)m = []c = []for i in range(self.b):if np.any(keep) == False:breakidx = np.argmax(iou[i][keep])np_max = np.max(iou[i][keep])m.append(np.argwhere(iou[i] == np_max).tolist()[0][0])c.append(np.max(iou[i][keep]))keep[idx] = 0match.append(m)conf.append(c)return match, confdef compute_loss(self, input, target, match, conf):# 計算損失ce_loss = nn.CrossEntropyLoss()input_bbox = input[:, :self.b * 5].reshape(-1, self.b, 5)input_class = input[:, self.b * 5:].reshape(-1, self.num_classes)input_bbox = torch.sigmoid(input_bbox)loss = torch.zeros([self.s ** 2], dtype=torch.float, device=self.device)xy_loss = torch.zeros_like(loss)wh_loss = torch.zeros_like(loss)conf_loss = torch.zeros_like(loss)class_loss = torch.zeros_like(loss)# 不同grid cell分別計算再求和for i in range(self.s ** 2):# 0 xy_loss, 1 wh_loss, 2 conf_loss, 3 class_lossl = torch.zeros([4], dtype=torch.float, device=self.device)# Negif target[i] is None:# λ_noobj = 0.5obj_conf_target = torch.zeros([self.b], dtype=torch.float, device=self.device)l[2] = torch.sum(torch.mul(0.5, torch.pow(input_bbox[i, :, 4] - obj_conf_target, 2)))else:# λ_coord = 5l[0] = torch.mul(5, torch.sum(torch.pow(input_bbox[i, :, 0] - target[i][match[i], 0], 2) +torch.pow(input_bbox[i, :, 1] - target[i][match[i], 1], 2)))l[1] = torch.mul(5, torch.sum(torch.pow(torch.sqrt(input_bbox[i, :, 2]) -torch.sqrt(target[i][match[i], 2]), 2) +torch.pow(torch.sqrt(input_bbox[i, :, 3]) -torch.sqrt(target[i][match[i], 3]), 2)))obj_conf_target = torch.tensor(conf[i], dtype=torch.float, device=self.device)l[2] = torch.sum(torch.pow(input_bbox[i, :, 4] - obj_conf_target, 2))l[3] = ce_loss(input_class[i].unsqueeze(dim=0).repeat(target[i].size(0), 1),target[i][:, 4].long())loss[i] = torch.sum(l)xy_loss[i] = torch.sum(l[0])wh_loss[i] = torch.sum(l[1])conf_loss[i] = torch.sum(l[2])class_loss[i] = torch.sum(l[3])return torch.sum(loss), torch.sum(xy_loss), torch.sum(wh_loss), torch.sum(conf_loss), torch.sum(class_loss)def cxcywh2xyxy(bbox):""":param bbox: [bbox, bbox, ..] tensor c_x(%), c_y(%), w(%), h(%), c"""bbox[:, 0] = bbox[:, 0] - bbox[:, 2] / 2bbox[:, 1] = bbox[:, 1] - bbox[:, 3] / 2bbox[:, 2] = bbox[:, 0] + bbox[:, 2]bbox[:, 3] = bbox[:, 1] + bbox[:, 3]return bboxdef match_get_iou(bbox1, bbox2, s, idx):""":param bbox1: [bbox, bbox, ..] tensor c_x(%), c_y(%), w(%), h(%), c:return:"""if bbox1 is None or bbox2 is None:return Nonebbox1 = np.array(bbox1.cpu())bbox2 = np.array(bbox2.cpu())# c_x, c_y轉(zhuǎn)換為對整張圖片的百分比bbox1[:, 0] = bbox1[:, 0] / sbbox1[:, 1] = bbox1[:, 1] / sbbox2[:, 0] = bbox2[:, 0] / sbbox2[:, 1] = bbox2[:, 1] / s# c_x, c_y加上grid cell左上角左邊變成完整坐標grid_pos = [(j / s, i / s) for i in range(s) for j in range(s)]bbox1[:, 0] = bbox1[:, 0] + grid_pos[idx][0]bbox1[:, 1] = bbox1[:, 1] + grid_pos[idx][1]bbox2[:, 0] = bbox2[:, 0] + grid_pos[idx][0]bbox2[:, 1] = bbox2[:, 1] + grid_pos[idx][1]bbox1 = cxcywh2xyxy(bbox1)bbox2 = cxcywh2xyxy(bbox2)# %return get_iou(bbox1, bbox2)def get_iou(bbox1, bbox2):""":param bbox1: [bbox, bbox, ..] tensor xmin ymin xmax ymax:param bbox2::return: area:"""s1 = abs(bbox1[:, 2] - bbox1[:, 0]) * abs(bbox1[:, 3] - bbox1[:, 1])s2 = abs(bbox2[:, 2] - bbox2[:, 0]) * abs(bbox2[:, 3] - bbox2[:, 1])ious = []for i in range(bbox1.shape[0]):xmin = np.maximum(bbox1[i, 0], bbox2[:, 0])ymin = np.maximum(bbox1[i, 1], bbox2[:, 1])xmax = np.minimum(bbox1[i, 2], bbox2[:, 2])ymax = np.minimum(bbox1[i, 3], bbox2[:, 3])in_w = np.maximum(xmax - xmin, 0)in_h = np.maximum(ymax - ymin, 0)in_s = in_w * in_hiou = in_s / (s1[i] + s2 - in_s)ious.append(iou)ious = np.array(ious)return iousdef nms(bbox, conf_th, iou_th):bbox = np.array(bbox.cpu())bbox[:, 4] = bbox[:, 4] * bbox[:, 5]bbox = bbox[bbox[:, 4] > conf_th]order = np.argsort(-bbox[:, 4])keep = []while order.size > 0:i = order[0]keep.append(i)iou = get_iou(np.array([bbox[i]]), bbox[order[1:]])[0]inds = np.where(iou <= iou_th)[0]order = order[inds + 1]return bbox[keep]# yolo后處理
def output_process(output, image_size, s, b, conf_th, iou_th):"""輸入是包含batch的模型輸出:return output: list[], bbox: xmin, ymin, xmax, ymax, obj_conf, classes_conf, classes"""batch_size = output.size(0)size = image_size // soutput = torch.sigmoid(output)# Get Class# 將class conf依次添加到bbox中classes_conf, classes = torch.max(output[:, :, b * 5:], dim=2)classes = classes.unsqueeze(dim=2).repeat(1, 1, 2).unsqueeze(dim=3)classes_conf = classes_conf.unsqueeze(dim=2).repeat(1, 1, 2).unsqueeze(dim=3)bbox = output[:, :, :b * 5].reshape(batch_size, -1, b, 5)bbox = torch.cat([bbox, classes_conf, classes], dim=3)# To Direct# 百分比形式轉(zhuǎn)直接表示bbox[:, :, :, [0, 1]] = bbox[:, :, :, [0, 1]] * sizebbox[:, :, :, [2, 3]] = bbox[:, :, :, [2, 3]] * image_size# 添加grid cell坐標grid_pos = [(j * image_size // s, i * image_size // s) for i in range(s) for j in range(s)]def to_direct(bbox):for i in range(s ** 2):bbox[i, :, 0] = bbox[i, :, 0] + grid_pos[i][0]bbox[i, :, 1] = bbox[i, :, 1] + grid_pos[i][1]return bboxbbox_direct = torch.stack([to_direct(b) for b in bbox])bbox_direct = bbox_direct.reshape(batch_size, -1, 7)# cxcywh to xyxybbox_direct[:, :, 0] = bbox_direct[:, :, 0] - bbox_direct[:, :, 2] / 2bbox_direct[:, :, 1] = bbox_direct[:, :, 1] - bbox_direct[:, :, 3] / 2bbox_direct[:, :, 2] = bbox_direct[:, :, 0] + bbox_direct[:, :, 2]bbox_direct[:, :, 3] = bbox_direct[:, :, 1] + bbox_direct[:, :, 3]bbox_direct[:, :, 0] = torch.maximum(bbox_direct[:, :, 0], torch.zeros(1))bbox_direct[:, :, 1] = torch.maximum(bbox_direct[:, :, 1], torch.zeros(1))bbox_direct[:, :, 2] = torch.minimum(bbox_direct[:, :, 2], torch.tensor([image_size]))bbox_direct[:, :, 3] = torch.minimum(bbox_direct[:, :, 3], torch.tensor([image_size]))# 整合不同batch中的bboxbbox = [torch.tensor(nms(b, conf_th, iou_th)) for b in bbox_direct]bbox = torch.stack(bbox)return bboxif __name__ == "__main__":import torch# Test yolox = torch.randn([1, 3, 448, 448])# B * 5 + n_classesnet = yolo(7, 2 * 5 + 20, 'resnet', pretrain=None)# net = yolo(7, 2 * 5 + 20, 'darknet', pretrain=None)print(net)out = net(x)print(out)print(out.size())# Test yolo_loss# 測試時假設(shè) s=2, class=2s = 2b = 2image_size = 448 # h, winput = torch.tensor([[[0.45, 0.24, 0.22, 0.3, 0.35, 0.54, 0.66, 0.7, 0.8, 0.8, 0.17, 0.9],[0.37, 0.25, 0.5, 0.3, 0.36, 0.14, 0.27, 0.26, 0.33, 0.36, 0.13, 0.9],[0.12, 0.8, 0.26, 0.74, 0.8, 0.13, 0.83, 0.6, 0.75, 0.87, 0.75, 0.24],[0.1, 0.27, 0.24, 0.37, 0.34, 0.15, 0.26, 0.27, 0.37, 0.34, 0.16, 0.93]]])target = [torch.tensor([[200, 200, 353, 300, 1],[220, 230, 353, 300, 1],[15, 330, 200, 400, 0],[100, 50, 198, 223, 1],[30, 60, 150, 240, 1]], dtype=torch.float)]criterion = yolo_loss('cpu', 2, 2, image_size, 2)loss = criterion(input, target)print(loss)
scheduler.py
論文作者也給出了他的訓練參數(shù)。我們首先復現(xiàn)學習率的調(diào)整方式。原文分成三段學習率,每一段都有不同的保持epochs長度。此外,我額外增加了熱身訓練階段。
# 路徑: ./scheduler.py
from torch.optim.lr_scheduler import _LRSchedulerclass Scheduler(_LRScheduler):def __init__(self, optimizer, step_warm_ep, lr_start, step_1_lr, step_1_ep,step_2_lr, step_2_ep, step_3_lr, step_3_ep, last_epoch=-1):self.optimizer = optimizerself.lr_start = lr_startself.step_warm_ep = step_warm_epself.step_1_lr = step_1_lrself.step_1_ep = step_1_epself.step_2_lr = step_2_lrself.step_2_ep = step_2_epself.step_3_lr = step_3_lrself.step_3_ep = step_3_epself.last_epoch = last_epochsuper(Scheduler, self).__init__(optimizer, last_epoch)def get_lr(self):if self.last_epoch == 0:return [self.lr_start for _ in self.optimizer.param_groups]lr = self._compute_lr_from_epoch()return [lr for _ in self.optimizer.param_groups]def _get_closed_form_lr(self):return self.base_lrsdef _compute_lr_from_epoch(self):if self.last_epoch < self.step_warm_ep:lr = ((self.step_1_lr - self.lr_start)/self.step_warm_ep) * self.last_epoch + self.lr_startelif self.last_epoch < self.step_warm_ep + self.step_1_ep:lr = self.step_1_lrelif self.last_epoch < self.step_warm_ep + self.step_1_ep + self.step_2_ep:lr = self.step_2_lrelif self.last_epoch < self.step_warm_ep + self.step_1_ep + self.step_2_ep + self.step_3_ep:lr = self.step_3_lrelse:lr = self.step_3_lrreturn lrif __name__ == '__main__':import torch.nn as nnimport torch.optim as optimimport numpy as npimport matplotlib.pyplot as pltimport warningswarnings.filterwarnings('ignore')batch_size = 16epoch = 135scheduler_params = {'lr_start': 1e-3,'step_warm_ep': 10,'step_1_lr': 1e-2,'step_1_ep': 75,'step_2_lr': 1e-3,'step_2_ep': 30,'step_3_lr': 1e-4,'step_3_ep': 20}model = nn.Sequential(nn.Linear(1, 10),nn.Linear(10, 1))optimizer = optim.SGD(model.parameters(), lr=scheduler_params['lr_start'])scheduler = Scheduler(optimizer, **scheduler_params)lrs = []for _ in range(epoch):lrs.append(optimizer.param_groups[0]['lr'])scheduler.step()print(lrs)lrs = np.array(lrs)# 使用plt可視化學習率plt.figure()plt.plot(lrs)plt.show()
train.py
訓練參數(shù)主要參考了論文原文。由于batch size較小,學習率都等比減小。優(yōu)化器選擇了SGD并帶有momentum與weight_decay。代碼中有凍結(jié)Backbone的選項。
root0712 存儲了VOC07/12兩年數(shù)據(jù)集的根目錄
model_root 存儲模型路徑
backbone 'resnet’則代表使用修改后的resnet;'darknet’則代表使用原文中的darknet
pretrain None則不使用預訓練模型;或直接輸入預訓練權(quán)重地址
with_amp 混合精度選項
transforms 需要使用代碼中定義的transforms而不是PyTorch直接給出的transforms
start_epoch 中斷訓練重啟的開始epoch
epoch 總epoch數(shù)
freeze_backbone_till 若為-1則不凍結(jié)backbone,其他數(shù)則會凍結(jié)backbone直到該epoch后解凍
# 路徑: ./train.py
from dataset.data import VOC0712Dataset
from dataset.transform import *
from model.yolo import yolo, yolo_loss
from scheduler import Schedulerimport torch
from torch.utils.data import DataLoader
from torch import optim
from torch.cuda.amp import autocast, GradScalerfrom tqdm import tqdm
import pandas as pd
import json
import osimport warningsclass CFG:device = 'cuda:0' if torch.cuda.is_available() else 'cpu'root0712 = [r'F:\AI\Dataset\VOC2007\VOCdevkit\VOC2007', r'F:\AI\Dataset\VOC2012\VOCdevkit\VOC2012']class_path = r'./dataset/classes.json'model_root = r'./log/ex7'# 若model_path是一個指向權(quán)重文件的str路徑,那么會將模型傳入指定模型權(quán)重model_path = None# 這里沒有編寫創(chuàng)建文件夾的代碼,直接運行需要手動將文件夾創(chuàng)建好backbone = 'resnet'pretrain = 'model/resnet50-19c8e357.pth'# 混合精度可選,若為False則采用常規(guī)精度with_amp = TrueS = 7B = 2image_size = 448transforms = Compose([ToTensor(),RandomHorizontalFlip(0.5),Resize(448, keep_ratio=False)])start_epoch = 0epoch = 135batch_size = 16num_workers = 2# freeze_backbone_till = -1 則不凍結(jié)freeze_backbone_till = 30scheduler_params = {'lr_start': 1e-3 / 4,'step_warm_ep': 10,'step_1_lr': 1e-2 / 4,'step_1_ep': 75,'step_2_lr': 1e-3 / 4,'step_2_ep': 40,'step_3_lr': 1e-4 / 4,'step_3_ep': 10}momentum = 0.9weight_decay = 0.0005def collate_fn(batch):return tuple(zip(*batch))class AverageMeter:def __init__(self):self.reset()def reset(self):self.val = 0self.avg = 0self.sum = 0self.count = 0def update(self, val, n=1):self.val = valself.sum += val * nself.count += nself.avg = self.sum / self.countdef train():device = torch.device(CFG.device)print('Train:\nDevice:{}'.format(device))with open(CFG.class_path, 'r') as f:json_str = f.read()classes = json.loads(json_str)CFG.num_classes = len(classes)train_ds = VOC0712Dataset(CFG.root0712, CFG.class_path, CFG.transforms, 'train')test_ds = VOC0712Dataset(CFG.root0712, CFG.class_path, CFG.transforms, 'test')train_dl = DataLoader(train_ds, batch_size=CFG.batch_size, shuffle=True,num_workers=CFG.num_workers, collate_fn=collate_fn)test_dl = DataLoader(test_ds, batch_size=CFG.batch_size, shuffle=False,num_workers=CFG.num_workers, collate_fn=collate_fn)yolo_net = yolo(s=CFG.S, cell_out_ch=CFG.B * 5 + CFG.num_classes, backbone_name=CFG.backbone, pretrain=CFG.pretrain)yolo_net.to(device)if CFG.model_path is not None:yolo_net.load_state_dict(torch.load(CFG.model_path))if CFG.freeze_backbone_till != -1:print('Freeze Backbone')for param in yolo_net.backbone.parameters():param.requires_grad_(False)param = [p for p in yolo_net.parameters() if p.requires_grad]optimizer = optim.SGD(param, lr=CFG.scheduler_params['lr_start'],momentum=CFG.momentum, weight_decay=CFG.weight_decay)criterion = yolo_loss(CFG.device, CFG.S, CFG.B, CFG.image_size, len(train_ds.classes))scheduler = Scheduler(optimizer, **CFG.scheduler_params)scaler = GradScaler()for _ in range(CFG.start_epoch):scheduler.step()best_train_loss = 1e+9train_losses = []test_losses = []lrs = []for epoch in range(CFG.start_epoch, CFG.epoch):if CFG.freeze_backbone_till != -1 and epoch >= CFG.freeze_backbone_till:print('Unfreeze Backbone')for param in yolo_net.backbone.parameters():param.requires_grad_(True)CFG.freeze_backbone_till = -1# Trainyolo_net.train()loss_score = AverageMeter()dl = tqdm(train_dl, total=len(train_dl))for images, labels in dl:batch_size = len(labels)images = torch.stack(images)images = images.to(device)labels = [label.to(device) for label in labels]optimizer.zero_grad()if CFG.with_amp:with autocast():outputs = yolo_net(images)loss, xy_loss, wh_loss, conf_loss, class_loss = criterion(outputs, labels)scaler.scale(loss).backward()scaler.step(optimizer)scaler.update()else:outputs = yolo_net(images)loss, xy_loss, wh_loss, conf_loss, class_loss = criterion(outputs, labels)loss.backward()optimizer.step()loss_score.update(loss.detach().item(), batch_size)dl.set_postfix(Mode='Train', AvgLoss=loss_score.avg, Loss=loss.detach().item(),Epoch=epoch, LR=optimizer.param_groups[0]['lr'])lrs.append(optimizer.param_groups[0]['lr'])scheduler.step()train_losses.append(loss_score.avg)print('Train Loss: {:.4f}'.format(loss_score.avg))if best_train_loss > loss_score.avg:print('Save yolo_net to {}'.format(os.path.join(CFG.model_root, 'yolo.pth')))torch.save(yolo_net.state_dict(), os.path.join(CFG.model_root, 'yolo.pth'))best_train_loss = loss_score.avgloss_score.reset()with torch.no_grad():# Testyolo_net.eval()dl = tqdm(test_dl, total=len(test_dl))for images, labels in dl:batch_size = len(labels)images = torch.stack(images)images = images.to(device)labels = [label.to(device) for label in labels]outputs = yolo_net(images)loss, xy_loss, wh_loss, conf_loss, class_loss = criterion(outputs, labels)loss_score.update(loss.detach().item(), batch_size)dl.set_postfix(Mode='Test', AvgLoss=loss_score.avg, Loss=loss.detach().item(), Epoch=epoch)test_losses.append(loss_score.avg)print('Test Loss: {:.4f}'.format(loss_score.avg))df = pd.DataFrame({'Train Loss': train_losses, 'Test Loss': test_losses, 'LR': lrs})df.to_csv(os.path.join(CFG.model_root, 'result.csv'), index=True)if __name__ == '__main__':warnings.filterwarnings('ignore')train()
voc_eval.py
測試時計算mAP的代碼
# 路徑: ./voc_eval.py
"""Adapted from:@longcw faster_rcnn_pytorch: https://github.com/longcw/faster_rcnn_pytorch@rbgirshick py-faster-rcnn https://github.com/rbgirshick/py-faster-rcnnLicensed under The MIT License [see LICENSE for details]這個文件的代碼有改動,我刪除了緩存的代碼,因此也不需要傳入緩存文件夾
"""import xml.etree.ElementTree as ET
import numpy as np
import osdef voc_ap(rec, prec, use_07_metric=False):""" ap = voc_ap(rec, prec, [use_07_metric])Compute VOC AP given precision and recall.If use_07_metric is true, uses theVOC 07 11 point method (default:False)."""if use_07_metric:# 11 point metricap = 0.for t in np.arange(0., 1.1, 0.1):if np.sum(rec >= t) == 0:p = 0else:p = np.max(prec[rec >= t])ap = ap + p / 11.else:# correct AP calculation# first append sentinel values at the endmrec = np.concatenate(([0.], rec, [1.]))mpre = np.concatenate(([0.], prec, [0.]))# compute the precision envelopefor i in range(mpre.size - 1, 0, -1):mpre[i - 1] = np.maximum(mpre[i - 1], mpre[i])# to calculate area under PR curve, look for points# where X axis (recall) changes valuei = np.where(mrec[1:] != mrec[:-1])[0]# and sum (\Delta recall) * precap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1])return apdef parse_rec(filename):""" Parse a PASCAL VOC xml file """tree = ET.parse(filename)objects = []for obj in tree.findall('object'):obj_struct = {}obj_struct['name'] = obj.find('name').textobj_struct['pose'] = obj.find('pose').textobj_struct['truncated'] = int(obj.find('truncated').text)obj_struct['difficult'] = int(obj.find('difficult').text)bbox = obj.find('bndbox')obj_struct['bbox'] = [int(bbox.find('xmin').text),int(bbox.find('ymin').text),int(bbox.find('xmax').text),int(bbox.find('ymax').text)]objects.append(obj_struct)return objectsdef voc_eval(detpath,annopath,imagesetfile,classname,ovthresh=0.5,use_07_metric=False):"""rec, prec, ap = voc_eval(detpath,annopath,imagesetfile,classname,[ovthresh],[use_07_metric])Top level function that does the PASCAL VOC evaluation.detpath: Path to detectionsdetpath.format(classname) should produce the detection results file.annopath: Path to annotationsannopath.format(imagename) should be the xml annotations file.imagesetfile: Text file containing the list of images, one image per line.classname: Category name (duh)cachedir: Directory for caching the annotations[ovthresh]: Overlap threshold (default = 0.5)[use_07_metric]: Whether to use VOC07's 11 point AP computation(default False)"""# assumes detections are in detpath.format(classname)# assumes annotations are in annopath.format(imagename)# assumes imagesetfile is a text file with each line an image name# cachedir caches the annotations in a pickle file# read list of imageswith open(imagesetfile, 'r') as f:lines = f.readlines()imagenames = [x.strip() for x in lines]# load annotsrecs = {}for i, imagename in enumerate(imagenames):recs[imagename] = parse_rec(annopath.format(imagename))# if i % 100 == 0:# print('Reading annotation for {:d}/{:d}'.format(# i + 1, len(imagenames)))# extract gt objects for this classclass_recs = {}npos = 0for imagename in imagenames:R = [obj for obj in recs[imagename] if obj['name'] == classname]bbox = np.array([x['bbox'] for x in R])difficult = np.array([x['difficult'] for x in R]).astype(np.bool)det = [False] * len(R)npos = npos + sum(~difficult)class_recs[imagename] = {'bbox': bbox,'difficult': difficult,'det': det}# read detsdetfile = detpath.format(classname)with open(detfile, 'r') as f:lines = f.readlines()splitlines = [x.strip().split(' ') for x in lines]image_ids = [x[0] for x in splitlines]confidence = np.array([float(x[1]) for x in splitlines])BB = np.array([[float(z) for z in x[2:]] for x in splitlines])# sort by confidencesorted_ind = np.argsort(-confidence)sorted_scores = np.sort(-confidence)BB = BB[sorted_ind, :]image_ids = [image_ids[x] for x in sorted_ind]# go down dets and mark TPs and FPsnd = len(image_ids)tp = np.zeros(nd)fp = np.zeros(nd)for d in range(nd):R = class_recs[image_ids[d]]bb = BB[d, :].astype(float)ovmax = -np.infBBGT = R['bbox'].astype(float)if BBGT.size > 0:# compute overlaps# intersectionixmin = np.maximum(BBGT[:, 0], bb[0])iymin = np.maximum(BBGT[:, 1], bb[1])ixmax = np.minimum(BBGT[:, 2], bb[2])iymax = np.minimum(BBGT[:, 3], bb[3])iw = np.maximum(ixmax - ixmin + 1., 0.)ih = np.maximum(iymax - iymin + 1., 0.)inters = iw * ih# unionuni = ((bb[2] - bb[0] + 1.) * (bb[3] - bb[1] + 1.) +(BBGT[:, 2] - BBGT[:, 0] + 1.) *(BBGT[:, 3] - BBGT[:, 1] + 1.) - inters)overlaps = inters / uniovmax = np.max(overlaps)jmax = np.argmax(overlaps)if ovmax > ovthresh:if not R['difficult'][jmax]:if not R['det'][jmax]:tp[d] = 1.R['det'][jmax] = 1else:fp[d] = 1.else:fp[d] = 1.# compute precision recallfp = np.cumsum(fp)tp = np.cumsum(tp)rec = tp / float(npos)# avoid divide by zero in case the first detection matches a difficult# ground truthprec = tp / np.maximum(tp + fp, np.finfo(np.float64).eps)ap = voc_ap(rec, prec, use_07_metric)return rec, prec, ap
test.py
# 路徑: ./test.py
from dataset.data import VOC0712Dataset, Compose, ToTensor, Resize
from dataset.draw_bbox import draw
from model.yolo import yolo, output_process
from voc_eval import voc_evalimport torchfrom tqdm import tqdm
import jsonclass CFG:device = 'cuda:0' if torch.cuda.is_available() else 'cpu'root = r'F:\AI\Dataset\VOC2007\VOCdevkit\VOC2007'class_path = r'dataset/classes.json'model_path = r'log/ex7/yolo.pth'# 這里也要手動新建det文件夾,用于保存每個class的目標情況detpath = r'det\{}.txt'annopath = r'F:\AI\Dataset\VOC2007\VOCdevkit\VOC2007\Annotations\{}.xml'imagesetfile = r'F:\AI\Dataset\VOC2007\VOCdevkit\VOC2007\ImageSets\Main\test.txt'classname = Nonetest_range = Noneshow_image = Falseget_ap = Truebackbone = 'resnet'S = 7B = 2image_size = 448get_info = Truetransforms = Compose([ToTensor(),Resize(448, keep_ratio=False)])num_classes = 0conf_th = 0.2iou_th = 0.5def test():device = torch.device(CFG.device)print('Test:\nDevice:{}'.format(device))dataset = VOC0712Dataset(CFG.root, CFG.class_path, CFG.transforms, 'test',data_range=CFG.test_range, get_info=CFG.get_info)with open(CFG.class_path, 'r') as f:json_str = f.read()classes = json.loads(json_str)CFG.classname = list(classes.keys())CFG.num_classes = len(CFG.classname)yolo_net = yolo(s=CFG.S, cell_out_ch=CFG.B * 5 + CFG.num_classes, backbone_name=CFG.backbone)yolo_net.to(device)yolo_net.load_state_dict(torch.load(CFG.model_path))bboxes = []with torch.no_grad():for image, label, image_name, input_size in tqdm(dataset):image = image.unsqueeze(dim=0)image = image.to(device)output = yolo_net(image)output = output_process(output.cpu(), CFG.image_size, CFG.S, CFG.B, CFG.conf_th, CFG.iou_th)if CFG.show_image:draw(image.squeeze(dim=0), output.squeeze(dim=0), classes, show_conf=True)draw(image.squeeze(dim=0), label, classes, show_conf=True)# 還原output[:, :, [0, 2]] = output[:, :, [0, 2]] * input_size[0] / CFG.image_sizeoutput[:, :, [1, 3]] = output[:, :, [1, 3]] * input_size[1] / CFG.image_sizeoutput = output.squeeze(dim=0).numpy().tolist()if len(output) > 0:pred = [[image_name, output[i][-3] * output[i][-2]] + output[i][:4] + [int(output[i][-1])]for i in range(len(output))]bboxes += preddet_list = [[] for _ in range(CFG.num_classes)]for b in bboxes:det_list[b[-1]].append(b[:-1])if CFG.get_ap:map = 0for idx in range(CFG.num_classes):file_path = CFG.detpath.format(CFG.classname[idx])txt = '\n'.join([' '.join([str(i) for i in item]) for item in det_list[idx]])with open(file_path, 'w') as f:f.write(txt)rec, prec, ap = voc_eval(CFG.detpath, CFG.annopath, CFG.imagesetfile, CFG.classname[idx])print(rec)print(prec)map += apprint(ap)map /= CFG.num_classesprint('mAP', map)if __name__ == '__main__':test()