Rilevamento delle frodi con risoluzione delle entità e reti neurali a grafo |  di Stefan Berkner |  Agosto 2023

 | Intelligenza-Artificiale

Una guida pratica su come la risoluzione delle entità migliora l’apprendimento automatico per rilevare le frodi

Rappresentazione di una rete neurale a grafo (immagine generata dall’Autore utilizzando Bing Image Creator)

Le frodi online sono un problema in continua crescita per la finanza, l’e-commerce e altri settori correlati. In risposta a questa minaccia, le organizzazioni utilizzano meccanismi di rilevamento delle frodi basati sull’apprendimento automatico e sull’analisi comportamentale. Queste tecnologie consentono il rilevamento di modelli insoliti, comportamenti anomali e attività fraudolente in tempo reale.

Purtroppo spesso viene presa in considerazione solo la transazione in corso, ad esempio un ordine, oppure il processo si basa esclusivamente sui dati storici del profilo del cliente, identificati da un ID cliente. Tuttavia, i truffatori professionisti possono creare profili di clienti utilizzando transazioni di basso valore per creare un’immagine positiva del proprio profilo. Inoltre, potrebbero creare più profili simili contemporaneamente. Solo dopo la frode l’azienda aggredita si rende conto che i profili dei clienti erano collegati tra loro.

Utilizzando la risoluzione delle entità è possibile combinare facilmente diversi profili cliente in un’unica visione del cliente a 360°, consentendo di vedere il quadro completo di tutte le transazioni storiche. Mentre l’utilizzo di questi dati nell’apprendimento automatico, ad esempio utilizzando una rete neurale o anche una semplice regressione lineare, fornirebbe già un valore aggiuntivo per il modello risultante, il valore reale emerge anche osservando come le singole transazioni sono collegate tra loro. È qui che entrano in gioco le reti neurali a grafo (GNN). Oltre a osservare le caratteristiche estratte dai record transazionali, offrono anche la possibilità di osservare le caratteristiche generate dai bordi del grafico (come le transazioni sono collegate tra loro) o anche solo il layout generale del grafico delle entità.

Prima di approfondire i dettagli, ho un disclaimer da inserire qui: sono uno sviluppatore ed esperto di risoluzione di entità e non un data scientist o un esperto di ML. Anche se penso che l’approccio generale sia corretto, potrei non seguire le migliori pratiche, né essere in grado di spiegare alcuni aspetti come il numero di nodi nascosti. Usa questo articolo come ispirazione e attingi alla tua esperienza per quanto riguarda il layout o la configurazione della GNN.

Ai fini di questo articolo voglio concentrarmi sulle informazioni ottenute dal layout del grafico delle entità. A questo scopo ho creato un piccolo script Golang che genera entità. Ogni entità è etichettata come fraudolenta o non fraudolenta ed è composta da record (ordini) e edge (come tali ordini sono collegati). Vedere il seguente esempio di una singola entità:

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

Ogni record ha due (potenziali) caratteristiche, il valore totale e il numero di articoli acquistati. Tuttavia, lo script di generazione ha randomizzato completamente questi valori, quindi non dovrebbero fornire valore quando si tratta di indovinare l’etichetta di frode. Ciascun bordo è inoltre dotato di due funzioni R1 e R2. Questi potrebbero ad esempio rappresentare se i due record A e B sono collegati tramite un nome e indirizzo simili (R1) o tramite un indirizzo email simile (R2). Inoltre ho intenzionalmente tralasciato tutti gli attributi che non sono rilevanti per questo esempio (nome, indirizzo, email, numero di telefono, ecc.), ma che di solito sono rilevanti in anticipo per il processo di risoluzione dell’entità. Poiché anche R1 e R2 sono randomizzati, non forniscono valore per il GNN. Tuttavia, in base all’etichetta di frode, i bordi sono disposti in due modi possibili: un layout a stella (frode=0) o un layout casuale (frode=1).

L’idea è che è più probabile che un cliente non fraudolento fornisca dati rilevanti corrispondenti in modo accurato, solitamente lo stesso indirizzo e lo stesso nome, con solo pochi errori di ortografia qua e là. Quindi potrebbero verificarsi nuove transazioni riconosciuto come duplicato.

Entità deduplicata (immagine dell’autore)

