YOLO系列算法(v3v4)損失函數詳解

YOLO算法是One-Stage目標檢測算法的開山之作,一面世就註定不平凡。而在監督學習中,損失函數指導着模型的學習方向,佔據着非常重要的地位。今天就由筆者來給大家講解YOLO系列算法的損失函數。由於原版YOLO算法使用C語言編寫,可能晦澀難懂,所以這裏筆者選擇了百度飛槳PaddleDetection(版本0.3.0)中復現的YOLOv3爲例進行講解,項目地址 https://github.com/PaddlePaddle/PaddleDetection

相信你看完這篇文章,再看YOLOv4的代碼或許就突然豁然開朗了,因爲損失函數部分是差不多的。好了廢話不多說,下面進入正題。本文基於讀者對YOLOv3算法有一定了解。

YOLOv3算法,如果你看過關於它的博客或一些第三方實現的話會知道,它有3個輸出張量,這三個輸出張量的形狀是(bz,3*(4+1+80), 13, 13),(bz,3*(4+1+80), 26, 26),(bz,3*(4+1+80), 52, 52)。其中的bz代表批大小,3代表一個格子會出3個預測框,而4+1+80代表一個預測框會攜帶有85位信息,前4位代表預測框的中心位置xy以及大小wh,第5位代表置信度,表示該預測框是前景的概率,最後的80位代表如果是前景,那麼預測框是80種物體的概率(COCO數據集中類別數是80)。然後後面的13、26、52就是代表一行(一列)的格子數了。第一個張量,形狀是(bz,3*(4+1+80), 13, 13),代表它輸出了13x13個網格,預測框數目是bz*3*13*13;第二個張量,形狀是(bz,3*(4+1+80), 26, 26),代表它輸出了26x26個網格,預測框數目是bz*3*26*26;第三個張量,形狀是(bz,3*(4+1+80), 52, 52),代表它輸出了52x52個網格,預測框數目是bz*3*52*52。

 

這三個張量就構成了YOLOv3的輸出。這裏也有必要再講解一個知識點——感受野。感受野即CNN能看到的視野大小。比如,一個3x3的卷積層,卷積核個數=1,步長stride=1,padding=1,一個形狀爲(N, C, H, W)的張量tensor1,經過這個卷積層之後,變成了形狀爲(N, 1, H, W)的張量tensor2。那麼新張量tensor2一個像素裏包含了tensor1多少個像素的信息呢?3x3=9個。張量tensor2的一個像素“看到了”原始特徵圖張量tensor1相同位置像素9宮格像素的內容,也就是說張量tensor2感受野是3x3大小了。假如我把卷積層換成了1x1卷積層呢?那麼只能“看到”原始特徵圖張量tensor1相同位置像素的內容,也就是說張量tensor2感受野是1x1大小。我再變點花樣,tensor1經過了2個卷積層,第一個卷積層是3x3大小,卷積核個數=1,步長stride=1,padding=1,第二個卷積層是3x3大小,卷積核個數=1,步長stride=1,padding=1,tensor1經過了這2個卷積層變成了張量tensor2,敢問張量tensor2感受野大小?5x5,張量tensor2的一個像素“看到了”原始特徵圖張量tensor1相同位置像素附近5x5像素的內容。我再變點花樣,tensor1經過了2個卷積層,第一個卷積層是3x3大小,卷積核個數=1,步長stride=2,padding=1,第二個卷積層是3x3大小,卷積核個數=1,步長stride=1,padding=1,tensor1經過了這2個卷積層變成了張量tensor2,敢問張量tensor2感受野大小?7x7,張量tensor2的一個像素“看到了”原始特徵圖張量tensor1相同位置像素附近7x7像素的內容。我們可以看到,疊加捲積層可以擴大感受野大小,卷積步長也會影響感受野大小。

 

回到正題,我們的這3個輸出張量中,13x13的張量有大感受野,它會被分配到3個最大尺度的先驗框,26x26的張量有中感受野,它會被分配到3箇中等尺度的先驗框,52x52的張量有小感受野,它會被分配到3個最小尺度的先驗框。這樣,這一層每個格子都會被分到3個先驗框,分別給了這個格子的3個預測框使用。畢竟是監督學習,監督信息也必須要有,我們的label張量也是差不多同樣的形狀。在PaddleDetection中,訓練時會先預處理圖片,比如讀圖片、進行數據增強、座標歸一化、隨機尺度、隨機插值等,數據預處理最後階段就是Gt2YoloTarget,Gt2YoloTarget()類裏就準備好了我們的label張量,我們看看它的神祕面紗:

