聊聊損失函式1. 噪聲魯棒損失函式簡析 & 程式碼實現

風雨中的小七發表於2023-01-01

今天來聊聊非常規的損失函式。在常用的分類交叉熵,以及迴歸均方誤差之外,針對訓練樣本可能存在的資料長尾,標籤噪聲,資料不均衡等問題,我們來聊聊適用不同場景有針對性的損失函式。第一章我們介紹,當標註標籤存在噪聲時可以嘗試的損失函式,這裡的標籤噪聲主要指獨立於特徵分佈的標籤噪聲。程式碼詳見pytorch, Tensorflow

Symmetric Loss Function

paper: Making Risk Minimization Tolerant to Label Noise

這裡我們用最基礎的二分類問題,和一個簡化的假設"標註噪聲和標籤獨立且均勻分佈",來解釋下什麼是對標註噪聲魯棒的損失函式。假設整體誤標註的樣本佔比為\(\eta\),則在真實標籤y=0和y=1中均有\(\eta\)比例的誤標註,1被標成0,0被標稱1。帶噪聲的損失函式如下

\[\begin{align} L(f(x), y_{noise}) &= (1-\eta)*L(f(x), y) + \eta * L(f(x), 1-y) \\ & = (1-2\eta)*L(f(x),y) + \eta*[L(f(x),y)+L(f(x),1-y)] \\ & = (1-2\eta)*L(f(x),y) + \eta K \\ \end{align} \]

因此如果損失函式滿足\(L(f(x),y)+L(f(x),1-y)=constant\),則帶噪聲的損失函式會和不帶噪聲的\(L(f(x),y)\)收斂到相同的解。作者認為這樣的損失函式就是symmetric的。

那有哪些常見的損失函式是symmetric loss呢?

MAE就是!對於二分類的softmax的輸出層\(L(f(x),y)+L(f(x),1-y)=|y-f(x)| + |1-y-f(x)| = 1\)

敲黑板!記住這一點,因為後面的GCE和SCE其實都和MAE有著脫不開的關係。這裡對symmetric loss的論證做了簡化,細節詳見論文~

Generalized Cross Entropy(GCE)

paper:Generalized Cross Entropy Loss for Training Deep Neural Networks with Noisy Labels

話接上文,MAE雖然是一種noise robust的損失函式,但是在深度學習中,因為MAE的梯度不是1就是-1,所有樣本梯度scale都相同,缺乏對樣本難易程度和模型置信度的刻畫,因此MAE很難收斂。

作者提出了一種融合MAE和Cross Entropy的方案,話不多說直接上Loss

\[L_{q}(f(x),y_j) = \frac{1-f_j(x)^q}{q} \]

作者使用了negative box-cox來作為損失函式,乍看和MAE沒啥關係。不過改變q的取值,就會發現玄妙所在

  • q->1: \(L=1-f_j(x)\), 就是MAE Loss
  • q->0: 根據洛必達法則,對分子分母同時求導,就會得到\(L=-log(f_j(x))\), 就是Cross Entropy

所以GCE損失函式透過控制q的取值,在MAE和CrossEntropy中尋找折中點。這個和Huber Loss的設計有些相似,只不過Huber是顯式的用alpha權重來融合RMSE和MAE,而GCE是隱式的融合。q->1, 對噪聲的魯棒性更好,但更難收斂。作者還提出了截斷GCE,對過大的loss進行截斷,這裡就不細說了~

pytorch實現如下,TF實現見文首連結

class GeneralizeCrossEntropy(nn.Module):
    def __init__(self, q=0.7):
        super(GeneralizeCrossEntropy, self).__init__()
        self.q = q

    def forward(self, logits, labels):
        # Negative box cox: (1-f(x)^q)/q
        labels = torch.nn.functional.one_hot(labels, num_classes=logits.shape[-1])
        probs = F.softmax(logits, dim=-1)
        loss = (1 - torch.pow(torch.sum(labels * probs, dim=-1), self.q)) / self.q
        loss = torch.mean(loss)
        return loss

Symmetric Cross Entropy(SCE)

Symmetric Cross Entropy for Robust Learning with Noisy Labels

作者是從交叉熵的另一個含義出發, 最小化交叉熵實際是為了最小化預測分佈和真實分佈的KL散度, 二者關聯如下,其中H(y)是真實標籤的資訊熵是個常數

\[\begin{align} KL(y||f(x)) &= \sum ylog(f(x)) - \sum ylog(y) \\ & = H(y, f(x)) - H(y) = CrossEntropy(y, f(x)) - H(y) \end{align} \]

考慮KL散度是非對稱的,KL(y||f(x))!=KL(f(x)||y), 前者度量的是使用預測分佈對資料進行編碼導致的資訊損失。然而當y本身存在噪聲時,y可能不是正確標籤,f(x)才是,這時就需要考慮另一個方向KL散度KL(f(x)||y)。於是作者使用對稱KL對應的對稱交叉熵(SCE)作為損失函式

\[SCE =CE + RCE = H(y,f(x)) + H(f(x),y) \\ = \sum_j y_jlog(f_j(x)) + \sum_j f_j(x)log(y_j) \]

看到這裡多少會有一種作者又拍腦袋了的感覺>.<.不過只需要對RCE的部分做下變換就豁然開朗了。以二分類為例,log(0)無法計算用常數A代替

\[RCE= H(f(x),y) = f_1(x) * log(1) + (1-f_1(x)) *log(0) = A(1-f_1(x)) \]

RCE的部分就是一個MAE!所以SCE本質上是顯式的融合交叉熵和MAE!pytorch實現如下,TF實現見文首連結

class SymmetricCrossEntropy(nn.Module):
    def __init__(self, alpha=0.1, beta=1):
        super(SymmetricCrossEntropy, self).__init__()
        self.alpha = alpha
        self.beta = beta
        self.epsilon = 1e-10

    def forward(self, logits, labels):
        # KL(p|q) + KL(q|p)
        labels = torch.nn.functional.one_hot(labels, num_classes=logits.shape[-1])
        probs = F.softmax(logits, dim=-1)
        # KL
        y_true = torch.clip(labels, self.eps, 1.0 - self.eps)
        y_pred = probs
        ce = -torch.mean(torch.sum(y_true * torch.log(y_pred), dim=-1))

        # reverse KL
        y_true = probs
        y_pred = torch.clip(labels, self.eps, 1.0 - self.eps)
        rce = -torch.mean(torch.sum(y_true * torch.log(y_pred), dim=-1))

        return self.alpha * ce + self.beta * rce

Peer Loss

  • Peer Loss Functions:Learning from Noisy Labels without Knowning Noise Rates
  • NLNL: Negative Learning for Noisy Labels

Peer Loss相比GCE和SCE只適用於Cross Entropy, 它的設計更加靈活。每個樣本的損失函式由常規loss和隨機label的loss加權得到,權重為alpha,這裡的loss支援任意的分類損失函式。隨機label作者透過打亂一個batch裡面的label順序得到~

原理上感覺Peer Loss和NLNL很是相似都是negative learning的思路。對比下二者的損失函式,PL是最小化帶噪標籤y的損失的同時,最大化模型在隨機標籤上的損失。NL是直接最大化模型在非真實標籤y上的損失。本質上都是negative learning,模型學習的不是x是什麼,而是x不是什麼,透過推動所有不正確分類的p->0,來得到正確的標籤。從這個邏輯上說感覺Peer Loss和NLNL在高維的多分類場景下應該有更好的表現~

\[PL(f(x),y) = L(f(x),y) - \alpha L(f(x),\tilde{y}) \]

\[NL(f(x),y) = L(1-f(x), \tilde{y}) \]

pytorch實現如下,TF實現見文首連結

class PeerLoss(nn.Module):
    def __init__(self, alpha=0.5, loss):
        super(PeerLoss, self).__init__()
        self.alpha = alpha
        self.loss = loss

    def forward(self, preds, labels):
        index = list(range(labels.shape[0]))
        rand_index = random.shuffle(index)
        rand_labels = labels[rand_index]
        loss_true = self.loss(preds, labels)
        loss_rand = self.loss(preds, rand_labels)
        loss = loss_true - self.alpha * loss_rand
        return loss

Bootstrap Loss

Training Deep Neural Networks on Noisy Labels with Bootstrapping

Bootstrap Loss是從預測一致性的角度來降低噪聲標籤對模型的影響,作者給了soft和hard兩種損失函式。

soft Bootstrap是在Cross Entropy的基礎上加上預測熵值,在最小化預測誤差的同時最小化機率熵值,推動機率趨近於0/1,得到更置信的預測。這裡其實用到了之前在半監督時提到的最小熵原則(小樣本利器3. 半監督最小熵正則)也就是推動分類邊界遠離高密度區。

對噪聲標籤,模型初始預估的熵值會較大(p->0.5), 因為加入了熵正則項,模型即便不去擬合噪聲標籤,而是向正確標籤移動(提高預測置信度降低熵值),也會降低損失函式.不過這裡感覺熵正則的引入也有可能使得模型預測置信度過高而導致過擬合

\[L_{soft} = \sum (\beta y_i + (1-\beta) p_i) log(p_i) \]

而Hard Bootstrap是把以上的預測機率值替換為預測機率最大的分類,Hard相比Soft更加類似label smoothing。舉個例子:當真實標籤為y=0,噪聲標籤y=1,預測機率為[0.7,0.3]時,\(\beta=0.9\)時Bootstrap擬合的y實際為[0.1,0.9], 會降低錯誤標籤的置信度,給模型學習其他標籤的機會。而當模型預測和標籤一致時y值不變,所以不會對正確有樣本有太多影響,效果上作者評估也是Hard Bootstrap的效果要顯著更好~

\[L_{hard} = \sum (\beta y_i + (1-\beta) argmx(p_i)) log(p_i) \]

pytorch實現如下,TF實現見文首連結

class BootstrapCrossEntropy(nn.Module):
    def __init__(self, beta=0.95, is_hard=0):
        super(BootstrapCrossEntropy, self).__init__()
        self.beta = beta
        self.is_hard = is_hard

    def forward(self, logits, labels):
        # (beta * y + (1-beta) * p) * log(p)
        labels = F.one_hot(labels, num_classes=logits.shape[-1])
        probs = F.softmax(logits, dim=-1)
        probs = torch.clip(probs, self.eps, 1 - self.eps)

        if self.is_hard:
            pred_label = F.one_hot(torch.argmax(probs, dim=-1), num_classes=logits.shape[-1])
        else:
            pred_label = probs
        loss = torch.sum((self.beta * labels + (1 - self.beta) * pred_label) * torch.log(probs), dim=-1)
        loss = torch.mean(- loss)
        return loss

對更多降噪loss感興趣的朋友望過來https://github.com/subeeshvasu/Awesome-Learning-with-Label-Noise

又到年末填坑時間,爭取把今年寫了一半的草稿都補完,衝鴨!


Reference

  1. https://zhuanlan.zhihu.com/p/147371861
  2. https://blog.csdn.net/suredied/article/details/113528384
  3. https://zhuanlan.zhihu.com/p/370775044
  4. https://zhuanlan.zhihu.com/p/569526954
  5. https://zhuanlan.zhihu.com/p/299404214

相關文章