Un cliente fraudolento potrebbe voler nascondere il fatto che dietro il computer si tratta sempre della stessa persona, utilizzando nomi e indirizzi diversi. Tuttavia, gli strumenti di risoluzione delle entità potrebbero comunque riconoscere la somiglianza (ad esempio somiglianza geografica e temporale, modelli ricorrenti nell’indirizzo e-mail, ID dispositivo, ecc.), ma il grafico dell’entità potrebbe apparire più complesso.

Entità complessa, forse fraudolenta (immagine dell’autore)

Per renderlo un po’ meno banale, lo script di generazione ha anche un tasso di errore del 5%, il che significa che le entità vengono etichettate come fraudolente quando hanno un layout a stella ed etichettate come non fraudolente per il layout casuale. Inoltre ci sono alcuni casi in cui i dati non sono sufficienti per determinare il layout effettivo (ad esempio, solo uno o due record).

{
"fraud":1,
"records":(
{
"id":0,
"totalValue":85,
"items":5
}
),
"edges":(

)
}

In realtà molto probabilmente otterresti preziose informazioni da tutti e tre i tipi di funzionalità (attributi del record, attributi del bordo e layout del bordo). I seguenti esempi di codice prenderanno in considerazione questo aspetto, ma i dati generati no.

L’esempio utilizza Python (ad eccezione della generazione dei dati) e DGL con un backend di Python. Puoi trovare il taccuino jupyter completo, i dati e lo script di generazione su github.

Iniziamo con l’importazione del set di dati:

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

Questo elabora il file delle entità, che è un file di linea JSON, in cui ogni riga rappresenta una singola entità. Durante l’iterazione su ciascuna entità, genera le caratteristiche del bordo (tensore lungo con forma (e, 2), e=numero di bordi) e le caratteristiche del nodo (tensore lungo con forma (n, 2), n=numero di nodi). Quindi procede a costruire il grafico basato su a e b (tensori lunghi ciascuno con forma (e, 1)) e assegna le caratteristiche del bordo e del grafico a quel grafico. Tutti i grafici risultanti vengono quindi aggiunti al set di dati.

Ora che abbiamo i dati pronti, dobbiamo pensare all’architettura della nostra GNN. Questo è quello che mi è venuto in mente, ma probabilmente può essere adattato molto di più alle esigenze reali:

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

Il costruttore prende il numero di caratteristiche del nodo, il numero di caratteristiche del bordo, il numero di nodi nascosti e il numero di etichette (classi). Quindi crea due livelli: a Livello NNConv che calcola i nodi nascosti in base alle caratteristiche del bordo e del nodo, quindi a Livello GraphSAGE che calcola l’etichetta risultante in base ai nodi nascosti.

Quasi lì. Successivamente prepariamo i dati per l’addestramento e il test.

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
)

Dividiamo con un rapporto 80/20 utilizzando il campionamento casuale e creiamo un caricatore di dati per ciascuno dei campioni.

L’ultimo passaggio consiste nell’inizializzare il modello con i nostri dati, eseguire l’addestramento e successivamente testare il risultato.

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)

Inizializziamo il modello fornendo le dimensioni delle caratteristiche per nodi e bordi (entrambi 2 nel nostro caso), i nodi nascosti (64) e la quantità di etichette (2 perché si tratta di frode o meno). L’ottimizzatore viene quindi inizializzato con un tasso di apprendimento di 0,01. Successivamente eseguiamo un totale di 50 iterazioni di formazione. Una volta terminato l’addestramento, testiamo i risultati utilizzando il caricatore di dati di test e stampiamo la precisione risultante.

Per varie esecuzioni, ho ottenuto una precisione tipica compresa tra il 70 e l’85%. Tuttavia, con poche eccezioni, si scende a qualcosa come il 55%.

Dato che l’unica informazione utilizzabile dal nostro set di dati di esempio è la spiegazione di come sono collegati i nodi, i risultati iniziali sembrano molto promettenti e suggeriscono che tassi di precisione più elevati sarebbero possibili con i dati del mondo reale e una maggiore formazione.

Ovviamente quando si lavora con dati reali, il layout non è così coerente e non fornisce una correlazione evidente tra layout e comportamento fraudolento. Pertanto, dovresti prendere in considerazione anche le caratteristiche dei bordi e dei nodi. Il punto chiave di questo articolo dovrebbe essere che la risoluzione delle entità fornisce i dati ideali per il rilevamento delle frodi utilizzando reti neurali a grafo e dovrebbe essere considerata parte dell’arsenale di strumenti di un ingegnere di rilevamento delle frodi.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *