咱們是如何改進YOLOv3進行紅外小目標檢測的?


【GiantPandCV導語】本文將介紹BBuf、小武和筆者一塊兒在過年期間完成的一個目標檢測項目,將描述咱們模型改進的思路、實驗思路、結果彙總和經驗性總結。聲明:這篇文章通過了三人贊成,而且全部創新點也將被公佈。此外,因爲經驗上的不足,可能整個實驗思路不夠成熟,比不上CV大組的嚴謹性,若有問題還煩請指教。ios


1. 紅外小目標檢測

紅外小目標檢測的目標比較小,目標極其容易和其餘物體混淆,有必定的挑戰性。git

另外,這本質上也是一個小目標領域的問題,不少適用於小目標的創新點也會被借鑑進來。github

數據來源自@小武

此外,該數據集還有一個特色,就是分背景,雖然一樣是檢測紅外小目標,區別是背景的不一樣,咱們對數據集進行了統計以及經過人工翻看的方式總結了其特色,以下表所示:web

背景類別 數量 特色 數據難度 測試mAP+F1 建議
trees 581 背景乾淨,目標明顯,數量較多 0.99+0.97
cloudless_sky 1320 背景乾淨,目標明顯,數量多 0.98+0.99
architecture 506 背景變化較大,目標形態變化較大,數量較多 通常 0.92+0.96 focal loss
continuous_cloud_sky 878 背景乾淨,目標形態變化不大,但個別目標容易會發生和背景中的雲混淆 通常 0.93+0.95 focal loss
complex_cloud 561 目標形態基本無變化,但背景對目標的定位影響巨大 較難 0.85+0.89 focal loss
sea 17 背景乾淨,目標明顯,數量極少 通常 0.87+0.88 生成高質量新樣本,可讓其轉爲簡單樣本(Mixup)
sea_sky 45 背景變化較大,且單張圖像中目標個數差別變化大,有密集的難點,且數量少 困難 0.68+0.77 paste策略

經過以上結果,能夠看出背景的不一樣對結果影響仍是蠻大的,最後一列也給出了針對性的建議,打算後續實施。算法

2. 實驗過程

首先,咱們使用的是U版的yolov3: https://github.com/ultralytics/yolov3,那時候YOLOv4/五、PPYOLO還都沒出,當時出了一個《從零開始學習YOLOv3》就是作項目的時候寫的電子書,其中的在YOLOv3中添加註意力機制那篇很受歡迎(能夠水不少文章出來,畢業要緊:)數組

咱們項目的代碼以及修改狀況能夠查看:https://github.com/GiantPandaCV/yolov3-point微信

將數據集轉成VOC格式的數據集,以前文章有詳細講述如何轉化爲標準的VOC數據集,以及如何將VOC格式數據集轉化爲U版的講解。當時接觸到幾個項目,都須要用YOLOv3,因爲每次都須要轉化,大概分別調用四、5個腳本吧,感受很累,因此當時花了一段時間構建了一個一鍵從VOC轉U版YOLOv3格式的腳本庫: https://github.com/pprp/voc2007_for_yolo_torchmarkdown

到此時爲止,咱們項目就已經能夠運行了,而後就是不少細節調整了。網絡

2.1 修改Anchor

紅外小目標的Anchor和COCO等數據集的Anchor是差距很大的,爲了更好更快速的收斂,採用了BBuf總結的一套專門計算Anchor的腳本:app

#coding=utf-8
import xml.etree.ElementTree as ET
import numpy as np

 
def iou(box, clusters):
    """
    計算一個ground truth邊界盒和k個先驗框(Anchor)的交併比(IOU)值。
    參數box: 元組或者數據,表明ground truth的長寬。
    參數clusters: 形如(k,2)的numpy數組,其中k是聚類Anchor框的個數
    返回:ground truth和每一個Anchor框的交併比。
    """

    x = np.minimum(clusters[:, 0], box[0])
    y = np.minimum(clusters[:, 1], box[1])
    if np.count_nonzero(x == 0) > 0 or np.count_nonzero(y == 0) > 0:
        raise ValueError("Box has no area")
    intersection = x * y
    box_area = box[0] * box[1]
    cluster_area = clusters[:, 0] * clusters[:, 1]
    iou_ = intersection / (box_area + cluster_area - intersection)
    return iou_


def avg_iou(boxes, clusters):
    """
    計算一個ground truth和k個Anchor的交併比的均值。
    """

    return np.mean([np.max(iou(boxes[i], clusters)) for i in range(boxes.shape[0])])

def kmeans(boxes, k, dist=np.median):
    """
    利用IOU值進行K-means聚類
    參數boxes: 形狀爲(r, 2)的ground truth框,其中r是ground truth的個數
    參數k: Anchor的個數
    參數dist: 距離函數
    返回值:形狀爲(k, 2)的k個Anchor框
    """

    # 便是上面提到的r
    rows = boxes.shape[0]
    # 距離數組,計算每一個ground truth和k個Anchor的距離
    distances = np.empty((rows, k))
    # 上一次每一個ground truth"距離"最近的Anchor索引
    last_clusters = np.zeros((rows,))
    # 設置隨機數種子
    np.random.seed()

    # 初始化聚類中心,k個簇,從r個ground truth隨機選k個
    clusters = boxes[np.random.choice(rows, k, replace=False)]
    # 開始聚類
    while True:
        # 計算每一個ground truth和k個Anchor的距離,用1-IOU(box,anchor)來計算
        for row in range(rows):
            distances[row] = 1 - iou(boxes[row], clusters)
        # 對每一個ground truth,選取距離最小的那個Anchor,並存下索引
        nearest_clusters = np.argmin(distances, axis=1)
        # 若是當前每一個ground truth"距離"最近的Anchor索引和上一次同樣,聚類結束
        if (last_clusters == nearest_clusters).all():
            break
        # 更新簇中心爲簇裏面全部的ground truth框的均值
        for cluster in range(k):
            clusters[cluster] = dist(boxes[nearest_clusters == cluster], axis=0)
        # 更新每一個ground truth"距離"最近的Anchor索引
        last_clusters = nearest_clusters

    return clusters

# 加載本身的數據集,只須要全部labelimg標註出來的xml文件便可
def load_dataset(path):
    dataset = []
    for xml_file in glob.glob("{}/*xml".format(path)):
        tree = ET.parse(xml_file)
        # 圖片高度
        height = int(tree.findtext("./size/height"))
        # 圖片寬度
        width = int(tree.findtext("./size/width"))
        
        for obj in tree.iter("object"):
            # 偏移量
            xmin = int(obj.findtext("bndbox/xmin")) / width
            ymin = int(obj.findtext("bndbox/ymin")) / height
            xmax = int(obj.findtext("bndbox/xmax")) / width
            ymax = int(obj.findtext("bndbox/ymax")) / height
            xmin = np.float64(xmin)
            ymin = np.float64(ymin)
            xmax = np.float64(xmax)
            ymax = np.float64(ymax)
            if xmax == xmin or ymax == ymin:
                print(xml_file)
            # 將Anchor的長寬放入dateset,運行kmeans得到Anchor
            dataset.append([xmax - xmin, ymax - ymin])
    return np.array(dataset)
 
if __name__ == '__main__':
    
    ANNOTATIONS_PATH = "F:\Annotations" #xml文件所在文件夾
    CLUSTERS = 9 #聚類數量,anchor數量
    INPUTDIM = 416 #輸入網絡大小
 
    data = load_dataset(ANNOTATIONS_PATH)
    out = kmeans(data, k=CLUSTERS)
    print('Boxes:')
    print(np.array(out)*INPUTDIM)    
    print("Accuracy: {:.2f}%".format(avg_iou(data, out) * 100))       
    final_anchors = np.around(out[:, 0] / out[:, 1], decimals=2).tolist()
    print("Before Sort Ratios:\n {}".format(final_anchors))
    print("After Sort Ratios:\n {}".format(sorted(final_anchors)))

經過瀏覽腳本就能夠知道,Anchor和圖片的輸入分辨率有沒有關係 這個問題了,當時這個問題有不少羣友都在問。經過kmeans函數獲得的結果其實是歸一化到0-1之間的,而後Anchor的輸出是在此基礎上乘以輸入分辨率的大小。因此我的認爲Anchor和圖片的輸入分辨率是有關係的。

此外,U版也提供了Anchor計算,以下:

def kmean_anchors(path='./2007_train.txt', n=5, img_size=(416416)):
    # from utils.utils import *; _ = kmean_anchors()
    # Produces a list of target kmeans suitable for use in *.cfg files
    from utils.datasets import LoadImagesAndLabels
    thr = 0.20  # IoU threshold

    def print_results(thr, wh, k):
        k = k[np.argsort(k.prod(1))]  # sort small to large
        iou = wh_iou(torch.Tensor(wh), torch.Tensor(k))
        max_iou, min_iou = iou.max(1)[0], iou.min(1)[0]
        bpr, aat = (max_iou > thr).float().mean(), (
            iou > thr).float().mean() * n  # best possible recall, anch > thr
        print('%.2f iou_thr: %.3f best possible recall, %.2f anchors > thr' %
              (thr, bpr, aat))
        print(
            'kmeans anchors (n=%g, img_size=%s, IoU=%.3f/%.3f/%.3f-min/mean/best): '
            % (n, img_size, min_iou.mean(), iou.mean(), max_iou.mean()),
            end='')
        for i, x in enumerate(k):
            print('%i,%i' % (round(x[0]), round(x[1])),
                  end=',  ' if i < len(k) - 1 else '\n')  # use in *.cfg
        return k

    def fitness(thr, wh, k):  # mutation fitness
        iou = wh_iou(wh, torch.Tensor(k)).max(1)[0]  # max iou
        bpr = (iou > thr).float().mean()  # best possible recall
        return iou.mean() * bpr  # product

    # Get label wh
    wh = []
    dataset = LoadImagesAndLabels(path,
                                  augment=True,
                                  rect=True,
                                  cache_labels=True)
    nr = 1 if img_size[0] == img_size[1else 10  # number augmentation repetitions
    for s, l in zip(dataset.shapes, dataset.labels):
        wh.append(l[:, 3:5] *
                  (s / s.max()))  # image normalized to letterbox normalized wh
    wh = np.concatenate(wh, 0).repeat(nr, axis=0)  # augment 10x
    wh *= np.random.uniform(img_size[0], img_size[1],
                            size=(wh.shape[0],
                                  1))  # normalized to pixels (multi-scale)

    # Darknet yolov3.cfg anchors
    use_darknet = False
    if use_darknet:
        k = np.array([[1013], [1630], [3323], [3061], [6245],
                      [59119], [11690], [156198], [373326]])
    else:
        # Kmeans calculation
        from scipy.cluster.vq import kmeans
        print('Running kmeans for %g anchors on %g points...' % (n, len(wh)))
        s = wh.std(0)  # sigmas for whitening
        k, dist = kmeans(wh / s, n, iter=30)  # points, mean distance
        k *= s
    k = print_results(thr, wh, k)
    # Evolve
    wh = torch.Tensor(wh)
    f, ng = fitness(thr, wh, k), 2000  # fitness, generations
    for _ in tqdm(range(ng), desc='Evolving anchors'):
        kg = (
            k.copy() *
            (1 + np.random.random() * np.random.randn(*k.shape) * 0.30)).clip(
                min=2.0)
        fg = fitness(thr, wh, kg)
        if fg > f:
            f, k = fg, kg.copy()
            print_results(thr, wh, k)
    k = print_results(thr, wh, k)

    return k

這個和超參數搜索那篇採用的方法相似,也是一種相似遺傳算法的方法,經過一代一代的篩選找到合適的Anchor。以上兩種方法筆者並無對比,有興趣能夠試試這兩種方法,對比看看。

Anchor這方面設置了三個不一樣的數量進行聚類:

3 anchor:

13, 18, 16, 22, 19, 25

6 anchor:

12,17, 14,17, 15,19, 15,21, 13,20, 19,24

9 anchor:

10,16, 12,17, 13,20, 13,22, 15,18, 15,20, 15,23, 18,23, 21,26

2.2 構建Baseline

因爲數據集是單類的,而且相對VOC等數據集來看,比較單一,因此不打算使用Darknet53這樣的深度神經網絡,採用的Baseline是YOLOv3-tiny模型,在使用原始Anchor的狀況下,該模型能夠在驗證集上達到mAP@0.5=93.2%,在測試集上達到mAP@0.5=0.869的結果。

那接下來換Anchor,用上一節獲得的新Anchor替換掉原來的Anchor,該改掉的模型爲yolov3-tiny-6a:

Epoch Model P R mAP@0.5 F1 dataset
baseline yolov3-tiny原版 0.982 0.939 0.932 0.96 valid
baseline yolov3-tiny原版 0.96 0.873 0.869 0.914 test
6a yolov3-tiny-6a 0.973 0.98 0.984 0.977 valid
6a yolov3-tiny-6a 0.936 0.925 0.915 0.931 test

能夠看到幾乎全部的指標都提高了,這說明Anchor先驗的引入是頗有必要的。

2.3 數據集部分改進

上邊已經分析過了,背景對目標檢測的結果仍是有必定影響的,因此咱們前後使用了幾種方法進行改進。

第一個:過採樣

經過統計不一樣背景的圖像的數量,好比以sea爲背景的圖像只有17張,而最多的cloudless_sky爲背景的圖像有1300+張,這就產生了嚴重的不平衡性。顯然cloudless_sky爲背景的很簡單,sea爲背景的難度更大,這樣因爲數據不平衡的緣由,訓練獲得的模型極可能也會在cloudless_sky這類圖片上效果很好,在其餘背景下效果通常。

因此首先要採用過採樣的方法,這裏的過採樣可能和別的地方的不太同樣,這裏指的是將某些背景數量小的圖片經過複製的方式擴充。

Epoch Model P R mAP@0.5 F1 dataset
baseline(os) yolov3-tiny原版 0.985 0.971 0.973 0.978 valid
baseline(os) yolov3-tiny原版 0.936 0.871 0.86 0.902 test
baseline yolov3-tiny原版 0.982 0.939 0.932 0.96 valid
baseline yolov3-tiny原版 0.96 0.873 0.869 0.914 test

:( 惋惜實驗結果不支持想法,一塊兒分析一下。ps:os表明over sample

而後進行分背景測試,結果以下:

均衡後的分背景測試

data num model P R mAP F1
trees 506 yolov3-tiny-6a 0.924 0.996 0.981 0.959
sea_sky 495 yolov3-tiny-6a 0.927 0.978 0.771 0.85
sea 510 yolov3-tiny-6a 0.923 0.935 0.893 0.929
continuous_cloud_sky 878 yolov3-tiny-6a 0.957 0.95 0.933 0.953
complex_cloud 561 yolov3-tiny-6a 0.943 0.833 0.831 0.885
cloudless_sky 1320 yolov3-tiny-6a 0.993 0.981 0.984 0.987
architecture 506 yolov3-tiny-6a 0.959 0.952 0.941 0.955

從分背景結果來看,確實sea訓練數據不多的結果很好,mAP提升了2個點,可是complex_cloud等mAP有所降低。總結一下就是對於訓練集中數據不多的背景類mAP有提高,可是其餘自己數量就不少的背景mAP略微降低或者保持。

第二個:在圖片中任意位置複製小目標

修改後的版本地址:https://github.com/pprp/SimpleCVReproduction/tree/master/SmallObjectAugmentation

具體實現思路就是,先將全部小目標摳出來備用。而後在圖像上覆制這些小目標,要求兩兩之間重合率不能達到一個閾值而且複製的位置不能超出圖像邊界。

效果以下:(這個是示意圖,比較誇張,複製的個數比較多

加強結果

這種作法來自當時比較新的論文《Augmentation for small object detection》,文中最好的結果是複製了1-2次。實際咱們項目中也試過1次、2次、3次到屢次的結果,都不盡如人意,結果太差就沒有記錄下來。。(話說論文中展現的效果最佳組合是原圖+加強後的圖,而且最好的結果也就提升了1個百分點)╮(╯﹏╰)╭

2.4 修改Backbone

修改Backbone常常被羣友問到這樣一件事,修改骨幹網絡之後沒法加載預訓練權重了,怎麼辦?

有如下幾個辦法:

  • 乾脆不加載,從頭訓練,簡單問題(好比紅外小目標)從頭收斂效果也不次於有預訓練權重的。

  • 不想改代碼的話,能夠選擇修改Backbone以後、YOLO Head以前的部分(好比SPP的位置屬於這種狀況)

  • 能力比較強的,能夠改一下模型加載部分代碼,跳過你新加入的模塊,這樣也能加載(筆者沒試過,別找我)。

修改Backbone咱們也從幾個方向入的手,分爲注意力模塊、即插即用模塊、修改FPN、修改激活函數、用成熟的網絡替換backbone和SPP系列。

1. 注意力模塊

這個項目中使用的注意力模塊,大部分都在公號上寫過代碼解析,感興趣的能夠翻看一下。筆者前一段時間公佈了一個電子書《卷積神經網絡中的即插即用模塊》也是由於這個項目中總結了不少注意力模塊,因此開始整理獲得的結果。具體模塊還在繼續更新:https://github.com/pprp/SimpleCVReproduction

當時實驗的模塊有:SE、CBAM等,因爲當時Baseline有點高,效果並不十分理想。(注意力模塊插進來不可能按照預期一下就提升多少百分點,須要多調參纔有可能超過原來的百分點)根據羣友反饋,SE直接插入成功率比較高。筆者在一個目標檢測比賽中見到有一個大佬是在YOLOv3的FPN的三個分支上各加了一個CBAM,最終超過Cascade R-CNN等模型奪得冠軍。

2. 即插即用模塊

注意力模塊也屬於即插即用模塊,這部分就說的是非注意力模塊的部分如 FFM、ASPP、PPM、Dilated Conv、SPP、FRB、CorNerPool、DwConv、ACNet等,效果還能夠,可是沒有超過當前最好的結果。

3. 修改FPN

FPN這方面花了老久時間,參考了好多版本才搞出了一個dt-6a-bifpn(dt表明dim target紅外目標;6a表明6個anchor),使人失望的是,這個BiFPN效果並很差,測試集上效果更差了。多是由於實現的cfg有問題,歡迎反饋。

你們都知道經過改cfg的方式改網絡結構是一件很痛苦的事情,推薦一個可視化工具:

https://lutzroeder.github.io/netron/

除此之外,爲了方便查找行數,筆者寫了一個簡單腳本用於查找行數(獻醜了

import os
import shutil
cfg_path = "./cfg/yolov3-dwconv-cbam.cfg"
save_path = "./cfg/preprocess_cfg/"
new_save_name = os.path.join(save_path,os.path.basename(cfg_path))

f = open(cfg_path, 'r')
lines = f.readlines()

# 去除以#開頭的,屬於註釋部分的內容
# lines = [x for x in lines if x and not x.startswith('#')]
# lines = [x.rstrip().lstrip() for x in lines]

lines_nums = []
layers_nums = []

layer_cnt = -1

for num, line in enumerate(lines):
    if line.startswith('['):
        layer_cnt += 1
        layers_nums.append(layer_cnt)
        lines_nums.append(num+layer_cnt)
        print(line)
        # s = s.join("")
    # s = s.join(line)
for i,num in enumerate(layers_nums):
    print(lines_nums[i], num)
    lines.insert(lines_nums[i]-1'# layer-%d\n' % (num-1))
fo = open(new_save_name, 'w')
fo.write(''.join(lines))
fo.close()
f.close()

咱們也嘗試了只用一個、兩個和三個YOLO Head的狀況,結果是3>2>1,可是用3個和2個效果幾乎同樣,差別不大小數點後3位的差別,因此仍是選用兩個YOLO Head。

4. 修改激活函數

YOLO默認使用的激活函數是leaky relu,激活函數方面使用了mish。效果並無提高,因此無疾而終了。

5. 用成熟的網絡替換backbone

這裏使用了ResNet10(第三方實現)、DenseNet、BBuf修改的DenseNet、ENet、VOVNet(本身改的)、csresnext50-panet(當時AB版darknet提供的)、PRN(做用不大)等網絡結構。

當前最強的網絡是dense-v3-tiny-spp,也就是BBuf修改的Backbone+原汁原味的SPP組合的結構完虐了其餘模型,在測試集上達到了mAP@0.5=0.93二、F1=0.951的結果。

6. SPP系列

這個得好好說說,咱們三人調研了好多論文、參考了好多trick,大部分都無效,其中歷來不會讓人失望的模塊就是SPP。咱們對SPP進行了深刻研究,在《卷積神經網絡中的各類池化操做》中提到過。

SPP是在SPPNet中提出的,SPPNet提出比較早,在RCNN以後提出的,用於解決重複卷積計算和固定輸出的兩個問題,具體方法以下圖所示:

在feature map上經過selective search得到窗口,而後將這些區域輸入到CNN中,而後進行分類。

實際上SPP就是多個空間池化的組合,對不一樣輸出尺度採用不一樣的劃窗大小和步長以確保輸出尺度相同,同時可以融合金字塔提取出的多種尺度特徵,可以提取更豐富的語義信息。經常使用於多尺度訓練和目標檢測中的RPN網絡。

在YOLOv3中有一個網絡結構叫yolov3-spp.cfg, 這個網絡每每能達到比yolov3.cfg自己更高的準確率,具體cfg以下:

### SPP ###
[maxpool]
stride=1
size=5

[route]
layers=-2

[maxpool]
stride=1
size=9

[route]
layers=-4

[maxpool]
stride=1
size=13

[route]
layers=-1,-3,-5,-6

### End SPP ###

這裏的SPP至關因而原來的SPPNet的變體,經過使用多個kernel size的maxpool,最終將全部feature map進行concate,獲得新的特徵組合。

再來看一下官方提供的yolov3和yolov3-spp在COCO數據集上的對比:

能夠看到,在幾乎不增長FLOPS的狀況下,YOLOv3-SPP要比YOLOv3-608mAP高接近3個百分點。

分析一下SPP有效的緣由:

  1. 從感覺野角度來說,以前計算感覺野的時候能夠明顯發現,maxpool的操做對感覺野的影響很是大,其中主要取決於kernel size大小。在SPP中,使用了kernel size很是大的maxpool會極大提升模型的感覺野,筆者沒有詳細計算過darknet53這個backbone的感覺野,在COCO上有效極可能是由於backbone的感覺野還不夠大。
  2. 第二個角度是從Attention的角度考慮,這一點啓發自CSDN@小楞,他在文章中這樣講:

出現檢測效果提高的緣由:經過spp模塊實現局部特徵和全局特徵(因此空間金字塔池化結構的最大的池化核要儘量的接近等於須要池化的featherMap的大小)的featherMap級別的融合,豐富最終特徵圖的表達能力,從而提升MAP。

Attention機制不少都是爲了解決遠距離依賴問題,經過使用kernel size接近特徵圖的size能夠以比較小的計算代價解決這個問題。另外就是若是使用了SPP模塊,就沒有必要在SPP後繼續使用其餘空間注意力模塊好比SK block,由於他們做用類似,可能會有必定冗餘。

在本實驗中,確實也獲得了一個很重要的結論,那就是:

SPP是有效的,其中size的設置應該接近這一層的feature map的大小

口說無憑,看一下實驗結果:

SPP系列實驗

Epoch Model P R mAP F1 dataset
baseline dt-6a-spp 0.99 0.983 0.984 0.987 valid
baseline dt-6a-spp 0.955 0.948 0.929 0.951 test
直連+5x5 dt-6a-spp-5 0.978 0.983 0.981 0.98 valid
直連+5x5 dt-6a-spp-5 0.933 0.93 0.914 0.932 test
直連+9x9 dt-6a-spp-9 0.99 0.983 0.982 0.987 valid
直連+9x9 dt-6a-spp-9 0.939 0.923 0.904 0.931 test
直連+13x13 dt-6a-spp-13 0.995 0.983 0.983 0.989 valid
直連+13x13 dt-6a-spp-13 0.959 0.941 0.93 0.95 test
直連+5x5+9x9 dt-6a-spp-5-9 0.988 0.988 0.981 0.988 valid
直連+5x5+9x9 dt-6a-spp-5-9 0.937 0.936 0.91 0.936 test
直連+5x5+13x13 dt-6a-spp-5-13 0.993 0.988 0.985 0.99 valid
直連+5x5+13x13 dt-6a-spp-5-13 0.936 0.939 0.91 0.938 test
直連+9x9+13x13 dt-6a-spp-9-13 0.981 0.985 0.983 0.983 valid
直連+9x9+13x13 dt-6a-spp-9-13 0.925 0.934 0.907 0.93 test

當前的feature map大小就是13x13,實驗結果表示,直接使用13x13的效果和SPP的幾乎同樣,運算量還減小了。

2.5 修改Loss

loss方面嘗試了focal loss,可是通過調整alpha和beta兩個參數,無論用默認的仍是本身慢慢調參,網絡都沒法收斂,因此當時給做者提了一個issue: https://github.com/ultralytics/yolov3/issues/811

glenn-jocher說效果很差就別用:(

做者回復

BBuf也研究了好長時間,發現focal loss在Darknet中能夠用,可是效果也通常般。最終focal loss也是無疾而終。此外還試着調整了ignore thresh,來配合focal loss,實驗結果以下(在AB版Darknet下完成實驗):

state model P R mAP F1 data
ignore=0.7 dt-6a-spp-fl 0.97 0.97 0.9755 0.97 valid
ignore=0.7 dt-6a-spp-fl 0.96 0.93 0.9294 0.94 test
ignore=0.3 dt-6a-spp-fl 0.95 0.99 0.9874 0.97 valid
ignore=0.3 dt-6a-spp-fl 0.89 0.92 0.9103 0.90 test

3. 經驗性總結

在這個實驗過程當中,和BBuf討論有了不少啓發,也進行了總結,在這裏公開出來,(可能部分結論不夠嚴謹,沒有通過嚴格對比實驗,感興趣的話能夠作一下對比實驗)。

  • SPP層是有效的,Size設置接近feature map的時候效果更好。
  • YOLOv三、YOLOv3-SPP、YOLOv3-tiny三者在檢測同一個物體的狀況下,YOLOv3-tiny給的該物體的置信度相比其餘兩個模型低。(其實也能夠形象化理解,YOLOv3-tiny的腦容量比較小,因此惟惟諾諾不敢肯定)
  • 我的感受Concate的方法要比Add的方法更柔和,對小目標效果更好。本實驗結果上是DenseNet做爲Backbone的時候效果是最佳的。
  • 多尺度訓練問題,這個文中沒提。多尺度訓練對於尺度分佈比較普遍的問題效果明顯,好比VOC這類數據集。可是對於尺度單一的數據集反而有副作用,好比紅外小目標數據集目標尺度比較統一,都很小。
  • Anchor對模型影響比較大,Anchor先驗不合理會致使更多的失配,從而下降Recall。
  • 當時跟羣友討論的時候就提到一個想法,對於小目標來講,淺層的信息更加有用,那麼進行FPN的時候,不該該單純將二者進行Add或者Concate,而是應該以必定的比例完成,好比對於小目標來講,引入更多的淺層信息,讓淺層網絡權重增大;大目標則相反。後邊經過閱讀發現,這個想法被ASFF實現了,並且想法比較完善。
  • PyTorch中的Upsample層是不可復現的。
  • 有卡能夠嘗試一下超參數進化方法。

PS: 以上內容不保證結論徹底正確,只是經驗性總結,歡迎入羣討論交流。

4. 致謝

感謝BBuf和小武和我一塊兒完成這個項目,感謝小武提供的數據和算法,沒有小武的支持,咱們沒法完成這麼多實驗。感謝BBuf的邀請,我才能加入這個項目,一塊兒討論對個人幫助很是大(怎麼沒早點碰見BB:)

雖然最後是爛尾了,可是學到了很多東西,不少文章都是在這個過程當中總結獲得的,在這個期間總結的文章有《CV中的Attention機制》、《從零開始學習YOLOv3》、《目標檢測和感覺野的總結和想法》、《PyTorch中模型的可復現性》、《目標檢測算法優化技巧》等,歡迎去幹貨錦集中回顧。

以上是整個實驗過程的一部分,後邊階段咱們還遇到了不少困難,想將項目往輕量化的方向進行,因爲種種緣由,最終沒有繼續下去,在這個過程當中,總結一下教訓,實驗說明和備份要作好,修改的數據集、訓練獲得的權重、當時的改動點要作好備份。如今回看以前的實驗記錄和cfg文件都有點想不起來某些模型的改動點在哪裏了,仍是整理的不夠詳細,實驗記錄太亂。

最後但願這篇文章能給你們提供一些思路。

5. 資源列表

官方代碼:https://github.com/ultralytics/yolov3

改進代碼:https://github.com/GiantPandaCV/yolov3-point

Focal Loss Issue: https://github.com/ultralytics/yolov3/issues/811

小目標加強庫(複製和粘貼的方式):https://github.com/pprp/SimpleCVReproduction/tree/master/SmallObjectAugmentation

pprp Github: https://github.com/pprp

BBuf Github:https://github.com/BBuf

以上涉及到的全部實驗結果已經整理成markdown文件,請在後臺回覆「紅外」得到。


爲了感謝讀者朋友們的長期支持,咱們今天將送出3本由機械工業出版社提供的《人臉識別與美顏算法實戰》書籍,對本書感興趣的能夠在留言版留言,咱們將抽取其中三位讀者送出一本正版書籍。

!!!留言區在這裏!!!

沒中獎的讀者若是有對此書感興趣的,能夠考慮點擊下方的當當網連接自行購買。



對文章有疑問或者但願加入GiantPandaCV交流羣的能夠添加如下微信

BBuf

pprp


本文分享自微信公衆號 - GiantPandaCV(BBuf233)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索