Graph Neural Networks for Fraud Detection: Architecture and Feature Design

Fraud Does Not Happen in Isolation

Traditional fraud detection treats each transaction independently. You extract features from the transaction, amount, time, merchant category, device fingerprint, feed them into a classifier, and get a fraud probability. This works for obvious fraud patterns, but it misses the organized stuff. Fraud rings, account takeover chains, synthetic identity networks, these are fundamentally relational problems, and you need relational models to catch them.

Graph neural networks bring the network structure into the model, and that structural information turns out to be one of the strongest signals for detecting organized fraud. I've been building GNN-based fraud detection systems for the past three years, and the architecture decisions you make early on determine whether the system actually works.

Graph Construction: The Critical First Step

Before you train any GNN, you need to construct the graph. This is where most of the modeling decisions happen, and they're irreversible. A poorly constructed graph can't be rescued by a better GNN architecture.

import torch
from torch_geometric.data import HeteroData

def build_fraud_graph(transactions_df, accounts_df, devices_df):
 data = HeteroData()
 data["account"].x = torch.tensor(
 accounts_df[["age_days", "num_transactions", "avg_amount",
 "num_devices", "num_merchants"]].values,
 dtype=torch.float
 )
 data["device"].x = torch.tensor(
 devices_df[["num_accounts", "os_encoded", "browser_encoded",
 "screen_hash", "timezone_offset"]].values,
 dtype=torch.float
 )
 data["account", "transacts_at", "merchant"].edge_index = \n torch.tensor(transaction_edges, dtype=torch.long)
 data["account", "transacts_at", "merchant"].edge_attr = \n torch.tensor(transaction_features, dtype=torch.float)
 data["account", "uses", "device"].edge_index = \n torch.tensor(device_edges, dtype=torch.long)
 data["account", "sends_to", "account"].edge_index = \n torch.tensor(transfer_edges, dtype=torch.long)
 return data

Edge Design Decisions

The choice of what constitutes an edge matters enormously. We experimented with several edge types: shared device, shared IP, shared merchant, and direct transfer.

Shared device edges turned out to be the single strongest signal for account takeover detection. If account A and account B have never been linked but suddenly share a device, that's worth investigating. The GNN picks up on this because the message passing aggregates information from the device node to both account nodes.

We did find that shared merchant edges added too much noise at scale. Popular merchants create dense cliques that drown out meaningful connections. We addressed this by weighting edges inversely proportional to merchant popularity.

Related reading: Image Segmentation Pipelines for Medical Imaging: U-Net Vari.

GNN Architecture for Fraud

We use a heterogeneous graph attention network variant with modifications for the fraud domain. The base architecture applies type-specific linear projections to node features, then runs message passing with attention weights that depend on both node features and edge features.

import torch.nn as nn
import torch.nn.functional as F
from torch_geometric.nn import HeteroConv, GATConv, Linear

class FraudGNN(nn.Module):
 def __init__(self, metadata, hidden_dim=128, num_layers=3):
 super().__init__()
 self.encoders = nn.ModuleDict()
 for node_type in metadata[0]:
 self.encoders[node_type] = Linear(-1, hidden_dim)
 self.convs = nn.ModuleList()
 for _ in range(num_layers):
 conv = HeteroConv({
 edge_type: GATConv(
 (-1, -1), hidden_dim, heads=4,
 add_self_loops=False, concat=False
 )
 for edge_type in metadata[1]
 })
 self.convs.append(conv)
 self.classifier = nn.Sequential(
 nn.Linear(hidden_dim, 64),
 nn.ReLU(),
 nn.Dropout(0.3),
 nn.Linear(64, 1),
 )

 def forward(self, x_dict, edge_index_dict):
 h_dict = {
 key: F.relu(self.encoders[key](x))
 for key, x in x_dict.items()
 }
 for conv in self.convs:
 h_dict = conv(h_dict, edge_index_dict)
 h_dict = {k: F.relu(v) for k, v in h_dict.items()}
 return self.classifier(h_dict["account"]).squeeze(-1)

Handling Class Imbalance

Fraud rates in real datasets are typically 0.1-0.5 percent. Standard oversampling doesn't work well for graph data because you can't independently duplicate nodes. They're connected to other nodes, and duplicating the connections creates artificial structure. We use focal loss with a gamma of 2.0, which reduces the weight on easy negatives and focuses the model on hard-to-classify examples near the decision boundary.

Neighborhood sampling during training also helps. Instead of training on the full graph, we sample 2-hop neighborhoods around labeled fraud nodes, which naturally creates a more balanced mini-batch.

Real-Time Scoring

Scoring a transaction in real time with a GNN requires the graph to be current. You can't rebuild the entire graph for each transaction. Instead, we maintain an evolving graph in memory and apply incremental updates as new edges arrive. The GNN forward pass for a single node with a 2-hop neighborhood takes about 5ms on a GPU, which fits within our 50ms latency budget for fraud scoring.

The graph gets fully rebuilt from the database once daily during low-traffic hours. Between rebuilds, new edges are appended and expired edges older than 30 days are lazily removed. This approach keeps the graph fresh without the cost of continuous full reconstruction.

See also: Data Versioning for Reproducible ML Experiments: DVC and Lak.

Feature Engineering for Graph Models

The node and edge features you feed into the GNN matter as much as the graph structure. Raw transaction features (amount, timestamp, merchant) are a starting point, but engineered features capture patterns that the GNN message passing alone can't learn efficiently.

For account nodes, we compute velocity features: number of transactions in the last hour, last day, and last week, compared to the account historical average. A sudden spike in transaction velocity is a strong fraud signal on its own, and giving it to the GNN as a node feature lets the model learn how velocity interacts with network structure.

For device nodes, we compute a device risk score based on historical fraud rates. Devices that have been associated with multiple fraudulent accounts get a higher risk score, and this score propagates through the GNN to connected accounts. This creates a guilt-by-association signal that's surprisingly effective for account takeover detection.

Edge features are often overlooked but they carry important information. For transaction edges, we include the amount relative to the account average, the time since the previous transaction, and whether the transaction was in a new merchant category. For device edges, we include the time of first association and whether multiple accounts share the device simultaneously versus sequentially.

Training Strategy for Production Scale

Production fraud graphs can have hundreds of millions of nodes and billions of edges. Training a GNN on a graph this large requires careful mini-batching. We use PyG NeighborLoader which samples subgraphs around target nodes, keeping the mini-batch manageable while providing enough graph context for meaningful message passing.

See also: Object Detection Model Selection: YOLO vs DETR vs EfficientD.

The sampling strategy affects model quality. We sample 15 neighbors at the first hop and 10 at the second hop. More neighbors gives better message aggregation but larger mini-batches. On our hardware (4x A100 GPUs), this sampling configuration allows batch sizes of 512 target nodes with 2-layer message passing, and training converges in about 50 epochs on our graph of 200M nodes.

We retrain the model daily on a sliding 90-day window of transaction data. The graph evolves constantly as new accounts are created, new devices appear, and transaction patterns change. A model trained on stale data misses emerging fraud patterns. The daily retraining takes about 3 hours on our GPU cluster, and the new model goes through a shadow scoring period before replacing the production model.

Explainability for Investigations

When the GNN flags a transaction as fraudulent, the investigation team needs to understand why. A raw fraud probability isn't actionable. They need to know which graph connections triggered the alert and what patterns the model identified.

We generate explanations by computing attention weight distributions across the message passing layers. High attention weights on a particular edge mean that connection was influential in the fraud prediction. We visualize this as a subgraph centered on the flagged account, with edge thickness proportional to attention weight.

For the investigation team, the most useful explanation format is a natural language summary: this account was flagged because it shares device X with 3 other accounts that have recent fraud history, and it made 5 transactions at merchant Y within 10 minutes, which is unusual for this account age. We generate these summaries by translating the high-attention subgraph features into templated sentences.

This explainability layer also helps us debug the model. When the fraud team disagrees with a prediction, we can trace the model reasoning and identify whether it found a genuine pattern that the team missed or whether it learned a spurious correlation that needs to be corrected. This feedback loop between the model and the investigation team has been the most valuable part of the entire system.