hans

hans

【Darknet】【yolo v2】訓練自己數據集的一些心得----VOC格式


-------【2017.11.2 更新】------------SSD 傳送門 ----------

http://blog.csdn.net/renhanchi/article/details/78411095

------- 【2017.10.30 更新】------------ 一些要說的 -----------

雖然寫完這篇博客後我幾乎再沒使用 darknet 和 yolo,但這半年多我一直在不斷更新這篇博客,也在不斷為大家解決問題。

如果你想通過這篇博客學習使用 darknet 和 yolo,包括一些 detection 的知識,我希望你能仔細、認真的讀本文的每一句話。


最近發現作者代碼改動很大,導致很多地方跟我寫的博客內容都不一樣了。我現在把我當初下載的 darknet 打包給大家。

** 新老版本的區別在於代碼,算法架構還是一樣的,所以放心使用老版本。
**

解壓縮後進入目錄,配置好 Makefile 後直接 make all 就可以了。

https://pan.baidu.com/s/1jIR2oTo

1. 前言#

關於用 yolo 訓練自己 VOC 格式數據的博文真的不少,但是當我按照他們的方法一步一步走下去的時候發現出了其他作者沒有提及的問題。這裡就我自己的經驗講講如何訓練自己的數據集。

2. 數據集#

這裡建議大家用 VOC 和 ILSVRC 比賽的數據集,因為 xml 文件都是現成的,省去很多功夫。

想自己標記的可以自己去 github 搜索 labelImg , 下載好 make 後直接運行就可以。具體使用方法先不做贅述。

也可以直接去下載現成的數據集

ILSVRC2015 比賽的地址是: http://image-net.org/challenges/LSVRC/2015/download-
images-3j16.php

VOC 比賽地址是: http://host.robots.ox.ac.uk/pascal/VOC/index.html

我的數據集是把 VOC2007,VOC2012,ILSVRC2013 和 ILSVRC2014 所有關於人的數據集單獨拿了出來,我只想做單獨檢測人的訓練。注意 ILSVRC 後綴名是 JPEG 的,可以自己改成 jpg,也可以不改,因為 darknet 代碼裡也兼容 JPEG 格式。但為了以後省事兒,我是都給改成 jpg 後綴名了。

關於怎麼把 VOC 所有關於人的數據集單獨拿出來,大家可以用下面這個 shell 腳本,稍微改一改就也能用來提取 ILSVRC 的數據。ILSVRC 數據集裡面人的類別不是 person,是 n00007846,這個在 xml 文件裡不影響後面的訓練,所以大家也不需要特意把 n00007846 都改成 person。原因是 labels.txt 文件裡面是用數字 0,1,2,3 等等表示類別的,而不是單詞。這些數字是對應 data/names.list
裡面類別的索引。

#!/bin/sh

year="VOC2012"

mkdir /your_path/VOCperson/${year}_Anno/  #創建文件夾
mkdir /your_path/VOCperson/${year}_Image/

cd /your_path/VOCdevkit/$year/Annotations/
grep -H -R "<name>person</name>" > /your_path/VOCperson/temp.txt  #找到有關鍵字的行,並把這些行存到臨時文檔

cd /your_path/VOCperson/
cat temp.txt | sort | uniq > $year.txt     #根據名字排序,並把相鄰的內容完全一樣的多餘行刪除。
find -name $year.txt | xargs perl -pi -e 's|.xml:\t\t<name>person</name>||g'   #把文檔中後綴名和其他無用信息刪掉,只保留沒後綴名的文件名

cat $year.txt | xargs -i cp /your_path/VOCdevkit/$year/Annotations/{}.xml /your_path/VOCperson/${year}_Anno/ #根據文件名複製註釋文件
cat $year.txt | xargs -i cp /your_path/VOCdevkit/$year/JPEGImages/{}.jpg /your_path/VOCperson/${year}_Image/ #根據文件名複製數據集

rm temp.txt

3. 訓練文件#

3.1 文件夾設置#

Annotations ---- 這個文件夾是放所有 xml 描述文件的。

JPEGImages ---- 這個文件夾是放所有 jpg 圖片文件的。

ImageSets -> Main ----
這個文件裡放一個 names.txt 文檔(我的這個文檔的名字是:train.txt,注意這個名字要跟下面 python 代碼中列表 sets 的第二個元素內容一致),文檔內容是所有訓練集圖片的名字,沒有後綴名。

PS: 官網自帶的訓練 VOC 方法裡面圖片是分別放在 2007 和 2012 兩個不同路徑的,我覺得麻煩,就一股腦把所有文件都放在一個文件裡面了。

3.2 txt 文檔#

一共需要準備三種 txt 文檔:

先是上面提到的在 ImageSets 文件下所有訓練數據名的 names.txt 文檔。

然後是所有圖片一一對應的 labels.txt。這些文檔是通過 scripts/voc_label.py
這個文件生成的,裡面路徑是要改一改的。我把所有圖片文件和 xml 文件都分別放到一個文件夾下了,而且訓練類別只有 person。所以開頭的 sets 和 classes,也是需要改的。這裡值得注意的是如果你是用的 ILSVRC 數據集,並且你也和我一樣懶懶的沒有把 xml 文件裡面的 n00007846 改成人的話,那麼你需要把 classes 改成 n00007846 這樣才能找到關於這一類別的 bbox 信息。還有一點需要注意,就是 ILSVRC 數據集的 xml 文件裡面沒有 difficult 這個信息,所以.py 文件裡關於這一點的東西註釋掉就好了。

最後是保存有所有訓練圖片絕對路徑的 paths.txt 文檔。注意這個文檔裡面圖片文件名是帶 jpg 後綴名的。生成上面 labels.txt 文檔的時候最後會自動生成 paths.txt。

下面是我的.py 文件

import xml.etree.ElementTree as ET
import pickle
import os
from os import listdir, getcwd
from os.path import join

sets=[('person','train')]
classes = ["n00007846"]

def convert(size, box):
    dw = 1./(size[0])
    dh = 1./(size[1])
    x = (box[0] + box[1])/2.0 - 1
    y = (box[2] + box[3])/2.0 - 1
    w = box[1] - box[0]
    h = box[3] - box[2]
    x = x*dw
    w = w*dw
    y = y*dh
    h = h*dh
    return (x,y,w,h)

def convert_annotation(year, image_id):
    in_file = open('/home/hans/darknet/person/VOC%s/Annotations/%s.xml'%(year, image_id))
    out_file = open('/home/hans/darknet/person/VOC%s/labels/%s.txt'%(year, image_id), 'w')
    tree=ET.parse(in_file)
    root = tree.getroot()
    size = root.find('size')
    w = int(size.find('width').text)
    h = int(size.find('height').text)

    for obj in root.iter('object'):
        # difficult = obj.find('difficult').text
        cls = obj.find('name').text
        if cls not in classes: # or int(difficult)==1:
            continue
        cls_id = classes.index(cls)
        xmlbox = obj.find('bndbox')
        b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text),
float(xmlbox.find('ymax').text))
        bb = convert((w,h), b)
        out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')

wd = getcwd()
for year, image_set in sets:
    if not os.path.exists('/home/hans/darknet/person/VOC%s/labels/'%(year)):
        os.makedirs('/home/hans/darknet/person/VOC%s/labels/'%(year))
    image_ids = open('/home/hans/darknet/person/VOC%s/ImageSets/Main/%s.txt'%(year, image_set)).read().strip().split()
    list_file = open('%s_%s.txt'%(year, image_set), 'w')
    for image_id in image_ids:
        list_file.write('%s/VOC%s/JPEGImages/%s.jpg\n'%(wd, year, image_id))
        convert_annotation(year, image_id)
    list_file.close()

3.3 訓練配置文件#

先是在 data 文件夾下創建一個 person.names 文件,內容只有 person。

然後是修改 cfg 文件夾下 voc.data 文件,classes 改成 1。 train 對應的路徑是上文的 paths.txt。
names 對應路徑是 person.names 文件的。backup 對應路徑是備份訓練權重文件的。

