我們平時訓練一個辨別好壞的分類模型,會遇到兩個棘手的問題:

1、類別不平衡,壞的樣本量往往遠遠小於好的;2、錯分代價不均等,漏殺和誤殺帶來的影響視具體問題完全不同。本文改寫了sklearn中的AdaBoost的源碼,使大家可以自由決定漏殺和誤殺對分類器的影響。採用的樣例數據是kaggle信用卡欺詐數據集。

代價矩陣

——讓我們一起複習一下西瓜書第35-36頁的內容

在一些場景下,當分類器將一個樣本錯分時,造成的後果是不同的,

有的時候需要保證正確:醫生把患者誤診為健康 比 把健康人誤診為患者後果更嚴重;

有的時候需要保證全面:門禁系統寧可冤枉好人也不能漏掉壞人;

所以可以根據分類任務的領域知識設定一個代價矩陣:

若預測=真實,無代價,即cost_11 = cost_00 = 0,

若將0判為1的損失更大,則 cost_01 > cost_10, 反之亦然,

損失程度相差越大,cost_01和 cost_10值的差別就要越大。

AdaBoost 和 AdaCost演算法原理比較

AdaBoost是基於boost方法的樹集成演算法,核心策略是每一輪迭代完成,更新樣本的權重和基分類器權重,這兩個權重是理解演算法以及看懂源碼的關鍵,先回顧下AdaBoost的訓練過程:

從Adaboost步驟7的公式可以看到:

對正確分類的實例,樣本權重調整係數為exp(-αt),都以相同的比例降低權重,

對所有的誤分類實例,樣本權重調整係數為exp(αt),都以相同的比例增加權重。

但是在AdaCost中,對於代價高的誤分類樣本,我們應該大大提高其權重。

實現方法是在調整係數的基礎上再乘以一個代價係數βi,所以AdaCost的樣本權重按照如下公式進行更新:

源碼改寫

看了上面的公式比較,改寫的方法也就明晰了,只需要在調節樣本權重的公式中,乘以代價係數βi,βi應該是一個代價調整函數,根據每次迭代每個樣本的誤判情況調整。

我的改寫方法也是非常簡單粗暴了,為了方便起見,令代價小的誤判和正確的判斷調整係數都為1,直接對代價高的誤判賦一個大於1的值,起到調高係數的作用。(見下面的代碼)

定義AdaCostClassifier類,要做的三個工作:

1、繼承AdaBoostClassifier

2、改寫類中的self._boost_real 方法,因為權重更新的公式在這裡3、在類中增加定義代價函數,返回的結果即使代價因子,供2中的更新公式使用

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import numpy as np
from numpy.core.umath_tests import inner1d
from sklearn.ensemble import AdaBoostClassifier

class AdaCostClassifier(AdaBoostClassifier):
def _boost_real(self, iboost, X, y, sample_weight, random_state):
"""Implement a single boost using the SAMME.R real algorithm."""
estimator = self._make_estimator(random_state=random_state)
estimator.fit(X, y, sample_weight=sample_weight)

y_predict_proba = estimator.predict_proba(X)

if iboost == 0:
self.classes_ = getattr(estimator, classes_, None)
self.n_classes_ = len(self.classes_)

y_predict = self.classes_.take(np.argmax(y_predict_proba, axis=1),
axis=0)

incorrect = y_predict != y

estimator_error = np.mean(
np.average(incorrect, weights=sample_weight, axis=0))

if estimator_error <= 0:
return sample_weight, 1., 0.

n_classes = self.n_classes_
classes = self.classes_
y_codes = np.array([-1. / (n_classes - 1), 1.])
y_coding = y_codes.take(classes == y[:, np.newaxis])

proba = y_predict_proba # alias for readability
proba[proba < np.finfo(proba.dtype).eps] = np.finfo(proba.dtype).eps

estimator_weight = (-1. * self.learning_rate
* (((n_classes - 1.) / n_classes) *
inner1d(y_coding, np.log(y_predict_proba))))

