『TensorFlow』SSD源碼學習_其七:損失函數

Fork版本項目地址:SSDpython

1、損失函數介紹

SSD損失函數分爲兩個部分:對應搜索框的位置loss(loc)和類別置信度loss(conf)。(搜索框指網絡生成的網格)git

詳細的說明以下:github

i指代搜索框序號,j指代真實框序號,p指代類別序號,p=0表示背景,網絡

x_{ij}^{p}=\left\{ 1,0 \right\}  中取1表示此時第i個搜索框和第j個類別框IOU大於閾值,此時真實框中對象類別爲p。app

cip表示第i個搜索框對應類別p的預測機率。dom

2、分類損失函數

有了上圖的分析,咱們能夠看具體實現了,首先咱們看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分數不夠二次篩選的標準),因此須要爲負樣本標註新的類別標籤。

3、定位損失函數

定位損失函數形式簡單一點,

        # 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

4、損失函數全覽

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)
相關文章
相關標籤/搜索