【從零開始學習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

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

\[d(box,centroid)=1-IoU(box,centroid) \]

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

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

2. 偏移公式

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

\[\left\{ \begin{aligned} x=&(t_x\times w_a)+x_a\\ y=&(t_y\times h_a)+y_a \end{aligned} \right. \]

其中\(x_a\)\(y_a\) 代表中心坐標,\(w_a\)\(h_a\)代表寬和高,\(t_x\)\(t_y\)是模型預測的Anchor相對於Ground Truth的偏移量,通過計算得到的x,y就是最終預測框的中心坐標。

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

\[\left\{ \begin{aligned} b_x&=\sigma(t_x)+c_x\\ b_y&=\sigma(t_y)+c_y\\ b_w&=p_we^{t_w}\\ b_h&=p_he^{t_h}\\ \sigma(t_o)&=Pr(object)\times IOU(b,object) \end{aligned} \right. \]

公式對應的意義

對照上圖進行理解:

  • \(c_x\)\(c_y\)分別代表中心點所處區域的左上角坐標。

  • \(p_w\)\(p_h\)分別代表Anchor的寬和高。

  • \(\sigma(t_x)\)\(\sigma(t_y)\)分別代表預測框中心點和左上角的距離,\(\sigma\)代表sigmoid函數,將偏移量限制在當前grid中,有利於模型收斂。

  • \(t_w\)\(t_h\)代表預測的寬高偏移量,Anchor的寬和高乘上指數化后的寬高,對Anchor的長寬進行調整。

  • \(\sigma(t_o)\)是置信度預測值,是當前框有目標的概率乘以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公式如下:

\[\begin{aligned} lbox &= \lambda_{coord}\sum_{i=0}^{S^2}\sum_{j=0}^{B}1_{i,j}^{obj}(2-w_i\times h_i)[(x_i-\hat{x_i})^2+(y_i-\hat{y_i})^2+(w_i-\hat{w_i})^2+(h_i-\hat{h_i})^2] \\ lcls &= \lambda_{class}\sum_{i=0}^{S^2}\sum_{j=0}^{B}1_{i,j}^{obj}\sum_{c\in classes}p_i(c)log(\hat{p_i}(c)) \\ lobj &= \lambda_{noobj}\sum_{i=0}^{S^2}\sum_{j=0}^{B}1_{i,j}^{noobj}(c_i-\hat{c_i})^2+\lambda_{obj}\sum_{i=0}^{S^2}\sum_{j=0}^{B}1_{i,j}^{obj}(c_i-\hat{c_i})^2 \\ loss &= lbox + lobj + lcls \end{aligned} \]

其中:

S: 代表grid size, \(S^2\)代表13x13,26x26, 52x52

B: box

\(1_{i,j}^{obj}\): 如果在i,j處的box有目標,其值為1,否則為0

\(1_{i,j}^{noobj}\): 如果在i,j處的box沒有目標,其值為1,否則為0

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

\[BCE(\hat{c_i},c_i)=-\hat{c_i}\times log(c_i)-(1-\hat{c_i})\times log(1-c_i) \]

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

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

1. lbox部分

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

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

\[IoU=\frac{|A\cap B|}{|A\cup B|} \]

而GIoU公式如下:

\[GIoU=IoU-\frac{|A_c-U|}{|A_c|} \]

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

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激活層,常用在自編碼器中。

計算公式

\[l_n=-w_n[y_nlog(x_n)+(1-y_n)log(1-x_n)] \]

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

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

計算公式

\[l_n=-w_n[y_nlog\sigma(x_n)+(1-y_n)log(1-\sigma(x_n))] \]


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM