La Latent Dirichlet Allocation (LDA)
est un modèle probabiliste génératif qui permet
de décrire des collections de documents de texte ou d’autres types de données discrètes.
La LDA fait
partie d’une catégorie de modèles appelés “topic models”, qui cherchent à découvrir des structures
thématiques cachées dans des vastes archives de documents. Le principe est de décomposer un
document comme une collection de thèmes qui se distinguent par des choix de mots différents.
Le but va être dans un premier temps de regarder dans le détail les termes les plus fréquents utilisés par les auteurs, et les représenter graphiquement.
La LDA est une technique d’estimation bayésienne.
Le cours d’Alberto Brietti
sur le sujet constitue une très bonne ressource pour comprendre
les fondements de cette technique.
Librairies nécessaires
Cette page évoquera les principales librairies pour faire du NLP, notamment :
La liste des modules à importer est assez longue, la voici :
import nltknltk.download('stopwords')nltk.download('punkt')nltk.download('genesis')nltk.download('wordnet')nltk.download('omw-1.4')import numpy as np # linear algebraimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)import matplotlib.pyplot as pltfrom wordcloud import WordCloud#from IPython.display import displayimport base64import stringimport reimport nltkfrom collections import Counterfrom time import time# from sklearn.feature_extraction.stop_words import ENGLISH_STOP_WORDS as stopwordsfrom sklearn.metrics import log_lossimport matplotlib.pyplot as pltfrom nltk.stem import WordNetLemmatizerfrom sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizerfrom sklearn.decomposition import NMF, LatentDirichletAllocation
[nltk_data] Downloading package stopwords to /github/home/nltk_data...
[nltk_data] Package stopwords is already up-to-date!
[nltk_data] Downloading package punkt to /github/home/nltk_data...
[nltk_data] Package punkt is already up-to-date!
[nltk_data] Downloading package genesis to /github/home/nltk_data...
[nltk_data] Package genesis is already up-to-date!
[nltk_data] Downloading package wordnet to /github/home/nltk_data...
[nltk_data] Package wordnet is already up-to-date!
[nltk_data] Downloading package omw-1.4 to /github/home/nltk_data...
[nltk_data] Package omw-1.4 is already up-to-date!
Données utilisées
Si vous avez déjà lu la section précédente et importé les données, vous
pouvez passer à la section suivante
Le code suivant permet d’importer le jeu de données spooky:
import pandas as pdurl='https://github.com/GU4243-ADS/spring2018-project1-ginnyqg/raw/master/data/spooky.csv'import pandas as pdtrain = pd.read_csv(url, encoding='latin-1')train.columns = train.columns.str.capitalize()train['ID'] = train['Id'].str.replace("id","")train = train.set_index('Id')
Le jeu de données met ainsi en regard un auteur avec une phrase qu’il a écrite:
train.head()
Text
Author
ID
Id
id26305
This process, however, afforded me no means of...
EAP
26305
id17569
It never once occurred to me that the fumbling...
HPL
17569
id11008
In his left hand was a gold snuff box, from wh...
EAP
11008
id27763
How lovely is spring As we looked from Windsor...
MWS
27763
id12958
Finding nothing else, not even gold, the Super...
HPL
12958
Les étapes de preprocessing sont expliquées dans le chapitre précédent. On applique les étapes suivantes :
Tokeniser
Retirer la ponctuation et les stopwords
Lemmatiser le texte
lemma = WordNetLemmatizer()train_clean = (train .groupby(["ID","Author"]) .apply(lambda s: nltk.word_tokenize(' '.join(s['Text']))) .apply(lambda words: [word for word in words if word.isalpha()]))from nltk.corpus import stopwords stop_words =set(stopwords.words('english'))train_clean = (train_clean .apply(lambda words: [lemma.lemmatize(w) for w in words ifnot w in stop_words]) .reset_index(name='tokenized'))train_clean.head(2)
ID
Author
tokenized
0
00001
MWS
[Idris, well, content, resolve, mine]
1
00002
HPL
[I, faint, even, fainter, hateful, modernity, ...
Principe de la LDA (Latent Dirichlet Allocation)
Le modèle Latent Dirichlet Allocation (LDA) est un modèle probabiliste génératif qui permet
de décrire des collections de documents de texte ou d’autres types de données discrètes. LDA fait
partie d’une catégorie de modèles appelés “topic models”, qui cherchent à découvrir des structures
thématiques cachées dans des vastes archives de documents.
Ceci permet d’obtenir des méthodes
efficaces pour le traitement et l’organisation des documents de ces archives : organisation automatique
des documents par sujet, recherche, compréhension et analyse du texte, ou même résumer des
textes.
Aujourd’hui, ce genre de méthodes s’utilisent fréquemment dans le web, par exemple pour
analyser des ensemble d’articles d’actualité, les regrouper par sujet, faire de la recommandation
d’articles, etc.
La LDA est une méthode qui considère les corpus comme des mélanges de sujets et
de mots. Chaque document peut être représenté comme le résultat d’un mélange :
de sujets
et, au sein de ces sujets, d’un choix de mots.
L’estimation des
paramètres de la LDA passe par l’estimation des distributions des variables
latentes à partir des données observées (posterior inference).
Mathématiquement, on peut se représenter la LDA comme une
technique de maximisation de log vraisemblance avec un algorithme EM (expectation maximisation)
dans un modèle de mélange.
La matrice termes-documents qui sert de point de départ est la suivante :
word_1
word_2
word_3
…
word_J
doc_1
3
0
1
…
0
…
…
…
…
…
…
doc_N
1
0
0
…
5
On dit que cette matrice est sparse (creuse en Français) car elle contient principalement des 0. En effet, un document n’utilise qu’une partie mineure du vocabulaire complet.
La LDA consiste à transformer cette matrice sparsedocument-terme en deux matrices de moindre dimension:
Une matrice document-sujet
Une matrice sujet-mots
En notant \(K_i\) le sujet \(i\). On obtient donc
Une matrice document-sujet ayant la structure suivante :
K_1
K_2
K_3
…
K_M
doc_1
1
0
1
…
0
…
…
…
…
…
…
doc_N
1
1
1
…
0
Une matrice sujets-mots ayant la structure suivante :
word_1
word_2
word_3
…
word_J
K_1
1
0
0
…
0
…
…
…
…
…
…
K_M
1
1
1
…
0
Ces deux matrices ont l’interprétation suivante :
La première nous renseigne sur la présence d’un sujet dans un document
La seconde nous renseigne sur la présence d’un mot dans un sujet
En fait, le principe de la LDA est de construire ces deux matrices à partir des fréquences d’apparition des mots dans le texte.
On va se concentrer sur Edgar Allan Poe.
corpus = train_clean[train_clean["Author"] =="EAP"]
Entraîner une LDA
Il existe plusieurs manières d’entraîner une LDA.
Nous allons utiliser Scikit ici avec la méthode LatentDirichletAllocation.
Comme expliqué dans la partie modélisation :
On initialise le modèle ;
On le met à jour avec la méthode fit.
from sklearn.feature_extraction.text import CountVectorizerfrom sklearn.decomposition import LatentDirichletAllocation# Initialise the count vectorizer with the English stop wordscount_vectorizer = CountVectorizer(stop_words='english')# Fit and transform the processed titlescount_data = count_vectorizer.fit_transform(corpus['tokenized'].apply(lambda s: ' '.join(s)))# Tweak the two parameters belownumber_topics =5number_words =10# Create and fit the LDA modellda = LatentDirichletAllocation(n_components=11, max_iter=5, learning_method ='online', learning_offset =50., random_state =0, n_jobs =1)lda.fit(count_data)
Visualiser les résultats
On peut déjà commencer par utiliser une fonction pour afficher les
résultats :
# Helper functiondef print_topics(model, count_vectorizer, n_top_words): words = count_vectorizer.get_feature_names_out()for topic_idx, topic inenumerate(model.components_):print("\nTopic #%d:"% topic_idx)print(" ".join([words[i]for i in topic.argsort()[:-n_top_words -1:-1]]))print_topics(lda, count_vectorizer, number_words)
Topic #0:
arm looking thousand respect hour table woman rest ah seen
Topic #1:
said dupin ha end write smith chair phenomenon quite john
Topic #2:
time thing say body matter course day place object immediately
Topic #3:
mere memory felt sat movement case sole green principle bone
Topic #4:
door room open small friend lady replied night window hand
Topic #5:
word man day idea good point house shall mind say
Topic #6:
eye figure form left sea hour ordinary life deep world
Topic #7:
foot great little earth let le year nature come nearly
Topic #8:
hand strange head color hair spoken read ear ghastly neck
Topic #9:
came looked shadow low dream like death light spirit tree
Topic #10:
eye know heart saw character far tell oh voice wall
La représentation sous forme de liste de mots n’est pas la plus pratique…
On peut essayer de se représenter un wordcloud de chaque sujet pour mieux voir si cette piste est pertinente :
tf_feature_names = count_vectorizer.get_feature_names_out()def wordcloud_lda(lda, tf_feature_names): fig, axs = plt.subplots(len(lda.components_) //3+1, 3)for i inrange(len(lda.components_)): corpus_lda = lda.components_[i] first_topic_words = [tf_feature_names[l] for l in corpus_lda.argsort()[:-50-1:-1]] k = i //3 j = (i - k*3) wordcloud = WordCloud(stopwords=stop_words, background_color="black",width =2500, height =1800) wordcloud = wordcloud.generate(" ".join(first_topic_words)) axs[k][j].set_title("Wordcloud pour le \nsujet {}".format(i)) axs[k][j].axis('off') axs[k][j].imshow(wordcloud) r =len(lda.components_) %3 [fig.delaxes(axs[len(lda.components_) //3,k-1]) for k inrange(r+1, 3+1) if r !=0]wc = wordcloud_lda(lda, tf_feature_names)wc
wc
Le module pyLDAvis offre quelques visualisations bien pratiques lorsqu’on
désire représenter de manière synthétique les résultats d’une LDA et observer la distribution sujet x mots.
Hint
Dans un notebook faire :
import pyLDAvis.sklearnpyLDAvis.enable_notebook()
Pour les utilisateurs de Windows, il est nécessaire d’ajouter l’argument
n_jobs = 1. Sinon, Python tente d’entraîner le modèle avec de la
parallélisation. Le problème est que les processus sont des FORKs, ce que
Windows ne supporte pas. Sur un système Unix (Linux, Mac OS), on peut se passer de cet
argument.
#!pip install pyLDAvis #à faire en haut du notebook sur colabimport pyLDAvisimport pyLDAvis.sklearn# pyLDAvis.enable_notebook()vis_data = pyLDAvis.sklearn.prepare(lda, count_data, count_vectorizer, n_jobs =1)pyLDAvis.display(vis_data)
Chaque bulle représente un sujet. Plus la bulle est grande, plus il y a de documents qui traitent de ce sujet.
Plus les barres sont loin les unes des autres, plus elles sont différentes. Un bon modèle aura donc tendance à avoir de grandes bulles qui ne se recoupent pas. Ce n’est pas vraiment le cas ici…
Les barres bleues représentent la fréquence de chaque mot dans le corpus.
Les barres rouges représentent une estimation du nombre de termes générés dans un sujet précis. La barre rouge la plus longue correspond au mot le plus utilisé dans ce sujet.
@book{galiana2023,
author = {Galiana, Lino},
title = {Python Pour La Data Science},
date = {2023},
url = {https://pythonds.linogaliana.fr/},
doi = {10.5281/zenodo.8229676},
langid = {en}
}