【從零開始學習YOLOv3】8. YOLOv3中Loss部分計算
- 2020 年 4 月 1 日
- 筆記
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變為概率分佈的形式。