0
雷鋒網(wǎng) AI 研習(xí)社按:本文為知乎主兔子老大為雷鋒網(wǎng) AI 研習(xí)社撰寫的獨家稿件。
YOLO全稱You Only Look Once,是一個十分容易構(gòu)造目標檢測算法,出自于CVPR2016關(guān)于目標檢測的方向的一篇優(yōu)秀論文(https://arxiv.org/abs/1506.02640 ),本文會對YOLO的思路進行總結(jié)并給出關(guān)鍵代碼的分析,在介紹YOLO前,不妨先看看其所在的領(lǐng)域的發(fā)展歷程。
相對于傳統(tǒng)的分類問題,目標檢測顯然更符合現(xiàn)實需求,因為往往現(xiàn)實中不可能在某一個場景只有一個物體(業(yè)務(wù)需求也很少會只要求分辨這是什么),但也因此目標檢測的需求變得更為復(fù)雜,不僅僅要求detector能夠檢驗出是什么物體,還的確定這個物體在圖片哪里。
總的來說,目標檢測先是經(jīng)歷了最為簡單而又暴力的歷程,這個過程高度的符合人類的直覺。簡單點來說,既然要我要識別出目標在哪里,那我就將圖片劃分成一個個一個個小圖片扔進detector,但detecror認為某樣物體在這個小區(qū)域 上了,OK,那我們就認為這個物體在這個小圖片上了。而這個思路,正是比較早期的目標檢測思路,比如R-CNN。
然后來的Fast R-CNN,F(xiàn)aster R-CNN雖有改進,比如不再是將圖片一塊塊的傳進CNN提取特征,而是整體放進CNN提取除 featuremap 然后再做進一步處理,但依舊是整體流程分為區(qū)域提取 和 目標分類 兩部分(two-stage),這樣做的一個特點是雖然精度是保證了,但速度上不去,于是以YOLO為主要代表的這種一步到位(one-stage)即 End To End 的目標算法應(yīng)運而生了。
細心的讀者可能已經(jīng)發(fā)現(xiàn),是的,YOLO的名字You only look once正是自身特點的高度概括。
YOLO的核心思想在于將目標檢測作為回歸問題解決 ,YOLO首先將圖片劃分成SxS個區(qū)域,注意這個區(qū)域的概念不同于上文提及將圖片劃分成N個區(qū)域扔進detector這里的區(qū)域不同。上文提及的區(qū)域是真的將圖片進行剪裁,或者說把圖片的某個局部的像素扔進detector,而這里的劃分區(qū)域,只的是邏輯上的劃分。
為什么是邏輯上的劃分呢?這體現(xiàn)再YOLO最后一層全連接層上,也就是YOLO針對每一幅圖片做出的預(yù)測。
其預(yù)測的向量是SxSx(B*5+C)長度的向量。其中S是劃分的格子數(shù),一般S=7,B是每個格子預(yù)測的邊框數(shù) ,一般B=2,C是跟你實際問題相關(guān)的類別數(shù),但要注意的是這里你應(yīng)該背景當作一個類別考慮進去。
不難得出,這個預(yù)測向量包括:
SxSxC 個類別信息,表示每一個格子可能屬于什么類別
SxSxB 個置信度,表示每一個格子的B個框的置信度,再YOLO進行預(yù)測后,一般只保留置信度為0.5以上的框。當然這個閾值也可以人工調(diào)整。
SxSxBx4 個位置信息,4個位置信息分別是xywh,其中xy為box的中心點。
說完YOLO的總體思路后,我們在看看YOLO的網(wǎng)絡(luò)結(jié)構(gòu)
該網(wǎng)絡(luò)結(jié)構(gòu)包括 24 個卷積層,最后接 2 個全連接層。文章設(shè)計的網(wǎng)絡(luò)借鑒 GoogleNet 的思想,在每個 1x1 的 歸約層(Reduction layer,1x1的卷積 )之后再接一個 3?3 的卷積層的結(jié)構(gòu)替代 Inception結(jié)構(gòu)。論文中還提到了 fast 版本的 Yolo,只有 9 個卷積層,其他則保持一致。
因為最后使用了全連接層,預(yù)測圖片要和train的圖片大小一致,而其他one-stage的算法,比如SSD,或者YOLO-V2則沒有這個問題,但這個不在本文討論范圍內(nèi)。
其實網(wǎng)絡(luò)架構(gòu)總體保持一致即可,個人不建議照抄全部參數(shù),還是需要根據(jù)你的實際任務(wù)或計算資源進行魔改,所以接下來重點會講述訓(xùn)練的過程和損失函數(shù)的構(gòu)建,其中也會給出MXNET版本的代碼進行解釋。文末會給出全部代碼的開源地址。
圖片來源于網(wǎng)絡(luò)
大體來說,損失函數(shù)分別由:
預(yù)測框位置的誤差 (1)(2)
IOU誤差(3)(4)
類別誤差(5)
其中,每一個組成部分對整體的貢獻度的誤差是不同的,需要乘上一個權(quán)重進行調(diào)和。相對來說,目標檢測的任務(wù)其實更在意位置誤差,故位置誤差的權(quán)重一般為5。在此,讀者可能費解,為什么框的寬和高取的是根號,而非直接計算?
想要了解這個問題,我們不妨來看看的圖像
這里額外多說一句,如果有打數(shù)據(jù)挖掘比賽經(jīng)驗的同學(xué),可能會比較清楚一種數(shù)據(jù)處理的手段,當某些時候,會對某一特征進行數(shù)據(jù)變換,比如和
,取 log 這些變換有一個特征,就是數(shù)值越大,懲罰越大(變換的幅度越大,比如4和4的平方,10到10的平方)。
而在損失函數(shù)中應(yīng)用這一方法,起到的作用則是使得小框產(chǎn)生的誤差比大框的誤差更為敏感,其目的是為了解決對小物體的檢測問題。但事實上,這樣的設(shè)定只能緩解卻沒有最終解決這個問題。
在說IOU誤差,IOU的定義為實際描框和預(yù)測框之間的交集除以兩者之間的并集。
聽上去很復(fù)雜,實際我們既然能獲得預(yù)測框坐標,只要通過簡單的換算,該比值實際能轉(zhuǎn)換成面積的計算。而同樣的,這里也有一個問題,我們感興趣的物體,對于整體圖片來說,畢竟屬于小數(shù)。換言之,就是在SxS個格子里面,預(yù)測出來的框大多是無效的框,這些無效框的誤差積累是會對損失函數(shù)產(chǎn)生影響,換句話說,我們只希望有物體的預(yù)測框有多準,而不在乎沒有物體的框預(yù)測得有多差。因此,我們也需要對這些無效框的在損失函數(shù)上得貢獻乘上一個權(quán)重,進行調(diào)整。
也就是λnoobj,該值一般取0.5。
關(guān)于分類誤差,論文雖然是采用mse來衡量,但是否采用交叉熵來衡量更合理呢?對于分類問題,采用mse和交叉熵來衡量,又會產(chǎn)生什么問題?這個問題留給讀者思考。
代碼實現(xiàn)
說完了損失函數(shù),下面來講述如何使用MXNET來實現(xiàn)YOLO,同理的,YOLO的網(wǎng)絡(luò)結(jié)構(gòu)較為簡單,你可以采用任何的框架搭出,如果像我一樣只是為了演示demo,對網(wǎng)絡(luò)結(jié)構(gòu)可以修改一下,采取網(wǎng)絡(luò)拓撲上比較簡單的模型。
同樣的,目標檢測常使用在ImageNet上預(yù)訓(xùn)練(pretrain)的模型 作為特征抽取器,同樣,因為這里只是演示demo,同樣也省略這一部分,只是重點講損失函數(shù)的構(gòu)造。
首先,雖然損失函數(shù)雖然是邏輯上分成三個部分,但我們不打算分開三個部分計算。
而是將整體式子拆分成 W x loss來計算,這樣在代碼上,實現(xiàn)起來要方便得多,以下時loss函數(shù)的計算過程:
def hybrid_forward(self, F, ypre, label):
label_pre, preds_pre, location_pre =self._split_y(ypre)
label_real, preds_real, location_real=self._split_y(label)
batch_size =len(label_real)
loss =nd.square(ypre - label)
class_weight=nd.ones(
shape =(batch_size, self.s*self.s*self.c))*self._scale_class_prob
location_weight = nd.ones(shape = (batch_size, self.s *self.s *self.b, 4))
confs =self._calculate_preds_loss(preds_pre,preds_real, location_pre, location_real)
preds_weight =self._scale_noobject_conf * (1. - confs) +self._scale_object_conf * confs
location_weight = (nd.expand_dims(preds_weight, axis=2) * location_weight) *self._scale_coordinate
location_weight = nd.reshape(location_weight, (-1, self.s *self.s *self.b *4))
W =nd.concat(*[class_weight, preds_weight, location_weight], dim=1)
total_loss = nd.sum(loss * W, 1)
return total_loss
可能會有童鞋好奇,為什么坐標誤差是用預(yù)測值和label直接mse算呢,w和h不是應(yīng)該要開根號嗎?是的,但我們?yōu)榱藬?shù)值穩(wěn)定,在人工構(gòu)建label時就已經(jīng)將wh以開根后的形式存儲好了,這是因為,神經(jīng)網(wǎng)絡(luò)的輸出在初始時,正負值時隨機的,盡管在數(shù)學(xué)上的結(jié)果是虛數(shù)i,但在DL相關(guān)的框架,該操作會直接造成nan,造成損失函數(shù)無法優(yōu)化,而且相應(yīng)代碼的書寫更為復(fù)雜。而采取直接以取根號后的形式我們只要在獲取輸出時,再將wh求一個平方即可。
另外要說的一點就是IOU誤差,雖然很多文章都將這一點直接成為IOU誤差,實際上計算時IOU誤差和置信度的結(jié)合。
def_iou(self, box, box_label):
wh = box[:, :, :, 2:4]
wh = nd.power(wh, 2)
center = box[:, :, :, 0:1]
predict_areas = wh[:, :, :, 0] * wh[:, :, :, 1]
predict_bottom_right = center +0.5* wh
predict_top_left =center -0.5* wh
wh = box_label[:, :, :, 2:4]
wh = nd.power(wh, 2)
center = box_label[:, :, :, 0:1]
label_areas = wh[:, :, :, 0] * wh[:, :, :, 1]
label_bottom_right = center +0.5* wh
label_top_left = center -0.5* wh
temp = nd.concat(*[predict_top_left[:, :, :, 0:1], label_top_left[:, :, :, 0:1]], dim=3)
temp_max1 = nd.max(temp, axis=3)
temp_max1 = nd.expand_dims(temp_max1, axis=3)
temp = nd.concat(*[predict_top_left[:, :, :, 1:], label_top_left[:, :, :, 1:]], dim=3)
temp_max2 = nd.max(temp, axis=3)
temp_max2 = nd.expand_dims(temp_max2, axis=3)
intersect_top_left = nd.concat(*[temp_max1, temp_max2], dim=3)
temp = nd.concat(*[predict_bottom_right[:, :, :, 0:1], label_bottom_right[:, :, :, 0:1]], dim=3)
temp_min1 = nd.min(temp, axis=3)
temp_min1 = nd.expand_dims(temp_min1, axis=3)
temp = nd.concat(*[predict_bottom_right[:, :, :, 1:], label_bottom_right[:, :, :, 1:]], dim=3)
temp_min2 = nd.min(temp, axis=3)
temp_min2 = nd.expand_dims(temp_min2, axis=3)
intersect_bottom_right = nd.concat(*[temp_min1, temp_min2], dim=3)
intersect_wh = intersect_bottom_right - intersect_top_left
intersect_wh = nd.relu(intersect_wh) # 把0過濾了
intersect = intersect_wh[:, :, :, 0] * intersect_wh[:, :, :, 1]
ious = intersect / (predict_areas + label_areas - intersect)
max_iou = nd.expand_dims(nd.max(ious,2),axis=2)
best_ = nd.equal(max_iou,ious)
best_boat = nd.ones(shape = ious.shape)
for batch in range(len(best_)):
best_box[batch] = best_[batch]
return nd.reshape(best_box, shape=(-1, self.s*self.s*self.b))
def_calculate_preds_loss(self, ypre, label, local_pre, local_label):
ious =self._iou(local_pre, local_label)
conf = label * ious
returnconf
置信度在label的表現(xiàn)形式時,這個地方有目標物體則為1,沒有則是0,這樣用mse優(yōu)化后,輸出值會在0~1附近,正好可以代表某個框是否框中物體的置信度。
但為什么不直接 對置信度用mse呢,這同樣是一個權(quán)重的調(diào)節(jié)的問題,但這里不能說我們就不care那些沒有物體的框的值了,因為這里的值是置信度,如果我們?nèi)斡善浒l(fā)展,萬一沒有物體的框的置信度比有框中物體的置信度還要高,那我們使用閾值過濾時,就可能出現(xiàn)問題了。只能說我們希望loss中更重視有框中物體的框的誤差。
這里補充另外一個知識點最大值抑制 ,簡單點來說,既然每個格子會生成B個框(一般B>1)這樣就有可能同時兩個框都框中了物體,那么到底采用那個框作為預(yù)測結(jié)果呢?答案是采用IOU值高的那個框,而IOU值小的,就會不被重視而受到抑制。
ious = intersect / (predict_areas + label_areas -intersect)
max_iou = nd.expand_dims(nd.max(ious,2),axis=2)
best_ = nd.equal(max_iou,ious)
best_boat =nd.ones(shape = ious.shape)
for batch in range(len(best_)):
best_box[batch] = best_[batch]
return nd.reshape(best_box, shape=(-1, self.s*self.s*self.b))
在求出IOU后,我們求出每一個格子的框中的最大值,再使用equal操作,使得最大值為1,其余值為0再參與后面的運算即可。
DEMO-github:(https://github.com/MashiMaroLjc/YOLO)
代碼使用的是李沐公開課的皮卡丘數(shù)據(jù)集,用MXNET的Gluon接口實現(xiàn),enjoy it!
運行效果:
上述文章若有不正確的地方,敬請指正。
雷峰網(wǎng)特約稿件,未經(jīng)授權(quán)禁止轉(zhuǎn)載。詳情見轉(zhuǎn)載須知。