深度學習筆記(十三)YOLO V3 (Tensorflow)


[原始代碼]

[代碼剖析]   推薦閱讀!

SSD 學習筆記

之前看了一遍 YOLO V3 的論文,寫的挺有意思的,尷尬的是,我這魚的記憶,看完就忘了 

於是只能借助於代碼,再看一遍細節了。

源碼目錄總覽

tensorflow-yolov3
    ├── checkpoint //保存模型的目錄
    ├── convert_weight.py //對權重去冗余,去掉訓練相關
    ├── core //核心代碼文件夾
    │   ├── backbone.py
    │   ├── common.py
    │   ├── config.py //配置文件
    │   ├── dataset.py //數據處理
    │   ├── __init__.py
    │   ├── utils.py
    │   └── yolov3.py //網絡核心結構
    ├── data
    │   ├── anchors // Anchor 配置
    │   │   ├── basline_anchors.txt
    │   │   └── coco_anchors.txt
    │   ├── classes //訓練預測目標的種類
    │   │   ├── coco.names
    │   │   └── voc.names
    │   ├── dataset //保存圖片的相關信息:路徑,box,置信度,類別編號
    │   │   ├── voc_test.txt //測試數據
    │   │   └── voc_train.txt //訓練數據
    ├── docs //比較混雜
    │   ├── Box-Clustering.ipynb //根據數據信息生成預選框anchors
    │   ├── images
    │   │   ├── 611_result.jpg
    │   │   ├── darknet53.png
    │   │   ├── iou.png
    │   │   ├── K-means.png
    │   │   ├── levio.jpeg
    │   │   ├── probability_extraction.png
    │   │   ├── road.jpeg
    │   │   ├── road.mp4
    │   │   └── yolov3.png
    │   └── requirements.txt // 環境需求
    ├── evaluate.py // 模型評估
    ├── freeze_graph.py //生成pb文件
    ├── image_demo.py // 利用 pb 模型的測試 demo
    ├── mAP//模型評估相關信息存儲
    │   ├── extra
    │   │   ├── class_list.txt
    │   │   ├── convert_gt_xml.py
    │   │   ├── convert_gt_yolo.py
    │   │   ├── convert_keras-yolo3.py
    │   │   ├── convert_pred_darkflow_json.py
    │   │   ├── convert_pred_yolo.py
    │   │   ├── find_class.py
    │   │   ├── intersect-gt-and-pred.py
    │   │   ├── README.md
    │   │   ├── remove_class.py
    │   │   ├── remove_delimiter_char.py
    │   │   ├── remove_space.py
    │   │   ├── rename_class.py
    │   │   └── result.txt
    │   ├── __init__.py
    │   └── main.py // 計算 mAP
    ├── README.md
    ├── scripts
    │   ├── show_bboxes.py
    │   └── voc_annotation.py //把xml轉化為網絡可以使用的txt文件
    ├── train.py //模型訓練
    └── video_demo.py // 利用 pb 模型的測試 demo

接下來,我按照看代碼的順序來詳細說明了。

core/dataset.py

#! /usr/bin/env python
# coding=utf-8
# ================================================================
#   Copyright (C) 2019 * Ltd. All rights reserved.
#
#   Editor      : VIM
#   File name   : dataset.py
#   Author      : YunYang1994
#   Created date: 2019-03-15 18:05:03
#   Description :
#
# ================================================================

import os
import cv2
import random
import numpy as np
import tensorflow as tf
import core.utils as utils
from core.config import cfg


class DataSet(object):
    """implement Dataset here"""

    def __init__(self, dataset_type):
        self.annot_path = cfg.TRAIN.ANNOT_PATH if dataset_type == 'train' else cfg.TEST.ANNOT_PATH
        self.input_sizes = cfg.TRAIN.INPUT_SIZE if dataset_type == 'train' else cfg.TEST.INPUT_SIZE
        self.batch_size = cfg.TRAIN.BATCH_SIZE if dataset_type == 'train' else cfg.TEST.BATCH_SIZE
        self.data_aug = cfg.TRAIN.DATA_AUG if dataset_type == 'train' else cfg.TEST.DATA_AUG

        self.train_input_sizes = cfg.TRAIN.INPUT_SIZE
        self.strides = np.array(cfg.YOLO.STRIDES)
        self.classes = utils.read_class_names(cfg.YOLO.CLASSES)
        self.num_classes = len(self.classes)
        self.anchors = np.array(utils.get_anchors(cfg.YOLO.ANCHORS))
        self.anchor_per_scale = cfg.YOLO.ANCHOR_PER_SCALE
        self.max_bbox_per_scale = 150

        self.annotations = self.load_annotations(dataset_type)  # read and shuffle annotations
        self.num_samples = len(self.annotations)  # dataset size
        self.num_batchs = int(np.ceil(self.num_samples / self.batch_size))  # 向上取整
        self.batch_count = 0  # batch index

    def load_annotations(self, dataset_type):
        with open(self.annot_path, 'r') as f:
            txt = f.readlines()
            annotations = [line.strip() for line in txt if len(line.strip().split()[1:]) != 0]
        # np.random.seed(1)  # for debug
        np.random.shuffle(annotations)
        return annotations

    def __iter__(self):
        return self

    def next(self):
        with tf.device('/cpu:0'):
            self.train_input_size_h, self.train_input_size_w = random.choice(self.train_input_sizes)
            self.train_output_sizes_h = self.train_input_size_h // self.strides
            self.train_output_sizes_w = self.train_input_size_w // self.strides
            # ================================================================ #
            batch_image = np.zeros((self.batch_size, self.train_input_size_h, self.train_input_size_w, 3))

            batch_label_sbbox = np.zeros((self.batch_size, self.train_output_sizes_h[0], self.train_output_sizes_w[0],
                                          self.anchor_per_scale, 5 + self.num_classes))
            batch_label_mbbox = np.zeros((self.batch_size, self.train_output_sizes_h[1], self.train_output_sizes_w[1],
                                          self.anchor_per_scale, 5 + self.num_classes))
            batch_label_lbbox = np.zeros((self.batch_size, self.train_output_sizes_h[2], self.train_output_sizes_w[2],
                                          self.anchor_per_scale, 5 + self.num_classes))
            # ================================================================ #
            batch_sbboxes = np.zeros((self.batch_size, self.max_bbox_per_scale, 4))
            batch_mbboxes = np.zeros((self.batch_size, self.max_bbox_per_scale, 4))
            batch_lbboxes = np.zeros((self.batch_size, self.max_bbox_per_scale, 4))

            num = 0 # sample in one batch's index
            if self.batch_count < self.num_batchs:
                while num < self.batch_size:
                    index = self.batch_count * self.batch_size + num
                    if index >= self.num_samples:  # 從頭開始
                        index -= self.num_samples
                    annotation = self.annotations[index]
                    # 樣本預處理
                    image, bboxes = self.parse_annotation(annotation)
                    # Anchor & GT 匹配
                    label_sbbox, label_mbbox, label_lbbox, sbboxes, mbboxes, lbboxes = self.preprocess_true_boxes(
                        bboxes)

                    batch_image[num, :, :, :] = image
                    batch_label_sbbox[num, :, :, :, :] = label_sbbox
                    batch_label_mbbox[num, :, :, :, :] = label_mbbox
                    batch_label_lbbox[num, :, :, :, :] = label_lbbox
                    batch_sbboxes[num, :, :] = sbboxes
                    batch_mbboxes[num, :, :] = mbboxes
                    batch_lbboxes[num, :, :] = lbboxes
                    num += 1
                self.batch_count += 1
                return batch_image, batch_label_sbbox, batch_label_mbbox, batch_label_lbbox, \
                       batch_sbboxes, batch_mbboxes, batch_lbboxes
            else:
                self.batch_count = 0
                np.random.shuffle(self.annotations)
                raise StopIteration

    def random_horizontal_flip(self, image, bboxes):

        if random.random() < 0.5:
            _, w, _ = image.shape
            image = image[:, ::-1, :]
            bboxes[:, [0, 2]] = w - bboxes[:, [2, 0]]

        return image, bboxes

    def random_crop(self, image, bboxes):

        if random.random() < 0.5:
            h, w, _ = image.shape
            max_bbox = np.concatenate([np.min(bboxes[:, 0:2], axis=0), np.max(bboxes[:, 2:4], axis=0)], axis=-1)

            max_l_trans = max_bbox[0]
            max_u_trans = max_bbox[1]
            max_r_trans = w - max_bbox[2]
            max_d_trans = h - max_bbox[3]

            crop_xmin = max(0, int(max_bbox[0] - random.uniform(0, max_l_trans)))
            crop_ymin = max(0, int(max_bbox[1] - random.uniform(0, max_u_trans)))
            crop_xmax = max(w, int(max_bbox[2] + random.uniform(0, max_r_trans)))
            crop_ymax = max(h, int(max_bbox[3] + random.uniform(0, max_d_trans)))

            image = image[crop_ymin: crop_ymax, crop_xmin: crop_xmax]

            bboxes[:, [0, 2]] = bboxes[:, [0, 2]] - crop_xmin
            bboxes[:, [1, 3]] = bboxes[:, [1, 3]] - crop_ymin

        return image, bboxes

    def random_translate(self, image, bboxes):

        if random.random() < 0.5:
            h, w, _ = image.shape
            max_bbox = np.concatenate([np.min(bboxes[:, 0:2], axis=0), np.max(bboxes[:, 2:4], axis=0)], axis=-1)

            max_l_trans = max_bbox[0]
            max_u_trans = max_bbox[1]
            max_r_trans = w - max_bbox[2]
            max_d_trans = h - max_bbox[3]

            tx = random.uniform(-(max_l_trans - 1), (max_r_trans - 1))
            ty = random.uniform(-(max_u_trans - 1), (max_d_trans - 1))

            M = np.array([[1, 0, tx], [0, 1, ty]])
            image = cv2.warpAffine(image, M, (w, h))

            bboxes[:, [0, 2]] = bboxes[:, [0, 2]] + tx
            bboxes[:, [1, 3]] = bboxes[:, [1, 3]] + ty

        return image, bboxes

    def parse_annotation(self, annotation):

        line = annotation.split()
        image_path = line[0]
        if not os.path.exists(image_path):
            raise KeyError("%s does not exist ... " % image_path)
        image = np.array(cv2.imread(image_path))
        bboxes = np.array([list(map(int, box.split(','))) for box in line[1:]])

        if self.data_aug:
            image, bboxes = self.random_horizontal_flip(np.copy(image), np.copy(bboxes))
            image, bboxes = self.random_crop(np.copy(image), np.copy(bboxes))
            image, bboxes = self.random_translate(np.copy(image), np.copy(bboxes))

        image, bboxes = utils.image_preporcess(np.copy(image), [self.train_input_size_h, self.train_input_size_w],
                                               np.copy(bboxes))
        return image, bboxes

    def bbox_iou(self, boxes1, boxes2):

        boxes1 = np.array(boxes1)
        boxes2 = np.array(boxes2)

        boxes1_area = boxes1[..., 2] * boxes1[..., 3]
        boxes2_area = boxes2[..., 2] * boxes2[..., 3]

        boxes1 = np.concatenate([boxes1[..., :2] - boxes1[..., 2:] * 0.5,
                                 boxes1[..., :2] + boxes1[..., 2:] * 0.5], axis=-1)
        boxes2 = np.concatenate([boxes2[..., :2] - boxes2[..., 2:] * 0.5,
                                 boxes2[..., :2] + boxes2[..., 2:] * 0.5], axis=-1)

        left_up = np.maximum(boxes1[..., :2], boxes2[..., :2])
        right_down = np.minimum(boxes1[..., 2:], boxes2[..., 2:])

        inter_section = np.maximum(right_down - left_up, 0.0)
        inter_area = inter_section[..., 0] * inter_section[..., 1]
        union_area = boxes1_area + boxes2_area - inter_area

        return inter_area / union_area

    def preprocess_true_boxes(self, bboxes):
        # ================================================================ #
        label = [np.zeros((self.train_output_sizes_h[i], self.train_output_sizes_w[i], self.anchor_per_scale,
                           5 + self.num_classes)) for i in range(3)]
        """ match info
        hypothesis input size 320 x 480, label dim
        | 40 x 60 x 3 x 17 |
        | 20 x 30 x 3 x 17 |
        | 10 x 15 x 3 x 17 |
        """
        bboxes_xywh = [np.zeros((self.max_bbox_per_scale, 4)) for _ in range(3)]
        """ match gt set
        bboxes_xywh dim
        | 3 x 150 x 4 |
        """
        bbox_count = np.zeros((3,))
        # ================================================================ #
        for bbox in bboxes:
            bbox_coor = bbox[:4] # xmin, ymin, xmax, ymax
            bbox_class_ind = bbox[4] # class

            # smooth onehot label
            onehot = np.zeros(self.num_classes, dtype=np.float)
            onehot[bbox_class_ind] = 1.0
            uniform_distribution = np.full(self.num_classes, 1.0 / self.num_classes)
            deta = 0.01
            smooth_onehot = onehot * (1 - deta) + deta * uniform_distribution

            # box transform into 3 feature maps [center_x, center_y, w, h]
            bbox_xywh = np.concatenate([(bbox_coor[2:] + bbox_coor[:2]) * 0.5, bbox_coor[2:] - bbox_coor[:2]], axis=-1)
            bbox_xywh_scaled = 1.0 * bbox_xywh[np.newaxis, :] / self.strides[:, np.newaxis]

            # =========================== match iou ========================== #
            iou = [] # 3x3
            exist_positive = False
            for i in range(3): # different feature map
                anchors_xywh = np.zeros((self.anchor_per_scale, 4))
                anchors_xywh[:, 0:2] = np.floor(bbox_xywh_scaled[i, 0:2]).astype(np.int32) + 0.5
                anchors_xywh[:, 2:4] = self.anchors[i]

                iou_scale = self.bbox_iou(bbox_xywh_scaled[i][np.newaxis, :], anchors_xywh)
                iou.append(iou_scale)
                iou_mask = iou_scale > 0.3

                if np.any(iou_mask):
                    xind, yind = np.floor(bbox_xywh_scaled[i, 0:2]).astype(np.int32)

                    label[i][yind, xind, iou_mask, :] = 0
                    label[i][yind, xind, iou_mask, 0:4] = bbox_xywh
                    label[i][yind, xind, iou_mask, 4:5] = 1.0
                    label[i][yind, xind, iou_mask, 5:] = smooth_onehot

                    bbox_ind = int(bbox_count[i] % self.max_bbox_per_scale)
                    bboxes_xywh[i][bbox_ind, :4] = bbox_xywh
                    bbox_count[i] += 1

                    exist_positive = True

            if not exist_positive:
                best_anchor_ind = np.argmax(np.array(iou).reshape(-1), axis=-1)
                best_detect = int(float(best_anchor_ind) / self.anchor_per_scale)
                best_anchor = int(best_anchor_ind % self.anchor_per_scale)
                xind, yind = np.floor(bbox_xywh_scaled[best_detect, 0:2]).astype(np.int32)

                label[best_detect][yind, xind, best_anchor, :] = 0
                label[best_detect][yind, xind, best_anchor, 0:4] = bbox_xywh
                label[best_detect][yind, xind, best_anchor, 4:5] = 1.0
                label[best_detect][yind, xind, best_anchor, 5:] = smooth_onehot

                bbox_ind = int(bbox_count[best_detect] % self.max_bbox_per_scale)
                bboxes_xywh[best_detect][bbox_ind, :4] = bbox_xywh
                bbox_count[best_detect] += 1
        label_sbbox, label_mbbox, label_lbbox = label # different size feature map's anchor match info
        sbboxes, mbboxes, lbboxes = bboxes_xywh       # different size feature map's matched gt set
        return label_sbbox, label_mbbox, label_lbbox, sbboxes, mbboxes, lbboxes

    def __len__(self):
        return self.num_batchs


if __name__ == '__main__':
    val = DataSet('test')
    for idx in range(val.num_batchs):
        batch_image, batch_label_sbbox, batch_label_mbbox, batch_label_lbbox, \
        batch_sbboxes, batch_mbboxes, batch_lbboxes = val.next()
    print('# ================================================================ #')
View Code

這部分是用來加載數據的。

整個 core/dataset.py 實現了一個 DataSet 類,每個 batch 的數據通過迭代函數 next() 獲得。

self.trainset = DataSet('train')
pbar = tqdm(self.trainset)
for train_data in pbar:
    ....
    pbar.set_description(("train loss: %.2f" "learn rate: %e") % (train_step_loss, learn_rate))

for test_data in self.testset:
    ....

函數返回 

batch_image       # one batch resized images
batch_label_sbbox # 第一個尺度下的匹配結果
batch_label_mbbox # 第二個尺度下的匹配結果
batch_label_lbbox # 第三個尺度下的匹配結果
batch_sbboxes     # 第一個尺度下匹配的 GT 集合
batch_mbboxes     # 第二個尺度下匹配的 GT 集合
batch_lbboxes     # 第三個尺度下匹配的 GT 集合

其中 batch_image 為網絡輸入特征,按照 NxHxWxC 維度排列batch_label_sbbox、batch_label_mbbox 、batch_label_lbbox 這三個用以確定不同尺度下的 anchor 做正樣本還是負樣本;batch_sbboxes、batch_mbboxes、batch_lbboxes 這三個就有點意思了,是為了后續區分負樣本是否有可能變成正樣本(回歸到 bbox 了) 做准備。

,可以繼續往下看。數據集在 DataSet 初始化的時候就被打亂了,如果你想每次調試運行看看上面的代碼是怎么工作的,可以再 load_annotations() 函數里固定隨機因子

np.random.seed(1)

數據格式

不管你訓練什么數據集(VOC、COCO),數據的標注格式都要轉換成以下格式 ' image_path [bbox class] ...':

D:/tyang/drive0703/JPEGImages/test_dataset_without2018/2017_07_24_10_49_026073.jpg 466,396,582,475,0 289,394,377,449,0 689,432,788,504,0 778,435,839,481,0 856,433,887,458,2 612,419,653,446,3

當然,scripts/voc_annotation.py 提供了將 VOC 的 xml 格式轉換成需求格式,這部分不是很難,其他格式的數據集,自己瞎寫寫轉一轉也沒啥問題。

數據預處理

對於每個樣本,先經過預處理函數 parse_annotation() 加載 image & bboxes,對於訓練數據,這里會執行一些 data argument。

假定不做 data argument,bboxes 就只是變成了縮放后的圖片下的框坐標值

Anchor & GT 匹配

然后,最重要的部分來了,通過 preprocess_true_boxes() 來實現 Anchor & GT 匹配:

label = [np.zeros((self.train_output_sizes_h[i], self.train_output_sizes_w[i], self.anchor_per_scale,
                           5 + self.num_classes)) for i in range(3)]
""" match info
hypothesis input size 320 x 480 with 12 classes, label dim
| 40 x 60 x 3 x 17 | # label_sbbox
| 20 x 30 x 3 x 17 | # label_mbbox
| 10 x 15 x 3 x 17 | # label_lbbox
"""
bboxes_xywh = [np.zeros((self.max_bbox_per_scale, 4)) for _ in range(3)]
""" match gt set
bboxes_xywh dim
| 3 x 150 x 4 |      # sbboxes, mbboxes, lbboxes
"""

