Quali sono i principali argomenti di un testo o di un insieme di testi?
![](https://static.wixstatic.com/media/9ca1d9_76fb612e86344477b4515e4c8781a6cc~mv2.jpg/v1/fill/w_980,h_551,al_c,q_85,usm_0.66_1.00_0.01,enc_auto/9ca1d9_76fb612e86344477b4515e4c8781a6cc~mv2.jpg)
A volte nel marketing, ma anche in discipline umanistiche come letteratura, storia, sociologia, è necessario analizzare un grande corpus formato da molti documenti. Una delle cose che si vogliono indagare è: "di che cosa parlano questi testi?" e un'altra è "possiamo classificare questi testi in base al loro argomento prevalente?".
Un esempio di applicazione nel marketing potrebbe essere:
Dato un insieme di conversazioni riguardanti un certo brand, quali sono gli argomenti prevalenti? Quanto pesano sul totale delle conversazioni?
Approccio n.1: LDA model
Per capirlo si possono usare varie tecniche. La più comune di queste è la Latent Dirichlet Allocation, introdotta da Blei, Ng e Jordan nel 2002. Questa tecnica ha sicuramente il pregio di essere piuttosto versatile: non tiene conto della lingua e delle sue strutture, ma si basa sul modello bag-of-words. In parole semplici: viene creata una matrice avente per colonna tutte le singole parole dei testi e per righe i singoli documenti. I valori al suo interno corrispondono al numero delle occorrenze della parola in colonna nel documento in riga.
Più semplice a vedersi che a dirsi:
![Bag-of-word](https://static.wixstatic.com/media/9ca1d9_f54baa0cccfd4fadb91e1b154bd99e07~mv2.png/v1/fill/w_702,h_168,al_c,q_85,enc_auto/9ca1d9_f54baa0cccfd4fadb91e1b154bd99e07~mv2.png)
Come utilizzarla? A questo link trovate una spiegazione molto esaustiva su come applicare questa tecnica con python.
I problemi
La rimozione delle parole non portatrici di significato è fatta in modo piuttosto grezzo: semplicemente prima di inserire i dati nella matrice di riferimento vengono eliminati quelli più frequenti di un certo valore ai quali si aggiunge una lista di cosiddette "stopwords" - congiunzioni, articoli, preposizioni, ... - delineata a priori. Se questa pulizia non è fatta bene i topic individuati sono di difficile interpretazione
il modello si basa su un numero di topic pre-inserito. Nel caso di un corpus poco variegato, in cui cioè si può rintracciare un numero limitato di topic, LDA funziona piuttosto bene. Ma che succede se vogliamo analizzare corpora più ampi? Penso ad un insieme di lettere di una corrispondenza o magari i contenuti pubblicati su un canale social di un gruppo di persone. Occorrerà individuare (e identificare) 20, 30, 40 topic diversi.
Il modello forza ogni documento ad un topic. O meglio, attribuisce ad ogni documento una probabilità che appartenga ad un certo topic il cui totale dà 100. Ci sarà sempre un topic con una probabilità maggiore di altri, ma non è detto che in quel documento si parli di alcuno dei topic individuati.
Come ovviare a questi problemi? Usando il meglio dell'intelligenza artificiale di Google: BERT.
Approccio n.2: BERT unito a c-TF-IDF
Scrivo questo articolo perché mi sembra ci sia un vuoto tra i blog italiani su come fare topic modelling usando BERT.
Questo articolo è ripreso in gran parte da questo articolo di Maarten Grootendorst , l'ideatore di BERTopic, l'algoritmo che ora vado a generalizzare e a "tradurre" in italiano.
![BERT topic modeling](https://static.wixstatic.com/media/9ca1d9_8751eacecbbc4939af692f66a84fd957~mv2.png/v1/fill/w_980,h_548,al_c,q_90,usm_0.66_1.00_0.01,enc_auto/9ca1d9_8751eacecbbc4939af692f66a84fd957~mv2.png)
1. DATASET + LIBRARIES
Il dataset di partenza che ho utilizzato può essere trovato qui
Si tratta di un database con più di 12mila canzoni etichettate come "indie" e "non indie" pubblicate tra il 2000 e il 2020, comprensivo dei testi e di alcune metriche elaborate da spotify. Importante notare che i testi sono tutti in minuscolo e sono privi di punteggiatura.
Ora iniziamo importanto le libraries
import pandas as pd
from sentence_transformers import SentenceTransformer
#NB: Ti potrebbe servire avere pyTorch installato.
import umap #necessario per dimensionality reduction
import hdbscan #necessario per clustering
import matplotlib.pyplot as plt
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer
A questo punto possiamo caricare il nostro dataset
path = 'C:/Users/User/Downloads/'
filename = 'INDIE DATABASE.csv'
data_raw = pd.read_csv(path+filename)
#seleziono solo la parte del database che mi serve, ovvero quello con le canzoni etichettate come indie
data_indie = data_raw[data_raw['label'] == 'indie']
#creo una lista con tutti i testi
data = list(data_indie['TESTO'])
2. Creazione degli embeddings
Il primo step è la conversione dei documenti in dati numerici. Per fare questo usaimo BERT in quanto è in grado di estrarre da un testo dei raggruppamenti (embeddings) basati sul contesto della parola. La cosa fantastica è che ci sono già molti modelli pre-allenati!
Per generare gli embedding utilizzeremo la library sentence_trasformers. Gli embedding che genera infatti sono di buona qualità e funzionano abbastanza bene a livello di singolo documento.
Ora è il momento di lanciare il codice sul nostro database
model = SentenceTransformer('distilbert-multilingual-nli-stsb-quora-ranking')
embeddings = model.encode(data, show_progress_bar=True)
Nota: stiamo utilizzando Distilbert, un modello pre allenato multilingue che dovrebbe garantire un buon trade-off tra velocità e performance. Ci sono diversi modelli alterativi tra cui scegliere.
A questo punto i nostri documenti iniziali sono stati trasformati in vettori a 768 dimensioni!
NOTA: con documenti molto grandi potrebbero esserci errori. Prova a suddividerli in singoli paragrafi!
3. CLUSTERING
A questo punto vogliamo che i documenti con gli stessi argomenti siano clusterizzati insieme in modo da riuscire a trovare i topic all'interno di questi cluster.
Prima di fare questo però è necessario diminuire la dimensionalità degli embeddings in quanto molti algoritmi di clustering sono in difficoltà nel gestire una ampia dimensionalità.
Noi utilizzeremo HDBSCAN, ma nel caso si utilizzasse - ad esempio - una k-mean clustering basata sulla cosine-distance, si può tranquillamente saltare questa parte.
PARTE I. UMAP
In questa sezione riduciamo la dimensionalità degli embeddings dei nostri documenti. Lo facciamo con UMAP in quanto a differenza di altri algoritmi riesce a mantenere una buona porione della struttura dimensionale originale locale anche in dimensionalità minori.
Nel nostro caso ridurremo la dimensionalità a 5 e manterremo la dimensione del local neighborhood a 15. Per ottimizzare la creazione di topic si può giocare con questi parametri.
(attenzione: una dimensionalità troppo bassa porta a una perdita di informazioni, mentre una troppo alta porta a scarsi risultati di clustering).
umap_embeddings = umap.UMAP(n_neighbors=15,
n_components=5,
metric='cosine').fit_transform(embeddings)
PARTE II: HDBSCAN
Dopo aver ridotto la dimensionalità degli embeddings a 5 procediamo con la clusterizzazione dei documenti con HDBSCAN. HDBSCAN è un algoritmo density-based che lavora piuttosto bene in accoppiata con UMAP. L'altro vantaggio è che non forza i punti data in uno dei cluster identificati ma, piuttosto, li considera come outliers.
cluster = hdbscan.HDBSCAN(min_cluster_size=15,
metric='euclidean',
cluster_selection_method='eom').fit(umap_embeddings)
Fantastico! ora è il momento di visualizzare i risultati
4. DATA VISUALIZATION
Utilizzeremo la libreria matplotlib:
# Prepare data
umap_data = umap.UMAP(n_neighbors=15, n_components=2, min_dist=0.0, metric='cosine').fit_transform(embeddings)
result = pd.DataFrame(umap_data, columns=['x', 'y'])
result['labels'] = cluster.labels_
# Visualize clusters
fig, ax = plt.subplots(figsize=(20, 10))
outliers = result.loc[result.labels == -1, :]
clustered = result.loc[result.labels != -1, :]
plt.scatter(outliers.x, outliers.y, color='#BDBDBD', s=0.05)
plt.scatter(clustered.x, clustered.y, c=clustered.labels, s=0.05, cmap='hsv_r')
plt.colorbar()
![](https://static.wixstatic.com/media/9ca1d9_bc3153dac49e49dbb616518dda948e2b~mv2.png/v1/fill/w_980,h_548,al_c,q_90,usm_0.66_1.00_0.01,enc_auto/9ca1d9_bc3153dac49e49dbb616518dda948e2b~mv2.png)
Risulta difficile vedere i topic generati in quanto sono più di 40 e - come si può vedere dall'alto numero di grigi - molti di questi non sono stati inseriti in un cluster specifico.
5. CREARE I TOPIC
Il primo passo per leggere i topic è la creazione di un dataframe che raccolga documenti topic
#create df
docs_df = pd.DataFrame(data, columns=["Doc"])
docs_df['Topic'] = cluster.labels_
docs_df['Doc_ID'] = range(len(docs_df))
A questo abbiamo un dataframe che per ogni documento ci fornisce il topic ad esso attribuito.
Ma quali sono questi topic?
Per capirlo proveremo a fare questo: applicheremo la TF-IDF non su un singolo documento, ma su tutti i documenti aventi in comune un certo topic! Questa variante, chiamata c-TF-IDF (dove 'c-' sta per class-based) si poggia su una intuizione brillante.
Nella TF-IDF tradizionale in sostanza si confronta l'importanza delle singole parole tra diversi documenti. La c-TF-IDF invece... tratta l'insieme di documenti aventi in comune un topic come... un documento unico! Avremo così un unico grande documento per ogni categoria.
Lo score della TF-IDF così ricavato rifletterà le parole più importanti dell'intero topic.
Ecco come fare:
#uniamo i documenti aventi lo stesso topic
docs_per_topic = docs_df.groupby(['Topic'], as_index = False).agg({'Doc': ' '.join})
#definiamo la funzione c_tf_idf:
def c_tf_idf(documents, m, ngram_range=(1, 1)):
count = CountVectorizer(ngram_range=ngram_range, stop_words="english").fit(documents)
t = count.transform(documents).toarray()
w = t.sum(axis=1)
tf = np.divide(t.T, w)
sum_t = t.sum(axis=0)
idf = np.log(np.divide(m, sum_t)).reshape(-1, 1)
tf_idf = np.multiply(tf, idf)
return tf_idf, count
#procediamo!
tf_idf, count = c_tf_idf(docs_per_topic.Doc.values, m=len(data))
Ora abbiamo il punteggio di importanza di ogni singola parola in un cluster. L'idea è che se stampiamo le prime 10 o 15 o 20 parole più importanti di ogni cluster, dovremmo riuscire a interpretarne il significato.
Procediamo.
6. INTERPRETARE I TOPIC
Con queste linee di codice prendiamo le top 20 parole per ogni topic basate sul loro c-TF-IDF score.
def extract_top_n_words_per_topic(tf_idf, count, docs_per_topic, n=20):
words = count.get_feature_names()
labels = list(docs_per_topic.Topic)
tf_idf_transposed = tf_idf.T
indices = tf_idf_transposed.argsort()[:, -n:]
top_n_words = {label: [(words[j], tf_idf_transposed[i][j]) for j in indices[i]][::-1] for i, label in enumerate(labels)}
return top_n_words
def extract_topic_sizes(df):
topic_sizes = (df.groupby(['Topic'])
.Doc
.count()
.reset_index()
.rename({"Topic": "Topic", "Doc": "Size"}, axis='columns')
.sort_values("Size", ascending=False))
return topic_sizes
top_n_words = extract_top_n_words_per_topic(tf_idf, count, docs_per_topic, n=20)
topic_sizes = extract_topic_sizes(docs_df); topic_sizes.head(15)
Con topic_sizes possiamo vedere quanto i vari topic sono frequenti:
![](https://static.wixstatic.com/media/9ca1d9_af048353a19347149f2b23c7893fc6c2~mv2.png/v1/fill/w_202,h_455,al_c,q_85,enc_auto/9ca1d9_af048353a19347149f2b23c7893fc6c2~mv2.png)
<-- ad esempio vediamo che il topic più frequente è il "-1". In realtà i valori etichettati con -1 sono documenti a cui non è stato assegnato alcun topic!!
E' un bene? Un male? Dipende dalla vostra intenzione di ricerca.
Intanto possiamo osservare che i topic più frequenti sono: 18, 8, 34, 41, 4.
Esplorare il contenuto di questi topic è molto semplice. Basta lanciare il comando
top_n_words[4][:15]
per vedere le 15 parole più importanti del topic 4. Eccole:
![](https://static.wixstatic.com/media/9ca1d9_ffb3ecad79a54eefae4644a5561c657b~mv2.png/v1/fill/w_390,h_267,al_c,q_85,enc_auto/9ca1d9_ffb3ecad79a54eefae4644a5561c657b~mv2.png)
<-- abbastanza chiaro, no?
Buon divertimento!
NOTA: Qui trovi tutti i codici:
Comments