【YOLO學習】使用YOLO v2訓練自己的數據

說明

這篇文章是訓練YOLO v2過程中的經驗總結,我使用YOLO v2訓練一組自己的數據,訓練後的model,在閾值爲.25的情況下,Recall值是95.54%,Precision 是97.27%。
需要注意的是,這一訓練過程可能只對我自己的訓練集有效,因爲我是根據我這一訓練集的特徵來對YOLO代碼進行修改,可能對你的數據集並不適用,所以僅供參考。

我的數據集

1,用於訓練的數據集一共1003張圖片和1003個與圖片對應的標記信息(xml格式)。
2,圖片的格式是 jpg,分辨率都是384*288,圖片的命名從0000.jpg到1002.jpg,與VOC數據集的命名方式差不多。
3,標記信息的格式xml,命名從0000.xml到1002.xml,標記內容格式與VOC中標記信息的格式類似,某一xml中具體內容如下:

<annotation>
    <folder>Image</folder>
    <filename>0000</filename>
    <source>
        <database>Pedestrian_ultrared</database>
    </source>
    <size>
        <width>384</width>
        <height>288</height>
    </size>
    <object>
        <name>n00000001</name>
        <bndbox>
            <xmin>232</xmin>
            <xmax>248</xmax>
            <ymin>161</ymin>
            <ymax>203</ymax>
        </bndbox>
    </object>
</annotation>

4,一共1003組信息,選擇900組用於訓練(Train),103組用於驗證(Validation)。
5,這個數據集只包含一個類別:人。當然也可以說是兩個類別:人、背景。

我要訓練的數據集的有以上這些特點,它數據的格式和VOC訓練數據集的格式很相似,而YOLO v2默認是能夠訓練VOC數據集的。爲了讓YOLO v2能像訓練VOC數據集一樣訓練我自己的數據集,我對代碼進行以下三個方面的修改:
一:修改分類的個數:在代碼中,默認VOC數據集是20類,而我要改成1類。
二:準備txt文檔:VOC訓練數據集中會自帶幾個txt文檔,用來指明文件名或者路徑地址,而如果你使用你自己的數據可能就需要自己生成這些文檔。
三:修改代碼中路徑信息,把代碼中VOC訓練數據集的路徑改成自己訓練數據集的路徑。

