【技术综述】万字长文详解Faster RCNN源代码
文章首發于微信公眾號《有三AI》
【技術綜述】萬字長文詳解Faster RCNN源代碼
作為深度學習算法工程師,如果你想提升C++水平,就去研究caffe源代碼,如果你想提升python水平,就去研究faster-rcnn源代碼吧,caffe源代碼我們已經解讀過了,今天這一期就解讀faster-rcnn源代碼
?
01?Faster R-CNN概述
1.1 基礎
目標檢測任務關注的是圖片中特定目標物體的位置。一個檢測任務包含兩個子任務,其一是輸出這一目標的類別信息,屬于分類任務。其二是輸出目標的具體位置信息,屬于定位任務。
分類的結果是一個類別標簽,對于單分類任務而言,它就是一個數,對于多分類任務,就是一個向量。定位任務的輸出是一個位置,用矩形框表示,包含矩形框左上角或中間位置的x,y坐標和矩形框的寬度高度。
目標檢測在生活中具有非常廣泛的應用,它也經過了非常長的發展階段。與計算機視覺領域里大部分的算法一樣,也經歷了從傳統的人工設計特征和淺層分類器的思路,到大數據時代使用深度神經網絡進行特征學習的思路這一過程。
相信大家已經看過很多目標檢測的原理綜述性文章,如果沒有,就參考本公眾號今天發的另一篇文章《一文道盡R-CNN系列目標檢測》。
本文包括兩個部分:
對經典算法Faster R-CNN的源代碼進行詳細的說明,選用的代碼為caffe版本, 鏈接為:https://github.com/rbgirshick/py-faster-rcnn。
基于該框架完成一個簡單的實踐。在正式解讀代碼之前,要先說清楚兩個重要概念,rpn與roi?pooling。
1.2?roi pooing
通常我們訓練一次取多張圖,也就是一個batch,一個batch中圖大小一致,這是為了從源頭上把控,從而獲得固定維度的特征。所以在早期進行目標檢測,會將候選的區域進行裁剪或縮放到統一尺度,如下圖紅色框。
副作用很明顯,一次只選到了目標一部分,或者把目標變形了。
sppnet不從源頭而是在最后一環的特征上做處理,將任意尺度(大于4*4)大小的特征,進行3種pooling,串接起來得到固定的21維,從而避免了固定尺寸輸入的約束,如下。
當然你用的時候,不必要局限于4*4,2*2,1*1。
以前為了滿足全連接層固定大小輸入,需要在輸入圖進行縮放,然后提取特征。現在既然已經可以在特征空間進行縮放了,那么就不必要求輸入一樣大了。
原來的那些只是因為在原始空間中有了輕微的位置或者尺寸改變就要重新提取特征的大量重復操作,就不需要了,因為任意的原始圖像中的輸入是可以映射到特征圖中的,卷積只會改變空間分辨率,不改變比例和位置。
映射很簡單,等比例縮放即可,實現時考慮好padding,stride等操作。
那這跟roi pooling有什么關系呢?
ROI pooling是一個簡化的spp池化,不需要這么復雜,直接一次分塊pooling就行了,在經過了從原始空間到特征空間的映射之后,設定好一個pooled_w,一個pooled_h,就是將W*H的輸入pooling為pooled_w*pooled_h的特征圖,然后放入全連接層。 它完成的下面的輸入輸出的變換。
1.2 RPN
region proposal network,可知道這就是一個網絡,而且是一個小小的網絡。它解決了roi pooling的輸入問題,就是如何得到一系列的proposal,也就是原圖中的候選框。
圖比較難畫就借用原文了,如上,而rpn網絡的功能示意圖如下。
最開始當我們想要去提取一些區域來做分類和回歸時,那就窮舉嘛,搞不同大小,不同比例的框,在圖像中從左到右,從上到下滑動,如上圖,一個就選了9種。這樣計算量很大,所以selective search利用語義信息改進,避免了滑動,但還是需要在原圖中操作,因為特征提取不能共享。
現在不是有了sppnet和roi pooling的框架,把上面的這個在原圖中進行窮舉滑動的操作,換到了比原圖小很多的特征空間(比如224*224 --> 13*13),還是搞滑動窗口,就得到了rpn,如下圖。
rpn實現了了上面的輸入輸出。不同與簡單滑窗的是,網絡會對這個過程進行學習,得到更有效的框。
剩下的就是一些普通cnn的知識,不用多說,有了上面的這些基礎后,我們開始解讀代碼。
?
02?py-faster-rcnn框架解讀
Faster R-CNN源代碼的熟悉幾乎是所有從事目標檢測的人員必須邁過的坎,由于目錄結構比較復雜而且廣泛為人所用,涉及的東西非常多,所以我們使用的時候維持該目錄結構不變,下面首先對它的目錄結構進行完整的分析。
目錄下包括caffe-fast-rcnn,data,experiments,lib,models,tools幾大模塊。
2.1 caffe-fast-rcnn
這是rcnn系列框架的caffe,因為目標檢測中使用到了很多官方caffe中不包括的網絡層,所以必須進行定制。這里需要注意的是caffe是以子模塊的方式被包含在其中,所以使用git clone命令下載代碼將得到空文件夾,必須要加上遞歸參數-recursive,具體做法如下:
git clone --recursive
https://github.com/rbgirshick/py-faster-rcnn.git
2.2 data
該文件夾下包含兩個子文件夾,一個是scripts,一個是demo。其中demo就是用于存放測試的圖像,script存儲著若干腳本,它用于獲取一些預訓練的模型。
比如運行fetch_imagenet_models.sh腳本,會在當前的文件夾下建立imagenet_models目錄,并下載VGG_CNN_M_1024.v2.caffemodel VGG16.v2.caffemodel ZF.v2.caffemodel模型,這些是在ImageNet上預先訓練好的模型,將用于初始化我們的檢測模型的訓練。
另外還在該目錄下建立數據集的軟鏈接。通常情況下,對于一些通用的數據集,我們會將它們放在公用的目錄而不是某一個項目下,所以這里通常需要建立通用數據集的軟鏈接,比如PASCAL VOC的目錄。
ln -s /your/path/to/VOC2012/VOCdevkit
VOCdevkit2012
2.3?experiments
下面分為3個目錄,分別是log,cfgs,scripts。cfgs存放的就是配置文件,比如faster_rcnn_end2end的配置如下:
EXP_DIR: faster_rcnn_end2end
TRAIN:
? HAS_RPN: True
? IMS_PER_BATCH: 1
? BBOX_NORMALIZE_TARGETS_PRECOMPUTED: True
? RPN_POSITIVE_OVERLAP: 0.7
? RPN_BATCHSIZE: 256
? PROPOSAL_METHOD: gt
? BG_THRESH_LO: 0.0
TEST:
? HAS_RPN: True
其中比較重要的包括HAS_RPN,RPN_BATCHSIZE等。Log目錄就存放log日志。scripts存放的就是bash訓練腳本,可以用end2end或者alt_opt兩種方式訓練。
每一個訓練腳本都包含兩個步驟,訓練和測試。
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}
如上就是訓練部分代碼,對于默認的任務,我們不需要修改這里的代碼,但是如果我們不想用預訓練,或者相關的配置發生了變化,比如yml格式的配置文件,預訓練模型的前綴格式,需要配置自定義的數據集等等,則需要修改此處代碼。
2.4 Models目錄
該目錄下包含兩個文件夾,coco和pascal voc,可知是兩個通用數據集。在各個數據集的子目錄下存儲了一系列模型結構的配置,如models/pascal/VGG_CNN_M_1024/目錄,存儲的就是用于訓練coco數據集的VGG模型。
在該目錄下,有fast_rcnn,faster_rcnn_alt_opt,faster_rcnn_end_to_end三套模型結構,各自有所不同。
fast_rcnn即fast_rcnn方法,它下面只包含了train.prototxt,test.prototxt,solver.prototxt三個文件,它對rcnn的改進主要在于重用了卷積特征,沒有region proposal框架。
faster_rcnn_alt_opt,faster_rcnn_end_to_end都是faster rcnn框架,包括了region proposal模塊。在faster_rcnn_alt_opt目錄下,包含了4個訓練文件和對應的solver文件,為:
stage1_fast_rcnn_solver30k40k.pt,stage1_fast_rcnn_train.pt,stage1_rpn_solver60k80k.pt,stage1_rpn_train.pt,stage2_fast_rcnn_solver30k40k.pt,stage2_fast_rcnn_train.pt,
stage2_rpn_solver60k80k.pt,stage2_rpn_train.pt。
其中stage1過程是分別采用了ImageNet分類任務上訓練好的模型進行region proposal的學習和faster rcnn檢測的學習,stage2則是在stage1已經訓練好的模型的基礎上進行進一步的學習。
faster_rcnn_end_to_end就是端到端的訓練方法,使用起來更加簡單,所以我們這一小節會使用faster_rcnn_end_to_end方法,目錄下只包括了train.prototxt,test.prototxt,solver.prototxt。
當要在我們自己的數據集上完成檢測任務的時候,就可以建立與pascal_voc和coco平級的目錄。
2.5 Lib目錄
lib目錄下包含了非常多的子目錄,包括datasets,fast_rcnn,nms,pycocotools,roi_data_layer,rpn,transform,utils,這是faster rcnn框架中很多方法的實現目錄,下面對其進行詳細解讀。
(1) utils目錄
這是最基礎的一個目錄,主要就是blob.py和bbox.pyx。blob.py用于將圖像進行預處理,包括減去均值,縮放等操作,然后封裝到caffe的blob中。
??? for i in xrange(num_images):
??????? im = ims[i]
??????? blob[i, 0:im.shape[0], 0:im.shape[1], :] = im
??? channel_swap = (0, 3, 1, 2)
??? blob = blob.transpose(channel_swap)
封裝的核心代碼如上,首先按照圖像的存儲格式(H,W,C)進行賦值,然后調整通道和高,寬的順序,這在我們使用訓練好的模型進行預測的時候是必要的操作。而且,有的時候采用RGB格式進行訓練,有的使用采用BGR格式進行訓練,也需要做對應的調整。
bbox.pyx用于計算兩個box集合的overlaps,即重疊度。一個輸入是(N,4)形狀的真值boxes,一個輸入是(K,4)形狀的查詢boxes,輸出為(N,K)形狀,即逐個box相互匹配的結果。
(2) datasets目錄
datasets目錄下有目錄VOCdevkit-matlab-wrapper,tools,以及腳本coco.py,pascal_voc.py,voc_eval.py,factory.py,ds_util.py,imdb.py。
我們按照調用關系來看,首先是ds_util.py,它包含了一些最基礎的函數,比如unique_boxes函數,可以通過不同尺度因子的縮放,從一系列的框中獲取不重復框的數組指標,用于過濾重復框。
具體實現采用了hash的方法,代碼如下:
v = np.array([1, 1e3, 1e6, 1e9])
hashes = np.round(boxes * scale).dot(v).astype(np.int)
box的4個坐標,與上面的v進行外積后轉換為一個數,再進行判斷。
xywh_to_xyxy,xyxy_to_xywh函數分別是框的兩種表達形式的相互轉換,前者采用一個頂點坐標和長寬表示矩形,后者采用兩個頂點坐標表示矩形,各有用途。
validata_boxes函數用于去除無效框,即超出邊界或者左右,上下,不滿足幾何關系的點,比如右邊頂點的x坐標小于左邊頂點。filer_small_boxes用于去掉過小的檢測框。
接下來看imdb.py,這是數據集類imdb的定義腳本,非常重要。從它的初始化函數_init_可以看出,類成員變量包括數據集的名字self._name,檢測的類別名字self._classes與具體的數量self._num_classes,候選區域的選取方法self._obj_proposer,roi數據集self._roidb與它的指針self._roidb_handler,候選框提取默認采用了selective_search方法。
roidb是它最重要的數據結構,它是一個數組。數組中的每一個元素其實就是一張圖的屬性,以字典的形式存儲它的若干屬性,共4個key,為boxes,gt_overlaps,gt_classes,flipped。
候選框boxes就是一個圖像中的若干的box,每一個box是一個4維的向量,包含左上角和右下角的坐標。類別信息gt_classes,就是對應boxes中各個box的類別信息。真值gt_overlaps,它的維度大小為boxes的個數乘以類別的數量,可知存儲的就是輸入box和真實標注之間的重疊度。另外如果設置了變量flipped,還可以存儲該圖像的翻轉版本,這就是一個鏡像操作,是最常用的數據增強操作。
roidb的生成調用了create_roidb_from_box_list函數,它將輸入的box_list添加到roidb中。如果沒有gt_roidb的輸入,那么就是下面的邏輯,可見就是將boxes存入數據庫中,并初始化gt_overlaps,gt_classes等變量。
boxes = box_list[i]
num_boxes = boxes.shape[0]
overlaps = np.zeros((num_boxes, self.num_classes), dtype=np.float32)
overlaps = scipy.sparse.csr_matrix(overlaps)
roidb.append({
??????????????? 'boxes' : boxes,
??????????????? 'gt_classes' : np.zeros((num_boxes,), dtype=np.int32),
??????????????? 'gt_overlaps' : overlaps,
??????????????? 'flipped' : False,
??????????????? 'seg_areas' : np.zeros((num_boxes,), dtype=np.float32),
??????????? })
如果輸入gt_roidb非空,則需要將輸入的box與其進行比對計算得到gt_overlaps,代碼如下:
if gt_roidb is not None and gt_roidb[i]['boxes'].size > 0:
gt_boxes = gt_roidb[i]['boxes']
??? gt_classes = gt_roidb[i]['gt_classes']
??? gt_overlaps = bbox_overlaps(boxes.astype(np.float),gt_boxes.astype(np.float))
??? argmaxes = gt_overlaps.argmax(axis=1)
??? maxes = gt_overlaps.max(axis=1)
I = np.where(maxes > 0)[0]
overlaps[I, gt_classes[argmaxes[I]]] = maxes[I]
將輸入的boxes與數據庫中boxes進行比對,調用了bbox_overlaps函數。比對完之后結果存入 overlaps。
bbox_overlaps的結果overlaps是一個二維矩陣,第一維大小等于輸入boxes中的框的數量,第二維就是類別數目,所存儲的每一個值就是與真實標注進行最佳匹配的結果,即重疊度。但是最后存儲的時候調用了overlaps = scipy.sparse.csr_matrix(overlaps)進行稀疏壓縮,因為其中大部分的值其實是空的,一張圖包含的類別數目有限。
還有一個變量gt_classes,在從該函數創建的時候并未賦值,即等于0,因為這個函數是用于將從rpn框架中返回的框添加到數據庫中,并非是真實的標注。當gt_classes非零,說明是真實的標注,這樣的數據集就是train或者val數據集,它們在一開始就被創建,反之則是test數據集。gt_classes非零的樣本和為零的樣本在數據集中是連續存儲的。
該腳本中另一個重要的函數就是evaluate_recall,這就是用于計算average iou的函數。它的輸入包括candidate_boxes,即候選框。假如沒有輸入,則評估時取該roidb中的非真值box。threholds,即IoU閾值,如果沒有輸入則默認從0.5到0.95,按照0.05的步長迭代。area,用于評估的面積大小閾值,默認覆蓋0到1e10的尺度,尺度是指框的面積。還有一個limit,用于限制評估的框的數量。
返回平均召回率average recall,每一個IoU重合度閾值下的召回向量,設定的IoU閾值向量,以及所有的真值標簽。
當進行評估的時候,首先要按照上面設計的面積大小閾值,得到有效的index。
max_gt_overlaps = self.roidb[i]['gt_overlaps'].toarray().max(axis=1)
gt_inds = np.where((self.roidb[i]['gt_classes'] > 0) &
????? (max_gt_overlaps == 1))[0]?? 首先獲得需要評估的index
gt_boxes = self.roidb[i]['boxes'][gt_inds, :]? 得到對應的boxes
gt_areas = self.roidb[i]['seg_areas'][gt_inds]
valid_gt_inds = np.where((gt_areas >= area_range[0]) &
?????? (gt_areas <= area_range[1]))[0]? 得到符合面積約束的index
gt_boxes = gt_boxes[valid_gt_inds, :]
num_pos += len(valid_gt_inds)? 記錄符合條件的框的個數
計算重疊度的過程是對每一個真值box進行遍歷,尋找到與其重疊度最大的候選框,得到各個真值box的被重疊度。挑選其中被重疊度最高的真值box,然后找到對應的與其重疊度最高的box,得到了一組匹配和相應的重疊度。標記這兩個box,后續的迭代不再使用,然后循環計算,直到所有的真值框被遍歷完畢。
pascal_voc.py和coco.py就是利用上面的幾個腳本來創建對應這兩個數據集的格式,用于后續對模型的測試,下面就是pascal voc的數據庫的創建過程。
def _load_pascal_annotation(self, index):
??????? """
??????? Load image and bounding boxes info from XML file in the PASCAL VOC
??????? format.
??????? """
??????? filename = os.path.join(self._data_path, 'Annotations', index + '.xml')
??????? tree = ET.parse(filename)
??????? objs = tree.findall('object')
??????? if not self.config['use_diff']:
??????????? non_diff_objs = [
??????????????? obj for obj in objs if int(obj.find('difficult').text) == 0]
??????????? objs = non_diff_objs
??????? num_objs = len(objs)
??????? 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]
??????????? gt_classes[ix] = cls
??????????? overlaps[ix, cls] = 1.0
??????????? seg_areas[ix] = (x2 - x1 + 1) * (y2 - y1 + 1)
?
??????? overlaps = scipy.sparse.csr_matrix(overlaps)
?
??????? return {'boxes' : boxes,
??????????????? 'gt_classes': gt_classes,
??????????????? 'gt_overlaps' : overlaps,
??????????????? 'flipped' : False,
??????????????? 'seg_areas' : seg_areas}
從上面腳本可知,輸入就是xml格式的標注文件,通過obj變量獲得x1,y1,x2,y2,即標注信息,以及cls類別信息,并標注overlaps等于1。另外seg_areas實際上就是標注框的面積。
(3) nms目錄
該目錄下主要是cpu和gpu版本的非極大值抑制計算方法,非極大抑制算法在目標檢測中應用相當廣泛,其主要目的是消除多余的框,找到最佳的物體檢測位置。
實現的核心思想是首先將各個框的置信度進行排序,然后選擇其中置信度最高的框A,將其作為標準,同時設置一個閾值。然后開始遍歷其他框,當其他框B與A的重合程度超過閾值就將B舍棄掉,然后在剩余的框中選擇置信度最大的框,重復上述操作。
我們以py_cpu_nms.py為例,并添加了注釋。
import numpy as np
def py_cpu_nms(dets, thresh):
??? """Pure Python NMS baseline."""
??? x1 = dets[:, 0]
??? y1 = dets[:, 1]
??? x2 = dets[:, 2]
??? y2 = dets[:, 3]
??? scores = dets[:, 4]??
??? areas = (x2 - x1 + 1) * (y2 - y1 + 1)? #此處用于計算每一個框的面積
??? order = scores.argsort()[::-1]??? #按照分數大小對其進行從高到低排序
?
??? keep = []
??? while order.size > 0:
??????? i = order[0]???? #取分數最高的那個框
??????? keep.append(i)? #保留這個框
??????? xx1 = np.maximum(x1[i], x1[order[1:]])
??????? yy1 = np.maximum(y1[i], y1[order[1:]])
??????? xx2 = np.minimum(x2[i], x2[order[1:]])
??????? yy2 = np.minimum(y2[i], y2[order[1:]])??????? #計算當前分數最大矩形框與其他矩形框的相交后的坐標
?
??????? w = np.maximum(0.0, xx2 - xx1 + 1)
??????? h = np.maximum(0.0, yy2 - yy1 + 1)?????
??????? inter = w * h????? #計算相交框的面積
??????? ovr = inter / (areas[i] + areas[order[1:]] - inter)? #計算IOU:重疊面積/(面積1+面積2-重疊面積)
?
??????? inds = np.where(ovr <= thresh)[0]?? #取出IOU小于閾值的框
??????? order = order[inds + 1]?? ??#更新排序序列
?
??? return keep
(4) roi data layer目錄
該目錄下有3個腳本,layer.py,minibatch.py,roidb.py。
layer.py包含了caffe的RoIDataLayer網絡層的實現。通常來說一個caffe網絡層的實現,需要包括setup,forward,backward等函數的實現,對于數據層還需實現shuffle,批量獲取數據等函數。
Roidatalayer是一個數據層,也是訓練時的輸入層,其中最重要的函數是setup函數,用于設置各類輸出數據的尺度信息。
根據是否有RPN模塊,這兩種情況下的配置是不一樣的,我們直接看caffe的網絡配置就能明白,比較fast rcnn和faster rcnn。
首先是fast rcnn:
name: "VGG_CNN_M_1024"
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': 21"
? }
}
可以看到,它的top輸出為rois,labels,bbox_targets, bbox_inside_weights, bbox_outside_weights總共5個屬性。
rois是selective search方法提取出的候選區域,尺度為(1,5),按照(index,x1,y1,x2,y2)的格式來存儲。labels和bbox_targets是區域的分類和回歸標簽,bbox_inside_weights是正樣本回歸loss的權重,默認為1,負樣本為0,表明在回歸任務中,只采用正樣本進行計算。bbox_outside_weights用于平衡正負樣本的權重,它們將在計算SmoothL1Loss的時候被使用,各自的計算方法如下:
bbox_inside_weights[labels == 1, :] = np.array(cfg.TRAIN.RPN_BBOX_INSIDE_WEIGHTS)
if cfg.TRAIN.RPN_POSITIVE_WEIGHT < 0:
??? # uniform weighting of examples (given non-uniform sampling)
??? num_examples = np.sum(labels >= 0)
??? positive_weights = np.ones((1, 4)) * 1.0 / num_examples
??? negative_weights = np.ones((1, 4)) * 1.0 / num_examples
else:
??? assert ((cfg.TRAIN.RPN_POSITIVE_WEIGHT > 0) &
??????????? (cfg.TRAIN.RPN_POSITIVE_WEIGHT < 1))
??? positive_weights = (cfg.TRAIN.RPN_POSITIVE_WEIGHT /
??????????????????????? np.sum(labels == 1))
??? negative_weights = ((1.0 - cfg.TRAIN.RPN_POSITIVE_WEIGHT) /
??????????????????????? np.sum(labels == 0))
bbox_outside_weights[labels == 1, :] = positive_weights
bbox_outside_weights[labels == 0, :] = negative_weights
?
?然后是faster rcnn:
name: "VGG_CNN_M_1024"
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': 21"
? }
}
可以看到,它的top輸出是im_info,gt_boxes,兩者的尺度分別為(1,3)和(1,4)。而上面的rois,labels,bbox_targets, bbox_inside_weights, bbox_outside_weights全部通過rpn框架來生成,rpn框架我們下面講。
roi_data中需要批量獲取數據,實現就在minibatch.py中了,它實現一次從roidb中獲取多個樣本的操作,主要函數是get_minibatch,根據是否使用rpn來進行操作。
如果使用rpn,則只需要輸出gt_boxes和im_info,直接從roidb數據庫中獲取即可。如果不使用rpn,則需要自己來生成前景和背景的rois訓練圖片,調用了兩個函數_sample_rois,_project_im_rois。
_sample_rois函數生成前景和背景,接口如下:
_sample_rois(roidb, fg_rois_per_image, rois_per_image, num_classes)
通過rois_per_image指定需要生成的訓練樣本的數量, fg_rois_per_image指定前景正樣本的數量。一個前景正樣本就是滿足與真值box中的最大重疊度大于一定閾值cfg.TRAIN.FG_THRESH的樣本,一個背景就是與真值box中的最大重疊度大于一定閾值cfg.TRAIN.FG_THRELO,小于一定閾值cfg.TRAIN.BG_THRESH_SH的樣本,選擇樣本的方法當然就是從符合條件的樣本中隨機選擇,如果滿足條件的樣本不夠,那就按照最低值來選擇。
_project_im_rois就是一個縮放,因為訓練的時候使用了不同的尺度。
(5) rpn目錄
該目錄就是region proposal模塊,包含有generate_anchors.py,proposal_layer.py,anchor_target_layer.py,proposal_target_layer.py,generate.py腳本。
rpn有幾個任務需要完成,產生一些anchors,完成anchor到圖像空間的映射,得到訓練樣本。
generate_anchors腳本就是用于產生anchors,它使用16*16的參考窗口,產生3個比例(1:1,1:2,2:1),三個縮放尺度(0.5, 1, 2)的anchors,共9個。在原論文中對應到原始圖像空間,3個尺度是(128, 256與512),代碼如下:
def generate_anchors(base_size=16, ratios=[0.5, 1, 2],
???????????????????? scales=2**np.arange(3, 6)):
??? base_anchor = np.array([1, 1, base_size, base_size]) - 1
??? ratio_anchors = _ratio_enum(base_anchor, ratios)
??? anchors = np.vstack([_scale_enum(ratio_anchors[i, :], scales)
???????????????????????? for i in xrange(ratio_anchors.shape[0])])
??? return anchors
anchor_target_layer.py就是實現了AnchorTargetLayer,它與generate_anchors配合使用,共同產生anchors的樣本rpn,用于rpn的分類和回歸任務,anchor_target_layer層的caffe配置如下。
layer {
? name: 'rpn-data'
? type: 'Python'
? bottom: 'rpn_cls_score'
? bottom: 'gt_boxes'
? bottom: 'im_info'
? bottom: 'data'
? top: 'rpn_labels'
? top: 'rpn_bbox_targets'
? top: 'rpn_bbox_inside_weights'
? top: 'rpn_bbox_outside_weights'
? python_param {
??? module: 'rpn.anchor_target_layer'
??? layer: 'AnchorTargetLayer'
??? param_str: "'feat_stride': 16"
? }
}
可知,anchor_target_layer的輸入是gt_boxes,im_info,rpn_cls_score,data,輸出就是rpn_labels,rpn_bbox_targets, rpn_bbox_inside_weights, rpn_bbox_outside_weights。rpn_cls_score是rpn網絡的第一個卷積的分類分支的輸出,rpn網絡正是通過rpn-data的指導學習到了如何提取proposals。
假如輸入rpn網絡的為256*13*13的特征,那么一個rpn的輸出通常是一個18*13*13的分類特征圖和一個36*13*13的回歸特征圖,它們都是通過1*1的卷積生成。13*13就是特征圖的大小,它并不會改變。一個18*1*1就對應每一個位置的9個anchor的分類信息,這里的分類不管具體的類別,只分前景與背景,anchor顯示里面有物體存在時可對其進行回歸。一個36*1*1就對應每一個位置的9個anchor的回歸信息,這是一個相對值。后面要做的,就是利用這些anchors,生成propasals了。
proposal_layer腳本,定義了ProposalLayer的類,就是從rpn的輸出開始,得到最終的proposals,輸入有三個,網絡配置如下。
layer {
? name: 'proposal'
? type: 'Python'
? bottom: 'rpn_cls_prob_reshape'
? bottom: 'rpn_bbox_pred'
? bottom: 'im_info'
? top: 'rpn_rois'
#? top: 'rpn_scores'
? python_param {
??? module: 'rpn.proposal_layer'
??? layer: 'ProposalLayer'
??? param_str: "'feat_stride': 16"
? }
}
可以看到,輸入了rpn_bbox_pred,rpn_cls_pro_shape以及im_info,輸出rpn_rois,也就是object proposals,由bbox_transform_inv函數完成坐標變換,接口如下:
proposals = bbox_transform_inv(anchors, bbox_deltas)
這里的bbox_deltas就是上面的rpn_bbox_pred,它就是預測的anchor的偏移量,它的尺寸大小是(1, 4 * A, H, W),其中H,W就是特征圖的大小,而A就是基礎anchors的個數,就是上面的9。Anchors的大小則是(K * A, 4),其中K是偏移位置的種類,偏移位置就是將anchors在特征圖上進行滑動的偏移量,可知包含了x和y兩個方向,產生的方法如下:
shift_x = np.arange(0, width) * self._feat_stride
shift_y = np.arange(0, height) * self._feat_stride
shift_x, shift_y = np.meshgrid(shift_x, shift_y)
shifts = np.vstack((shift_x.ravel(), shift_y.ravel(),
???????? shift_x.ravel(), shift_y.ravel())).transpose()
得到了初始的proposals之后,再經過裁剪,過濾,排序,非極大值抑制后就可以用了。
proposal_target_layer,就是從上面選擇出的object proposals采樣得到訓練樣本,流程與上面roi_data_layer層中沒有rpn模塊時產生訓練樣本類似,因此不再贅述。
labels, rois, bbox_targets, bbox_inside_weights = _sample_rois(
??????????? all_rois, gt_boxes, fg_rois_per_image,
??????????? rois_per_image, self._num_classes)
最后是generate腳本,就是高層的調用腳本,即使用RPN方法從imdb或者圖像中產生proposals。
(6) fast_rcnn目錄
該目錄有bbox_transform.py,config.py,nms.wrapper.py,test.py,train.py幾個腳本。
config.py是一個配置參數的腳本,它配置了非常多的默認變量,非常重要。如果想要修改,也不應該在該腳本中修改,而是到先前提到的experements目錄下進行配置。
配置包含兩部分,一個是訓練部分的配置,一個是測試部分的配置。訓練部分的配置如下,我們添加注釋。
# Training options
__C.TRAIN.SCALES = (600,) #訓練尺度,可以配置為一個數組
__C.TRAIN.MAX_SIZE = 1000 #縮放后圖像最長邊的上限
__C.TRAIN.IMS_PER_BATCH = 2 #每一個batch使用的圖像數量
__C.TRAIN.BATCH_SIZE = 128 #每一個batch中使用的ROIs的數量
__C.TRAIN.FG_FRACTION = 0.25 #每一個batch中前景的比例
__C.TRAIN.FG_THRESH = 0.5 #ROI前景閾值
__C.TRAIN.BG_THRESH_HI = 0.5 #ROI背景高閾值
__C.TRAIN.BG_THRESH_LO = 0.1 #ROI背景低閾值
__C.TRAIN.USE_FLIPPED = True #訓練時是否進行水平翻轉
__C.TRAIN.BBOX_REG = True #是否訓練回歸
__C.TRAIN.BBOX_THRESH = 0.5 #用于訓練回歸的roi與真值box的重疊閾值
__C.TRAIN.SNAPSHOT_ITERS = 10000 #snapshot間隔
__C.TRAIN.SNAPSHOT_INFIX = '' #snapshot前綴
__C.TRAIN.BBOX_NORMALIZE_TARGETS = True #bbox歸一化方法,去均值和方差
__C.TRAIN.BBOX_NORMALIZE_MEANS = (0.0, 0.0, 0.0, 0.0)
__C.TRAIN.BBOX_NORMALIZE_STDS = (0.1, 0.1, 0.2, 0.2)
__C.TRAIN.BBOX_INSIDE_WEIGHTS = (1.0, 1.0, 1.0, 1.0) #rpn 前景box權重
__C.TRAIN.PROPOSAL_METHOD = 'selective_search' #默認proposal方法
__C.TRAIN.ASPECT_GROUPING = True #一個batch中選擇尺度相似的樣本
__C.TRAIN.HAS_RPN = False #是否使用RPN
__C.TRAIN.RPN_POSITIVE_OVERLAP = 0.7 #正樣本IoU閾值
__C.TRAIN.RPN_NEGATIVE_OVERLAP = 0.3 #負樣本IoU閾值
__C.TRAIN.RPN_CLOBBER_POSITIVES = False
__C.TRAIN.RPN_FG_FRACTION = 0.5 #前景樣本的比例
__C.TRAIN.RPN_BATCHSIZE = 256 #RPN樣本數量
__C.TRAIN.RPN_NMS_THRESH = 0.7 #NMS閾值
__C.TRAIN.RPN_PRE_NMS_TOP_N = 12000 #使用NMS前,要保留的top scores的box數量
__C.TRAIN.RPN_POST_NMS_TOP_N = 2000 #使用NMS后,要保留的top scores的box數量
__C.TRAIN.RPN_MIN_SIZE = 16 #原始圖像空間中的proposal最小尺寸閾值
測試時相關配置類似,此處不再一一解釋。
bbox_transform.py中的bbox_transform函數計算的是兩個N * 4的矩陣之間的相關回歸矩陣,兩個輸入矩陣一個是anchors,一個是gt boxes,本質上是在求解每一個anchor相對于它的對應gt box的(dx, dy, dw, dh)的四個回歸值,返回結果的shape為[N, 4],使用了log指數變換。
bbox_transform.py中的bbox_transform_inv函數用于將rpn網絡產生的deltas進行變換處理,求出變換后的對應到原始圖像空間的boxes,它輸入boxes和deltas,boxes表示原始anchors,即未經任何處理僅僅是經過平移之后產生的anchors,shape為[N, 4],N表示anchors的數目。deltas就是RPN網絡產生的數據,即網絡'rpn_bbox_pred'層的輸出,shape為[N, (1 + classes) * 4],classes表示類別數目,1 表示背景,N表示anchors的數目,核心代碼如下。
widths = boxes[:, 2] - boxes[:, 0] + 1.0
heights = boxes[:, 3] - boxes[:, 1] + 1.0
ctr_x = boxes[:, 0] + 0.5 * widths
ctr_y = boxes[:, 1] + 0.5 * heights
dx = deltas[:, 0::4]
dy = deltas[:, 1::4]
dw = deltas[:, 2::4]
dh = deltas[:, 3::4]
pred_ctr_x = dx * widths[:, np.newaxis] + ctr_x[:, np.newaxis]
pred_ctr_y = dy * heights[:, np.newaxis] + ctr_y[:, np.newaxis]
pred_w = np.exp(dw) * widths[:, np.newaxis]
pred_h = np.exp(dh) * heights[:, np.newaxis]
pred_boxes = np.zeros(deltas.shape, dtype=deltas.dtype)
# x1
pred_boxes[:, 0::4] = pred_ctr_x - 0.5 * pred_w
# y1
pred_boxes[:, 1::4] = pred_ctr_y - 0.5 * pred_h
# x2
pred_boxes[:, 2::4] = pred_ctr_x + 0.5 * pred_w
# y2
pred_boxes[:, 3::4] = pred_ctr_y + 0.5 * pred_h
可以看出,它與bbox_transform是配合使用的,bbox_transform使用了對數變換將anchor存儲下來,而bbox_transform_inv則將其恢復到圖像空間。
網絡的回歸坐標預測的是一個經過平移和尺度縮放的因子,如果采用原始的圖像坐標,則可能覆蓋從0~1000這樣幾個數量級差距的數值,很難優化。
train.py和test.py分別是訓練主腳本和測試主腳本。在訓練主腳本中,定義了一個類solverWrapper,包含訓練的函數和存儲模型結果的函數。
test.py腳本中最重要的函數是im_detect,它的輸入是caffe的模型指針,輸入BGR順序的彩色圖像,以及可選的R*4的候選框,這適用于使用selective search提取候選框的方法,擁有rpn框架的faster rcnn則不需要。
返回包括兩個值,一個是scores,一個是boxes。scores就是各個候選框中各個類別的概率,boxes就是各個候選中的目標的回歸坐標。
im_detect方法首先調用_get_blobs函數,它輸入im和boxes。在_get_blobs函數中首先調用_get_image_blob獲得不同尺度的測試輸入,測試尺度在cfg.TEST.SCALES中進行配置。
假如沒有rpn網絡,則boxes非空,這時候需要配置的輸入為blobs['rois']。調用_get_rois_blob函數,它會調用_project_im_rois得到不同尺度的輸入RoI。
假如有rpn網絡,則需要配置blobs['im_info'],它會用于輔助RPN框架從特征空間到原始圖像空間的映射。
Forward部分代碼如下:
forward_kwargs = {'data': blobs['data'].astype(np.float32, copy=False)}
if cfg.TEST.HAS_RPN:
?? forward_kwargs['im_info'] = blobs['im_info'].astype(np.float32, copy=False)
else:
?? forward_kwargs['rois'] = blobs['rois'].astype(np.float32, copy=False)
blobs_out = net.forward(**forward_kwargs)
前向傳播的結果在blobs_out中,分類器如果使用SVM,則分類結果為scores = net.blobs['cls_score'].data,如果使用cnn softmax,則分類結果為scores = blobs_out['cls_prob']。
如果有邊界回歸網絡,獲取回歸結果的代碼如下:
box_deltas = blobs_out['bbox_pred']
pred_boxes = bbox_transform_inv(boxes, box_deltas)
pred_boxes = clip_boxes(pred_boxes, im.shape)
可知原始的回歸結果是一個偏移量,它需要通過bbox_transform_inv反投影到圖像空間。test.py腳本中還包含函數apply_nms,用于對網絡輸出的結果進行非極大值抑制。
2.6 tools 目錄
該目錄包含的就是最高層的可執行腳本,包括_init_paths.py,compress_net.py,demo.py,eval_recall.py,reval.py,rpn_genetate.py這幾個腳本。
_init_paths.py,用于初始化若干路徑,包括caffe的路徑以及lib的路徑,一般大型的工程用這樣的一個文件剝離出路徑是很好的選擇。
compress_net.py,這是用于壓縮參數的腳本,使用了SVD矩陣分解的方法來對模型進行壓縮,這通常對于全連接層是非常有效的,因為對于一些經典的網絡如AlexNet,VGGNet等,全連接層占據了網絡的絕大部分參數。腳本中給出的例子對VGGNet的fc6層和fc7層進行了壓縮,讀者可以使用這個腳本去對更多的帶全連接層的網絡進行壓縮嘗試。
demo.py,這是一個demo演示腳本,調用了fast_rcnn中的test腳本中的檢測函數,使用了工程自帶的一些圖像以及預先提取好的proposal,配置好模型之后就可以進行演示。如果要測試自己的模型和數據,也可以非常方便進行修改。
eval_recall.py,這是用于在測試數據集上對所訓練的模型進行評估的腳本,默認使用的數據集是voc_2007_test,它會統計在不同閾值下的檢測框召回率。
reval.py:對已經檢測好的結果進行評估。
rpn_genetate.py,這個腳本調用了rpn中的genetate函數,產生一個測試數據集的proposal并將其存儲到pkl文件。
test_net.py,測試訓練好的fast rcnn網絡的腳本,調用了fast rcnn的test函數。
train_faster_rcnn_alt_opt.py,這是faster rcnn文章中的使用交替的訓練方法來訓練faster rcnn網絡的具體實現,它包括4個階段,分別是:
RPN第1階段,使用在imagenet分類任務上進行訓練的模型來初始化參數,生成proposals。
fast rcnn第1階段,使用在imagenet分類任務上進行訓練的模型來初始化參數,使用剛剛生成的proposal進行fast rcnn的訓練。
RPN第2階段,使用fast rcnn訓練好的參數進行初始化,并生成proposal。
fast rcnn 第2階段,使用RPN第2階段中的模型進行參數初始化。
train_net.py,訓練腳本。
train_svms.py,R-CNN網絡的SVM訓練腳本,可以不關注。
在熟悉了框架后,就可以使用我們的數據進行訓練了。
?
03?網絡分析
下面我們開始一個任務,就來個貓臉檢測吧,使用VGG CNN 1024網絡,看一下網絡結構圖,然后我們按模塊解析一下。
3.1 input
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,因為我們只有一個貓臉,前景類別數目等于1。
3.2 rpn
layer {
? name: "rpn_conv/3x3"
? type: "Convolution"
? bottom: "conv5"
? top: "rpn/output"
? param { lr_mult: 1.0 }
? param { lr_mult: 2.0 }
? convolution_param {
??? num_output: 256
??? kernel_size: 3 pad: 1 stride: 1
??? weight_filler { type: "gaussian" std: 0.01 }
??? bias_filler { type: "constant" value: 0 }
? }
}
layer {
? name: "rpn_relu/3x3"
? type: "ReLU"
? bottom: "rpn/output"
? top: "rpn/output"
}
layer {
? name: "rpn_cls_score"
? type: "Convolution"
? bottom: "rpn/output"
? top: "rpn_cls_score"
? param { lr_mult: 1.0 }
? param { lr_mult: 2.0 }
? convolution_param {
??? num_output: 18?? # 2(bg/fg) * 9(anchors)
??? kernel_size: 1 pad: 0 stride: 1
??? weight_filler { type: "gaussian" std: 0.01 }
??? bias_filler { type: "constant" value: 0 }
? }
}
?
layer {
? name: "rpn_bbox_pred"
? type: "Convolution"
? bottom: "rpn/output"
? top: "rpn_bbox_pred"
? param { lr_mult: 1.0 }
? param { lr_mult: 2.0 }
? convolution_param {
??? num_output: 36?? # 4 * 9(anchors)
??? kernel_size: 1 pad: 0 stride: 1
??? weight_filler { type: "gaussian" std: 0.01 }
??? bias_filler { type: "constant" value: 0 }
? }
}
具體的網絡拓撲結構圖如下:
從上圖可以看出,rpn網絡的輸入來自于conv5卷積層的輸出,后面接了rpn_conv/3x3層,輸出通道數為256,stride=1。
rpn_conv/3x3層產生了兩個分支,一個是rpn_cls_score,一個是rpn_bbox_pred,分別是分類和回歸框的特征。
rpn_cls_score輸出為18個通道,這是9個anchors的前背景概率,它一邊和gt_boxes,im_info,data一起作為AnchorTargetLayer層的輸入,產生分類的真值rpn_labels,回歸的真值rpn_bbox_targets。另一邊則經過rpn_cls_score_reshape進行reshape,然后與rpn_labels一起產生分類損失。
rpn_bbox_ppred輸出為36個通道,就是9個anchors的回歸預測結果,它與rpn_bbox_targets比較產生回歸損失。rpn_cls_score_reshape重新reshape后得到rpn_cls_prob,rpn_cls_prob_shape,它與rpn_bbox_pred以及輸入,共同得到了region prososal,就是候選的檢測框。
在ProposalLayer層中配置了一個重要參數,就是feat_stride,這是前面的卷積層的feat_stride大小。ProposalLayer層完成的功能就是根據RPN的輸出結果,提取出所需的目標框,而目標框是在原始的圖像空間,所以這里需要預先計算出feat_stride的大小。ProposalLayer層的輸出與data層一起獲得最終的proposal roi,這將作為roi pooling層的輸入。
3.3 roi pooing
前面得到了proposal roi之后,就可以進行roi pooling層,配置如下:
layer {
? name: "roi_pool5"
? type: "ROIPooling"
? bottom: "conv5"
? bottom: "rois"
? top: "pool5"
? roi_pooling_param {
??? pooled_w: 6
??? pooled_h: 6
??? spatial_scale: 0.0625 # 1/16
? }
}
可以看到,它配置了幾個參數,最終spatial_scale對應的就是前面的feat_stride,等于1/16,用于從圖像空間的roi到特征空間roi的投影。
而pooled_w,pooled_h則是最終要pooling的特征圖的大小,這里配置為6*6,從13*13的輸入下采樣而來。
?
04?訓練與測試
寫到這里我們就簡略一些。要做的就是三步,為了簡單,保持使用pascal接口,步驟如下。
(1)準備voc格式的數據,可以找開源數據集或者使用labelme等工具集標注,然后配置好路徑。替換掉pacvoc的ImageSets/Main目錄下面的文件list,以及JPEGS和Annotations目錄下的文件。
(2)然后到lib\datasets\pascal_voc.py中更改self._classes中的類別,由于我們這里是二分類的檢測,所以將多余的類別刪除,只保留背景,添加face類別。
(3)使用experements/tools下面的腳本訓練吧。
遇到了坑,就直接跳和爬吧!
?
感受一下大小臉,大姿態,遮擋,誤檢,漏檢。
路漫漫其修遠兮.......
更多請關注微信公眾號《有三AI》與知乎《有三AI學院》。
總結
以上是生活随笔為你收集整理的【技术综述】万字长文详解Faster RCNN源代码的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【项目实战课】NLP入门第1课,人人免费
- 下一篇: 概念区分:并行、分布式、集群、云、超算