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曲線,下凹的稍平緩)
如下圖所示