Caffe學習系列——Faster-RCNN訓練自己的資料集
由於要實現服裝的目標檢測,所以一直在研究Faster-RCNN 。講到目標檢測,不得不提到rbg大神的深度神經網路檢測算法系列RCNN、Fast-RCNN、Faster-RCNN,其還在github上開源了自己的程式碼,造福廣大碼農。這是rbg大神的主頁 https://people.eecs.berkeley.edu/~rbg/index.html ,以及本篇博文會用到的Faster-RCNN的github原始碼地址 https://github.com/rbgirshick/py-faster-rcnn,歡迎前去膜拜學習。對於RCNN、Fast-RCNN、Faster-RCNN的介紹就不在這裡贅述了,可以看看我的上一篇博文 。該github原始碼是Faster-RCNN python版本的實現,並使用caffe。除了python版本,rbg大神還提供了matlab程式碼。這兩種實現方式有些許的不同,主要體現在以下幾點:
- 由於使用了python layer,python版本的在測試時間上會比matlab版本的慢10%左右,但是準確率差不多甚至略高一點。
- 由於在實現方式上有一點點不同,所以使用MATLAB程式碼訓練的模型與Python版本的不相容
- 目前已經實現的方式有兩種:Alternative training和Approximate join training。推薦使用第二種,因為第二種使用的視訊記憶體更小,而且訓練會更快。
本篇博文主要分為兩部分,第一部分講如何配置py-faster-rcnn並訓練PASCALVOC2007,執行demo。第二部分講如何對程式碼和資料集進行修改實現自己資料的訓練與檢測。
配置與執行Demo
1.配置、編譯與安裝環境
-
在進行這一步之前,需已經在自己的機器上配置好caffe環境以及各種依賴項的安裝,在配置之前,需確保已經安裝以下幾個python包:cython、easydict和python-opencv。安裝命令如下:
1
2
3pip install cython
pip install easydict
apt-get install python-opencv -
從github上clone專案,注意!一定要在clone時加入–recursive引數,不然會很麻煩,也不要直接下載,在機器上裝個git來clone,這樣會省去很多時間。
-
Cython模組編譯
cd $FRCN_ROOT /lib
make -
caffe和pycaffe的編譯
在編譯之前,需要複製$FRCN_ROOT/caffe-fast-rcnn 的Makefile.config.example,然後重新命名為Makefile.config。
需要注意的是裡面有幾個配置需要新增
開啟USE_CUDNN=1,這個選項預設情況下是關閉的,需要開啟讓CUDA支援DNN
開啟WITH_PYTHON_LAYER=1,預設關閉,需開啟,因為FasterRCNN需要支援Python介面。
執行以下命令進行編譯cd $FRCN_ROOT/caffe-fast-rcnn
make -j8 && make pycaffe
2.執行demo
-
下載訓練好的模型,下載後這個faster_rcnn_models資料夾在$FRCN_ROOT/data下面,可以從data/README.md中檢視關於這個的詳細介紹。這些模型是在VOC 2007 上訓練的。
cd $FRCN_ROOT
./data/scripts/fetch_faster_rcnn_models.sh
這裡有個小建議,就是下載模型時,直接去指令碼檔案中複製URL使用迅雷下載更快。 -
執行demo
cd $FRCN_ROOT
./tools/demo.py
這個demo展示了使用在PASCAL VOC 2007上訓練的VGG16網路來進行目標檢測。執行結果如下圖所示:
3.訓練PASCAL VOC 2007的資料集
將VOCdevkit改名為VOCdevkit2007,然後放到data資料夾下,亦可以使用軟連線的方式,
cd $FRCN_ROOT/data
ln -s $VOCdevkit VOCdevkit2007
-
下載預訓練的ImageNet模型
cd $FRCN_ROOT
./data/scripts/fetch_imagenet_models.sh
VGG16來自Caffe Mode Zoo,ZF是由MSRA訓練的結構 - 訓練資料
(1) 使用交替優化(alternating optimization)演算法來訓練和測試Faster R-CNN1
2
3
4
5
6
7
8cd $FRCN_ROOT
./experiments/scripts/faster_rcnn_alt_opt.sh [GPU_ID] [NET] [--set ...]
# GPU_ID是你想要訓練的GPUID
# 你可以選擇如下的網路之一進行訓練:ZF, VGG_CNN_M_1024, VGG16
# --set ... 執行你自定義fast_rcnn.config引數,例如.
# --set EXP_DIR seed_rng1701 RNG_SEED 1701
#例如命令
./experiments/scripts/faster_rcnn_alt_opt.sh 0 ZF pascal_voc
輸出的結果在 $FRCN_ROOT/output下。訓練過程截圖:
(2) 使用近似聯合訓練( approximate joint training)
cd $FRCN_ROOT
./experiments/scripts/faster_rcnn_end2end.sh [GPU_ID] [NET] [--set ...]
這個方法是聯合RPN模型和Fast R-CNN網路訓練。而不是交替訓練。用此種方法比交替優化快1.5倍,但是準確率相近。所以推薦使用這種方法
訓練Fast R-CNN網路的結果儲存在這個目錄下:
output/<experiment directory>/<dataset name>/
測試儲存在這個目錄下:
output/<experiment directory>/<dataset name>/<network snapshot name>/
4.遇到的問題
- TypeError: ‘numpy.float64’ object cannot be interpreted as an index
這個錯誤是$FRCN_ROOT/lib/roi_data_layer下的minibatch.py中的npr.choice引起的,所以需要改成ruxia所示1
2
3
4if fg_inds.size > 0:
for i in range(0,len(fg_inds)):
fg_inds[i] = int(fg_inds[i])
fg_inds = npr.choice(fg_inds, size=int(fg_rois_per_this_image), replace=False)
注意有兩個npr.choice,所以兩個地方都按照如上來改。
- labels[fg_rois_per_this_image:] = 0
TypeError: slice indices must be integers or None or have an index method
這個錯誤是由numpy的版本引起的,只要將fg_rois_per_this_image強制轉換為int型就可以了
labels[int(fg_rois_per_this_image):] = 0
使用Faster-RCNN訓練自己的資料集
1.工程目錄簡介
由於需要訓練自己的資料集,所以需要對這個工程各個目錄的作用有所瞭解
- caffe-fast-rcnn:caffe框架目錄
- data:用來存放pretrained模型以及讀取檔案的cache快取,還有一些下載模型的指令碼
- experiments:存放配置檔案以及執行的log檔案,另外這個目錄下有scripts,裡面存放end2end和alt_opt兩種訓練方式的指令碼
- lib:用來存放一些python介面檔案,如其下的datasets主要負責資料庫讀取,config負責一些訓練的配置選項
- models:裡面存放了三個模型檔案,小型網路ZF,中型網路VGG_CNN_M_1024以及大型網路VGG16,根據你的硬體條件來選擇使用哪種網路,ZF和VGG_CNN_M_1024需要至少3G記憶體,VGG16需要更多的記憶體,但不會超過11G。
- output:這裡存放的是訓練完成後的輸出目錄,這是運行了訓練後才會出現的目錄
- tools:裡面存放的是訓練和測試的Python檔案
2.建立資料集
我個人覺得訓練模型最頭疼的是資料集的準備,本文我以服裝的識別為例來說明如何用自己的資料集進行目標檢測。在準備資料集的時候,假如你是第一次用自己的資料集進行訓練,那麼最好是參照上一章節跑的demo中的VOC2007資料集的格式來準備,這樣,關於後續訓練過程會涉及到的配置更改會較為簡單,比較容易成功。在本次的訓練過程中,我使用的是由香港中文大學Large-scale
Fashion (DeepFashion) Database提供的服裝標記相關的資料集,在此特別感謝一下他們願意將資料集公開。想要獲取他們的資料集只需要發郵件申請即可。基於上述已標記的資料集,可將資料集整理得到如下所示的格式:
0000001.jpg Blouse 72 79 232 273
0000002.jpg Shorts 67 59 155 161
如果一張圖片有多個目標,則格式如下:(比如兩個目標)
000002.jpg dog 44 28 132 121
000002.jpg car 50 27 140 110
- 將上述的txt轉成xml,可參考如下matlab程式碼:
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117%%
%該程式碼可以做voc2007資料集中的xml檔案,
%txt檔案每行格式為:0000001.jpg Tee 44 28 132 121
%即每行由圖片名、目標型別、包圍框座標組成,空格隔開
%包圍框座標為左上角和右下角
%@author:bealin
%%
clc;
clear;
%注意修改下面四個變數
imgpath='/home/linbiyuan/py-faster-rcnn/data/VOCdevkit2007/VOC2007/JPEGImages/';%影象存放資料夾
txtpath='/home/linbiyuan/py-faster-rcnn/data/VOCdevkit2007/VOC2007/alldata.txt';%txt檔案
xmlpath_new='/home/linbiyuan/py-faster-rcnn/data/VOCdevkit2007/VOC2007/Annotations/';%修改後的xml儲存資料夾
foldername='VOC2007';%xml的folder欄位名
fidin=fopen(txtpath,'r');
while ~feof(fidin)
tline=fgetl(fidin);
str = regexp(tline, ' ','split');
filepath=[imgpath,str{1}];
img=imread(filepath);
[h,w,d]=size(img);
rectangle('Position',[str2double(str{3}),str2double(str{4}),str2double(str{5})-str2double(str{3}),str2double(str{6})-str2double(str{4})],'LineWidth',4,'EdgeColor','r');
Createnode=com.mathworks.xml.XMLUtils.createDocument('annotation');
Root=Createnode.getDocumentElement;%根節點
node=Createnode.createElement('folder');
node.appendChild(Createnode.createTextNode(sprintf('%s',foldername)));
Root.appendChild(node);
node=Createnode.createElement('filename');
node.appendChild(Createnode.createTextNode(sprintf('%s',str{1})));
Root.appendChild(node);
source_node=Createnode.createElement('source');
Root.appendChild(source_node);
node=Createnode.createElement('database');
node.appendChild(Createnode.createTextNode(sprintf('My Database')));
source_node.appendChild(node);
node=Createnode.createElement('annotation');
node.appendChild(Createnode.createTextNode(sprintf('VOC2007')));
source_node.appendChild(node);
node=Createnode.createElement('image');
node.appendChild(Createnode.createTextNode(sprintf('flickr')));
source_node.appendChild(node);
node=Createnode.createElement('flickrid');
node.appendChild(Createnode.createTextNode(sprintf('NULL')));
source_node.appendChild(node);
owner_node=Createnode.createElement('owner');
Root.appendChild(owner_node);
node=Createnode.createElement('flickrid');
node.appendChild(Createnode.createTextNode(sprintf('NULL')));
owner_node.appendChild(node);
node=Createnode.createElement('name');
node.appendChild(Createnode.createTextNode(sprintf('lby')));
owner_node.appendChild(node);
size_node=Createnode.createElement('size');
Root.appendChild(size_node);
node=Createnode.createElement('width');
node.appendChild(Createnode.createTextNode(sprintf('%s',num2str(w))));
size_node.appendChild(node);
node=Createnode.createElement('height');
node.appendChild(Createnode.createTextNode(sprintf('%s',num2str(h))));
size_node.appendChild(node);
node=Createnode.createElement('depth');
node.appendChild(Createnode.createTextNode(sprintf('%s',num2str(d))));
size_node.appendChild(node);
node=Createnode.createElement('segmented');
node.appendChild(Createnode.createTextNode(sprintf('%s','0')));
Root.appendChild(node);
object_node=Createnode.createElement('object');
Root.appendChild(object_node);
node=Createnode.createElement('name');
node.appendChild(Createnode.createTextNode(sprintf('%s',str{2})));
object_node.appendChild(node);
node=Createnode.createElement('pose');
node.appendChild(Createnode.createTextNode(sprintf('%s','Unspecified')));
object_node.appendChild(node);
node=Createnode.createElement('truncated');
node.appendChild(Createnode.createTextNode(sprintf('%s','0')));
object_node.appendChild(node);
node=Createnode.createElement('difficult');
node.appendChild(Createnode.createTextNode(sprintf('%s','0')));
object_node.appendChild(node);
bndbox_node=Createnode.createElement('bndbox');
object_node.appendChild(bndbox_node);
node=Createnode.createElement('xmin');
node.appendChild(Createnode.createTextNode(sprintf('%s',num2str(str{3}))));
bndbox_node.appendChild(node);
node=Createnode.createElement('ymin');
node.appendChild(Createnode.createTextNode(sprintf('%s',num2str(str{4}))));
bndbox_node.appendChild(node);
node=Createnode.createElement('xmax');
node.appendChild(Createnode.createTextNode(sprintf('%s',num2str(str{5}))));
bndbox_node.appendChild(node);
node=Createnode.createElement('ymax');
node.appendChild(Createnode.createTextNode(sprintf('%s',num2str(str{6}))));
bndbox_node.appendChild(node);
%儲存xml%
lastname=str{1};
tempname=strrep(lastname,'.jpg','.xml');
xmlwrite(tempname,Createnode);
fprintf('%s\n',tempname);
end
fclose(fidin);
由於我的資料集是單標籤的,所以這份程式碼是針對單標籤來生成xml。假如你的資料集是多標籤的,txt的格式可前所示,而相應的matlab程式碼做出一點修改即可。生成的xml格式如下
這份程式碼執行生成的xml檔案是在當前目錄下的,所以還需要將所有的xml放到VOC2007下的Annotations中,可參考如下linux命令:
for xml in *.xml;do mv $xml Annotations/;done
- 將所有的訓練圖片放到JPEGImages資料夾中,生成ImageSet\Main裡的四個txt檔案,分別是:trainval.txt(訓練和驗證集總和)、train.txt(訓練集)、val.txt(驗證集)、test.txt(測試集),trainval集佔整個資料集的70%,train集佔trainval集的70%,val集佔trainval集的30%,test集佔整個資料集的30%。可參考以下程式碼進行資料集的劃分:
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%%
%該程式碼根據已生成的xml,製作VOC2007資料集中的trainval.txt;train.txt;test.txt和val.txt
%trainval佔總資料集的70%,test佔總資料集的30%;train佔trainval的70%,val佔trainval的30%;
%上面所佔百分比可根據自己的資料集修改
%注意修改下面兩個路徑
xmlfilepath='/home/linbiyuan/py-faster-rcnn/data/VOCdevkit2007/VOC2007/Annotations';
txtsavepath='/home/linbiyuan/py-faster-rcnn/data/VOCdevkit2007/VOC2007/ImageSets/Main/';
xmlfile=dir(xmlfilepath);
numOfxml=length(xmlfile)-2;%減去.和.. 總的資料集大小
trainval=sort(randperm(numOfxml,floor(numOfxml*0.7)));%trainval為資料集的50%
test=sort(setdiff(1:numOfxml,trainval));%test為剩餘50%
trainvalsize=length(trainval);%trainval的大小
train=sort(trainval(randperm(trainvalsize,floor(trainvalsize*0.7))));
val=sort(setdiff(trainval,train));
ftrainval=fopen([txtsavepath 'trainval.txt'],'w');
ftest=fopen([txtsavepath 'test.txt'],'w');
ftrain=fopen([txtsavepath 'train.txt'],'w');
fval=fopen([txtsavepath 'val.txt'],'w');
for i=1:numOfxml
if ismember(i,trainval)
fprintf(ftrainval,'%s\n',xmlfile(i+2).name(1:end-4));
if ismember(i,train)
fprintf(ftrain,'%s\n',xmlfile(i+2).name(1:end-4));
else
fprintf(fval,'%s\n',xmlfile(i+2).name(1:end-4));
end
else
fprintf(ftest,'%s\n',xmlfile(i+2).name(1:end-4));
end
end
fclose(ftrainval);
fclose(ftrain);
fclose(fval);
fclose(ftest);
至此,資料集的構建就完成啦,你可以新建一個資料夾,將上述三個資料夾放到裡面去,也可將上述三個資料夾分貝替換VOC2007資料集中的Annotations、ImageSets和JPEGImages,這樣可免去一些訓練的修改。本文選擇的是替換~
3.訓練自己的資料
1. 修改prototxt配置檔案
這些配置檔案都在models下的pascal_voc下。裡面有三種網路結構:ZF、VGG16、VGG_CNN_M_1024,本文選擇的是VGG_CNN_M_1024 。每個網路結構中都有三個資料夾,分別是faster_rcnn_end2end、faster_rcnn_alt_opt、faster_rcnn。使用近似聯合訓練,比交替優化快1.5倍,但是準確率相近,所以推薦使用這種方法。更改faster_rcnn_end2end資料夾下的train.prototxt和test.prototxt,train中需要更改的地方有三處,
第一處是input-data層,將原先的21改成:你的實際類別數+1(背景),我目標檢測一共有46類,所以加上背景這一類,一共47類。
第二處是cls_score層
第三處是bbox_pred,這裡需將原來的84改成(你的類別數+1)4,即(46+1)4=188
test.prototxt中沒有input-data層,所以只需按照train中的修改cls_score層以及bbox_pred層即可
2. 修改lib/datasets/pascal_voc.py,將類別改成自己的類別
這裡有一個注意點就是,這裡的類別以及你之前的類別名稱最好是全部小寫,假如是大寫的話,則會報keyError的錯誤,這時只需要在pascal_voc。py中第218行的lower去掉即可
datasets目錄下主要有三個檔案,分別是
(1) factory.py:這是一個工廠類,用類生成imdb類並且返回資料庫供網路訓練和測試使用;
(2) imdb.py:是資料庫讀寫類的基類,封裝了許多db的操作;
(3) pascl_voc.pyRoss用這個類來操作
3. 修改py-faster-rcnn/lib/datasets/imdb.py
在使用自己的資料進行訓練時,假如你的資料集中的圖片沒有統一整理過就會報 assert(boxes[:,2] >= boxes[:,0]).all() 這個錯誤,故需在imdb.py中加入如下幾行
4. 開始訓練
cd py-faster-rcnn
./experiments/scripts/faster_rcnn_end2end.sh 0 VGG_CNN_M_1024 pascal_voc
由於訓練過程太長,可以將訓練過程產生的輸出定向輸入到log檔案中,這樣可方便檢視。只需在上述命令中加入定向輸入的命令即可,如下:
./experiments/scripts/faster_rcnn_end2end.sh 0 VGG_CNN_M_1024 pascal_voc > /home/lby/log/clothdirector.log 2>&1
!!!訓練前需要將cache中的pki檔案以及VOCdevkit2007中annotations_cache的快取刪掉。
訓練過程中會遇到的問題
- roidb[i][‘image’] = imdb.image_path_at(i)
IndexError: list index out of range
解決方法:刪除data/cache裡面的pki檔案
注意:不管在訓練過程中遇到什麼問題,修正過後,重新訓練之前都需要將cache中的pki檔案刪除之後再重新執行, - R = [obj for obj in recs[imagename] if obj[‘name’] == classname]
KeyError: ‘0000001’
這是在測試時出現錯誤,刪掉VOCdevkit2007中annotations_cache的快取
4.測試結果
訓練完成之後,將output中的最終模型拷貝到data/faster_rcnn_models,修改tools下的demo.py,我是使用VGG_CNN_M_1024這個中型網路,不是預設的ZF,所以要改的地方挺多
1. 修改class
1 |
CLASSES = ('__background__', |
2. 增加你自己訓練的模型
1 |
NETS = {'vgg16': ('VGG16', |
3. 修改prototxt,如果你用的是ZF,就不用改了
1 |
prototxt = os.path.join(cfg.MODELS_DIR, NETS[args.demo_net][0], |
4. 開始檢測
執行 ./tools/demo.py –net myvgg1024
假如不想那麼麻煩輸入引數,可以在demo的parse_args()裡修改預設引數
parser.add_argument(‘–net’, dest=’demo_net’, help=’Network to use [myvgg1024]’,
choices=NETS.keys(), default=’myvgg1024’)
這樣只需要輸入 ./tools/demo.py 就可以了
檢測結果:
遇到的問題
- Cannot copy param 0 weights from layer“”:已放棄(核心已轉儲)
沒有修改prototxt,詳情請見第3步 -
Makefile:2: recipe for target ‘all’ failed
Traceback (most recent call last):
File “setup.py”, line 59, in
CUDA = locate_cuda()
File “setup.py”, line 56, in locate_cuda
raise EnvironmentError(‘The CUDA %s path could not be located in %s’ % (k, v))
EnvironmentError: The CUDA lib64 path could not be located in /usr/lib64
Makefile:2: recipe for target ‘all’ failed
解決方法:開啟 setup.py,把lib64改為libcudaconfig = {‘home’:home, ‘nvcc’:nvcc,
'include': pjoin(home, 'include'), 'lib64': pjoin(home, 'lib')}
-
make error:command ‘/usr/local/bin/nvcc’ failed with exit status 1
新增 export PATH=/usr/local/cuda/bin:”$PATH” 到你的 ~/.bashrc