Fraud Detection with Entity Resolution and Graph Neural Networks | by Stefan Berkner | Aug, 2023


A practical guide to how entity resolution improves machine learning to detect fraud

Representation of a Graph Neural Network (Image generated by the Author using Bing Image Creator)

Online fraud is an ever-growing issue for finance, e-commerce and other related industries. In response to this threat, organizations use fraud detection mechanisms based on machine learning and behavioral analytics. These technologies enable the detection of unusual patterns, abnormal behaviors, and fraudulent activities in real time.

Unfortunately, often only the current transaction, e.g. an order, is taken into consideration, or the process is based solely on historic data from the customer’s profile, which is identified by a customer id. However, professional fraudsters may create customer profiles using low value transactions to build up a positive image of their profile. Additionally, they might create multiple similar profiles at the same time. It is only after the fraud took place that the attacked company realizes that these customer profiles were related to each other.

Using entity resolution it is possible to easily combine different customer profiles into a single 360° customer view, allowing one to see the full picture of all historic transactions. While using this data in machine learning, e.g. using a neural network or even a simple linear regression, would already provide additional value for the resulting model, the real value arises from also looking at how the individual transactions are connected to each other. This is where graph neural networks (GNN) come into play. Beside looking at features extracted from the transactional records, they offer also the possibility to look at features generated from the graph edges (how transactions are linked with each other) or even just the general layout of the entity graph.

Before we dive deeper into the details, I have one disclaimer to put here: I am a developer and entity resolution expert and not a data scientist or ML expert. While I think the general approach is correct, I might not be following best practices, nor can I explain certain aspects such as the number of hidden nodes. Use this article as an inspiration and draw upon your own experience when it comes to the GNN layout or configuration.

For the purposes of this article I want to focus on the insights gained from the entity graph’s layout. For this purpose I created a small Golang script that generates entities. Each entity is labeled as either fraudulent or non-fraudulent and consists of records (orders) and edges (how those orders are linked). See the following example of a single entity:

{
"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
}
]
}

Each record has two (potential) features, the total value and the number of items purchased. However, the generation script completely randomized these values, hence they should not provide value when it comes to guessing the fraud label. Each edge also comes with two features R1 and R2. These could e.g. represent whether the two records A and B are linked via a similar name and address (R1) or the via a similar email address (R2). Furthermore I intentionally left out all the attributes that are not relevant for this example (name, address, email, phone number, etc.), but are usually relevant for the entity resolution process beforehand. As R1 and R2 are also randomized, they also don’t provide value for the GNN. However, based on the fraud label, the edges are laid out in two possible ways: a star-like layout (fraud=0) or a random layout (fraud=1).

The idea is that a non-fraudulent customer is more likely to provide accurate matching relevant data, usually the same address and same name, with only a few spelling errors here and there. Hence new transactions may get recognized as a duplicate.

Deduplicated Entity (Image by the Author)

A fraudulent customer might want to hide the fact that they are still the same person behind the computer, using various names and addresses. However, entity resolution tools may still recognize the similarity (e.g. geographical and temporal similarity, recurring patterns in the email address, device IDs etc.), but the entity graph may look more complex.

Complex, Possibly Fraudulent Entity (Image by the Author)

To make it a little less trivial, the generation script also has a 5% error rate, meaning that entities are labeled as fraudulent when they have a star-like layout and labeled as non-fraudulent for the random layout. Also there are some cases where the data is insufficient to determine the actual layout (e.g. only one or two records).

{
"fraud":1,
"records":[
{
"id":0,
"totalValue":85,
"items":5
}
],
"edges":[

]
}

In reality you most likely would gain valuable insights from all three kinds of features (record attributes, edge attributes and edge layout). The following code examples will consider this, but the generated data does not.

The example uses python (except for the data generation) and DGL with a pytorch backend. You can find the full jupyter notebook, the data and the generation script on github.

Let’s start with importing the dataset:

import os

os.environ["DGLBACKEND"] = "pytorch"
import pandas as pd
import torch
import dgl
from dgl.data import DGLDataset

class 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])

This processes the entities file, which is a JSON-line file, where each row represents a single entity. While iterating over each entity, it generates the edge features (long tensor with shape [e, 2], e=number of edges) and the node features (long tensor with shape [n, 2], n=number of nodes). It then proceeds to build the graph based on a and b (long tensors each with shape [e, 1]) and assigns the edge and graph features to that graph. All resulting graphs are then added to the dataset.

Now that we have the data ready, we need to think about the architecture of our GNN. This is what I came up with, but probably can be adjusted much more to the actual needs:

import torch.nn as nn
import torch.nn.functional as F
from dgl.nn import NNConv, SAGEConv

class 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")

The constructor takes the number of node features, number of edge features, number of hidden nodes and the number of labels (classes). It then creates two layers: a NNConv layer which calculates the hidden nodes based on the edge and node features, and then a GraphSAGE layer that calculates the resulting label based on the hidden nodes.

Almost there. Next we prepare the data for training and testing.

from torch.utils.data.sampler import SubsetRandomSampler
from dgl.dataloading import GraphDataLoader

num_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
)

We split with a 80/20 ratio using random sampling and create a data loader for each of the samples.

The last step is to initialize the model with our data, run the training and afterwards test the result.

h_feats = 64
learn_iterations = 50
learn_rate = 0.01

model = 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 = 0
num_tests = 0
for 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_tests
print("Test accuracy:", acc)

We initialize the model by providing the feature sizes for nodes and edges (both 2 in our case), the hidden nodes (64) and the amount of labels (2 because it’s either fraud or not). The optimizer is then initialized with a learning rate of 0.01. Afterwards we run a total of 50 training iterations. Once the training is done, we test the results using the test data loader and print the resulting accuracy.

For various runs, I had a typical accuracy in the range of 70 to 85%. However, with a few exceptions going down to something like 55%.

Given that the only usable information from our example dataset is the explanation of how the nodes are connected, the initial results look very promising and suggest that higher accuracy rates would be possible with real-world data and more training.

Obviously when working with real data, the layout is not that consistent and does not provide an obvious correlation between the layout and fraudulent behavior. Hence, you should also take the edge and node features into consideration. The key takeaway from this article should be that entity resolution provides the ideal data for fraud detection using graph neural networks and should be considered part of a fraud detection engineer’s arsenal of tools.



Source link

Leave a Comment