Fork版本項目地址:SSD
一、損失函數介紹
SSD損失函數分為兩個部分:對應搜索框的位置loss(loc)和類別置信度loss(conf)。(搜索框指網絡生成的網格)
詳細的說明如下:
i指代搜索框序號,j指代真實框序號,p指代類別序號,p=0表示背景,
中取1表示此時第i個搜索框和第j個類別框IOU大於閾值,此時真實框中對象類別為p。
cip表示第i個搜索框對應類別p的預測概率。

二、分類損失函數
有了上圖的分析,我們可以看具體實現了,首先我們看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類也可以(不過一般來說是不會有真實框框住背景並進行標注的), 然后看負樣本,
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預測值最小的目標數目的點。
# 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)