...
grid_h = int(h / downsample_ratio)
grid_w = int(w / downsample_ratio)
target = np.zeros((len(mask), 6 + self.num_classes, grid_h, grid_w), dtype=np.float32)
for b in range(gt_bbox.shape[0]):
    gx, gy, gw, gh = gt_bbox[b, :]
    cls = gt_class[b]
...

即target,形狀是(3,(6+80), grid_h, grid_w),沒有批大小那一維是因爲是在遍歷每一張圖片,後面會拼接這一批所有圖片的target(不同的輸出層分開拼接)。3代表每個格子有3個預測框的註解,(6+80)就有點迷,小編,你剛纔不是說每個預測框會輸出85位信息的嗎?沒錯,但是我們的label可以帶多一些信息,後面你也會看到,我們帶多了另外的張量來在訓練過程中確定負樣本。我們繼續看:

...
# x, y, w, h, scale
target[best_n, 0, gj, gi] = gx * grid_w - gi
target[best_n, 1, gj, gi] = gy * grid_h - gj
target[best_n, 2, gj, gi] = np.log(gw * w / self.anchors[best_idx][0])
target[best_n, 3, gj, gi] = np.log(gh * h / self.anchors[best_idx][1])
target[best_n, 4, gj, gi] = 2.0 - gw * gh

# objectness record gt_score
target[best_n, 5, gj, gi] = score
# classification
target[best_n, 6 + cls, gj, gi] = 1.
...

找到了!這是某個預測框被選定爲正樣本時需要做的事。我們看到,0、1位用來監督xy,2、3位用來監督wh,第4位填了一個權重,即2 - gw*gh,由於預處理階段gw、gh進行了歸一化,所以這個權重表示的是“2.0 - gt的面積/輸入圖片大小的面積”,亦即“2.0 - gt面積佔圖片面積的比重”。第4位表明,若gt面積越小,權重越接近2,若gt面積越大,權重越接近1。這個權重叫做tscale,在後面的xywh損失、iou損失計算那裏都會乘上,注意這些損失都是和預測框的位置有關。而分類損失和預測框位置無關,不用乘tscale。乘以tscale表明,若gt越小,它應該越受到重視,由此改善小目標檢測。然後第5位是置信度,是填的score,如果你沒用mixup增強,會是1,用了mixup增強,它會是0~1之間的一個數值;由於mixup融合了兩張圖片,gt它不能100%是它了(因爲它變透明瞭),你可以這樣理解。最後填的是類別向量,真實類別處填1,其餘保持爲0。沒有用smooth_onehot。

上面說到如果某個預測框被選定爲正樣本,將如何填寫target張量。那麼某個預測框是如何被選定爲正樣本的呢?抱歉我總是喜歡倒敘。仔細看看代碼,你會發現是這樣的。遍歷所有的gt框,計算它的中心點座標,它的中心點座標會落在3個格子裏(大感受野輸出層、中感受野輸出層、小感受野輸出層各佔1個格子),這3個格子每個格子帶有3個先驗框共計9個先驗框。我們計算這個gt和9個先驗框的iou(計算時假設gt和先驗框的中心點位置相同),與gt有最大iou的先驗框所在的預測框被選定爲正樣本(哈哈,你就是天選之人!),這樣,正樣本就確定了。再仔細看看代碼,你會發現給正樣本框填寫label的地方有兩處,第二處和self.iou_thresh變量有關,這是怎麼回事呢?原來,原版YOLOv3的策略是一個gt只分配給了一個預測框,在這裏,PaddleDetection的大佬們爲了讓YOLOv3預測出更多的物體,允許一個gt分配給多個預測框,具體策略就是,先把gt分配給最高iou先驗框所在的預測框,這時候3個格子的所有9個預測框還剩下8個,假如它們持有的先驗框與gt的iou>self.iou_thresh,它們也會被選爲正樣本。

