實戰貼:如何使用機器學習檢測欺詐?

本文最初發表於 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 競賽數據部分複製而來。

數據分成兩個文件identitytransaction,這兩個文件由TransactionID連接。但並非所有交易都有相應的身份信息。

類別特徵——交易(Transaction)

  • ProductCD
  • card1-card6
  • addr1addr2
  • 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章