最後是選取.cfg 網絡,在 darknet 官網還有很多博文裡面都是用的 yolo-
voc.cfg,我用這個網絡訓練一直失敗。體現在訓練好久後 test,沒有 bbox 和 predict 結果。出現這種問題有兩種情況,一是訓練發散了。二是訓練不充分,predict 結果置信概率太低。對於訓練發散,其實很容易發現的,就是在訓練的時候觀察迭代次數後邊的兩個 loss 值,如果這兩個值不斷變大到幾百就說明訓練發散啦。可以通過降低 learning
rate 和 提高 batch 數量解決這個問題。關於參數修改和訓練輸出分別表示什麼我會在後文提及。
對於訓練不充分的時候如何顯示出 predict 結果,可以在 test 的時候設置 threshold,darknet 默認是.25,你可以嘗試逐漸降低這個值看效果。

./darknet detector test cfg/voc.data cfg/yolo_voc.cfg -thresh 0.25

當時對 darknet 理解還不夠深,出現上面問題當時我解決不了的時候,我換成了現在用的 yolo-voc.2.0.cfg 這個網絡。關於裡面的參數我簡單說說:

batch:
每一次迭代送到網絡的圖片數量,也叫批數量。增大這個可以讓網絡在較少的迭代次數內完成一個 epoch。在固定最大迭代次數的前提下,增加 batch 會延長訓練時間,但會更好的尋找到梯度下降的方向。如果你顯存夠大,可以適當增大這個值來提高內存利用率。這個值是需要大家不斷嘗試選取的,過小的話會讓訓練不夠收斂,過大的話會陷入局部最優。

subdivision:
這個參數很有意思的,它會讓你的每一 batch 不是一下子都丟到網絡裡。而是分成 subdivision 對應數字的份數,一份一份的跑完後,在一起打包算作完成一次 iteration。這樣會降低對顯存的佔用情況。如果設置這個參數為 1 的話就是一次性把所有 batch 的圖片都丟到網絡裡,如果為 2 的話就是一次丟一半。

angle: 圖片旋轉角度,這個用來增強訓練效果的。從本質上來說,就是通過旋轉圖片來變相的增加訓練樣本集。

saturation,exposure,hue: 飽和度,曝光度,色調,這些都是為了增強訓練效果用的。

learning_rate: 學習率,訓練發散的話可以降低學習率。學習遇到瓶頸,loss 不變的話也減低學習率。

max_batches: 最大迭代次數。

policy: 學習策略,一般都是 step 這種步進式。

step,scales: 這兩個是組合一起的,舉個例子:learn_rate: 0.001, step:100,25000,35000
scales: 10, .1, .1 這組數據的意思就是在 0-100 次 iteration 期間 learning
rate 為原始 0.001,在 100-25000 次 iteration 期間 learning
rate 為原始的 10 倍 0.01,在 25000-35000 次 iteration 期間 learning rate 為當前值的 0.1 倍,就是 0.001,
在 35000 到最大 iteration 期間使用 learning
rate 為當前值的 0.1 倍,就是 0.0001。隨著 iteration 增加,降低學習率可以是模型更有效的學習,也就是更好的降低 train loss。

最後一層卷積層中 filters 數值是 5×(類別數 + 5)。具體原因就不多說了,知道就好哈。

region 裡需要把 classes 改成你的類別數。

最後一行的 random
,是一個開關。如果設置為 1 的話,就是在訓練的時候每一 batch 圖片會隨便改成 320-640(32 整倍數)大小的圖片。目的和上面的色度,曝光度等一樣。如果設置為 0 的話,所有圖片就只修改成默認的大小
416*416。(2018.04.08 更新,評論給裡有朋友說這裡如果設置為 1 的話,訓練的時候 obj 和 noobj 會出現全為 0 的情況,設置為 0 後一切正常。)

3.4 開始訓練#

可以自己去下載 pre_trained 文件來提高自己的訓練效率。

以上所有準備工作做好後就可以訓練自己的模型了。

terminal 運行:

./darknet detector train cfg/voc.data cfg/yolo_voc.cfg darknet19_448.conv.23

-------- 【2017.06.29 更新】 --------- 新版本源代碼被作者做了很大改動,老版本的可以看看下面內容 --------------------------------------

