1. 程式人生 > >Faster RCNN(2)程式碼分析

Faster RCNN(2)程式碼分析

目錄

執行程式碼

程式碼分析

執行程式碼

原作者的程式碼實現py-faster-rcnn,用的框架是caffe,由於對caffe不熟悉,所以在github上找了一個tensorflow版本的程式碼實現,地址是tf-faster-rcnn

在github上閱讀程式碼之前,肯定是要先讀一遍readme,根據作者寫的說明將程式碼執行起來,這樣也便於後面在程式碼中新增log來分析程式碼。

1.安裝環境

下載程式碼

git clone https://github.com/endernewton/tf-faster-rcnn.git

保證除了tensorflow外還需要cythonopencv-python

easydict這三個包。

sudo pip install Cython
sudo pip install opencv-python
sudo pip install easydict

根據自己的電腦配置來修改setup.py

cd tf-faster-rcnn/lib
vim setup.py

比如根據我的電腦是GTX 1080 (Ti),所以修改了-arch為sm_61,具體的型號可以在README中檢視。

        extra_compile_args={'gcc': ["-Wno-unused-function"],
                            'nvcc': ['-arch=sm_61',
                                     '--ptxas-options=-v',
                                     '-c',
                                     '--compiler-options',
                                     "'-fPIC'"]},
        include_dirs = [numpy_include, CUDA['include']]

如果我們的訓練電腦只有CPU,那麼可以把./lib/model/config.py中的__C.USE_GPU_NMS = True改為False。

然後執行make編譯出gpu_nms.so和cpu_nms.so,這部分是原作者為nms做GPU加速而設計出來的程式碼。

2.準備資料

下載VOCdevkit

wget http://host.robots.ox.ac.uk/pascal/VOC/voc2007/VOCtrainval_06-Nov-2007.tar
wget http://host.robots.ox.ac.uk/pascal/VOC/voc2007/VOCtest_06-Nov-2007.tar
wget http://host.robots.ox.ac.uk/pascal/VOC/voc2007/VOCdevkit_08-Jun-2007.tar

tar xvf VOCtrainval_06-Nov-2007.tar
tar xvf VOCtest_06-Nov-2007.tar
tar xvf VOCdevkit_08-Jun-2007.tar

建立軟連結

cd $FRCN_ROOT/data
ln -s $VOCdevkit VOCdevkit2007

3.下載Pre-trained Weights

./data/scripts/fetch_faster_rcnn_models.sh

下載使用Resnet101網路對VOC07+12資料集訓練出來的weights。如果無法下載可以試試作者提供的google drive下載。

下載的檔案解壓後放在data目錄下,然後建立軟連結

NET=res101
TRAIN_IMDB=voc_2007_trainval+voc_2012_trainval
mkdir -p output/${NET}/${TRAIN_IMDB}
cd output/${NET}/${TRAIN_IMDB}
ln -s ../../../data/voc_2007_trainval+voc_2012_trainval ./default
cd ../../..

我們準備的資料是VOC2007,而下載的weights是根據2007和2012進行訓練的,不過我們只是需要將流程跑通,不下載VOC2012關係不大。

4.執行demo和test

執行demo

GPU_ID=0
CUDA_VISIBLE_DEVICES=${GPU_ID} ./tools/demo.py

可以看到對data/demo中的圖片都進行了預測。

執行test

GPU_ID=0
./experiments/scripts/test_faster_rcnn.sh $GPU_ID pascal_voc_0712 res101

可以得到對每個類別預測的準確率

Saving cached annotations to /local/share/DeepLearning/stesha/tf-faster-rcnn-master/data/VOCdevkit2007/VOC2007/ImageSets/Main/test.txt_annots.pkl
AP for aeroplane = 0.8300
AP for bicycle = 0.8684
AP for bird = 0.8129
AP for boat = 0.7411
AP for bottle = 0.6853
AP for bus = 0.8764
AP for car = 0.8805
AP for cat = 0.8830
AP for chair = 0.6231
AP for cow = 0.8683
AP for diningtable = 0.7080
AP for dog = 0.8852
AP for horse = 0.8727
AP for motorbike = 0.8297
AP for person = 0.8272
AP for pottedplant = 0.5319
AP for sheep = 0.8115
AP for sofa = 0.7767
AP for train = 0.8461
AP for tvmonitor = 0.7938
Mean AP = 0.7976
~~~~~~~~
Results:
0.830
0.868
0.813
0.741
0.685
0.876
0.880
0.883
0.623
0.868
0.708
0.885
0.873
0.830
0.827
0.532
0.811
0.777
0.846
0.794
0.798
~~~~~~~~

5.訓練資料集

基於ImageNet的分類訓練的權重來訓練Faster RCNN,所以我們需要先下載ImageNet訓練權重

mkdir -p data/imagenet_weights
cd data/imagenet_weights
wget -v http://download.tensorflow.org/models/vgg_16_2016_08_28.tar.gz
tar -xzvf vgg_16_2016_08_28.tar.gz
mv vgg_16.ckpt vgg16.ckpt
cd ../..

然後就可以訓練了,也可以將資料集替換成自己的資料進行訓練。

./experiments/scripts/train_faster_rcnn.sh 0 pascal_voc vgg16

程式碼分析

模型訓練

1.配置和資料準備

當我們執行train_faster_rcnn.sh進行訓練時實際上是執行python ./tools/trainval_net.py並且傳入了一些引數。在trainval_net.py一開始會打印出所有引數

if __name__ == '__main__':
  args = parse_args()

  print('Called with args:')
  print(args)


output:
Called with args:
Namespace(cfg_file='experiments/cfgs/vgg16.yml', imdb_name='voc_2007_trainval', imdbval_name='voc_2007_test', max_iters=70000, net='vgg16', set_cfgs=['ANCHOR_SCALES', '[8,16,32]', 'ANCHOR_RATIOS', '[0.5,1,2]', 'TRAIN.STEPSIZE', '[50000]'], tag=None, weight='data/imagenet_weights/vgg16.ckpt')

然後讀取cfg_file中的配置資訊放入config.py,整理set_cfgs中的資訊後也放入config.py。常用的配置資訊一開始已經放入config.py中了,這步操作相當於是增加了一些網路特有的配置資訊。程式碼如下

  if args.cfg_file is not None:
    cfg_from_file(args.cfg_file)
  if args.set_cfgs is not None:
    cfg_from_list(args.set_cfgs)

接著準備imdb和roidb

  imdb, roidb = combined_roidb(args.imdb_name)
  print('{:d} roidb entries'.format(len(roidb)))

imdb是imdb.py的類物件,便於後面使用imdb提供的方法。

roidb是通過_load_pascal_annotation解析xml檔案,獲取其中ground truth的boxes,gt_classes,gt_overlaps,flipped,seg_areas資訊。

boxes的shape是(len(objs), 4),表示圖片中每個元素有一組box資訊:xmin,xmax,ymin,ymax

gt_classes的shape是len(objs),圖片中每個元素有一個數字表示的類別資訊cls

gt_overlaps的shape是(len(objs), num_classes),圖片中每個類別有一個對應cls位置為1.0,其他位置都為0的矩陣。

flipped表示是原圖還是翻轉圖True或者False。

seg_areas的shape是len(objs),每個類別有一個矩陣面積。

然後在prepare_roidb函式的呼叫過程中還會增加一些資訊到roidb中:

image表示對應的image的路徑

width表示圖片的寬

height表示圖片的長

max_overlaps表示gt_overlaps每一行最大的值。因為是ground truth的最大值,所以都是1.0,比如一張圖片4個物體那麼max_overlaps的值為[1. 1. 1. 1.]

max_classes表示gt_overlaps每一行最大值的位置。其實就是一張圖片上每個物體的類別,比如一張圖片上4個物體,max_classes的值為[15 15 18  9]