label 里保存的是三個尺度下的 Anchor 的匹配情況;bboxes_xywh 里則保存的是三個尺度小被匹配上的 GT 集合。

網絡在三個尺度的 feature map 下檢測目標,這三個尺度的 feature map 由輸入圖片分別經過 stride=8, 16, 32 獲得。

假定訓練中輸入尺度是 320x480,那么這三個 feature map size: 40x60, 20x30, 10x15。

對於每個 feature map 下,作者分別設計(聚類)了三種 anchor,例如 data/anchors/basline_anchors.txt 中:

# small
1.25,1.625
2.0,3.75
4.125,2.875 
# middle
1.875,3.8125
3.875,2.8125
3.6875,7.4375
# large
3.625,2.8125
4.875,6.1875
11.65625,10.1875

因此,第一個尺度里有 40 x 60 x 3 = 7200 個 anchor;第二個尺度里有 20 x 30 x 3 = 1800 個 anchor;第三個尺度里有 10x 15x 3 = 450 個 anchor。

值得注意的是,原始 bbox 是按照 [xmin, ymin, xmax, ymax],需要轉換成 [center_x, center_y, w, h] 被保存在匹配結果里,即:

[116 117 145 140] -> [130.5 128.5 29. 23. ]
[ 72 116 94 133] -> [ 83. 124.5 22. 17. ]
[172 128 197 149] -> [184.5 138.5 25. 21. ]
[194 128 209 142] -> [201.5 135. 15. 14. ]
[214 128 221 135] -> [217.5 131.5 7. 7. ]
[153 124 163 132] -> [158. 128. 10. 8. ]

label 最后一個維度是 5 + self.num_classes,其中 5, 前 4 維是 [center_x, center_y, w, h] GT bbox, 第 5 維是 0/1, 0 表示無匹配,1 表示匹配成功。self.num_classes 用來表示目標類別,之所以要用這么多維數據來表示,是因為將整形 label 轉換成了 one-hot 形式。同時這里做了 label smooth 操作,例如 label 0 ->[9.90833333e-01 8.33333333e-04 8.33333333e-04 8.33333333e-04 8.33333333e-04 8.33333333e-04 8.33333333e-04 8.33333333e-04 8.33333333e-04 8.33333333e-04 8.33333333e-04 8.33333333e-04]

# smooth onehot label
onehot = np.zeros(self.num_classes, dtype=np.float)
onehot[bbox_class_ind] = 1.0
uniform_distribution = np.full(self.num_classes, 1.0 / self.num_classes)
deta = 0.01
smooth_onehot = onehot * (1 - deta) + deta * uniform_distribution

每個 GT bbox 會和三個尺度上所在位置處所有的 anchor 嘗試匹配 match anchor,並將匹配結果保存作為網絡訓練輸入的一部分。對於每個 bbox(下面會以 [130.5 128.5  29.   23. ] 這個 bbox 為例):

  • 將 bbox 映射到每個 feature map 上,獲得 bbox_xywh_scaled
  • 在每個尺度上嘗試匹配,為了計算效率,只利用中心的在 bbox 中心點的 anchor 嘗試匹配,例如第一個尺度上的 bbox=[16.3125 16.0625  3.625   2.875 ] 將嘗試與 small anchor 集合(anchors_xywh進行匹配:
  • 匹配計算 bbox_iou -> [0.19490255 0.47240051 0.65717606],如果找到滿足大於 0.3 的一對,即為匹配成功 (False, True, True)。匹配成功后就往 labelbboxes_xywh 里填信息就好了。
    """
    label[1][16, 16, [False  True  True], :] = 0
    label[1][16, 16, [False  True  True], 0:4] = bbox_xywh #[130.5  128.5  29.  23.]
    label[1][16, 16, [False  True  True], 4:5] = 1.0
    label[1][16, 16, [False  True  True], 5:] = smooth_obehot
    bboxes_xywh[1][bbox_ind, :4] = bbox_xywh #[130.5  128.5  29.   23. ]
    """ bbox_ind 是這個尺度下累計匹配成功的 GT 數量,代碼中限制范圍 [0-149]
  • 如果有 bbox 在各個 feature map 都找不到滿足的匹配 anchor,那就退而求其次,在所有三個尺度下的 anchor(9個) 里尋找一個最大匹配就好了。

 到此,匹配結束。

train.py

INPUT

輸入層對應 dataset.py 每個batch 返回的變量

with tf.name_scope('define_input'):
    self.input_data = tf.placeholder(dtype=tf.float32, name='input_data', shape=[None, None, None, 3])
    self.label_sbbox = tf.placeholder(dtype=tf.float32, name='label_sbbox')
    self.label_mbbox = tf.placeholder(dtype=tf.float32, name='label_mbbox')
    self.label_lbbox = tf.placeholder(dtype=tf.float32, name='label_lbbox')
    self.true_sbboxes = tf.placeholder(dtype=tf.float32, name='sbboxes')
    self.true_mbboxes = tf.placeholder(dtype=tf.float32, name='mbboxes')
    self.true_lbboxes = tf.placeholder(dtype=tf.float32, name='lbboxes')
    self.trainable = tf.placeholder(dtype=tf.bool, name='training')

MODEL & LOSS

YOLOV3 的 loss 分為三部分,回歸 loss, 二分類(前景/背景) loss, 類別分類 loss

with tf.name_scope("define_loss"):
    self.model = YOLOV3(self.input_data, self.trainable, self.net_flag)
    self.net_var = tf.global_variables()
    self.giou_loss, self.conf_loss, self.prob_loss = self.model.compute_loss(self.label_sbbox,
                                                                             self.label_mbbox,
                                                                             self.label_lbbox,
                                                                             self.true_sbboxes,
                                                                             self.true_mbboxes,
                                                                             self.true_lbboxes)
    self.loss = self.giou_loss + self.conf_loss + self.prob_loss

Learning rate

with tf.name_scope('learn_rate'):
    self.global_step = tf.Variable(1.0, dtype=tf.float64, trainable=False, name='global_step')
    warmup_steps = tf.constant(self.warmup_periods * self.steps_per_period,
                               dtype=tf.float64, name='warmup_steps') # warmup_periods epochs
    train_steps = tf.constant((self.first_stage_epochs + self.second_stage_epochs) * self.steps_per_period,
                              dtype=tf.float64, name='train_steps')
    self.learn_rate = tf.cond(
        pred=self.global_step < warmup_steps,
        true_fn=lambda: self.global_step / warmup_steps * self.learn_rate_init,
        false_fn=lambda: self.learn_rate_end + 0.5 * (self.learn_rate_init - self.learn_rate_end) * (
                    1 + tf.cos((self.global_step - warmup_steps) / (train_steps - warmup_steps) * np.pi)))
    global_step_update = tf.assign_add(self.global_step, 1.0)
    """
    訓練分為兩個階段,第一階段里前面又划分出一段作為“熱身階段”:
    熱身階段:learn_rate = (global_step / warmup_steps) * learn_rate_init
    其他階段:learn_rate_end + 0.5 * (learn_rate_init - learn_rate_end) * (
                    1 + tf.cos((global_step - warmup_steps) / (train_steps - warmup_steps) * np.pi))
    """

假定遍歷一遍數據集需要 100 batch, warmup_periods=2, first_stage_epochs=20, second_stage_epochs=30, learn_rate_init=1e-4, learn_rate_end=1e-6, 那么整個訓練過程中學習率是這樣的:

import numpy as np
import matplotlib.pyplot as plt

steps_per_period = 100
warmup_periods=2
first_stage_epochs=20
second_stage_epochs=30
learn_rate_init=1e-4
learn_rate_end=1e-6
warmup_steps = warmup_periods * steps_per_period
train_steps = (first_stage_epochs + second_stage_epochs) * steps_per_period

def learn_rate_strategy(global_step, warmup_steps, train_steps,
                        learn_rate_init, learn_rate_end):
    """

    :param global_step:
    :param warmup_steps:
    :param learn_rate_init:
    :param learn_rate_end:
    :return:
    """
    if global_step < warmup_steps:
        learn_rate = (global_step / warmup_steps) * learn_rate_init
    else:
        learn_rate = learn_rate_end + 0.5 * (learn_rate_init - learn_rate_end) * (
                            1 + np.cos((global_step - warmup_steps) / (train_steps - warmup_steps) * np.pi))
    return learn_rate

learn_rate_list = []

for step in range(train_steps):
    learing_rate = learn_rate_strategy(step, warmup_steps, train_steps,
                        learn_rate_init, learn_rate_end)
    learn_rate_list.append(learing_rate)

step = range(train_steps)

print(learn_rate_list[-1])

plt.plot(step, learn_rate_list, 'g-', linewidth=2, label='learing_rate')
plt.xlabel('step')
plt.ylabel('learing rate')
plt.legend(loc='upper right')
plt.tight_layout()
plt.show()
View Code

two stage train 

整個訓練按照任務划分成了兩個階段,之所以這么設計,是考慮作者是拿原始的 DarkNet 來 finetune 的。

finetune 的一般流程就是,利用預訓練的模型賦初值,先固定 backbone,只訓練最后的分類/回歸層。然后放開全部訓練。

也可以對於淺層特征可以用小的學習率來微調(因為網絡里淺層特征提取的邊界紋理信息可能都是相近的,不需要作大調整),越接近於輸出層可能需要調整的越多,輸出層因為沒有用其他模型初始化(隨機初始化),因此需要從頭訓練。

for epoch in range(1, 1 + self.first_stage_epochs + self.second_stage_epochs):
    if epoch <= self.first_stage_epochs:
        train_op = self.train_op_with_frozen_variables
    else:
        train_op = self.train_op_with_all_variables

first_stage_train

這個階段將專注於訓練最后的檢測部分,即分類和回歸

with tf.name_scope("define_first_stage_train"):
    self.first_stage_trainable_var_list = []
    for var in tf.trainable_variables():
        var_name = var.op.name
        var_name_mess = str(var_name).split('/')
        if var_name_mess[0] in ['conv_sbbox', 'conv_mbbox', 'conv_lbbox']:
            self.first_stage_trainable_var_list.append(var)

    first_stage_optimizer = tf.train.AdamOptimizer(self.learn_rate).minimize(self.loss,
                                                                             var_list=self.first_stage_trainable_var_list)
    with tf.control_dependencies(tf.get_collection(tf.GraphKeys.UPDATE_OPS)):
        with tf.control_dependencies([first_stage_optimizer, global_step_update]):
            with tf.control_dependencies([moving_ave]):
                self.train_op_with_frozen_variables = tf.no_op()

second_stage_train

這個階段就是整體訓練,沒什么好說的

with tf.name_scope("define_second_stage_train"):
    second_stage_trainable_var_list = tf.trainable_variables()
    second_stage_optimizer = tf.train.AdamOptimizer(self.learn_rate).minimize(self.loss,
                                                                              var_list=second_stage_trainable_var_list)

    with tf.control_dependencies(tf.get_collection(tf.GraphKeys.UPDATE_OPS)):
        with tf.control_dependencies([second_stage_optimizer, global_step_update]):
            with tf.control_dependencies([moving_ave]):
                self.train_op_with_all_variables = tf.no_op()

ExponentialMovingAverage

with tf.name_scope("define_weight_decay"):
    moving_ave = tf.train.ExponentialMovingAverage(self.moving_ave_decay).apply(tf.trainable_variables())

這個我涉世未深,還不甚明了,參見 tf.train.ExponentialMovingAverage

core/backbone.py

這里定義了 Darknet-53 的主題框架

網絡結構 代碼結構

當然,你也可以根據你的需要,定義一些其他的 backbone,例如 mobilenet_v2。

core/yolov3.py

這里是整個 YOLOV3 代碼的靈魂之處了。

__build_nework

YOLOV3 同 SSD 一樣,是多尺度目標檢測。選擇了 stride=8, 16, 32 三個尺度的 feature map 來設計 anchor, 以便分別實現對小、中和大物體的預測。

假定輸入尺度是 320x480,那么這三個 feature map 的大小就是 40x60, 20x30, 10x15。分別在這三個尺度的 feature map 的基礎上,通過 3*(4+1+classes) 個 3x3 的卷積核卷積來預測分類和回歸結果。這里 3 代表每個尺度下設計了 3 中不同尺寸的 anchor,4 是 bbox 回歸預測,1 則是表示該 anchor 是否包含目標,classes 則是你的數據集里具體的類別數量了。如此,可以推測出每個尺度下的預測輸出維度為(我的數據集包含 12 個類別目標):

batch_size x 40 x 60 x 51
batch_size x 20 x 30 x 51
batch_size x 10 x 15 x 51

這些預測輸出將和 core/dataset.py 文件里獲得的 GT 信息作比較,計算 loss。

upsample

網絡在 backbone 特征提取的基礎上加上了上采樣特征連接,加強了淺層特征表示。

def upsample(input_data, name, method="deconv"):
    assert method in ["resize", "deconv"]

    if method == "resize":
        with tf.variable_scope(name):
            input_shape = tf.shape(input_data)
            output = tf.image.resize_nearest_neighbor(input_data, (input_shape[1] * 2, input_shape[2] * 2))

    if method == "deconv":
        # replace resize_nearest_neighbor with conv2d_transpose To support TensorRT optimization
        numm_filter = input_data.shape.as_list()[-1]
        output = tf.layers.conv2d_transpose(input_data, numm_filter, kernel_size=2, padding='same',
                                            strides=(2, 2), kernel_initializer=tf.random_normal_initializer())

這里提供了兩種實現方式,最近鄰縮放和反卷積。

decode

SSD 類似,anchor 的回歸並非直接坐標回歸,而是通過編碼后進行回歸:

我們知道,檢測框實際上是在先驗框的基礎上回歸出來的。如上圖所示:在其中一個輸出尺度下的 feature map 上,有一個黑色的先驗框($c_x, c_y, p_w, p_h$),其中 $c_x$ 和 $c_y$ 分別表示中心網格距離圖像左上角的距離,$p_w$ 和 $p_h$ 則分別表示先驗框的寬和高。

記網絡回歸輸出為($t_x, t_y, t_w, t_h$),其中$t_x$ 和 $t_y$ 用以偏移先驗框的中心到檢測框,$t_w$ 和 $t_h$ 則用來縮放先驗框到檢測框大小,那么藍色的檢測框($b_x, b_y, b_w, b_h$)可以用以下表達式表示:

\begin{equation}
\label{a}
\begin{split}
& b_x =  \sigma(t_x) + c_x \\
& b_y =  \sigma(t_y) + c_y \\
& b_w =  p_w e^{t_w} \\
& b_h =  p_h e^{t_h} \\
\end{split}
\end{equation}

 具體實現:

def decode(self, conv_output, anchors, stride):
    """
    return tensor of shape [batch_size, output_size, output_size, anchor_per_scale, 5 + num_classes]
           contains (x, y, w, h, score, probability)
    """

    conv_shape = tf.shape(conv_output)
    batch_size = conv_shape[0]
    output_size_h = conv_shape[1]
    output_size_w = conv_shape[2]
    anchor_per_scale = len(anchors)

    conv_output = tf.reshape(conv_output,
                             (batch_size, output_size_h, output_size_w, anchor_per_scale, 5 + self.num_class))

    conv_raw_dxdy = conv_output[:, :, :, :, 0:2]
    conv_raw_dwdh = conv_output[:, :, :, :, 2:4]
    conv_raw_conf = conv_output[:, :, :, :, 4:5]
    conv_raw_prob = conv_output[:, :, :, :, 5:]

    # 划分網格
    y = tf.tile(tf.range(output_size_h, dtype=tf.int32)[:, tf.newaxis], [1, output_size_w])
    x = tf.tile(tf.range(output_size_w, dtype=tf.int32)[tf.newaxis, :], [output_size_h, 1])

    xy_grid = tf.concat([x[:, :, tf.newaxis], y[:, :, tf.newaxis]], axis=-1)
    xy_grid = tf.tile(xy_grid[tf.newaxis, :, :, tf.newaxis, :], [batch_size, 1, 1, anchor_per_scale, 1])
    xy_grid = tf.cast(xy_grid, tf.float32) # 計算網格左上角的位置

    # 根據論文公式計算預測框的中心位置
    pred_xy = (tf.sigmoid(conv_raw_dxdy) + xy_grid) * stride
    # 根據論文公式計算預測框的長和寬大小
    pred_wh = (tf.exp(conv_raw_dwdh) * anchors) * stride
    # 合並邊界框的位置和長寬信息
    pred_xywh = tf.concat([pred_xy, pred_wh], axis=-1)

    pred_conf = tf.sigmoid(conv_raw_conf) # 計算預測框里object的置信度
    pred_prob = tf.sigmoid(conv_raw_prob) # 計算預測框里object的類別概率

    return tf.concat([pred_xywh, pred_conf, pred_prob], axis=-1)

compute_loss & loss_layer

代碼分別從三個尺度出發,分別計算 邊界框損失(giou_loss)、是否包含目標的置信度損失(conf_loss)以及具體類別的分類損失(prob_loss)

def compute_loss(self, label_sbbox, label_mbbox, label_lbbox, true_sbbox, true_mbbox, true_lbbox):

    with tf.name_scope('smaller_box_loss'):
        loss_sbbox = self.loss_layer(self.conv_sbbox, self.pred_sbbox, label_sbbox, true_sbbox,
                                     anchors=self.anchors[0], stride=self.strides[0])

    with tf.name_scope('medium_box_loss'):
        loss_mbbox = self.loss_layer(self.conv_mbbox, self.pred_mbbox, label_mbbox, true_mbbox,
                                     anchors=self.anchors[1], stride=self.strides[1])

    with tf.name_scope('bigger_box_loss'):
        loss_lbbox = self.loss_layer(self.conv_lbbox, self.pred_lbbox, label_lbbox, true_lbbox,
                                     anchors=self.anchors[2], stride=self.strides[2])

    with tf.name_scope('giou_loss'):
        giou_loss = loss_sbbox[0] + loss_mbbox[0] + loss_lbbox[0]

    with tf.name_scope('conf_loss'):
        conf_loss = loss_sbbox[1] + loss_mbbox[1] + loss_lbbox[1]

    with tf.name_scope('prob_loss'):
        prob_loss = loss_sbbox[2] + loss_mbbox[2] + loss_lbbox[2]

    return giou_loss, conf_loss, prob_loss

GIoU Loss

同 SSD(smooth L1 loss) 等檢測算法相比,這里使用 GIoU 來衡量檢測框和 GT bbox 之間的差距,具體可以參考論文和本代碼作者的解讀

giou = tf.expand_dims(self.bbox_giou(pred_xywh, label_xywh), axis=-1)
input_size_w = tf.cast(input_size_w, tf.float32)
input_size_h = tf.cast(input_size_h, tf.float32)
# giou_loss 權重, [1, 2], 增加小目標的回歸權重
bbox_loss_scale = 2.0 - 1.0 * label_xywh[:, :, :, :, 2:3] * label_xywh[:, :, :, :, 3:4] / (
        input_size_w * input_size_h)
# 所有匹配框計算回歸 loss, 未匹配的 anchor 計算出來的 giou 值是無效值,因為 label_xywh 為 0
giou_loss = respond_bbox * bbox_loss_scale * (1 - giou) # giou_loss = (2 - bbox_area/image_area) * (1 - giou)

 Focal Loss

網格中的 anchor 是否包含目標,這是個邏輯回歸問題。作者這里引入了 Focal Loss,給純背景框的 loss 進行壓縮,Focal loss 的作用可參考論文

iou = self.bbox_iou(pred_xywh[:, :, :, :, np.newaxis, :], bboxes[:, np.newaxis, np.newaxis, np.newaxis, :, :])
# 找出與真實框 iou 值最大的預測框(3 種 anchor 回歸后取 1,匹配最好的Anchor經過回歸后不一定是與 bbox iou 最高)
max_iou = tf.expand_dims(tf.reduce_max(iou, axis=-1), axis=-1)
# 匹配階段和回歸后均為負樣本的純背景 anchor (意味着匹配階段為負樣本,回歸性能卻不錯的 Anchor 被 conf_loss 忽略)
respond_bgd = (1.0 - respond_bbox) * tf.cast(max_iou < self.iou_loss_thresh, tf.float32)
# 計算 loss 權重,給予分錯檢測框 loss 更多的懲罰
conf_focal = self.focal(respond_bbox, pred_conf)
# 計算置信度的損失
conf_loss = conf_focal * (
        # 匹配階段為正樣本的 anchor 分類 loss
        respond_bbox * tf.nn.sigmoid_cross_entropy_with_logits(labels=respond_bbox, logits=conv_raw_conf) 
        +
        # 純背景 anchor 分類 loss
        respond_bgd * tf.nn.sigmoid_cross_entropy_with_logits(labels=respond_bbox, logits=conv_raw_conf) 
) # 這里對匹配階段為負樣本, 經過回歸后和 bbox 的 iou > 0.5 的 anchor, 不計算分類 loss

分類損失

最后這個分類損失就沒什么好說的了,采用的是二分類的交叉熵,即把所有類別的分類問題歸結為是否屬於這個類別,這樣就把多分類看做是二分類問題。

prob_loss = respond_bbox * tf.nn.sigmoid_cross_entropy_with_logits(labels=label_prob, logits=conv_raw_prob)

K-means

先驗框的設計一直是個比較頭疼的問題,我在 SSD 里設計的 anchor 基本上是基於數據集的目標分布來人工設計出來的,這種設計比一定合適,而且很麻煩

YOLOV3 里就比較偷懶了,他也是根據數據集分布來設計的,只不過人家更高級,用聚類來自動實現。

we run k-means clustering on the training set bounding boxes to automatically find good priors.

即使用 k-means 算法對訓練集上的 boudnding box 尺度做聚類。此外,考慮到訓練集上的圖片尺寸不一,因此對此過程進行歸一化處理。

k-means 聚類算法有個坑爹的地方在於,類別的個數需要事先指定。這就帶來一個問題,先驗框 anchor 的數目等於多少最合適?一般來說,anchor 的類別越多,那么 YOLO 算法就越能在不同尺度下與真實框進行回歸,但是這樣就導致模型的復雜度更高,網絡的參數量更龐大。

We choose k = 5 as a good tradeoff between model complexity and high recall. If we use 9 centroids we see a much higher average IOU. This indicates that using k-means to generate our bounding box starts the model off with a better representation and makes the task easier to learn.

在上面這張圖里,作者發現 k=5 時就能較好的實現高召回率和模型復雜度之間的平衡。由於在 YOLOV3 算法里一種有 3 個尺度預測,因此只能是 3 的倍數,所以最終選擇了 9 個先驗框。這里還有個問題需要解決,k-means 度量距離的選取很關鍵。距離度量如果使用標准的歐氏距離,大框框就會比小框產生更多的錯誤。在目標檢測領域,我們度量兩個邊界框之間的相似度往往以 IOU 大小作為標准。因此,這里的度量距離也和 IOU 有關。需要特別注意的是,這里的IOU計算只用到了 boudnding box 的長和寬。在作者的代碼里,是認為兩個先驗框的左上角位置是相重合的。(其實在這里偏移至哪都無所謂,因為聚類的時候是不考慮 anchor 框的位置信息的。)

\begin{equation}
\label{b}
d(box, centroid) = 1 - IOU(box, centroid)
\end{equation}

如果兩個邊界框之間的IOU值越大,那么它們之間的距離就會越小。

def kmeans(boxes, k, dist=np.median,seed=1):
    """
    Calculates k-means clustering with the Intersection over Union (IoU) metric.
    :param boxes: numpy array of shape (r, 2), where r is the number of rows
    :param k: number of clusters
    :param dist: distance function
    :return: numpy array of shape (k, 2)
    """
    rows = boxes.shape[0]

    distances     = np.empty((rows, k)) ## N row x N cluster
    last_clusters = np.zeros((rows,))

    np.random.seed(seed)

    # initialize the cluster centers to be k items
    clusters = boxes[np.random.choice(rows, k, replace=False)]

    while True:
        # 為每個點指定聚類的類別(如果這個點距離某類別最近,那么就指定它是這個類別)
        for icluster in range(k): # I made change to lars76's code here to make the code faster
            distances[:,icluster] = 1 - iou(clusters[icluster], boxes)

        nearest_clusters = np.argmin(distances, axis=1)
    # 如果聚類簇的中心位置基本不變了,那么迭代終止。
        if (last_clusters == nearest_clusters).all():
            break
            
        # 重新計算每個聚類簇的平均中心位置,並它作為聚類中心點
        for cluster in range(k):
            clusters[cluster] = dist(boxes[nearest_clusters == cluster], axis=0)

        last_clusters = nearest_clusters

    return clusters,nearest_clusters,distances

NMS 處理

 熟悉檢測的人一般都了解非極大值抑制(Non-Maximum Suppression,NMS)這一過程。直白的理解就是去除掉那些重疊率較高並且 score 評分較低的檢測框。

NMS 的算法非常簡單,迭代流程如下:

  • 流程1:判斷檢測框的數目是否大於0,如果不是則結束迭代;
  • 流程2:按照 score 排序選出評分最大的檢測框 A 並取出;
  • 流程3:計算這個邊界框 A 與剩下所有檢測框的 iou 並剔除那些 iou 值高於閾值的其他檢測框,重復上述步驟
while len(cls_bboxes) > 0:
    max_ind = np.argmax(cls_bboxes[:, 4])
    best_bbox = cls_bboxes[max_ind]
    best_bboxes.append(best_bbox)
    cls_bboxes = np.concatenate([cls_bboxes[: max_ind], cls_bboxes[max_ind + 1:]])
    iou = bboxes_iou(best_bbox[np.newaxis, :4], cls_bboxes[:, :4])
    weight = np.ones((len(iou),), dtype=np.float32)

    assert method in ['nms', 'soft-nms']

    if method == 'nms':
        iou_mask = iou > iou_threshold
        weight[iou_mask] = 0.0

    if method == 'soft-nms':
        weight = np.exp(-(1.0 * iou ** 2 / sigma))

    cls_bboxes[:, 4] = cls_bboxes[:, 4] * weight
    score_mask = cls_bboxes[:, 4] > 0.
    cls_bboxes = cls_bboxes[score_mask]

在 YOLO 算法中,NMS 的處理有兩種情況:一種是所有的預測框一起做 NMS 處理,另一種情況是分別對每個類別的預測框做 NMS 處理。后者會出現一個預測框既屬於類別 A 又屬於類別 B 的現象,這比較適合於一個小單元格中同時存在多個物體的情況。

evaluate.py

該代碼輸出包括三部分:

1. 每張圖片的檢測結果(文件名按從0開始的序號表示了,mAP/predicted/

# class score xmin ymin xmax ymax
car 0.9625 680 425 780 504
car 0.9160 470 397 591 471
car 0.9015 775 434 840 489
car 0.4170 293 392 370 429
bus 0.4381 844 433 887 468
van 0.4556 613 411 653 444

2. 每張圖片的GT(mAP/ground-truth/)

# class xmin ymin xmax ymax
car 466 396 582 475
car 289 394 377 449
car 689 432 788 504
car 778 435 839 481
bus 856 433 887 458
van 612 419 653 446

3. 每張圖片可視化檢測結果(self.write_image_path)

mAP/main.py

這部分就是利用上一步得到的 GT 和檢測結果來計算 mAP 的過程了。

搬運源作者的訓練技巧

1. Xavier initialization

2. 學習率的設置

學習率是最影響性能的超參數之一,如果我們只能調整一個超參數,那么最好的選擇就是它。 其實在我們的大多數的煉丹過程中,遇到 loss 變成 NaN 的情況大多數是由於學習率選擇不當引起的。有句話講得好啊,步子大了容易扯到蛋。由於神經網絡在剛開始訓練的時候是非常不穩定的,因此剛開始的學習率應當設置得很低很低,這樣可以保證網絡能夠具有良好的收斂性。但是較低的學習率會使得訓練過程變得非常緩慢,因此這里會采用以較低學習率逐漸增大至較高學習率的方式實現網絡訓練的“熱身”階段,稱為 warmup stage。但是如果我們使得網絡訓練的 loss 最小,那么一直使用較高學習率是不合適的,因為它會使得權重的梯度一直來回震盪,很難使訓練的損失值達到全局最低谷。因此最后采用了這篇論文里的 cosine 的衰減方式,這個階段可以稱為 consine decay stage。

 

 

 3. 加載預訓練模型

其實加載預訓練模型,也是避免梯度溢出的一種有效方式。

目前針對目標檢測的主流做法是基於 Imagenet 數據集預訓練的模型來提取特征,然后在 COCO 數據集進行目標檢測fine-tunning訓練(比如 yolo 算法),也就是大家常說的遷移學習。其實遷移學習是建立在數據集分布相似的基礎上的,像 yymnist 這種與 COCO 數據集分布完全不同的情況,就沒有必要加載 COCO 預訓練模型的必要了吧。

在 tensorflow-yolov3 版本里,由於 README 里訓練的是 VOC 數據集,因此推薦加載預訓練模型。由於在 YOLOv3 網絡的三個分支里的最后卷積層與訓練的類別數目有關,因此除掉這三層的網絡權重以外,其余所有的網絡權重都加載進來了。

我加載的是作者已經訓練好的網絡,因此可以這么干。但事實上作者是利用 darknet53 網絡在 Imagenet 數據集上進行分類訓練得到 darknet53.conv.74 權重后,再加載至 YOLOv3 網絡里進行目標檢測訓練的!

作者在 PASCAL VOC 2012 上比賽刷到了 88.38% 的成績

Convert to PB

 1. 調用 convert_weight.py 去掉 ckpt 模型中有關訓練的冗余參數,比較轉換前后的模型,你會發現大小變小了很多

2. 調用 freeze_graph.py 轉換 ckpt 為 pb 模型

3. 最后可以調用 video_demo.py 測試下模型

小結

最后,我們來對比下和 SSD 的區別:

1. anchor 部分:SSD 是基於經驗/數據集目標分布手動設計各個尺度的 anchor 的;而 YOLOV3 則是根據數據集目標分布通過聚類來設計各個尺度的 anchor 的,看起來更高級點。

2. loss 層面:假定有N個類別,SSD 里分為 softmax_cross_entropy 分類(N+1)和 smooth L1 回歸兩個損失函數;而 YOLOV3 則有三個損失函數組成,一個判斷是否包含目標(1)的 sigmoid_cross_entropy,一個具體類別(N)的 sigmoid_cross_entropy,一個 giou_loss。這樣的話最后輸出的目標類別 score,其實是前景的得分上具體類別的得分,因此不難發現 YOLOV3 的輸出 score 值域都相對較低點。同時作者一個比較有意思的改進是:對匹配階段為負樣本, 經過回歸后和 bbox 的 iou > 0.5 的 anchor, 不計算分類 loss!

scores = pred_conf * pred_prob[np.arange(len(pred_coor)), classes]

3. bbox 的編碼方式不同,卷積層預測輸出的 4 個值的具體物理含義也就不同。

4. YOLOV3 對 GT 的類別做了 label smoothing 操作。

5. anchor 匹配方面:SSD 分為兩個階段完成,第一階段從 GT 出發為每個 GT 尋找一個最大IOU匹配,第二階段從 Anchor 出發,為每個 Anchor 尋找 $IOU \ge 0.5$ 的匹配;YOLOV3 就簡單粗暴了,為每個 GT 尋找一個最大IOU匹配就完事了。。。等會,麻蛋,這個代碼是這樣操作的,從 GT 出發,每個尺度下,在每個 GT 的中心點位置(三種 anchor)尋找滿足 $IOU \ge 0.3$ 的匹配(因此每個尺度下最多三個匹配了),如果找不到滿足條件的,就退而求其次拿個最大 IOU 匹配來充數了。

6. 作者代碼中輸入尺度是方形的(W=H),但往往我們的輸入尺度是矩形的,因此需要改一下的。

 部分實驗記錄(just for myself)

[- base line: basline_anchor+GIOU+relu6, mAP = 81.37%]
[- leaky_relu/swish: mAP = 82.13%/82.25%]
[- DIOU/CIOU: mAP = 81.46%/82.36%]
[- Ecarx Anchor + CIOU: mAP = 83.53%]


免責聲明!

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



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