我們讀yolov3論文時都知道邊框預測的公式,然而難以準確理解為何作者要這麼做,這裡我就獻醜來總結解釋一下個人的見解,總結串聯一下學習時容易遇到的疑惑,期待對大家有所幫助,理解錯誤的地方還請大家批評指正,我只是個小白哦,發出來也是為了與大家多多交流,看看理解的對不對。
論文中邊框預測公式如下:
其中,Cx,Cy是feature map中grid cell的左上角坐標,在yolov3中每個grid cell在feature map中的寬和高均為1。如下圖1的情形時,這個bbox邊界框的中心屬於第二行第二列的g rid cell,它的左上角坐標為(1,1),故Cx=1,Cy=1.公式中的Pw、Ph是預設的anchor box映射到feature map中的寬和高(anchor box原本設定是相對於416*416坐標系下的坐標,在yolov3.cfg文件中寫明了,代碼中是把cfg中讀取的坐標除以stride如32映射到feature map坐標系中)。
最終得到的邊框坐標值是bx,by,bw,bh即邊界框bbox相對於feature map的位置和大小,是我們需要的預測輸出坐標。但我們網絡實際上的學習目標是tx,ty,tw,th這4個offsets,其中tx,ty是預測的坐標偏移值,tw,th是尺度縮放,有了這4個offsets,自然可以根據之前的公式去求得真正需要的bx,by,bw,bh4個坐標。至於為何不直接學習bx,by,bw,bh呢?因為YOLO 的輸出是一個卷積特徵圖,包含沿特徵圖深度的邊界框屬性。邊界框屬性由彼此堆疊的單元格預測得出。因此,如果你需要在 (5,6) 處訪問該單元格的第二個邊框bbox,那麼你需要通過 map[5,6, (5+C): 2*(5+C)] 將其編入索引。這種格式對於輸出處理過程(例如通過目標置信度進行閾值處理、添加對中心的網格偏移、應用錨點等)很不方便,因此我們求偏移量即可。那麼這樣就只需要求偏移量,也就可以用上面的公式求出bx,by,bw,bh,反正是等價的。另外,通過學習偏移量,就可以通過網絡原始給定的anchor box坐標經過線性回歸微調(平移加尺度縮放)去逐漸靠近groundtruth.為何微調可看做線性回歸看後文。
這裡需要注意的是,雖然輸入尺寸是416*416,但原圖是按照縱橫比例縮放至416*416的, 取 min(w/img_w, h/img_h)這個比例來縮放,保證長的邊縮放為需要的輸入尺寸416,而短邊按比例縮放不會扭曲,img_w,img_h是原圖尺寸768,576, 縮放後的尺寸為new_w, new_h=416,312,需要的輸入尺寸是w,h=416*416.如圖2所示:
剩下的灰色區域用(128,128,128)填充即可構造為416*416。不管訓練還是測試時都需要這樣操作原圖。pytorch代碼中比較好理解這一點。下面這個函數實現了對原圖的變換。
def letterbox_image(img, inp_dim): """ lteerbox_image()將圖片按照縱橫比進行縮放,將空白部分用(128,128,128)填充,調整圖像尺寸 具體而言,此時某個邊正好可以等於目標長度,另一邊小於等於目標長度 將縮放後的數據拷貝到畫布中心,返回完成縮放 """ img_w, img_h = img.shape[1], img.shape[0] w, h = inp_dim new_w = int(img_w * min(w/img_w, h/img_h)) new_h = int(img_h * min(w/img_w, h/img_h)) resized_image = cv2.resize(img, (new_w,new_h), interpolation = cv2.INTER_CUBIC) canvas = np.full((inp_dim[1], inp_dim[0], 3), 128) canvas[(h-new_h)//2:(h-new_h)//2 + new_h,(w-new_w)//2:(w-new_w)//2 + new_w, :] = resized_image return canvas而且我們注意yolov3需要的訓練數據的label是根據原圖尺寸歸一化了的,這樣做是因為怕大的邊框的影響比小的邊框影響大,因此做了歸一化的操作,這樣大的和小的邊框都會被同等看待了,而且訓練也容易收斂。既然label是根據原圖的尺寸歸一化了的,自己製作數據集時也需要歸一化才行,如何轉為yolov3需要的label網上有一大堆教程,也可參考我的文章:將實例分割數據集轉為目標檢測數據集
https://zhuanlan.zhihu.com/p/49979730,這裡不再贅述。這裡解釋一下anchor box,YOLO3為每種FPN預測特徵圖(13*13,26*26,52*52)設定3種anchor box,總共聚類出9種尺寸的anchor box。在COCO數據集這9個anchor box是:(10x13),(16x30),(33x23),(30x61),(62x45),(59x119),(116x90),(156x198),(373x326)。分配上,在最小的13*13特徵圖上由於其感受野最大故應用最大的anchor box (116x90),(156x198),(373x326),(這幾個坐標是針對416*416下的,當然要除以32把尺度縮放到13*13下),適合檢測較大的目標。中等的26*26特徵圖上由於其具有中等感受野故應用中等的anchor box (30x61),(62x45),(59x119),適合檢測中等大小的目標。較大的52*52特徵圖上由於其具有較小的感受野故應用最小的anchor box(10x13),(16x30),(33x23),適合檢測較小的目標。同Faster-Rcnn一樣,特徵圖的每個像素(即每個grid)都會有對應的三個anchor box,如13*13特徵圖的每個grid都有三個anchor box (116x90),(156x198),(373x326)(這幾個坐標需除以32縮放尺寸)
那麼4個坐標tx,ty,tw,th是怎麼求出來的呢?對於訓練樣本,在大多數文章裡需要用到ground truth的真實框來求這4個坐標:
上面這個公式是faster-rcnn系列文章用到的公式,Px,Py在faster-rcnn系列文章是預設的anchor box在feature map上的中心點坐標。Pw、Ph是預設的anchor box的在feature map上的寬和高。至於Gx、Gy、Gw、Gh自然就是ground truth在這個feature map的4個坐標了(其實上面已經描述了這個過程,要根據原圖坐標系先根據原圖縱橫比不變映射為416*416坐標下的一個子區域如416*312,取 min(w/img_w, h/img_h)這個比例來縮放成416*312,再填充為416*416,坐標變換上只需要讓ground truth在416*312下的y1,y2(即左上角和右下角縱坐標)加上圖2灰色部分的一半,y1=y1+(416-416/768*576)/2=y1+(416-312)/2,y2同樣的操作,把x1,x2,y1,y2的坐標系的換算從針對實際紅框的坐標系(416*312)變為416*416下了,這樣保證bbox不會扭曲,然後除以stride得到相對於feature map的坐標)。
用x,y坐標減去anchor box的x,y坐標得到偏移量好理解,為何要除以feature map上anchor box的寬和高呢?我認為可能是為了把絕對尺度變為相對尺度,畢竟作為偏移量,不能太大了對吧。而且不同尺度的anchor box如果都用Gx-Px來衡量顯然不對,有的anchor box大有的卻很小,都用Gx-Px會導致不同尺度的anchor box權重相同,而大的anchor box肯定更能容忍大點的偏移量,小的anchor box對小偏移都很敏感,故除以寬和高可以權衡不同尺度下的預測坐標偏移量。
但是在yolov3中與faster-rcnn系列文章用到的公式在前兩行是不同的,yolov3裡Px和Py就換為了feature map上的grid cell左上角坐標Cx,Cy了,即在yolov3裡是Gx,Gy減去grid cell左上角坐標Cx,Cy。x,y坐標並沒有針對anchon box求偏移量,所以並不需要除以Pw,Ph。
也就是說是tx = Gx - Cx
ty = Gy - Cy
這樣就可以直接求bbox中心距離grid cell左上角的坐標的偏移量。
tw和th的公式yolov3和faster-rcnn系列是一樣的,是物體所在邊框的長寬和anchor box長寬之間的比率,不管Faster-RCNN還是YOLO,都不是直接回歸bounding box的長寬而是**尺度縮放到對數空間,是怕訓練會帶來不穩定的梯度。**因為如果不做變換,直接預測相對形變tw和th,那麼要求tw,th>0,因為你的框的寬高不可能是負數。這樣,是在做一個有不等式條件約束的優化問題,沒法直接用SGD來做。所以先取一個對數變換,將其不等式約束去掉,就可以了。
這裡就有個重要的疑問了,一個尺度的feature map有三個anchors,那麼對於某個ground truth框,究竟是哪個anchor負責匹配它呢?和YOLOv1一樣,對於訓練圖片中的ground truth,若其中心點落在某個cell內,那麼該cell內的3個anchor box負責預測它,**具體是哪個anchor box預測它,需要在訓練中確定,即由那個與ground truth的IOU最大的anchor box預測它,而剩餘的2個anchor box不與該ground truth匹配。**YOLOv3需要假定每個cell至多含有一個grounth truth,而在實際上基本不會出現多於1個的情況。與ground truth匹配的anchor box計算坐標誤差、置信度誤差(此時target為1)以及分類誤差,而其它的anchor box只計算置信度誤差(此時target為0)。
有了平移(tx,ty)和尺度縮放(tw,th)才能讓anchor box經過微調與grand truth重合。如圖3,紅色框為anchor box,綠色框為Ground Truth,平移+尺度縮放可實線紅色框先平移到虛線紅色框,然後再縮放到綠色框。邊框回歸最簡單的想法就是通過平移加尺度縮放進行微調嘛。
圖3
邊框回歸為何只能微調?當輸入的 Proposal 與 Ground Truth 相差較小時,,即IOU很大時(RCNN 設置的是 IoU>0.6), 可以認為這種變換是一種線性變換, 那麼我們就可以用線性回歸(線性回歸就是給定輸入的特徵向量 X, 學習一組參數 W, 使得經過線性回歸後的值跟真實值 Y(Ground Truth)非常接近. 即Y≈WX )來建模對窗口進行微調, 否則會導致訓練的回歸模型不work(當 Proposal跟 GT 離得較遠,就是複雜的非線性問題了,此時用線性回歸建模顯然就不合理了)
那麼訓練時用的ground truth的4個坐標去做差值和比值得到tx,ty,tw,th,測試時就用預測的bbox就好了,公式修改就簡單了,把Gx和Gy改為預測的x,y,Gw、Gh改為預測的w,h即可。
網絡可以不斷學習tx,ty,tw,th偏移量和尺度縮放,預測時使用這4個offsets求得bx,by,bw,bh即可,那麼問題是:
這個公式tx,ty為何要sigmoid一下啊?前面講到了在yolov3中沒有讓Gx - Cx後除以Pw得到tx,而是直接Gx - Cx得到tx,這樣會有問題是導致tx比較大且很可能>1.(因為沒有除以Pw歸一化尺度)。用sigmoid將tx,ty壓縮到[0,1]區間內,可以有效的確保目標中心處於執行預測的網格單元中,防止偏移過多。舉個例子,我們剛剛都知道了網絡不會預測邊界框中心的確切坐標而是預測與預測目標的grid cell左上角相關的偏移tx,ty。如13*13的feature map中,某個目標的中心點預測為(0.4,0.7),它的cx,cy即中心落入的grid cell坐標是(6,6),則該物體的在feature map中的中心實際坐標顯然是(6.4,6.7).這種情況沒毛病,但若tx,ty大於1,比如(1.2,0.7)則該物體在feature map的的中心實際坐標是(7.2,6.7),注意這時候該物體中心在這個物體所屬grid cell外面了,但(6,6)這個grid cell卻檢測出我們這個單元格內含有目標的中心(yolo是採取物體中心歸哪個grid cell整個物體就歸哪個grid celll了),這樣就矛盾了,因為左上角為(6,6)的grid cell負責預測這個物體,這個物體中心必須出現在這個grid cell中而不能出現在它旁邊網格中,一旦tx,ty算出來大於1就會引起矛盾,因而必須歸一化。
看最後兩行公式,tw為何要指數呀,這就好理解了嘛,因為tw,th是log尺度縮放到對數空間了,當然要指數回來,而且這樣可以保證大於0。至於左邊乘以Pw或者Ph是因為tw=log(Gw/Pw)當然應該乘回來得到真正的寬高。
記feature map大小為W,H(如13*13),可將bbox相對於整張圖片的位置和大小計算出來(使4個值均處於[0,1]區間內)約束了bbox的位置預測值到[0,1]會使得模型更容易穩定訓練(如果不是[0,1]區間,yolo的每個bbox的維度都是85,前5個屬性是(Cx,Cy,w,h,confidence),後80個是類別概率,如果坐標不歸一化,和這些概率值一起訓練肯定不收斂啊)。
只需要把之前計算的bx,bw都除以W,把by,bh都除以H。即
box get_yolo_box(float *x, float *biases, int n, int index, int i, int j, int lw, int lh, int w, int h, int stride){ box b; b.x = (i + x[index + 0*stride]) / lw; // 此處相當於知道了X的index,要找Y的index,向後偏移l.w*l.h個索引 b.y = (j + x[index + 1*stride]) / lh; b.w = exp(x[index + 2*stride]) * biases[2*n] / w; b.h = exp(x[index + 3*stride]) * biases[2*n+1] / h; return b;}
float delta_yolo_box(box truth, float *x, float *biases, int n, int index, int i, int j, int lw, int lh, int w, int h, float *delta, float scale, int stride){ box pred = get_yolo_box(x, biases, n, index, i, j, lw, lh, w, h, stride); float iou = box_iou(pred, truth);
float tx = (truth.x*lw - i); float ty = (truth.y*lh - j); float tw = log(truth.w*w / biases[2*n]); float th = log(truth.h*h / biases[2*n + 1]); //scale = 2 - groundtruth.w * groundtruth.h delta[index + 0*stride] = scale * (tx - x[index + 0*stride]); delta[index + 1*stride] = scale * (ty - x[index + 1*stride]); delta[index + 2*stride] = scale * (tw - x[index + 2*stride]); delta[index + 3*stride] = scale * (th - x[index + 3*stride]); return iou;}上述兩個函數來自yolov3的darknet框架的src/yolo_layer.c代碼,其中函數參數float* x來自前一個卷積層的輸出。先來看函數get_region_box()的參數,biases中存儲的是預設的anchor box的寬和高,(lw,lh)是yolo層輸入的feature map寬高(13*13),(w,h)是整個網絡輸入圖尺度416*416,get_yolo_box()函數利用了論文中的公式,而且把結果分別利用feature map寬高和輸入圖寬高做了歸一化,這就對應了我剛剛談到的公式了(雖然b.w和b.h是除以416,但這是因為下面的函數中的tw和th用的是w,h=416,x,y都是針對feature map大小的)。注意這裡的truth.x並非訓練label的txt文件的原始歸一化後的坐標,而是經過修正後的(不僅考慮了按照原始圖片縱橫比坐標系(416*312)變為網絡輸入416*416坐標系下label的變化,也考慮了數據增強後label的變化)而且這個機制是用來限制回歸,避免預測很遠的目標,那麼這個預測範圍是多大呢?(b.x,b.y)最小是(i,j),最大是(i+1,x+1),即中心點在feature map上最多移動一個像素(假設輸入圖下採樣n得到feature map,feature map中一個像素對應輸入圖的n個像素)(b.w,b.h)最大是(2.7 * anchor.w,2.7*anchor.h),最小就是(anchor.w,anchor.h),這是在輸入圖尺寸下的值。第二個函數delta_yolo_box中詳細顯示了tx,ty,tw,th如何的得到的,驗證了之前的說法是基本正確的。
我們還可以注意到代碼中有個注釋scale = 2 - groundtruth.w * groundtruth.h,這是什麼含義?實際上,我們知道yolov1裡**作者在loss裡對寬高都做了開根號處理,是為了使得大小差別比較大的邊框差別減小。**因為對不同大小的bbox預測中,想比於大的bbox預測偏差,小bbox預測偏差相同的尺寸對IOU影響更大,而均方誤差對同樣的偏差loss一樣,為此取根號。例如,同樣將一個 100x100 的目標與一個 10x10 的目標都預測大了 10 個像素,預測框為 110 x 110 與 20 x 20。顯然第一種情況我們還可以接受,但第二種情況相當於把邊界框預測大了 1 倍,但如果不使用根號函數,那麼損失相同,但把寬高都增加根號時:
顯然加根號後對小框預測偏差10個像素帶來了更大的損失。而在yolov2和v3裡,損失函數進行了改進,不再簡單地加根號了,而是用scale = 2 - groundtruth.w * groundtruth.h加大對小框的損失。
得到除以了W,H後的bx,by,bw,bh,如果將這4個值分別乘以輸入網絡的圖片的寬和高(如416*416)就可以得到bbox相對於坐標系(416*416)位置和大小了。但還要將相對於輸入網絡圖片(416x416)的邊框屬性變換成原圖按照縱橫比不變進行縮放後的區域的坐標(416*312)。應該將方框的坐標轉換為相對於填充後的圖片中包含原始圖片區域的計算方式。
具體見下面pytorch的代碼,很詳細簡單地解釋了如何做到,代碼中scaling_factor = torch.min(416/im_dim_list,1)[0].view(-1,1) 即416/最長邊,得到scaling_factor這個縮放比例。output[:,[1,3]] -= (inp_dim - scaling_factor*im_dim_list[:,0].view(-1,1))/2output[:,[2,4]] -= (inp_dim - scaling_factor*im_dim_list[:,1].view(-1,1))/2其實代碼的含義就是把y1,y2減去圖2灰色部分的一半,y1=y1-(416-416/768*576)/2=y1-(416-312)/2,把x1,x2,y1,y2的坐標系換算到了針對實際紅框的坐標系(416*312)下了。這樣保證bbox不會扭曲,
在作者的darknet的c原始碼src/yolo_layer.c中也是類似處理的,void correct_yolo_boxes(detection *dets, int n, int w, int h, int netw, int neth, int relative){ int i; int new_w=0; int new_h=0; if (((float)netw/w) < ((float)neth/h)) { new_w = netw; new_h = (h * netw)/w; } else { new_h = neth; new_w = (w * neth)/h; } for (i = 0; i < n; ++i){ box b = dets[i].bbox; b.x = (b.x - (netw - new_w)/2./netw) / ((float)new_w/netw); b.y = (b.y - (neth - new_h)/2./neth) / ((float)new_h/neth); b.w *= (float)netw/new_w; b.h *= (float)neth/new_h; if(!relative){ b.x *= w; b.w *= w; b.y *= h; b.h *= h; } dets[i].bbox = b; }}既然得到了這個坐標,就可以除以scaling_factor 縮放至真正的測試圖片原圖大小尺寸下的bbox實際坐標了,大功告成了!!!
至此總結一下,我們得以知道,原來網絡中通過feature map學習到的位置信息是偏移量tx,ty,tw,th,就是在Yolo檢測層中,也就是最後的feture map,維度為(batch_size, num_anchors*bbox_attrs, grid_size, grid_size),對於每張圖就是(num_anchors*bbox_attrs, grid_size, grid_size)對於coco的80類,bbox_attrs就是80+5,5表示網絡中學習到的參數tx,ty,tw,th,以及是否有目標的score。也就是對於3層預測層,最深層是255*13*13,255是channel,物理意義表徵bbox_attrs×3,3是anchor個數。為了計算loss,輸出特徵圖需要變換為(batch_size, grid_size*grid_size*num_anchors, 5+類別數量)的tensor,這裡的5就已經是通過之前詳細闡述的邊框預測公式轉換完的結果,即bx,by,bw,bh.對於尺寸為416*416的圖像,通過三個檢測層檢測後,有[(52*52)+(26*26)+(13*13)]*3=10647個預測框,也就是維度為(batchsize,10647,85).然後可以轉為x1,y1,x2,y2來算iou,通過score濾去和執行nms去掉絕大多數多餘的框,計算loss等操作了。
最後的小插曲:解釋一下confidence是什麼,Pr(Object) ∗ IOU(pred&groundtruth)
如果某個grid cell無object則Pr(Object) =0,否則Pr(Object) =1,則此時的confidence=IOU,即預測的bbox和ground truth的IOU值作為置信度。因此這個confidence不僅反映了該grid cell是否含有物體,還預測這個bbox坐標預測的有多準。在預測階段,類別的概率為類別條件概率和confidence相乘:
Pr(Classi|Object) ∗ Pr(Object) ∗ IOU(pred&groundtruth) = Pr(Classi) ∗ IOU(pred&groundtruth)
這樣每個bbox具體類別的score就有了,乘積既包含了bbox中預測的class的概率又反映了bbox是否包含目標和bbox坐標的準確度。
這篇博客參考了幾篇優質博客,但記不清從哪些地方看到過,這裡感謝他們的付出,期待我的這篇博客能夠幫助大家。