我们平时训练一个辨别好坏的分类模型,会遇到两个棘手的问题:

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、模型还可以进行细致的调参

并且,同样的演算法对不同的数据集的影响是有很大差异的,我们要适应这一点,大家可以试试在这份数据的基础上改一下正负样本比例或者是特征的个数,看一下数据集对结果的影响。

以上就是这次的简单实验<(‵^′)>

推荐阅读:

相关文章