上文已經介紹了利用過欠(過)採樣的方式來解決正負樣本不均衡的問題,本篇文章,我們介紹解決正負樣本不均衡問題的第二招——通過正負樣本的懲罰權重解決樣本不均衡。
我們先假設現在有這樣一個數據集:數據集中有100個0樣本,10個1樣本,總共有110個樣本。則0、1樣本的比例是10:1。在以下的所有方法介紹中,我們都以處理這個數據集爲例。
一、簡單粗暴法:
1、原理
先看這樣一個例子:假如我要參加一場既考語文又考數學的考試,但是我手裏有一套語文模擬卷和十套數學模擬卷,因爲我能夠練習的語文題比較少,所以我對於這套語文卷就要更加重視一些。
同理,給樣本量較少的類別的損失值賦予更高的權重,給樣本量較多的類別損失值賦予更低的權重,簡單粗暴地爲小樣本標籤增加損失函數的權值,可以讓模型更關注類別較少的樣本中所含的信息。
我們以交叉損失函數來例,詳細說明具體的執行過程。
正常的交叉熵損失函數計算公式如下所示:
Loss = −ylg( p ) − (1−y)lg(1−p)
如果對於一個1樣本,我們的模型給出該樣本是1樣本的概率爲0.3,則Loss值爲-lg(0.3)= 0.52。
如果對於一個0樣本,我們的模型給出該樣本是1樣本的概率爲0.7(即預測是0樣本的概率也爲0.3),則Loss將獲得-log(0.3)= 0.52。
可以看到,對於0、1樣本有相同的偏差情況下,模型給出的損失函數是一樣的。
對於上邊的數據集來說,既然1樣本只有0樣本的1/10,我們可以令1樣本的損失權重增加10倍,新的損失函數變成了:
Loss2 = −10ylg ( p ) − 1(1−y)lg(1−p)
對於上述1樣本來說,雖然我們的模型給出正樣本的概率爲0.3,Loss2的值則會變爲-10lg(0.3)= 5.2;對於上述的負樣本來說,loss2依舊是0.52。
這意味着,對於每個預測錯誤的1樣本,我們模型就會給予相對於0樣本來說10倍的損失值,從而使得模型對預測錯誤的正樣本有足夠的重視,保證充分利用僅有的正樣本。
2、在scikit-learn的實現
以上這個過程在大多數算法中都不用我們自己實現,在scikit-learn中,很多模型和算法都提供了一個接口,來讓我們根據實際樣本量的比例調整類別權重。
以SVM算法爲例,通過設置class_weigh
的值,來手動指定不同的類別權重,比如我們想要將正負樣本損失權重比例設置爲10:1,只需要添加這樣的參數:class_weight= {1:10,0:1}
。
當然,我們也可以進一步偷懶,只需要:class_weight = 'balanced'
,那麼SVM會將權重設置爲與不同類別樣本數量呈反比的權重來進行自動均衡處理,權重計算公式爲:樣本總數量 / (類別數 *某類樣本數量 )。
對於上邊所示的數據集來說,經過計算,0樣本的權重爲:110/(2100) = 0.55,1樣本的權重爲:110/(210)= 5.5。可以看到,0、1樣本數量的比例是10:1,計算之後0、1樣本損失權重的比例就是:0.55:5.5 = 1:10,正好和樣本數量成反比。
SVM、決策樹、隨機森林等算法都可以用這種方法來指定。
1) 首先導入需要的包以及生成一個正負樣本不均衡的數據集:
from sklearn.svm import SVC
from sklearn.model_selection import train_test_split
from sklearn.metrics import recall_score
import numpy as np
############################ 生成不均衡的分類數據集###########
from sklearn import datasets
X,Y = datasets.make_classification(n_samples = 1000,
n_features = 4,
n_classes = 2,
weights = [0.95,0.05])
train_x,test_x,train_y,test_y = train_test_split(X,Y,test_size=0.1,random_state=0)
這裏生成的0樣本和1樣本的比例是95:5。
2)不對正負樣本進行處理訓練一個SVM分類器
SVM_model = SVC(random_state = 66) #設定一個隨機數種子,保證每次運行結果不變化
SVM_model.fit(train_x,train_y)
pred1 =SVM_model.predict(train_x)
accuracy1 = recall_score(train_y,pred1)
print('在訓練集上的召回率:\n',accuracy1)
pred2 = SVM_model.predict(test_x)
accuracy2 = recall_score(test_y,pred2)
print('在測試集上的召回率:\n',accuracy2)
輸出結果:
在訓練集上的召回率:
0.56
在測試集上的召回率:
0.2
因爲0、1樣本非常不均衡,就算模型把所有的樣本都判斷爲0樣本,模型也會獲得95%的正確率,而且在實際使用中,我們更希望模型能夠把少量樣本篩選出來,比如反洗錢中的可疑交易、信用卡中的不良貸款等。
所以這裏我們用召回率來評判對正樣本的檢測效果,可以看到,在正常情況下,在測試集上的召回率只有0.2。
3)對正負樣本損失值設置不同權重訓練一個SVM分類器
SVM_model2 = SVC(random_state = 66,class_weight = 'balanced') #設定一個隨機數種子,保證每次運行結果不變化
SVM_model2.fit(train_x,train_y)
pred1 =SVM_model2.predict(train_x)
accuracy1 = recall_score(train_y,pred1)
print('在訓練集上的召回率:\n',accuracy1)
pred2 = SVM_model2.predict(test_x)
accuracy2 = recall_score(test_y,pred2)
print('在測試集上的召回率:\n',accuracy2)
輸出:
在訓練集上的召回率:
0.8
在測試集上的召回率:
0.8
可以看到,給正樣本足夠的權重之後,召回率有了明顯的上升,變爲0.8。
(這裏如果用精確度來評價,你會發現這裏精確度反而下降了,不用擔心這是模型爲了找出1樣本而誤判了一部分0樣本,這是正常現象。)
另外,有一些算法可能沒有提供接口,我們也可以訓練時在fit函數中通過sample_weight
參數進行指定。具體可以參考scikit-learn文檔。
注意:如果一不小心兩個參數都使用了,那麼最終正負樣本的權重是:class_weight*sample_weight
二、更加精細化的處理方法:
在第一種方法中,我們簡單粗暴地通過各類樣本量的反比來確定損失權重,這種方法效果已經會非常顯著了。但是,上面的改進並沒有對一些更難分類的樣本做一些處理,我們不僅希望平衡樣本,而且希望將易分類的樣本提供的loss比重減小,將難分類的樣本提供的loss比重增大。這就好比:我們不僅要更關注這套語文模擬卷,更要重點關注這套卷子裏做錯的題。Focal loss(權重動態調整)的思想就是完美地兼顧了這兩個方面。
Focal loss提出的原始想法是用來解決圖像檢測中類別不均衡的問題,這裏多說一句,圖像檢測領域是一個典型的正負樣本不均衡的領域,因爲在一張圖像中,大多數情況下,目標總是佔據圖像的一小部分,佔據圖像大部分的是背景。如果你日常處理一些樣本不均衡的數據問題,不妨看看圖像檢測領域,找找靈感。
我們依舊以爲二分類交叉熵損失函數爲例。Focal losss損失函數在的基礎上從
變成了:
仔細看這個公式,損失函數的權重是由兩個權重共同決定:
,第一個權重不必多說,就是方法一中的根據正負樣本量自定義的損失權重,重點看第二個權重。
若是對於某個1樣本來說,模型預測其爲1樣本的概率是0.9,那麼第二個權重爲:(1-0.9)^ 2 = 0.01,而如果模型預測某個樣本爲1樣本的概率是0.6,那麼第二個權重變爲:(1-0.6)^2 = 0.16。樣本預測越不準確,損失值所佔的權重就會更大。
原論文中有一張圖形象地說明了,橫座標代表預測對的概率,縱座標代表損失值,,中間紅色虛線代表預測對的概率是0.5,虛線左半邊代表模型預測偏差很大,右半邊代表模型預測偏差較小。gamma = 0時,就是第二個權重項不參與下的損失值:
可以看到,相對於圖中藍色曲線來說,隨着gamma增大,在紅線的左半邊,幾條曲線損失值差別不大,但是紅線的右半邊,損失值迅速趨近於0。
而且更巧妙的是,一般來說,如果0樣本遠比1樣本多的話,模型肯定會傾向於將樣本預測爲0這一類(全部樣本都判爲0類,模型準確率依舊很高),上邊的第二個權重也會促使模型花更多精力去關注數量較少的1樣本。
總結一句話,Focal loss就是在,進一步對比較難分類的樣本加大損失權重,使模型對分錯的樣本更加重視,從而使得模型快速收斂,且獲得更好的性能表現。
關於Focal loss的思想,scikit-learn中還沒有實現,感興趣的可以自己實現。