如何通过实体解析提高机器学习检测欺诈的实用指南
在线欺诈对金融、电子商务和其他相关行业来说是一个不断增长的问题。为了应对这一威胁,组织机构使用基于机器学习和行为分析的欺诈检测机制。这些技术能够实时检测异常模式、异常行为和欺诈活动。
不幸的是,通常只考虑当前的交易,例如一个订单,或者过程仅基于客户个人资料的历史数据,该资料通过客户ID进行标识。然而,专业的欺诈者可能会使用低价交易创建客户个人资料,以建立他们个人资料的正面形象。此外,他们可能同时创建多个相似的个人资料。只有在发生欺诈行为之后,受攻击的公司才意识到这些客户个人资料彼此相关。
使用实体解析,可以将不同的客户个人资料轻松合并为单个360°客户视图,从而能够看到所有历史交易的全貌。在使用这些数据进行机器学习时,例如使用神经网络或甚至简单的线性回归,已经为生成的模型提供了额外的价值,真正的价值还在于查看各个交易是如何相互关联的。这就是图形神经网络(GNN)的作用。除了查看从交易记录中提取的特征外,它们还提供了查看从图形边缘(交易如何相互链接)生成的特征,甚至只是实体图的一般布局的可能性。
示例数据
在我们深入了解细节之前,我要声明一点:我是一名开发者和实体解析专家,而不是数据科学家或机器学习专家。虽然我认为一般的方法是正确的,但我可能没有遵循最佳实践,也无法解释一些方面,比如隐藏节点的数量。在涉及GNN布局或配置时,请根据自己的经验使用本文作为灵感。
为了本文的目的,我想重点关注从实体图的布局中获得的洞察。为此,我创建了一个小型的Golang脚本来生成实体。每个实体都被标记为欺诈或非欺诈,并包含记录(订单)和边缘(这些订单如何链接)。
以下是单个实体的示例:
{ "fraud":1, "records":[ { "id":0, "totalValue":85, "items":2 }, { "id":1, "totalValue":31, "items":4 }, { "id":2, "totalValue":20, "items":9 } ], "edges":[ { "a":1, "b":0, "R1":1, "R2":1 }, { "a":2, "b":1, "R1":0, "R2":1 } ]}
每个记录有两个(潜在的)特征,即总价值和购买的商品数量。然而,生成脚本完全随机化这些值,因此它们在猜测欺诈标签时不应提供价值。每个边缘还附带两个特征R1和R2。它们可以表示两个记录A和B是否通过相似的名称和地址(R1)或相似的电子邮件地址(R2)相互关联。此外,我有意省略了对于本示例不相关的所有属性(姓名、地址、电子邮件、电话号码等),但通常在实体解析过程中是相关的。由于R1和R2也是随机化的,它们对于GNN也没有提供价值。然而,基于欺诈标签,边缘的布局有两种可能的方式:星型布局(fraud=0)或随机布局(fraud=1)。
这个想法是非欺诈客户更有可能提供准确匹配的相关数据,通常是相同的地址和相同的姓名,只有一些拼写错误。因此,新的交易可能会被识别为重复。
一个欺诈客户可能会想要隐藏他们在电脑后面仍然是同一个人的事实,使用不同的姓名和地址。然而,实体解析工具可能仍然能够识别相似性(例如地理和时间上的相似性,电子邮件地址中的重复模式,设备ID等),但实体图可能看起来更加复杂。
为了使其稍微复杂一些,生成脚本还具有5%的错误率,这意味着当实体具有星形布局时,它们被标记为欺诈,并且在随机布局下被标记为非欺诈。此外,还有一些情况下数据不足以确定实际布局(例如只有一两条记录)。
{ "fraud":1, "records":[ { "id":0, "totalValue":85, "items":5 } ], "edges":[ ]}
实际上,您很可能会从三种特征(记录属性、边属性和边布局)中获得有价值的见解。下面的代码示例将考虑这一点,但生成的数据并没有。
创建数据集
示例使用python(除了数据生成)和DGL与pytorch后端。您可以在github上找到完整的jupyter笔记本、数据和生成脚本。
让我们从导入数据集开始:
import osos.environ["DGLBACKEND"] = "pytorch"import pandas as pdimport torchimport dglfrom dgl.data import DGLDatasetclass EntitiesDataset(DGLDataset): def __init__(self, entitiesFile): self.entitiesFile = entitiesFile super().__init__(name="entities") def process(self): entities = pd.read_json(self.entitiesFile, lines=1) self.graphs = [] self.labels = [] for _, entity in entities.iterrows(): a = [] b = [] r1_feat = [] r2_feat = [] for edge in entity["edges"]: a.append(edge["a"]) b.append(edge["b"]) r1_feat.append(edge["R1"]) r2_feat.append(edge["R2"]) a = torch.LongTensor(a) b = torch.LongTensor(b) edge_features = torch.LongTensor([r1_feat, r2_feat]).t() node_feat = [[node["totalValue"], node["items"]] for node in entity["records"]] node_features = torch.tensor(node_feat) g = dgl.graph((a, b), num_nodes=len(entity["records"])) g.edata["feat"] = edge_features g.ndata["feat"] = node_features g = dgl.add_self_loop(g) self.graphs.append(g) self.labels.append(entity["fraud"]) self.labels = torch.LongTensor(self.labels) def __getitem__(self, i): return self.graphs[i], self.labels[i] def __len__(self): return len(self.graphs)dataset = EntitiesDataset("./entities.jsonl")print(dataset)print(dataset[0])
这个过程处理实体文件,它是一个JSON-line文件,每一行代表一个单独的实体。在迭代每个实体时,它生成边特征(形状为[e, 2]的长张量,e=边的数量)和节点特征(形状为[n, 2]的长张量,n=节点的数量)。然后根据a和b(每个形状为[e, 1]的长张量)构建图,并将边和图特征分配给该图。然后将所有生成的图添加到数据集中。
模型架构
现在我们已经准备好了数据,我们需要考虑GNN的架构。这是我想到的,但实际上可能还需要根据实际需求进行调整:
import torch.nn as nnimport torch.nn.functional as Ffrom dgl.nn import NNConv, SAGEConvclass EntityGraphModule(nn.Module): def __init__(self, node_in_feats, edge_in_feats, h_feats, num_classes): super(EntityGraphModule, self).__init__() lin = nn.Linear(edge_in_feats, node_in_feats * h_feats) edge_func = lambda e_feat: lin(e_feat) self.conv1 = NNConv(node_in_feats, h_feats, edge_func) self.conv2 = SAGEConv(h_feats, num_classes, "pool") def forward(self, g, node_features, edge_features): h = self.conv1(g, node_features, edge_features) h = F.relu(h) h = self.conv2(g, h) g.ndata["h"] = h return dgl.mean_nodes(g, "h")
构造函数接受节点特征数量、边特征数量、隐藏节点数量和标签数量(类别)。然后创建两个层:一个NNConv层,根据边和节点特征计算隐藏节点,然后是一个GraphSAGE层,根据隐藏节点计算结果标签。
训练和测试
接近尾声。接下来我们为训练和测试准备数据。
from torch.utils.data.sampler import SubsetRandomSamplerfrom dgl.dataloading import GraphDataLoadernum_examples = len(dataset)num_train = int(num_examples * 0.8)train_sampler = SubsetRandomSampler(torch.arange(num_train))test_sampler = SubsetRandomSampler(torch.arange(num_train, num_examples))train_dataloader = GraphDataLoader( dataset, sampler=train_sampler, batch_size=5, drop_last=False)test_dataloader = GraphDataLoader( dataset, sampler=test_sampler, batch_size=5, drop_last=False)
我们使用随机抽样以80/20的比例划分数据,并为每个样本创建一个数据加载器。
最后一步是用我们的数据初始化模型,进行训练,然后测试结果。
h_feats = 64learn_iterations = 50learn_rate = 0.01model = EntityGraphModule( dataset.graphs[0].ndata["feat"].shape[1], dataset.graphs[0].edata["feat"].shape[1], h_feats, dataset.labels.max().item() + 1)optimizer = torch.optim.Adam(model.parameters(), lr=learn_rate)for _ in range(learn_iterations): for batched_graph, labels in train_dataloader: pred = model(batched_graph, batched_graph.ndata["feat"].float(), batched_graph.edata["feat"].float()) loss = F.cross_entropy(pred, labels) optimizer.zero_grad() loss.backward() optimizer.step()num_correct = 0num_tests = 0for batched_graph, labels in test_dataloader: pred = model(batched_graph, batched_graph.ndata["feat"].float(), batched_graph.edata["feat"].float()) num_correct += (pred.argmax(1) == labels).sum().item() num_tests += len(labels)acc = num_correct / num_testsprint("Test accuracy:", acc)
我们通过提供节点和边的特征大小(在我们的案例中都是2)、隐藏节点数量(64)以及标签数量(2,因为它要么是欺诈,要么不是)来初始化模型。然后使用学习率为0.01初始化优化器。之后运行总共50次的训练迭代。训练完成后,我们使用测试数据加载器测试结果,并打印得到的准确率。
在各种运行中,我得到的典型准确率范围是70%到85%。然而,个别情况下准确率下降到55%左右。
结论
考虑到我们的示例数据集中唯一可用的信息是节点连接的解释,初始结果看起来非常有希望,并且表明在使用真实数据和更多训练的情况下,更高的准确率是可能的。
显然,在处理真实数据时,布局不够一致,不能提供布局和欺诈行为之间的明显相关性。因此,您还应考虑边和节点特征。本文的核心要点是,使用图神经网络进行实体解析提供了进行欺诈检测的理想数据,应视为欺诈检测工程师工具库的一部分。
原文发表于https://tilores.io。