# 樣本更新的公式,只需要改寫這裡
if not iboost == self.n_estimators - 1:
sample_weight *= np.exp(estimator_weight *
((sample_weight > 0) |
(estimator_weight < 0)) *
self._beta(y, y_predict)) # 在原來的基礎上乘以self._beta(y, y_predict),即代價調整函數
return sample_weight, 1., estimator_error

# 新定義的代價調整函數
def _beta(self, y, y_hat):
res = []
for i in zip(y, y_hat):
if i[0] == i[1]:
res.append(1) # 正確分類,係數保持不變,按原來的比例減少
elif i[0] == 1 and i[1] == -1:
res.append(1.25) # 在信用卡的情景下,將好人誤殺代價應該更大一些,比原來的增加比例要高
elif i[0] == -1 and i[1] == 1:
res.append(1) # 將負例誤判為正例,代價不變,按原來的比例增加
else:
print(i[0], i[1])

return np.array(res)

信用卡數據實戰

數據集地址:

Credit Card Fraud Detection

數據量284807,31個特徵

284315個正例,492個負例,嚴重不平衡,

控制相同的訓練集和測試集,相同的參數(基分類器的個數n_estimators=100),訓練原始的AdaBoost 和改寫後的AdaCoost並比較兩者的結果:

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import pandas as pd

from adacost import AdaCostClassifier # 上節定義好的類,我把它單獨放在了adacost.py 文件中
from sklearn.ensemble import AdaBoostClassifier

from sklearn.model_selection import train_test_split
from sklearn.metrics import recall_score, precision_score, f1_score

def load_creditcard_data(): # 將數據集讀進來,注意將正例標記為1,負例標記為-1
df = pd.read_csv(creditcard.csv)
df.loc[df.Class == 1, Class] = -1
df.loc[df.Class == 0, Class] = 1
print(df.shape)
print(df.Class.value_counts())
return df.drop(Class, axis=1), df[Class]

def compare(clfs): # 比較不同分類器的結果
for clf in clfs:
y_pred = clf.predict(X_test)
print(pd.Series(y_pred).value_counts())
print(recall_score(y_test, y_pred, pos_label=-1),
precision_score(y_test, y_pred, pos_label=-1),
f1_score(y_test, y_pred, pos_label=-1),
)
return

if __name__ == __main__:
X, y = load_creditcard_data()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
print(pd.Series(y_test).value_counts())

clf1 = AdaBoostClassifier(n_estimators=100)
clf1.fit(X_train, y_train)

clf2 = AdaCostClassifier(n_estimators=100)
clf2.fit(X_train, y_train)

compare([clf1, clf2])

模型評估

採用三個評價指標來評估模型:

1、召回率 recall,

2、準確率 precision,本案例假設我們想盡量減少模型誤判,而對漏判比較

寬容,所以對precision的要求更高

3、f1score

不加代價的AdaBoost:

整體效果是可以的,但是真實情況下91%的準確率,即好人誤殺率達到了9%,不能算作一個好的欺詐檢測模型。

AdaCost我試了三個代價係數值,得到的結果如下:

比較上面兩個表可以看出

隨著代價係數的提高,精確率可以一直提升到1,但同時也犧牲掉了一部分的召回率,我們要根據自己的業務目的,權衡好兩個指標,選擇一個合理的代價係數。

結語

其實這不是一個完美的優化,畢竟優化完以後f1score還下降了,可以完善的地方還有:

1、對於正判的樣本,樣本權重可以降低的少一點

2、模型還可以進行細緻的調參

並且,同樣的演算法對不同的數據集的影響是有很大差異的,我們要適應這一點,大家可以試試在這份數據的基礎上改一下正負樣本比例或者是特徵的個數,看一下數據集對結果的影響。

以上就是這次的簡單實驗<(‵^′)>

推薦閱讀:

相关文章