好了,Gt2YoloTarget()類裏的最後你發現每張圖片(也就是每個sample字典)target有3個,分別對應3個不同感受野的輸出層。再然後,PaddleDetection會拼接這一批所有圖片的target(不同的輸出層分開拼接),也就是說,這一批樣本攜帶有3個target,形狀分別是(bz, 3, 86, l_grid, l_grid)、(bz, 3, 86, m_grid, m_grid)、(bz, 3, 86, s_grid, s_grid),由於是多尺度訓練, l_grid、m_grid、s_grid和被選到的尺度有關,假如被選到的尺度是416,那麼l_grid、m_grid、s_grid就是13、26、52,它們分別是416除以32、16、8得到。13x13的輸出層,因爲經歷過了5個步長是2的卷積,分辨率縮小爲原來的1/2^5,即下采樣倍率是32。

而且,數據預處理階段,我們的sample字典(也就是一張圖片)一直帶有一個gt_bbox,它的形狀是(num_max_boxes, 4),num_max_boxes默認是50,在PadBox()預處理那裏,如果gt_bbox的數量少於num_max_boxes,那麼填充座標是0的bboxes以湊夠num_max_boxes。在這裏,我們也拼接了這一批所有圖片的gt_bbox,得到一個形狀是(bz, num_max_boxes, 4)的label張量gt_bbox,後面看到,我們利用它在訓練過程中確定負樣本。

我們終於把標記準備好了,現在,圖片張量進入了網絡進行了前向傳播,輸出了3個預測張量,這三個預測張量和我們的label張量開始計算損失了!

PaddleDetection版YOLOv3是逐層逐圖片計算損失,也就是它是先遍歷3個輸出層,如下圖所示:

    def _get_fine_grained_loss(self, outputs, targets, gt_box, batch_size,
                               num_classes, mask_anchors, ignore_thresh):
        assert len(outputs)     == len(targets), \
            "YOLOv3 output layer number not equal target number"

        loss_xys, loss_whs, loss_objs, loss_clss = [], [], [], []
        if self._iou_loss is not None:
            loss_ious = []
        if self._iou_aware_loss is not None:
            loss_iou_awares = []
        for i, (output, target,
                anchors) in enumerate(zip(outputs, targets, mask_anchors)):
            downsample = self.downsample[i]
            an_num = len(anchors) // 2
            if self._iou_aware_loss is not None:
                ioup, output = self._split_ioup(output, an_num, num_classes)
            '''
            x:    [-1, 3, -1, -1]
            obj:  [-1, 3, -1, -1]
            cls:  [-1, 3, -1, -1, 80]
            '''
            x, y, w, h, obj, cls = self._split_output(output, an_num,
                                                      num_classes)
            '''
            tx:     [-1, 3, -1, -1]      tx是0到1的數值,0.3表示gt中心點位於格子邊長的30%處。從loss_x處可以看出。
            tw:     [-1, 3, -1, -1]      tw是根據yolo的輸出公式將gt的w編碼後的值。從loss_w處可以看出。
            tscale: [-1, 3, -1, -1]      tscale填的是 2.0 - gw * gh,也就是2.0 - gt面積/圖片面積
            tobj:   [-1, 3, -1, -1]      tobj要麼是0要麼是1
            tcls:   [-1, 3, -1, -1, 80]  tcls類別向量是one-hot形式,真實類別處填1
            '''
            tx, ty, tw, th, tscale, tobj, tcls = self._split_target(target)

            # tobj帶上了面積權重
            tscale_tobj = tscale * tobj
            loss_x = fluid.layers.sigmoid_cross_entropy_with_logits(
                x, tx) * tscale_tobj
            loss_x = fluid.layers.reduce_sum(loss_x, dim=[1, 2, 3])
            loss_y = fluid.layers.sigmoid_cross_entropy_with_logits(
                y, ty) * tscale_tobj
            loss_y = fluid.layers.reduce_sum(loss_y, dim=[1, 2, 3])
            # NOTE: we refined loss function of (w, h) as L1Loss
            loss_w = fluid.layers.abs(w - tw) * tscale_tobj
            loss_w = fluid.layers.reduce_sum(loss_w, dim=[1, 2, 3])
            loss_h = fluid.layers.abs(h - th) * tscale_tobj
            loss_h = fluid.layers.reduce_sum(loss_h, dim=[1, 2, 3])
            if self._iou_loss is not None:
                loss_iou = self._iou_loss(x, y, w, h, tx, ty, tw, th, anchors,
                                          downsample, self._batch_size)
                loss_iou = loss_iou * tscale_tobj
                loss_iou = fluid.layers.reduce_sum(loss_iou, dim=[1, 2, 3])
                loss_ious.append(fluid.layers.reduce_mean(loss_iou))

            if self._iou_aware_loss is not None:
                loss_iou_aware = self._iou_aware_loss(
                    ioup, x, y, w, h, tx, ty, tw, th, anchors, downsample,
                    self._batch_size)
                loss_iou_aware = loss_iou_aware * tobj
                loss_iou_aware = fluid.layers.reduce_sum(
                    loss_iou_aware, dim=[1, 2, 3])
                loss_iou_awares.append(fluid.layers.reduce_mean(loss_iou_aware))

            #scale_x_y = self.scale_x_y if not isinstance(
            #    self.scale_x_y, Sequence) else self.scale_x_y[i]
            loss_obj_pos, loss_obj_neg = self._calc_obj_loss(
                output, obj, tobj, gt_box, self._batch_size, anchors,
                num_classes, downsample, self._ignore_thresh)

            loss_cls = fluid.layers.sigmoid_cross_entropy_with_logits(cls, tcls)
            loss_cls = fluid.layers.elementwise_mul(loss_cls, tobj, axis=0)
            loss_cls = fluid.layers.reduce_sum(loss_cls, dim=[1, 2, 3, 4])

            loss_xys.append(fluid.layers.reduce_mean(loss_x + loss_y))
            loss_whs.append(fluid.layers.reduce_mean(loss_w + loss_h))
            loss_objs.append(
                fluid.layers.reduce_mean(loss_obj_pos + loss_obj_neg))
            loss_clss.append(fluid.layers.reduce_mean(loss_cls))

        losses_all = {
            "loss_xy": fluid.layers.sum(loss_xys),
            "loss_wh": fluid.layers.sum(loss_whs),
            "loss_obj": fluid.layers.sum(loss_objs),
            "loss_cls": fluid.layers.sum(loss_clss),
        }
        if self._iou_loss is not None:
            losses_all["loss_iou"] = fluid.layers.sum(loss_ious)
        if self._iou_aware_loss is not None:
            losses_all["loss_iou_aware"] = fluid.layers.sum(loss_iou_awares)
        return losses_all