這裡有一點我還是要說一下, 其他博文有讓我們修改.c 源文件。其實這個對於我等懶人是真的不用修改的。原因是這樣的,在官網裡有一段執行 test 的代碼是:

./darknet detect cfg/yolo.cfg yolo.weights data/dog.jpg

這是一段簡寫的執行語句。它的完整形式是這樣的:

./darknet detector test cfg/coco.data cfg/yolo.cfg yolo.weights data/dog.jpg

其實修改.c 文件的作用就是讓我們可以使用簡寫的 test 執行語句,程序會自動調用.c 裡面設置好的路徑內容。我個人覺得這個很沒有必要。還有就是最新的版本中已經沒有 yolo.cu 這個文件了。


4. 訓練輸出#

這裡我講一講關於輸出的東西都是些什麼東西,有些我也不太懂,只挑些有用的,我懂得講講。

Region Avg IOU: 這個是預測出的 bbox 和實際標註的 bbox 的交集 除以 他的並集。顯然,這個數值越大,說明預測的結果越好。

Avg Recall: 這個表示平均召回率, 意思是 檢測出物體的個數 除以 標註的所有物體個數。

count: 標註的所有物體的個數。 如果 count = 6, recall = 0.66667,
就是表示一共有 6 個物體(可能包含不同類別,這個不管類別),然後我預測出來了 4 個,所以 Recall 就是 4 除以 6 = 0.66667 。

有一行跟上面不一樣的,最開始的是 iteration 次數,然後是 train loss,然後是 avg train loss, 然後是學習率,
然後是一 batch 的處理時間, 然後是已經一共處理了多少張圖片。 重點關注 train loss 和 avg train
loss,這兩個值應該是隨著 iteration 增加而逐漸降低的。如果 loss 增大到幾百那就是訓練發散了,如果 loss 在一段時間不變,就需要降低 learning
rate 或者改變 batch 來加強學習效果。當然也可能是訓練已經充分。這個需要自己判斷。

5. 可視化#

這裡我給大家分享一個關於 loss 可視化的 matlab 代碼。可以很直觀的看到你的 loss 的變化曲線。

首先在訓練的時候,可以通過 script 命令把 terminal 的輸出都錄像到一個 txt 文檔中。

script -a log.txt




./darknet detector train cfg/voc.data cfg/yolo_voc.cfg darknet19_448.conv.23

訓練完成後記得使用 ctrl+D 或者輸入 exit 結束螢幕錄像。

下面是 matlab 代碼:

clear;
clc;
close all;

train_log_file = 'log.txt';


[~, string_output] = dos(['cat ', train_log_file, ' | grep "avg," | awk ''{print $3}''']);
train_loss = str2num(string_output);
n = 1:length(train_loss);
idx_train = (n-1);

figure;plot(idx_train, train_loss);

grid on;
legend('Train Loss');
xlabel('iterations');
ylabel('avg loss');
title(' Train Loss Curve');

我畫的是 avg train
loss 的曲線圖。我這裡 batch 為 8,曲線震盪幅度很大。學習率是 0.0001,25000 次迭代後降到 0.00001,。大家注意看後面 loss 趨於穩定在 7-8 左右降低就不明顯,並且趨於不變了。我的理解是可能遇到兩種情況,一是陷入局部最優,二是學習遇到瓶頸。但沒有解決這個問題。降低 batch,提高學習效果,loss 不變。提高 batch,讓網絡更顧全全局,loss 降低一點點,繼續不變。降低學習率提高學習效果,降低一點點,繼續不變。希望有大神看到此文請予以指教。25000 次迭代後有一點下降是因為我降低了 learning
rate,但降低不明顯,而且很快就又趨於平穩了。訓練集 4.3W+,是從 ILSVRC2015 訓練集和 VOC2012 訓練集中挑出來的所有關於人的數據。

----------- 【2017.09.26 更新】 -----------------

一直犯懶沒有寫這個補充說明,上面我說可能陷入局部最優。做了很多工作並沒有提升。我當時一直用 caffe 的 loss 標準來對待 darknet,
其實這個有很大問題。因為兩種框架輸出 loss 並不一定是相同單位的。其實我上面訓練好的模型的實際使用效果是非常非常好的,遠遠的一顆小小人頭都能檢測的到.

