【從零開始學習YOLOv3】8. YOLOv3中Loss部分計算

YOLOv1是一個anchor-free的,從YOLOv2開始引入了Anchor,在VOC2007數據集上將mAP提升了10個百分點。YOLOv3也繼續使用了Anchor,本文主要講ultralytics版YOLOv3的Loss部分的計算, 實際上這部分loss和原版差距非常大,並且可以通過arc指定loss的構建方式, 如果想看原版的loss可以在下方release的v6中下載源碼。

Github地址: https://github.com/ultralytics/yolov3

Github release: https://github.com/ultralytics/yolov3/releases

  • 1. Anchor
  • 2. 偏移公式
  • 3. Loss
  • 4. 代碼
  • 5. 補充

1. Anchor

Faster R-CNN中Anchor的大小和比例是由人手工設計的,可能並不貼合數據集,有可能會給模型性能帶來負面影響。YOLOv2和YOLOv3則是通過聚類算法得到最適合的k個框。聚類距離是通過IoU來定義,IoU越大,邊框距離越近。

Anchor越多,平均IoU會越大,效果越好,但是會帶來計算量上的負擔,下圖是YOLOv2論文中的聚類數量和平均IoU的關係圖,在YOLOv2中選擇了5個anchor作為精度和速度的平衡。

YOLOv2中聚類Anchor數量和IoU的關係圖

2. 偏移公式

在Faster RCNN中,中心坐標的偏移公式是:

其中、 代表中心坐標,和代表寬和高,和是模型預測的Anchor相對於Ground Truth的偏移量,通過計算得到的x,y就是最終預測框的中心坐標。

而在YOLOv2和YOLOv3中,對偏移量進行了限制,如果不限制偏移量,那麼邊框的中心可以在圖像任何位置,可能導致訓練的不穩定。

公式對應的意義

對照上圖進行理解:

  • 和分別代表中心點所處區域的左上角坐標。
  • 和分別代表Anchor的寬和高。
  • 和分別代表預測框中心點和左上角的距離,代表sigmoid函數,將偏移量限制在當前grid中,有利於模型收斂。
  • 和代表預測的寬高偏移量,Anchor的寬和高乘上指數化後的寬高,對Anchor的長寬進行調整。
  • 是置信度預測值,是當前框有目標的概率乘以bounding box和ground truth的IoU的結果

3. Loss

YOLOv3中有一個參數是ignore_thresh,在ultralytics版版的YOLOv3中對應的是train.py文件中的iou_t參數(默認為0.225)。

正負樣本是按照以下規則決定的

  • 如果一個預測框與所有的Ground Truth的最大IoU<ignore_thresh時,那這個預測框就是負樣本
  • 如果Ground Truth的中心點落在一個區域中,該區域就負責檢測該物體。將與該物體有最大IoU的預測框作為正樣本(注意這裡沒有用到ignore thresh,即使該最大IoU<ignore thresh也不會影響該預測框為正樣本)

在YOLOv3中,Loss分為三個部分:

  • 一個是xywh部分帶來的誤差,也就是bbox帶來的loss
  • 一個是置信度帶來的誤差,也就是obj帶來的loss
  • 最後一個是類別帶來的誤差,也就是class帶來的loss

在代碼中分別對應lbox, lobj, lcls,yolov3中使用的loss公式如下:

其中:

S: 代表grid size, 代表13×13,26×26, 52×52

B: box

: 如果在i,j處的box有目標,其值為1,否則為0

: 如果在i,j處的box沒有目標,其值為1,否則為0

BCE(binary cross entropy)具體計算公式如下:

以上是論文中yolov3對應的darknet。而pytorch版本的yolov3改動比較大,有較大的改動空間,可以通過參數進行調整。

分成三個部分進行具體分析:

1. lbox部分

在ultralytics版版的YOLOv3中,使用的是GIOU,具體講解見GIOU講解鏈接

簡單來說是這樣的公式,IoU公式如下:

而GIoU公式如下:

其中代表兩個框最小閉包區域面積,也就是同時包含了預測框和真實框的最小框的面積。

yolov3中提供了IoU、GIoU、DIoU和CIoU等計算方式,以GIoU為例:

if GIoU:  # Generalized IoU https://arxiv.org/pdf/1902.09630.pdf      c_area = cw * ch + 1e-16# convex area      return iou - (c_area - union) / c_area  # GIoU  

可以看到代碼和GIoU公式是一致的,再來看一下lbox計算部分:

giou = bbox_iou(pbox.t(), tbox[i],  				x1y1x2y2=False, GIoU=True)  lbox += (1.0 - giou).sum() if red == 'sum'else (1.0 - giou).mean()  

可以看到box的loss是1-giou的值。

2. lobj部分

lobj代表置信度,即該bounding box中是否含有物體的概率。在yolov3代碼中obj loss可以通過arc來指定,有兩種模式:

如果採用default模式,使用BCEWithLogitsLoss,將obj loss和cls loss分開計算:

BCEobj = nn.BCEWithLogitsLoss(pos_weight=ft([h['obj_pw']]), reduction=red)  if'default'in arc:  # separate obj and cls      lobj += BCEobj(pi[..., 4], tobj)  # obj loss      # pi[...,4]對應的是該框中含有目標的置信度,和giou計算BCE      # 相當於將obj loss和cls loss分開計算  

如果採用BCE模式,使用的也是BCEWithLogitsLoss, 計算對象是所有的cls loss:

BCE = nn.BCEWithLogitsLoss(reduction=red)  elif'BCE'in arc:  # unified BCE (80 classes)      t = torch.zeros_like(pi[..., 5:])  # targets      if nb:          t[b, a, gj, gi, tcls[i]] = 1.0# 對應正樣本class置信度設置為1          lobj += BCE(pi[..., 5:], t)#pi[...,5:]對應的是所有的class  

3. lcls部分

如果是單類的情況,cls loss=0

如果是多類的情況,也分兩個模式:

如果採用default模式,使用的是BCEWithLogitsLoss計算class loss。

BCEcls = nn.BCEWithLogitsLoss(pos_weight=ft([h['cls_pw']]), reduction=red)  # cls loss 只計算多類之間的loss,單類不進行計算  if'default'in arc and model.nc > 1:      t = torch.zeros_like(ps[:, 5:])  # targets      t[range(nb), tcls[i]] = 1.0# 設置對應class為1      lcls += BCEcls(ps[:, 5:], t)  # 使用BCE計算分類loss  

如果採用CE模式,使用的是CrossEntropy同時計算obj loss和cls loss。

CE = nn.CrossEntropyLoss(reduction=red)  elif'CE'in arc:  # unified CE (1 background + 80 classes)      t = torch.zeros_like(pi[..., 0], dtype=torch.long)  # targets      if nb:      t[b, a, gj, gi] = tcls[i] + 1# 由於cls是從零開始計數的,所以+1      lcls += CE(pi[..., 4:].view(-1, model.nc + 1), t.view(-1))      # 這裡將obj loss和cls loss一起計算,使用CrossEntropy Loss  

以上三部分總結下來就是下圖:

4. 代碼

ultralytics版版的yolov3的loss已經和論文中提出的部分大相徑庭了,代碼中很多地方地方是來自作者的經驗。另外,這裡讀的代碼是2020年2月份左右作者發佈的版本,關注這個庫的人會知道,作者更新速度非常快,在筆者寫這篇文章的時候,loss也出現了大幅改動,添加了label smoothing等新的機制,去掉了通過arc來調整loss的機制,簡化了loss部分。

這部分的代碼添加了大量注釋,很多是筆者通過debug得到的結果,理解的時候需要講一下debug的配置:

  • 單類數據集class=1
  • batch size=2
  • 模型是yolov3.cfg

計算loss這部分代碼可以大概上分為兩部分,一部分是正負樣本選取,一部分是loss計算。

1. 正負樣本選取部分

這部分主要工作是在每個yolo層將預設的anchor和ground truth進行匹配,得到正樣本,回顧一下上文中在YOLOv3中正負樣本選取規則:

  • 如果一個預測框與所有的Ground Truth的最大IoU<ignore_thresh時,那這個預測框就是負樣本
  • 如果Ground Truth的中心點落在一個區域中,該區域就負責檢測該物體。將與該物體有最大IoU的預測框作為正樣本(注意這裡沒有用到ignore thresh,即使該最大IoU<ignore thresh也不會影響該預測框為正樣本)