把20類改成1類

  1. cfg/voc.data文件中:

    • classes 改成1。
    • names=data/pasacal.names。
    • pasacal.names這一個文件要存在於darknet目錄下的data文件夾裏,沒有的話可以自己在那個目錄下創建一個pasacal.txt,加上內容之後,修改文件後綴名變成pasacal.names即可,當然名字和路徑都可以自己定義。這個文件中的行數要和類數一致,每一行都是一個類別的名字。比如我的這一文件中就只有一行數據:“person”。這個文件在測試你訓練的model的時候會用到,系統會在圖片上畫出bounding box,bounding box上面的文字,也就是這個框中物體的名字,應該就來自這個文件。
  2. cfg/yolo_voc.cfg文件中 :

    • 【region】層中 classes 改成1。
    • 【region】層上方第一個【convolution】層,其中的filters值要進行修改,改成(classes+ coords+ 1)* (NUM) ,我的情況中:(1+4+1)* 5=30,我把filters 的值改成了30。
    • 修改filters的建議來源自(https://groups.google.com/forum/#!topic/darknet/B4rSpOo84yg),我修改了之後一切正常。
  3. src/yolo.c 文件中 :

    • 位置大約第14行左右改成:char *voc_names={“n00000001”},原來裏面有20類的名字,我改成了唯一1類的名字。
    • 位置大約第328行左右,修改draw_detection這個函數最後一個參數:20改成1。這個函數用於把系統檢測出的框給畫出來,並把畫完框的圖片傳回第一個參數im中,用於保存和顯示。
    • 位置大約第361行左右,demo函數中,倒數第三個參數我把20改成了1,雖然不知道有沒有用,反正對結果沒什麼影響。
  4. src/yolo_kernels.cu 文件中 :

    • 位置第62行,draw_detection這個函數最後一個參數20改成1。
  5. scripts/voc_label.py 文件中 :

    • 位置第9行改成:classes=[“n00000001”],因爲我只有一類。

準備txt文檔

一共需要準備四個txt格式文檔:train.txt與val.txt,infrared_train.txt與infrared_val.txt:

train.txt與val.txt
在生成infrared_train.txt與infrared_val.txt這兩個文件時,會分別用到這兩個文檔。文檔裏包含了用於訓練/驗證的圖片的名稱,裏面的數據組成很簡單,每行都是一個圖片的名稱,並不包含圖片的後綴(.jpg),比如文檔中:
第一行是: 0000
第二行是: 0001
…..

生成腳本:creat_list.py:

#這個小腳本是用來打開圖片文件所在文件夾,把前900個用於訓練的圖片的名稱保存在tain.txt,後103個用於驗證的圖片保存在val.txt
import os
from os import listdir, getcwd
from os.path import join
if __name__ == '__main__':
    source_folder='/home/yolo_v2_tinydarknet/darknet/infrared/image/dout/'#地址是所有圖片的保存地點
    dest='/home/yolo_v2_tinydarknet/darknet/infrared/train.txt' #保存train.txt的地址
    dest2='/home/yolo_v2_tinydarknet/darknet/infrared/val.txt'  #保存val.txt的地址
    file_list=os.listdir(source_folder)       #賦值圖片所在文件夾的文件列表
    train_file=open(dest,'a')                 #打開文件
    val_file=open(dest2,'a')                  #打開文件
    for file_obj in file_list:                #訪問文件列表中的每一個文件
        file_path=os.path.join(source_folder,file_obj) 
        #file_path保存每一個文件的完整路徑
        file_name,file_extend=os.path.splitext(file_obj)
        #file_name 保存文件的名字,file_extend保存文件擴展名
        file_num=int(file_name) 
        #把每一個文件命str轉換爲 數字 int型 每一文件名字都是由四位數字組成的  如 0201 代表 201     高位補零  
        if(file_num<900):                     #保留900個文件用於訓練
            #print file_num
            train_file.write(file_name+'\n')  #用於訓練前900個的圖片路徑保存在train.txt裏面,結尾加回車換行
        else :
            val_file.write(file_name+'\n')    #其餘的文件保存在val.txt裏面
    train_file.close()#關閉文件
    val_file.close()

infrared_train.txt與infrared_val.txt
這個文檔用於告訴訓練系統哪些圖片是用來進行訓練,哪些是用於驗證的。
文檔裏包含了所有用於訓練/驗證的圖片的完整路徑,每一行都是一個圖片的完整路徑,例如
第一行是: /home/yolo_v2_tinydarknet/darknet/infrared/image/dout/0000.jpg
第二行是 :/home/yolo_v2_tinydarknet/darknet/infrared/image/dout/0001.jpg
…..

生成腳本:voc_label_change.py

#此腳本修改自voc_label.py。修改的原因是:我的訓練集跟voc有些不同。
#由於數據集中包括用於訓練的數據和用於驗證的數據,所以此腳本可能需要分別對這兩種數據各運行一次,對兩種數據只需要簡單地註釋掉相應語句即可
#這個腳本需要train.txt ,這個文件是我用腳本creat_list.py生成的,保存了用於訓練的圖片的名字id,保存了用於訓練的圖片的名字
#這個腳本需要val.txt文件,這個文件是我用腳本creat_list.py生成的,保存了用於驗證的圖片的名字id,保存了用於驗證的圖片的名字
#這個腳本還需要xml格式的標籤文件,我的訓練集xml文件的格式與voc2007的類似,xml文件的名稱與對應的用於訓練的圖片的名稱相同
#這個腳本會生成 indrared_train.txt文件 ,用於保存每一用於訓練的圖片的完整的路徑,隨後會被voc.data yolo.c使用
#這個腳本會生成 indrared_val.txt文件 ,用於保存每一用於驗證的圖片的完整的路徑,隨後會被voc.data yolo.c使用
#這個腳本還會生成 txt格式的yolo可識別的標籤文件,轉換自每一個用於訓練或驗證的圖片對應的xml文件,txt格式的文件名稱與對應的xml文件名相同,但是內容不同,擴展名不同
#這個腳本 需要與圖片對應的xml文件所在的地址,需要,轉換後生成的txt的完整保存路徑
import xml.etree.ElementTree as ET
import pickle
import os
from os import listdir, getcwd
from os.path import join
#sets=[('2012', 'train'), ('2012', 'val'), ('2007', 'train'), ('2007', 'val'), ('2007', 'test')] #按照自己的文件格式改的,不需要判斷是那個voc數據包
classes = ["n00000001"]#因爲我的數據集只有一個類別
def convert(size, box):#voc_label.py 自帶的函數,沒有修改
    dw = 1./size[0]
    dh = 1./size[1]
    x = (box[0] + box[1])/2.0
    y = (box[2] + box[3])/2.0
    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(image_id):
    #in_file = open('VOCdevkit/VOC%s/Annotations/%s.xml'%(year, image_id))
    in_file = open('/home/yolo_v2_tinydarknet/darknet/infrared/labels/dout_original/%s.xml'%(image_id))#與圖片對應的xml文件所在的地址
    out_file = open('/home/yolo_v2_tinydarknet/darknet/infrared/labels/%s.txt'%(image_id),'w') #與此xml對應的轉換後的txt,這個txt的保存完整路徑
    tree=ET.parse(in_file)
    root = tree.getroot()
    size = root.find('size')  #訪問size標籤的數據
    w = int(size.find('width').text)#讀取size標籤中寬度的數據
    h = int(size.find('height').text)#讀取size標籤中高度的數據

    for obj in root.iter('object'):
       # difficult = obj.find('difficult').text   #由於自己的文件裏面沒有diffcult這一個標籤,所以就屏蔽之
        cls = obj.find('name').text
        if cls not in classes :#or int(difficult) == 1:
            continue
        cls_id = classes.index(cls)
        xmlbox = obj.find('bndbox')   #訪問boundbox標籤的數據並進行處理,都按yolo自帶的代碼來,沒有改動
        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')

#image_ids = open('VOCdevkit/VOC%s/ImageSets/Main/%s.txt'%(year, image_set)).read().strip().split()  #之前代碼是按照sets裏面的字符來訪問保存有圖片名字的train或者val的txt文件
image_ids = open('/home/yolo_v2_tinydarknet/darknet/infrared/train.txt').read().strip().split()  #如果是訓練集數據打開這一行,註釋下一行
#image_ids = open('/home/yolo_v2_tinydarknet/darknet/infrared/val.txt').read().strip().split()  #如果是驗證數據集數據打開這一行,註釋上一行
#list_file = open('%s_%s.txt'%(year, image_set), 'w')
list_file = open('infrared_train.txt', 'w')     #把結果寫入到indrared_train.txt文件中,如果是訓練集數據打開這一行,註釋下一行
#list_file = open('infrared_val.txt', 'w')     #把結果寫入到indrared_train.txt文件中,如果是驗證數據集數據打開這一行,註釋上一行
for image_id in image_ids:
    #list_file.write('%s/VOCdevkit/VOC%s/JPEGImages/%s.jpg\n'%(wd, year, image_id))
    list_file.write('/home/yolo_v2_tinydarknet/darknet/infrared/image/dout/%s.jpg\n'%(image_id))  #把每一用於訓練或驗證的圖片的完整的路徑寫入到infrared_train.txt中  這個文件會被voc.data yolo.c調用
    convert_annotation(image_id)   #把圖片的名稱id傳給函數,用於把此圖片對應的xml中的數據轉換成yolo要求的txt格式
list_file.close() #關閉文件

修改路徑

  1. cfg/voc.data文件中:

    • train = /home/yolo_v2_tinydarknet/darknet/infrared/infrared_train.txt //infrared_train.txt的完整路徑
    • valid = /home/yolo_v2_tinydarknet/darknet/infrared/infrared_val.txt //infrared_val.txt的完整路徑
    • backup = /home/yolo_v2_tinydarknet/darknet/backup/ /* 這個路徑是YOLO用於備份的,在訓練過程中YOLO會不斷地對產生的weights文件進行備份,darknet目錄下就自帶一個backup文件夾,這個路徑指向那裏。*/
  2. src/yolo.c 文件中:

    • train_yolo函數中:

      char *train_images =" /home/yolo_v2_tinydarknet/darknet/infrared/infrared_train.txt";//infrared_train.txt的完整路徑
      char *backup_directory = "/home/yolo_v2_tinydarknet/darknet/backup/";//可以修改爲自己的路徑
    • validate_yolo函數中:

      char *base = "/home/yolo_v2_tinydarknet/darknet/results/comp4_det_test_";//可以修改自己的路徑,好像是用於保存測試結果
      list *plist=get_paths("/home/yolo_v2_tinydarknet/darknet/infrared/infrared_val.txt");//infrared_val.txt的完整路徑
    • validate_yolo_recall函數中:

      char *base = "/home/yolo_v2_tinydarknet/darknet/results/comp4_det_test_";//可以修改自己的路徑
      list *plist = get_paths("/home/yolo_v2_tinydarknet/darknet/infrared/infrared_val.txt");//infrared_val.txt的完整路徑
  3. src/detector.c 文件中:
    • 位置第375行改成:list *plist = get_paths(“/home/yolo_v2_tinydarknet/darknet/infrared/infrared_val.txt”);//改成infrared_val.txt的完整路徑
    • 需要注意的是,這個文件裏的validate_detector_recall函數是用來計算輸出recall值的,後面會說。

對了,可能你注意到,輸入到系統的infrared_train.txt或者infrared_val.txt都只是圖片的完整路徑,你也知道,要進行訓練的話,除了需要圖片還需要標記信息,然而標記信息僅僅用我的腳本voc_label_change.py從xml轉換成了YOLO可識別的txt格式,但是它們的完整路徑並沒有輸入進系統,那麼系統該怎麼找到它們呢?

因爲在訓練集中,一個圖片文件和這一圖片文件對應標記文件,他們倆除了後綴名之外其餘的名稱是一樣的,所以src/yolo.c中有以下語句:

          find_replace(path, "dout", "labels", labelpath);
          find_replace(labelpath, "JPEGImages", "labels", labelpath);
          find_replace(labelpath, ".jpg", ".txt", labelpath);
          find_replace(labelpath, ".JPEG", ".txt", labelpath);

函數會找到路徑中的圖片後綴名.jpg,自動替換成.txt。比如:
/home/yolo_v2_tinydarknet/darknet/infrared/image/dout/0000.jpg
自動替換後變成了
/home/yolo_v2_tinydarknet/darknet/infrared/image/dout/0000.txt
所以在使用voc_label_new.py轉換生成txt格式的標記信息之後,只需要把這些txt格式的標記文件複製到圖片所在的目錄下即可。系統根據替換後的路徑地址來讀取對應標記文件。

開始訓練!

通過複雜地修改和準備,終於可以開始訓練了
我是按照YOLO的官方指南來的,首先下載一個預訓練的model(當然你也可以自己生成),放到darkent/目錄下。
下載地址 (76 MB):http://pjreddie.com/media/files/darknet19_448.conv.23
然後運行指令:./darknet detector train cfg/voc.data cfg/yolo_voc.cfg darknet19_448.conv.23
就可以開始訓練了,系統默認會迭代45000次,我花了一週時間才訓練完。
當然迭代次數是可以修改的,應該是在cfg/yolo_voc.cfg修改max_batches的值就行。

評估性能

經過漫長的訓練過程,model終於訓練好了,爲了評估性能,可以使用以下指令
./darknet detector recall cfg/voc.data cfg/yolo_voc.cfg backup/yolo_voc_final.weights

需要注意的是,在使用這個指令之前,我先修改一下src/detector.c 這一函數

  • 位置第375行改成:list *plist = get_paths(“/home/yolo_v2_tinydarknet/darknet/infrared/infrared_val.txt”);//改成infrared_val.txt的完整路徑
  • 運行上面的指令會調用validate_detector_recall函數,這個函數中有個參數thresh(閾值),默認的值是.001,這個默認值設的很小,會讓系統識別出更多的框來,導致proposals值激增,還會讓recall值變高,達到98.5%。最終我改成了 .25。
  • 上面的函數只會顯示出recall值,沒有precision值,precision的值計算方法是:識別爲正確的個數/畫了多少個框,所以我修改了代碼。我把第447行顯示結果的代碼修改爲 :
fprintf(stderr, "ID:%5d Correct:%5d Total:%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, "proposals:%5d\tPrecision:%.2f%%\n",proposals,100.*correct/(float)proposals); 

我運行後顯示的結果是:
…..
ID: 101 Correct: 106 Total: 111 RPs/Img: 1.07 IOU: 82.00% Recall:95.50% proposals: 109 Precision:97.25%
ID: 102 Correct: 107 Total: 112 RPs/Img: 1.07 IOU: 82.11% Recall:95.54% proposals: 110 Precision:97.27%

結果中的參數,我的理解是:

Correct :可以理解爲正確地畫了多少個框,遍歷每張圖片的Ground Truth,網絡會預測出很多的框,對每一Groud Truth框與所有預測出的框計算IoU,在所有IoU中找一個最大值,如果最大值超過一個預設的閾值,則correct加一。

Total:一共有多少個Groud Truth框。

Rps/img:p 代表proposals, r 代表region。 意思就是平均下來每個圖片會有預測出多少個框。預測框的決定條件是,預測某一類的概率大於閾值。在validation_yolo_recall函數中,默認的這一個閾值是0.001,這一閾值設置的比較低,這就會導致會預測出很多個框,但是這樣做是可以提升recall的值,一般yolo用於畫框的默認值是.25,使用這個閾值會讓畫出來的框比較準確。而validation_yolo_recall使用的閾值改成。25的時候,Rps/img 值會降低,recall的值會降低,所以validation_yolo_recall默認使用一個較低的閾值,有可能作者的目的就是爲了提高recall值,想在某種程度上體現網絡的識別精度比較高。

IoU、Recall、Precision:解釋起來比較麻煩,請看我的博客有詳細說明:
http://blog.csdn.net/hysteric314/article/details/54093734

要說的

1,這是我的經驗總結,可能對你並沒有意義,僅供參考。
2,如果你發現了錯誤,歡迎留言指正,多謝!
3,接下來一段時間可能要比較忙了,馬上就要中期了,不過如果有新進展的話還會繼續更新博客。
4,具體的YOLOv2的安裝,教程很多,在這就不贅述了,可以參考他們的官網:http://pjreddie.com/darknet/yolo/
5,如果對YOLO感興趣,可以跳牆去訪問這個論壇,YOLO作者會親自答疑:https://groups.google.com/forum/#!forum/darknet

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