1. 程式人生 > >Faster R-CNN 原始碼解析(Tensorflow版)

Faster R-CNN 原始碼解析(Tensorflow版)

演算法原理

Feature extraction + Region proposal network + Classification and regression:
Faster R-CNN 框架(VGG16)
圖片連結

資料生成(imdb, roidb)

  1. datasets/imdb.py: 定義通用的影象資料庫類imdb
  2. datasets/factory.py: 利用lambda表示式像工廠一樣自定義自己所需的資料庫類,以下都以voc_2007_trainval資料集為例,在繼承imdb類的基礎上,定義pascal_voc類。

     #  以voc資料集為例,按照imdb的命名,利用pascal_voc()函式生成不同的imdb 
    for year in ['2007', '2012']: for split in ['train', 'val', 'trainval', 'test']: name = 'voc_{}_{}'.format(year, split) #year='2007', split='trainval' __sets[name] = (lambda split=split, year=year: pascal_voc(split, year)) def get_imdb(name): """Get an imdb (image database) by name.""" if
    name not in __sets: raise KeyError('Unknown dataset: {}'.format(name)) return __sets[name]()
  3. datasets/pascal_voc.py: 定義pascal_voc類(繼承自imdb)。在這一部分,根據自己資料庫的具體情況來定義成員變數和成員函式。下面列出一些重要的成員變數及成員函式:

    1. 部分成員變數

      self._data_path = os.path.join(self._devkit_path, 'VOC' + self._year)  #資料庫路徑
      self._classes = ('__background__'
      , # always index 0, 訓練類別標籤,包含背景類 'person') # Default to roidb handler self._roidb_handler = self.gt_roidb #感興趣區域(ROI)資料庫 self._salt = str(uuid.uuid4()) #?? self._comp_id = 'comp4' # ??
    2. 部分成員函式
      gt_roidb(): 呼叫_load_pascal_annotation()函式,返回ROI資料庫。儲存緩衝檔案(第一次執行時),或載入資料庫緩衝檔案。
      _load_pascal_annotation(): 從VOC資料庫的XML檔案中載入影象和bbox等資訊, 包括:bboxes座標,類別,overlap矩陣,bbox面積等。
  4. model.train_val.get_training_roidb(imdb): 返回roidb (RoI資料庫) 用來訓練模型。
    主要呼叫兩個函式:

    imdb.append_flipped_images() # imdb類的一個成員函式,用來水平翻轉訓練集(資料增強)
    rdl_roidb.prepare_roidb(imdb) # roidb.py中定義的函式,下文介紹
    1. 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
圖片連結
這部分介紹RPN網路的構建,首先在Conv5_3特徵圖的基礎上,生成anchors;然後預測每個anchor的類別及位置

  1. 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的面積保持不變,只是高寬比發生改變。
  2. self._region_proposal(),首先預測anchors屬於前/背景的分數,以及座標位置。包括兩層網路結構:

    1. 第一層:3*3的卷積層

      rpn = slim.conv2d(net_conv, 512, [3, 3], trainable=is_training, weights_initializer=initializer,
                          scope="rpn_conv/3x3")
    2. 第二層:兩個分支,都用了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?

  3. _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

    正負樣本生成策略:

    1. 只保留影象內部的anchors
    2. 對於每個gt_box,找到與它IoU最大的anchor則設為正樣本
    3. 對於每個anchor,與任意一個gt_box的IoU>0.7則為正樣本,IoU<0.3設為負樣本
    4. 其他anchor則被忽略
    5. 假如正負樣本過多,則進行取樣,取樣比例由RPN_BATCHSIZE, RPN_FG_FRACTION等控制
  4. _region_proposal()中的_proposal_layer()呼叫proposal_layer()函式。功能:生成proposal,並進行篩選(NMS等)。主要流程可概括為以下四點:

    1. 利用座標變換生成proposal:proposals = bbox_transform_inv(anchors, rpn_bbox_pred)
    2. 按前景概率對proposal進行降排,然後留下RPN_PRE_NMS_TOP_N個proposal
    3. 對剩下的proposal進行NMS操作,閾值是0.7
    4. 對剩下的proposal,保留RPN_POST_NMS_TOP_N個, 得到最終的rois和相應的rpn_socre。
  5. _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()函式,其主要步驟如下:

    1. 確定每張圖片中roi的數目,以及前景fg_roi的數目
    2. 從所有的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網路主要進行了三個工作:

  1. 預測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
  2. 生成訓練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
  3. 生成訓練分類和迴歸網路的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++)。

  1. 座標對映。將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);
  2. 在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 這部分程式碼省略

    roi_pooling
    圖片連結

Classification and Regression

  1. 直接上程式碼

    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

  1. 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)
  2. 分類loss都採用的是:softmax_cross_entropy;迴歸loss都採用的是:smooth_L1_loss

模型訓練

  1. 論文中採用4步交替訓練策略
    1. 先用預訓練好的ImageNet來初始化RPN網路,然後微調(finetune)RPN網路;
    2. 根據第一步訓練好的RPN來生成RoIs,然後單獨訓練 Fast R-CNN。在這一步訓練過程中,Fast R-CNN的引數初始化也是採用ImageNet預訓練的模型。兩個網路完全分開訓練,不存在共享網路層。
    3. 採用上一步Fast R-CNN訓練好的網路引數,來重新初始化RPN的共享卷積層。(注意:這一步只對RPN的區域性網路進行微調,前半部分和Fast R-CNN共享的卷積層訓練好後就固定不變了)
    4. 繼續固定共享網路層引數,用步驟3微調後的RPN網路生成的bbox對Fast R-CNN的非共享層進行引數微調。
  2. 本文所用的程式碼採用近似聯合訓練策略

    1. 思路:把RPN的損失函式和Fast R-CNN的損失函式根據一定比例加在一起,然後進行整體的SGD訓練
    2. **問題:**RPN後續的網路層,無法對RPN的bbox座標進行求導更新,即ROI的誤差無法反向傳播到RPN網路,因此只能稱之為近似聯合訓練。
    loss = cross_entropy + loss_box + rpn_cross_entropy + rpn_loss_box

訓練/測試流程

這部分內容會在後續新增。

問題:

  1. 為什麼縮放M x N?
  2. RPN 網路的兩個卷乘層 3x3, 1x1, 為什麼要這樣設定?