Guide pratiche
Costruire sistemi di raccomandazione basati sull’IA
Una guida pratica alla creazione di sistemi di raccomandazione con il filtraggio collaborativo, i metodi basati sui contenuti e i moderni approcci di deep learning. Dalla progettazione alla messa in produzione.

AI.info Team ·
Inquadrare il problema: prima il recupero dei candidati, poi il ranking
Molti sistemi di raccomandazione in produzione che gestiscono cataloghi più ampi di quelli usati per semplici esempi sono costruiti in due fasi, anziché attorno a un solo modello:
- La generazione dei candidati (recupero) riduce milioni di articoli a poche centinaia di candidati plausibili per ciascun utente, ottimizzando la recall.
- Il ranking prende quei candidati e li ordina con precisione, ottimizzando la precisione nelle prime posizioni della lista, di solito con un modello più pesante che può permettersi di assegnare un punteggio a poche centinaia di articoli invece che a milioni.
Tentare di applicare un unico modello di deep learning all’intero catalogo al momento della richiesta non è scalabile oltre qualche migliaio di articoli. Progettate fin dal primo giorno la suddivisione in due fasi, anche se inizialmente la fase di ranking si basa solo su una semplice euristica.
I dati: il feedback implicito è quello che avete davvero
La maggior parte dei team non dispone di valutazioni a stelle: dispone di clic, aggiunte al carrello, tempo di visione e acquisti. Questo è feedback implicito: solo segnali positivi, nessun segnale negativo esplicito. E l’assenza di un’interazione non significa che un articolo non piaccia: potrebbe voler dire che «non è mai stato mostrato». Questa distinzione determina tutte le scelte successive:
- Non potete addestrare un modello di regressione su una scala da 1 a 5. Servono modelli progettati per i segnali impliciti (ALS con ponderazione della confidenza, BPR, WARP).
- La distorsione a favore degli articoli popolari è forte: un modello ingenuo si limita a reimparare a «raccomandare ciò che è popolare». È importante ridurre i pesi o usare il campionamento negativo.
- La distorsione dovuta all’esposizione fa sì che i dati di addestramento riflettano solo ciò che il sistema attuale mostra già alle persone. Questo limita, senza che sia evidente, quanto un nuovo modello possa migliorare i risultati finché non introducete una componente di esplorazione.
Iniziate costruendo una matrice sparsa delle interazioni tra utenti e articoli, assegnando pesi agli eventi (per esempio, acquisto=5, aggiunta al carrello=3, clic=1) come misura indiretta della confidenza, secondo l’approccio descritto da Hu, Koren e Volinsky in «Collaborative Filtering for Implicit Feedback Datasets»: è il paper su cui si basa l’implementazione di ALS della libreria implicit.
| Evento | Peso |
|---|---|
| Acquisto | 5 |
| Aggiunta al carrello | 3 |
| Clic | 1 |
Prima di addestrare qualsiasi modello, dedicate davvero del tempo alla pipeline delle interazioni: eliminate i clic ripetuti a breve distanza (un utente che aggiorna una pagina non dovrebbe generare cinque segnali distinti), ponete un limite alla durata delle sessioni anomale e scegliete un intervallo temporale da considerare (di solito 30-90 giorni), in modo che la matrice rifletta i comportamenti attuali anziché uno storico ormai superato. Questo lavoro poco appariscente migliora le metriche offline più della sostituzione di un algoritmo con un altro, ed è il passaggio in cui la maggior parte dei team investe troppo poco.
Il punto di partenza: fattorizzazione della matrice con implicit
Prima di passare a una rete neurale, mettete in funzione un modello di riferimento basato sulla fattorizzazione della matrice. La libreria implicit offre una veloce implementazione multithread di Alternating Least Squares (ALS), che usa Cython/OpenMP (e kernel CUDA per ALS e BPR sulle GPU compatibili). Dovrebbe essere il vostro primo punto di verifica, non l’ultima risorsa: è davvero competitiva su molti cataloghi di medie dimensioni e si può mettere in funzione in un pomeriggio.
import implicit
from implicit.nearest_neighbours import bm25_weight
from scipy.sparse import csr_matrix
# user_item_csr: sparse matrix, rows=users, cols=items, values=raw event counts
weighted = bm25_weight(user_item_csr, K1=100, B=0.8).tocsr()
model = implicit.als.AlternatingLeastSquares(
factors=64,
regularization=0.05,
iterations=20,
use_gpu=False,
)
model.fit(weighted)
# top-10 recommendations for a single user, excluding items already interacted with
user_id = 4821
recommended = model.recommend(
user_id,
weighted[user_id],
N=10,
filter_already_liked_items=True,
)
# item-to-item similarity for "more like this" surfaces
similar = model.similar_items(itemid=901, N=10)
La ponderazione BM25 (presa in prestito dal ranking dei risultati di ricerca) riduce, prima della fattorizzazione, il peso di utenti e articoli con un numero spropositato di interazioni. È una soluzione semplice ed efficace alla distorsione a favore degli articoli popolari: è facile trascurarla, ma non dovreste farlo.
Risolvere il problema del cold start con un modello ibrido: LightFM
Il filtraggio collaborativo puro non dispone di una rappresentazione per un utente o un elemento appena arrivato: non c’è uno storico di interazioni da fattorizzare. LightFM risolve il problema apprendendo rappresentazioni latenti sia dalle interazioni sia dai metadati delle caratteristiche aggiuntive (categoria dell’elemento, fascia di prezzo, canale di registrazione dell’utente, tag). Così, anche un elemento con zero interazioni ottiene un embedding utilizzabile, costruito soltanto a partire dalle sue caratteristiche.
LightFM implementa anche la funzione di perdita WARP (Weighted Approximate-Rank Pairwise), che campiona ripetutamente elementi negativi finché non ne trova uno che viola l’ordinamento desiderato, poi aggiorna il modello per correggere quella specifica violazione. È una forma di apprendimento attivo sugli esempi negativi e supera sistematicamente la più comune funzione di perdita BPR nell’ottimizzazione della precisione in cima alla lista: LightFM è una delle poche librerie a offrire un’implementazione di WARP pronta per l’uso in produzione.
from lightfm import LightFM
from lightfm.evaluation import precision_at_k, auc_score
model = LightFM(loss="warp", no_components=64, learning_rate=0.05)
model.fit(
interactions_train, # sparse COO matrix of user-item interactions
item_features=item_features, # sparse feature matrix, one-hot/multi-hot metadata
epochs=30,
num_threads=4,
)
precision = precision_at_k(
model, interactions_test, item_features=item_features, k=10
).mean()
auc = auc_score(model, interactions_test, item_features=item_features).mean()
print(f"precision@10={precision:.4f} auc={auc:.4f}")
Per i nuovi elementi, affianca al modello un semplice meccanismo di ripiego: ordinali in base alla popolarità nella categoria o a un punteggio di somiglianza dei contenuti, finché non accumulano abbastanza interazioni — spesso ne bastano alcune decine — perché prevalga l’embedding appreso. Per i nuovi utenti, combina il punteggio di LightFM con una lista non personalizzata basata sulla popolarità o sugli elementi «di tendenza nella categoria», e aumenta il peso del punteggio personalizzato man mano che cresce il numero di interazioni dell’utente. Una semplice combinazione lineare basata su quel numero funziona sorprendentemente bene ed evita il classico errore di mostrare a un utente appena arrivato un feed vuoto o privo di senso.
Anche il campionamento degli esempi negativi conta: poiché osservi soltanto interazioni positive, LightFM (e più in generale BPR/WARP) si addestra contrapponendo le interazioni osservate a elementi campionati con cui l’utente non ha interagito. Se gli esempi negativi vengono estratti in modo uniforme e casuale, il modello impara soprattutto a distinguere gli elementi popolari da quelli poco noti, anziché cogliere preferenze più precise. Campionare gli esempi negativi in proporzione alla popolarità degli elementi (o escludere nelle prime fasi dell’addestramento quelli ancora privi di interazioni dall’insieme dei possibili esempi negativi) produce ordinamenti più accurati.
Scalare il recupero con modelli neurali a due torri
Quando il catalogo e l’insieme delle caratteristiche diventano troppo ampi per la fattorizzazione di matrici — perché vuoi integrare embedding testuali, immagini, sequenze di sessione o contesto in tempo reale nella fase di recupero — passa a un’architettura a due torri (dual encoder) con TensorFlow Recommenders (TFRS). Una torre codifica l’utente o la query (attività recente, dati demografici, contesto), l’altra l’elemento candidato. Entrambe producono embedding di lunghezza fissa nello stesso spazio e il punteggio è il loro prodotto scalare.
La proprietà che permette a questa architettura di scalare è che, poiché le due torri interagiscono soltanto attraverso un prodotto scalare, puoi precalcolare offline l’embedding di ogni elemento ed eseguire il recupero cercando i vicini più prossimi in modo approssimativo rispetto all’embedding dell’utente calcolato al momento della richiesta.
import tensorflow as tf
import tensorflow_recommenders as tfrs
class TwoTowerModel(tfrs.Model):
def __init__(self, user_model, item_model, candidate_items):
super().__init__()
self.user_model = user_model
self.item_model = item_model
self.task = tfrs.tasks.Retrieval(
metrics=tfrs.metrics.FactorizedTopK(
candidates=candidate_items.batch(128).map(item_model)
)
)
def compute_loss(self, features, training=False):
user_embeddings = self.user_model(features["user_id"])
item_embeddings = self.item_model(features["item_id"])
return self.task(user_embeddings, item_embeddings)
model = TwoTowerModel(user_tower, item_tower, item_ids_dataset)
model.compile(optimizer=tf.keras.optimizers.Adagrad(0.1))
model.fit(train_dataset.batch(4096), epochs=5)
tfrs.metrics.FactorizedTopK calcola durante l’addestramento le metriche di recupero (recall per diversi valori di k) sull’intero insieme dei candidati: è il controllo di base da fare prima di qualsiasi distribuzione. Se recall@50 è basso in addestramento, sarà peggiore in produzione dopo la quantizzazione e la ricerca approssimativa.
Per servire le richieste, esporta gli embedding della torre degli elementi in un indice per la ricerca approssimativa dei vicini più prossimi (ANN), come FAISS o ScaNN (integrato in TFRS tramite tfrs.layers.factorized_top_k.ScaNN). Così, al momento della richiesta, il recupero richiede una ricerca vettoriale inferiore al millisecondo, anziché un passaggio completo del modello su ogni elemento.
Ripartisci esplicitamente il tempo di latenza disponibile tra le fasi della pipeline: al calcolo dell’embedding dell’utente, alla ricerca nell’indice ANN e al passaggio di ordinamento va assegnata una quota del tempo totale a disposizione per la richiesta (spesso 50-150 ms dall’inizio alla fine per un’interfaccia rivolta agli utenti). Aggiorna l’indice degli embedding degli elementi secondo una cadenza adeguata alla velocità con cui cambia il catalogo — ogni ora per un inventario che cambia rapidamente, ogni giorno per cataloghi stabili — anziché cercare di aggiornarlo in tempo reale: per la fase di recupero, la complessità operativa aggiuntiva raramente vale la pena.
La fase di ranking
Il recupero produce una rosa di candidati; il ranking decide l’ordine in cui l’utente li vede. In questa fase ci si possono permettere caratteristiche più ricche e più lente da calcolare, perché si valutano solo centinaia di candidati anziché milioni:
- Caratteristiche che incrociano dati dell’utente e dell’elemento (quanto tempo è passato dall’ultima interazione con questa categoria, il prezzo rispetto alla spesa abituale dell’utente).
- Un modello ad alberi con gradient boosting (LightGBM/XGBoost con un obiettivo pairwise o LambdaMART) è una solida scelta di partenza, economica da gestire: nella maggior parte dei casi tratta caratteristiche tabellari eterogenee meglio di un modello profondo.
- Se usi già l’ecosistema TFRS,
tfrs.tasks.Rankingcon una funzione di perdita pointwise o listwise è il passo successivo naturale, una volta che hai registrato le caratteristiche della fase di recupero da usare per l’addestramento.
Non passare direttamente a un modello di ranking profondo. Dopo una prima versione basata sul recupero, un modello di ranking GBDT con caratteristiche ben scelte è di solito il secondo passo con il maggiore impatto, ed è molto più facile analizzare l’importanza delle sue caratteristiche che in una rete neurale.
Valutazione: prima le metriche offline, poi i test A/B
Le metriche offline indicano se vale la pena sottoporre un modello a una sperimentazione; non dicono se otterrà risultati migliori in produzione.
- Recall@k: tra tutti gli elementi con cui l’utente ha effettivamente interagito nel periodo tenuto da parte per la valutazione, quale quota compare tra i primi k candidati? È la metrica principale per la fase di recupero.
- NDCG@k (Normalized Discounted Cumulative Gain): premia la presenza degli elementi più pertinenti vicino alla cima della lista, normalizzando il risultato rispetto all’ordine ideale (IDCG@k). È la metrica standard per la fase di ranking, perché conta l’ordine, non soltanto la presenza.
- Precision@k / MAP: sono complementi utili, soprattutto precision@k quando si mostrano solo pochi spazi (per esempio, un carosello nella homepage).
Suddividi i dati in base al tempo, non in modo casuale: tieni da parte le interazioni più recenti di ciascun utente anziché un campione casuale, perché una suddivisione casuale lascia trapelare informazioni sul futuro e gonfia i punteggi offline rispetto ai risultati che vedrai nell’uso reale.
Ottenere buoni risultati offline è necessario, ma non sufficiente. Prima di un’adozione su larga scala, passa a un test A/B che misuri il CTR, il tasso di conversione e una metrica di controllo (durata della sessione, tasso di ritorno). Non è raro che un modello con un NDCG offline migliore perda un test A/B perché privilegia troppo gli elementi già popolari o riduce la varietà del catalogo in un modo che le metriche offline non penalizzano: monitora la copertura (la quota del catalogo che viene consigliata almeno una volta) come metrica di controllo della varietà, accanto alla principale metrica aziendale.
Problemi ricorrenti
- Concentrazione sulla popolarità: i consigli convergono sulla stessa dozzina di bestseller. Si può correggere con una ponderazione (BM25, propensione inversa) o con un termine esplicito che favorisca la varietà nella fase di riordinamento.
- Cicli di feedback: un modello addestrato solo su ciò che un modello precedente ha mostrato non può scoprire gli elementi che quest’ultimo non ha mai mostrato. Introduci una piccola quota di esplorazione casuale nel traffico di produzione e registrala: è anche ciò che ti permetterà, in seguito, di calcolare metriche offline non distorte.
- Disallineamento tra addestramento e uso: le caratteristiche calcolate in modo diverso durante l’addestramento (pipeline batch) e durante l’uso (servizio in tempo reale) peggiorano le prestazioni del modello senza che sia subito evidente. Usa lo stesso codice per calcolare le caratteristiche in entrambi i percorsi e registra i loro valori durante l’uso, così da poterli confrontare con i dati di addestramento.
- Cold start ignorato: se non progetti esplicitamente una soluzione alternativa per i nuovi utenti o elementi, la tua architettura finirà per collocarli sempre in fondo alla classifica, perché non accumuleranno mai le interazioni necessarie per competere.
Da dove cominciare, concretamente
Se stai costruendo questo sistema oggi, configura questa settimana ALS con implicit sui tuoi log delle interazioni e stabilisci i valori di riferimento per precision@k e recall@k. Poi decidi se ti servono caratteristiche aggiuntive (passando a LightFM) oppure maggiore scalabilità o contesto (passando a un modello TFRS a due torri), in base ai limiti che la prima versione mostra davvero, non a quale architettura sarebbe più interessante costruire.