上篇文章闡述了Fast RCNN網絡模型,介於Faster RCNN屬於RCNN系列的經典模型,以及是目前項目暫使用的目標檢測模型,本篇文章會結合論文以及tensorflow版本的代碼實現詳細的闡述該模型。【可能篇幅會很長,畢竟經典模型,慎重】
Faster RCNN論文:https://arxiv.org/abs/1506.01497
Faster RCNN論文翻譯:https://alvinzhu.xyz/2017/10/12/faster-r-cnn/
一、概述
Faster RCNN(Fast Regions with CNN features)相對於Fast RCNN是一種更快速的目標檢測模型。相對於Fast RCNN 66%的mAP,其不僅在縮減訓練、測試時長的情況下,也提高了準確度。(主幹網絡VGG16, mAP70.7%, resnet101 mAP75%)。
【Faster RCNN目標檢測模型提出了與RCNN、SPPNet、Fast RCNN(選擇搜索算法)不一樣的區域提取模式RPN網絡模型,該模型優化了Fast RCNN在時間上的性能瓶頸。RPN網絡和檢測網絡共享全圖的卷積,並且可以在每個位置同時預測目標邊界和objectness得分。】
二、Faster RCNN網絡模型
Faster RCNN物體檢測系統由三個模塊組成:
- 特徵提取網絡
- RPN網絡
- 區域歸一化、物體分類以及邊框迴歸
1、特徵提取網絡
Faster RCNN提取特徵的主幹網絡可以是VGG16的前13層,13Conv+4次池化。
2、RPN網絡
RPN(Region Proposal Network) 區域提案網絡,較之Fast RCNN單獨的Selective Search選擇搜索算法提取候選框,將候選框提取融合到整個網絡中。
區域提案網絡(Region Proposal Network, RPN),它和檢測網絡共享全圖的卷積特徵,使得區域提案几乎不花時間。RPN是一個全卷積網絡,在每個位置同時預測目標邊界和objectness得分。RPN是端到端訓練的,生成高質量區域提案框,用於Fast R-CNN來檢測。我們通過共享其卷積特徵進一步將RPN和Fast R-CNN合併到一個網絡中。使用最近流行的神經網絡術語“注意力”機制,RPN模塊將某塊anchor box打分比較高,則後面的網絡對其進行訓練。
說到RPN網絡,則需要提到錨點(anchor)和 邊框迴歸。
【Anchor】
【原理】滑動窗口在特徵圖上滑動,每經過一個anchor點就會產生3種尺度和3種長寬比K(K = 9)個提案框。每個提案框包含2類信息,一個是該提案框是否包含物體,二是該提案框的座標編碼。每個anchor在cls分類層(softmax二分類)會輸出2*K個分類得分(每個anchor box 前景背景得分),在reg迴歸層(線性迴歸)會產生4K個輸出(每個anchor box都有4個座標),對於大小爲H*W的卷積特徵映射,總共會產生W*H*K個anchor boxes。
【屬性】使用Anchor提取候選區域具有一些屬性:
1、平移不變性,即圖片中物體的也會被其它的anchor的anchor box框選到;
2、基於參照多個尺度和縱橫比設計的錨點,可以簡單地使用單尺度圖像上的卷積特徵,無需額外的成本來縮放尺寸。
(a) 圖像金字塔 (b)卷積金字塔 (d)參考框金字塔(RPN)
【邊框迴歸】
【摘要】RCNN、Fast RCNN、Faster RCNN都需要用到邊界框迴歸來預測物體的目標檢測框。邊界框迴歸要做的就是利用某種映射關係,使得候選目標框經過映射後更加接近於真實目標框。
【原理】
設Anchor的座標 預測邊框G'的座標 真實邊框G的座標
邊框迴歸就是尋找一種變換, 使得
A和G'之間的關係:
【平移】:
【縮放】:
邊框迴歸需要【學習】的就是:
【當anchor A與GT相差較小時(在進行線性迴歸時會篩選anchor和gt IOU在一定範圍內的anchor,這也就保證了anchor與GT的相差不會很大),可以認爲這種變換時一種線性變換,則可以用線性迴歸建模。即Y = WX】
【Y = WX 】輸入的X是 cnn feature map,定義爲,那麼:
【表示的是Anchor的座標和預測的貼近真實框的預測的座標的線性關係,而Anchor的座標和真實框的座標的線性關係是怎麼樣呢,這兩個關係又是什麼關係呢,是否是學習與被學習的關係。】
根據上面Anchor和預測G’的座標關係可知Anchor座標和GT座標的關係是:
【通過上面的公式,可得知就是要擬合(變成)】,,所以整個線性迴歸其實就是學習一組
優化目標爲:
是正則項防止過擬合
Smooth_L1損失函數:
【Propoasl層】
Proposal層輸入三個參數,anchor box前景、背景打分,以及anchor box偏移關係和im_Info(原圖信息),那該層的作用:
對於所有的anchor boxes,結合輸入的偏移關係,進行迴歸,也就是將偏移疊加到anchor boxes上修正原始anchor boxes,
利用anchor box的打分情況和NMS篩選出一定數目的偏移後的anchor boxes。
【RPN】
RPN主要作用是生成區域提案,裏面設計了分類和邊框迴歸。
【分類】:anchor產生的anchor box 通過softmax二分類網絡判斷該區域提案爲前景(包含目標)和背景(不包含)的得分。基於該分類的得分,可以作爲篩選過多anchor的手段。
【邊框迴歸】:
通過anchor和真實邊框的關係(平移、縮放),來優化anchor和預測邊框(其實在這裏預測邊框是不存在,是anchor疊加回歸輸出的偏移)的關係,邊框迴歸輸出的不是座標而是一種關係,也就是anchor和預測邊框之間的偏移情況,在Propoasl層纔將迴歸輸出的偏移疊加到anchor座標上,來修正anchor座標,這也是對anchor的第一次修正,後面還有第二次修正。
邊框迴歸的偏移關係,輸入是特徵向量,所以邊框迴歸要學習的是參數,
也就是通過不同目標的特徵,分別學習針對該目標特徵對應的參數,也就是該參數的維度應該是包含可以檢測的所有物體類別數目。
【輸出】
RPN網絡最終得到訓練(256)、測試(300)對應的anchor box的前景、後景得分情況,以及該anchor box的座標。
3、區域歸一化、物體分類以及邊框迴歸
區域歸一化、物體分類以及邊框迴歸這裏的結構就和Fast RCNN基本一致了,在此就不做闡述了。
三、Faster RCNN代碼解析
該圖來自:https://www.cnblogs.com/darkknightzh/p/10043864.html
下面就結合上面的圖,針對代碼一步一步分析:
代碼的主體結構在_build_network函數裏。
def _build_network(self, is_training=True):
"""
該函數總體流程:
1、通過分類網絡(vgg16、resnet)得到特徵net_cov
2、將net_cov送入rpn網絡得到候選區域anchors,訓練則篩選出2000個anchor,測試則篩選出
300個anchors,在進一步篩選出256個anchors用於分類
3、將256個anchors進行rois_pooling操作得到pool5的7*7的特徵圖
4、將pool5通過兩個fc得到fc7得到21維的cls_score和21*4的bbox_pred
:param is_training:
:return:
"""
# 是否使用截斷正態分佈
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(特徵圖是原圖/16,anchors的個數是特徵圖像素數*9,而每個
#anchor是在原圖上的座標,該函數返回anchors的座標矩陣和anchors數量 )
self._anchor_component()
# RPN網絡對特徵進行處理,最終得到256(訓練)個anchors對應類別以及座標或者300(測
#試)個anchors對應類別以及座標
rois = self._region_proposal(net_conv, is_training, initializer)
# roi pooling層將特徵向量resize到指定大小
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):
# region classification
cls_prob, bbox_pred = self._region_classification(fc7, is_training,
initializer,initializer_bbox)
self._score_summaries.update(self._predictions)
"""
rois: 256個anchors的類別
cls_prob: 256個anchor中每一類別的概率
bbox_pred: 預測位置的偏移
"""
return rois, cls_prob, bbox_pred
self._image_to_head() 提取輸入圖片特徵
def _image_to_head(self, is_training, reuse=None):
with tf.variable_scope(self._scope, self._scope, reuse=reuse):
net = slim.repeat(self._image, 2, slim.conv2d, 64, [3, 3],
trainable=False, scope='conv1')
net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool1')
net = slim.repeat(net, 2, slim.conv2d, 128, [3, 3],
trainable=False, scope='conv2')
net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool2')
net = slim.repeat(net, 3, slim.conv2d, 256, [3, 3],
trainable=is_training, scope='conv3')
net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool3')
net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3],
trainable=is_training, scope='conv4')
net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool4')
net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3],
trainable=is_training, scope='conv5')
self._act_summaries.append(net)
self._layers['head'] = net
return net
self._anchor_component()生成anchor box
def _anchor_component(self):
with tf.variable_scope('ANCHOR_' + self._tag) as scope:
# 獲取圖片的形狀
height = tf.to_int32(tf.ceil(self._im_info[0] /
np.float32(self._feat_stride[0]))) # 特徵圖的高(原圖的1/16)
width = tf.to_int32(tf.ceil(self._im_info[1] /
np.float32(self._feat_stride[0]))) # 特徵圖的寬(原圖的1/16)
# 配置端到端
if cfg.USE_E2E_TF:
anchors, anchor_length = generate_anchors_pre_tf(
height,
width,
self._feat_stride,
self._anchor_scales,
self._anchor_ratios)
else:
anchors, anchor_length = tf.py_func(generate_anchors_pre,
[height, width,
self._feat_stride,
self._anchor_scales,
self._anchor_ratios],
[tf.float32, tf.int32],
name="generate_anchors")
anchors.set_shape([None, 4])
anchor_length.set_shape([])
self._anchors = anchors
self._anchor_length = anchor_length
def generate_anchors_pre_tf(height, width, feat_stride=16, anchor_scales=(8, 16, 32),
anchor_ratios=(0.5, 1, 2)):
shift_x = tf.range(width) * feat_stride # width
shift_y = tf.range(height) * feat_stride # height
shift_x, shift_y = tf.meshgrid(shift_x, shift_y)
sx = tf.reshape(shift_x, shape=(-1,))
sy = tf.reshape(shift_y, shape=(-1,))
shifts = tf.transpose(tf.stack([sx, sy, sx, sy]))
K = tf.multiply(width, height)
shifts = tf.transpose(tf.reshape(shifts, shape=[1, K, 4]), perm=(1, 0, 2))
"生成anchor"
anchors = generate_anchors(ratios=np.array(anchor_ratios),
scales=np.array(anchor_scales))
A = anchors.shape[0]
anchor_constant = tf.constant(anchors.reshape((1, A, 4)), dtype=tf.int32)
length = K * A
anchors_tf = tf.reshape(tf.add(anchor_constant, shifts), shape=(length, 4))
return tf.cast(anchors_tf, dtype=tf.float32), length
def generate_anchors(base_size=16, ratios=[0.5, 1, 2],
scales=2 ** np.arange(3, 6)):
"""
在(0, 0, 15, 15) 的基準窗口上,通過不同尺度、比例變換獲得9個anchor boxes.
"""
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 range(ratio_anchors.shape[0])])
"""
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.]
])
"""
return anchors
self._region_proposal(net_conv, is_training, initializer)通過RPN網絡產生對應數量分類和第一次迴歸的anchor座標和得分。
def _region_proposal(self, net_conv, is_training, initializer):
"特徵提取網絡返回的特徵再經歷個3*3的卷積"
rpn = slim.conv2d(net_conv, cfg.RPN_CHANNELS, [3, 3],
trainable=is_training,
weights_initializer=initializer,
scope="rpn_conv/3x3")
self._act_summaries.append(rpn)
"1*1的卷積"
rpn_cls_score = slim.conv2d(rpn, self._num_anchors * 2, [1, 1],
trainable=is_training,
weights_initializer=initializer,
padding='VALID', activation_fn=None,
scope='rpn_cls_score')
"重新定義符合caffe數據格式的特徵向量"
rpn_cls_score_reshape = self._reshape_layer(rpn_cls_score, 2,
'rpn_cls_score_reshape')
"softmax二分類,給前景、後景打分"
rpn_cls_prob_reshape = self._softmax_layer(rpn_cls_score_reshape,
"rpn_cls_prob_reshape")
"得到該anchor的預測(屬於前景還是後景)"
rpn_cls_pred = tf.argmax(tf.reshape(rpn_cls_score_reshape, [-1, 2]), axis=1,
name="rpn_cls_pred")
"重新改爲原來的數據格式"
rpn_cls_prob = self._reshape_layer(rpn_cls_prob_reshape, self._num_anchors * 2,
"rpn_cls_prob")
"邊框偏移預測"
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')
"訓練生成256個anchor boxes座標信息、類別標籤"
if is_training:
"""
通過迴歸預測的偏移,分類的得分和原始座標,得到2000個修正後的anchor box邊框和得分
"""
rois, roi_scores = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
"對比真實框判斷圖片中對應的修正後的anchor是正樣本、負樣本、還是不關注"
rpn_labels = self._anchor_target_layer(rpn_cls_score, "anchor")
"訓練批次大小是256,該函數產生256個anchor boxes,裏面包含座標信息,類別標籤(前後景)"
with tf.control_dependencies([rpn_labels]):
rois, _ = self._proposal_target_layer(rois, roi_scores, "rpn_rois")
"測試生成300個anchor boxes座標信息、類別標籤"
else:
"cfg[TEST].RPN_POST_NMS_TOP_N = 300"
if cfg.TEST.MODE == 'nms':
rois, _ = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
elif cfg.TEST.MODE == 'top':
rois, _ = self._proposal_top_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
else:
raise NotImplementedError
self._predictions["rpn_cls_score"] = rpn_cls_score "anchor前後景的得分情況"
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
return rois
self._crop_pool_layer(net_conv, rois, "pool5")將特徵向量resize到固定大小
def _crop_pool_layer(self, bottom, rois, name):
"""
將256個archors從特徵圖中裁剪出來縮放到14*14,並進一步max pool到7*7的固定大小,方便rcnn網
絡分類及迴歸座標。
"""
with tf.variable_scope(name) as scope:
"類別"
batch_ids = tf.squeeze(tf.slice(rois, [0, 0], [-1, 1], name="batch_id"), [1])
"獲取邊界框的標準化座標"
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
bboxes = tf.stop_gradient(tf.concat([y1, x1, y2, x2], axis=1))
pre_pool_size = cfg.POOLING_SIZE * 2
crops = tf.image.crop_and_resize(bottom, bboxes, tf.to_int32(batch_ids),
[pre_pool_size, pre_pool_size],
name="crops")
return slim.max_pool2d(crops, [2, 2], padding='SAME')
self._head_to_tail(pool5, is_training)添加fc6、fc7以及防止過擬合的dropout
def _head_to_tail(self, pool5, is_training, reuse=None):
with tf.variable_scope(self._scope, self._scope, reuse=reuse):
pool5_flat = slim.flatten(pool5, scope='flatten')
fc6 = slim.fully_connected(pool5_flat, 4096, scope='fc6')
if is_training:
fc6 = slim.dropout(fc6, keep_prob=0.5, is_training=True,
scope='dropout6')
fc7 = slim.fully_connected(fc6, 4096, scope='fc7')
if is_training:
fc7 = slim.dropout(fc7, keep_prob=0.5, is_training=True,
scope='dropout7')
return fc7
self._region_classification()分類和迴歸
def _region_classification(self, fc7, is_training, initializer, initializer_bbox):
"21類分類得分"
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")
"預測邊框偏移,和在rpn網絡裏面一樣都是預測的偏移"
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
self._add_losses()損失函數,該模型的損失包括rpn網絡的損失和rcnn網絡的損失,在rpn和rcnn中都包含分類損失和迴歸損失,分類損失用的交叉熵損失,迴歸損失用的是smooth l1 損失。【邊框迴歸的原理及推導過程在前面有提及】
def _add_losses(self, sigma_rpn=3.0):
with tf.variable_scope('LOSS_' + self._tag) as scope:
"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']
"預測偏移和anchor與gt真實偏移的smooth l1 損失"
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=cls_score,
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']
"""
預測偏移和anchor與gt真實偏移的smooth l1 損失(注意兩次邊框迴歸都是和anchor和gt真實偏移
進行迴歸,因爲兩次都是爲了修正anchor使之更接近gt)
"""
loss_box = self._smooth_l1_loss(bbox_pred, bbox_targets, bbox_inside_weights,
bbox_outside_weights)
self._losses['cross_entropy'] = cross_entropy
self._losses['loss_box'] = loss_box
self._losses['rpn_cross_entropy'] = rpn_cross_entropy
self._losses['rpn_loss_box'] = rpn_loss_box
"總損失爲四個損失之和"
loss = cross_entropy + loss_box + rpn_cross_entropy + rpn_loss_box
regularization_loss = tf.add_n(tf.losses.get_regularization_losses(), 'regu')
"總損失添加正則項"
self._losses['total_loss'] = loss + regularization_loss
self._event_summaries.update(self._losses)
return loss
【上面只是介紹了特徵提取、產生anchor、rpn網絡、crop_pooling層、添加fc和dropout、分類和迴歸以及額外添加的loss主體函數,每個主體函數裏面還調用許多主體函數的具體實現過程,Faster RCNN的代碼量比較大,在此就不在闡述,可以參考源碼自己細化理解。也可以參考博客】
四、創新與挑戰
1、創新
Faster RCNN就是RPN + Fast RCNN,RPN內部的分類網絡可以生成高質量的區域提案框,內部的迴歸層可以優化、修正區域提案框。
在多任務損失訓練添加了RPN網絡裏面的分類和迴歸損失。
2、挑戰
Faster RCNN一張圖片的處理速度還不是很快。
總結:Faster RCNN是RCNN系列的一個階段性成果,RPN網絡的創新,使得區域提案不再是時間性能瓶頸,邊框偏移的兩次優化提高了整體目標檢測的預測性能。