for i, (output, target, anchors) in enumerate(zip(outputs, targets, mask_anchors))那裏就是開始遍歷3個輸出層,downsample = self.downsample[i]即獲取這一輸出層的下采樣倍率,第0個輸出層是32,即一個格子的邊長是32像素。an_num = len(anchors) // 2,即本層每個格子有3個預測框。self._split_output()即將本層的輸出output切分,獲得x, y, w, h, obj, cls,關於它們的形狀已經在註釋中給出,x : [-1, 3, -1, -1]是每個預測框輸出的x,第0維表示是這一批的哪一張圖片,第2、3維表示是哪個格子,第1維表示的是是這個格子的第幾個預測框(共3個)。obj同理。cls形狀是[-1, 3, -1, -1, 80],第4維表示的是這一個預測框的80個類別的概率。以上說的是預測張量的。接着是對label張量進行切分,即self._split_target(),與self._split_output()差不多,只是多了一個tscale,即上文說到的2.0-gt面積/圖片面積。

首先計算的是x的損失,用的是二值交叉熵損失,注意這裏不用對x做一次sigmoid()操作,因爲fluid.layers.sigmoid_cross_entropy_with_logits(x, tx)裏會對x做sigmoid()激活操作。還要乘多一個tscale_tobj,這樣,就只對正樣本計算了損失,而且帶上了上文所說的面積權重。負樣本不會計算x損失,因爲負樣本的tobj處是0,0乘任何數都得0,也就是說負樣本的x損失是0,而常數0的導數是0,也就是說負樣本不會得到梯度。接着是計算y的損失,也是用的二值交叉熵。這裏說的x、y指的是預測框中心點座標的xy,而且這些xy只能是0到1之間的數值,所以解碼時要使用sigmoid激活。0~1之間的數值表示的是預測框中心點座標位於這個格子的百分之多少處。這樣就把預測框的中心點限制在了這個格子內。我們在填寫target張量時,tx ty填寫的是gx * grid_w - gi、gy * grid_h - gj,這也是0~1的數值,代表着同樣的意義。單位1表示的都是格子的邊長而並非是1像素。

接着是計算wh的損失,用的是絕對值損失,也就是L1損失。YOLOv3的座標解碼公式如下:

我們這裏的w、h即公式裏的tw th,我們直接在輸出tw th時監督,而不是解碼成bw bh時監督,所以纔會在填寫target張量時在w那裏寫入ln(bw/pw),在x那裏寫入(bx-cx)。我們是在未解碼w h 時監督的,用的絕對值損失。同樣,也只有正樣本才計算wh損失,帶上了面積權重。

接着是計算iou損失,這一項是爲了輔助監督預測框的座標和大小,作爲xywh損失的補充。它同樣乘上了tscale_tobj,即只計算正樣本的損失。iou損失,即我們希望預測框和gt之間的iou儘可能大,iou即交併比。計算iou損失時,就真的需要把上述的xywh解碼成bx by bw bh,用上面的公式。再和gt框計算iou損失。

iou_aware_loss,最新的43.6精度的模型使用了這個損失,由於筆者也沒仔細看過,暫時不講。

最後兩個損失是置信度損失和分類損失。先講分類損失,即loss_cls,它和xy損失是一樣的,用的是二值交叉熵。填寫target時,真實類別處填了1,其餘79位填了0,是onehot形式的。二值交叉熵常用於作爲分類損失。和xy損失一樣,也只有正樣本才計算分類損失。

最後將置信度損失,即loss_obj,這個損失是最複雜的損失,所以放到最後講。注意,前面講到的損失都是隻有正樣本才計算損失,因爲只有正樣本纔會有xywh類別 這些屬性,纔會監督。而負樣本(即背景框)是沒有xywh類別 這些屬性的,不需要計算這些損失。只有置信度損失才計算負樣本的,因爲我們希望負樣本(即背景框)輸出的置信度是接近0的,這樣在預測的時候纔可以把背景框過濾掉。說到負樣本,肯定有正樣本(即前景框)了,剛纔我們說到的損失,都是隻計算了正樣本框的損失,而且可能你也發現了,正樣本在數據預處理階段(即Gt2YoloTarget()類裏)就已經確定好了,根據gt中心點落在了哪3個格子(每個輸出層各1個),和這3個格子持有的共9個先驗框計算iou,最高iou的先驗框對應的預測框就被選爲了正樣本,然後填寫它的xywh、置信位、面積權重、類別onehot。但是負樣本的確定有點不同,在這一批圖片經過網絡的前向傳播後,開始計算損失時,纔會確定負樣本。而且,不僅僅有負樣本,還有一種樣本叫做“忽略樣本”,“忽略樣本”不參與置信度損失的計算。YOLOv3算法,訓練時一共這3種樣本(對置信度損失而言)。我們來看看源代碼,計算置信度損失的代碼在函數self._calc_obj_loss()裏:

    def _calc_obj_loss(self, output, obj, tobj, gt_box, batch_size, anchors,
                       num_classes, downsample, ignore_thresh):
        # A prediction bbox overlap any gt_bbox over ignore_thresh, 
        # objectness loss will be ignored, process as follows:

        # 將output解碼爲bbox,用博客裏提供的公式,這裏用了飛槳官方的API fluid.layers.yolo_box()。
        # bbox的形狀是[batch_size, -1, 4],比如[batch_size, 13*13*3, 4],bbox的座標表示的是左上角座標+右下角座標。img_size 設爲 1.0 以歸一化bbox,即bbox的座標都是0~1之間的數值,表示的是在圖片寬高的百分之多少處。
        # prob的形狀是[batch_size, -1, 80],比如[batch_size, 13*13*3, 80],prob是每個預測框的置信位分別乘以自己的80個類別的預測概率得到的。也就是YOLOv3的“分數”公式。fluid.layers.yolo_box()中會對置信位做sigmoid()激活操作,對所有預測框80個類別位做sigmoid()激活操作,二者相乘,就得到了prob
        bbox, prob = fluid.layers.yolo_box(
            x=output,
            img_size=fluid.layers.ones(
                shape=[batch_size, 2], dtype="int32"),
            anchors=anchors,
            class_num=num_classes,
            conf_thresh=0.,
            downsample_ratio=downsample,
            clip_bbox=False)

        # 2. 將這一批每張圖片切成一個獨立的張量,組合成一個list
        if batch_size > 1:
            preds = fluid.layers.split(bbox, batch_size, dim=0)
            gts = fluid.layers.split(gt_box, batch_size, dim=0)
        else:
            preds = [bbox]
            gts = [gt_box]
            probs = [prob]
        ious = []
        
        # 遍歷這一批的每張圖片
        for pred, gt in zip(preds, gts):

            def box_xywh2xyxy(box):
                x = box[:, 0]
                y = box[:, 1]
                w = box[:, 2]
                h = box[:, 3]
                return fluid.layers.stack(
                    [
                        x - w / 2.,
                        y - h / 2.,
                        x + w / 2.,
                        y + h / 2.,
                    ], axis=1)

            pred = fluid.layers.squeeze(pred, axes=[0])   # [3*grid_h*grid_w, 4],即去掉第0維。
            gt = box_xywh2xyxy(fluid.layers.squeeze(gt, axes=[0]))   # [50, 4],即去掉第0維,且將xywh(中心點座標+寬高)變成xyxy(左上角座標+右下角座標)。
            # 返回張量的形狀爲[3*grid_h*grid_w, 50]。兩組矩形所有框對的iou。
            ious.append(fluid.layers.iou_similarity(pred, gt))

        # iou形狀是[batch_size, 3*grid_h*grid_w, 50],即拼接了ious,所有圖片這一層的ious匯合。
        iou = fluid.layers.stack(ious, axis=0)
        # 3. Get iou_mask by IoU between gt bbox and prediction bbox,
        #    Get obj_mask by tobj(holds gt_score), calculate objectness loss

        # [batch_size, 3*grid_h*grid_w]   只留下了與gt的最大iou
        max_iou = fluid.layers.reduce_max(iou, dim=-1)
        # [batch_size, 3*grid_h*grid_w]   負樣本框
        iou_mask = fluid.layers.cast(max_iou <= ignore_thresh, dtype="float32")
        if self.match_score:
            max_prob = fluid.layers.reduce_max(prob, dim=-1)
            iou_mask = iou_mask * fluid.layers.cast(
                max_prob <= 0.25, dtype="float32")
        # [-1, 255, -1, -1]
        output_shape = fluid.layers.shape(output)
        an_num = len(anchors) // 2
        # [batch_size, 3, grid_h, grid_w]
        iou_mask = fluid.layers.reshape(iou_mask, (-1, an_num, output_shape[2],
                                                   output_shape[3]))
        iou_mask.stop_gradient = True

        # NOTE: tobj holds gt_score, obj_mask holds object existence mask
        # [batch_size, 3, grid_h, grid_w]   正樣本框,正樣本處是1,其他位置是0
        obj_mask = fluid.layers.cast(tobj > 0., dtype="float32")
        obj_mask.stop_gradient = True

        # For positive objectness grids, objectness loss should be calculated
        # For negative objectness grids, objectness loss is calculated only iou_mask == 1.0
        # 正樣本框、忽略框、負樣本框的loss
        loss_obj = fluid.layers.sigmoid_cross_entropy_with_logits(obj, obj_mask)
        # 只留下正樣本框的loss
        loss_obj_pos = fluid.layers.reduce_sum(loss_obj * tobj, dim=[1, 2, 3])
        # 只留下負樣本框的loss,因爲被分配到gt的正樣本框中有的可能與gt最高iou不足0.7
        loss_obj_neg = fluid.layers.reduce_sum(
            loss_obj * (1.0 - obj_mask) * iou_mask, dim=[1, 2, 3])

        return loss_obj_pos, loss_obj_neg

