Rilevamento delle frodi con la risoluzione delle entità e reti neurali grafiche

Rilevamento frodi con risoluzione entità e reti neurali grafiche

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

Rappresentazione di una rete neurale grafica (Immagine generata dall'Autore utilizzando Bing Image Creator)

La frode online è un problema in continua crescita per il settore finanziario, l’e-commerce e altre industrie correlate. In risposta a questa minaccia, le organizzazioni utilizzano meccanismi di rilevamento frodi basati sull’apprendimento automatico e sull’analisi comportamentale. Queste tecnologie consentono la rilevazione di modelli insoliti, comportamenti anomali e attività fraudolente in tempo reale.

Purtroppo, spesso si prende in considerazione solo la transazione corrente, ad esempio un ordine, o il processo si basa esclusivamente su dati storici dal profilo del cliente, identificato da un ID cliente. Tuttavia, i truffatori professionisti possono creare profili clienti utilizzando transazioni di scarso valore per costruire un’immagine positiva del loro profilo. Inoltre, potrebbero creare contemporaneamente più profili simili. È solo dopo che si è verificata la frode che l’azienda attaccata si rende conto che questi profili clienti erano correlati tra loro.

Utilizzando la risoluzione delle entità, è possibile combinare facilmente diversi profili clienti in una visione completa del cliente a 360°, consentendo di vedere l’intero quadro 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 vero valore deriva anche dall’analisi di come le singole transazioni sono connesse tra loro. Ecco dove entrano in gioco le reti neurali grafiche (GNN). Oltre a guardare le caratteristiche estratte dai record transazionali, offrono anche la possibilità di guardare le caratteristiche generate dagli archi del grafo (come le transazioni sono collegate tra loro) o anche solo la disposizione generale del grafo delle entità.

Dati di esempio

Prima di approfondire i dettagli, voglio fare una premessa: sono uno sviluppatore ed esperto di risoluzione delle entità, non un data scientist o un esperto di apprendimento automatico. Sebbene ritenga che l’approccio generale sia corretto, potrei non seguire le migliori pratiche, né posso spiegare certi aspetti come il numero di nodi nascosti. Utilizza questo articolo come ispirazione e basati sulla tua esperienza personale per quanto riguarda la disposizione o la configurazione delle GNN.

Per gli scopi di questo articolo, voglio concentrarmi sulle informazioni ottenute dalla disposizione del grafo delle entità. A questo scopo, ho creato uno script in Golang che genera entità. Ogni entità viene etichettata come fraudolenta o non fraudolenta e consiste in record (ordini) e archi (come tali ordini sono collegati). Ecco un esempio di un’unica 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 completamente randomizzato questi valori, quindi non dovrebbero fornire informazioni utili per indovinare l’etichetta di frode. Ogni arco è anche accompagnato da due caratteristiche R1 e R2. Queste potrebbero ad esempio rappresentare se i due record A e B sono collegati tramite un nome e un indirizzo simili (R1) o tramite un indirizzo email simile (R2). Inoltre, ho intenzionalmente omesso tutti gli attributi non rilevanti per questo esempio (nome, indirizzo, email, numero di telefono, ecc.), ma sono di solito rilevanti per il processo di risoluzione delle entità anticipato. Poiché R1 e R2 sono anche casualizzati, non forniscono valore per le GNN. Tuttavia, in base all’etichetta di frode, gli archi vengono disposti in due modi possibili: una disposizione a forma di stella (frode=0) o una disposizione casuale (frode=1).

L’idea è che un cliente non fraudolento è più propenso a fornire dati pertinenti per il matching accurato, di solito lo stesso indirizzo e lo stesso nome, con solo alcuni errori di ortografia qua e là. Di conseguenza, le nuove transazioni possono essere riconosciute come duplicati.

Entità deduplicata (Immagine dell'autore)

Un cliente fraudolento potrebbe voler nascondere il fatto di essere ancora la stessa persona dietro al computer, utilizzando vari nomi e indirizzi. Tuttavia, gli strumenti di risoluzione delle entità possono comunque riconoscere la similarità (ad esempio, la similarità geografica e temporale, i modelli ricorrenti nell’indirizzo email, gli ID dei dispositivi, ecc.), ma il grafo delle entità potrebbe apparire più complesso.

Entità complessa, possibilmente fraudolenta (Immagine dell'autore)

Per rendere la situazione 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 una disposizione a forma di stella e vengono etichettate come non fraudolente per la disposizione casuale. Inoltre, ci sono alcuni casi in cui i dati sono insufficienti per determinare la disposizione effettiva (ad esempio, solo uno o due record).

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

Nella realtà, è probabile che si ottengano informazioni preziose da tutti e tre i tipi di caratteristiche (attributi dei record, attributi degli archi e disposizione degli archi). Gli esempi di codice seguenti terranno conto di ciò, ma i dati generati no.

Creazione del Dataset

L’esempio utilizza python (ad eccezione della generazione dei dati) e DGL con un backend pytorch. È possibile trovare il notebook Jupyter completo, i dati e lo script di generazione su GitHub.

Iniziamo importando il dataset:

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

Ciò processa il file delle entità, che è un file JSON-line, dove ogni riga rappresenta un’entità singola. Mentre si itera su ogni entità, si generano le caratteristiche degli archi (tensore long con forma [e, 2], e=numero di archi) e le caratteristiche dei nodi (tensore long con forma [n, 2], n=numero di nodi). Successivamente si procede a costruire il grafo basato su a e b (tensori long ognuno con forma [e, 1]) e si assegnano le caratteristiche degli archi e del grafo a quel grafo. Tutti i grafi risultanti vengono quindi aggiunti al dataset.

Architettura del Modello

Ora che i dati sono pronti, dobbiamo pensare all’architettura del nostro GNN. Questo è ciò che ho ideato, ma probabilmente può essere adattato molto di più alle effettive esigenze:

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

Il costruttore prende il numero di caratteristiche dei nodi, il numero di caratteristiche degli archi, il numero di nodi nascosti e il numero di etichette (classi). Crea quindi due livelli: un livello NNConv che calcola i nodi nascosti basandosi sulle caratteristiche degli archi e dei nodi, e quindi un livello GraphSAGE che calcola l’etichetta risultante basandosi sui nodi nascosti.

Allenamento e Test

Quasi fatto. Ora prepariamo i dati per l’allenamento 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)

Facciamo una suddivisione con un rapporto 80/20 utilizzando un campionamento casuale e creiamo un caricatore di dati per ciascun campione.

L’ultimo passo è inizializzare il modello con i nostri dati, eseguire l’allenamento 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("Accuracy del test:", acc)

Inizializziamo il modello fornendo le dimensioni delle caratteristiche per i nodi e gli archi (entrambi 2 nel nostro caso), i nodi nascosti (64) e il numero di etichette (2 perché è o frode o no). L’ottimizzatore viene quindi inizializzato con un tasso di apprendimento di 0.01. In seguito eseguiamo un totale di 50 iterazioni di allenamento. Una volta completato l’allenamento, testiamo i risultati utilizzando il caricatore di dati di test e stampiamo l’accuratezza risultante.

In diverse esecuzioni, ho ottenuto un’accuratezza tipica nell’intervallo dal 70% all’85%. Tuttavia, con alcune eccezioni, la percentuale è scesa a circa il 55%.

Conclusioni

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

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

Originariamente pubblicato su https://tilores.io.