1. 程式人生 > 實用技巧 >ACL2020文章,用Dice Loss處理NLP任務的資料不均衡問題,Tensorflow實現

ACL2020文章,用Dice Loss處理NLP任務的資料不均衡問題,Tensorflow實現

文章連結:https://zhuanlan.zhihu.com/p/128066632 (本文大部分內容都摘抄自這篇文章,主要用作個人筆記。)

論文標題:Dice Loss for Data-imbalanced NLP Tasks

論文作者:Xiaofei Sun, Xiaoya Li, Yuxian Meng, Junjun Liang, Fei Wu, Jiwei Li

論文連結:https://arxiv.org/pdf/1911.02855.pdf

資料不均衡導致的兩個問題

  • 訓練與測試失配。佔據絕大多數的負例會支配模型的訓練過程,導致模型傾向於負例,而測試時使用的F1指標需要每個類都能準確預測;
  • 簡單負例過多。負例佔絕大多數也意味著其中包含了很多簡單樣本,這些簡單樣本對於模型學習困難樣本幾乎沒有幫助,反而會在交叉熵的作用下推動模型遺忘對困難樣本的知識。

使用交叉熵損失存在的問題

  • 大量簡單負例會在交叉熵的作用下推動模型忽視困難正例的學習,而序列標註任務往往使用F1衡量,從而在正例上預測欠佳直接導致了F1值偏低;
  • 交叉熵“平等”地看待每一個樣本,無論正負,都盡力把它們推向1(正例)或0(負例)。但實際上,對分類而言,將一個樣本分類為負只需要它的概率<0.5即可,完全沒有必要將它推向0;

解決方案

  • 基於已有的Dice Loss,提出一個自適應損失:DSC,在訓練時推動模型更加關注困難的樣本,降低簡單負例的學習度,從而在整體上提高基於F1值的效果。

效果

  • 在多個任務上實驗,包括:詞性標註、命名實體識別、問答和段落識別。F1值都取得了一定的提升;

傳統二分類交叉熵

存在問題:對每個樣本,CE對它們都一視同仁,不管當前樣本是簡單還是複雜。當簡單樣本有很多的時候,模型的訓練就會被這些簡單樣本佔據,使得模型難以從複雜樣本中學習。

一種簡單的改進方法是,降低模型在簡單樣本上的學習速率,從而得到下述加權交叉熵損失:

存在問題:對不同樣本,我們可以設定不同的權重,從而控制模型在該樣本上學習的程度。但是此時,權重的選擇又變得比較困難。

DSC

  • DSC是一種用於衡量兩個集合之間相似度的指標:

令A是所有模型預測為正的樣本的集合,令B為所有實際上為正類的樣本集合,那麼DSC就可以重寫為:

其中,TP是True Positive,FN是False Negative,FP是False Negative,D是資料集,f是一個分類模型。於是,在這個意義上,DSC是和F1等價的。推導公式如下:

然而上述表示式是離散的。為此,我們需要把上述DSC表示式轉化為連續的版本,從而視為一種soft F1,對單個樣本x,我們直接定義它的DSC:

可以看到,若x是負類,那麼它的DSC就為0,從而不會對訓練有貢獻。為了讓負類也能有所貢獻,我們增加一個平滑項:

但這樣一來,又需要我們根據不同的資料集手動地調整平滑項。而且,當easy-negative樣本很多的時候,即便使用上述平滑項,整個模型訓練過程仍然會被它們主導。基於此,我們使用一種“自調節”的DSC:

事實上,這比較類似Focal Loss (FL),即降低已分好類的樣本的學習權重:

不過,FL即使能對簡單樣本降低學習權重,但是它本質上仍然是在鼓勵簡單樣本趨向0或1,這就和DSC有了根本上的區別。因此,我們說DSC通過“平衡”簡單樣本和困難樣本的學習過程,從而提高了最終的F1值(因為F1要求各類都有比較好的結果)。

Dice Loss(DL)與Tversky Loss(TL)

總結

本文使用現有的Dice Loss,並提出了一種新型的自適應損失DSC,用於各種資料分佈不平衡的NLP任務中,以緩解訓練時的交叉熵與測試時的F1的失配問題。實驗表明,使用該損失可以顯著提高標註任務、分類任務的F1值,並且也說明了F1效果的提升與資料不平衡的程度、資料量大小有密切的關係。

Tensorflow版本Dice Loss

import tensorflow as tf
tf.enable_eager_execution()

def dice_loss(n_classes, logits, label, smooth=1.e-5):
    epsilon = 1.e-6
    alpha = 2.0   # 這個是dice coe的係數,見下邊的解釋
    y_true = tf.one_hot(label, n_classes)
    softmax_prob = tf.nn.softmax(logits)
    print("{}".format(softmax_prob.numpy()))
    y_pred = tf.clip_by_value(softmax_prob, epsilon, 1. - epsilon)

    y_pred_mask = tf.multiply(y_pred, y_true)
    common = tf.multiply((tf.ones_like(y_true) - y_pred_mask), y_pred_mask)
    nominator = tf.multiply(tf.multiply(common, y_true), alpha) + smooth
    denominator = common + y_true + smooth
    dice_coe = tf.divide(nominator, denominator)
    return tf.reduce_mean(tf.reduce_max(1 - dice_coe, axis=-1))

n_classes = 6
logits = tf.constant([[0.1, 0.2, 0.8, 1.2, 1.24, 2.96],
                      [0.1, 0.2, 0.8, 1.2, 1.24, 2.96]])
for label in range(0, n_classes):
    loss = dice_loss(n_classes, logits, [label, label+1])
    print("{}".format(loss.numpy()))

原論文中的公式11,這裡有係數2(loss曲線,下凹的更厲害)

原論文中的表2,這裡沒有係數2(loss曲線,下凹的稍平緩)

如下圖所示