YOLOv4損失函數全面解析

點擊上方「視學算法」,選擇「星標」公衆號node

精選做品,第一時間送達python

來源|周威@知乎,https://zhuanlan.zhihu.com/p/159209199nginx

本文已獲做者受權,不得二次轉載。

1.前言

若是您對YOLO V4的結構比較感興趣,建議您能夠結合代碼以及個人這篇文章進行消化。代碼是基於Keras版本的,結構很清晰,連接以下:git

YOLO V4 Keras:https:github.com/Ma-Dan/keras-yolo4github

YOLO V4相較於YOLO V3作了不少小創新,堪稱目標檢測tricks萬花筒。既然YOLO V4結構你們都懂了,根據網絡的輸出標籤信息進行損失函數的設定,實現網絡參數的更新,一個完整的YOLO V4模型訓練過程就能夠被複現了。web

YOLO V4原文中提到,在進行bounding box regression的時候,傳統的目標檢測模型(好比YOLO V3)等,都是直接根據預測框真實框中心點座標以及寬高信息設定MSE(均方偏差)損失函數的。爲了方便你們理解,下面給出了YOLO V3的總損失函數。算法

YOLO V3的損失函數

能夠看出,第一行的兩個,就是用在bounding box regression的損失函數MSE。有關該損失函數的具體解析能夠見我文章《YOLO V3 深度解析 (下)》(https://zhuanlan.zhihu.com/p/138857662),這裏就不進行贅述。微信

2. IOU損失函數理論部分

鑑於MSE存在的一些問題,好比原文中提到網絡

However, to directly estimate the coordinate values of each point of the BBox is to treat these points as independent variables, but in fact does not consider the integrity of the object itself.app

意思就是MSE損失函數將檢測框中心點座標寬高等信息做爲獨立的變量對待的,可是實際上他們之間是有關係的。從直觀上來講,框的中心點和寬高的確存在着必定的關係。因此解決方法是使用IOU損失代替MSE損失。

接着做者就IOU損失依次提到了如下的一些的損失函數。

  • (1)IOU損失
  • (2)GIOU損失
  • (3)DIOU損失
  • (4)CIOU損失

(1)IOU損失

其中IOU損失定義很是簡單,即1與預測框A和真實框B之間交併比的差值

可是這樣該損失函數會有一些問題,該損失函數只在bounding box重疊的時候才管用,在他們沒有重疊狀況下,將不會提供滑動梯度。(這句話摘自論文《Distance-IoU Loss: Faster and Better Learning for Bounding Box Regression》)

(2)GIOU損失

其實GIOU的全稱叫作 :generalized IoU loss。提出來是爲了緩解上述IOU損失在檢測框不重疊時出現的梯度問題。定義也是比較簡單的,就在在原來的IOU損失的基礎上加上一個懲罰項,公式以下:

上式中A是預測框,B是真實框,C是A和B的最小包圍框,A,B,C的關係具體以下圖所示。

A,B,C含義

那麼該懲罰項的意思就是下圖右邊黃色區域的比值。

懲罰項含義

雖然GIOU能夠解決檢測框非重疊形成的梯度消失問題,可是他還存在如下的限制,這裏咱們依舊是參考CIOU論文中的內容。

GIOU迴歸過程

上圖中綠色真實框黑色先驗框Anchor藍色預測框。預測框是以先驗框爲基礎進行位置移動和大小縮放的。能夠看出來,GIOU首先嚐試增大預測框的大小,使得它可以與真實框有所重疊(如上圖中間所示),而後才能進行上述公式中   的計算。那麼這樣作的話,會消耗大量的時間在預測框嘗試與真實框接觸上,這會影響損失的收斂速度。因此DIOU和GIOU的提出解決了上述GIOU的問題。

(3)DIOU

DIOU和CIOU都出自論文《Distance-IoU Loss: Faster and Better Learning for Bounding Box Regression》(https://link.zhihu.com/?target=https%3A//arxiv.org/abs/1911.08287)。做者說他直接在IOU損失的基礎上加了一個簡單的懲罰項,用來最小化兩個檢測框中心點的標準化距離, 這樣能夠加速損失的收斂過程。以下圖所示爲GIOU和DIOU的對比。

紅色框是DIOU損失中的預測框。能夠很明顯的看出,DIOU的收斂速度較GIOU更快。

那麼有關DIOU的定義是怎麼樣的呢?下面給出公式定義:

相比於IOU損失,DIOU損失也多出了一個懲罰項   。該懲罰項具體的參數含義爲

  • A : 預測框 B:真實框
  •  : 預測框 中心點座標   :真實框 中心點座標
  •  是 歐式距離的計算
  • c 爲 A , B  最小包圍框對角線長度

我給出了下圖,便於你們理解。

因此兩個框距離越遠,DIOU越接近2,距離越近,DIOU越接近0

提出DIOU還不夠,做者進一步地提出了CIOU(Complete IoU Loss)。

(4)CIOU

CIOU做者考慮的更加全面一些,DIOU考慮到了兩個檢測框的中心距離。而CIOU考慮到了三個幾何因素,分別爲

  • (1)重疊面積
  • (2)中心點距離
  • (3)長寬比

這裏仔細觀察,會發現,CIOU比DIOU多了一個長寬比的信息,那麼CIOU的公式定義以下:

那麼這個   對長寬比的懲罰項了。論文中提到,   是一個正數,   用來測量長寬比的一致性(v measures the consistency of aspect ratio)。具體定義以下:

上述公式中,參數說明以下:

  •  和   爲真實框的寬、高
  •  和   爲預測框的寬、高

若真實框和預測框的寬高類似,那麼   爲0,該懲罰項就不起做用了。因此很直觀地,這個懲罰項做用就是控制預測框的寬高可以儘量快速地與真實框的寬高接近。

那麼至此,有關YOLO V4損失函數的理論部分就說完了。

3.IOU損失函數的實戰部分

說完了上述四個IOU理論部分,咱們迴歸其在YOLO V4框架中的位置並進行解析。結合keras的代碼,以下爲CIOU損失函數的定義。

    
      
    
    
    
     
     
              
     
     

    
      
    
    
    
     
     
              
     
     
def bbox_ciou(boxes1, boxes2):'''    計算ciou = iou - p2/c2 - av    :param boxes1: (8, 13, 13, 3, 4)   pred_xywh    :param boxes2: (8, 13, 13, 3, 4)   label_xywh    :return:


   舉例時假設pred_xywh和label_xywh的shape都是(1, 4)    '''


# 變成左上角座標、右下角座標    boxes1_x0y0x1y1 = tf.concat([boxes1[..., :2] - boxes1[..., 2:] * 0.5,                                 boxes1[..., :2] + boxes1[..., 2:] * 0.5], axis=-1)    boxes2_x0y0x1y1 = tf.concat([boxes2[..., :2] - boxes2[..., 2:] * 0.5,                                 boxes2[..., :2] + boxes2[..., 2:] * 0.5], axis=-1)'''    逐個位置比較boxes1_x0y0x1y1[..., :2]和boxes1_x0y0x1y1[..., 2:],即逐個位置比較[x0, y0]和[x1, y1],小的留下。    好比留下了[x0, y0]    這一步是爲了不一開始w h 是負數,致使x0y0成了右下角座標,x1y1成了左上角座標。    '''    boxes1_x0y0x1y1 = tf.concat([tf.minimum(boxes1_x0y0x1y1[..., :2], boxes1_x0y0x1y1[..., 2:]),                                 tf.maximum(boxes1_x0y0x1y1[..., :2], boxes1_x0y0x1y1[..., 2:])], axis=-1)    boxes2_x0y0x1y1 = tf.concat([tf.minimum(boxes2_x0y0x1y1[..., :2], boxes2_x0y0x1y1[..., 2:]),                                 tf.maximum(boxes2_x0y0x1y1[..., :2], boxes2_x0y0x1y1[..., 2:])], axis=-1)


# 兩個矩形的面積    boxes1_area = (boxes1_x0y0x1y1[..., 2] - boxes1_x0y0x1y1[..., 0]) * (                boxes1_x0y0x1y1[..., 3] - boxes1_x0y0x1y1[..., 1])    boxes2_area = (boxes2_x0y0x1y1[..., 2] - boxes2_x0y0x1y1[..., 0]) * (                boxes2_x0y0x1y1[..., 3] - boxes2_x0y0x1y1[..., 1])


# 相交矩形的左上角座標、右下角座標,shape 都是 (8, 13, 13, 3, 2)    left_up = tf.maximum(boxes1_x0y0x1y1[..., :2], boxes2_x0y0x1y1[..., :2])    right_down = tf.minimum(boxes1_x0y0x1y1[..., 2:], boxes2_x0y0x1y1[..., 2:])


# 相交矩形的面積inter_area。iou    inter_section = tf.maximum(right_down - left_up, 0.0)    inter_area = inter_section[..., 0] * inter_section[..., 1]    union_area = boxes1_area + boxes2_area - inter_area    iou = inter_area / (union_area + K.epsilon())


# 包圍矩形的左上角座標、右下角座標,shape 都是 (8, 13, 13, 3, 2)    enclose_left_up = tf.minimum(boxes1_x0y0x1y1[..., :2], boxes2_x0y0x1y1[..., :2])    enclose_right_down = tf.maximum(boxes1_x0y0x1y1[..., 2:], boxes2_x0y0x1y1[..., 2:])


# 包圍矩形的對角線的平方    enclose_wh = enclose_right_down - enclose_left_up    enclose_c2 = K.pow(enclose_wh[..., 0], 2) + K.pow(enclose_wh[..., 1], 2)


# 兩矩形中心點距離的平方    p2 = K.pow(boxes1[..., 0] - boxes2[..., 0], 2) + K.pow(boxes1[..., 1] - boxes2[..., 1], 2)


# 增長av。加上除0保護防止nan。    atan1 = tf.atan(boxes1[..., 2] / (boxes1[..., 3] + K.epsilon()))    atan2 = tf.atan(boxes2[..., 2] / (boxes2[..., 3] + K.epsilon()))    v = 4.0 * K.pow(atan1 - atan2, 2) / (math.pi ** 2)    a = v / (1 - iou + v)


   ciou = iou - 1.0 * p2 / enclose_c2 - 1.0 * a * v    return ciou

以上,代碼原做者也是作了一個很是詳細的代碼註釋呀。能夠看出,該函數定義和理論部分一致,特別是最後一行代碼,和咱們理論部分說的如出一轍哈。

    
      
    
    
    
     
     
              
     
     

    
      
    
    
    
     
     
              
     
     
ciou = iou - 1.0 * p2 / enclose_c2 - 1.0 * a * v

該CIOU函數定義被用在求解總損失函數上了,咱們知道YOLO V3的損失函數主要分爲三部分,分別爲:

  • (1)bounding box regression損失
  • (2)置信度損失
  • (3)分類損失

YOLO V4相較於YOLO V3,只在bounding box regression作了創新,用CIOU代替了MSE,其餘兩個部分沒有作實質改變。其代碼分別定義以下:

(1)bounding box regression損失

    
      
    
    
    
     
     
              
     
     

    
      
    
    
    
     
     
              
     
     
def loss_layer(conv, pred, label, bboxes, stride, num_class, iou_loss_thresh):    conv_shape = tf.shape(conv)    batch_size = conv_shape[0]    output_size = conv_shape[1]    input_size = stride * output_size    conv = tf.reshape(conv, (batch_size, output_size, output_size,                             3, 5 + num_class))    conv_raw_prob = conv[:, :, :, :, 5:]


   pred_xywh = pred[:, :, :, :, 0:4]    pred_conf = pred[:, :, :, :, 4:5]


   label_xywh = label[:, :, :, :, 0:4]    respond_bbox = label[:, :, :, :, 4:5]    label_prob = label[:, :, :, :, 5:]


   ciou = tf.expand_dims(bbox_ciou(pred_xywh, label_xywh), axis=-1)  # (8, 13, 13, 3, 1)    input_size = tf.cast(input_size, tf.float32)


   # 每一個預測框xxxiou_loss的權重 = 2 - (ground truth的面積/圖片面積)    bbox_loss_scale = 2.0 - 1.0 * label_xywh[:, :, :, :, 2:3] * label_xywh[:, :, :, :, 3:4] / (input_size ** 2)    ciou_loss = respond_bbox * bbox_loss_scale * (1 - ciou)  # 1. respond_bbox做爲mask,有物體才計算xxxiou_loss

(2)置信度損失

   
# 2. respond_bbox做爲mask,有物體才計算類別loss    prob_loss = respond_bbox * tf.nn.sigmoid_cross_entropy_with_logits(labels=label_prob, logits=conv_raw_prob)


(3)分類損失    # 3. xxxiou_loss和類別loss比較簡單。重要的是conf_loss,是一個focal_loss    # 分兩步:第一步是肯定 grid_h * grid_w * 3 個預測框 哪些做爲反例;第二步是計算focal_loss。    expand_pred_xywh = pred_xywh[:, :, :, :, np.newaxis, :]  # 擴展爲(?, grid_h, grid_w, 3,   1, 4)    expand_bboxes = bboxes[:, np.newaxis, np.newaxis, np.newaxis, :, :]  # 擴展爲(?,      1,      1, 1, 150, 4)    iou = bbox_iou(expand_pred_xywh, expand_bboxes)  # 全部格子的3個預測框 分別 和 150個ground truth  計算iou。(?, grid_h, grid_w, 3, 150)    max_iou = tf.expand_dims(tf.reduce_max(iou, axis=-1), axis=-1)  # 與150個ground truth的iou中,保留最大那個iou。(?, grid_h, grid_w, 3, 1)


   # respond_bgd表明  這個分支輸出的 grid_h * grid_w * 3 個預測框是不是 反例(背景)    # label有物體,respond_bgd是0。沒物體的話:若是和某個gt(共150個)的iou超過iou_loss_thresh,respond_bgd是0;若是和全部gt(最多150個)的iou都小於iou_loss_thresh,respond_bgd是1。    # respond_bgd是0表明有物體,不是反例;權重respond_bgd是1表明沒有物體,是反例。    # 有趣的是,模型訓練時因爲不斷更新,對於同一張圖片,兩次預測的 grid_h * grid_w * 3 個預測框(對於這個分支輸出)  是不一樣的。用的是這些預測框來與gt計算iou來肯定哪些預測框是反例。    # 而不是用固定大小(不固定位置)的先驗框。    respond_bgd = (1.0 - respond_bbox) * tf.cast(max_iou < iou_loss_thresh, tf.float32)


   # 二值交叉熵損失    pos_loss = respond_bbox * (0 - K.log(pred_conf + K.epsilon()))    neg_loss = respond_bgd  * (0 - K.log(1 - pred_conf + K.epsilon()))


   conf_loss = pos_loss + neg_loss    # 回顧respond_bgd,某個預測框和某個gt的iou超過iou_loss_thresh,不被看成是反例。在參與「預測的置信位 和 真實置信位 的 二值交叉熵」時,這個框也可能不是正例(label裏沒標這個框是1的話)。這個框有可能不參與置信度loss的計算。    # 這種框通常是gt框附近的框,或者是gt框所在格子的另外兩個框。它既不是正例也不是反例不參與置信度loss的計算。(論文裏稱之爲ignore)

最後對上述的三個損失取個平均便可,以下

    
      
    
    
    
     
     
              
     
     

    
      
    
    
    
     
     
              
     
     
   ciou_loss = tf.reduce_mean(tf.reduce_sum(ciou_loss, axis=[1, 2, 3, 4]))  # 每一個樣本單獨計算本身的ciou_loss,再求平均值    conf_loss = tf.reduce_mean(tf.reduce_sum(conf_loss, axis=[1, 2, 3, 4]))  # 每一個樣本單獨計算本身的conf_loss,再求平均值    prob_loss = tf.reduce_mean(tf.reduce_sum(prob_loss, axis=[1, 2, 3, 4]))  # 每一個樣本單獨計算本身的prob_loss,再求平均值

至此,結合代碼,有關YOLO V4損失函數的實戰部分也就說完了!

4.小結

本文結合了四個IOU損失理論定義,以及CIOU在YOLO V4中代碼定義,詳細地分析了DIOU損失CIOU損失。在當前目標檢測模型中,這樣的損失函數的確可以提升模型的表現,因此我認爲後面這種損失函數會大量替代MSE損失函數作bounding box regression,因此弄懂並理解它們是有必要的。

插個題外話,其實我我的有關論文+專利+導師任務+自學的事兒很多。有時候會好久纔會有時間進行更新。可是寫這種總結性文章,可以讓我更加深入地對閱讀的論文和代碼進行理解,若是本文或者以前其餘文章中哪裏有理解錯誤的地方,歡迎批評指正,咱們共同窗習!


本文分享自微信公衆號 - 視學算法(visualAlgorithm)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索