部分解釋已經寫在了代碼裏。我們看看ious,它存放的是所有預測框(比如3*13*13個)和所有gt(這裏是50個)兩兩之間的iou,用的是fluid.layers.iou_similarity(pred, gt)來計算的,返回張量的形狀爲[3*grid_h*grid_w, 50]。代表着每一個預測框分別和50個gt框的iou。接着我們用fluid.layers.reduce_max(iou, dim=-1)獲得一個max_iou張量,即每一個預測框分別和50個gt框的iou中,只留下了最大的iou。“若某預測框和所有gt的iou不大於設定的閾值ignore_thresh=0.7,而且本身不是正樣本框的話,那麼它就作爲負樣本框。”。這裏我們只留下了最大iou的原因是,只要最大iou小於閾值ignore_thresh,那麼就確保了該預測框和所有gt的iou小於閾值ignore_thresh。接着是iou_mask = fluid.layers.cast(max_iou <= ignore_thresh, dtype="float32"),即iou_mask裏的元素是1的話表示該位置的預測框和所有gt的iou不大於設定的閾值ignore_thresh,但現在還不能說它就是負樣本,因爲有可能正樣本框和所有gt的iou不大於設定的閾值ignore_thresh,即它有可能本身是正樣本。我們後面會進一步確定。obj_mask = fluid.layers.cast(tobj > 0., dtype="float32"),即obj_mask裏的元素是1的話表示該位置的預測框是正樣本。爲什麼不直接用tobj表示obj_mask呢?因爲如果你用了mixup增強,tobj不會是非0即1,正樣本的score是一個0到1之間的數值。loss_obj = fluid.layers.sigmoid_cross_entropy_with_logits(obj, obj_mask),這一步,我們計算了所有框(正樣本框、負樣本框、忽略樣本框)的損失,用obj_mask作爲label,即正樣本輸出的置信度應該接近於1,負樣本和忽略樣本輸出的置信度應該接近於0。不是說不計算忽略樣本的損失?別急,這不是我們需要的損失,只是先暫時表達出來,後面我們會取感興趣的損失出來。loss_obj_pos = fluid.layers.reduce_sum(loss_obj * tobj, dim=[1, 2, 3]),我們把正樣本的損失取了出來,因爲負樣本和忽略樣本的tobj處是0,所以loss_obj * tobj起到了過濾掉負樣本損失和忽略樣本損失的作用。loss_obj_neg = fluid.layers.reduce_sum(loss_obj * (1.0 - obj_mask) * iou_mask, dim=[1, 2, 3]),我們把負樣本的損失取了出來,首先(1.0 - obj_mask)某元素是1的話表示的是不是正樣本 ,iou_mask某元素是1的話表示的是該位置的預測框和所有gt的iou不大於設定的閾值ignore_thresh,所以loss_obj * (1.0 - obj_mask) * iou_mask起到了過濾掉正樣本損失和忽略樣本損失的作用。最後,return loss_obj_pos, loss_obj_neg  即置信度損失只計算了正樣本的和負樣本的。爲什麼要有“忽略樣本”這種機制呢?“忽略樣本”框一般是gt框附近的框,或者是gt框所在格子的另外兩個框。這種框,若作爲負樣本進行訓練,可能對於正樣本的檢出率有負面影響,比如你看到了一隻狗的大部分,但你不足以作爲正樣本,總不能說你看到的這一大部分是“背景”,作爲負樣本訓練的話,會影響卷積層的權重,就是說你認爲你看到的這一大部分是背景,可能會降低正樣本的檢出率。而如果作爲正樣本進行訓練的話,可能會是低質量的正樣本,因爲可能連物體中心點的座標都預測不準,你還指望預測框能很好地包圍物體嗎?預測時在nms階段可能也過濾不掉這些低質量的正樣本。所以,索性就不管它們了,置信位愛輸出什麼輸出什麼。當然,上述只是筆者的猜想和理解。

