Faster R-CNN 原始碼解析(Tensorflow版)
演算法原理
Feature extraction + Region proposal network + Classification and regression:
圖片連結
資料生成(imdb, roidb)
- datasets/imdb.py: 定義通用的影象資料庫類imdb
datasets/factory.py: 利用lambda表示式像工廠一樣自定義自己所需的資料庫類,以下都以voc_2007_trainval資料集為例,在繼承imdb類的基礎上,定義pascal_voc類。
# 以voc資料集為例,按照imdb的命名,利用pascal_voc()函式生成不同的imdb
datasets/pascal_voc.py: 定義pascal_voc類(繼承自imdb)。在這一部分,根據自己資料庫的具體情況來定義成員變數和成員函式。下面列出一些重要的成員變數及成員函式:
部分成員變數
self._data_path = os.path.join(self._devkit_path, 'VOC' + self._year) #資料庫路徑 self._classes = ('__background__'
- 部分成員函式
gt_roidb(): 呼叫_load_pascal_annotation()函式,返回ROI資料庫。儲存緩衝檔案(第一次執行時),或載入資料庫緩衝檔案。
_load_pascal_annotation(): 從VOC資料庫的XML檔案中載入影象和bbox等資訊, 包括:bboxes座標,類別,overlap矩陣,bbox面積等。
model.train_val.get_training_roidb(imdb): 返回roidb (RoI資料庫) 用來訓練模型。
主要呼叫兩個函式:imdb.append_flipped_images() # imdb類的一個成員函式,用來水平翻轉訓練集(資料增強) rdl_roidb.prepare_roidb(imdb) # roidb.py中定義的函式,下文介紹
- roi_data_layer.roidb.prepare_roidb(imdb): imdb預設的roidb包含:boxes, gt_overlaps, gt_classed和filpped四個keys, 該函式在此基礎進行了擴充,便於模型訓練。擴充的內容包括:’image’:儲存圖片路徑, ‘width’和’height’:儲存圖片尺寸,’max_overlaps’和’max_classes’:儲存最大的overlap以及對應的類別。
小結:至此,生成imdb和roidb兩個資料庫類,記錄資料庫中影象路徑,各類別標籤以及標註等資訊。
演算法的網路框架主要分為三部分, 包括特徵提取網路(VGG16, ResNet, MobileNet等),RPN網路和Classification and regression網路。特徵網路的選取較靈活,在nets資料夾中定義了各個模型的結構,這部分的程式碼不作詳細介紹。下文將主要介紹RPN網路和分類迴歸網路,構建網路的程式碼為network.py中的_build_network()函式
def _build_network(self, is_training=True):
# select initializers
if cfg.TRAIN.TRUNCATED:
initializer = tf.truncated_normal_initializer(mean=0.0, stddev=0.01)
initializer_bbox = tf.truncated_normal_initializer(mean=0.0, stddev=0.001)
else:
initializer = tf.random_normal_initializer(mean=0.0, stddev=0.01)
initializer_bbox = tf.random_normal_initializer(mean=0.0, stddev=0.001)
net_conv = self._image_to_head(is_training)
with tf.variable_scope(self._scope, self._scope):
# 生成anchors
self._anchor_component()
# RPN網路
rois = self._region_proposal(net_conv, is_training, initializer)
# RoI pooling
if cfg.POOLING_MODE == 'crop':
pool5 = self._crop_pool_layer(net_conv, rois, "pool5")
else:
raise NotImplementedError
fc7 = self._head_to_tail(pool5, is_training)
with tf.variable_scope(self._scope, self._scope):
# 分類/迴歸網路
cls_prob, bbox_pred = self._region_classification(fc7, is_training,
initializer, initializer_bbox)
self._score_summaries.update(self._predictions)
return rois, cls_prob, bbox_pred
RPN
圖片連結
這部分介紹RPN網路的構建,首先在Conv5_3特徵圖的基礎上,生成anchors;然後預測每個anchor的類別及位置
self._anchor_component(), 主要呼叫layer_utils/generate_anchors.py: 生成anchors。
輸入影象的尺寸(W, H), 經過feature extraction模組後,得到尺寸為(W/16, H/16)的特徵圖,記為(w, h)(VGG16的網路結構,所有stride的乘積為16,具體原理請參考論文);然後特徵圖的每個點生成k個anchors,論文中設定3種ratios:[0.5, 1, 2], 3種sacles:[8, 16, 32],每個特徵圖共產生w*h*9個anchors。# array([[ -83., -39., 100., 56.], # [-175., -87., 192., 104.], # [-359., -183., 376., 200.], # [ -55., -55., 72., 72.], # [-119., -119., 136., 136.], # [-247., -247., 264., 264.], # [ -35., -79., 52., 96.], # [ -79., -167., 96., 184.], # [-167., -343., 184., 360.]]) # 上述結果是在batchsize=16(什麼意思?)的基礎上,即以(0, 0, 15, 15)作為參考視窗,生成9個anchors。注意:生成不同ratio的anchor的時候,anchor的面積保持不變,只是高寬比發生改變。
self._region_proposal(),首先預測anchors屬於前/背景的分數,以及座標位置。包括兩層網路結構:
第一層:3*3的卷積層
rpn = slim.conv2d(net_conv, 512, [3, 3], trainable=is_training, weights_initializer=initializer, scope="rpn_conv/3x3")
第二層:兩個分支,都用了1*1的卷積核;第一支得到特徵圖(height, width, 9*2),用於判斷bbox中是否含有物體;第二支得到特徵圖 (height, width, 9*4),用於得到bbox的座標。
# shape = (1, ?, ?, 18) , 其中,batchsize=1 rpn_cls_score = slim.conv2d(rpn, self._num_anchors * 2, [1, 1], trainable=is_training, weights_initializer=initializer, # change it so that the score has 2 as its channel size # shape = (1, ?, ?, 2) rpn_cls_score_reshape = self._reshape_layer(rpn_cls_score, 2, 'rpn_cls_score_reshape') # shape = (1, ?, ?, 2) rpn_cls_prob_reshape = self._softmax_layer(rpn_cls_score_reshape, "rpn_cls_prob_reshape") # shape = (?,) rpn_cls_pred = tf.argmax(tf.reshape(rpn_cls_score_reshape, [-1, 2]), axis=1, name="rpn_cls_pred") # shape = (1, ?, ?, 18) rpn_cls_prob = self._reshape_layer(rpn_cls_prob_reshape, self._num_anchors * 2, "rpn_cls_prob") # shape = (1, ?, ?, 36) rpn_bbox_pred = slim.conv2d(rpn, self._num_anchors * 4, [1, 1], trainable=is_training, weights_initializer=initializer, padding='VALID', activation_fn=None, scope='rpn_bbox_pred')
疑問:兩次reshape的過程具體是怎麼進行的? 為什麼要reshape?
_region_proposal() 中的_anchor_target_layer()呼叫anchor_target_layer.py函式得到訓練RPN所需的標籤。為了訓練RPN網路,需要構建兩個損失函式:用於分類(前景/背景2類)的softmax_cross_entropy, 另一類是用於迴歸bbox的smooth_l1_loss。該函式根據cls_prob和bbox_pred為anchors分配標籤(1:前景,0:背景,-1:忽略),即rpn_labels;並計算anchor與gt bbox之間的差值, 即rpn_bbox_targets。另外,bbox_inside_weights, rpn_bbox_outside_weights ????
def _anchor_target_layer(self, rpn_cls_score, name): rpn_labels, rpn_bbox_targets, rpn_bbox_inside_weights, rpn_bbox_outside_weights = tf.py_func( anchor_target_layer,[rpn_cls_score, self._gt_boxes, self._im_info, self._feat_stride, self._anchors, self._num_anchors], [tf.float32, tf.float32, tf.float32, tf.float32]) #省略了部分程式碼 self._anchor_targets['rpn_labels'] = rpn_labels self._anchor_targets['rpn_bbox_targets'] = rpn_bbox_targets self._anchor_targets['rpn_bbox_inside_weights'] = rpn_bbox_inside_weights self._anchor_targets['rpn_bbox_outside_weights'] = rpn_bbox_outside_weights self._score_summaries.update(self._anchor_targets) return rpn_labels
正負樣本生成策略:
- 只保留影象內部的anchors
- 對於每個gt_box,找到與它IoU最大的anchor則設為正樣本
- 對於每個anchor,與任意一個gt_box的IoU>0.7則為正樣本,IoU<0.3設為負樣本
- 其他anchor則被忽略
- 假如正負樣本過多,則進行取樣,取樣比例由RPN_BATCHSIZE, RPN_FG_FRACTION等控制
_region_proposal()中的_proposal_layer()呼叫proposal_layer()函式。功能:生成proposal,並進行篩選(NMS等)。主要流程可概括為以下四點:
- 利用座標變換生成proposal:proposals = bbox_transform_inv(anchors, rpn_bbox_pred)
- 按前景概率對proposal進行降排,然後留下RPN_PRE_NMS_TOP_N個proposal
- 對剩下的proposal進行NMS操作,閾值是0.7
- 對剩下的proposal,保留RPN_POST_NMS_TOP_N個, 得到最終的rois和相應的rpn_socre。
_region_proposal()中的_proposal_target_layer()為上一步中得到的proposal分配所屬物體類別,並得到proposal和 gt_bbox的的座標位置間的差別,便於訓練後續Fast R-CNN的分類和迴歸網路。(注:這一步在測試中沒有,因為測試時沒有ground truth)
def _proposal_target_layer(self, rois, roi_scores, name): rois, roi_scores, labels, bbox_targets, bbox_inside_weights, bbox_outside_weights = tf.py_func( proposal_target_layer, [rois, roi_scores, self._gt_boxes, self._num_classes], [tf.float32, tf.float32, tf.float32, tf.float32, tf.float32, tf.float32]) self._proposal_targets['rois'] = rois self._proposal_targets['labels'] = tf.to_int32(labels, name="to_int32") self._proposal_targets['bbox_targets'] = bbox_targets self._proposal_targets['bbox_inside_weights'] = bbox_inside_weights self._proposal_targets['bbox_outside_weights'] = bbox_outside_weights return rois, roi_scores
主要呼叫proposal_target_layer()函式,其主要步驟如下:
- 確定每張圖片中roi的數目,以及前景fg_roi的數目
- 從所有的rpn_rois中進行取樣,並得到rois的類別標籤以及bbox的迴歸目標(bbox_targets),即真值與預測值之間的偏差。
labels, rois, roi_scores, bbox_targets, bbox_inside_weights = _sample_rois( all_rois, all_scores, gt_boxes, fg_rois_per_image, rois_per_image, _num_classes)
計算rois與gt_bboxes之間的overlap矩陣,對於每一個roi,最大的overlap的gt_bbox的標籤即為該roi的類別標籤,並根據TRAIN.FG_THRESH和TRAIN.BG_THRESH_HI/LO 選擇前景roi和背景roi。
小結:
RPN網路主要進行了三個工作:
預測anchor的類別(屬於前景/背景)及其位置
self._predictions["rpn_cls_score"] = rpn_cls_score self._predictions["rpn_cls_score_reshape"] = rpn_cls_score_reshape self._predictions["rpn_cls_prob"] = rpn_cls_prob self._predictions["rpn_cls_pred"] = rpn_cls_pred self._predictions["rpn_bbox_pred"] = rpn_bbox_pred self._predictions["rois"] = rois
生成訓練RPN網路的標籤資訊(anchor target layer)
self._anchor_targets['rpn_labels'] = rpn_labels self._anchor_targets['rpn_bbox_targets'] = rpn_bbox_targets self._anchor_targets['rpn_bbox_inside_weights'] = rpn_bbox_inside_weights self._anchor_targets['rpn_bbox_outside_weights'] = rpn_bbox_outside_weights
生成訓練分類和迴歸網路的RoI(proposal layer)以及對應的標籤資訊(proposal target layer)
self._proposal_targets['rois'] = rois self._proposal_targets['labels'] = tf.to_int32(labels, name="to_int32") self._proposal_targets['bbox_targets'] = bbox_targets self._proposal_targets['bbox_inside_weights'] = bbox_inside_weights self._proposal_targets['bbox_outside_weights'] = bbox_outside_weights
RoI Pooling
FC layer需要固定尺寸的輸入。在最早的R-CNN演算法中,將輸入的影象直接resize成相同的尺寸。而Faster R-CNN對輸入影象的尺寸沒有要求,經過Proposal layer和 Proposal target layer之後,會得到許多不同尺寸的RoI。Faster R-CNN採用RoI Pooling層(原理參考SPPNet 論文),將不同尺寸ROI對應的特徵圖取樣為相同尺寸,然後輸入後續的FC層。這版程式碼中沒有實現RoI pooling layer, 而是把RoI對應的特徵圖resize成相同尺寸後,再進行max pooling。
# 沒有實現RoI pooling layer
pool5 = self._crop_pool_layer(net_conv, rois, "pool5")
def _crop_pool_layer(self, bottom, rois, name):
with tf.variable_scope(name) as scope:
batch_ids = tf.squeeze(tf.slice(rois, [0, 0], [-1, 1], name="batch_id"), [1])
# 得到歸一化的bbox座標(相對原圖的尺寸進行歸一化)
bottom_shape = tf.shape(bottom)
height = (tf.to_float(bottom_shape[1]) - 1.) * np.float32(self._feat_stride[0])
width = (tf.to_float(bottom_shape[2]) - 1.) * np.float32(self._feat_stride[0])
x1 = tf.slice(rois, [0, 1], [-1, 1], name="x1") / width
y1 = tf.slice(rois, [0, 2], [-1, 1], name="y1") / height
x2 = tf.slice(rois, [0, 3], [-1, 1], name="x2") / width
y2 = tf.slice(rois, [0, 4], [-1, 1], name="y2") / height
# Won't be back-propagated to rois anyway, but to save time
bboxes = tf.stop_gradient(tf.concat([y1, x1, y2, x2], axis=1))
pre_pool_size = cfg.POOLING_SIZE * 2
# 裁剪特徵圖,並resize成相同的尺寸
crops = tf.image.crop_and_resize(bottom, bboxes, tf.to_int32(batch_ids), [pre_pool_size, pre_pool_size], name="crops")
# 進行標準的max pooling
return slim.max_pool2d(crops, [2, 2], padding='SAME')
需要說明的是,我感覺這是一種比較取巧的方法。和標準的ROI pooling之間有什麼區別,還是本質上是等價的?
ROIs:在Fast RCNN中,指的是Selective Search的輸出;在Faster RCNN中指的是RPN的輸出,一堆矩形候選框框,形狀為1x5x1x1(4個座標+索引index),其中值得注意的是:座標的參考系不是針對feature map這張圖的,而是針對原圖的(神經網路最開始的輸入)。下面給出roi pooling層的流程及程式碼(C++)。
座標對映。將roi座標對映到feature map
int roi_start_w = round(rois_flat[index_roi + 1] * spatial_scale); // spatial_scale 1/16 int roi_start_h = round(rois_flat[index_roi + 2] * spatial_scale); int roi_end_w = round(rois_flat[index_roi + 3] * spatial_scale); int roi_end_h = round(rois_flat[index_roi + 4] * spatial_scale);
在feature map上的roi區域做max pooling或者average pooling。
% 確定pooling的視窗。應為roi的尺寸不同,所以視窗的尺寸也會自適應變化 float bin_size_h = (float)(roi_height) / (float)(pooled_height); // 9/7 float bin_size_w = (float)(roi_width) / (float)(pooled_width); // 7/7=1 for (ph = 0; ph < pooled_height; ++ph){ for (pw = 0; pw < pooled_width; ++pw){ int hstart = (floor((float)(ph) * bin_size_h)); int wstart = (floor((float)(pw) * bin_size_w)); int hend = (ceil((float)(ph + 1) * bin_size_h)); int wend = (ceil((float)(pw + 1) * bin_size_w)); hstart = fminf(fmaxf(hstart + roi_start_h, 0), data_height); hend = fminf(fmaxf(hend + roi_start_h, 0), data_height); wstart = fminf(fmaxf(wstart + roi_start_w, 0), data_width); wend = fminf(fmaxf(wend + roi_start_w, 0), data_width); % max/average pooling 這部分程式碼省略
Classification and Regression
直接上程式碼
def _region_classification(self, fc7, is_training, initializer, initializer_bbox): # 分類 cls_score = slim.fully_connected(fc7, self._num_classes, weights_initializer=initializer, trainable=is_training, activation_fn=None, scope='cls_score') cls_prob = self._softmax_layer(cls_score, "cls_prob") cls_pred = tf.argmax(cls_score, axis=1, name="cls_pred") # 迴歸 bbox_pred = slim.fully_connected(fc7, self._num_classes * 4, weights_initializer=initializer_bbox, trainable=is_training, activation_fn=None, scope='bbox_pred') self._predictions["cls_score"] = cls_score self._predictions["cls_pred"] = cls_pred self._predictions["cls_prob"] = cls_prob self._predictions["bbox_pred"] = bbox_pred return cls_prob, bbox_pred
小結:至此,資料準備和整個Faster R-CNN的網路已經搭建完成。為了訓練網路,需要構建損失函式。
Loss
Loss分為4部分:RPN, class loss,RPN, bbox loss,RCNN, class loss,RCNN, bbox loss。
# RPN, class loss rpn_cls_score = tf.reshape(self._predictions['rpn_cls_score_reshape'], [-1, 2]) rpn_label = tf.reshape(self._anchor_targets['rpn_labels'], [-1]) rpn_select = tf.where(tf.not_equal(rpn_label, -1)) rpn_cls_score = tf.reshape(tf.gather(rpn_cls_score, rpn_select), [-1, 2]) rpn_label = tf.reshape(tf.gather(rpn_label, rpn_select), [-1]) rpn_cross_entropy = tf.reduce_mean( tf.nn.sparse_softmax_cross_entropy_with_logits(logits=rpn_cls_score, labels=rpn_label)) # RPN, bbox loss rpn_bbox_pred = self._predictions['rpn_bbox_pred'] rpn_bbox_targets = self._anchor_targets['rpn_bbox_targets'] rpn_bbox_inside_weights = self._anchor_targets['rpn_bbox_inside_weights'] rpn_bbox_outside_weights = self._anchor_targets['rpn_bbox_outside_weights'] rpn_loss_box = self._smooth_l1_loss(rpn_bbox_pred, rpn_bbox_targets, rpn_bbox_inside_weights, rpn_bbox_outside_weights, sigma=sigma_rpn, dim=[1, 2, 3]) # RCNN, class loss cls_score = self._predictions["cls_score"] label = tf.reshape(self._proposal_targets["labels"], [-1]) cross_entropy = tf.reduce_mean( tf.nn.sparse_softmax_cross_entropy_with_logits( logits=tf.reshape(cls_score, [-1, self._num_classes]), labels=label)) # RCNN, bbox loss bbox_pred = self._predictions['bbox_pred'] bbox_targets = self._proposal_targets['bbox_targets'] bbox_inside_weights = self._proposal_targets['bbox_inside_weights'] bbox_outside_weights = self._proposal_targets['bbox_outside_weights'] loss_box = self._smooth_l1_loss(bbox_pred, bbox_targets, bbox_inside_weights, bbox_outside_weights)
分類loss都採用的是:softmax_cross_entropy;迴歸loss都採用的是:smooth_L1_loss
模型訓練
- 論文中採用4步交替訓練策略。
- 先用預訓練好的ImageNet來初始化RPN網路,然後微調(finetune)RPN網路;
- 根據第一步訓練好的RPN來生成RoIs,然後單獨訓練 Fast R-CNN。在這一步訓練過程中,Fast R-CNN的引數初始化也是採用ImageNet預訓練的模型。兩個網路完全分開訓練,不存在共享網路層。
- 採用上一步Fast R-CNN訓練好的網路引數,來重新初始化RPN的共享卷積層。(注意:這一步只對RPN的區域性網路進行微調,前半部分和Fast R-CNN共享的卷積層訓練好後就固定不變了)
- 繼續固定共享網路層引數,用步驟3微調後的RPN網路生成的bbox對Fast R-CNN的非共享層進行引數微調。
本文所用的程式碼採用近似聯合訓練策略。
- 思路:把RPN的損失函式和Fast R-CNN的損失函式根據一定比例加在一起,然後進行整體的SGD訓練
- **問題:**RPN後續的網路層,無法對RPN的bbox座標進行求導更新,即ROI的誤差無法反向傳播到RPN網路,因此只能稱之為近似聯合訓練。
loss = cross_entropy + loss_box + rpn_cross_entropy + rpn_loss_box
訓練/測試流程
這部分內容會在後續新增。
問題:
- 為什麼縮放M x N?
- RPN 網路的兩個卷乘層 3x3, 1x1, 為什麼要這樣設定?