下面設定output_dir

  output_dir = get_output_dir(imdb, args.tag)
  print('Output will be saved to `{:s}`'.format(output_dir))
#./tensorboard/vgg16/voc_2007_trainval/default

再設定tensorboard路徑

  #tensorboard/vgg16/voc_2007_trainval/default
  tb_dir = get_output_tb_dir(imdb, args.tag)
  print('TensorFlow summaries will be saved to `{:s}`'.format(tb_dir))

用同樣的方式取出validation set的valroidb,但是不進行圖片的翻轉

  orgflip = cfg.TRAIN.USE_FLIPPED
  cfg.TRAIN.USE_FLIPPED = False
  _, valroidb = combined_roidb(args.imdbval_name)
  print('{:d} validation roidb entries'.format(len(valroidb)))
  cfg.TRAIN.USE_FLIPPED = orgflip

準備網路例項,以vgg16網路結構為例

net = vgg16()

最後一步就是訓練網路,將前面準備的所有資料都傳入了train_net函式。

  train_net(net, imdb, roidb, valroidb, output_dir, tb_dir,
            pretrained_model=args.weight,
            max_iters=args.max_iters)

2.訓練網路train_net

對roidb和valroidb進行過濾,就是隻保留max_overlaps大於等於0.5(前景),或者小於0.5大於等於0.1(背景)的值。我想對於ground truth的roidb而言,應該只能去掉一些圖片中沒有任何標記的例子。

  roidb = filter_roidb(roidb)
  valroidb = filter_roidb(valroidb)

建立SolverWrapper物件,將函式的引數都儲存在SolverWrapper內部,便於後面呼叫train_model的時候直接使用。

    sw = SolverWrapper(sess, network, imdb, roidb, valroidb, output_dir, tb_dir,
                       pretrained_model=pretrained_model)

然後進行訓練,max_iters是傳入的值為70000

sw.train_model(sess, max_iters)

3.train_model

首先對roidb和valroidb初始化RoIDataLayer物件,並且呼叫_shuffle_roidb_inds打亂db index順序,比如roidb的長度是10022,所以RoIDataLayer內部儲存的self._perm是打亂[0, 1, 2, ......, 10019, 10020, 10021]這個陣列的順序的向量。另外因為cfg.TRAIN.ASPECT_GROUPING為False,所以呼叫的是np.random.permutation,這個方法和shuffle的區別是會返回一個打亂順序的陣列,但是不會改變原來的陣列。

    self.data_layer = RoIDataLayer(self.roidb, self.imdb.num_classes)
    self.data_layer_val = RoIDataLayer(self.valroidb, self.imdb.num_classes, random=True)

接著呼叫construct_graph函式

    lr, train_op = self.construct_graph(sess)

在這個函式中呼叫了一個很重要的函式create_architecture,這個函式是搭建網路的核心,現在先跳過,後面重點分析這個函式。另外在construct_graph中取出了'total_loss',設定了learning_rate,指定了optimizer。

再回到train_model函式,接著判斷是否有可以restore的snapshot,如果有就恢復最後一個snapshot

    # Find previous snapshots if there is any to restore from
    lsf, nfiles, sfiles = self.find_previous()

    # Initialize the variables or restore them from the last snapshot
    if lsf == 0:
      rate, last_snapshot_iter, stepsizes, np_paths, ss_paths = self.initialize(sess)
    else:
      rate, last_snapshot_iter, stepsizes, np_paths, ss_paths = self.restore(sess, 
                                                                            str(sfiles[-1]), 
                                                                            str(nfiles[-1]))

如果呼叫initialize,其實會將ImageNet中訓練的vgg16的checkpoint恢復。恢復引數的程式碼在vgg16.py中的fix_variables函式中實現。

接著進入訓練的迴圈中,blobs是每次取出來的一個minibatch,因為data_layer中的self._perm是已經打亂過順序了,所以第一次取minibatch順序就是亂的。另外因為TRAIN.IMS_PER_BATCH為1,所以我們每次只取一個roidb的資料,對應一張圖片。

