(原創)tensorflow目標檢測框架(object detection api)源碼細粒度剖析


一.前言

Tensorflow 推出的 Object Detection API是一套抽象程度極高的目標檢測框架,可以快速用於生產部署。但網絡上大多數相關的中英文文章均只局限於應用層面的分析,對於該套框架的算法實現源碼沒有針對性的分析文章。對於選擇tensorflow作為入門框架的深度學習新手,不僅應注重於算法本身的理解,更應注重算法的編碼實現。本人也是剛入門深度學習的新手,深深困擾於tensorflow 目標檢測框架的抽象代碼,因此花費了大量時間分析源碼,希望能對讀者有益,同時受限於眼界,文章中必然存在有錯誤或不得其義的理解,歡迎各位指正。

二. 目標檢測算法簡介

Object Detection API實現了多種目標檢測算法,包括faster-rcnn, rfcn, ssd, mask-rcnn等。本文針對於ssd算法的具體算法進行分析。其他算法可相應進行分析。
ssd算法的原文鏈接如下:
[https://arxiv.org/abs/1512.02325]
對ssd算法實現分析較好的文章有:

  1. [http://www.cnblogs.com/xuanyuyt/p/7222867.html#_label0]
  2. [https://zhuanlan.zhihu.com/p/24954433]
  3. [https://zhuanlan.zhihu.com/p/29410169]

三. Object Detection API簡介

1.模型總體架構簡介
tensorflow object dectection API下的所有模型必須實現DetectionModel接口(請參閱完整定義object_detection/core/model.py),每一個模型需要實現如下五個功能:

2.模型配置文件簡介
tensorflow dectection API采用protobuf文件來管理模型參數的配置,通過這種方式實現了多種算法的參數靈活更換。但也是因為這種參數配置方式,加大了分析源碼的難度。
詳細的參數配置文檔見[https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/configuring_jobs.md]

四. ssd算法源碼解析

現在進入正題,分析ssd算法的源碼實現。本文采用的ssd算法的特征提取層采用mobilenetv1,其他的特征提取層如mobilenetv2, resnet,inception等可以同樣分析。關於特征提取層的網絡代碼實現本文不詳細描述,有時間再開一篇博文解讀。
首先附上ssd+mobilenetv1在驗證階段的tensorboard模塊圖:

ssd+mobilenetv1 tensorflow模塊圖 Figure 1. ssd+mobilenetv1 tensorflow模塊圖
該模塊圖雖然因凍結去除了Loss模塊等模塊,但還是能看出整體的數據流。至於在訓練階段的模塊圖,由於太過雜亂,本文暫不放置。

A) ssd算法概述

ssd算法是一種直接預測目標類別和bounding box的多目標檢測算法。與傳統的faster rcnn相比,該算法沒有生成 region proposal 的過程,因此極大提高了檢測速度。
ssd算法的結構如下:

  1. SSD模型的第一環節是特征提取。特征提取可以采用主流的一些卷積模型(如VGG,Inception等),特征提取時的不同卷積層feature map的輸出將同時送到到下一環節”檢測“。
  2. SSD模型的第二環節是檢測。檢測環節采用一系列的小卷積模塊(3*31*1)來預測物體的類別與坐標。由於上一層輸入的不同層數的feature map有不同的感受野,因此檢測環節可以認為是對不同尺寸的圖像進行回歸和分類。檢測環節可以細分成如下幾個子模塊。
  • box generator: 針對不同卷積層(如19*1910*10)的feature map cell(feature map中的每個小格子),產生不同尺寸(scale)、不同縱橫比(aspect ratios)的default boxes。
  • classification: 上述的default boxes通過classification預測對應feature map cell的類別(C+1類別, C為所有分類,1為背景)
  • localization:上述的default boxes通過localization預測對應feature map cell的坐標
  1. SSD模型的第三環節是損失計算。該環節主要用於訓練過程,損失函數包括Classification loss 和Localization losses。通過損失的最小化,縮短Classification 和Localization的預測誤差。
  2. SSD模型的第四環節是后處理。 該環節主要用於驗證過程,通過NMS(非極大值抑制)篩選出置信度最高、存在目標的區域。

注:后文出現的default box和anchor, anchor box的意思均指同一個box。
此處附上論文中的SSD模塊框架圖,幫助理解。

SSD模塊框架 Figure 2. ssd論文模塊框架

B) ssd訓練過程源碼分析

Train總體流程分析

ssd訓練流程從Train.py中main()開始,

  1. 首先通過get_configs_from_pipeline_file()獲得"model", "train", ”input“的相關protobuf配置參數。 本文采用"ssd_mobilenet_v1_pets.config"加以分析。該config文件的詳情見附錄。
  2. 固定相關參數,生成"model", ”input“的偏函數,便於調用。其中input的數據處理框架采用dataset機制,可以高效地向模型輸入數據。這種機制比Reader的方式要高效很多,具體輸入線程機制對比可參考美團技術團隊的博文[https://www.toutiao.com/i6540566996331790852/?iid=29552265324&app=news_article&timestamp=1522848438].
  3. 調用Trainer.train(create_input_dict_fn, model_fn, train_config...)。進行創建ssd模型,創建input輸入流,創建算法主流程, 選擇訓練優化器, 計算total loss和梯度, 更新梯度,靜態圖所有操作節點添加好后,調用slim.learning.train()輸入數據流進行反復訓練迭代。
    相應的流程圖例如下:
Train總體流程 Figure 3. Train總體流程
我們將目光聚焦到核心算法的實現,因此本文主要分析創建ssd模型(model_builder.build())和創建算法主流程(model_deploy.create_clones())這兩個步驟。

創建ssd模型分析

主體模型的選擇在model_builder.build()中,由於選擇的是ssd模型,因此在model_builder._build_ssd_model(ssd_config, is_training, add_summaries)中構建ssd模型。其中輸入的ssd_config參數即為從"ssd_mobilenet_v1_pets.config"中提取出來的參數。
模型配置主要涉及到如下幾個方面:

  1. 特征提取層(feature_extractor)
  2. box_coder(不知道何用,注釋說 Scales location targets as used in paper for joint training,應該和faster-rcnn有關)
  3. 匹配規則(matcher)
  4. 區域相似度度量規則(region_similarity_calculator)
  5. 卷積預測層模塊參數(ssd_box_predictor)
  6. 預測用的系列defaut_boxes(anchor_generator)
  7. 圖像后處理(non_max_suppression_fn, score_conversion_fn,只用於驗證流程,在訓練流程中不使用
  8. 損失函數及難樣本挖掘規則(classification_loss、localization_loss、hard_example_miner)。
    相應的流程圖例如下:
創建ssd模型 Figure 4. 創建ssd模型

模型參數配置完后實例化ssd_meta_arch.SSDMetaArch(), SSDMetaArch()即為繼承自DetectionModel()的類,其本身還有preprocess, _compute_clip_window(), predict()、postprocess()等方法。
所以說ssd_meta_arch.SSDMetaArch()是整個框架中最重要的數據結構
因此簡單將SSDMetaArch()的方法整理一下,如下圖:

SSDMetaArch()數據結構 Figure 5. SSDMetaArch()數據結構

對anchor核心參數計算的分析

上一節創建ssd模型分析說到模型配置涉及的幾個主要方面,其中的anchor_generator需要着重分析,因為default_boxes的生成方法是論文的核心。因此對照源碼,分析anchor生成的編碼實現流程。這段代碼基本與原文的公式對應,通過create_ssd_anchors()生成了特征提取層每層feature map需要使用到的anchor的aspect_ratio和scale參數。具體的anchor生成過程會在后續分析。

def create_ssd_anchors(num_layers=6, min_scale=0.2,
                       max_scale=0.95,
                       scales=None,
                       aspect_ratios=(1.0, 2.0, 3.0, 1.0 / 2, 1.0 / 3),
                       interpolated_scale_aspect_ratio=1.0,
                       base_anchor_size=None,
                       anchor_strides=None,
                       anchor_offsets=None,
                       reduce_boxes_in_lowest_layer=True):

  #base_anchor_size = [1.0,1,0]
  base_anchor_size = tf.constant(base_anchor_size, dtype=tf.float32)
  box_specs_list = []

  #min_scale = 0.2,  max_scale = 0.95, num_layers = 6, 這三個參數在"ssd_mobilenet_v1_pets.config"中定義
  #scales = [0.2, 0.35 , 0.5, 0.65 0.80, 0.95, 1]
  scales = [min_scale + (max_scale - min_scale) * i / (num_layers - 1)
              for i in range(num_layers)] + [1.0]

  for layer, scale, scale_next in zip(
      range(num_layers), scales[:-1], scales[1:]):
    layer_box_specs = []
    if layer == 0 and reduce_boxes_in_lowest_layer:
      #layer =0時,只有三組box, 如何選取??
      layer_box_specs = [(0.1, 1.0), (scale, 2.0), (scale, 0.5)]
    else:

      #aspect_ratios= [1.0, 2.0 , 0.5, 3.0, 0.3333]
      #aspect_ratios可在"ssd_mobilenet_v1_pets.config"中自行定義
      for aspect_ratio in aspect_ratios:
        layer_box_specs.append((scale, aspect_ratio))

      #多增加一個anchor, aspect ratio=1, scale是現在的scale與下一層scale的乘積開平方。
      if interpolated_scale_aspect_ratio > 0.0:
        layer_box_specs.append((np.sqrt(scale*scale_next),
                                interpolated_scale_aspect_ratio))
    box_specs_list.append(layer_box_specs)

  return MultipleGridAnchorGenerator(box_specs_list, base_anchor_size,
                                     anchor_strides, anchor_offsets)

算法主流程分析

Trainer.train()中的model_deploy.create_clones(deploy_config, model_fn, [input_queue])可將將輸入數據流及模型算法布署到多台主機進行分布式訓練,本文只關心其模型算法的布署細節,因此只關心其調用的Trainer._create_losses()方法細節。
Trainer._create_losses()分成如下四個流程:

  1. 獲得一定batch的圖片及對應的真實box的坐標、分類標簽。
  2. 圖片預處理,將圖片縮放成300*300
  3. 對圖片進行預測
  4. 計算損失
    相應的流程圖例如下:
create_loss主流程 Figure 6. create_loss主流程

流程三預測及流程四計算損失是整個算法的核心,因此接下來着重這兩部分的分析。

predict流程分析

預測流程從SSDMetaArch.predict()開始調用, 可以分成如下四個流程:

  1. 特征提取: mobilenetv1的特征提取層提取出不同層的feature map, 一共獲得6層。(6層featuremap的尺寸是[(19,19) (10,10) (5,5) (3,3) (2,2) (1,1)])
  2. anchor生成:根據各個特征提取層的尺寸及之前確定的scales, aspect ratios參數生成1917個anchor。(6層featuremap的單個cell對應anchor個數為[3,6,6,6,6,6])
    $$1917=19193+10106 + 556 + 336+226+116$$
  3. 對每一層feature map分別預測其anchor的分類及坐標.
  4. 將所有feature map的anchor的分類及坐標整合在一起。
    相應的流程圖例如下:
predict總體流程 Figure 7. predict總體流程

對特征提取層的分析實際就是分析對應的mobilenetv1的結構,本文不作深入分析。預測anchor的分類及坐標的代碼比較清晰,就是簡單的多次卷積操作,因此本文不做詳細說明。
因此重點分析如何生成1917個anchor。隨后在計算損失的環節anchor的真實分類及坐標標簽將會與預測分類及坐標值對比,從而計算出總的損失值。

1). anchor_generator分析

anchor generator的流程會生成每個feature map對應的所有anchor,然后將所有anchor串聯起來。因此我們以其中一個feature map(19,19)為例,分析該feature map上使用的anchor的生成細節。生成anchor的細節在GridAnchorGenerator.tile_anchors()中。
生成anchor的步驟如下:

  1. 通過調用MultipleGridAnchorGenerator.create_ssd_anchors()計算出了每一層使用的anchor的scale和aspect ratio,因此可以計算出第k個feature map下所使用的anchor的長和寬,公式分別為\(h_k^a = {s_k}/\sqrt {{a_r}}\)\(w_k^a = {s_k}\sqrt {{a_r}}\)。k=0,1...5
  2. 確定所有anchor的中心位置的縱坐標和橫坐標。中心位置的公式為\((\frac{{i + 0.5}}{{\left| {{f_k}} \right|}},\frac{{j + 0.5}}{{\left| {{f_k}} \right|}})\),其中\(i,j \in [0,\left| {{f_k}} \right|)\)
  3. 生成anchor的三維網格坐標。
  4. 計算獲得所有anchor的bbox_centetrs與bbox_sizes坐標,shape均為(1083,2),其中每個bbox_center坐標均為(y_center,x_center)形式,每個bbox_sizes坐標均為(height,width)形式。
  5. 計算獲得所有anchor的坐標,shape為(1083,4)。其中每個anchor的坐標形式均為(ymin,xmin,ymax,xmax)。
    相應的流程圖例如下:
anchor核心參數確定 Figure 8. anchor核心參數確定
*PS: bbox_corners里有負坐標出現??這是怎么回事?這個問題還沒研究透*

loss流程分析

預測流程從ssd_meta_arch.SSDMetaArch.loss()開始調用, 可以分成如下五個流程:

  1. 所有anchor與真實box按照IOU進行配對,返回anchor對應的groundth box的坐標及分類標簽。
  2. 計算每一個anchor的location loss,注意anchor只有對應groundth box才會有有效的location loss。
  3. 計算每一個anchor的classification loss,每個anchor都會有有效的classification loss,包括對應背景標簽的anchor。
  4. 由於正負樣本的不均衡,因此對loss進行難樣本挖掘(apply_hard_mining),保證正負樣本比例在1:3。
  5. 根據公式對location loss和classification loss進行標幺化。
    相應的流程圖例如下:
loss總體流程 Figure 9. loss總體流程

loss計算在算法里是最核心的部分,同時也是分析難度最大的部分,為了盡可能描述清楚loss的編碼流程,這一部分的每個流程都會詳細分析,並輔以必要的代碼分析。
1)matching strategy
第一部分的matching strategy指的是每個anchor與groundth box的配對。也就是對所有anchor區分出正樣本和負樣本。這部分的原理如論文所述,截圖如下:

matching_strategy
matching strategy一共做了兩個操作: * 對每個groundth box,選出與其交並比最大(也就是重疊度最高)的default box, 一一配對。 * 對剩下的default boxes, 找出與其交並比最大且大於0.5的groundth box,一一配對。 圍繞着matching strategy,代碼實現上轉換了一下思路。 先貼相應的流程圖例:
matching_strategy總體流程 Figure 10. matching_strategy總體流程
由流程圖可以看出,代碼會單獨對batch size中的每張圖片進行匹配規則,然后將結果堆疊起來。因此我們分析每張圖片的匹配代碼。 ``` # 計算groundtruth_boxes與anchors之間的交並比,match_quality_matrix的shape是(groundtruth_boxes_num, 1917) match_quality_matrix = self._similarity_calc.compare(groundtruth_boxes, anchors)
  # 運行matching strategy, 獲得每個anchorc對應的groundth box編號 match(1917, )
  match = self._matcher.match(match_quality_matrix, **params)

  # 獲得每個anchor匹配的正樣本groundtruth box與anchor的坐標差距,即論文中的$\widehat g_j^m$,無效或負樣本(背景)用[0 0 0 0],  reg_targets(1917,4)
  reg_targets = self._create_regression_targets(anchors, groundtruth_boxes, match)

  # 獲得每個anchor匹配的正樣本groundtruth box的分類  cls_targets (1917, num_classes + 1)
  cls_targets = self._create_classification_targets(groundtruth_labels, match)
  
  #表示anchor是否對應一個groundtruth box,是1,否0,用於計算location loss。 reg_weights (1917,)
  reg_weights = self._create_regression_weights(match, groundtruth_weights)

  #表示anchor是否對應一個groundtruth box或者背景,是1,否0,用於計算classification loss。 cls_weights (1917,)
  cls_weights = self._create_classification_weights(match, groundtruth_weights)

限於篇幅,我們着重關注match和reg_targets的源碼實現。

##### A) match細節(ArgMaxMatcher._match(self, similarity_matrix))
先貼相應的流程圖例:
<center>
<img src="https://images2018.cnblogs.com/blog/1410422/201805/1410422-20180530183614537-1391787123.png" width="100%" alt="match細節" align=center/>
Figure 11. match細節
</center>

從match細節實現上,可以看出匹配思路與論文中的稍微不一樣。編碼思路如下:
1. 首先獲得每個default box對應的最匹配groundtruth box。
2. 然后再將其中IOU<unmatched_threshold(0.5)的default box重對應成unmatched(即標記為-1)。 由於"ssd_mobilenet_v1_pets.config" 中定義unmatched_threshold = matched_threshold = 0.5, 因此不會有default box 對應ignored(-2)。  如果unmatched_threshold != matched_threshold,由可能會有default box 對應ignored標記, 這種情況本文不加以分析,有興趣自行分析。<font color=#ff0000 size=5 face="黑體">(注:Yolo v3論文中提及了這種Dual IOU threshold方式,談及是faster rcnn里使用,但似乎我在faster rcnn論文中沒有找到這一點,不清楚雙閾值的設定好處如何??不知道是否有網友幫忙解惑。)</font>
3. 由於流程2的閾值的引入,可能會出現某個groundtruth box無對應的default box的特殊情況出現,因此強行保證每個groundtruth box能至少對應上一個default box。

##### B) reg_targets細節(TargetAssigner._create_regression_targets(anchors, groundtruth_boxes,match))
先貼相應的流程圖例:
<center>
<img src="https://images2018.cnblogs.com/blog/1410422/201805/1410422-20180530190408321-1952954892.png" width="100%" alt="reg_targets" align=center/>
Figure 12. reg_targets
</center>

reg_targets的主要目的是獲得每個anchor匹配的正樣本groundtruth box與anchor的坐標差距,即論文中的$\widehat g_j^m$。此處將論文中的相關公式貼出
$$\begin{array}{l}
\widehat g_j^{cx} = (g_j^{cx} - d_i^{cx})/d_i^w\\
\widehat g_j^{cy} = (g_j^{cy} - d_i^{cy})/d_i^h\\
\widehat g_j^w = \log (\frac{{g_j^w}}{{d_i^w}})\\
\widehat g_j^h = \log (\frac{{g_j^h}}{{d_i^h}})
\end{array}$$

**2)localization_loss**
第二部分的localization_loss 計算每個anchor的location_loss,貼出論文中的相關公式:
$$\sum\limits_{m \in \{ cx,cy,w,h\} } {x_{ij}^ksmoot{h_{L1}}(l_i^m - \widehat g_j^m)} $$
貼上相應的流程圖例:
<center>
<img src="https://images2018.cnblogs.com/blog/1410422/201805/1410422-20180530193320854-645632651.png" width="50%" alt="localization_loss" align=center/>
Figure 13. localization_loss
</center>
需要注意的是這里的localizatio_loss計算了每個anchor的loss,由於負樣本遠遠高於正樣本,因此后續還有難樣本挖掘算法挑選出1:3的正負樣本以計算總的localizatio_loss。

**3)classification_loss**
第三部分的classification_loss 計算每個anchor的classification_loss,貼出論文中的相關公式:
$$\left\{ \begin{array}{l}
x_{ij}^p\log (\widehat c_i^p){\rm{  }}(i \in Pos)\\
\log (\widehat c_i^0){\rm{ }}(i \in Neg)
\end{array} \right.where{\rm{   }}\widehat c_i^p = \frac{{\exp (c_i^p)}}{{\sum\limits_p {\exp (c_i^p)} }}$$
貼上相應的流程圖例:
<center>
<img src="https://images2018.cnblogs.com/blog/1410422/201805/1410422-20180530194350671-392266190.png" width="50%" alt="classification_loss" align=center/>
Figure 14. classification_loss
</center>
需要注意的是tensorflow官方給的實現classification_loss使用的是**WeightedSigmoidClassificationLoss**,而不是論文里的softmax_loss。 當然源碼里是提供了WeightedSoftmaxClassificationLoss供訓練。但官方放出的源碼采用WeightedSigmoidClassificationLoss,是否意味着此處用sigmoid loss比softmax loss的訓練效果更好??? <font color=#ff0000 size=3 face="黑體">(注:yolo v3論文中談及 softmax的使用對性能沒有好處,說softmax基於一個box只有一個類別的假設。官方不使用softmax而使用sigmoidloss,也是為了適用於每個類別相互獨立但互不排斥的情況)</font>

**4)apply hard mining**
第四部分的apply hard mining對正負樣本進行難樣本挖掘,從而挑選出1:3的正負樣本。首先看一下論文對難樣本挖掘的方法介紹。
<center>
<img src="https://images2018.cnblogs.com/blog/1410422/201805/1410422-20180530195031062-1961879128.png" width="60%" alt="paper_hard_mining" align=center/>
</center>
論文里介紹的難樣本挖掘技術比較簡單,缺乏細節。
因此結合源碼對難樣本挖掘的細節進行分析。
首先貼上相應的流程圖例:
<center>
<img src="https://images2018.cnblogs.com/blog/1410422/201805/1410422-20180530210125889-1982435200.png" width="90%" alt="hard_mining" align=center/>
Figure 15. hard_mining
</center>

源碼采用的難樣本挖掘技術分成以下幾步:
>1. 采用非大值抑制貪婪算法,在最多取3000張樣本的情況取出分類置信度最高的anchor。每選出一個高置信度的anchor,都會將與其iou大於一定閾值的anchor剔除。 具體的非大值抑制貪婪算法實現源碼可參考
   [https://www.pyimagesearch.com/2014/11/17/non-maximum-suppression-object-detection-python/] 及[https://www.pyimagesearch.com/2015/02/16/faster-non-maximum-suppression-python/]。
>2. 對挑選出來的anchor按差不多1:3的比例挑選正負樣本。其中正樣本的數量是確定的,負樣本的數量可能是正樣本的3倍,也可能限制於總樣本的數量。

**4)標幺化**
第五部分標幺化比較簡單,就是計算出難樣本挖掘后正樣本的個數,loss除以這個系數即可,附上論文公式。
$$L(x,c,{\mathop{\rm l}\nolimits} ,g) = \frac{1}{N}({L_{conf}}(x,c) + \alpha {L_{loc}}(x,l,g))$$
注意公式中的$\alpha $似乎在源碼中直接取了1。

## C) ssd驗證過程源碼分析
驗證過程相對於訓練過程而言多了一個postprocess的模塊處理,這部分源碼之后再進行分析。
未完待續

## D) ssd算法總結:
看完上文對ssd算法源碼分析,讀者是否對ssd算法有了細致入微的理解? 我歸納了ssd算法需要理解的幾個核心論點(啟發於[https://becominghuman.ai/tensorflow-object-detection-api-basics-of-detection-7b134d689c75]),能夠解讀這幾個問題,就差不多能掌握ssd算法的算法實現。讀者可以對照前面的分析,對不理解的地方再進行研究。
1. anchor box的生成算法。
2. matching strategy的算法。
3. training loss的算法。
4. 非極大值抑制的算法
5. 難樣本挖掘的算法。 


<div style="background:#FFF68F; color:#0; font-size:small;">
    <p >
            作者: 
            <a href="http://www.cnblogs.com/HaijunLv/">HaijunLv</a>
    </p>
    <p >
            出處:
            <a href="https://www.cnblogs.com/HaijunLv/p/9101957.html">https://www.cnblogs.com/HaijunLv/p/9101957.html></a>
    </p>
    <p >
            關於作者:專注深度學習領域,請多多賜教!
    </p>

    <p >
             本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出,
             <a href="#" onclick="Curgo()" style="background:#b6ff00; color:#0; font-size:medium;">原文鏈接</a>
             如有問題, 可郵件(248354172@qq.com)咨詢.
    </p>


    <script type="text/javascript">
     function    Curgo()   
     {   
         window.open(window.location.href);
     }   
    </script>
</div>
轉載請注明出處


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM