Faster-RCNN雖然有人覺得它過時了,但它畢竟是個經典,有的項目還在使用,部署環境是服務器的,一般只是把backbone由老舊的ZF,VGG換成了Resnet101之類的,對於需要部署到嵌入式環境進行前端邊緣計算的,ZF之類的小網絡由於識別效果還行而且佔用資源較少,性價比高,也仍是不錯的選擇。把Faster-RCNN琢磨透了,對掌握其他相關RCNN網絡是很有幫助的。
一般的文章都只講了使用VOC 2007格式數據集訓練Faster-RCNN模型時的配置修改,對於COCO格式數據集則沒有涉及到,本文結合本人的實踐經驗,對分別使用這兩種數據集訓練py-faster-rcnn的配置修改都做說明,既是記錄備忘也供分享。
關於py-faster-rcnn的安裝部署,參見我的AI之路(31)--在Jetson Nano上試驗安裝部署py-faster-rcnn一文,雖然部署環境是在jetson nano嵌入式板子上,但是在PC和服務器上部署也差不多,我在本人的PC和公司AI服務器上都安裝部署過,比在jetson nano上順利多了,因爲jetson nano使用的ARM64芯片平臺,一般的PC和服務器使用多是X86-64平臺,X86-64平臺擁有豐富的軟件支持環境源,應該比ARM平臺上更容易安裝部署。
首先是解決下載準備預訓練模型和準備數據集的問題:
py-faster-rcnn/data/scripts/fetch_faster_rcnn_models.sh 是用來下載使用VOC2007數據集訓練好了的模型的
py-faster-rcnn/data/scripts/fetch_imagenet_models.sh是用來下載使用ImageNet數據集訓練好了的模型的
不過,拜託偉大的牆,使用這些腳本都下載不了,因爲dropbox被禁止訪問了:
https://dl.dropboxusercontent.com/s/o6ii098bu51d139/faster_rcnn_models.tgz?dl=0
https://dl.dropbox.com/s/gstw7122padlf0l/imagenet_models.tgz?dl=0
如果不能翻牆,可以到網上搜索其他下載了的人上傳的文件,imagenet訓練出的模型文件網上好找,但是VOC2007訓練出的模型文件難找,我好不容易從網上搜到了個bittrorrent地址,然後用迅雷下載到了 :),我上傳到這裏(imagenet_models : part1,part2,part3,part4,part5 , faster_rcnn_models : part1, part2, part3, part4)供方便下載使用。
把上面的模型文件分別加壓到py-faster-rcnn/data/faster_rcnn_models/下:
-rw-rw-r-- 1 xx xx 548317115 9月 29 16:59 VGG16_faster_rcnn_final.caffemodel
-rw-rw-r-- 1 xx xx 237181903 9月 29 16:59 ZF_faster_rcnn_final.caffemodel
和py-faster-rcnn/data/imagenet_models/下:
-rw-rw-r-- 1 xx xx 553432430 9月 29 16:59 VGG16.v2.caffemodel
-rw-rw-r-- 1 xx xx 349003349 9月 29 16:59 VGG_CNN_M_1024.v2.caffemodel
-rw-rw-r-- 1 xx xx 249432131 9月 29 16:59 ZF.v2.caffemodel
由於官方ImageNet數據集擁有圖片數量遠超VOC2007的,一般採用py-faster-rcnn/data/imagenet_models/下的這些訓練好了的模型作爲訓練自己的模型的起始基礎,也就是pretrained-model,例如,如果你的backbone採用ZF網絡,那麼使用ZF.v2.caffemodel作爲預訓練模型,如果你的backbone採用VGG16網絡,那麼使用VGG16.v2.caffemodel作爲預訓練模型,而訓練py-faster-rcnn的腳本里根據你啓動訓練時執行的命令參數裏指定的網絡是ZF還是VGG16(VGG_CNN_M我就不說了,這個模型大小介於ZF和VGG16之間,至於使用Resnet之類網絡作爲backbone,這裏先不說,後面寫文章說到mmdetection框架時再說),腳本里在訓練啓動時自動使用上面的對應模型文件的全路徑來加載模型作爲pretrained model開始訓練。例如,看在py-faster-rcnn/experiments/scripts/faster_rcnn_end2end.sh裏:
time ./tools/train_net.py --gpu ${GPU_ID} \
--solver models/${PT_DIR}/${NET}/faster_rcnn_end2end/solver.prototxt \
--weights data/imagenet_models/${NET}.v2.caffemodel \
--imdb ${TRAIN_IMDB} \
--iters ${ITERS} \
--cfg experiments/cfgs/faster_rcnn_end2end.yml \
${EXTRA_ARGS}
${NET}就是命令行傳入的NET參數的值,例如ZF或VGG16或VGG_CNN_M_1024
好,解決了預訓練模型的問題,下面解決準備數據集問題:
目前開源免費數據標註工具有不少,我們習慣用LableImage或者Labelme,前者標註圖片保存後生成xml標註文件,後者標註圖片保存後生成json文件,無論哪種文件,要用來做PASCAL VOC格式數據集或者COCO格式數據集都得自己寫腳本處理或轉換一下,另外數據預處理需要進行裁剪縮放或其他特定處理,這些一般都是需要自己寫腳本,處理完畢的標註文件和圖片按照一定路徑存放和安裝一定比例劃分成訓練數據集和測試、驗證數據集。
py-faster-rcnn默認支持VOC 2007格式和COCO2014格式數據集,所以你的處理腳本需要把數據處理後生成VOC2007格式或者COCO2014格式。VOC2007下有Annotations、JPEGImages、ImageSets/Main 三個目錄(路徑),分別用來存放標註文件、圖片文件、訓練和測試、驗證數據集的文件列表。COCO2014下有annotations、images/train2014、images/val2014三個目錄(路徑),annotations下存放用於訓練的標註文件instances_train2014.json和用於驗證測試的標註文件instances_minival2014.json,images/train2014下存放用於訓練的圖片文件COCO_train2014_[0-9]{12}.jpg、images/val2014下存放用於驗證測試的圖片文件COCO_val2014_[0-9]{12}.jpg,[0-9]{12}表示12位數字,一般前面都是0,後面都是文件序號。
關於如何處理自己的數據和生成VOC2007或COCO2014格式數據集,這是個較大的話題,後面有空再寫單獨的文章再說。如果你只是用py-faster-rcnn跑起來玩玩,不是想要訓練出自己特定用途的模型,那可以到相關官網網站上下載對應的數據集來跑即可,當然官網一般在國外,下載相當的慢,最好找個國內的下載地址,比如這裏可以下載VOC2007數據集 https://pan.baidu.com/s/1mhMKKw4,會省很多時間。
準備好了自己的數據集後把數據集放到py-faster-rcnn下對應的位置,對於VOC2007格式的數據集,存放的位置應該是
py-faster-rcnn/data/VOCdevkit2007/VOC2007
對於COCO格式數據,存放的位置是:
py-faster-rcnn/data/coco
當然,你也可以把數據集存放在別的位置,然後建個鏈接鏈到上面這些默認路徑位置。
準備好了數據集,下面就得修改py-faster-rcnn的腳本了,爲什麼要改腳本呢?三個原因:
一是py-faster-rcnn在處理圖片像素數據的座標時總是以(1,1)爲原點,而不是通常的(0,0),如果label標註的頂點的座標x<1或者y<1(x=0或y=0,甚至出現負數,lableme就有這個Bug,不小心整體拖動了label框到邊上越界了,labelme不報錯並且能保存這種錯誤的負數座標!!!)都會導致訓練啓動後在讀取數據時出assertion錯誤而停止。
二是解決numpy版本不同引起的錯誤 TypeError: 'numpy.float64' object cannot be interpreted as an index.
三是py-faster-rcnn的腳本里參數配置是針對PASCAL VOC默認的20種分類(class)或者COCO默認的80種分類來設置的,而你自己需要用來做訓練模型的數據的類別並沒有那麼多,一般就一種或幾種,所以需要針對你的類別數調整相關的網絡參數設置。
解決上述問題需修改下列代碼:
(1)py-faster-rcnn/lib/datasets/imdb.py:
def append_flipped_images(self):
num_images = self.num_images
widths = self._get_widths()
for i in xrange(num_images):
boxes = self.roidb[i]['boxes'].copy()
oldx1 = boxes[:, 0].copy()
oldx2 = boxes[:, 2].copy()
boxes[:, 0] = widths[i] - oldx2 # - 1
boxes[:, 2] = widths[i] - oldx1 # - 1
(2)py-faster-rcnn/lib/datasets/pascal_voc.py:
class pascal_voc(imdb):
def __init__(self, image_set, year, devkit_path=None):
imdb.__init__(self, 'voc_' + year + '_' + image_set)
self._year = year
self._image_set = image_set
self._devkit_path = self._get_default_path() if devkit_path is None \
else devkit_path
self._data_path = os.path.join(self._devkit_path, 'VOC' + self._year)
#把默認的20個class註釋掉,增加你自己的class:
self._classes = ('__background__', # always index 0
#'aeroplane', 'bicycle', 'bird', 'boat',
#'bottle', 'bus', 'car', 'cat', 'chair',
#'cow', 'diningtable', 'dog', 'horse',
#'motorbike', 'person', 'pottedplant',
#'sheep', 'sofa', 'train', 'tvmonitor'
'your_class')
...
def _load_pascal_annotation(self, index):
...
# Load object bounding boxes into a data frame.
for ix, obj in enumerate(objs):
bbox = obj.find('bndbox')
# Make pixel indexes 0-based
x1 = float(bbox.find('xmin').text) #- 1
y1 = float(bbox.find('ymin').text) #- 1
x2 = float(bbox.find('xmax').text) #- 1
y2 = float(bbox.find('ymax').text) #- 1
cls = self._class_to_ind[obj.find('name').text.lower().strip()]
(3)py-faster-rcnn/lib/roi_data_layer/minibatch.py:
def get_minibatch(roidb, num_classes):
"""Given a roidb, construct a minibatch sampled from it."""
num_images = len(roidb)
# Sample random scales to use for each image in this batch
random_scale_inds = npr.randint(0, high=len(cfg.TRAIN.SCALES),
size=num_images)
assert(cfg.TRAIN.BATCH_SIZE % num_images == 0), \
'num_images ({}) must divide BATCH_SIZE ({})'. \
format(num_images, cfg.TRAIN.BATCH_SIZE)
rois_per_image = cfg.TRAIN.BATCH_SIZE / num_images
fg_rois_per_image = int(np.round(cfg.TRAIN.FG_FRACTION * rois_per_image))
def _get_bbox_regression_labels(bbox_target_data, num_classes):
...
for ind in inds:
cls = clss[ind]
start = int(4 * cls)
end = start + 4
bbox_targets[ind, start:end] = bbox_target_data[ind, 1:]
bbox_inside_weights[ind, start:end] = cfg.TRAIN.BBOX_INSIDE_WEIGHTS
return bbox_targets, bbox_inside_weights
(4)py-faster-rcnn/lib/rpn/proposal_target_layer.py:
def forward(self, bottom, top):
...
num_images = 1
rois_per_image = cfg.TRAIN.BATCH_SIZE / num_images
fg_rois_per_image = int(np.round(cfg.TRAIN.FG_FRACTION * rois_per_image))
def _get_bbox_regression_labels(bbox_target_data, num_classes):
...
for ind in inds:
cls = clss[ind]
start =int( 4 * cls)
end = start + 4
bbox_targets[ind, start:end] = bbox_target_data[ind, 1:]
bbox_inside_weights[ind, start:end] = cfg.TRAIN.BBOX_INSIDE_WEIGHTS
return bbox_targets, bbox_inside_weights
-------------------------------------------------------------------------------------------------------------------------------------------------------------------
這裏需要說明的是,如果imdb.py和pascal_voc.py做了上面的修改,啓動訓練後運行到加載數據時,如果在imdb.py的append_flipped_images()裏面仍舊在 assert (boxes[:, 2] >= boxes[:, 0]).all() 出發生assertion錯誤的話,那說明你的標註數據裏肯定有標註點越界超出範圍了(x或y的值爲負數或者值大於圖片本身的width或者height),這是由於標註工具有bug導致這種非法數據也能保存,可以修改代碼把它們忽略掉或者像有的文章建議把出錯越界的x數據強制設置爲0,但我更願意把它們一一找出來在標註工具裏手工修正以徹底解決問題,那怎麼把這些數據找出來呢,可以在imdb.py的def append_flipped_images()裏在 assert (boxes[:, 2] >= boxes[:, 0]).all() 的前面增加打印,把非法的xmin,xmax數據以及對應的flip之前的原始xmin,xmax數據打出來:
def append_flipped_images(self):
...
for i in xrange(num_images):
boxes = self.roidb[i]['boxes'].copy()
oldx1 = boxes[:, 0].copy()
oldx2 = boxes[:, 2].copy()
boxes[:, 0] = widths[i] - oldx2 # - 1
boxes[:, 2] = widths[i] - oldx1 # - 1
for b in range(len(boxes)):
if boxes[b][2] < boxes[b][0]:
print("==invalid data== i=",str(i),",widths[i]=",widths[i],"b=",str(b),"oldx1[b]=",str(oldx1[b]),"oldx2[b]=",str(oldx2[b]), "xmin=",str(boxes[b][0]),",xmax=",str(boxes[b][2]),",ymin=",str(boxes[b][1]),",ymax=",str(boxes[b][3]))
assert (boxes[:, 2] >= boxes[:, 0]).all()
知道了原始的xmin,xmax錯誤數據,然後在pascal_voc.py裏的_load_pascal_annotation()增加對對應數據的打印即可找到他們所在的標註文件和原始的xmin,xmax等座標數據:
def _load_pascal_annotation(self, index):
...
boxes = np.zeros((num_objs, 4), dtype=np.uint16)
gt_classes = np.zeros((num_objs), dtype=np.int32)
overlaps = np.zeros((num_objs, self.num_classes), dtype=np.float32)
# "Seg" area for pascal is just the box area
seg_areas = np.zeros((num_objs), dtype=np.float32)
# Load object bounding boxes into a data frame.
for ix, obj in enumerate(objs):
bbox = obj.find('bndbox')
# Make pixel indexes 0-based
x1 = float(bbox.find('xmin').text) #- 1
y1 = float(bbox.find('ymin').text) #- 1
x2 = float(bbox.find('xmax').text) #- 1
y2 = float(bbox.find('ymax').text) #- 1
cls = self._class_to_ind[obj.find('name').text.lower().strip()]
boxes[ix, :] = [x1, y1, x2, y2] #這裏x1,y1,x2,y2被自動由float類型轉換成了uint16類型
#假設前面imdb.py的append_flipped_images()裏打印出的非法數據oldx1[b],oldx2[b]分別是339,462
if boxes[ix,2]==462 and boxes[ix,0]==339 or boxes[ix,2] < boxes[ix,0] or boxes[ix,2]-1 < boxes[ix,0] -1 :
print("==invalid data== boxes[ix,2]=",str(boxes[ix,2]),",boxes[ix,0]=",str(boxes[ix,0]))
print("xmin=",bbox.find('xmin').text,",xmax=",bbox.find('xmax').text)
print("index=",index,",x1=",str(x1),",x2=",str(x2),"x1-1=",str(x1-1),"x2-1=",str(x2-1))
assert(False)
...
index就是非法標註數據所在的標註文件的名字,這樣,當加載解析到這個標註文件時,會出assertion錯誤,到這個文件裏找到錯誤的x1,x2數據手工修改即可,最好是用標註工具打開這個標註文件拖動label框達到修正的目的,然後保存即可。
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
對於解決class個數的問題,無論是使用COCO數據集還是PASCAL VOC數據集訓練,無論是使用two-stage迭代訓練還是end2end的訓練方式,要修改的網絡參數都是input-data層的num_classes,cls_score和bbox_pred層的num_ouput,需對用於訓練或測試的prototxt文件做類似如下修改(假設只有一個class,以數據集爲PASCAL VOC2007格式、backbone爲ZF、使用迭代訓練和數據集爲COCO格式、backbone爲VGG16、使用端到端訓練兩種組合方式爲例):
1. 數據集爲PASCAL VOC2007格式、backbone爲ZF、使用迭代訓練
(1)py-faster-rcnn/models/pascal_voc/ZF/faster_rcnn_alt_opt/stage1_rpn_train.pt
name: "ZF"
layer {
name: 'input-data'
type: 'Python'
top: 'data'
top: 'im_info'
top: 'gt_boxes'
python_param {
module: 'roi_data_layer.layer'
layer: 'RoIDataLayer'
param_str: "'num_classes': 2" #num_classes的值爲class個數加1(background)
}
}
(2)py-faster-rcnn/models/pascal_voc/ZF/faster_rcnn_alt_opt/stage1_fast_rcnn_train.pt
name: "ZF"
layer {
name: 'data'
type: 'Python'
top: 'data'
top: 'rois'
top: 'labels'
top: 'bbox_targets'
top: 'bbox_inside_weights'
top: 'bbox_outside_weights'
python_param {
module: 'roi_data_layer.layer'
layer: 'RoIDataLayer'
param_str: "'num_classes': 2" #num_classes的值爲class個數加1(background)
}
}
layer {
name: "cls_score"
type: "InnerProduct"
bottom: "fc7"
top: "cls_score"
param { lr_mult: 1.0 }
param { lr_mult: 2.0 }
inner_product_param {
num_output: 2 #cls_core的num_output值與input_data layer的num_classes相等
weight_filler {
type: "gaussian"
std: 0.01
}
bias_filler {
type: "constant"
value: 0
}
}
}
layer {
name: "bbox_pred"
type: "InnerProduct"
bottom: "fc7"
top: "bbox_pred"
param { lr_mult: 1.0 }
param { lr_mult: 2.0 }
inner_product_param {
num_output: 8 # bbox_pred的num_output的值爲input_data layer的num_classes的值的4倍(顯然是因爲box有四個值嘛)
weight_filler {
type: "gaussian"
std: 0.001
}
bias_filler {
type: "constant"
value: 0
}
}
}
(3)py-faster-rcnn/models/pascal_voc/ZF/faster_rcnn_alt_opt/stage2_rpn_train.pt
name: "ZF"
layer {
name: 'input-data'
type: 'Python'
top: 'data'
top: 'im_info'
top: 'gt_boxes'
python_param {
module: 'roi_data_layer.layer'
layer: 'RoIDataLayer'
param_str: "'num_classes': 2"
}
}
(4)py-faster-rcnn/models/pascal_voc/ZF/faster_rcnn_alt_opt/stage2_fast_rcnn_train.pt
name: "ZF"
layer {
name: 'data'
type: 'Python'
top: 'data'
top: 'rois'
top: 'labels'
top: 'bbox_targets'
top: 'bbox_inside_weights'
top: 'bbox_outside_weights'
python_param {
module: 'roi_data_layer.layer'
layer: 'RoIDataLayer'
param_str: "'num_classes': 2"
}
}
layer {
name: "cls_score"
type: "InnerProduct"
bottom: "fc7"
top: "cls_score"
param { lr_mult: 1.0 }
param { lr_mult: 2.0 }
inner_product_param {
num_output: 2
weight_filler {
type: "gaussian"
std: 0.01
}
bias_filler {
type: "constant"
value: 0
}
}
}
layer {
name: "bbox_pred"
type: "InnerProduct"
bottom: "fc7"
top: "bbox_pred"
param { lr_mult: 1.0 }
param { lr_mult: 2.0 }
inner_product_param {
num_output: 8
weight_filler {
type: "gaussian"
std: 0.001
}
bias_filler {
type: "constant"
value: 0
}
}
}
(5)py-faster-rcnn/models/pascal_voc/ZF/faster_rcnn_alt_opt/faster_rcnn_test.pt
layer {
name: "cls_score"
type: "InnerProduct"
bottom: "fc7"
top: "cls_score"
inner_product_param {
num_output: 2
}
}
layer {
name: "bbox_pred"
type: "InnerProduct"
bottom: "fc7"
top: "bbox_pred"
inner_product_param {
num_output: 8
}
}
2.數據集爲COCO格式、backbone爲VGG16、使用端到端訓練:
(1)py-faster-rcnn/models/coco/VGG16/faster_rcnn_end2end/train.prototxt
layer {
name: 'input-data'
type: 'Python'
top: 'data'
top: 'im_info'
top: 'gt_boxes'
python_param {
module: 'roi_data_layer.layer'
layer: 'RoIDataLayer'
param_str: "'num_classes': 2" #num_classes的值爲class個數加1(background)
}
}
layer {
name: "cls_score"
type: "InnerProduct"
bottom: "fc7"
top: "cls_score"
param {
lr_mult: 1
}
param {
lr_mult: 2
}
inner_product_param {
num_output: 2 #cls_core的num_output值與input_data layer的num_classes相等
weight_filler {
type: "gaussian"
std: 0.01
}
bias_filler {
type: "constant"
value: 0
}
}
}
layer {
name: "bbox_pred"
type: "InnerProduct"
bottom: "fc7"
top: "bbox_pred"
param {
lr_mult: 1
}
param {
lr_mult: 2
}
inner_product_param {
num_output: 8 # bbox_pred的num_output的值爲input_data layer的num_classes的值的4倍(顯然是因爲box有四個值嘛)
weight_filler {
type: "gaussian"
std: 0.001
}
bias_filler {
type: "constant"
value: 0
}
}
}
(2)py-faster-rcnn/models/coco/VGG16/faster_rcnn_end2end/test.prototxt
layer {
name: "cls_score"
type: "InnerProduct"
bottom: "fc7"
top: "cls_score"
param {
lr_mult: 1
decay_mult: 1
}
param {
lr_mult: 2
decay_mult: 0
}
inner_product_param {
num_output: 2
weight_filler {
type: "gaussian"
std: 0.01
}
bias_filler {
type: "constant"
value: 0
}
}
}
layer {
name: "bbox_pred"
type: "InnerProduct"
bottom: "fc7"
top: "bbox_pred"
param {
lr_mult: 1
decay_mult: 1
}
param {
lr_mult: 2
decay_mult: 0
}
inner_product_param {
num_output: 8
weight_filler {
type: "gaussian"
std: 0.001
}
bias_filler {
type: "constant"
value: 0
}
}
}
以上是必須修改的網絡參數文件,lr、gamma、momentum等超參數和stepsize等參數在相應路徑下名字裏含有solver的相關.prototxt(或.pt)文件裏,可根據需要做修改。
OK,準備完畢,開始訓練,對於第一種組合,在py-faster-rcnn下執行下面命令啓動(默認指定0號GPU,可以指定其它GPU):
./experiments/scripts/faster_rcnn_alt_opt.sh 0 ZF pascal_voc
對於第二種組合,在在py-faster-rcnn下執行下面命令啓動(默認指定0號GPU,可以指定其它GPU):
./experiments/scripts/faster_rcnn_end2end.sh 0 VGG16 coco
對於其它數據集格式+backbone+alt/end2end 組合,仿照上面修改配置後執行對應的命令即可:
./experiments/scripts/<faster_rcnn_alt_opt.sh | faster_rcnn_end2end.sh> <GPU> <NET> <pascal_voc | coco>