技術乾貨 | 基於MindSpore更好的理解Focal Loss

華為雲開發者社群發表於2021-05-24

【本期推薦專題】物聯網從業人員必讀:華為雲專家為你詳細解讀LiteOS各模組開發及其實現原理。

摘要:Focal Loss的兩個性質算是核心,其實就是用一個合適的函式去度量難分類和易分類樣本對總的損失的貢獻。

本文分享自華為雲社群《技術乾貨 | 基於MindSpore更好的理解Focal Loss》,原文作者:chengxiaoli。

今天更新一下愷明大神的Focal Loss,它是 Kaiming 大神團隊在他們的論文Focal Loss for Dense Object Detection提出來的損失函式,利用它改善了影像物體檢測的效果。ICCV2017RBG和Kaiming大神的新作(https://arxiv.org/pdf/1708.02002.pdf)。

使用場景

最近一直在做人臉表情相關的方向,這個領域的 DataSet 數量不大,而且往往存在正負樣本不均衡的問題。一般來說,解決正負樣本數量不均衡問題有兩個途徑:

1. 設計取樣策略,一般都是對數量少的樣本進行重取樣

2. 設計 Loss,一般都是對不同類別樣本進行權重賦值

本文講的是第二種策略中的 Focal Loss。

理論分析

論文分析

我們知道object detection按其流程來說,一般分為兩大類。一類是two stage detector(如非常經典的Faster R-CNN,RFCN這樣需要region proposal的檢測演算法),第二類則是one stage detector(如SSD、YOLO系列這樣不需要region proposal,直接回歸的檢測演算法)。

對於第一類演算法可以達到很高的準確率,但是速度較慢。雖然可以通過減少proposal的數量或降低輸入影像的解析度等方式達到提速,但是速度並沒有質的提升。

對於第二類演算法速度很快,但是準確率不如第一類。

所以目標就是:focal loss的出發點是希望one-stage detector可以達到two-stage detector的準確率,同時不影響原有的速度。

So,Why?and result?

這是什麼原因造成的呢?the Reason is:Class Imbalance(正負樣本不平衡),樣本的類別不均衡導致的。

我們知道在object detection領域,一張影像可能生成成千上萬的candidate locations,但是其中只有很少一部分是包含object的,這就帶來了類別不均衡。那麼類別不均衡會帶來什麼後果呢?引用原文講的兩個後果:

(1) training is inefficient as most locations are easy negatives that contribute no useful learning signal;
(2) en masse, the easy negatives can overwhelm training and lead to degenerate models.

意思就是負樣本數量太大(屬於背景的樣本),佔總的loss的大部分,而且多是容易分類的,因此使得模型的優化方向並不是我們所希望的那樣。這樣,網路學不到有用的資訊,無法對object進行準確分類。其實先前也有一些演算法來處理類別不均衡的問題,比如OHEM(online hard example mining),OHEM的主要思想可以用原文的一句話概括:In OHEM each example is scored by its loss, non-maximum suppression (nms) is then applied, and a minibatch is constructed with the highest-loss examples。OHEM演算法雖然增加了錯分類樣本的權重,但是OHEM演算法忽略了容易分類的樣本。

因此針對類別不均衡問題,作者提出一種新的損失函式:Focal Loss,這個損失函式是在標準交叉熵損失基礎上修改得到的。這個函式可以通過減少易分類樣本的權重,使得模型在訓練時更專注於難分類的樣本。為了證明Focal Loss的有效性,作者設計了一個dense detector:RetinaNet,並且在訓練時採用Focal Loss訓練。實驗證明RetinaNet不僅可以達到one-stage detector的速度,也能有two-stage detector的準確率。

公式說明

介紹focal loss,在介紹focal loss之前,先來看看交叉熵損失,這裡以二分類為例,原來的分類loss是各個訓練樣本交叉熵的直接求和,也就是各個樣本的權重是一樣的。公式如下: https://i.iter01.com/images/ba4d79c014d02a3b147be22c4c6da0be20b51cfc3192fa404b48f221803d5b56.png因為是二分類,p表示預測樣本屬於1的概率(範圍為0-1),y表示label,y的取值為{+1,-1}。當真實label是1,也就是y=1時,假如某個樣本x預測為1這個類的概率p=0.6,那麼損失就是-log(0.6),注意這個損失是大於等於0的。如果p=0.9,那麼損失就是-log(0.9),所以p=0.6的損失要大於p=0.9的損失,這很容易理解。這裡僅僅以二分類為例,多分類分類以此類推為了方便,用pt代替p,如下公式2:。這裡的pt就是前面Figure1中的橫座標。 https://i.iter01.com/images/99e5b35f4fd082e5a905e223da43e0de423a80990434b4fab653ee3972318214.png為了表示簡便,我們用p_t表示樣本屬於true class的概率。所以(1)式可以寫成: https://i.iter01.com/images/fa4e3e593a90ae7fd2c7f31467f18b66e445f2f9b3b2c44a1c7f1899f504ed28.png接下來介紹一個最基本的對交叉熵的改進,也將作為本文實驗的baseline,既然one-stage detector在訓練的時候正負樣本的數量差距很大,那麼一種常見的做法就是給正負樣本加上權重,負樣本出現的頻次多,那麼就降低負樣本的權重,正樣本數量少,就相對提高正樣本的權重。因此可以通過設定 https://i.iter01.com/images/e2b67fca00fca0f46eba065f148aecf7c32535858f6979739d1b62bbff06b3f8.png 的值來控制正負樣本對總的loss的共享權重。 https://i.iter01.com/images/6a02636dfdb0a18a988c8a522f0bc8e286448e800ac0f2ce53a011a414b8a2c4.png 取比較小的值來降低負樣本(多的那類樣本)的權重。 https://i.iter01.com/images/e7268ef15b83b05e1ca2958e4d9a24b26a5b0088dcbe682e2d432842886091b2.png

顯然前面的公式3雖然可以控制正負樣本的權重,但是沒法控制容易分類和難分類樣本的權重,於是就有了Focal Loss,這裡的γ稱作focusing parameter,γ>=0,稱為調製係數:

 https://i.iter01.com/images/a7db95ef7b9c88eb29a7cfffa84f57dc057f50cfc578bcb2e39ce76cb25787df.png

為什麼要加上這個調製係數呢?目的是通過減少易分類樣本的權重,從而使得模型在訓練時更專注於難分類的樣本。

通過實驗發現,繪製圖看如下Figure1,橫座標是pt,縱座標是loss。CE(pt)表示標準的交叉熵公式,FL(pt)表示focal loss中用到的改進的交叉熵。Figure1中γ=0的藍色曲線就是標準的交叉熵損失(loss)。

https://i.iter01.com/images/f9c26f8490902ec83211f0711a7a8b1327d2035d38bc196e9a6f0dcaf4ab83cb.jpg

這樣就既做到了解決正負樣本不平衡,也做到了解決easy與hard樣本不平衡的問題。

結論

作者將類別不平衡作為阻礙one-stage方法超過top-performing的two-stage方法的主要原因。為了解決這個問題,作者提出了focal loss,在交叉熵裡面用一個調整項,為了將學習專注於hard examples上面,並且降低大量的easy negatives的權值。是同時解決了正負樣本不平衡以及區分簡單與複雜樣本的問題。

MindSpore程式碼實現

我們來看一下,基於MindSpore實現Focal Loss的程式碼:

import mindspore

import mindspore.common.dtype as mstype

from mindspore.common.tensor import Tensor

from mindspore.common.parameter import Parameter

from mindspore.ops import operations as P

from mindspore.ops import functional as F

from mindspore import nn



class FocalLoss(_Loss):



    def __init__(self, weight=None, gamma=2.0, reduction='mean'):

        super(FocalLoss, self).__init__(reduction=reduction)

        # 校驗gamma,這裡的γ稱作focusing parameter,γ>=0,稱為調製係數

        self.gamma = validator.check_value_type("gamma", gamma, [float])

        if weight is not None and not isinstance(weight, Tensor):

            raise TypeError("The type of weight should be Tensor, but got {}.".format(type(weight)))

        self.weight = weight

        # 用到的mindspore運算元

        self.expand_dims = P.ExpandDims()

        self.gather_d = P.GatherD()

        self.squeeze = P.Squeeze(axis=1)

        self.tile = P.Tile()

        self.cast = P.Cast()



    def construct(self, predict, target):

        targets = target

        # 對輸入進行校驗

        _check_ndim(predict.ndim, targets.ndim)

        _check_channel_and_shape(targets.shape[1], predict.shape[1])

        _check_predict_channel(predict.shape[1])



        # 將logits和target的形狀更改為num_batch * num_class * num_voxels.

        if predict.ndim > 2:

            predict = predict.view(predict.shape[0], predict.shape[1], -1) # N,C,H,W => N,C,H*W

            targets = targets.view(targets.shape[0], targets.shape[1], -1) # N,1,H,W => N,1,H*W or N,C,H*W

        else:

            predict = self.expand_dims(predict, 2) # N,C => N,C,1

            targets = self.expand_dims(targets, 2) # N,1 => N,1,1 or N,C,1

       

        # 計算對數概率

        log_probability = nn.LogSoftmax(1)(predict)

        # 只保留每個voxel的地面真值類的對數概率值。

        if target.shape[1] == 1:

            log_probability = self.gather_d(log_probability, 1, self.cast(targets, mindspore.int32))

            log_probability = self.squeeze(log_probability)



        # 得到概率

        probability = F.exp(log_probability)



        if self.weight is not None:

            convert_weight = self.weight[None, :, None]  # C => 1,C,1

            convert_weight = self.tile(convert_weight, (targets.shape[0], 1, targets.shape[2])) # 1,C,1 => N,C,H*W

            if target.shape[1] == 1:

                convert_weight = self.gather_d(convert_weight, 1, self.cast(targets, mindspore.int32))  # selection of the weights  => N,1,H*W

                convert_weight = self.squeeze(convert_weight)  # N,1,H*W => N,H*W

            # 將對數概率乘以它們的權重

            probability = log_probability * convert_weight

        # 計算損失小批量

        weight = F.pows(-probability + 1.0, self.gamma)

        if target.shape[1] == 1:

            loss = (-weight * log_probability).mean(axis=1)  # N

        else:

            loss = (-weight * targets * log_probability).mean(axis=-1)  # N,C



        return self.get_loss(loss)

使用方法如下:

from mindspore.common import dtype as mstype

from mindspore import nn

from mindspore import Tensor



predict = Tensor([[0.8, 1.4], [0.5, 0.9], [1.2, 0.9]], mstype.float32)

target = Tensor([[1], [1], [0]], mstype.int32)

focalloss = nn.FocalLoss(weight=Tensor([1, 2]), gamma=2.0, reduction='mean')

output = focalloss(predict, target)

print(output)



0.33365273

Focal Loss的兩個重要性質

1. 當一個樣本被分錯的時候,pt是很小的,那麼調製因子(1-Pt)接近1,損失不被影響;當Pt→1,因子(1-Pt)接近0,那麼分的比較好的(well-classified)樣本的權值就被調低了。因此調製係數就趨於1,也就是說相比原來的loss是沒有什麼大的改變的。當pt趨於1的時候(此時分類正確而且是易分類樣本),調製係數趨於0,也就是對於總的loss的貢獻很小。

2. 當γ=0的時候,focal loss就是傳統的交叉熵損失,當γ增加的時候,調製係數也會增加。 專注引數γ平滑地調節了易分樣本調低權值的比例。γ增大能增強調製因子的影響,實驗發現γ取2最好。直覺上來說,調製因子減少了易分樣本的損失貢獻,拓寬了樣例接收到低損失的範圍。當γ一定的時候,比如等於2,一樣easy example(pt=0.9)的loss要比標準的交叉熵loss小100+倍,當pt=0.968時,要小1000+倍,但是對於hard example(pt < 0.5),loss最多小了4倍。這樣的話hard example的權重相對就提升了很多。

這樣就增加了那些誤分類的重要性Focal Loss的兩個性質算是核心,其實就是用一個合適的函式去度量難分類和易分類樣本對總的損失的貢獻。

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章