【計算機視覺】【神經網路與深度學習】YOLO v2 detection訓練自己的資料
轉自:http://blog.csdn.net/hysteric314/article/details/54097845
說明
這篇文章是訓練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>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
4,一共1003組資訊,選擇900組用於訓練(Train),103組用於驗證(Validation)。
5,這個資料集只包含一個類別:人。當然也可以說是兩個類別:人、背景。
我要訓練的資料集的有以上這些特點,它資料的格式和VOC訓練資料集的格式很相似,而YOLO v2預設是能夠訓練VOC資料集的。為了讓YOLO v2能像訓練VOC資料集一樣訓練我自己的資料集,我對程式碼進行以下三個方面的修改:
一:修改分類的個數:在程式碼中,預設VOC資料集是20類,而我要改成1類。
二:準備txt文件:VOC訓練資料集中會自帶幾個txt文件,用來指明檔名或者路徑地址,而如果你使用你自己的資料可能就需要自己生成這些文件。
三:修改程式碼中路徑資訊,把程式碼中VOC訓練資料集的路徑改成自己訓練資料集的路徑。
把20類改成1類
-
cfg/voc.data檔案中:
- classes 改成1。
- names=data/pasacal.names。
- pasacal.names這一個檔案要存在於darknet目錄下的data資料夾裡,沒有的話可以自己在那個目錄下建立一個pasacal.txt,加上內容之後,修改檔案字尾名變成pasacal.names即可,當然名字和路徑都可以自己定義。這個檔案中的行數要和類數一致,每一行都是一個類別的名字。比如我的這一檔案中就只有一行資料:“person”。這個檔案在測試你訓練的model的時候會用到,系統會在圖片上畫出bounding box,bounding box上面的文字,也就是這個框中物體的名字,應該就來自這個檔案。
-
cfg/yolo_voc.cfg檔案中 :
- 【region】層中 classes 改成1。
- 【region】層上方第一個【convolution】層,其中的filters值要進行修改,改成(classes+ coords+ 1)* (NUM) ,我的情況中:(1+4+1)* 5=30,我把filters 的值改成了30。
-
src/yolo.c 檔案中 :
- 位置大約第14行左右改成:char *voc_names={“n00000001”},原來裡面有20類的名字,我改成了唯一1類的名字。
- 位置大約第328行左右,修改draw_detection這個函式最後一個引數:20改成1。這個函式用於把系統檢測出的框給畫出來,並把畫完框的圖片傳回第一個引數im中,用於儲存和顯示。
- 位置大約第361行左右,demo函式中,倒數第三個引數我把20改成了1,雖然不知道有沒有用,反正對結果沒什麼影響。
-
src/yolo_kernels.cu 檔案中 :
- 位置第62行,draw_detection這個函式最後一個引數20改成1。
-
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()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
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
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
修改路徑
-
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資料夾,這個路徑指向那裡。*/
-
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/";//可以修改為自己的路徑
- 1
- 2
- 1
- 2
-
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的完整路徑
- 1
- 2
- 1
- 2
-
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的完整路徑
- 1
- 2
- 1
- 2
-
- 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);
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
函式會找到路徑中的圖片字尾名.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);
- 1
- 2
- 1
- 2
我執行後顯示的結果是:
…..
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值,想在某種程度上體現網路的識別精度比較高。
要說的
1,這是我的經驗總結,可能對你並沒有意義,僅供參考。
2,如果你發現了錯誤,歡迎留言指正,多謝!
3,接下來一段時間可能要比較忙了,馬上就要中期了,不過如果有新進展的話還會繼續更新部落格。
4,具體的YOLOv2的安裝,教程很多,在這就不贅述了,可以參考他們的官網:http://pjreddie.com/darknet/yolo/
5,如果對YOLO感興趣,可以跳牆去訪問這個論壇,YOLO作者會親自答疑:https://groups.google.com/forum/#!forum/darknet