並且現在看上面訓練參數應該可以有所改進,我在這裡記一下如果以後再次用到 darknet 的話方便查閱。因為做的 finetune, 學習率很低這個沒毛病,
但是相應 momentum 可以考慮適當增加,從 0.9 調整到 0.99. 還可以考慮將學習策略改成 poly.


----------- 【2017.10.30 更新】 ------------------------

最近開始著手做一些 detection
的東西,回過頭來看 darknet,再糾正一點。對於 detection 而言,應該不要盲目用 loss 來評判一個模型的好壞。loss 應該用作在訓練當中判斷訓練是否正常進行。比如訓練一開始 loss 一直升高,最後 NAN,說明學習率大了。那我們要用哪個數值來評判模型的好壞呢?應該是 mAP,這個應該是當前主流標準,其實就是平均的 precision。在 darknet 訓練過程中並不輸出 precision 的結果,所以我們也可以通過 recall 來判斷。recall 越接近 1.0,就說明模型檢測到實際的物體數量越準確。如果用老版本的 darknet,並通過修改源代碼,可以在測試階段輸出 precision,具體內容我已經在本文中寫出來了。


----------- 【2017.11.30 更新】添加 IOU --------------

IOU 這個輸出我之前是忽略了,這個輸出其實也可以在訓練階段判斷模型是否在正確訓練,以及最後效果如何。


1668629536594.jpg

----------------- 【 2017.09.19 更新】 python 可視化代碼 --------------------------------------------

我一開始是寫給 caffe 的,不過道理都一樣,就順便把這個也更新了。

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
"""
Created on Tue Aug 29 10:05:13 2017

@author: hans

http://blog.csdn.net/renhanchi
"""
import matplotlib.pyplot as plt
import numpy as np
import commands

train_log_file = "vegetable_squeezenet.log"

display = 10 #solver
test_interval = 100 #solver

train_output = commands.getoutput("cat " + train_log_file + " | grep 'avg,' | awk '{print $3}'")  #train loss

train_loss = train_output.split("\n")

_,ax1 = plt.subplots()

l1, = ax1.plot(display*np.arange(len(train_loss)), train_loss)

ax1.set_xlabel('Iteration')
ax1.set_ylabel('Train Loss')

plt.legend([l1], ['Train Loss'], loc='upper right')
plt.show()

如果覺得波動太大,不好看變化趨勢。可以參考
http://blog.csdn.net/renhanchi/article/details/78411095
裡可視化代碼,改一改就好了。


6. 評價模型#

-------- 【2017.07.03 更新】 -------------------------------------------------------------------------