希望大家看完這篇筆者寫的文章後,能對YOLO算法的損失函數有更深的理解。

最後,筆者打一個廣告,歡迎大家來GitHub關注筆者的賬號miemie2013,給筆者的倉庫點star,筆者復現了很多算法哦,包括YOLOv4,筆者復現的YOLOv4損失函數也和上面所講解的沒有太大出入。

Keras版YOLOv3: https://github.com/miemie2013/Keras-DIOU-YOLOv3
Pytorch版YOLOv3:https://github.com/miemie2013/Pytorch-DIOU-YOLOv3
PaddlePaddle版YOLOv3:https://github.com/miemie2013/Paddle-DIOU-YOLOv3
PaddlePaddle完美復刻版版yolact: https://github.com/miemie2013/PaddlePaddle_yolact
Keras版YOLOv4: https://github.com/miemie2013/Keras-YOLOv4
Paddle版YOLOv4:https://github.com/miemie2013/Paddle-YOLOv4
Keras版SOLO: https://github.com/miemie2013/Keras-SOLO
Paddle版SOLO: https://github.com/miemie2013/Paddle-SOLO

寫完這篇文章,小編累得只剩下半口氣了,果然寫文章真的不比寫代碼輕鬆,小編感覺自己還是更喜歡更適合寫代碼一點。如果你有什麼想和小編說的,歡迎在評論區留言和小編互動哦!

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