blobs = self.data_layer.forward()
  def forward(self):
    """Get blobs and copy them into this layer's top blob vector."""
    blobs = self._get_next_minibatch()
    return blobs

具體準備blobs的程式碼在get_minibatch函式中實現,blobs的內容有三部分:

blobs['data']:是預處理後的圖片資料。1.圖片減去均值,2.短邊壓縮到600,如果壓縮後長邊大於1000,那麼長邊壓縮到1000

blobs['im_info']:三個數字,前兩個是壓縮後的圖片寬高,第三個是圖片的壓縮比例

blobs['gt_boxes']:shape是(len(objs), 5),5個數字中前4個是boxes資訊,第5個是對應obj的cls資訊,過濾掉了background這個類別的資料。

然後呼叫train_step進行訓練

        # Compute the graph without summary
        rpn_loss_cls, rpn_loss_box, loss_cls, loss_box, total_loss = \
          self.net.train_step(sess, blobs, train_op)
  def train_step(self, sess, blobs, train_op):
    feed_dict = {self._image: blobs['data'], self._im_info: blobs['im_info'],
                 self._gt_boxes: blobs['gt_boxes']}
    rpn_loss_cls, rpn_loss_box, loss_cls, loss_box, loss, _ = sess.run([self._losses["rpn_cross_entropy"],
                                                                        self._losses['rpn_loss_box'],
                                                                        self._losses['cross_entropy'],
                                                                        self._losses['loss_box'],
                                                                        self._losses['total_loss'],
                                                                        train_op],
                                                                       feed_dict=feed_dict)
    return rpn_loss_cls, rpn_loss_box, loss_cls, loss_box, loss

訓練是將rpn的loss和fast rcnn的loss相加作為total loss來進行優化的,跟論文中提到的交替訓練方式不太一樣。不過這樣會讓訓練更加的方便。

4.搭建網路create_architecture

前面跳過了create_architecture,現在再來分析這個函式。

首先建立了三個placeholder,對應我們從minibatch中取出來的blobs的三個資訊。因為傳入圖片的尺寸並不固定,所以self._image的長寬shape設定為None。

    self._image = tf.placeholder(tf.float32, shape=[1, None, None, 3])
    self._im_info = tf.placeholder(tf.float32, shape=[3])
    self._gt_boxes = tf.placeholder(tf.float32, shape=[None, 5])

然後初始化了一些引數,self._num_anchors表示每個中心點anchor的個數,是9個。

self._num_anchors = self._num_scales * self._num_ratios

然後兩個關鍵的函式就是self._build_networkself._add_losses,將搭建網路的中間引數和建立的loss最後都放入layers_to_output中做為create_architecture函式的返回值。

先看_build_network,在這個函式的實現中也有兩個主要的部分,

1.呼叫_image_to_head,搭建了vgg網路,如果傳入的圖片尺寸是(1, 600, 800, 3),經過計算後成為(1, 38, 50, 512)

  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

2.呼叫self._anchor_component()

  def _anchor_component(self):
    with tf.variable_scope('ANCHOR_' + self._tag) as scope:
      # just to get the shape right
      # 用圖片原本的長寬除以16,得到的就是經過vgg運算後的feature map的尺寸
      height = tf.to_int32(tf.ceil(self._im_info[0] / np.float32(self._feat_stride[0])))
      width = tf.to_int32(tf.ceil(self._im_info[1] / np.float32(self._feat_stride[0])))
      if cfg.USE_E2E_TF:
        # 生成了相對原圖的所有anchors
        anchors, anchor_length = generate_anchors_pre_tf(
          height,
          width,
          self._feat_stride,
          self._anchor_scales,
          self._anchor_ratios
        )
      ......

