Fork版本項目地址:SSDpython
SSD損失函數分爲兩個部分:對應搜索框的位置loss(loc)和類別置信度loss(conf)。(搜索框指網絡生成的網格)git
詳細的說明以下:github
i指代搜索框序號,j指代真實框序號,p指代類別序號,p=0表示背景,網絡
中取1表示此時第i個搜索框和第j個類別框IOU大於閾值,此時真實框中對象類別爲p。app
cip表示第i個搜索框對應類別p的預測機率。dom
有了上圖的分析,咱們能夠看具體實現了,首先咱們看Lconf部分的計算,其分爲最大化第一個累加符號和最大化第二個累加符號兩個部分(這牽扯到另外一個問題:Pos框和Neg框的選擇,這一點咱們在下面分析代碼中會也說起,注意二者都是對搜索框進行的討論),咱們將分別討論兩部分的實現邏輯,根據代碼來看,首先肯定正樣本框的掩碼:函數
dtype = logits.dtype pmask = gscores > match_threshold # (所有搜索框數目, 21),類別搜索框和真實框IOU大於閾值 fpmask = tf.cast(pmask, dtype) # 浮點型前景掩碼(前景假定爲含有對象的IOU足夠的搜索框標號) n_positives = tf.reduce_sum(fpmask) # 前景總數
也就是說只要IOU到達閾值就認爲這個搜索框是正樣本(fpmask標記),注意,即便是第0類也能夠(不過通常來講是不會有真實框框住背景並進行標註的), 而後看負樣本,spa
no_classes = tf.cast(pmask, tf.int32) predictions = slim.softmax(logits) # 此時每一行的21個數轉化爲機率 nmask = tf.logical_and(tf.logical_not(pmask), gscores > -0.5) # IOU達不到閾值的類別搜索框位置記1 fnmask = tf.cast(nmask, dtype) nvalues = tf.where(nmask, predictions[:, 0], # 框內無物體標記爲背景預測機率 1. - fnmask) # 框內有物體位置標記爲1 nvalues_flat = tf.reshape(nvalues, [-1])
此時的負樣本(fnmask標記)一樣的爲{0,1},且和正樣本互補,可是這樣會致使負樣本過多,因此創建nvalue用於篩選負樣本,nvalue中fnmask爲1的位置記爲對應搜索框的第0類(背景)預測機率,不然記爲1(fpmask標記位置),對象
# 在nmask中剔除n_neg個最不可能背景點(對應的class0機率最低) max_neg_entries = tf.cast(tf.reduce_sum(fnmask), tf.int32) # 3 × 前景掩碼數量 + batch_size n_neg = tf.cast(negative_ratio * n_positives, tf.int32) + batch_size n_neg = tf.minimum(n_neg, max_neg_entries) val, idxes = tf.nn.top_k(-nvalues_flat, k=n_neg) # 最不可能爲背景的n_neg個點 max_hard_pred = -val[-1] # Final negative mask. nmask = tf.logical_and(nmask, nvalues < max_hard_pred) # 不是前景,又最不像背景的n_neg個點 fnmask = tf.cast(nmask, dtype)
在進一步處理中,咱們但願負樣本不要超過正樣本數目的3倍,確保可以收斂(具體推導不明),因爲知道這些負樣本都屬於背景(和真實框IOU不足),因此理論上其class 0預測值越大越好,咱們取class 0預測值最小的3倍正樣本數目的負樣本,最大化其class 0預測值,達到最小化損失函數的目的。篩選後的負樣本(fnmask標記)爲原負樣本中class 0預測值最小的目標數目的點。blog
# Add cross-entropy loss. with tf.name_scope('cross_entropy_pos'): loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=gclasses) # 0-20 loss = tf.div(tf.reduce_sum(loss * fpmask), batch_size, name='value') tf.losses.add_loss(loss) with tf.name_scope('cross_entropy_neg'): loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=no_classes) # {0,1} loss = tf.div(tf.reduce_sum(loss * fnmask), batch_size, name='value') tf.losses.add_loss(loss)
對應兩部分的損失函數計算。上面的公式中第一部分比第二部分多了個x,其實是爲了肯定cp中p的取值,而第二部分不須要了,由於p恆爲0。
no_classes爲標籤,只要保證fnmask中標記點(負樣本)對應位置爲0便可。對應的gclasses其實只要pnmask爲1位置有真實分類標籤便可,之因此額外劃分出no_classes是由於gclasses在迭代生成時有可能給得分(IOU)不足夠高的搜索框標註上類別信息,而在本函數一開始,咱們就使用得分(IOU)對搜索框進行了二次篩選(在gclasses、gscores、glocalisations生成過程當中會對IOU進行一次篩選),fpmask可能會將一些一次篩選中標記了gclasses(且不爲0)的搜索框剔除,這對正樣本沒有影響(正樣本位置必定是gclasses標記位置的子集),可是會影響負樣本(新的認定爲背景的搜索框在gclasses上有標記類別,同時也是說其gscore分數不夠二次篩選的標準),因此須要爲負樣本標註新的類別標籤。
定位損失函數形式簡單一點,
# Add localization loss: smooth L1, L2, ... with tf.name_scope('localization'): # Weights Tensor: positive mask + random negative. weights = tf.expand_dims(alpha * fpmask, axis=-1) loss = custom_layers.abs_smooth(localisations - glocalisations) loss = tf.div(tf.reduce_sum(loss * weights), batch_size, name='value') tf.losses.add_loss(loss)
調用函數以下:
def abs_smooth(x): """Smoothed absolute function. Useful to compute an L1 smooth error. Define as: x^2 / 2 if abs(x) < 1 abs(x) - 0.5 if abs(x) > 1 We use here a differentiable definition using min(x) and abs(x). Clearly not optimal, but good enough for our purpose! """ absx = tf.abs(x) minx = tf.minimum(absx, 1) r = 0.5 * ((absx - 1) * minx + absx) return r
def ssd_losses(logits, localisations, # 預測類別,位置 gclasses, glocalisations, gscores, # ground truth類別,位置,得分 match_threshold=0.5, negative_ratio=3., alpha=1., label_smoothing=0., device='/cpu:0', scope=None): with tf.name_scope(scope, 'ssd_losses'): # 提取類別數和batch_size lshape = tfe.get_shape(logits[0], 5) # tensor_shape函數能夠取代 num_classes = lshape[-1] batch_size = lshape[0] # Flatten out all vectors! flogits = [] fgclasses = [] fgscores = [] flocalisations = [] fglocalisations = [] for i in range(len(logits)): # 按照ssd特徵層循環 flogits.append(tf.reshape(logits[i], [-1, num_classes])) fgclasses.append(tf.reshape(gclasses[i], [-1])) fgscores.append(tf.reshape(gscores[i], [-1])) flocalisations.append(tf.reshape(localisations[i], [-1, 4])) fglocalisations.append(tf.reshape(glocalisations[i], [-1, 4])) # And concat the crap! logits = tf.concat(flogits, axis=0) # 所有的搜索框,對應的21類別的輸出 gclasses = tf.concat(fgclasses, axis=0) # 所有的搜索框,真實的類別數字 gscores = tf.concat(fgscores, axis=0) # 所有的搜索框,和真實框的IOU localisations = tf.concat(flocalisations, axis=0) glocalisations = tf.concat(fglocalisations, axis=0) """[<tf.Tensor 'ssd_losses/concat:0' shape=(279424, 21) dtype=float32>, <tf.Tensor 'ssd_losses/concat_1:0' shape=(279424,) dtype=int64>, <tf.Tensor 'ssd_losses/concat_2:0' shape=(279424,) dtype=float32>, <tf.Tensor 'ssd_losses/concat_3:0' shape=(279424, 4) dtype=float32>, <tf.Tensor 'ssd_losses/concat_4:0' shape=(279424, 4) dtype=float32>] """ dtype = logits.dtype pmask = gscores > match_threshold # (所有搜索框數目, 21),類別搜索框和真實框IOU大於閾值 fpmask = tf.cast(pmask, dtype) # 浮點型前景掩碼(前景假定爲含有對象的IOU足夠的搜索框標號) n_positives = tf.reduce_sum(fpmask) # 前景總數 # Hard negative mining... no_classes = tf.cast(pmask, tf.int32) predictions = slim.softmax(logits) # 此時每一行的21個數轉化爲機率 nmask = tf.logical_and(tf.logical_not(pmask), gscores > -0.5) # IOU達不到閾值的類別搜索框位置記1 fnmask = tf.cast(nmask, dtype) nvalues = tf.where(nmask, predictions[:, 0], # 框內無物體標記爲背景預測機率 1. - fnmask) # 框內有物體位置標記爲1 nvalues_flat = tf.reshape(nvalues, [-1]) # Number of negative entries to select. # 在nmask中剔除n_neg個最不可能背景點(對應的class0機率最低) max_neg_entries = tf.cast(tf.reduce_sum(fnmask), tf.int32) # 3 × 前景掩碼數量 + batch_size n_neg = tf.cast(negative_ratio * n_positives, tf.int32) + batch_size n_neg = tf.minimum(n_neg, max_neg_entries) val, idxes = tf.nn.top_k(-nvalues_flat, k=n_neg) # 最不可能爲背景的n_neg個點 max_hard_pred = -val[-1] # Final negative mask. nmask = tf.logical_and(nmask, nvalues < max_hard_pred) # 不是前景,又最不像背景的n_neg個點 fnmask = tf.cast(nmask, dtype) # Add cross-entropy loss. with tf.name_scope('cross_entropy_pos'): loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=gclasses) # 0-20 loss = tf.div(tf.reduce_sum(loss * fpmask), batch_size, name='value') tf.losses.add_loss(loss) with tf.name_scope('cross_entropy_neg'): loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=no_classes) # {0,1} loss = tf.div(tf.reduce_sum(loss * fnmask), batch_size, name='value') tf.losses.add_loss(loss) # Add localization loss: smooth L1, L2, ... with tf.name_scope('localization'): # Weights Tensor: positive mask + random negative. weights = tf.expand_dims(alpha * fpmask, axis=-1) loss = custom_layers.abs_smooth(localisations - glocalisations) loss = tf.div(tf.reduce_sum(loss * weights), batch_size, name='value') tf.losses.add_loss(loss)