def build_targets(model, targets):      # targets = [image, class, x, y, w, h]      # 這裡的image是一個數字,代表是當前batch的第幾個圖片      # x,y,w,h都進行了歸一化,除以了寬或者高        nt = len(targets)        tcls, tbox, indices, av = [], [], [], []        multi_gpu = type(model) in (nn.parallel.DataParallel,                                  nn.parallel.DistributedDataParallel)        reject, use_all_anchors = True, True      for i in model.yolo_layers:          # yolov3.cfg中有三個yolo層,這部分用於獲取對應yolo層的grid尺寸和anchor大小          # ng 代表num of grid (13,13) anchor_vec [[x,y],[x,y]]          # 注意這裡的anchor_vec: 假如現在是yolo第一個層(downsample rate=32)          # 這一層對應anchor為:[116, 90], [156, 198], [373, 326]          # anchor_vec實際值為以上除以32的結果:[3.6,2.8],[4.875,6.18],[11.6,10.1]          # 原圖 416x416 對應的anchor為 [116, 90]          # 下採樣32倍後 13x13 對應的anchor為 [3.6,2.8]          if multi_gpu:              ng = model.module.module_list[i].ng              anchor_vec = model.module.module_list[i].anchor_vec          else:              ng = model.module_list[i].ng,              anchor_vec = model.module_list[i].anchor_vec            # iou of targets-anchors          # targets中保存的是ground truth          t, a = targets, []            gwh = t[:, 4:6] * ng[0]            if nt:  # 如果存在目標              # anchor_vec: shape = [3, 2] 代表3個anchor              # gwh: shape = [2, 2] 代表 2個ground truth              # iou: shape = [3, 2] 代表 3個anchor與對應的兩個ground truth的iou              iou = wh_iou(anchor_vec, gwh)  # 計算先驗框和GT的iou                if use_all_anchors:                  na = len(anchor_vec)  # number of anchors                  a = torch.arange(na).view(                      (-1, 1)).repeat([1, nt]).view(-1)  # 構造 3x2 -> view到6                  # a = [0,0,1,1,2,2]                  t = targets.repeat([na, 1])                  # targets: [image, cls, x, y, w, h]                  # 複製3個: shape[2,6] to shape[6,6]                  gwh = gwh.repeat([na, 1])                  # gwh shape:[6,2]              else:  # use best anchor only                  iou, a = iou.max(0)  # best iou and anchor                  # 取iou最大值是darknet的默認做法,返回的a是下角標                # reject anchors below iou_thres (OPTIONAL, increases P, lowers R)              if reject:                  # 在這裡將所有閾值小於ignore thresh的去掉                  j = iou.view(-1) > model.hyp['iou_t']                  # iou threshold hyperparameter                  t, a, gwh = t[j], a[j], gwh[j]            # Indices          b, c = t[:, :2].long().t()  # target image, class          # 取的是targets[image, class, x,y,w,h]中 [image, class]            gxy = t[:, 2:4] * ng[0]  # grid x, y            gi, gj = gxy.long().t()  # grid x, y indices          # 注意這裡通過long將其轉化為整形,代表格子的左上角            indices.append((b, a, gj, gi))          # indice結構體保存內容為:          '''          b: 一個batch中的角標          a: 代表所選中的正樣本的anchor的下角標          gj, gi: 代表所選中的grid的左上角坐標          '''          # Box          gxy -= gxy.floor()  # xy          # 現在gxy保存的是偏移量,是需要YOLO進行擬合的對象          tbox.append(torch.cat((gxy, gwh), 1))  # xywh (grids)          # 保存對應偏移量和寬高(對應13x13大小的)          av.append(anchor_vec[a])  # anchor vec          # av 是anchor vec的縮寫,保存的是匹配上的anchor的列表            # Class          tcls.append(c)          # tcls用於保存匹配上的類別列表          if c.shape[0]:  # if any targets              assert c.max() < model.nc, 'Model accepts %g classes labeled from 0-%g, however you labelled a class %g. '                                          'See https://github.com/ultralytics/yolov3/wiki/Train-Custom-Data' % (                                             model.nc, model.nc - 1, c.max())      return tcls, tbox, indices, av  

梳理一下在每個YOLO層的匹配流程:

  • 將ground truth和anchor進行匹配,得到iou
  • 然後有兩個方法匹配:
    • 使用yolov3原版的匹配機制,僅僅選擇iou最大的作為正樣本
    • 使用ultralytics版版yolov3的默認匹配機制,use_all_anchors=True的時候,選擇所有的匹配對
  • 對以上匹配的部分在進行篩選,對應原版yolo中ignore_thresh部分,將以上匹配到的部分中iou<ignore_thresh的部分篩選掉。
  • 最後將匹配得到的內容返回到compute_loss函數中。

2. loss計算部分

這部分就是yolov3中核心loss計算,這部分對照上文的講解進行理解。

def compute_loss(p, targets, model):      # p: (bs, anchors, grid, grid, classes + xywh)      # predictions, targets, model      ft = torch.cuda.FloatTensor if p[0].is_cuda else torch.Tensor      lcls, lbox, lobj = ft([0]), ft([0]), ft([0])      tcls, tbox, indices, anchor_vec = build_targets(model, targets)      '''      以yolov3為例,有三個yolo層      tcls: 一個list保存三個tensor,每個tensor中有6(2個gtx3個anchor)個代表類別的數字      tbox: 一個list保存三個tensor,每個tensor形狀[6,4],6(2個gtx3個anchor)個bbox      indices: 一個list保存三個tuple,每個tuple中保存4個tensor:              分別代表        b: 一個batch中的角標                              a: 代表所選中的正樣本的anchor的下角標                              gj, gi: 代表所選中的grid的左上角坐標      anchor_vec: 一個list保存三個tensor,每個tensor形狀[6,2],                  6(2個gtx3個anchor)個anchor,注意大小是相對於13x13feature map的anchor大小      '''        h = model.hyp  # hyperparameters      arc = model.arc  # # (default, uCE, uBCE) detection architectures      # 具體使用的損失函數是通過arc參數決定的      red = 'sum'# Loss reduction (sum or mean)        # Define criteria      BCEcls = nn.BCEWithLogitsLoss(pos_weight=ft([h['cls_pw']]), reduction=red)      BCEobj = nn.BCEWithLogitsLoss(pos_weight=ft([h['obj_pw']]), reduction=red)      #BCEWithLogitsLoss = sigmoid + BCELoss      BCE = nn.BCEWithLogitsLoss(reduction=red)      CE = nn.CrossEntropyLoss(reduction=red)  # weight=model.class_weights        # class label smoothing https://arxiv.org/pdf/1902.04103.pdf eqn 3      # cp, cn = smooth_BCE(eps=0.0)      # 這是最新的版本中提供了label smoothing的功能,只能用在多類問題        if'F'in arc:  # add focal loss          g = h['fl_gamma']          BCEcls, BCEobj, BCE, CE = FocalLoss(BCEcls, g), FocalLoss(              BCEobj, g), FocalLoss(BCE, g), FocalLoss(CE, g)          # focal loss可以用在cls loss或者obj loss        # Compute losses      np, ng = 0, 0# number grid points, targets      # np這個命名真的迷,建議改一下和numpy縮寫重複      for i, pi in enumerate(p):  # layer index, layer predictions          # 在yolov3中,p有三個yolo layer的輸出pi          # 形狀為:(bs, anchors, grid, grid, classes + xywh)          b, a, gj, gi = indices[i]  # image, anchor, gridy, gridx          tobj = torch.zeros_like(pi[..., 0])          # tobj = target obj, 形狀為(bs, anchors, grid, grid)          np += tobj.numel() # 返回tobj中元素個數            # Compute losses          nb = len(b)          if nb:              ng += nb # number of targets 用於最後算平均loss              # (bs, anchors, grid, grid, classes + xywh)              ps = pi[b, a, gj, gi] # 即找到了對應目標的classes+xywh,形狀為[6(2x3),6]                # GIoU              pxy = torch.sigmoid(                  ps[:, 0:2] # 將x,y進行sigmoid              )  # pxy = pxy * s - (s - 1) / 2,  s = 1.5  (scale_xy)              pwh = torch.exp(ps[:, 2:4]).clamp(max=1E3) * anchor_vec[i]              # 防止溢出進行clamp操作,乘以13x13feature map對應的anchor              # 這部分和上文中偏移公式是一致的              pbox = torch.cat((pxy, pwh), 1)  # predicted box              # pbox: predicted bbox shape:[6, 4]              giou = bbox_iou(pbox.t(), tbox[i], x1y1x2y2=False,                              GIoU=True)  # giou computation              # 計算giou loss, 形狀為6              lbox += (1.0 - giou).sum() if red == 'sum'else (1.0 - giou).mean()              # bbox loss直接由giou決定              tobj[b, a, gj, gi] = giou.detach().type(tobj.dtype)              # target obj 用giou取代1,代表該點對應置信度                # cls loss 只計算多類之間的loss,單類不進行計算              if'default'in arc and model.nc > 1:                  t = torch.zeros_like(ps[:, 5:])  # targets                  t[range(nb), tcls[i]] = 1.0# 設置對應class為1                  lcls += BCEcls(ps[:, 5:], t)  # 使用BCE計算分類loss            if'default'in arc:  # separate obj and cls              lobj += BCEobj(pi[..., 4], tobj)  # obj loss              # pi[...,4]對應的是該框中含有目標的置信度,和giou計算BCE              # 相當於將obj loss和cls loss分開計算            elif'BCE'in arc:  # unified BCE (80 classes)              t = torch.zeros_like(pi[..., 5:])  # targets              if nb:                  t[b, a, gj, gi, tcls[i]] = 1.0# 對應正樣本class置信度設置為1              lobj += BCE(pi[..., 5:], t)              #pi[...,5:]對應的是所有的class            elif'CE'in arc:  # unified CE (1 background + 80 classes)              t = torch.zeros_like(pi[..., 0], dtype=torch.long)  # targets              if nb:                  t[b, a, gj, gi] = tcls[i] + 1# 由於cls是從零開始計數的,所以+1              lcls += CE(pi[..., 4:].view(-1, model.nc + 1), t.view(-1))              # 這裡將obj loss和cls loss一起計算,使用CrossEntropy Loss      # 使用對應的權重來平衡,這個參數是作者通過參數搜索(random search)的方法搜索得到的      lbox *= h['giou']      lobj *= h['obj']      lcls *= h['cls']        if red == 'sum':          bs = tobj.shape[0]  # batch size          lobj *= 3 / (6300 * bs) * 2          # 6300 = (10 ** 2 + 20 ** 2 + 40 ** 2) * 3          # 輸入為320x320的圖片,則存在6300個anchor          # 3代表3個yolo層, 2是一個超參數,通過實驗獲取          # 如果不想計算的話,可以修改red='mean'          if ng:              lcls *= 3 / ng / model.nc              lbox *= 3 / ng      loss = lbox + lobj + lcls      return loss, torch.cat((lbox, lobj, lcls, loss)).detach()  

需要注意的是,三個部分的loss的平衡權重不是按照yolov3原文的設置來做的,是通過超參數進化來搜索得到的,具體請看:【從零開始學習YOLOv3】4. YOLOv3中的參數進化

5. 補充

補充一下BCEWithLogitsLoss的用法,在這之前先看一下BCELoss:

torch.nn.BCELoss功能是二分類任務是的交叉熵計算函數,可以認為是CrossEntropy的特例。其分類限定為二分類,y的值必須為{0,1},input應該是概率分佈的形式。在使用BCELoss前一般會先加一個sigmoid激活層,常用在自編碼器中。

計算公式

是每個類別的loss權重,用於類別不均衡問題。

torch.nn.BCEWithLogitsLoss的相當於Sigmoid+BCELoss, 即input會經過Sigmoid激活函數,將input變為概率分佈的形式。