本文最初发表于 Towards Data Science 博客,经原作者 Kurtis Pykes 授权,InfoQ 中文站翻译并分享。
机器学习是人工智能的一个子集,它赋予了系统从经验中自动学习和改进的能力,无需进行显式编程。如此说来,我们(人类)已经可以向计算机提供大量的数据集,让计算机学习模式,这样它在面对一个或多个新实例时,能够学习如何作出决定——当我发现这一见解时,我立即知道世界即将发生改变。
报告显示,欺诈行为给全球经济造成了 3.89 万亿英镑的损失,在过去十年里损失上升了 56%。
——Crowe UK
作为欺诈行为的受害者,我萌生了防止这种情况再次发生在我(以及其他任何人)身上的想法,这促使我开始思考一个与我所习惯的完全不同的领域。
欺诈检测问题
在机器学习术语中,诸如欺诈检测之类的问题,可以被归类为分类问题,其目标是预测离散标签 0 或 1,其中,0 通常表示交易是非欺诈性的,1 表示交易似乎是欺诈性的。
因此,这个问题要求从业人员构建足够智能的模型,以便能够在给定各种用户交易数据的情况下,正确地检测出欺诈性和非欺诈性的交易。为了保护用户隐私,这些交易数据通常都经过匿名化处理。
由于完全依赖基于规则的系统并不是最有效的策略,因此,机器学习已成为许多金融机构用来解决这一类问题的方法。
这个问题(欺诈检测)之所以如此具有挑战性,是因为当我们在现实世界对其进行建模时,发生的大多数交易都是真实的交易,只有很小一部分是欺诈行为。这意味着我们要处理数据不平衡的问题:我写的文章《过采样和欠采样》(Oversampling and Undersampling)就是处理这一类问题的一种方法。然而,对于这篇文章,我们的主要重点将是开始我们的机器学习框架来检测欺诈行为——如果你不熟悉构建自己的框架,那你可能需要在阅读本文之前,先阅读这篇文章《构建机器学习项目》(Structuring Machine Learning Projects)。
数据
这些数据是由IEEE 计算智能协会(IEEE Computational Intelligence Society,IEEE-CIS)的研究人员整理出来的,用于预测欺诈性在线交易概率的任务,以二进制目标isFraud
来表示。
注:数据部分是从 Kaggle 竞赛数据部分复制而来。
数据分成两个文件identity
和transaction
,这两个文件由TransactionID
连接。但并非所有交易都有相应的身份信息。
类别特征——交易(Transaction)
ProductCD
card1
-card6
addr1
、addr2
P_emaildomain
R_emaildomain
M1
-M9
类别特征——身份信息(Identity)
DeviceType
DeviceInfo
id_12
-id_38
TransactionDT
特征是给定引用日期时间(不是实际时间戳)开始的时间间隔(timedelta)。
你可以从比赛主持人的这篇文章《数据描述(详情及讨论)》(Data Description (Details and Discussion))中了解更多有关数据的信息。
文件
train_{transaction, identity}.csv——训练集
test_{transaction, identity}.csv——测试集(你必须预测这些观察值的isFraud
值)
sample_submission.csv——正确格式的样本提交文件
构建框架
在处理任何机器学习任务时,第一步是建立一个可靠的交叉验证策略。
注:该框架背后的总体思路来自于Abhishek Thakur。
——GitHub
当面对不平衡的数据问题时,通常采用的方法是使用StratifiedKFold
,它以这样一种方式随机地分割数据,以保持相同的类分布。
我实现了 create folds,作为preprocessing.py
的一部分。
import config
import numpy as np
import pandas as pd
from sklearn.model_selection import StratifiedKFold
def read_all_data():
train_transactions = pd.read_csv(config.TRAIN_TRANSACTIONS)
train_identity = pd.read_csv(config.TRAIN_IDENTITY)
test_transactions = pd.read_csv(config.TEST_TRANSACTIONS)
test_identity = pd.read_csv(config.TEST_IDENTITY)
return train_transactions, train_identity, test_transactions, test_identity
def merge_data(df1, df2):
# merge dataframe on the index
merged_df = df1.merge(df2, how="left", on="TransactionID")
return merged_df
def create_folds(df):
# create a new column
df["kfold"] = -1
# shuffle data
df = df.sample(frac=1, random_state=42).reset_index(drop=True)
# initialize kfold
skf = StratifiedKFold(n_splits=5, shuffle=False)
for fold, (train_idx, val_idx) in enumerate(skf.split(X=df, y=df.isFraud.values)):
print(len(train_idx), len(val_idx))
df.loc[val_idx, 'kfold'] = fold
df.to_csv(config.DATA_DIR + "train_folds.csv", index=False)
if __name__ == "__main__":
train_transactions, train_identity, test_transactions, test_identity = read_all_data()
merged_test = merge_data(test_transactions, test_identity)
merged_train = merge_data(train_transactions, train_identity)
del train_transactions, train_identity, test_transactions, test_identity
# renaming test id columns
for col in merged_test.columns:
if "id" in col:
merged_test.rename(columns={col : col.replace("-", "_")}, inplace=True)
merged_test.to_csv(config.DATA_DIR + "test_df.csv", index=False)
create_folds(merged_train)
复制代码
这段代码合并了来自训练集和测试集的身份信息和交易数据,然后重命名了merded_test
数据中的列名,因为 id 列使用的是“-”而不是“_”,这将导致稍后检查以确保测试中的列名完全相同时出现问题。接下来,我们在训练数据中添加一个名为kfold
的列名,并根据它所在的 fold 设置索引,然后保存到 CSV 文件中。
你可能已经注意到,我们导入config
并将其作为通向各种交易的路径。所有的config
都是另一个脚本的变量,这样我们就不必在不同的脚本重复调用这些变量了。
# Directory Paths
DATA_DIR = "../input/"
MODEL_OUTPUT = "../models"
# Training data
TRAINING_DATA = DATA_DIR + "train_folds.csv"
TRAIN_TRANSACTIONS = DATA_DIR + "train_transaction.csv"
TRAIN_IDENTITY = DATA_DIR + "train_identity.csv"
# Test data
TEST_DATA = DATA_DIR + "test_df.csv"
TEST_TRANSACTIONS = DATA_DIR + "test_transaction.csv"
TEST_IDENTITY = DATA_DIR + "test_identity.csv"
# Categorical Features
CATEGORICAL_FEATURES = [
"ProductCD", "card1", "card2", "card3", "card4",
"card5", "card6", "addr1", "addr2", "P_emaildomain",
"R_emaildomain", "M1", "M2", "M3", "M4", "M5",
"M6", "M7", "M8", "M9", "DeviceType", "DeviceInfo",
"id_12", "id_13", "id_14", "id_15", "id_16", "id_17",
"id_18", "id_19", "id_20", "id_21", "id_22", "id_23",
"id_24", "id_25", "id_26", "id_27", "id_28", "id_29",
"id_30", "id_31", "id_32", "id_33", "id_34", "id_35",
"id_36", "id_37", "id_38"
]
复制代码
在处理机器学习问题时,以允许快速迭代的方式快速构建管道是非常重要的,因此我们将构建的下一个脚本是model_dispatcher.py
,我们将其称为分类器,而train.py
是我们的训练模型的脚本。
让我们从model_dispatcher.py
开始。
from sklearn import linear_model, ensemble
models = {"logistic_regression": linear_model.LogisticRegression(verbose=True, max_iter=1000, random_state=10),
"random_forest": ensemble.RandomForestClassifier(verbose=True, n_estimators=100, criterion="gini")}
复制代码
在这里,我们简单地导入了一个逻辑回归和随机森林,并创建了一个字典,这样我们就可以通过运行逻辑回归模型models["logistic_regression"]
来将算法调用到我们的训练脚本中。
训练脚本如下所示:
import os
import config
import model_dispatcher
import joblib
import argparse
import pandas as pd
from sklearn import preprocessing
from sklearn import metrics
def pipe(fold:int, model:str):
df = pd.read_csv(config.TRAINING_DATA)
df_test = pd.read_csv(config.TEST_DATA)
X_train = df[df["kfold"] != fold].reset_index(drop=True)
X_valid = df[df["kfold"] == fold].reset_index(drop=True)
y_train = X_train.isFraud.values
y_valid = X_valid.isFraud.values
X_train = X_train.drop(["isFraud", "kfold"], axis=1)
X_valid = X_valid.drop(["isFraud", "kfold"], axis=1)
X_valid = X_valid[X_train.columns]
label_encoders = {}
for c in config.CATEGORICAL_FEATURES:
lbl = preprocessing.LabelEncoder()
X_train.loc[:, c] = X_train.loc[:, c].astype(str).fillna("NONE")
X_valid.loc[:, c] = X_valid.loc[:, c].astype(str).fillna("NONE")
df_test.loc[:, c] = df_test.loc[:, c].astype(str).fillna("NONE")
lbl.fit(X_train[c].values.tolist() +
X_valid[c].values.tolist() +
df_test[c].values.tolist())
X_train.loc[:, c] = lbl.transform(X_train[c].values.tolist())
X_valid.loc[:, c] = lbl.transform(X_valid[c].values.tolist())
label_encoders[c] = lbl
# data is ready to train
clf = model_dispatcher.models[model]
clf.fit(X_train.fillna(0), y_train)
preds = clf.predict_proba(X_valid.fillna(0))[:, 1]
print(metrics.roc_auc_score(y_valid, preds))
joblib.dump(label_encoders, f"{config.MODEL_OUTPUT}/{model}_{fold}_label_encoder.pkl")
joblib.dump(clf, f"{config.MODEL_OUTPUT}/{model}_{fold}.pkl")
joblib.dump(X_train.columns, f"{config.MODEL_OUTPUT}/{model}_{fold}_columns.pkl")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--fold",
type=int
)
parser.add_argument(
"--model",
type=str
)
args = parser.parse_args()
pipe(
fold=args.fold,
model=args.model
)
复制代码
我希望你能读懂代码,但如果看不明白的话,我来总结一下这段代码所发生的的事情:将训练数据设置为列kfold
中的值,并且与我们通过的 fold 相同的值就是测试集。然后,我们对分类变量进行标签编码,并用 0 填充所有缺失值,最后将数据训练到逻辑回归模型上。
我们得到当前的 fold 的预测,并打印出ROC_AUC。
注:从目前的情况看,代码本身并不会运行,因此我们必须在运行每个 Fold 时,传递 fold 和 model 的值。
让我们看看逻辑回归模型的输出。
### Logistic Regression
# Fold 0
ROC_AUC_SCORE: 0.7446056326560758
# Fold 1
ROC_AUC_SCORE: 0.7476247589462117
# Fold 2
ROC_AUC_SCORE: 0.7395710927094167
# Fold 3
ROC_AUC_SCORE: 0.7365641912867861
# Fold 4
ROC_AUC_SCORE: 0.7115696956435416
复制代码
这些都是相当不错的结果,但让我们使用更强大的随机森林模型,看看是否还可以改善。
### Random Forest
# Fold 0
ROC_AUC_SCORE: 0.9280242455299264
# Fold 1
ROC_AUC_SCORE: 0.9281600723876517
# Fold 2
ROC_AUC_SCORE: 0.9265254015330469
# Fold 3
ROC_AUC_SCORE: 0.9224746067992484
# Fold 4
ROC_AUC_SCORE: 0.9196977372298685
复制代码
很明显,随机森林模型产生了更好的结果。让我们在 Kaggle 上进行后期提交,看看我们在排行榜上的位置。这是最重要的部分——要做到这一点,我们必须运行inference.py
。
import os
import pandas as pd
import numpy as np
import config
import model_dispatcher
from sklearn import preprocessing
from sklearn import metrics
import joblib
def predict(test_data_path:str , model_name:str, model_path:str):
df = pd.read_csv(test_data_path)
test_idx = df["TransactionID"].values
predictions = None
for FOLD in range(5):
df = pd.read_csv(test_data_path)
encoders = joblib.load(os.path.join(model_path, f"{model_name}_{FOLD}_label_encoder.pkl"))
cols = joblib.load(os.path.join(model_path, f"{model_name}_{FOLD}_columns.pkl"))
for c in encoders:
lbl = encoders[c]
df.loc[:, c] = df.loc[:, c].astype(str).fillna("NONE")
df.loc[:, c] = lbl.transform(df[c].values.tolist())
clf = joblib.load(os.path.join(model_path, f"{model_name}_{FOLD}.pkl"))
df = df[cols]
preds = clf.predict_proba(df.fillna(0))[:, 1]
if FOLD == 0:
predictions = preds
else:
predictions += preds
predictions /= 5
sub = pd.DataFrame(np.column_stack((test_idx, predictions)), columns=["TransactionID", "isFraud"])
return sub
if __name__ == "__main__":
submission = predict(test_data_path=config.TEST_DATA,
model_name="random_forest",
model_path=f"{config.MODEL_OUTPUT}/")
submission.loc[:, "TransactionID"] = submission.loc[:, "TransactionID"].astype(int)
submission.to_csv(f"{config.DATA_DIR}/rf_submission.csv", index=False)
复制代码
注:提交给 Kaggle 的过程并不在本文讨论的范畴,因此我将直接在排行榜上列出模型的得分以及它是如何做到的。
考虑到这个分数可以转换成 Kaggle 的私人排行榜(因为它是公共排行榜上的分数),我们在 Kaggle 的私人排行榜上排名为 3875/6351(前 61%)。虽然从 Kaggle 的角度来看,这个得分看起来并不咋样,但在现实世界的场景中,我们可能会根据任务的情况来解决这个分数。
但是,这个项目的目标并非提出最好的模型,而是创建我们自己的 API,我们将在后面的文章中讨论这个问题。
为了构建快速迭代的快速管道,我们拥有的代码是可以的,但是如果我们想部署这个模型的话,就必须做大量的清理工作,这样我们才能遵循软件工程最佳实践。
总结
在现实世界中,欺诈检测是一个非常普遍且具有挑战性的问题,提高正确率对于防止在顾客在商店进行真正的交易时信用卡被拒的尴尬非常重要。我们已经构建了一种非常简单的方法,使用分类变量的标签编码,用 0 填充所有缺失值,并使用随机森林,没有任何调整或方法来处理数据的不平衡性,但我们的模型仍然得到了很高的分数。为了改进模型,我们可能要先从随机森林模型中寻找重要的特征,放弃不那么重要的特征,或者我们可以使用其他更为强大的模型,比如 Light Gradient Boosting Machine 和神经网络。
注:在编写这个脚本时,模块并不是最好的,但它的格式允许我进行快速迭代。在以后的工作中,我计划将这个模型作为 API 部署到云服务器上。
作者介绍:
Kurtis Pykes,痴迷于数据科学、人工智能和商业技术应用。
原文链接:
https://towardsdatascience.com/using-machine-learning-to-detect-fraud-f204910389cf
评论