目录
数据集准备
导入数据集
数据可视化
处理非平衡数据集
建模与调参
建模
调参
网格搜索(GridSearchCV)
随机搜索(RandomizedSearchCV)
贝叶斯优化
GridSearchCV例子
optuna框架例子
实验结果
随着信用卡在当今交易中的普遍使用,相关的欺诈行为不可避免地发生,并造成相当大的金钱损失。为了解决这个问题,本文使用了一个包含合法信用卡交易以及欺诈交易的数据集来寻求一个有效的预测方案。本文通过对交易数据的处理和分析,发现数据是不平衡的,因此采用分层抽样和过采样的方法对不平衡数据集进行更为可靠的分析。同时,由于抽样的随机性,采用交叉验证的方法进行最终的模型评估与选择。在此基础上,利用五种决策树相关算法分别建立了模型并进行调参。
机器学习新手可以根据这篇文章,大致了解从数据集处理到建模,再到调参和模型选择一系列的过程。
本文使用的数据集是从kaggle 上下载的信用卡欺诈数据集,是一个很典型的非平衡的数据集。
https://www.kaggle.com/mlg-ulb/creditcardfraud
本文的数据处理过程在google colab上进行,这个需要科学上网才能使用,他是一个在线的notebook,能够支持gpu、tpu,因此很适合进行机器学习和深度学习项目的开展。
首先把数据集拖至谷歌的云端以备之后的使用。
在google colab 中新建笔记本便可以开始进行实验。导入谷歌的drive包,并进行mount操作,
from google.colab import drive
drive.mount('/content/drive')
这个运行后会出现以下界面,点击链接进行登陆授权会获得授权钥匙,将授权钥匙填入回车后便会连接到谷歌的云盘。
在右侧的文件里就会连接到云盘,在代码中便可以直接导入这个数据集。
import pandas as pd
df=pd.read_csv("/content/drive/MyDrive/creditcard.csv")
df.head() ##Return the first 5 rows
可以来简单看一下这个数据集的一些特征从而更好地理解这个数据集。
检查是否有空值:
df.isnull().sum().max()
显示所有的属性:
df.columns
显示欺诈与非欺诈数据的比例:
print('No Frauds', round(df['Class'].value_counts()[0]/len(df) * 100,2), '% of the dataset')
print('Frauds', round(df['Class'].value_counts()[1]/len(df) * 100,2), '% of the dataset')
这个数据集包含2013年9月欧洲持卡人通过信用卡进行的交易。此数据集显示两天内发生的交易,其中284807个交易中有492个欺诈。数据集高度不平衡,正类(欺诈)占所有交易的0.172%。
数据集只包含PCA变换结果的数值输入变量。遗憾的是,由于保密问题,数据集无法提供原始功能和更多有关数据的背景信息。特征V1,V2,…V28是用PCA得到的主成分,唯一没有用PCA变换的特征是“时间”和“数量”。功能“Time”包含每个事务和数据集中第一个事务之间经过的秒数。功能“Amount”是交易金额,此功能可用于依赖成本敏感学习。功能“Class”是响应变量,如果是欺诈,它的值为1,否则为0。
import seaborn as sns
colors = ["#0101DF", "#DF0101"]
sns.countplot('Class', data=df, palette=colors)
plt.title('Class Distributions \n (0: No Fraud || 1: Fraud)', fontsize=14)
可以从这里看出这个数据集是一个很典型的非平衡数据集,所以一开始需要考虑如何处理这个非平衡性,让模型达到更好的效果,大致查到这些方法:
1.收集更多的数据: 好处:更够揭露数据类别的本质差别,增加样本少的数目以便后面的数据重采样。
2.尝试改变性能评价标准:
当数据不平衡时,准确度已经失去了它原有的意义,
可以参考的度量标准有:1> 混淆矩阵CM 2>精度 3>召回率 4>F1 分数(权衡精度和召回率);5.Kappa 6,ROC曲线3.重采样数据:
1,拷贝一部分样本偏少的数据多分,已达到平衡(过采样);
2,删除一部分样本偏多的数据,以使得达到平衡(欠采样);
在实际中,过采样和欠采样都会使用的。
在测试中,如果样本总数比较多,可以用欠采样的数据进行测试,如果样本总数比较少,可以用过采样的数据进行测试;另外应该测试随机采样的数据和非随机采样的数据,同时,测试不同比例正负样本的数据。4.生成合成数据:
最简单的是,随机采样样本数目比较少的属性,
另外一个比较出名的方法为:SMOTE:它是一种过采样的方法,它从样本比较少的类别中创建新的样本实例,一般,它从相近的几个样本中,随机的扰动一个特征,5.使用不同的算法:
不要试图用一个方法解所有的问题,尝试一些其他不同的方法,比如决策树一般在不平衡数据集上表现的比较的好。6.尝试惩罚模型:
意思就是添加新的惩罚项到cost函数中,以使得小样本的类别被判断错误的cost更大,迫使模型重视小样本的数据。
比如:带惩罚项的SVM7.使用不同的视角:
不平衡的数据集,有专门的邻域和算法做这个,可以参考他们的做法和术语。
比如:异常检测。8.尝试新的改进:
比如:1.把样本比较多的类别,分解为一些更多的小类别,比如:原始我们想区分数字0和其它数字这二分类问题,我们可以把其它数字在分为9类,变成0–9的分类问题
这个数据集因为是欧洲的银行发布的,所以第一个方法中的收集更多数据是不可行的;第二个方法中的改变性能评价标准是有必要的,通常我们将precision作为衡量模型好坏的标准,但由于该数据集中的证样本数量属实太少,判断错误若干个也不会大幅度影响precision,于是此处决定用ROC曲线、PR曲线作为评价标准;第三个方法的重采样也是很好的尝试点,需要说明的是,第四的方法所说的人工生成数据集也是重采样的方法,他通过已存在的数据集根据k近邻的方法生成若干相似的数据,也是一种过采样的方法,而SMOTE方法在后文也会用到;第五点说到采用多种算法,而此文也将使用多个决策树基础的算法进行预测,最后得到表现较好的模型,并且决策树算法中也自带一些惩罚机制,也就是谈到的第六点;后面两点中,异常检测在本文不作考虑,而该数据集只有二分类所以第八点也不适用。
在训练模型之前,我们将数据集分为层次化的训练和测试集,以便它们能够保留与原始数据集相同的正负类比例。该技术在scikit学习库中实现,用于在Python中进行机器学习,因此,我们所做的工作与使用train_test_split函数时定义“stratify=y”即可。
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X.values, y.values, stratify=y, test_size = 0.25, random_state = 1)
对比过采样和欠采样,欠采样的总样本数量较少,容易出现过拟合,因此在此处,我们使用过拟合方法,对正样本(欺诈交易)进行人工扩充。(如果采用欠采样的方法,则是在负样本中去与正样本随机抽取相同数量的数据构成一个总数量较小的新数据集,代码可自行搜索此处不作赘述)。SMOTE方法有很多种变体,本文使用最基础的,若想尝试其他方法,可以搜索相关原理,使用方法与smote相同。
from imblearn.over_sampling import SMOTE, BorderlineSMOTE, SVMSMOTE, ADASYN
sm = SMOTE(random_state = 1)
X_train_sm, y_train_sm = sm.fit_sample(X_train, y_train)
X_test_sm = np.array(X_test)
y_test_sm = np.array(y_test)
这个部分内容是如何使用数据集训练模型,同时通过一些方法优化模型的超参数。
先写一个评分函数,对模型结果可以看得更直观。
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report, f1_score
def print_score(label, prediction, train=True):
if train:
clf_report = pd.DataFrame(classification_report(label, prediction, output_dict=True))
print("Train Result:\n================================================")
print(f"Accuracy Score: {accuracy_score(label, prediction) * 100:.2f}%")
print("_______________________________________________")
print(f"Classification Report:\n{clf_report}")
print("_______________________________________________")
print(f"Confusion Matrix: \n {confusion_matrix(y_train, prediction)}\n")
elif train==False:
clf_report = pd.DataFrame(classification_report(label, prediction, output_dict=True))
print("Test Result:\n================================================")
print(f"Accuracy Score: {accuracy_score(label, prediction) * 100:.2f}%")
print("_______________________________________________")
print(f"Classification Report:\n{clf_report}")
print("_______________________________________________")
print(f"Confusion Matrix: \n {confusion_matrix(label, prediction)}\n")
然后把训练集测试集fit进模型,以xgboost为例,代码和截图见下图,这种建模很简单,毕竟人人都是调包侠,按照标准改下参数就可以建模。
from xgboost import XGBClassifier
xgb_model_0 = XGBClassifier()
xgb_model_0.fit(X_train_sm, y_train_sm, eval_metric='aucpr')
y_pred = xgb_model_0.predict(X_test_sm)
y_pred_prob = xgb_model_0.predict_proba(X_test_sm)[:,1]
print_score(y_test_sm, y_pred, train=False)
fig, (ax1, ax2) = plt.subplots(1,2,figsize = (12,6))
#precision-recall curve
precision, recall, thresholds_pr = precision_recall_curve(y_test_sm, y_pred_prob)
avg_pre = average_precision_score(y_test_sm, y_pred_prob)
ax1.plot(precision, recall, label = " average precision = {:0.2f}".format(avg_pre), lw = 3, alpha = 0.7)
ax1.set_xlabel('Precision', fontsize = 14)
ax1.set_ylabel('Recall', fontsize = 14)
ax1.set_title('Precision-Recall Curve', fontsize = 18)
ax1.legend(loc = 'best')
#find default threshold
close_default = np.argmin(np.abs(thresholds_pr - 0.5))
ax1.plot(precision[close_default], recall[close_default], 'o', markersize = 8)
#roc-curve
fpr, tpr, thresholds_roc = roc_curve(y_test, y_pred_prob)
roc_auc = auc(fpr,tpr)
ax2.plot(fpr,tpr, label = " area = {:0.2f}".format(roc_auc), lw = 3, alpha = 0.7)
ax2.plot([0,1], [0,1], 'r', linestyle = "--", lw = 2)
ax2.set_xlabel("False Positive Rate", fontsize = 14)
ax2.set_ylabel("True Positive Rate", fontsize = 14)
ax2.set_title("ROC Curve", fontsize = 18)
ax2.legend(loc = 'best')
#find default threshold
close_default = np.argmin(np.abs(thresholds_roc - 0.5))
ax2.plot(fpr[close_default], tpr[close_default], 'o', markersize = 8)
plt.tight_layout()
调参的方法主要有三个:网格搜索、随机搜索以及贝叶斯优化。
grid search就是对网格中每个交点进行遍历,从而找到最好的一个组合。
网格的维度就是超参的个数,若有k个超参,每个超参有m个候选,那么需要遍历k^m个组合,因此它的好处是效果不错,适用于需要对整个参数空间进行搜索的情况,缺陷是计算代价非常非常大,面临维度灾难。
随机搜索顾名思义就是在超参的搜索分布中随机搜索超参进行尝试,其搜索策略为:
1. 对于搜索范围是distribution的超参数,根据给定的distribution进行随机采样
2. 对于搜搜范围是list的超参数,在给定的list中等概率采样
3. 对1,2两步中得到的n_iter组采样结果,进行遍历
注意:若给定的搜索范围均为list,则不放回抽样n_iter次
随机搜索的好处在于搜索速度快,但是容易错过一些重要的信息
由于网格搜索的计算代价非常大,而随机搜索虽然速度很快,但容易遗漏重要信息,因此我们可以采用贝叶斯优化。贝叶斯优化就是通过在初期进行超参尝试后,会逐步学习(随着从目标函数获得更多的反馈),对初始搜索空间不同部分进行调整和采样,随着时间的增长,超参的搜索范围逐步集中。因此贝叶斯优化也是借鉴了其思想,即用先验概率来在一定程度上决定后验概率;贝叶斯优化的好处是相比网格搜索,搜索的效率更高,但同时避免了随机搜索会遗漏重要信息的影响。在这里将采用一个超参数优化框架——optuna,他是一个已封装好的贝叶斯优化框架,效率高且调用时的代码简单。
接下来将举网格搜索和贝叶斯搜索的例子以供参考学习。
由于网格搜索是一种贪心算法,他会将给出的所有参数进行组合并训练,若同时训练多个参数需要非常长的时间,通常的做法是,取其中相关的一至两个参数进行训练,将得到的最优参数解固定,用于下一轮训练。但由于参数是分开训练的,所以很可能陷入局部最优。不同的参数组合得到的结果有可能有较大差别,因此这个方法并不推荐(除非有足够算力同时对所有参数进行训练)。
以默认值为初始参数,对max_depth和min_child_weight进行训练。
from sklearn.model_selection import GridSearchCV
xgb_model = xgb.XGBClassifier(
learning_rate=0.1,
gamma= 0,
subsample=0.8,
colsample_bytree=0.8,
objective='binary:logistic',
##"nthread=4,
scale_pos_weight=1,
n_estimators=xgb_cv.shape[0],
seed=0,
nthread=-1
)
xgb_params = {
"max_depth":range(3, 10, 2),
"min_child_weight":range(1, 6, 2),
}
grid = GridSearchCV(xgb_model, param_grid=xgb_params, cv=5, scoring='roc_auc')
grid.fit(X_train_sm, y_train_sm)
上一个代码段中的超算数范围为(3,10,2)意味着从3到10每2个值进行一次跳跃,因此将取到3、5、7、9,(1,6,2)同理。因为跨步较大,为了得到更好的精度,得到优化数值时,可以在其周围以更小的精度再进行一次调参,如下代码所示:
xgb_params2 = {
"max_depth":range(2, 4),
"min_child_weight":range(4, 6),
}
grid2 = GridSearchCV(xgb_model, param_grid=xgb_params2, cv=5, scoring='roc_auc')
grid2.fit(X_train, y_train)
print(grid2.best_params_)
test_predict2 = grid2.predict(X_test_sm)
print("accuracy_score:"+str(accuracy_score(y_test_sm, test_predict2)))
在max_depth和min_child_weight确定后,对subsample和colsample_bytree进行调优。
xgb_model2 = xgb.XGBClassifier(**best_params_)
xgb_params3 = {
"subsample":np.arange(0.3, 1, 0.1),
"colsample_bytree":np.arange(0.3, 1, 0.1)
}
grid3 = GridSearchCV(xgb_model2, param_grid=xgb_params3, cv=5)
grid3.fit(X_train_sm, y_train_sm)
在subsample和colsample_bytree确定后,对reg_alpha和reg_lambda调优。
xgb_model3 = xgb.XGBClassifier(**best_params_)
xgb_params5 = {
"reg_alpha":[0.1, 1],
"reg_lambda":[0.5, 1, 2]
}
grid5 = GridSearchCV(xgb_model3, param_grid=xgb_params5, cv=5)
grid5.fit(X_train_sm, y_train_sm)
以类似的方法可以对更多的参数进行调优,由此决定最终的超参数组合。
optuna的代码并不复杂,一个极简的 optuna 的优化程序中只有三个最核心的概念,目标函数(objective),单次试验(trial),和研究(study)。其中 objective 负责定义待优化函数并指定参/超参数数范围,trial 对应着 objective 的单次执行,而 study 则负责管理优化,决定优化的方式,总试验的次数、试验结果的记录等功能。
import optuna
dtrain = xgb.DMatrix(X_train_sm, label=y_train_sm)
dvalid = xgb.DMatrix(X_test_sm, label=y_test_sm)
def objective(trial):
# params specifies the XGBoost hyperparameters to be tuned
params = {
'n_estimators': trial.suggest_int('n_estimators', 400, 3000),
'max_depth': trial.suggest_int('max_depth', 10, 20),
'min_child_weight':trial.suggest_int('min_child_weight', 1, 6),
'learning_rate': trial.suggest_uniform('learning_rate', 0.01, .1),
'subsample': trial.suggest_uniform('subsample', 0.50, 1),
'colsample_bytree': trial.suggest_uniform('colsample_bytree', 0.50, 1),
'gamma': trial.suggest_int('gamma', 0, 10),
'tree_method': 'gpu_hist',
'objective': 'binary:logistic'
}
bst = xgb.train(params, dtrain)
preds = bst.predict(dvalid)
pred_labels = np.rint(preds)
# trials will be evaluated based on average_precision
average_precision = average_precision_score(y_test_sm, pred_labels)
return average_precision
study = optuna.create_study(direction='maximize')
study.optimize(objective,n_trials=20)
本实验使用了cart、randomforest、xgboost、lightgbm以及深度森林五个模型进行预测和对比,每个模型都使用了optuna的框架进行调优,最终得到的参数如下所示:
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from deepforest import CascadeForestClassifier
from xgboost import XGBClassifier
from lightgbm import LGBMClassifier
CARTC = DecisionTreeClassifier(criterion="entropy"
,random_state=3
,splitter="random"
,max_depth=7)
RFC = RandomForestClassifier(random_state=3)
XGBC = XGBClassifier(
n_estimators=2977,
max_depth=13,
min_child_weight=1,
learning_rate=0.057,
subsample=0.82,
colsample_bytree=0.66,
gamma=1,
n_jobs=-1,
n_thread=-1,
tree_method='gpu_hist',
objective='binary:logistic')
LGBC = LGBMClassifier(boosting_type='gbdt',
class_weight=None,
colsample_bytree=0.5112837457460335,
importance_type='split',
learning_rate=0.02,
max_depth=7,
metric='None',
min_child_samples=195,
min_child_weight=0.01,
min_split_gain=0.0,
n_estimators=3000,
n_jobs=4,
num_leaves=44,
objective=None,
random_state=42,
reg_alpha=2,
reg_lambda=10,
silent=True,
subsample=0.8137506311449016,
subsample_for_bin=200000,
subsample_freq=0)
DEEPF = CascadeForestClassifier(use_predictor='True',
predictor="forest",
n_bins=173,n_estimators=6,
n_trees=1000,
min_samples_leaf=1,
partial_mode='True',
delta=1.4142915557953846e-05,
verbose=1,
random_state=33)
用数据集训练模型,同时绘制ROC和PR曲线观察结果:
def results_sm_graph(balancing_technique):
print(balancing_technique)
fig, (ax1, ax2) = plt.subplots(1,2,figsize = (12,6))
model_name = ["CART","RF", "XGB", "LGB","DF"]
for clf,i in zip([CARTC,RFC,XGBC,LGBC,DEEPF], model_name):
if i != "DF" :
model=clf.fit(X_train_sm, y_train_sm)
else :
model=clf
clf.fit(X_train_sm, y_train_sm)
y_pred = model.predict(X_test_sm)
y_pred_prob = model.predict_proba(X_test_sm)[:,1]
print("#"*25,i,"#"*25)
print("Training Accuracy = {:.3f}".format(model.score(X_train_sm, y_train_sm)))
print("Test Accuracy = {:.3f}".format(model.score(X_test_sm, y_test_sm)))
print("ROC_AUC_score : %.6f" % (roc_auc_score(y_test_sm, y_pred)))
#Confusion Matrix
print(confusion_matrix(y_test_sm, y_pred))
print("-"*15,"CLASSIFICATION REPORT","-"*15)
print(classification_report(y_test_sm, y_pred))
#precision-recall curve
precision, recall, thresholds_pr = precision_recall_curve(y_test_sm, y_pred_prob)
avg_pre = average_precision_score(y_test_sm, y_pred_prob)
ax1.plot(precision, recall, label = i+ " average precision = {:0.3f}".format(avg_pre), lw = 3, alpha = 0.7)
ax1.set_xlabel('Precision', fontsize = 14)
ax1.set_ylabel('Recall', fontsize = 14)
ax1.set_title('Precision-Recall Curve', fontsize = 18)
ax1.legend(loc = 'best')
#find default threshold
close_default = np.argmin(np.abs(thresholds_pr - 0.5))
ax1.plot(precision[close_default], recall[close_default], 'o', markersize = 8)
#roc-curve
fpr, tpr, thresholds_roc = roc_curve(y_test, y_pred_prob)
roc_auc = auc(fpr,tpr)
ax2.plot(fpr,tpr, label = i+ " area = {:0.4f}".format(roc_auc), lw = 3, alpha = 0.7)
ax2.plot([0,1], [0,1], 'r', linestyle = "--", lw = 2)
ax2.set_xlabel("False Positive Rate", fontsize = 14)
ax2.set_ylabel("True Positive Rate", fontsize = 14)
ax2.set_title("ROC Curve", fontsize = 18)
ax2.legend(loc = 'best')
#find default threshold
close_default = np.argmin(np.abs(thresholds_roc - 0.5))
ax2.plot(fpr[close_default], tpr[close_default], 'o', markersize = 8)
plt.tight_layout()
results_sm_graph("Model Collection - SMOTE")
最终可以看出xgboost和lightgbm是表现较好的模型。
References
[1] Nilson Report – Card Fraud Losses Reach $28.65 Billion, 01-Oct-2020."Card Fraud Losses Reach $28.65 Billion,” https://nilsonreport.com/mention/1313/1link
[2] A. Dal Pozzolo, O. Caelen, and G. Bontempi, “When is undersampling effective in unbalanced classification tasks?,” In Joint European Conference on Machine Learning and Knowledge Discovery in Databases, pp. 200–215, 2015.
[3] F. Carcillo, Y.-A. Le Borgne, O. Caelen, Y. Kessaci, F. Oblé, and G. Bontempi, “Combining unsupervised and supervised learning in credit card fraud detection,” Information Sciences, vol. 557, pp. 317–331, 2021.
[4] A. D. Pozzolo, G. Boracchi, O. Caelen, C. Alippi, and G. Bontempi, “Credit Card Fraud Detection: A Realistic Modeling and a Novel Learning Strategy,” IEEE Transactions on Neural Networks and Learning Systems, vol. 29, no. 8, pp. 3784–3797, 2018.
[5] V. L. Parsons, “Stratified Sampling,” Wiley Online Library, 15-Feb-2017.
[6] N. V. Chawla, K. W. Bowyer, L. O. Hall, and W. P. Kegelmeyer, “SMOTE: Synthetic Minority Over-sampling Technique,” Journal of Artificial Intelligence Research, vol. 16, pp. 321–357, 2002.
[7] T. Saito and M. Rehmsmeier, “The Precision-Recall Plot Is More Informative than the ROC Plot When Evaluating Binary Classifiers on Imbalanced Datasets,” PLOS ONE, vol. 10, no. 3, 2015.
[8] L. Breiman, J. H. Friedman, R. A. Olshen, and C. J. Stone, Classification and regression trees. Boca Raton ; London ; New York: CRC Press, 2017.
[9] T. Akiba, S. Sano, T. Yanase, T. Ohta, and M. Koyama, “Optuna: A Next-generation Hyperparameter Optimization Framework,” Proceedings of the 25th ACM SIGKDD International Conference on Knowledge Discovery & Data Mining, 2019.
[10] A. M. Prasad, L. R. Iverson, and A. Liaw, “Newer Classification and Regression Tree Techniques: Bagging and Random Forests for Ecological Prediction,” Ecosystems, vol. 9, no. 2, pp. 181–199, 2006.
[11] T. Chen and C. Guestrin, “XGBoost: A Scalable Tree Boosting System,” Proceedings of the 22nd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining, 2016.
[12] G. Ke, Q. Meng, T. Finley, T. Wang, W. Chen, W. Ma, Q. Ye, and T.-Y. Liu, “LightGBM: A Highly Efficient Gradient Boosting Decision Tree,” Advances in Neural Information Processing Systems, 2017.
[13]Z.-H. Zhou and J. Feng, “Deep Forest,” National Science Review, vol. 6, no. 1, pp. 74–86, Jan. 2019.