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


Fork版本項目地址:SSD

一、損失函數介紹

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

詳細的說明如下:

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

x_{ij}^{p}=\left\{ 1,0 \right\}  中取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)

 


免責聲明!

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



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