減少信用卡欺詐識別誤殺:基於代價敏感的AdaCost演算法(改寫skle

germo發表於2021-09-09

我們平時訓練一個辨別好壞的分類模型,會遇到兩個棘手的問題:
1、類別不平衡,壞的樣本量往往遠遠小於好的;
2、錯分代價不均等,漏殺和誤殺帶來的影響視具體問題完全不同。
本文改寫了sklearn中的AdaBoost的原始碼,使大家可以自由決定漏殺和誤殺對分類器的影響。採用的樣例資料是kaggle信用卡欺詐資料集。

代價矩陣

——讓我們一起復習一下西瓜書第35-36頁的內容
在一些場景下,當分類器將一個樣本錯分時,造成的後果是不同的,
有的時候需要保證正確:醫生把患者誤診為健康 比 把健康人誤診為患者後果更嚴重;
有的時候需要保證全面:門禁系統寧可冤枉好人也不能漏掉壞人;
所以可以根據分類任務的領域知識設定一個代價矩陣:


預測0類 預測1類
真實0類 0 cost_01
真實1類 cost_10 0

若預測=真實,無代價,即cost_11 = cost_00 = 0,
若將0判為1的損失更大,則 cost_01 > cost_10, 反之亦然,
損失程度相差越大,cost_01和 cost_10值的差別就要越大。

AdaBoost 和 AdaCost演算法原理比較

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

圖片描述

AdaBoost的訓練過程.JPG

從Adaboost步驟7的公式可以看到:
對正確分類的例項,樣本權重調整係數為exp(-αt),都以相同的比例降低權重,
對所有的誤分類例項,樣本權重調整係數為exp(αt),都以相同的比例增加權重。

但是在AdaCost中,對於代價高的誤分類樣本,我們應該大大提高其權重。
實現方法是在調整係數的基礎上再乘以一個代價係數βi,所以AdaCost的樣本權重按照如下公式進行更新:


圖片描述

AdaCost樣本權重更新公式.png

原始碼改寫

看了上面的公式比較,改寫的方法也就明晰了,只需要在調節樣本權重的公式中,乘以代價係數βi,βi應該是一個代價調整函式,根據每次迭代每個樣本的誤判情況調整。
我的改寫方法也是非常簡單粗暴了,為了方便起見,令代價小的誤判和正確的判斷調整係數都為1,直接對代價高的誤判賦一個大於1的值,起到調高係數的作用。(見下面的程式碼)

定義AdaCostClassifier類,要做的三個工作:
1、繼承AdaBoostClassifier
2、改寫類中的self._boost_real 方法,因為權重更新的公式在這裡
3、在類中增加定義代價函式,返回的結果即使代價因子,供2中的更新公式使用

#!/usr/bin/env python3# -*- coding:utf-8 -*-import numpy as npfrom numpy.core.umath_tests import inner1dfrom sklearn.ensemble import AdaBoostClassifierclass 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)

信用卡資料實戰

資料集地址:
資料量284807,31個特徵
284315個正例,492個負例,嚴重不平衡,
控制相同的訓練集和測試集,相同的引數(基分類器的個數n_estimators=100),訓練原始的AdaBoost 和改寫後的AdaCoost並比較兩者的結果:

#!/usr/bin/env python3# -*- coding:utf-8 -*-import pandas as pdfrom adacost import AdaCostClassifier  # 上節定義好的類,我把它單獨放在了adacost.py 檔案中from sklearn.ensemble import AdaBoostClassifierfrom sklearn.model_selection import train_test_splitfrom sklearn.metrics import recall_score, precision_score, f1_scoredef 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), 'n')    returnif __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%,不能算作一個好的欺詐檢測模型。

AdaBoost recall precision f1score

0.74 0.91 0.81
AdaCost我試了三個代價係數值,得到的結果如下:
AdaCost recall precision f1score
β=1.25 0.56 0.95 0.71
β=1.45 0.44 0.98 0.54
β=1.8 0.37 1.0 0.60

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

結語

其實這不是一個完美的最佳化,畢竟最佳化完以後f1score還下降了,可以完善的地方還有:
1、對於正判的樣本,樣本權重可以降低的少一點
2、模型還可以進行細緻的調參

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

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



作者:雙er
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1868/viewspace-2815975/,如需轉載,請註明出處,否則將追究法律責任。

相關文章