yolov--15--史上最詳細的Yolov3網絡邊框預測分析

Yolov-1-TX2上用YOLOv3訓練自己數據集的流程(VOC2007-TX2-GPU)

Yolov--2--一文全面瞭解深度學習性能優化加速引擎---TensorRT

Yolov--3--TensorRT中yolov3性能優化加速(基於caffe)

yolov-5-目標檢測:YOLOv2算法原理詳解

yolov--8--Tensorflow實現YOLO v3

yolov--9--YOLO v3的剪枝優化

yolov--10--目標檢測模型的參數評估指標詳解、概念解析

yolov--11--YOLO v3的原版訓練記錄、mAP、AP、recall、precision、time等評價指標計算

yolov--12--YOLOv3的原理深度剖析和關鍵點講解


我們讀yolov3論文時都知道邊框預測的公式,然而難以準確理解爲何作者要這麼做,這裏我就獻醜來總結解釋一下個人的見解,總結串聯一下學習時容易遇到的疑惑,期待對大家有所幫助,理解錯誤的地方還請大家批評指正,我只是個小白哦,發出來也是爲了與大家多多交流,看看理解的對不對。

論文中邊框預測公式如下:

其中,Cx,Cy是feature map中grid cell的左上角座標,在yolov3中每個grid cell在feature map中的寬和高均爲1。如下圖1的情形時,這個bbox邊界框的中心屬於第二行第二列的grid 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#inp_dim是需要resize的尺寸(如416*416)
    # 取min(w/img_w, h/img_h)這個比例來縮放,縮放後的尺寸爲new_w, new_h,即保證較長的邊縮放後正好等於目標長度(需要的尺寸),另一邊的尺寸縮放後還沒有填充滿.
    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) #將圖片按照縱橫比不變來縮放爲new_w x new_h,768 x 576的圖片縮放成416x312.,用了雙三次插值
    # 創建一個畫布, 將resized_image數據拷貝到畫布中心。
    canvas = np.full((inp_dim[1], inp_dim[0], 3), 128)#生成一個我們最終需要的圖片尺寸hxwx3的array,這裏生成416x416x3的array,每個元素值爲128
    # 將wxhx3的array中對應new_wxnew_hx3的部分(這兩個部分的中心應該對齊)賦值爲剛剛由原圖縮放得到的數組,得到最終縮放後圖片
    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網上有一大堆教程,也可參考我的文章:將實例分割數據集轉爲目標檢測數據集,這裏不再贅述。

這裏解釋一下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是怎麼求出來的呢?對於訓練樣本,faster-rcnn系列文章裏需要用到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)。

  • 第一, 9個anchor會被三個輸出張量平分的。根據大中小三種size各自取自己的anchor。
  • 第二,每個輸出y在每個自己的網格都會輸出3個預測框,這3個框是9除以3得到的,這是作者設置的,我們可以從輸出張量的維度來看,13x13x255。255是怎麼來的呢,3*(5+80)。80表示80個種類,5表示位置信息和置信度,3表示要輸出3個prediction。在代碼上來看,3*(5+80)中的3是直接由num_anchors/3得到的。
  • 第三,作者使用了logistic迴歸來對每個anchor包圍的內容進行了一個目標性評分(objectness score)。

有了平移(tx,ty)和尺度縮放(tw,th)才能讓anchor box經過微調與grand truth重合。如圖3,紅色框爲anchor box,綠色框爲Ground Truth,平移+尺度縮放可實線紅色框先平移到虛線紅色框,然後再縮放到綠色框。

邊框迴歸最簡單的想法就是通過平移加尺度縮放進行微調嘛。

邊框迴歸爲何只能微調?

當輸入的 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這個縮放比例。

#scaling_factor*img_w和scaling_factor*img_h是圖片按照縱橫比不變進行縮放後的圖片,即原圖是768x576按照縱橫比長邊不變縮放到了416*372。
#經座標換算,得到的座標還是在輸入網絡的圖片(416x416)座標系下的絕對座標,但是此時已經是相對於416*372這個區域的座標了,而不再相對於(0,0)原點。
output[:,[1,3]] -= (inp_dim - scaling_factor*im_dim_list[:,0].view(-1,1))/2#x1=x1−(416−scaling_factor*img_w)/2,x2=x2-(416−scaling_factor*img_w)/2
output[:,[2,4]] -= (inp_dim - scaling_factor*im_dim_list[:,1].view(-1,1))/2#y1=y1-(416−scaling_factor*img_h)/2,y2=y2-(416−scaling_factor*img_h)/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;
	// 此處new_w表示輸入圖片經壓縮後在網絡輸入大小的letter_box中的width,new_h表示在letter_box中的height,
	// 以1280*720的輸入圖片爲例,在進行letter_box的過程中,原圖經resize後的width爲416, 那麼resize後的對應height爲720*416/1280,
	//所以height爲234,而超過234的上下空餘部分在作爲網絡輸入之前填充了128,new_h=234
    int new_w=0;
    int new_h=0;
	// 如果w>h說明resize的時候是以width/圖像的width爲resize比例的,先得到中間圖的width,再根據比例得到height
    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;
		// 此處的公式很不好理解還是接着上面的例子,現有new_w=416,new_h=234,因爲resize是以w爲長邊壓縮的
		// 所以x相對於width的比例不變,而b.y表示y相對於圖像高度的比例,在進行這一步的轉化之前,b.y表示
		// 的是預測框的y座標相對於網絡height的比值,要轉化到相對於letter_box中圖像的height的比值時,需要先
		// 計算出y在letter_box中的相對座標,即(b.y - (neth - new_h)/2./neth),再除以比例
        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實際座標了,大功告成了!!!

最後的小插曲:解釋一下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座標的準確度。


若加微信請備註下姓名_公司/學校,相遇即緣分,感謝您的支持,願真誠交流,共同進步,謝謝~ 

 


借鑑:

https://blog.csdn.net/qq_34199326/article/details/84109828

https://zhuanlan.zhihu.com/p/49995236

https://blog.csdn.net/leviopku/article/details/82660381#commentBox

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章