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