1. 程式人生 > >【深度學習SSD】——深刻解讀SSD tensorflow及原始碼詳解

【深度學習SSD】——深刻解讀SSD tensorflow及原始碼詳解

  1. <code class="language-python"># Copyright 2016 Paul Balanca. All Rights Reserved.  
  2. #  
  3. # Licensed under the Apache License, Version 2.0 (the "License");  
  4. # you may not use this file except in compliance with the License.  
  5. # You may obtain a copy of the License at  
  6. #  
  7. #     http://www.apache.org/licenses/LICENSE-2.0  
  8. #  
  9. # Unless required by applicable law or agreed to in writing, software  
  10. # distributed under the License is distributed on an "AS IS" BASIS,  
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  
  12. # See the License for the specific language governing permissions and  
  13. # limitations under the License.  
  14. # ==============================================================================  
  15. """Definition of 300 VGG-based SSD network. 
  16. This model was initially introduced in: 
  17. SSD: Single Shot MultiBox Detector 
  18. Wei Liu, Dragomir Anguelov, Dumitru Erhan, Christian Szegedy, Scott Reed, 
  19. Cheng-Yang Fu, Alexander C. Berg 
  20. https://arxiv.org/abs/1512.02325 
  21. Two variants of the model are defined: the 300x300 and 512x512 models, the 
  22. latter obtaining a slightly better accuracy on Pascal VOC. 
  23. Usage: 
  24.     with slim.arg_scope(ssd_vgg.ssd_vgg()): 
  25.         outputs, end_points = ssd_vgg.ssd_vgg(inputs) 
  26. This network port of the original Caffe model. The padding in TF and Caffe 
  27. is slightly different, and can lead to severe accuracy drop if not taken care 
  28. in a correct way! 
  29. In Caffe, the output size of convolution and pooling layers are computing as 
  30. following: h_o = (h_i + 2 * pad_h - kernel_h) / stride_h + 1 
  31. Nevertheless, there is a subtle difference between both for stride > 1. In 
  32. the case of convolution: 
  33.     top_size = floor((bottom_size + 2*pad - kernel_size) / stride) + 1 
  34. whereas for pooling: 
  35.     top_size = ceil((bottom_size + 2*pad - kernel_size) / stride) + 1 
  36. Hence implicitely allowing some additional padding even if pad = 0. This 
  37. behaviour explains why pooling with stride and kernel of size 2 are behaving 
  38. the same way in TensorFlow and Caffe. 
  39. Nevertheless, this is not the case anymore for other kernel sizes, hence 
  40. motivating the use of special padding layer for controlling these side-effects. 
  41. @@ssd_vgg_300 
  42. """  
  43. import math  
  44. from collections import namedtuple  
  45. import numpy as np  
  46. import tensorflow as tf  
  47. import tf_extended as tfe  
  48. from nets import custom_layers  
  49. from nets import ssd_common  
  50. slim = tf.contrib.slim  
  51. # =========================================================================== #  
  52. # SSD class definition.  
  53. # =========================================================================== #  
  54. #collections模組的namedtuple子類不僅可以使用item的index訪問item,還可以通過item的name進行訪問可以將namedtuple理解為c中的struct結構,其首先將各個item命名,然後對每個item賦予資料  
  55. SSDParams = namedtuple('SSDParameters', ['img_shape',           #輸入影象大小  
  56.                                          'num_classes',         #分類類別數  
  57.                                          'no_annotation_label', #無標註標籤  
  58.                                          'feat_layers',         #特徵層  
  59.                                          'feat_shapes',         #特徵層形狀大小  
  60.                                          'anchor_size_bounds',  #錨點框大小上下邊界,是與原圖相比得到的小數值  
  61.                                          'anchor_sizes',        #初始錨點框尺寸  
  62.                                          'anchor_ratios',       #錨點框長寬比  
  63.                                          'anchor_steps',        #特徵圖相對原始影象的縮放  
  64.                                          'anchor_offset',       #錨點框中心的偏移  
  65.                                          'normalizations',      #是否正則化  
  66.                                          'prior_scaling'        #是對特徵圖參考框向gtbox做迴歸時用到的尺度縮放(0.1,0.1,0.2,0.2)  
  67.                                          ])  
  68. class SSDNet(object):  
  69.     """Implementation of the SSD VGG-based 300 network. 
  70.     The default features layers with 300x300 image input are: 
  71.       conv4 ==> 38 x 38 
  72.       conv7 ==> 19 x 19 
  73.       conv8 ==> 10 x 10 
  74.       conv9 ==> 5 x 5 
  75.       conv10 ==> 3 x 3 
  76.       conv11 ==> 1 x 1 
  77.     The default image size used to train this network is 300x300.    #訓練輸入影象尺寸預設為300x300 
  78.     """  
  79.     default_params = SSDParams(             #預設引數  
  80.         img_shape=(300, 300),  
  81.         num_classes=21,  #包含背景在內,共21類目標類別  
  82.         no_annotation_label=21,  
  83.         feat_layers=['block4', 'block7', 'block8', 'block9', 'block10', 'block11'], #特徵層名字  
  84.         feat_shapes=[(38, 38), (19, 19), (10, 10), (5, 5), (3, 3), (1, 1)],       #特徵層尺寸  
  85.         anchor_size_bounds=[0.15, 0.90],                                             
  86.         # anchor_size_bounds=[0.20, 0.90],                                        #論文中初始預測框大小為0.2x300~0.9x300;實際程式碼是[45,270]  
  87.         anchor_sizes=[(21., 45.),   #直接給出的每個特徵圖上起初的錨點框大小;如第一個特徵層框大小是h:21;w:45;  共6個特徵圖用於迴歸  
  88.                       (45., 99.),   #越小的框能夠得到原圖上更多的區域性資訊,反之得到更多的全域性資訊;  
  89.                       (99., 153.),  
  90.                       (153., 207.),  
  91.                       (207., 261.),  
  92.                       (261., 315.)],  
  93.         # anchor_sizes=[(30., 60.),  
  94.         #               (60., 111.),  
  95.         #               (111., 162.),  
  96.         #               (162., 213.),  
  97.         #               (213., 264.),  
  98.         #               (264., 315.)],  
  99.         anchor_ratios=[[2, .5],             #每個特徵層上的每個特徵點預測的box長寬比及數量;如:block4: def_boxes:4  
  100.                        [2, .5, 3, 1./3],    #block7: def_boxes:6   (ratios中的4個+預設的1:1+額外增加的一個=6)  
  101.                        [2, .5, 3, 1./3],    #block8: def_boxes:6  
  102.                        [2, .5, 3, 1./3],    #block9: def_boxes:6  
  103.                        [2, .5],             #block10: def_boxes:4  
  104.                        [2, .5]],            #block11: def_boxes:4   #備註:實際上略去了預設的ratio=1以及多加了一個sqrt(初始框寬*初始框高),後面程式碼有  
  105.         anchor_steps=[8, 16, 32, 64, 100, 300],   #特徵圖錨點框放大到原始圖的縮放比例;  
  106.         anchor_offset=0.5,                        #每個錨點框中心點在該特徵圖cell中心,因此offset=0.5  
  107.         normalizations=[20, -1, -1, -1, -1, -1],  #是否歸一化,大於0則進行,否則不做歸一化;目前看來只對block_4進行正則化,因為該層比較靠前,其norm較大,需做L2正則化(僅僅對每個畫素在channel維度做歸一化)以保證和後面檢測層差異不是很大;  
  108.         prior_scaling=[0.1, 0.1, 0.2, 0.2]        #特徵圖上每個目標與參考框間的尺寸縮放(y,x,h,w)解碼時用到  
  109.         )  
  110.     def __init__(self, params=None):      #網路引數的初始化  
  111.         """Init the SSD net with some parameters. Use the default ones 
  112.         if none provided. 
  113.         """  
  114.         if isinstance(params, SSDParams):  #是否有引數輸入,是則用輸入的,否則使用預設的  
  115.             self.params = params           #isinstance是python的內建函式,如果引數1與引數2的型別相同則返回true;  
  116.         else:  
  117.             self.params = SSDNet.default_params  
  118.     # ======================================================================= #  
  119.     def net(self, inputs,                            #定義網路模型  
  120.             is_training=True,                        #是否訓練  
  121.             update_feat_shapes=True,                 #是否更新特徵層的尺寸  
  122.             dropout_keep_prob=0.5,                   #dropout=0.5  
  123.             prediction_fn=slim.softmax,              #採用softmax預測結果  
  124.             reuse=None,  
  125.             scope='ssd_300_vgg'):                    #網路名:ssd_300_vgg   (基礎網路時VGG,輸入訓練影象size是300x300)  
  126.         """SSD network definition. 
  127.         """  
  128.         r = ssd_net(inputs,                               #網路輸入引數r  
  129.                     num_classes=self.params.num_classes,   
  130.                     feat_layers=self.params.feat_layers,  
  131.                     anchor_sizes=self.params.anchor_sizes,  
  132.                     anchor_ratios=self.params.anchor_ratios,  
  133.                     normalizations=self.params.normalizations,  
  134.                     is_training=is_training,  
  135.                     dropout_keep_prob=dropout_keep_prob,  
  136.                     prediction_fn=prediction_fn,  
  137.                     reuse=reuse,  
  138.                     scope=scope)  
  139.         # Update feature shapes (try at least!)    #下面這步我的理解就是讓讀者自行更改特徵層的輸入,未必論文中介紹的那幾個block  
  140.         if update_feat_shapes:                                               #是否更新特徵層影象尺寸?  
  141.             shapes = ssd_feat_shapes_from_net(r[0], self.params.feat_shapes)  #輸入特徵層影象尺寸以及inputs(應該是預測的特徵尺寸),輸出更新後的特徵圖尺寸列表  
  142.             self.params = self.params._replace(feat_shapes=shapes)        #將更新的特徵圖尺寸shapes替換當前的特徵圖尺寸  
  143.         return r                                                      #更新網路輸入引數r    
  144.     def arg_scope(self, weight_decay=0.0005, data_format='NHWC'):  #定義權重衰減=0.0005,L2正則化項係數;資料型別是NHWC  
  145.         """Network arg_scope. 
  146.         """  
  147.         return ssd_arg_scope(weight_decay, data_format=data_format)  
  148.     def arg_scope_caffe(self, caffe_scope):  
  149.         """Caffe arg_scope used for weights importing. 
  150.         """  
  151.         return ssd_arg_scope_caffe(caffe_scope)        
  152.     # ======================================================================= #  
  153.     def update_feature_shapes(self, predictions):    #更新特徵形狀尺寸(來自預測結果)  
  154.         """Update feature shapes from predictions collection (Tensor or Numpy 
  155.         array). 
  156.         """  
  157.         shapes = ssd_feat_shapes_from_net(predictions, self.params.feat_shapes)  
  158.         self.params = self.params._replace(feat_shapes=shapes)  
  159.     def anchors(self, img_shape, dtype=np.float32):             #輸入原始影象尺寸;返回每個特徵層每個參考錨點框的位置及尺寸資訊(x,y,h,w)          
  160.         """Compute the default anchor boxes, given an image shape. 
  161.         """  
  162.         return ssd_anchors_all_layers(img_shape,                #這是個關鍵函式;檢測所有特徵層中的參考錨點框位置和尺寸資訊  
  163.                                       self.params.feat_shapes,  
  164.                                       self.params.anchor_sizes,   
  165.                                       self.params.anchor_ratios,  
  166.                                       self.params.anchor_steps,  
  167.                                       self.params.anchor_offset,  
  168.                                       dtype)  
  169.     def bboxes_encode(self, labels, bboxes, anchors,  #編碼,用於將標籤資訊,真實目標資訊和錨點框資訊編碼在一起;得到預測真實框到參考框的轉換值  
  170.                       scope=None):  
  171.         """Encode labels and bounding boxes.    
  172.         """  
  173.         return ssd_common.tf_ssd_bboxes_encode(  
  174.             labels, bboxes, anchors,  
  175.             self.params.num_classes,  
  176.             self.params.no_annotation_label,      #未標註的標籤(應該代表背景)  
  177.             ignore_threshold=0.5,                 #IOU篩選閾值  
  178.             prior_scaling=self.params.prior_scaling,  #特徵圖目標與參考框間的尺寸縮放(0.1,0.1,0.2,0.2)  
  179.             scope=scope)  
  180.     def bboxes_decode(self, feat_localizations, anchors,  #解碼,用錨點框資訊,錨點框與預測真實框間的轉換值,得到真是的預測框(ymin,xmin,ymax,xmax)  
  181.                       scope='ssd_bboxes_decode'):  
  182.         """Encode labels and bounding boxes. 
  183.         """  
  184.         return ssd_common.tf_ssd_bboxes_decode(  
  185.             feat_localizations, anchors,  
  186.             prior_scaling=self.params.prior_scaling,  
  187.             scope=scope)  
  188.     def detected_bboxes(self, predictions, localisations,   #通過SSD網路,得到檢測到的bbox  
  189.                         select_threshold=None, nms_threshold=0.5,  
  190.                         clipping_bbox=None, top_k=400, keep_top_k=200):  
  191.         """Get the detected bounding boxes from the SSD network output.     
  192.         """  
  193.         # Select top_k bboxes from predictions, and clip    #選取top_k=400個框,並對框做修建(超出原圖尺寸範圍的切掉)  
  194.         rscores, rbboxes = \                                                       #得到對應某個類別的得分值以及bbox  
  195.             ssd_common.tf_ssd_bboxes_select(predictions, localisations,  
  196.                                             select_threshold=select_threshold,  
  197.                                             num_classes=self.params.num_classes)  
  198.         rscores, rbboxes = \                                     #按照得分高低,篩選出400個bbox和對應得分  
  199.             tfe.bboxes_sort(rscores, rbboxes, top_k=top_k)  
  200.         # Apply NMS algorithm.                                   #應用非極大值抑制,篩選掉與得分最高bbox重疊率大於0.5的,保留200個  
  201.         rscores, rbboxes = \  
  202.             tfe.bboxes_nms_batch(rscores, rbboxes,  
  203.                                  nms_threshold=nms_threshold,  
  204.                                  keep_top_k=keep_top_k)  
  205.         if clipping_bbox is not None:  
  206.             rbboxes = tfe.bboxes_clip(clipping_bbox, rbboxes)  
  207.         return rscores, rbboxes                              #返回裁剪好的bbox和對應得分  
  208. #儘管一個ground truth可以與多個先驗框匹配,但是ground truth相對先驗框還是太少了,  
  209. #所以負樣本相對正樣本會很多。為了保證正負樣本儘量平衡,SSD採用了hard negative mining,  
  210. #就是對負樣本進行抽樣,抽樣時按照置信度誤差(預測背景的置信度越小,誤差越大)進行降序排列,  
  211. #選取誤差的較大的top-k作為訓練的負樣本,以保證正負樣本比例接近1:3  
  212.     def losses(self, logits, localisations,  
  213.                gclasses, glocalisations, gscores,  
  214.                match_threshold=0.5,  
  215.                negative_ratio=3.,  
  216.                alpha=1.,  
  217.                label_smoothing=0.,  
  218.                scope='ssd_losses'):  
  219.         """Define the SSD network losses. 
  220.         """  
  221.         return ssd_losses(logits, localisations,  
  222.                           gclasses, glocalisations, gscores,  
  223.                           match_threshold=match_threshold,  
  224.                           negative_ratio=negative_ratio,  
  225.                           alpha=alpha,  
  226.                           label_smoothing=label_smoothing,  
  227.                           scope=scope)  
  228. # =========================================================================== #  
  229. # SSD tools...  
  230. # =========================================================================== #  
  231. def ssd_size_bounds_to_values(size_bounds,  
  232.                               n_feat_layers,  
  233.                               img_shape=(300, 300)):  
  234.     """Compute the reference sizes of the anchor boxes from relative bounds. 
  235.     The absolute values are measured in pixels, based on the network 
  236.     default size (300 pixels). 
  237.     This function follows the computation performed in the original 
  238.     implementation of SSD in Caffe. 
  239.     Return: 
  240.       list of list containing the absolute sizes at each scale. For each scale, 
  241.       the ratios only apply to the first value. 
  242.     """  
  243.     assert img_shape[0] == img_shape[1]  
  244.     img_size = img_shape[0]  
  245.     min_ratio = int(size_bounds[0] * 100)  
  246.     max_ratio = int(size_bounds[1] * 100)  
  247.     step = int(math.floor((max_ratio - min_ratio) / (n_feat_layers - 2)))  
  248.     # Start with the following smallest sizes.  
  249.     sizes = [[img_size * size_bounds[0] / 2, img_size * size_bounds[0]]]  
  250.     for ratio in range(min_ratio, max_ratio + 1, step):  
  251.         sizes.append((img_size * ratio / 100.,  
  252.                       img_size * (ratio + step) / 100.))  
  253.     return sizes  
  254. def ssd_feat_shapes_from_net(predictions, default_shapes=None):  
  255.     """Try to obtain the feature shapes from the prediction layers. The latter 
  256.     can be either a Tensor or Numpy ndarray. 
  257.     Return: 
  258.       list of feature shapes. Default values if predictions shape not fully 
  259.       determined. 
  260.     """  
  261.     feat_shapes = []  
  262.     for l in predictions:              #l:是預測的特徵形狀  
  263.         # Get the shape, from either a np array or a tensor.  
  264.         if isinstance(l, np.ndarray):  #如果l是np.ndarray型別,則將l的形狀賦給shape;否則將shape作為list  
  265.             shape = l.shape  
  266.         else:  
  267.             shape = l.get_shape().as_list()  
  268.         shape = shape[1:4]  
  269.         # Problem: undetermined shape...     #如果預測的特徵尺寸未定,則使用預設的形狀;否則將shape中的值賦給特徵形狀列表中  
  270.         if None in shape:    
  271.             return default_shapes  
  272.         else:  
  273.             feat_shapes.append(shape)  
  274.     return feat_shapes                        #返回更新後的特徵尺寸list  
  275. def ssd_anchor_one_layer(img_shape,         #檢測單個特徵圖中所有錨點的座標和尺寸資訊(未與原圖做除法)  
  276.                          feat_shape,  
  277.                          sizes,  
  278.                          ratios,  
  279.                          step,  
  280.                          offset=0.5,  
  281.                          dtype=np.float32):  
  282.     """Computer SSD default anchor boxes for one feature layer. 
  283.     Determine the relative position grid of the centers, and the relative 
  284.     width and height. 
  285.     Arguments: 
  286.       feat_shape: Feature shape, used for computing relative position grids; 
  287.       size: Absolute reference sizes; 
  288.       ratios: Ratios to use on these features; 
  289.       img_shape: Image shape, used for computing height, width relatively to the 
  290.         former; 
  291.       offset: Grid offset. 
  292.     Return: 
  293.       y, x, h, w: Relative x and y grids, and height and width. 
  294.     """  
  295.     # Compute the position grid: simple way.  
  296.     # y, x = np.mgrid[0:feat_shape[0], 0:feat_shape[1]]  
  297.     # y = (y.astype(dtype) + offset) / feat_shape[0]  
  298.     # x = (x.astype(dtype) + offset) / feat_shape[1]  
  299.     # Weird SSD-Caffe computation using steps values...    #歸一化到原圖的錨點中心座標(x,y);其座標值域為(0,1)  
  300.     y, x = np.mgrid[0:feat_shape[0], 0:feat_shape[1]]      #對於第一個特徵圖(block4:38x38);y=[[0,0,……0],[1,1,……1],……[37,37,……,37]];而x=[[0,1,2……,37],[0,1,2……,37],……[0,1,2……,37]]  
  301.     y = (y.astype(dtype) + offset) * step / img_shape[0]   #將38個cell對應錨點框的y座標偏移至每個cell中心,然後乘以相對原圖縮放的比例,再除以原圖  
  302.     x = (x.astype(dtype) + offset) * step / img_shape[1]   #可以得到在原圖上,相對原圖比例大小的每個錨點中心座標x,y  
  303.     # Expand dims to support easy broadcasting.    #將錨點中心座標擴大維度  
  304.     y = np.expand_dims(y, axis=-1)   #對於第一個特徵圖,y的shape=38x38x1;x的shape=38x38x1  
  305.     x = np.expand_dims(x, axis=-1)  
  306.     # Compute relative height and width.  
  307.     # Tries to follow the original implementation of SSD for the order.  
  308.     num_anchors = len(sizes) + len(ratios)      #該特徵圖上每個點對應的錨點框數量;如:對於第一個特徵圖每個點預測4個錨點框(block4:38x38),2+2=4  
  309.     h = np.zeros((num_anchors, ), dtype=dtype)     #對於第一個特徵圖,h的shape=4x;w的shape=4x  
  310.     w = np.zeros((num_anchors, ), dtype=dtype)  
  311.     # Add first anchor boxes with ratio=1.  
  312.     h[0] = sizes[0] / img_shape[0]            #第一個錨點框的高h[0]=起始錨點的高/原圖大小的高;例如:h[0]=21/300  
  313.     w[0] = sizes[0] / img_shape[1]            #第一個錨點框的寬w[0]=起始錨點的寬/原圖大小的寬;例如:h[0]=45/300  
  314.     di = 1  #錨點寬個數偏移  
  315.     if len(sizes) > 1:                       
  316.         h[1] = math.sqrt(sizes[0] * sizes[1]) / img_shape[0]    #第二個錨點框的高h[1]=sqrt(起始錨點的高*起始錨點的寬)/原圖大小的高;例如:h[1]=sqrt(21*45)/300  
  317.         w[1] = math.sqrt(sizes[0] * sizes[1]) / img_shape[1]    #第二個錨點框的高w[1]=sqrt(起始錨點的高*起始錨點的寬)/原圖大小的寬;例如:w[1]=sqrt(21*45)/300  
  318.         di += 1     #di=2  
  319.     for i, r in enumerate(ratios):                            #遍歷長寬比例,第一個特徵圖,r只有兩個,2和0.5;共四個錨點寬size(h[0]~h[3])  
  320.         h[i+di] = sizes[0] / img_shape[0] / math.sqrt(r)      #例如:對於第一個特徵圖,h[0+2]=h[2]=21/300/sqrt(2);w[0+2]=w[2]=45/300*sqrt(2)  
  321.         w[i+di] = sizes[0] / img_shape[1] * math.sqrt(r)      #例如:對於第一個特徵圖,h[1+2]=h[3]=21/300/sqrt(0.5);w[1+2]=w[3]=45/300*sqrt(0.5)  
  322.     return y, x, h, w                                         #返回沒有歸一化前的錨點座標和尺寸  
  323. def ssd_anchors_all_layers(img_shape,                 #檢測所有特徵圖中錨點框的四個座標資訊; 輸入原始圖大小  
  324.                            layers_shape,              #每個特徵層形狀尺寸  
  325.                            anchor_sizes,              #起始特徵圖中框的長寬size  
  326.                            anchor_ratios,             #錨點框長寬比列表  
  327.                            anchor_steps,              #錨點框相對原圖縮放比例  
  328.                            offset=0.5,                #錨點中心在每個特徵圖cell中的偏移  
  329.                            dtype=np.float32):              
  330.     """Compute anchor boxes for all feature layers. 
  331.     """  
  332.     layers_anchors = []           #用於存放所有特徵圖中錨點框位置尺寸資訊  
  333.     for i, s in enumerate(layers_shape):                 #6個特徵圖尺寸;如:第0個是38x38  
  334.         anchor_bboxes = ssd_anchor_one_layer(img_shape, s,     #分別計算每個特徵圖中錨點框的位置尺寸資訊;  
  335.                                              anchor_sizes[i],    #輸入:第i個特徵圖中起始錨點框大小;如第0個是(21., 45.)  
  336.                                              anchor_ratios[i],   #輸入:第i個特徵圖中錨點框長寬比列表;如第0個是[2, .5]  
  337.                                              anchor_steps[i],    #輸入:第i個特徵圖中錨點框相對原始圖的縮放比;如第0個是8  
  338.                                              offset=offset, dtype=dtype)  #輸入:錨點中心在每個特徵圖cell中的偏移  
  339.         layers_anchors.append(anchor_bboxes)              #將6個特徵圖中每個特徵圖上的點對應的錨點框(6個或4個)儲存  
  340.     return layers_anchors  
  341. # =========================================================================== #  
  342. # Functional definition of VGG-based SSD 300.  
  343. # =========================================================================== #  
  344. def tensor_shape(x, rank=3):  
  345.     """Returns the dimensions of a tensor. 
  346.     Args: 
  347.       image: A N-D Tensor of shape. 
  348.     Returns: 
  349.       A list of dimensions. Dimensions that are statically known are python 
  350.         integers,otherwise they are integer scalar tensors. 
  351.     """  
  352.     if x.get_shape().is_fully_defined():  
  353.         return x.get_shape().as_list()  
  354.     else:  
  355.         static_shape = x.get_shape().with_rank(rank).as_list()  
  356.         dynamic_shape = tf.unstack(tf.shape(x), rank)  
  357.         return [s if s is not None else d  
  358.                 for s, d in zip(static_shape, dynamic_shape)]  
  359. def ssd_multibox_layer(inputs,                    #輸入特徵層  
  360.                        num_classes,               #類別數  
  361.                        sizes,                     #參考先驗框的尺度  
  362.                        ratios=[1],                #預設的先驗框長寬比為1  
  363.                        normalization=-1,          #預設不做正則化  
  364.                        bn_normalization=False):  
  365.     """Construct a multibox layer, return a class and localization predictions. 
  366.     """  
  367.     net = inputs  
  368.     if normalization > 0:    #如果輸入整數,則進行L2正則化  
  369.         net = custom_layers.l2_normalization(net, scaling=True)    #對通道所在維度進行正則化,隨後乘以gamma縮放係數  
  370.     # Number of anchors.  
  371.     num_anchors = len(sizes) + len(ratios)  #每層特徵圖參考先驗框的個數[4,6,6,6,4,4]  
  372.     # Location.     #每個先驗框對應4個座標資訊  
  373.     num_loc_pred = num_anchors * 4    #特徵圖上每個單元預測的座標所需維度=錨點框數*4  
  374.     loc_pred = slim.conv2d(net, num_loc_pred, [3, 3], activation_fn=None,   #通過對特徵圖進行3x3卷積得到位置資訊和類別權重資訊  
  375.                            scope='conv_loc')                                #該部分是定位資訊,輸出維度為[特徵圖h,特徵圖w,每個單元所有錨點框座標]  
  376.     loc_pred = custom_layers.channel_to_last(loc_pred)  
  377.     loc_pred = tf.reshape(loc_pred,                                         #最後整個特徵圖所有錨點框預測目標位置 tensor為[h*w*每個cell先驗框數,4]  
  378.                           tensor_shape(loc_pred, 4)[:-1]+[num_anchors, 4])  
  379.     # Class prediction.                                                #類別預測  
  380.     num_cls_pred = num_anchors * num_classes                            #特徵圖上每個單元預測的類別所需維度=錨點框數*種類數  
  381.     cls_pred = slim.conv2d(net, num_cls_pred, [3, 3], activation_fn=None, #該部分是類別資訊,輸出維度為[特徵圖h,特徵圖w,每個單元所有錨點框對應類別資訊]  
  382.                            scope='conv_cls')  
  383.     cls_pred = custom_layers.channel_to_last(cls_pred)  
  384.     cls_pred = tf.reshape(cls_pred,  
  385.                           tensor_shape(cls_pred, 4)[:-1]+[num_anchors, num_classes]) #最後整個特徵圖所有錨點框預測類別 tensor為[h*w*每個cell先驗框數,種類數]  
  386.     return cls_pred, loc_pred  #返回預測得到的類別和box位置 tensor  
  387. def ssd_net(inputs,                                             #定義ssd網路結構  
  388.             num_classes=SSDNet.default_params.num_classes,      #分類數  
  389.             feat_layers=SSDNet.default_params.feat_layers,      #特徵層  
  390.             anchor_sizes=SSDNet.default_params.anchor_sizes,  
  391.             anchor_ratios=SSDNet.default_params.anchor_ratios,  
  392.             normalizations=SSDNet.default_params.normalizations, #正則化  
  393.             is_training=True,  
  394.             dropout_keep_prob=0.5,  
  395.             prediction_fn=slim.softmax,  
  396.             reuse=None,  
  397.             scope='ssd_300_vgg'):  
  398.     """SSD net definition. 
  399.     """  
  400.     # if data_format == 'NCHW':  
  401.     #     inputs = tf.transpose(inputs, perm=(0, 3, 1, 2))  
  402.     # End_points collect relevant activations for external use.  
  403.     end_points = {}   #用於收集每一層輸出結果  
  404.     with tf.variable_scope(scope, 'ssd_300_vgg', [inputs], reuse=reuse):  
  405.         # Original VGG-16 blocks.  
  406.         net = slim.repeat(inputs, 2, slim.conv2d, 64, [3, 3], scope='conv1')    #VGG16網路的第一個conv,重複2次卷積,核為3x3,64個特徵  
  407.         end_points['block1'] = net                                              #conv1_2結果存入end_points,name='block1'  
  408.         net = slim.max_pool2d(net, [2, 2], scope='pool1')  
  409.         # Block 2.  
  410.         net = slim.repeat(net, 2, slim.conv2d, 128, [3, 3], scope='conv2')      #重複2次卷積,核為3x3,128個特徵  
  411.         end_points['block2'] = net                                              #conv2_2結果存入end_points,name='block2'  
  412.         net = slim.max_pool2d(net, [2, 2], scope='pool2')  
  413.         # Block 3.  
  414.         net = slim.repeat(net, 3, slim.conv2d, 256, [3, 3], scope='conv3')      #重複3次卷積,核為3x3,256個特徵  
  415.         end_points['block3'] = net                                              #conv3_3結果存入end_points,name='block3'  
  416.         net = slim.max_pool2d(net, [2, 2], scope='pool3')  
  417.         # Block 4.  
  418.         net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3], scope='conv4')       #重複3次卷積,核為3x3,512個特徵  
  419.         end_points['block4'] = net                                               #conv4_3結果存入end_points,name='block4'  
  420.         net = slim.max_pool2d(net, [2, 2], scope='pool4')  
  421.         # Block 5.  
  422.         net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3], scope='conv5')       #重複3次卷積,核為3x3,512個特徵  
  423.         end_points['block5'] = net                                               #conv5_3結果存入end_points,name='block5'  
  424.         net = slim.max_pool2d(net, [3, 3], stride=1, scope='pool5')  
  425.         # Additional SSD blocks.                                                  #去掉了VGG的全連線層  
  426.         # Block 6: let's dilate the hell out of it!  
  427.         net = slim.conv2d(net, 1024, [3, 3], rate=6, scope='conv6')              #將VGG基礎網路最後的池化層結果做擴展卷積(帶孔卷積);  
  428.         end_points['block6'] = net                                                #conv6結果存入end_points,name='block6'  
  429.         net = tf.layers.dropout(net, rate=dropout_keep_prob, training=is_training) #dropout層  
  430.         # Block 7: 1x1 conv. Because the fuck.  
  431.         net = slim.conv2d(net, 1024, [1, 1], scope='conv7')                          #將dropout後的網路做1x1卷積,輸出1024特徵,name='block7'  
  432.         end_points['block7'] = net  
  433.         net = tf.layers.dropout(net, rate=dropout_keep_prob, training=is_training)   #將卷積後的網路繼續做dropout  
  434.         # Block 8/9/10/11: 1x1 and 3x3 convolutions stride 2 (except lasts).  
  435.         end_point = 'block8'                                                           
  436.         with tf.variable_scope(end_point):  
  437.             net = slim.conv2d(net, 256, [1, 1], scope='conv1x1')           #對上述dropout的網路做1x1卷積,然後做3x3卷積,,輸出512特徵圖,name=‘block8’  
  438.             net = custom_layers.pad2d(net, pad=(1, 1))  
  439.             net = slim.conv2d(net, 512, [3, 3], stride=2, scope='conv3x3', padding='VALID')  
  440.         end_points[end_point] = net  
  441.         end_point = 'block9'  
  442.         with tf.variable_scope(end_point):  
  443.             net = slim.conv2d(net, 128, [1, 1], scope='conv1x1')                 #對上述網路做1x1卷積,然後做3x3卷積,輸出256特徵圖,name=‘block9’  
  444.             net = custom_layers.pad2d(net, pad=(1, 1))  
  445.             net = slim.conv2d(net, 256, [3, 3], stride=2, scope='conv3x3', padding='VALID')  
  446.         end_points[end_point] = net  
  447.         end_point = 'block10'  
  448.         with tf.variable_scope(end_point):  
  449.             net = slim.conv2d(net, 128, [1, 1], scope='conv1x1')               #對上述網路做1x1卷積,然後做3x3卷積,輸出256特徵圖,name=‘block10’  
  450.             net = slim.conv2d(net, 256, [3, 3], scope='conv3x3', padding='VALID')  
  451.         end_points[end_point] = net  
  452.         end_point = 'block11'  
  453.         with tf.variable_scope(end_point):  
  454.             net = slim.conv2d(net, 128, [1, 1], scope='conv1x1')              #對上述網路做1x1卷積,然後做3x3卷積,輸出256特徵圖,name=‘block11’  
  455.             net = slim.conv2d(net, 256, [3, 3], scope='conv3x3', padding='VALID')  
  456.         end_points[end_point] = net  
  457.         # Prediction and localisations layers. #預測和定位  
  458.         predictions = []  
  459.         logits = []  
  460.         localisations = []  
  461.         for i, layer in enumerate(feat_layers):               #遍歷特徵層  
  462.             with tf.variable_scope(layer + '_box'):                #起個命名範圍     
  463.                 p, l = ssd_multibox_layer(end_points[layer],   #做多尺度大小box預測的特徵層,返回每個cell中每個先驗框預測的類別p和預測的位置l  
  464.                                           num_classes,         #種類數  
  465.                                           anchor_sizes[i],     #先驗框尺度(同一特徵圖上的先驗框尺度和長寬比一致)  
  466.                                           anchor_ratios[i],    #先驗框長寬比  
  467.                                           normalizations[i])   #每個特徵正則化資訊,目前是隻對第一個特徵圖做歸一化操作;  
  468.              #把每一層的預測收集  
  469.             predictions.append(prediction_fn(p))  #prediction_fn為softmax,預測類別  
  470.             logits.append(p) #把每個cell每個先驗框預測的類別的概率值存在logits中  
  471.             localisations.append(l)   #預測位置資訊  
  472.         return predictions, localisations, logits, end_points  #返回類別預測結果,位置預測結果,所屬某個類別的概率值,以及特徵層  
  473. ssd_net.default_image_size = 300  
  474. def ssd_arg_scope(weight_decay=0.0005, data_format='NHWC'):  #權重衰減係數=0.0005;其是L2正則化項的係數  
  475.     """Defines the VGG arg scope. 
  476.     Args: 
  477.       weight_decay: The l2 regularization coefficient. 
  478.     Returns: 
  479.       An arg_scope. 
  480.     """  
  481.     with slim.arg_scope([slim.conv2d, slim.fully_connected],  
  482.                         activation_fn=tf.nn.relu,  
  483.                         weights_regularizer=slim.l2_regularizer(weight_decay),  
  484.                         weights_initializer=tf.contrib.layers.xavier_initializer(),  
  485.                         biases_initializer=tf.zeros_initializer()):  
  486.         with slim.arg_scope([slim.conv2d, slim.max_pool2d],  
  487.                             padding='SAME',  
  488.                             data_format=data_format):  
  489.             with slim.arg_scope([custom_layers.pad2d,  
  490.                                  custom_layers.l2_normalization,  
  491.                                  custom_layers.channel_to_last],  
  492.                                 data_format=data_format) as sc:  
  493.                 return sc  
  494. # =========================================================================== #  
  495. # Caffe scope: importing weights at initialization.  
  496. # =========================================================================== #  
  497. def ssd_arg_scope_caffe(caffe_scope):  
  498.     """Caffe scope definition. 
  499.     Args: 
  500.       caffe_scope: Caffe scope object with loaded weights. 
  501.     Returns: 
  502.       An arg_scope. 
  503.     """  
  504.     # Default network arg scope.  
  505.     with slim.arg_scope([slim.conv2d],  
  506.                         activation_fn=tf.nn.relu,  
  507.                         weights_initializer=caffe_scope.conv_weights_init(),  
  508.                         biases_initializer=caffe_scope.conv_biases_init()):  
  509.         with slim.arg_scope([slim.fully_connected],  
  510.                             activation_fn=tf.nn.relu):  
  511.             with slim.arg_scope([custom_layers.l2_normalization],  
  512.                                 scale_initializer=caffe_scope.l2_norm_scale_init()):  
  513.                 with slim.arg_scope([slim.conv2d, slim.max_pool2d],  
  514.                                     padding='SAME') as sc:  
  515.                     return sc  
  516. # =========================================================================== #  
  517. # SSD loss function.  
  518. # =========================================================================== #  
  519. def ssd_losses(logits, localisations,    #損失函式定義為位置誤差和置信度誤差的加權和;  
  520.                gclasses, glocalisations, gscores,  
  521.                match_threshold=0.5,  
  522.                negative_ratio=3.,    
  523.                alpha=1.,             #位置誤差權重係數  
  524.                label_smoothing=0.,  
  525.                device='/cpu:0',  
  526.                scope=None):  
  527.     with tf.name_scope(scope, 'ssd_losses'):  
  528.         lshape = tfe.get_shape(logits[0], 5)  
  529.         num_classes = lshape[-1]  
  530.         batch_size = lshape[0]  
  531.         # Flatten out all vectors!  
  532.         flogits = []  
  533.         fgclasses = []  
  534.         fgscores = []  
  535.         flocalisations = []  
  536.         fglocalisations = []  
  537.         for i in range(len(logits)):  
  538.             flogits.append(tf.reshape(logits[i], [-1, num_classes]))    #將類別的概率值reshape成(-1,21)  
  539.             fgclasses.append(tf.reshape(gclasses[i], [-1]))             #真實類別  
  540.             fgscores.append(tf.reshape(gscores[i], [-1]))               #預測真實目標的得分  
  541.             flocalisations.append(tf.reshape(localisations[i], [-1, 4]))  #預測真實目標邊框座標(編碼形式的值)  
  542.             fglocalisations.append(tf.reshape(glocalisations[i], [-1, 4])) #用於將真實目標gt的座標進行編碼儲存  
  543.         # And concat the crap!  
  544.         logits = tf.concat(flogits, axis=0)  
  545.         gclasses = tf.concat(fgclasses, axis=0)  
  546.         gscores = tf.concat(fgscores, axis=0)  
  547.         localisations = tf.concat(flocalisations, axis=0)  
  548.         glocalisations = tf.concat(fglocalisations, axis=0)  
  549.         dtype = logits.dtype  
  550.         # Compute positive matching mask...  
  551.         pmask = gscores > match_threshold  #預測框與真實框IOU>0.5則將這個先驗作為正樣本  
  552.         fpmask = tf.cast(pmask, dtype)  
  553.         n_positives = tf.reduce_sum(fpmask)  #求正樣本數量N  
  554.         # Hard negative mining... 為了保證正負樣本儘量平衡,SSD採用了hard negative mining,就是對負樣本進行抽樣,抽樣時按照置信度誤差(預測背景的置信度越小,誤差越大)進行降序排列,選取誤差的較大的top-k作為訓練的負樣本,以保證正負樣本比例接近1:3  
  555.         no_classes = tf.cast(pmask, tf.int32)  
  556.         predictions = slim.softmax(logits)   #類別預測  
  557.         nmask = tf.logical_and(tf.logical_not(pmask),  
  558.                                gscores > -0.5)  
  559.         fnmask = tf.cast(nmask, dtype)       
  560.         nvalues = tf.where(nmask,  
  561.                            predictions[:, 0],  
  562.                            1. - fnmask)  
  563.         nvalues_flat = tf.reshape(nvalues, [-1])  
  564.         # Number of negative entries to select.  
  565.         max_neg_entries = tf.cast(tf.reduce_sum(fnmask), tf.int32)    
  566.         n_neg = tf.cast(negative_ratio * n_positives, tf.int32) + batch_size #負樣本數量,保證是正樣本3倍  
  567.         n_neg = tf.minimum(n_neg, max_neg_entries)  
  568.         val, idxes = tf.nn.top_k(-nvalues_flat, k=n_neg) #抽樣時按照置信度誤差(預測背景的置信度越小,誤差越大)進行降序排列,選取誤差的較大的top-k作為訓練的負樣本  
  569.         max_hard_pred = -val[-1]  
  570.         # Final negative mask.  
  571.         nmask = tf.logical_and(nmask, nvalues < max_hard_pred)  
  572.         fnmask = tf.cast(nmask, dtype)  
  573.         # Add cross-entropy loss.    #交叉熵  
  574.         with tf.name_scope('cross_entropy_pos'):  
  575.             loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits,   #類別置信度誤差  
  576.                                                                   labels=gclasses)  
  577.             loss = tf.div(tf.reduce_sum(loss * fpmask), batch_size, name='value')  #將置信度誤差除以正樣本數後除以batch-size  
  578.             tf.losses.add_loss(loss)  
  579.         with tf.name_scope('cross_entropy_neg'):  
  580.             loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits,  
  581.                                                                   labels=no_classes)  
  582.             loss = tf.div(tf.reduce_sum(loss * fnmask), batch_size, name='value')  
  583.             tf.losses.add_loss(loss)  
  584.         # Add localization loss: smooth L1, L2, ...  
  585.         with tf.name_scope('localization'):  
  586.             # Weights Tensor: positive mask + random negative.  
  587.             weights = tf.expand_dims(alpha * fpmask, axis=-1)  
  588.             loss = custom_layers.abs_smooth(localisations - glocalisations)   #先驗框對應邊界的位置預測值-真實位置;然後做Smooth L1 loss  
  589.             loss = tf.div(tf.reduce_sum(loss * weights), batch_size, name='value')  #將上面的loss*權重(=alpha/正樣本數)求和後除以batch-size  
  590.             tf.losses.add_loss(loss)  #獲得置信度誤差和位置誤差的加權和  
  591. def ssd_losses_old(logits, localisations,  
  592.                    gclasses, glocalisations, gscores,  
  593.                    match_threshold=0.5,  
  594.                    negative_ratio=3.,  
  595.                    alpha=1.,  
  596.                    label_smoothing=0.,  
  597.                    device='/cpu:0',  
  598.                    scope=None):  
  599.     """Loss functions for training the SSD 300 VGG network. 
  600.     This function defines the different loss components of the SSD, and 
  601.     adds them to the TF loss collection. 
  602.     Arguments: 
  603.       logits: (list of) predictions logits Tensors; 
  604.       localisations: (list of) localisations Tensors; 
  605.       gclasses: (list of) groundtruth labels Tensors; 
  606.       glocalisations: (list of) groundtruth localisations Tensors; 
  607.       gscores: (list of) groundtruth score Tensors; 
  608.     """  
  609.     with tf.device(device):  
  610.         with tf.name_scope(scope, 'ssd_losses'):  
  611.             l_cross_pos = []  
  612.             l_cross_neg = []  
  613.             l_loc = []  
  614.             for i in range(len(logits)):  
  615.                 dtype = logits[i].dtype  
  616.                 with tf.name_scope('block_%i' % i):  
  617.                     # Sizing weight...  
  618.                     wsize = tfe.get_shape(logits[i], rank=5)  
  619.                     wsize = wsize[1] * wsize[2] * wsize[3]  
  620.                     # Positive mask.  
  621.                     pmask = gscores[i] > match_threshold  
  622.                     fpmask = tf.cast(pmask, dtype)  
  623.                     n_positives = tf.reduce_sum(fpmask)  
  624.                     # Select some random negative entries.  
  625.                     # n_entries = np.prod(gclasses[i].get_shape().as_list())  
  626.                     # r_positive = n_positives / n_entries  
  627.                     # r_negative = negative_ratio * n_positives / (n_entries - n_positives)  
  628.                     # Negative mask.  
  629.                     no_classes = tf.cast(pmask, tf.int32)  
  630.                     predictions = slim.softmax(logits[i])  
  631.                     nmask = tf.logical_and(tf.logical_not(pmask),  
  632.                                            gscores[i] > -0.5)  
  633.                     fnmask = tf.cast(nmask, dtype)  
  634.                     nvalues = tf.where(nmask,  
  635.                                        predictions[:, :, :, :, 0],  
  636.                                        1. - fnmask)  
  637.                     nvalues_flat = tf.reshape(nvalues, [-1])  
  638.                     # Number of negative entries to select.  
  639.                     n_neg = tf.cast(negative_ratio * n_positives, tf.int32)  
  640.                     n_neg = tf.maximum(n_neg, tf.size(nvalues_flat) // 8)  
  641.                     n_neg = tf.maximum(n_neg, tf.shape(nvalues)[0] * 4)  
  642.                     max_neg_entries = 1 + tf.cast(tf.reduce_sum(fnmask), tf.int32)  
  643.                     n_neg = tf.minimum(n_neg, max_neg_entries)  
  644.                     val, idxes = tf.nn.top_k(-nvalues_flat, k=n_neg)  
  645.                     max_hard_pred = -val[-1]  
  646.                     # Final negative mask.  
  647.                     nmask = tf.logical_and(nmask, nvalues < max_hard_pred)  
  648.                     fnmask = tf.cast(nmask, dtype)  
  649.                     # Add cross-entropy loss.  
  650.                     with tf.name_scope('cross_entropy_pos'):  
  651.                         fpmask = wsize * fpmask  
  652.                         loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits[i],  
  653.                                                                               labels=gclasses[i])  
  654.                         loss = tf.losses.compute_weighted_loss(loss, fpmask)  
  655.                         l_cross_pos.append(loss)  
  656.                     with tf.name_scope('cross_entropy_neg'):  
  657.                         fnmask = wsize * fnmask  
  658.                         loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits[i],  
  659.                                                                               labels=no_classes)  
  660.                         loss = tf.losses.compute_weighted_loss(loss, fnmask)  
  661.                         l_cross_neg.append(loss)  
  662.                     # Add localization loss: smooth L1, L2, ...  
  663.                     with tf.name_scope('localization'):  
  664.                         # Weights Tensor: positive mask + random negative.  
  665.                         weights = tf.expand_dims(alpha * fpmask, axis=-1)  
  666.                         loss = custom_layers.abs_smooth(localisations[i] - glocalisations[i])  
  667.                         loss = tf.losses.compute_weighted_loss(loss, weights)  
  668.                         l_loc.a