Topic modeling con BERTopic: che cosa c’è dentro un archivio che nessuno ha letto

Quarantamila ticket, dieci anni di verbali, l'archivio di una newsletter: raccogliere testo è facile, sapere di cosa parla è un altro mestiere. BERTopic raggruppa i documenti per significato e poi dice a parole di cosa parla ogni gruppo — con quattro passi che si possono sostituire uno per uno, un mucchio di scarti che spaventa la prima volta, e una trappola sulle stopword che vale il tutorial da sola.

Un archivista riceve quarantamila fascicoli e la richiesta di scrivere l’indice entro venerdì. Non può leggerli. Quello che può fare è metterli in pile — questi si somigliano, questi no — e poi guardare una pila alla volta per capire come chiamarla. L’ordine non lo decide lui: lo trovano i fascicoli mettendosi vicini.

È esattamente il mestiere del topic modeling, e capita più spesso di quanto sembri: i ticket di assistenza di un anno, i verbali di un consiglio comunale, le risposte aperte di un questionario, l’archivio di una newsletter. Raccogliere il testo è la parte facile e ormai la fa chiunque; sapere di che cosa parla è la parte che resta indietro.

Quattro passi indipendenti: ognuno si sostituisce senza rifare gli altri. 1. Vettori sentence-transformers ↔ qualunque modello di embedding 2. Meno dimensioni UMAP ↔ PCA, o nessuna riduzione 3. Gruppi HDBSCAN ↔ k-means, che non scarta niente 4. Parole del tema c-TF-IDF ↔ si affina con MMR, KeyBERT, un LLM Tema −1: gli scarti non è un tema — è ciò che non somiglia a niente Le stopword si tolgono al passo 4, non al passo 1: il passo 1 ha bisogno della frase intera.

La forza di BERTopic non è nessuno dei quattro algoritmi: è che sono quattro e non uno. Ognuno si cambia senza rieseguire gli altri, e la maggior parte del lavoro di messa a punto consiste nel rifare il quarto passo, che costa secondi.

Perché non LDA, e quando invece sì

Il metodo classico — Latent Dirichlet Allocation, 2003 — vede ogni documento come un sacchetto di parole e cerca le miscele di temi che spiegano quei sacchetti. Funziona, è veloce, gira su CPU e non ha bisogno di GPU né di modelli scaricati. Il suo limite è dichiarato nella premessa: dentro un sacchetto «guasto», «rotto» e «non funziona» sono tre stringhe senza parentela. Il senso non ci entra.

BERTopic parte dall’altra estremità. Trasforma ogni documento in un vettore con un modello di embedding, raggruppa i vettori, e solo alla fine torna alle parole per dare un nome ai gruppi. Il paper è di Maarten Grootendorst (2022) e il contributo originale non è la parte neurale — quella c’era già in Top2Vec e nel lavoro di Sia e colleghi — ma l’ultimo passo, di cui si parla più avanti.

Se il vostro corpus è in italiano c’è una ragione in più per guardare da questa parte, e si tocca con mano più avanti: gli strumenti classici arrivano con le loro liste di parole vuote già pronte per l’inglese e non per l’italiano, mentre qui la lingua la porta il modello di embedding.

Il giro minimo

pip install bertopic
from bertopic import BERTopic

# documenti: una lista di stringhe. Un ticket, un verbale, un'email per elemento.
modello = BERTopic(language="multilingual", min_topic_size=15, verbose=True)
temi, probabilita = modello.fit_transform(documenti)

print(modello.get_topic_info().head(12))

language="multilingual" carica paraphrase-multilingual-MiniLM-L12-v2, che copre oltre cinquanta lingue: il valore predefinito, "english", su un corpus italiano produce gruppi che sembrano casuali. Se volete qualità maggiore e potete aspettare, la documentazione indica paraphrase-multilingual-mpnet-base-v2, da passare con embedding_model=.

get_topic_info() restituisce una tabella: un tema per riga, quanti documenti contiene, e le dieci parole che lo rappresentano. È il primo momento in cui si capisce se il corpus ha una struttura o no.

Qui diamo per noti UMAP e HDBSCAN: perché in molte dimensioni le distanze smettono di distinguere, che cosa vuol dire raggruppare per densità invece che intorno a un centro, e perché un algoritmo di questa famiglia può legittimamente rifiutarsi di assegnare un punto. Il libro li costruisce da capo.

Leggi «Riduzione e clustering» nel libro →

Il tema −1, che la prima volta sembra un guasto

Nella tabella c’è quasi sempre una riga in cima con etichetta -1, e spesso è la più affollata di tutte. Non è un tema ed è la prima cosa da sapere: HDBSCAN raggruppa per densità e ha il diritto di dire «questo documento non somiglia abbastanza a nessun gruppo». Il -1 è il mucchio di quei rifiuti.

È una virtù, non un difetto: k-means avrebbe infilato ogni documento da qualche parte, e i temi ne sarebbero usciti diluiti. Ma quando il mucchio diventa la maggioranza del corpus l’analisi non serve più a niente, e ci sono tre modi di ridurlo, in ordine di invasività. (La documentazione avverte anche del contrario: se gli scarti sono tantissimi, a volte non c’è nessuna struttura da trovare, e nessuna delle tre strade la inventa.)

# 1. dopo l'addestramento, riassegnare gli scarti senza rifare niente
temi_nuovi = modello.reduce_outliers(documenti, temi)
modello.update_topics(documenti, topics=temi_nuovi)

# 2. rendere HDBSCAN meno schizzinoso (min_samples si abbassa da solo con min_topic_size)
from hdbscan import HDBSCAN
raggruppatore = HDBSCAN(min_cluster_size=15, min_samples=5,
                        metric="euclidean", cluster_selection_method="eom",
                        prediction_data=True)
modello = BERTopic(hdbscan_model=raggruppatore, language="multilingual")

# 3. cambiare algoritmo: k-means non scarta niente, per definizione
from sklearn.cluster import KMeans
modello = BERTopic(hdbscan_model=KMeans(n_clusters=40), language="multilingual")

La prima strada è quella da provare per prima, e non solo perché è la più rapida: permette di confrontare più strategie e più soglie senza riaddestrare, cosa che le altre due non consentono. La documentazione avverte, a proposito della seconda, di trovare un equilibrio: abbassare la soglia riduce gli scarti ma comincia a mettere documenti in temi a cui non appartengono — e quel danno, a differenza del mucchio di scarti, non si vede nella tabella.

La trappola delle stopword

Alla prima esecuzione i temi sono spesso rappresentati da parole come «di», «che», «per», «non». Il riflesso di chi viene dall’NLP classico è togliere le stopword dai documenti prima di darli in pasto al modello. È esattamente la cosa da non fare, e la ragione è nella struttura a quattro passi: il modello di embedding ha bisogno della frase intera per capirla. Un testo mutilato produce vettori peggiori, quindi gruppi peggiori — e i gruppi non si vedono, si vedono solo le parole.

Le stopword si tolgono al quarto passo, che riguarda solo la rappresentazione. È l’operazione che la documentazione consiglia caldamente, dichiarando di non averle trovato praticamente nessuno svantaggio.

from sklearn.feature_extraction.text import CountVectorizer

# scikit-learn ha una lista incorporata solo per l'inglese: per l'italiano
# si passa una lista propria (nltk, spaCy, o compilata a mano sul corpus)
IT = ["di", "che", "e", "il", "la", "per", "non", "un", "una", "in", "con"]

conta_parole = CountVectorizer(stop_words=IT, ngram_range=(1, 2), min_df=5)
modello.update_topics(documenti, vectorizer_model=conta_parole)

Due dettagli in quel frammento. Il primo è un limite di scikit-learn da conoscere: stop_words accetta la stringa "english" — l’unica lingua incorporata — oppure una lista. Per l’italiano si passa una lista, e vale la pena costruirla guardando il corpus invece di scaricarne una generica: in un archivio di ticket «buongiorno», «cordiali» e «saluti» sono stopword a tutti gli effetti.

Il secondo è ngram_range=(1, 2): ammette le coppie di parole nelle rappresentazioni, e «carta d’imbarco» dice molto più di «carta» e «imbarco» separati. C’è anche una strada automatica, che smorza le parole troppo diffuse invece di elencarle: ClassTfidfTransformer(reduce_frequent_words=True), dal modulo bertopic.vectorizers.

Il pezzo originale: c-TF-IDF

Il contributo del paper sta qui. I metodi che raggruppavano embedding prima di BERTopic descrivevano ogni gruppo con le parole più vicine al suo centro. È un’assunzione che regge solo se i gruppi sono più o meno sferici, e i gruppi di documenti non lo sono quasi mai: quando la forma è allungata o incavata, il centro cade in una zona dove non c’è nessun documento, e le parole che ne escono descrivono un tema che non esiste.

BERTopic non guarda il centro. Concatena tutti i documenti di un gruppo in un unico documento gigante e chiede: quali parole sono frequenti qui e rare negli altri gruppi? È il TF-IDF di sempre, spostato dal documento alla classe:

$$W_{t,c} = \mathrm{tf}_{t,c} \cdot \log\left(1 + \frac{A}{\mathrm{tf}_t}\right)$$

dove $\mathrm{tf}_{t,c}$ è quante volte il termine $t$ compare nella classe $c$, $\mathrm{tf}_t$ quante volte compare in tutte le classi messe insieme, e $A$ è il numero medio di parole per classe. L’uno dentro il logaritmo serve solo a tenere il risultato positivo.

La conseguenza pratica è quella che rende il metodo comodo: il quarto passo dipende solo dai raggruppamenti, non dai vettori. Cambiare stopword, allargare agli n-gram, chiedere più parole per tema — tutto si rifà con update_topics, in secondi, senza toccare gli embedding.

Come si muovono i temi nel tempo

Se i documenti hanno una data, la stessa proprietà dà quasi gratis l’analisi temporale. Il modello si addestra una volta sola sul corpus intero, ignorando le date; poi per ogni intervallo si ricalcola il solo c-TF-IDF, e si vede come lo stesso tema si dica con parole diverse a distanza di anni.

temi_nel_tempo = modello.topics_over_time(documenti, date, nr_bins=20)
modello.visualize_topics_over_time(temi_nel_tempo, top_n_topics=10)

L’esempio che il paper usa è chiaro: un tema «automobili» contiene «auto» e «veicolo» in qualunque epoca, ma i documenti del 2020 lo rappresentano anche con «Tesla» e «guida autonoma», parole che nel 1990 non sarebbero comparse. Il vantaggio dichiarato è il costo: le rappresentazioni locali si ottengono senza rifare né gli embedding né il raggruppamento.

Quattro cose da sapere prima di fidarsi

I risultati cambiano fra un’esecuzione e l’altra. UMAP è stocastico, e la stessa identica riga di codice eseguita due volte dà temi diversi. Per un’analisi che finisce in una relazione è inaccettabile: si passa un UMAP proprio con random_state=42, e la documentazione dichiara che questo costa in prestazioni — la parte parallela viene disattivata.

Un documento, un tema. È l’assunzione del metodo, e gli autori la elencano per primi fra i punti deboli: i documenti veri parlano spesso di più cose. Spezzarli in paragrafi aiuta ma non è una soluzione elegante; la matrice di probabilità di HDBSCAN dà un’approssimazione della distribuzione, ma resta un rattoppo a valle, perché l’addestramento è avvenuto lo stesso su un tema per documento.

Le parole del tema sono ridondanti. I documenti sono rappresentati in modo contestuale, i temi no: escono da un sacchetto di parole, e capita di leggere «guasto, guasti, difetto, difettoso, rotto» — cinque volte la stessa cosa al posto di cinque informazioni. Il rimedio è nella libreria: MaximalMarginalRelevance(diversity=0.3) da bertopic.representation, che riordina le parole candidate premiando la varietà.

I punteggi di coerenza dicono meno di quanto sembra. Il paper stesso lo dichiara due volte: coerenza e diversità sono indicazioni, non verdetti, perché misurano per procura un giudizio soggettivo; e cita un lavoro del 2021 secondo cui la correlazione fra coerenza automatica e giudizio umano potrebbe valere per i modelli classici e non per quelli neurali. Nei confronti riportati BERTopic è competitivo ma non domina: sulla diversità dei temi viene battuto sistematicamente da CTM in tutti e tre gli insiemi di dati provati.

Un modello di temi non vi dice di cosa parla il vostro archivio. Vi dice come si dispone, e chi legge decide se quella disposizione ha un nome.

Vale la pena tenerlo a mente perché è il punto in cui questi strumenti vengono fraintesi più spesso. Il risultato di BERTopic non è una risposta: è un’ipotesi di ordine, prodotta senza che nessuno abbia letto niente, che riduce quarantamila fascicoli a quaranta pile da guardare. L’archivista serve ancora — ma adesso può cominciare da venerdì mattina invece che da agosto.

I commenti sono riservati agli iscritti.

Accedi per commentare