關鍵函式是generate_anchors_pre_tf,這個函式的目的是生成相對原圖座標而言的所有anchors。以feature map的座標還原到原圖上的位置,然後以這些位置為中心點與相對尺寸的9個anchors相加,為每個中心點生成9個anchors。所以anchors的個數為38*50*9=17100,就是論文中說的,feature map的每個畫素點對應9個anchors。

def generate_anchors_pre_tf(height, width, feat_stride=16, anchor_scales=(8, 16, 32), anchor_ratios=(0.5, 1, 2)):
  # 將feature map的橫向還原到原圖,並且間隔16設定一個點
  shift_x = tf.range(width) * feat_stride # width
  # 將feature map的縱向還原到原圖,並且間隔16設定一個點
  shift_y = tf.range(height) * feat_stride # height
  # 將x與y的座標對應起來,生成原圖上橫縱都間隔16的網格點座標sw和sy。
  shift_x, shift_y = tf.meshgrid(shift_x, shift_y)
  sx = tf.reshape(shift_x, shape=(-1,))
  sy = tf.reshape(shift_y, shape=(-1,))
  # sx和sy作為原圖框的起始座標和結束座標,因為起始座標和結束座標相同,所以其實[sx,sy,sx,sy]框是一個面積為0的點
  shifts = tf.transpose(tf.stack([sx, sy, sx, sy]))
  K = tf.multiply(width, height)
  # shifts尺寸為(width*height, 1, 4)
  shifts = tf.transpose(tf.reshape(shifts, shape=[1, K, 4]), perm=(1, 0, 2))

  # 利用anchor_ratios和anchor_scales生成固定比例的框
  # 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.]])
  anchors = generate_anchors(ratios=np.array(anchor_ratios), scales=np.array(anchor_scales))
  A = anchors.shape[0]
  # anchor_constant尺寸為(1, 9, 4)
  anchor_constant = tf.constant(anchors.reshape((1, A, 4)), dtype=tf.int32)

  length = K * A
  # (1, 9, 4) + (width*height, 1, 4),按照廣播原則相加得到的尺寸為(width*height, 9, 4),然後reshape成(width*height*9, 4),這就是相對原圖上的所有anchors
  anchors_tf = tf.reshape(tf.add(anchor_constant, shifts), shape=(length, 4))
  
  return tf.cast(anchors_tf, dtype=tf.float32), length

3.呼叫self._region_proposal搭建rpn網路

  def _region_proposal(self, net_conv, is_training, initializer):
    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)
    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')
    # change it so that the score has 2 as its channel size
    rpn_cls_score_reshape = self._reshape_layer(rpn_cls_score, 2, 'rpn_cls_score_reshape')
    rpn_cls_prob_reshape = self._softmax_layer(rpn_cls_score_reshape, "rpn_cls_prob_reshape")
    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')
    if is_training:
      rois, roi_scores = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
      rpn_labels = self._anchor_target_layer(rpn_cls_score, "anchor")
      # Try to have a deterministic order for the computing graph, for reproducibility
      with tf.control_dependencies([rpn_labels]):
        rois, _ = self._proposal_target_layer(rois, roi_scores, "rpn_rois")
    ......

    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

    return rois

以feature map尺寸為(38, 50, 512)為例,下圖是這些變數之間的生成關係與尺寸。

裡面呼叫的三個比較重要的函式需要解說一下

# 由於計算出來的rpn_bbox_pred中的dx,dy,dw,dh都是相對anchor的偏移
# 所以這個函式就是相對anchors計算出來pred_boxes的座標,將超出範圍的pred_box進行裁剪
# 然後先取出分數排行前12000的scores和boxes,再用nms取出2000個boxes和對應的scroes
# 剩下proposals的shape是(2000, 4),返回的rois是對proposals第一列增加了一個數字全為0的列
# 所以rois的shape是(2000, 5),roi_scores的shape是(2000,)
rois, roi_scores = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
# 這個函式實際上是呼叫了anchor_target_layer函式
# anchor_target_layer函式的實現比較複雜,是為了生成四個返回值
# rpn_labels, rpn_bbox_targets, rpn_bbox_inside_weights, rpn_bbox_outside_weights
# 1.所有anchors中超出圖片範圍的剔除,生成在圖片內部的anchor列表,假設是5944個
# 2.建立labels向量,長度為5944,裡面的值預設全部設定為-1
# 3.計算每一個anchor與每一個gt_box之間的IOU,假設有4個gt_box,那麼生成的overlaps尺寸為(5944, 4)
# 4.每個anchor與4個gt_box之間的IOU取出最大值,如果最大值小於0.3,那麼這個anchor對應的label標記為0(負樣本)
# 5.每個anchor與4個gt_box之間的IOU取出最大值,如果最大值小於0.7,那麼這個anchor對應的label標記為1(正樣本)
# 6.為了防止最大值沒有大於0.7的情況,取出overlaps中列最大值,如果有相同的最大值也一起取出,對應的label標記為1
# 7. 篩選正負樣本,使它們的個數都不超過128個,沒有被篩選上的anchor對應的label標記為-1(不關心的值)
# 8. gt_boxes[argmax_overlaps, :]表示取gt_boxes的哪一組值來進行計算,是根據anchor跟哪一個box的IOU最大決定的
# 9. bbox_targets的資料是anchor相對gt_box的dx,dy,dw,dh,是偏移壓縮比例。(5944, 4)
# 10. bbox_inside_weights是(5944, 4)的全0矩陣,將label為1的位置的資料改為[1.0,1.0,1.0,1.0]
# 11. bbox_outside_weights是(5944, 4)的全0矩陣,將label為1和為0的位置的資料改為[0.00390625 0.00390625 0.00390625 0.00390625](用1除以正負樣本總和算出來的)
# 12. 把長度5944的label還原長度為17100的label,用-1來補新增位置的值
# 13. 把尺寸為(5944, 4)的bbox_targets,還原到尺寸為(17100, 4),用0來補新增位置的值
# 14. bbox_inside_weights和bbox_outside_weights同樣操作,尺寸都成為(17100, 4)
# 15. rpn_labels是對labels進行reshape,尺寸為(1, 1, 9*38, 50)
# 16. rpn_bbox_targets是對bbox_targets進行reshape,尺寸為(1, 38, 50, 36)
# 17. rpn_bbox_inside_weights和rpn_bbox_outside_weights同上一樣,尺寸為(1, 38, 50, 36)
rpn_labels = self._anchor_target_layer(rpn_cls_score, "anchor")
# 這個函式實際上是呼叫了proposal_target_layer,然後呼叫_sample_rois
# 因為anchors已經準備了256個正負樣本參與計算,而proposal還是2000個,所以要進一步過濾proposal的個數
# 1.overlaps是計算出來的大約2000個proposal與gt_boxes之間的IOU,(2000, 4)
# 2.gt_assignment表示這2000個proposal跟哪個obj的overlap最大,(2000,)
# 3.labels = gt_boxes[gt_assignment, 4],gt_boxes的尺寸是(len(objs), 5),所以labels是每一個proposal對應的object的cls,(2000,)
# 4.fg_inds是最大IOU大於等於0.5的index,bg_inds是最大IOU大於等於0.1小於0.5的index
# 5.調整fg_inds和bg_inds,使他們的個數相加為rois_per_image(256)
# 6.保留labels中的fg_inds和bg_inds,(256,)
# 7.從fg_rois_per_image到結束是背景位置,設定label的值為0,而此時前景的label仍然是cls id
# 8.計算出proposal與gt_box的相對位移dx,dy,dw,dh,放入bbox_target_data並將label列放入第一列的位置,尺寸是(256, 5)
# 9.建立尺寸為(256, 84)的bbox_targets和bbox_inside_weights,84是因為有21個類,每個類留4個位置
# 經過計算後返回的rois尺寸為(256, 5)
rois, _ = self._proposal_target_layer(rois, roi_scores, "rpn_rois")

4.呼叫_crop_pool_layer實現RoI Pooling層,因為proposal的尺寸各不相同,如果要送入region calssification會計算出問題,所以這裡將他們統一尺寸。原理上是將每個proposal對應到feature map上的位置,然後劃分成同樣的尺寸比如7*7,這樣在49個區域裡面進行max pooling,就可以將所有proposal轉成7x7的尺寸。最後的返回值是(256, 7, 7, 512)

5.呼叫_head_to_tail繼續構建網路,最後的fc7尺寸為(256, 4096)

  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

6.呼叫_region_classification搭建proposal的分類網路,cls_score的尺寸為(256, 21),cls_prob的尺寸為(256, 21),cls_pred的尺寸為(256,),bbox_pred的尺寸為(256, 84)

  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

至此,_build_network的所有工作就完成了,返回值是rois, cls_prob, bbox_pred,但是還儲存了很多中間變數放在了self._predictions中。

再看_add_losses

首先是RPN class loss,RPN的loss都是相對anchors來計算的。

      # 是RPN網路中由feature map通過卷積計算生成的(1, 38, 50, 18)reshape得到的,尺寸是(9*38*50, 2)
      rpn_cls_score = tf.reshape(self._predictions['rpn_cls_score_reshape'], [-1, 2])
      # (9*38*50,)
      rpn_label = tf.reshape(self._anchor_targets['rpn_labels'], [-1])
      # 找出rpn_label中不為-1的部分,-1表示not care的資料
      rpn_select = tf.where(tf.not_equal(rpn_label, -1))
      # 根據rpn_select找出rpn_cls_score的對應位置
      rpn_cls_score = tf.reshape(tf.gather(rpn_cls_score, rpn_select), [-1, 2])
      # 根據rpn_select找出rpn_label的對應位置
      rpn_label = tf.reshape(tf.gather(rpn_label, rpn_select), [-1])
      # 計算cross entropy loss
      rpn_cross_entropy = tf.reduce_mean(
        tf.nn.sparse_softmax_cross_entropy_with_logits(logits=rpn_cls_score, labels=rpn_label))

其次是RPN bbox loss

      # (1, 38, 50, 36)
      rpn_bbox_pred = self._predictions['rpn_bbox_pred']
      # (1, 38, 50, 36)
      rpn_bbox_targets = self._anchor_targets['rpn_bbox_targets']
      # (1, 38, 50, 36)
      rpn_bbox_inside_weights = self._anchor_targets['rpn_bbox_inside_weights']
      # (1, 38, 50, 36)
      rpn_bbox_outside_weights = self._anchor_targets['rpn_bbox_outside_weights']
      # l1 loss
      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,RCNN的loss都是相對proposal

      # (256, 21)
      cls_score = self._predictions["cls_score"]
      # (256,)
      label = tf.reshape(self._proposal_targets["labels"], [-1])
      # cross entropy loss
      cross_entropy = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits=cls_score, labels=label))

最後是RCNN bbox loss

      # (256, 84)
      bbox_pred = self._predictions['bbox_pred']
      # (256, 84)
      bbox_targets = self._proposal_targets['bbox_targets']
      # (256, 84)
      bbox_inside_weights = self._proposal_targets['bbox_inside_weights']
      # (256, 84)
      bbox_outside_weights = self._proposal_targets['bbox_outside_weights']
      # l1 loss
      loss_box = self._smooth_l1_loss(bbox_pred, bbox_targets, bbox_inside_weights, bbox_outside_weights)

total_loss就是將以上4個loss相加,我們要優化的就是total_loss的值。

總結一下RPN和RCNN的loss:

模型預測

1.demo.py

從demo.py入手看如何預測一張圖片。

當執行demo.py指令碼的時候,會執行main下面的程式碼。

主要是準備session,呼叫create_architecture構建網路,restore網路,呼叫demo預測

2.構建網路

函式create_architecture的實現主體和training時一樣,有以下幾個區別。