下面命令不適用於新版本 darknet( 如果你下載了最上面百度雲盤中老版本的 darknet,下面命令是適用的

首先使用下面的命令:

./darknet detector recall cfg/xxx.data cfg/xxx.cfg backup/xxx.weights

下面命令在新版本上是適用

在 xxxx.data 文件中 train 下面加上一句話: valid=path/to/valid/images.txt

./darknet detector valid cfg/.....data cfg/.....cfg backup/....weights

然後輸出一連串的數字數字數字,什麼鬼??


------ 【2017.06.29 更新】 --- 源代碼被作者做了很大改動,新版本已經不適用下面內容了 - -----------

如果你下載了最上面百度雲盤中老版本的 darknet,下面命令還是適用的

輸出是累積的,結果只有 recall,沒有 precision。因為第一代 yolo 有一定缺陷,precision 跟其他方法相比很低,所以作者乾脆把 threshold 設置的特別低,這樣就只看 recall,放棄 precision 了。第二代 yolo 已經修復了這個缺陷,所以可以自己修改代碼把 precision 調出來。大家打開 src/detector.c
,找到 validate_detector_recall 函數,其中 float thresh = .25;
就是用來設置 threshold 的,我這裡改成了 0.25,原作者的值是 0.0001。所以我一開始的 precision 只有 1% 多點 -0-。
繼續往下看,找到這句話

fprintf(stderr, "%5d\t%5d\t%5d\tRPs/Img: %.2f\tIOU: %.2f%%\tRecall:%.2f%%\t", i, correct, total, (float)proposals/(i+1), 
avg_iou*100/total, 100.*correct/total);

把它改成下面這句話

fprintf(stderr, "Number: %5d\tCorrect: %5d\tTotal: %5d\tRPs/Img: %.2f\tIOU: %.2f%%\tRecall:%.2f%%\tProposals: %5d\t
Precision: %.2f%%\n", i, correct, total, (float)proposals/(i+1), avg_iou*100/total, 100.*correct/total, proposals, 
100.*correct/(float)proposals);

~~
~~

重新 make 後,再次執行 recall 命令,就有 precision 啦。


------ 【2017.12.22 更新】 --- 重新表述一下 correct -----------------------------------------

Correct 表示正確的識別出了多少 bbox。這個值算出來的步驟是這樣的,丟進網絡一張圖片,網絡會對每一類物體都預測出很多 bbox,每個 bbox 都有其置信概率,概率大於 threshold 的某一類物體的 bbox 與其實際的 bbox(也就是 labels 中 txt 的內容)計算 IOU,找出當前類物體 IOU 最大的 bbox,如果這個最大值大於預設的 IOU 的 threshold,說明當前類物體分類正確,那麼 correct 加一。
我再多說兩句,對於 bbox 的 threshold,我們可以在命令行通過 -
thres 調整的,同時也是上面修改源代碼輸出 precision 所提及的 threshold。 而 IOU 的 threshold 只能通過源代碼調整。


關於輸出的參數,我的理解如下

Number 表示處理到第幾張圖片。

Correct
表示正確的識別除了多少 bbox。這個值算出來的步驟是這樣的,丟進網絡一張圖片,網絡會預測出很多 bbox,每個 bbox 都有其置信概率,概率大於 threshold 的 bbox 與實際的 bbox(也就是 labels 中 txt 的內容)計算 IOU,找出 IOU 最大的 bbox,如果這個最大值大於預設的 IOU 的 threshold,那麼 correct 加一。

Total 表示實際有多少個 bbox。

Rps/img 表示平均每個圖片會預測出來多少個 bbox。

IOU 我上面解釋過哈。

Recall 我上面也解釋過。通過代碼我們也能看出來就是 Correct 除以 Total 的值。

Proposal 表示所有預測出來的 bbox 中,大於 threshold 的 bbox 的數量。

Precision 表示精確度,就是 Correct 除以 Proposal 的值。

關於預測的框和 recall,precision 一些邏輯上面的東西,我再多說點,給大家捋一捋順序。

例如識別 人 這一類

  1. 圖片中實際有 n 個人,對應 n 個 bbox 信息。這個 n 的值就是 Total 的值

  2. 圖片丟進網絡,預測出 N 個 bbox,這 N 個 bbox 中置信概率大於 threshold 的 bbox 的數量就是 Proposal 的值,記為 Npro

  3. n 個實際 bbox 分別和 Npro 個 bbox 計算 IOU,得到 n 個 IOU 最大的 bbox,這 n 個 IOU 最大的 bbox 再與 IOU threshold 比較,得到 nCor 個大於 IOU threshold 的 bbox。nCor 就是 Correct 的值。nCor 小於等於 n。

  4. 有了 Total,Proposal 和 Correct,就能算出 Recall 和 Precision 了。

總結一下就是圖片丟網絡裡,預測出了 Npro 個物體,但有的預測對了,有的預測的不對。nCor 就是預測對了的物體數量。Recall 就是表示預測對的物體數量(nCor)和實際有多少個物體數量(Total)的比值。Precision 就是預測對的物體數量(nCor)和一共預測出所有物體數量(Proposal)的比值。聯繫上面提到的作者一開始把 threshold 設置為 0.0001,這將導致 Proposal 的值(Npro)非常大,所以 Correct 的值(nCor)也會隨之相應大一點。導致計算出的 Recall 值很大,但 Precision 值特別低。

注明:以上內容部分參考自 http://blog.csdn.net/hysteric314/article/details/54097845

關於 recall,precision 和 IOU,可以看看這邊文章
http://blog.csdn.net/hysteric314/article/details/54093734

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。