代码示例 / 时间序列 / 支付卡欺诈检测的事件分类

支付卡欺诈检测的事件分类

作者: achoum
创建日期: 2024/02/01
最后修改: 2024/02/01
描述: 使用Temporian和前馈神经网络检测欺诈性支付卡交易。

在Colab中查看 GitHub源代码

此笔记本依赖于Keras 3、Temporian和其他一些库。您可以按照以下方式安装它们:

pip install temporian keras pandas tf-nightly scikit-learn -U
import keras  # 用于训练机器学习模型
import temporian as tp  # 将交易转换为表格数据

import numpy as np
import os
import pandas as pd
import datetime
import math
import tensorflow as tf
from sklearn.metrics import RocCurveDisplay

引言

支付欺诈检测对银行、企业和消费者至关重要。在欧洲,欺诈交易的估计金额为 2019年的18.9亿欧元。 全球约有 3.6% 的商业收入因欺诈而损失。在本笔记本中,我们训练和评估一个模型,以检测欺诈交易,使用附在 《可重复的信用卡欺诈检测机器学习》 中的合成数据集,由Le Borgne等人提供。

欺诈交易通常无法通过孤立地查看交易来检测。相反,欺诈交易是通过查看来自同一用户、同一商户或具有其他类型关系的多个交易之间的模式来检测的。为了以机器学习模型可以理解的方式表达这些关系,并通过特征工程增强特征,我们使用了 Temporian 预处理库。

我们将交易数据集预处理为表格数据集,并使用前馈神经网络来学习欺诈模式并做出预测。


加载数据集

数据集包含在2018年4月1日至2018年9月30日期间采样的支付交易。每一天都有一个CSV文件存储这些交易。

注意:下载数据集大约需要1分钟。

start_date = datetime.date(2018, 4, 1)
end_date = datetime.date(2018, 9, 30)

# 将数据集加载为Pandas数据框。
cache_path = "fraud_detection_cache.csv"
if not os.path.exists(cache_path):
    print("下载数据集")
    dataframes = []
    num_files = (end_date - start_date).days
    counter = 0
    while start_date <= end_date:
        if counter % (num_files // 10) == 0:
            print(f"[{100 * (counter+1) // num_files}%]", end="", flush=True)
        print(".", end="", flush=True)
        url = f"https://github.com/Fraud-Detection-Handbook/simulated-data-raw/raw/6e67dbd0a3bfe0d7ec33abc4bce5f37cd4ff0d6a/data/{start_date}.pkl"
        dataframes.append(pd.read_pickle(url))
        start_date += datetime.timedelta(days=1)
        counter += 1
    print("完成", flush=True)
    transactions_dataframe = pd.concat(dataframes)
    transactions_dataframe.to_csv(cache_path, index=False)
else:
    print("从缓存加载数据集")
    transactions_dataframe = pd.read_csv(
        cache_path, dtype={"CUSTOMER_ID": bytes, "TERMINAL_ID": bytes}
    )

print(f"找到 {len(transactions_dataframe)} 笔交易")
下载数据集
[0%]..................[10%]..................[20%]..................[30%]..................[40%]..................[50%]..................[59%]..................[69%]..................[79%]..................[89%]..................[99%]...完成
找到 1754155 笔交易

每笔交易由一行表示,具有以下感兴趣的列:

  • TX_DATETIME: 交易的日期和时间。
  • CUSTOMER_ID: 客户的唯一标识符。
  • TERMINAL_ID: 进行交易的终端的标识符。
  • TX_AMOUNT: 交易的金额。
  • TX_FRAUD: 交易是否欺诈(1)或不是(0)。
transactions_dataframe = transactions_dataframe[
    ["TX_DATETIME", "CUSTOMER_ID", "TERMINAL_ID", "TX_AMOUNT", "TX_FRAUD"]
]

transactions_dataframe.head(4)
.dataframe tbody tr th:only-of-type {
    vertical-align: middle;
}
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
TX_DATETIME CUSTOMER_ID TERMINAL_ID TX_AMOUNT TX_FRAUD
0 2018-04-01 00:00:31 596 3156 57.16 0
1 2018-04-01 00:02:10 4961 3412 81.51 0
2 2018-04-01 00:07:56 2 1365 146.00 0
3 2018-04-01 00:09:29 4128 8737 64.49 0

数据集高度不平衡,大多数交易都是合法的。

fraudulent_rate = transactions_dataframe["TX_FRAUD"].mean()
print("Rate of fraudulent transactions:", fraudulent_rate)
Rate of fraudulent transactions: 0.008369271814634397

pandas dataframe 被转换成了 Temporian EventSet,这更适合后续的数据探索和特征预处理。

transactions_evset = tp.from_pandas(transactions_dataframe, timestamps="TX_DATETIME")

transactions_evset
WARNING:root:特征 "CUSTOMER_ID" 是一个 numpy.object_ 数组,将被转换为 numpy.string_(注意:numpy.string_ 等价于 numpy.bytes_)。
WARNING:root:特征 "TERMINAL_ID" 是一个 numpy.object_ 数组,将被转换为 numpy.string_(注意:numpy.string_ 等价于 numpy.bytes_)。
features [4]: CUSTOMER_ID (str_) , TERMINAL_ID (str_) , TX_AMOUNT (float64) , TX_FRAUD (int64)
indexes [0]: none
events: 1754155
index values: 1
memory usage: 28.1 MB
index ( ) 共 1754155 个事件
timestamp CUSTOMER_ID TERMINAL_ID TX_AMOUNT TX_FRAUD
2018-04-01 00:00:31+00:00 596 3156 57.16 0
2018-04-01 00:02:10+00:00 4961 3412 81.51 0
2018-04-01 00:07:56+00:00 2 1365 146 0
2018-04-01 00:09:29+00:00 4128 8737 64.49 0
2018-04-01 00:10:34+00:00 927 9906 50.99 0

可以绘制整个数据集,但结果图将难以阅读。相反,我们可以按客户对交易进行分组。

# 添加索引“CUSTOMER_ID”并绘制
transactions_evset.add_index("CUSTOMER_ID").plot(indexes="3774")

png

请注意此客户的几个欺诈交易。


准备训练数据

单独的欺诈交易是无法检测的。相反,我们需要连接相关交易。对于每笔交易,我们计算在过去 n 天内同一终端的交易总和和数量。因为我们不知道 n 的正确值,所以我们使用多个 n 的值,并为每个值计算一组特征。

# 按终端分组交易
transactions_per_terminal = transactions_evset.add_index("TERMINAL_ID")

# 按终端移动统计数据
tmp_features = []
for n in [7, 14, 28]:
    tmp_features.append(
        transactions_per_terminal["TX_AMOUNT"]
        .moving_sum(tp.duration.days(n))
        .rename(f"sum_transactions_{n}_days")
    )

    tmp_features.append(
        transactions_per_terminal.moving_count(tp.duration.days(n)).rename(
            f"count_transactions_{n}_days"
        )
    )

feature_set_1 = tp.glue(*tmp_features)

feature_set_1
features [6]: sum_transactions_7_days (float64) , count_transactions_7_days (int32) , sum_transactions_14_days (float64) , count_transactions_14_days (int32) , sum_transactions_28_days (float64) , count_transactions_28_days (int32)
indexes [1]: TERMINAL_ID (str_)
events: 1754155
index values: 10000
memory usage: 85.8 MB
index ( TERMINAL_ID: 0 ) with 178 events
timestamp sum_transactions_7_days count_transactions_7_days sum_transactions_14_days count_transactions_14_days sum_transactions_28_days count_transactions_28_days
2018-04-02 01:00:01+00:00 16.07 1 16.07 1 16.07 1
2018-04-02 09:49:55+00:00 83.9 2 83.9 2 83.9 2
2018-04-03 12:14:41+00:00 110.7 3 110.7 3 110.7 3
2018-04-05 16:47:41+00:00 151.2 4 151.2 4 151.2 4
2018-04-07 06:05:21+00:00 199.6 5 199.6 5 199.6 5
index ( TERMINAL_ID: 1 ) with 139 events
timestamp sum_transactions_7_days count_transactions_7_days sum_transactions_14_days count_transactions_14_days sum_transactions_28_days count_transactions_28_days
2018-04-01 16:24:39+00:00 70.36 1 70.36 1 70.36 1
2018-04-02 11:25:03+00:00 87.79 2 87.79 2 87.79 2
2018-04-04 08:31:48+00:00 211.6 3 211.6 3 211.6 3
2018-04-04 14:15:28+00:00 315 4 315 4 315 4
2018-04-04 20:54:17+00:00 446.5 5 446.5 5 446.5 5
index ( TERMINAL_ID: 10 ) with 151 events
timestamp sum_transactions_7_days count_transactions_7_days sum_transactions_14_days count_transactions_14_days sum_transactions_28_days count_transactions_28_days
2018-04-01 14:11:55+00:00 2.9 1 2.9 1 2.9 1
2018-04-02 11:01:07+00:00 17.04 2 17.04 2 17.04 2
2018-04-03 13:46:58+00:00 118.2 3 118.2 3 118.2 3
2018-04-04 03:27:11+00:00 161.7 4 161.7 4 161.7 4
2018-04-05 17:58:10+00:00 171.3 5 171.3 5 171.3 5
index ( TERMINAL_ID: 100 ) with 188 events
timestamp sum_transactions_7_days count_transactions_7_days sum_transactions_14_days count_transactions_14_days sum_transactions_28_days count_transactions_28_days
2018-04-02 10:37:42+00:00 6.31 1 6.31 1 6.31 1
2018-04-04 19:14:23+00:00 12.26 2 12.26 2 12.26 2
2018-04-07 04:01:22+00:00 65.12 3 65.12 3 65.12 3
2018-04-07 12:18:27+00:00 112.4 4 112.4 4 112.4 4
2018-04-07 21:11:03+00:00 170.4 5 170.4 5 170.4 5
… (9996 more indexes not shown)

让我们来看看终端“3774”的特征。

feature_set_1.plot(indexes="3774")

png

交易的欺诈状态在交易时不知道 (否则就没有问题了)。但是,银行知道交易在进行后一周是否是欺诈交易。我们创建了一组 特征,指示最近N天内欺诈交易的数量和比例。

# 将交易滞后一个星期。
lagged_transactions = transactions_per_terminal.lag(tp.duration.weeks(1))

# 每个客户的移动统计
tmp_features = []
for n in [7, 14, 28]:
    tmp_features.append(
        lagged_transactions["TX_FRAUD"]
        .moving_sum(tp.duration.days(n), sampling=transactions_per_terminal)
        .rename(f"count_fraud_transactions_{n}_days")
    )

    tmp_features.append(
        lagged_transactions["TX_FRAUD"]
        .cast(tp.float32)
        .simple_moving_average(tp.duration.days(n), sampling=transactions_per_terminal)
        .rename(f"rate_fraud_transactions_{n}_days")
    )

feature_set_2 = tp.glue(*tmp_features)

交易日期和时间可以与欺诈相关联。虽然每笔交易都有时间戳,但机器学习模型可能会很难直接处理它们。相反,我们从 时间戳中提取各种信息丰富的日历特征,例如小时、星期几(例如,星期一、星期二)和 月份中的天数(1-31)。

feature_set_3 = tp.glue(
    transactions_per_terminal.calendar_hour(),
    transactions_per_terminal.calendar_day_of_week(),
)

最后,我们将所有特征和标签组合在一起。

all_data = tp.glue(
    transactions_per_terminal, feature_set_1, feature_set_2, feature_set_3
).drop_index()

print("所有可用特征:")
all_data.schema.feature_names()
所有可用特征:

['CUSTOMER_ID',
 'TX_AMOUNT',
 'TX_FRAUD',
 'sum_transactions_7_days',
 'count_transactions_7_days',
 'sum_transactions_14_days',
 'count_transactions_14_days',
 'sum_transactions_28_days',
 'count_transactions_28_days',
 'count_fraud_transactions_7_days',
 'rate_fraud_transactions_7_days',
 'count_fraud_transactions_14_days',
 'rate_fraud_transactions_14_days',
 'count_fraud_transactions_28_days',
 'rate_fraud_transactions_28_days',
 'calendar_hour',
 'calendar_day_of_week',
 'TERMINAL_ID']

我们提取输入特征的名称。

input_feature_names = [k for k in all_data.schema.feature_names() if k.islower()]

print("模型的输入特征:")
input_feature_names
模型的输入特征:

['sum_transactions_7_days',
 'count_transactions_7_days',
 'sum_transactions_14_days',
 'count_transactions_14_days',
 'sum_transactions_28_days',
 'count_transactions_28_days',
 'count_fraud_transactions_7_days',
 'rate_fraud_transactions_7_days',
 'count_fraud_transactions_14_days',
 'rate_fraud_transactions_14_days',
 'count_fraud_transactions_28_days',
 'rate_fraud_transactions_28_days',
 'calendar_hour',
 'calendar_day_of_week']

为了使神经网络正常工作,数值输入必须进行归一化。一种常见的方法是应用z标准化,这涉及从训练数据中估计的均值中减去并 divid 各值的标准差。在预测中,不建议进行这样的z标准化,因为这会导致未来信息泄露。具体而言,为了在时间t对交易进行分类,我们不能依赖于时间t之后的数据,因为在预测时,尚无后续数据可用。简而言之,在时间t,我们只能使用时间t之前或同时的数据。

因此,解决方案是在时间上应用z标准化,这意味着我们使用从该交易过去数据计算的均值和标准差来对每笔交易进行归一化。

未来信息泄漏是极其有害的。幸运的是,Temporian 可以提供帮助:唯一可以导致未来泄漏的操作是 EventSet.leak()。如果您不 使用 EventSet.leak(),你的预处理保证不会产生未来泄漏。

注意:对于高级管道,你还可以通过编程检查特征是否不依赖于 EventSet.leak() 操作。

# 将所有值(例如整数)转换为浮点数。
values = all_data[input_feature_names].cast(tp.float32)

# 应用时间上的 z 规范化。
normalized_features = (
    values - values.simple_moving_average(math.inf)
) / values.moving_standard_deviation(math.inf)

# 恢复特征的原始名称。
normalized_features = normalized_features.rename(values.schema.feature_names())

print(normalized_features)
indexes: []
features: [('sum_transactions_7_days', float32), ('count_transactions_7_days', float32), ('sum_transactions_14_days', float32), ('count_transactions_14_days', float32), ('sum_transactions_28_days', float32), ('count_transactions_28_days', float32), ('count_fraud_transactions_7_days', float32), ('rate_fraud_transactions_7_days', float32), ('count_fraud_transactions_14_days', float32), ('rate_fraud_transactions_14_days', float32), ('count_fraud_transactions_28_days', float32), ('rate_fraud_transactions_28_days', float32), ('calendar_hour', float32), ('calendar_day_of_week', float32)]
events:
     (1754155 events):
        timestamps: ['2018-04-01T00:00:31' '2018-04-01T00:02:10' '2018-04-01T00:07:56' ...
     '2018-09-30T23:58:21' '2018-09-30T23:59:52' '2018-09-30T23:59:57']
        'sum_transactions_7_days': [ 0.      1.      1.3636 ... -0.064  -0.2059  0.8428]
        'count_transactions_7_days': [   nan    nan    nan ... 1.0128 0.6892 1.66  ]
        'sum_transactions_14_days': [ 0.      1.      1.3636 ... -0.7811  0.156   1.379 ]
        'count_transactions_14_days': [   nan    nan    nan ... 0.2969 0.2969 2.0532]
        'sum_transactions_28_days': [ 0.      1.      1.3636 ... -0.7154 -0.2989  1.9396]
        'count_transactions_28_days': [    nan     nan     nan ...  0.1172 -0.1958  1.8908]
        'count_fraud_transactions_7_days': [    nan     nan     nan ... -0.1043 -0.1043 -0.1043]
        'rate_fraud_transactions_7_days': [    nan     nan     nan ... -0.1137 -0.1137 -0.1137]
        'count_fraud_transactions_14_days': [    nan     nan     nan ... -0.1133 -0.1133  0.9303]
        'rate_fraud_transactions_14_days': [    nan     nan     nan ... -0.1216 -0.1216  0.5275]
        ...
memory usage: 112.3 MB
/home/gbm/my_venv/lib/python3.11/site-packages/temporian/implementation/numpy/operators/binary/arithmetic.py:100: RuntimeWarning: 在除法中遇到无效值
  return evset_1_feature / evset_2_feature

前几笔交易将使用均值和标准差的较差估计进行规范化,因为在它们之前只有几笔交易。为了解决这个问题,我们从训练数据集中移除第一周的数据。

请注意,前面的值包含 NaN。在 Temporian 中,NaN 表示缺失值,所有操作符都会相应处理。例如,在计算移动平均值时,NaN 值不会被包含在计算中,也不会生成 NaN 结果。

然而,神经网络无法原生处理 NaN 值。因此,我们用零替换它们。

normalized_features = normalized_features.fillna(0.0)

最后,我们将特征和标签组合在一起。

normalized_all_data = tp.glue(normalized_features, all_data["TX_FRAUD"])

将数据集拆分为训练集、验证集和测试集

为了评估我们的机器学习模型的质量,我们需要训练集、验证集和测试集。由于系统是动态的(新的欺诈模式不断产生),因此训练集必须在验证集之前,验证集必须在测试集之前:

  • 训练集: 2018年4月8日至2018年7月31日
  • 验证集: 2018年8月1日至2018年8月31日
  • 测试集: 2018年9月1日至2018年9月30日

为了使示例运行得更快,我们将有效地将训练集的大小减少为: - 训练集: 2018年7月1日至2018年7月31日

# begin_train = datetime.datetime(2018, 4, 8).timestamp() # 完整的训练数据集
begin_train = datetime.datetime(2018, 7, 1).timestamp()  # 减少的训练数据集
begin_valid = datetime.datetime(2018, 8, 1).timestamp()
begin_test = datetime.datetime(2018, 9, 1).timestamp()

is_train = (normalized_all_data.timestamps() >= begin_train) & (
    normalized_all_data.timestamps() < begin_valid
)
is_valid = (normalized_all_data.timestamps() >= begin_valid) & (
    normalized_all_data.timestamps() < begin_test
)
is_test = normalized_all_data.timestamps() >= begin_test

is_trainis_validis_test 是随着时间推移的布尔特征,指示树的三重划分界限。让我们绘制它们。

tp.plot(
    [
        is_train.rename("训练集"),
        is_valid.rename("验证集"),
        is_test.rename("测试集"),
    ]
)

png

我们在每个折叠中过滤输入特征和标签。

train_ds_evset = normalized_all_data.filter(is_train)
valid_ds_evset = normalized_all_data.filter(is_valid)
test_ds_evset = normalized_all_data.filter(is_test)

print(f"训练示例: {train_ds_evset.num_events()}")
print(f"验证示例: {valid_ds_evset.num_events()}")
print(f"测试示例: {test_ds_evset.num_events()}")
训练示例: 296924
验证示例: 296579
测试示例: 288064

在计算特征之后拆分数据集是很重要的,因为训练数据集的一些特征是根据训练窗口内的交易计算得出的。


创建 TensorFlow 数据集

我们将数据集从 EventSets 转换为 TensorFlow 数据集,因为 Keras 原生支持它们。

non_batched_train_ds = tp.to_tensorflow_dataset(train_ds_evset)
non_batched_valid_ds = tp.to_tensorflow_dataset(valid_ds_evset)
non_batched_test_ds = tp.to_tensorflow_dataset(test_ds_evset)

使用 TensorFlow 数据集应用以下处理步骤:

  1. 使用 extract_features_and_label 分离特征和标签,以符合 Keras 的预期格式。
  2. 对数据集进行批处理,这意味着示例被分组到小批量中。
  3. 对训练示例进行洗牌,以提高小批量训练的质量。

正如我们之前所提到的,数据集在合法交易方面是不平衡的。虽然我们希望在这种原始分布上评估我们的模型,但神经网络通常在强烈不平衡的数据集上训练表现不佳。因此,我们使用 rejection_resample 将训练数据集重新采样为 80% 合法 / 20% 欺诈的比例。

def extract_features_and_label(example):
    features = {k: example[k] for k in input_feature_names}
    labels = tf.cast(example["TX_FRAUD"], tf.int32)
    return features, labels


# 训练数据集中欺诈交易的目标比例。
target_rate = 0.2

# 每个小批量中的示例数量。
batch_size = 32

train_ds = (
    non_batched_train_ds.shuffle(10000)
    .rejection_resample(
        class_func=lambda x: tf.cast(x["TX_FRAUD"], tf.int32),
        target_dist=[1 - target_rate, target_rate],
        initial_dist=[1 - fraudulent_rate, fraudulent_rate],
    )
    .map(lambda _, x: x)  # 移除 "rejection_resample" 添加的标签副本。
    .batch(batch_size)
    .map(extract_features_and_label)
    .prefetch(tf.data.AUTOTUNE)
)

# 测试和验证数据集不需要重新采样或洗牌。
valid_ds = (
    non_batched_valid_ds.batch(batch_size)
    .map(extract_features_and_label)
    .prefetch(tf.data.AUTOTUNE)
)
test_ds = (
    non_batched_test_ds.batch(batch_size)
    .map(extract_features_and_label)
    .prefetch(tf.data.AUTOTUNE)
)
WARNING:tensorflow:From /home/gbm/my_venv/lib/python3.11/site-packages/tensorflow/python/data/ops/dataset_ops.py:4956: Print (from tensorflow.python.ops.logging_ops) is deprecated and will be removed after 2018-08-20.
更新指令:
使用 tf.print 代替 tf.Print。注意 tf.print 返回一个没有输出的操作,直接打印输出。在非 defuns 或急切模式下,除非在 session.run 中直接指定该操作或用作其他操作的控制依赖项,否则该操作不会被执行。这在图模式中才是一个问题。下面是确保 tf.print 在图模式中执行的示例:
WARNING:tensorflow:From /home/gbm/my_venv/lib/python3.11/site-packages/tensorflow/python/data/ops/dataset_ops.py:4956: Print (from tensorflow.python.ops.logging_ops) is deprecated and will be removed after 2018-08-20.
更新指令:
使用 tf.print 代替 tf.Print。注意 tf.print 返回一个没有输出的操作,直接打印输出。在非 defuns 或急切模式下,除非在 session.run 中直接指定该操作或用作其他操作的控制依赖项,否则该操作不会被执行。这在图模式中才是一个问题。下面是确保 tf.print 在图模式中执行的示例:

我们打印训练数据集的前四个示例。这是识别上面可能出现的一些错误的简单方法。

for features, labels in train_ds.take(1):
    print("特征")
    for feature_name, feature_value in features.items():
        print(f"\t{feature_name}: {feature_value[:4]}")
    print(f"标签: {labels[:4]}")
features sum_transactions_7_days: [-0.9417254 -1.1157728 -0.5594417 0.7264878] count_transactions_7_days: [-0.23363686 -0.8702531 -0.23328805 0.7198456 ] sum_transactions_14_days: [-0.9084115 2.8127224 0.7297886 0.0666021] count_transactions_14_days: [-0.54289246 2.4122045 0.1963075 0.3798441 ] sum_transactions_28_days: [-0.44202712 2.3494742 0.20992276 0.97425723] count_transactions_28_days: [0.02585898 1.8197156 0.12127225 0.9692807 ] count_fraud_transactions_7_days: [ 8.007475 -0.09783722 1.9282814 -0.09780706] rate_fraud_transactions_7_days: [14.308702 -0.10952345 1.6929103 -0.10949575] count_fraud_transactions_14_days: [12.411182 -0.1045466 1.0330476 -0.1045142] rate_fraud_transactions_14_days: [15.742149 -0.11567765 1.0170861 -0.11565071] count_fraud_transactions_28_days: [ 7.420907 -0.11298086 0.572011 -0.11293571] rate_fraud_transactions_28_days: [10.065552 -0.12640427 0.5862939 -0.12637936] calendar_hour: [-0.68766755 0.6972711 -1.6792761 0.49967623] calendar_day_of_week: [1.492013 1.4789637 1.4978485 1.4818214] labels: [1 0 0 0] 抽样器拒绝的样本比例很高: [0.991630733][0.991630733 0.00836927164][0 1] 抽样器拒绝的样本比例很高: [0.991630733][0.991630733 0.00836927164][0 1] 抽样器拒绝的样本比例很高: [0.991630733][0.991630733 0.00836927164][0 1] 抽样器拒绝的样本比例很高: [0.991630733][0.991630733 0.00836927164][0 1] 抽样器拒绝的样本比例很高: [0.991630733][0.991630733 0.00836927164][0 1] 抽样器拒绝的样本比例很高: [0.991630733][0.991630733 0.00836927164][0 1] 抽样器拒绝的样本比例很高: [0.991630733][0.991630733 0.00836927164][0 1] 抽样器拒绝的样本比例很高: [0.991630733][0.991630733 0.00836927164][0 1] 抽样器拒绝的样本比例很高: [0.991630733][0.991630733 0.00836927164][0 1] 抽样器拒绝的样本比例很高: [0.991630733][0.991630733 0.00836927164][0 1]

训练模型

原始数据集是事务性的,但处理后的数据是表格形式的,只包含规范化的数值。因此,我们训练一个前馈神经网络。

inputs = [keras.Input(shape=(1,), name=name) for name in input_feature_names]
x = keras.layers.concatenate(inputs)
x = keras.layers.Dense(32, activation="sigmoid")(x)
x = keras.layers.Dense(16, activation="sigmoid")(x)
x = keras.layers.Dense(1, activation="sigmoid")(x)
model = keras.Model(inputs=inputs, outputs=x)

我们的目标是区分欺诈交易和合法交易,因此我们使用二元分类目标。由于数据集不平衡,准确率不是一个有效的指标。相反,我们使用曲线下面积(AUC)来评估模型。

model.compile(
    optimizer=keras.optimizers.Adam(0.01),
    loss=keras.losses.BinaryCrossentropy(),
    metrics=[keras.metrics.Accuracy(), keras.metrics.AUC()],
)
model.fit(train_ds, validation_data=valid_ds)
      5/Unknown  1s 15ms/step - accuracy: 0.0000e+00 - auc: 0.4480 - loss: 0.7678

被采样器拒绝的示例比例很高: [0.991630733][0.991630733 0.00836927164][0 1]
被采样器拒绝的示例比例很高: [0.991630733][0.991630733 0.00836927164][0 1]
被采样器拒绝的示例比例很高: [0.991630733][0.991630733 0.00836927164][0 1]
被采样器拒绝的示例比例很高: [0.991630733][0.991630733 0.00836927164][0 1]
被采样器拒绝的示例比例很高: [0.991630733][0.991630733 0.00836927164][0 1]
被采样器拒绝的示例比例很高: [0.991630733][0.991630733 0.00836927164][0 1]
被采样器拒绝的示例比例很高: [0.991630733][0.991630733 0.00836927164][0 1]
被采样器拒绝的示例比例很高: [0.991630733][0.991630733 0.00836927164][0 1]
被采样器拒绝的示例比例很高: [0.991630733][0.991630733 0.00836927164][0 1]
被采样器拒绝的示例比例很高: [0.991630733][0.991630733 0.00836927164][0 1]

    433/Unknown  23s 51ms/step - accuracy: 0.0000e+00 - auc: 0.8060 - loss: 0.3632

/usr/lib/python3.11/contextlib.py:155: UserWarning: Your input ran out of data; interrupting training. Make sure that your dataset or generator can generate at least `steps_per_epoch * epochs` batches. You may need to use the `.repeat()` function when building your dataset.
  self.gen.throw(typ, value, traceback)

 433/433 ━━━━━━━━━━━━━━━━━━━━ 30s 67ms/step - accuracy: 0.0000e+00 - auc: 0.8060 - loss: 0.3631 - val_accuracy: 0.0000e+00 - val_auc: 0.8252 - val_loss: 0.2133

<keras.src.callbacks.history.History at 0x7f8f74f0d750>

我们在测试数据集上评估模型。

model.evaluate(test_ds)
 9002/9002 ━━━━━━━━━━━━━━━━━━━━ 7s 811us/step - accuracy: 0.0000e+00 - auc: 0.8357 - loss: 0.2161

[0.2171599417924881, 0.0, 0.8266682028770447]

AUC约为83%,我们简单的欺诈检测器显示出良好的结果。

绘制ROC曲线是理解和选择模型操作点的一个好方法,即对模型输出施加的阈值,以区分欺诈交易和合法交易。

计算测试预测:

predictions = model.predict(test_ds)
predictions = np.nan_to_num(predictions, nan=0)
 9002/9002 ━━━━━━━━━━━━━━━━━━━━ 10s 1ms/step

从测试集中提取标签:

labels = np.concatenate([label for _, label in test_ds])

最后,我们绘制ROC曲线。

_ = RocCurveDisplay.from_predictions(labels, predictions)

png

Keras模型准备好用于未知欺诈状态的交易,即服务。我们将模型保存在磁盘上以供将来使用。

注意: 模型不包括在Pandas和Temporian中进行的数据准备和预处理步骤。这些必须手动应用于输入模型的数据。虽然此处未演示,但Temporian的预处理也可以使用tp.save保存到磁盘。

model.save("fraud_detection_model.keras")

可以稍后使用以下代码重新加载模型:

loaded_model = keras.saving.load_model("fraud_detection_model.keras")

# 使用加载的模型生成5个测试示例的预测。
loaded_model.predict(test_ds.rebatch(5).take(1))
 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 71ms/step

/usr/lib/python3.11/contextlib.py:155: 用户警告: 你的输入数据耗尽;中断训练。确保你的数据集或生成器可以生成至少 `steps_per_epoch * epochs` 批次。构建数据集时你可能需要使用 `.repeat()` 函数。
  self.gen.throw(typ, value, traceback)

array([[0.08197185],
       [0.16517264],
       [0.13180313],
       [0.10209075],
       [0.14283912]], dtype=float32)

结论

我们训练了一个前馈神经网络来识别欺诈交易。为了将它们输入到模型中,交易经过预处理并使用 Temporian 转换为表格数据集。现在,给读者提出一个问题:有什么方法可以进一步提高模型的性能?

以下是一些想法:

  • 在整个数据集上训练模型,而不是单个月的数据。
  • 训练模型更多的时期,并使用提前停止来确保模型在不发生过拟合的情况下完全训练。
  • 通过增加层数来增强前馈网络的能力,同时确保模型得到了正则化。
  • 计算额外的预处理特征。例如,除了按终端聚合交易外,还可以按客户聚合交易。
  • 使用 Keras Tuner 对模型进行超参数调整。注意,预处理的参数(例如,聚合的天数)也是可以调优的超参数。