1.在構建網路的時候_region_proposal中只需要呼叫_proposal_layer,因為我們不需要構建loss,所以training中後面的步驟不需要。

    if is_training:
      rois, roi_scores = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
      rpn_labels = self._anchor_target_layer(rpn_cls_score, "anchor")
      # Try to have a deterministic order for the computing graph, for reproducibility
      with tf.control_dependencies([rpn_labels]):
        rois, _ = self._proposal_target_layer(rois, roi_scores, "rpn_rois")
    else:
      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

另外在_proposal_layer函式中,因為cfg_key為test,所以pre_nms_topN為6000,pos_nms_topN為300,因此我們的proposal有300個。

def proposal_layer(rpn_cls_prob, rpn_bbox_pred, im_info, cfg_key, _feat_stride, anchors, num_anchors):
  ......
  pre_nms_topN = cfg[cfg_key].RPN_PRE_NMS_TOP_N
  post_nms_topN = cfg[cfg_key].RPN_POST_NMS_TOP_N
  nms_thresh = cfg[cfg_key].RPN_NMS_THRESH

2.在create_architecture中需要對預測資料做處理

    if testing:
      stds = np.tile(np.array(cfg.TRAIN.BBOX_NORMALIZE_STDS), (self._num_classes))
      means = np.tile(np.array(cfg.TRAIN.BBOX_NORMALIZE_MEANS), (self._num_classes))
      self._predictions["bbox_pred"] *= stds
      self._predictions["bbox_pred"] += means

做這些處理是因為我們對target做過相反的處理

  if cfg.TRAIN.BBOX_NORMALIZE_TARGETS_PRECOMPUTED:
    # Optionally normalize targets by a precomputed mean and stdev
    targets = ((targets - np.array(cfg.TRAIN.BBOX_NORMALIZE_MEANS))
               / np.array(cfg.TRAIN.BBOX_NORMALIZE_STDS))

self._predictions["bbox_pred"]的尺寸是(300, 84),就是我們前面選出來的300個proposal。

3.demo函式

im_detect就是進行預測的主要程式碼,生成的scores shape是(300, 21),boxes的shape是(300, 84)

scores, boxes = im_detect(sess, net, im)

然後對除了__background__以外的每個類別單獨分析

    for cls_ind, cls in enumerate(CLASSES[1:]):
        cls_ind += 1 # because we skipped background
        #將對應class的box挑選出來,(300, 4)
        cls_boxes = boxes[:, 4*cls_ind:4*(cls_ind + 1)]
        #將對應class的分數挑選出來,(300, 1)
        cls_scores = scores[:, cls_ind]
        #合併成(300, 5)的資料score放在最後
        dets = np.hstack((cls_boxes,
                          cls_scores[:, np.newaxis])).astype(np.float32)
        #keep表示通過nms挑選出來的index,比如挑選出來30個
        keep = nms(dets, NMS_THRESH)
        #取出挑選出來的dets,(30, 5)
        dets = dets[keep, :]
        #將dets中score大於0.8的框保留下來畫在圖片上,這樣就拿到了bbox和score和class id
        im = vis_detections(im, cls, dets, thresh=CONF_THRESH)

4.im_detect預測

呼叫net.test_image進行預測

  def test_image(self, sess, image, im_info):
    feed_dict = {self._image: image,
                 self._im_info: im_info}

    cls_score, cls_prob, bbox_pred, rois = sess.run([self._predictions["cls_score"],
                                                     self._predictions['cls_prob'],
                                                     self._predictions['bbox_pred'],
                                                     self._predictions['rois']],
                                                    feed_dict=feed_dict)
    return cls_score, cls_prob, bbox_pred, rois

然後再根據相對座標計算出真實座標

  if cfg.TEST.BBOX_REG:
    # Apply bounding-box regression deltas
    box_deltas = bbox_pred
    pred_boxes = bbox_transform_inv(boxes, box_deltas)
    pred_boxes = _clip_boxes(pred_boxes, im.shape)

預測完成。

以上為本文所有內容,感謝閱讀,歡迎留言。