Latent Dirichlet Allocation (LDA)

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.

Tutoriel
NLP
Author

Lino Galiana

Published

2024-04-27

Cette page approfondit les exercices présentés dans la section précédente. On va ainsi continuer notre exploration de la littérature anglophones :

Les données sont disponibles dans la base de données spooky.csv et peuvent être importées par Python en utilisant directement l’url https://github.com/GU4243-ADS/spring2018-project1-ginnyqg/raw/master/data/spooky.csv.

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.

Ce notebook est librement inspiré de :

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.

1 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 nltk

nltk.download("stopwords")
nltk.download("punkt")
nltk.download("genesis")
nltk.download("wordnet")
nltk.download("omw-1.4")

import numpy as np  # linear algebra
import pandas as pd  # data processing, CSV file I/O (e.g. pd.read_csv)
import matplotlib.pyplot as plt
from wordcloud import WordCloud

# from IPython.display import display
import base64
import string
import re
import nltk

from collections import Counter
from time import time

# from sklearn.feature_extraction.stop_words import ENGLISH_STOP_WORDS as stopwords
from sklearn.metrics import log_loss
import matplotlib.pyplot as plt

from nltk.stem import WordNetLemmatizer
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from 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!

2 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 pd

url = "https://github.com/GU4243-ADS/spring2018-project1-ginnyqg/raw/master/data/spooky.csv"
import pandas as pd

train = 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 :

  1. Tokeniser
  2. Retirer la ponctuation et les stopwords
  3. 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 if not w in stop_words]
).reset_index(name="tokenized")

train_clean.head(2)
/tmp/ipykernel_5477/2725603950.py:5: DeprecationWarning:

DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
ID Author tokenized
0 00001 MWS [Idris, well, content, resolve, mine]
1 00002 HPL [I, faint, even, fainter, hateful, modernity, ...

3 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 sparse document-terme en deux matrices de moindre dimension :

  1. Une matrice document-sujet
  2. 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"]

4 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 :

  1. On initialise le modèle ;
  2. On le met à jour avec la méthode fit.
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation

# Initialise the count vectorizer with the English stop words
count_vectorizer = CountVectorizer(
    stop_words="english"
)  # Fit and transform the processed titles
count_data = count_vectorizer.fit_transform(
    corpus["tokenized"].apply(lambda s: " ".join(s))
)

# Tweak the two parameters below
number_topics = 5
number_words = 10  # Create and fit the LDA model
lda = LatentDirichletAllocation(
    n_components=11,
    max_iter=5,
    learning_method="online",
    learning_offset=50.0,
    random_state=0,
    n_jobs=1,
)
lda.fit(count_data)

5 Visualiser les résultats

On peut déjà commencer par utiliser une fonction pour afficher les résultats :

# Helper function
def print_topics(model, count_vectorizer, n_top_words):
    words = count_vectorizer.get_feature_names_out()
    for topic_idx, topic in enumerate(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 in range(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 in range(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.

#!pip install pyLDAvis #à faire en haut du notebook sur colab
import pyLDAvis
import 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.

6 Références

Informations additionnelles

environment files have been tested on.

Latest built version: 2024-04-27

Python version used:

'3.11.6 | packaged by conda-forge | (main, Oct  3 2023, 10:40:35) [GCC 12.3.0]'
Package Version
affine 2.4.0
aiobotocore 2.12.2
aiohttp 3.9.3
aioitertools 0.11.0
aiosignal 1.3.1
alembic 1.13.1
aniso8601 9.0.1
annotated-types 0.6.0
appdirs 1.4.4
archspec 0.2.3
astroid 3.1.0
asttokens 2.4.1
attrs 23.2.0
Babel 2.14.0
bcrypt 4.1.2
beautifulsoup4 4.12.3
black 24.4.2
blinker 1.7.0
blis 0.7.11
bokeh 3.4.0
boltons 23.1.1
boto3 1.34.51
botocore 1.34.51
branca 0.7.1
Brotli 1.1.0
cachetools 5.3.3
cartiflette 0.0.2
Cartopy 0.23.0
catalogue 2.0.10
cattrs 23.2.3
certifi 2024.2.2
cffi 1.16.0
charset-normalizer 3.3.2
chromedriver-autoinstaller 0.6.4
click 8.1.7
click-plugins 1.1.1
cligj 0.7.2
cloudpathlib 0.16.0
cloudpickle 3.0.0
colorama 0.4.6
comm 0.2.2
commonmark 0.9.1
conda 24.3.0
conda-libmamba-solver 24.1.0
conda-package-handling 2.2.0
conda_package_streaming 0.9.0
confection 0.1.4
contextily 1.6.0
contourpy 1.2.1
cryptography 42.0.5
cycler 0.12.1
cymem 2.0.8
cytoolz 0.12.3
dask 2024.4.1
dask-expr 1.0.10
debugpy 1.8.1
decorator 5.1.1
dill 0.3.8
distributed 2024.4.1
distro 1.9.0
docker 7.0.0
duckdb 0.10.1
en-core-web-sm 3.7.1
entrypoints 0.4
et-xmlfile 1.1.0
exceptiongroup 1.2.0
executing 2.0.1
fastjsonschema 2.19.1
fiona 1.9.6
flake8 7.0.0
Flask 3.0.2
folium 0.16.0
fontawesomefree 6.5.1
fonttools 4.51.0
fr-core-news-sm 3.7.0
frozenlist 1.4.1
fsspec 2023.12.2
GDAL 3.8.4
gensim 4.3.2
geographiclib 2.0
geopandas 0.12.2
geoplot 0.5.1
geopy 2.4.1
gitdb 4.0.11
GitPython 3.1.43
google-auth 2.29.0
graphene 3.3
graphql-core 3.2.3
graphql-relay 3.2.0
graphviz 0.20.3
great-tables 0.5.0
greenlet 3.0.3
gunicorn 21.2.0
h11 0.14.0
htmltools 0.5.1
hvac 2.1.0
idna 3.6
imageio 2.34.1
importlib_metadata 7.1.0
importlib_resources 6.4.0
inflate64 1.0.0
ipykernel 6.29.3
ipython 8.22.2
ipywidgets 8.1.2
isort 5.13.2
itsdangerous 2.1.2
jedi 0.19.1
Jinja2 3.1.3
jmespath 1.0.1
joblib 1.3.2
jsonpatch 1.33
jsonpointer 2.4
jsonschema 4.21.1
jsonschema-specifications 2023.12.1
jupyter-cache 1.0.0
jupyter_client 8.6.1
jupyter_core 5.7.2
jupyterlab_widgets 3.0.10
kaleido 0.2.1
kiwisolver 1.4.5
kubernetes 29.0.0
langcodes 3.4.0
language_data 1.2.0
lazy_loader 0.4
libmambapy 1.5.7
llvmlite 0.42.0
locket 1.0.0
lxml 5.2.1
lz4 4.3.3
Mako 1.3.2
mamba 1.5.7
mapclassify 2.6.1
marisa-trie 1.1.0
Markdown 3.6
MarkupSafe 2.1.5
matplotlib 3.8.3
matplotlib-inline 0.1.6
mccabe 0.7.0
menuinst 2.0.2
mercantile 1.2.1
mizani 0.11.2
mlflow 2.11.3
mlflow-skinny 2.11.3
msgpack 1.0.7
multidict 6.0.5
multivolumefile 0.2.3
munkres 1.1.4
murmurhash 1.0.10
mypy 1.9.0
mypy-extensions 1.0.0
nbclient 0.10.0
nbformat 5.10.4
nest_asyncio 1.6.0
networkx 3.3
nltk 3.8.1
numba 0.59.1
numpy 1.26.4
oauthlib 3.2.2
opencv-python-headless 4.9.0.80
openpyxl 3.1.2
outcome 1.3.0.post0
OWSLib 0.28.1
packaging 23.2
pandas 2.2.1
paramiko 3.4.0
parso 0.8.4
partd 1.4.1
pathspec 0.12.1
patsy 0.5.6
Pebble 5.0.7
pexpect 4.9.0
pickleshare 0.7.5
pillow 10.3.0
pip 24.0
pkgutil_resolve_name 1.3.10
platformdirs 4.2.0
plotly 5.19.0
plotnine 0.13.5
pluggy 1.4.0
polars 0.20.18
preshed 3.0.9
prometheus_client 0.20.0
prometheus-flask-exporter 0.23.0
prompt-toolkit 3.0.42
protobuf 4.25.3
psutil 5.9.8
ptyprocess 0.7.0
pure-eval 0.2.2
py7zr 0.20.8
pyarrow 15.0.0
pyarrow-hotfix 0.6
pyasn1 0.5.1
pyasn1-modules 0.3.0
pybcj 1.0.2
pycodestyle 2.11.1
pycosat 0.6.6
pycparser 2.21
pycryptodomex 3.20.0
pydantic 2.7.1
pydantic_core 2.18.2
pyflakes 3.2.0
Pygments 2.17.2
PyJWT 2.8.0
pylint 3.1.0
PyNaCl 1.5.0
pynsee 0.1.7
pyOpenSSL 24.0.0
pyparsing 3.1.2
pyppmd 1.1.0
pyproj 3.6.1
pyshp 2.3.1
PySocks 1.7.1
python-dateutil 2.9.0
python-dotenv 1.0.1
python-magic 0.4.27
pytz 2024.1
pyu2f 0.1.5
pywaffle 1.1.0
PyYAML 6.0.1
pyzmq 25.1.2
pyzstd 0.15.10
QtPy 2.4.1
querystring-parser 1.2.4
rasterio 1.3.10
referencing 0.34.0
regex 2023.12.25
requests 2.31.0
requests-cache 1.2.0
requests-oauthlib 2.0.0
rpds-py 0.18.0
rsa 4.9
Rtree 1.2.0
ruamel.yaml 0.18.6
ruamel.yaml.clib 0.2.8
s3fs 2023.12.2
s3transfer 0.10.1
scikit-image 0.23.2
scikit-learn 1.4.1.post1
scipy 1.13.0
seaborn 0.13.2
selenium 4.20.0
setuptools 69.2.0
shapely 2.0.3
six 1.16.0
smart-open 6.4.0
smmap 5.0.0
sniffio 1.3.1
snuggs 1.4.7
sortedcontainers 2.4.0
soupsieve 2.5
spacy 3.7.4
spacy-legacy 3.0.12
spacy-loggers 1.0.5
SQLAlchemy 2.0.29
sqlparse 0.4.4
srsly 2.4.8
stack-data 0.6.2
statsmodels 0.14.1
tabulate 0.9.0
tblib 3.0.0
tenacity 8.2.3
texttable 1.7.0
thinc 8.2.3
threadpoolctl 3.4.0
tifffile 2024.4.24
tomli 2.0.1
tomlkit 0.12.4
toolz 0.12.1
topojson 1.8
tornado 6.4
tqdm 4.66.2
traitlets 5.14.2
trio 0.25.0
trio-websocket 0.11.1
truststore 0.8.0
typer 0.9.4
typing_extensions 4.11.0
tzdata 2024.1
Unidecode 1.3.8
url-normalize 1.4.3
urllib3 1.26.18
wasabi 1.1.2
wcwidth 0.2.13
weasel 0.3.4
webcolors 1.13
webdriver-manager 4.0.1
websocket-client 1.7.0
Werkzeug 3.0.2
wheel 0.43.0
widgetsnbextension 4.0.10
wordcloud 1.9.3
wrapt 1.16.0
wsproto 1.2.0
xgboost 2.0.3
xlrd 2.0.1
xyzservices 2024.4.0
yarl 1.9.4
yellowbrick 1.5
zict 3.0.0
zipp 3.17.0
zstandard 0.22.0

View file history

SHA Date Author Description
06d003a 2024-04-23 10:09:22 Lino Galiana Continue la restructuration des sous-parties (#492)
005d89b 2023-12-20 17:23:04 Lino Galiana Finalise l’affichage des statistiques Git (#478)
3fba612 2023-12-17 18:16:42 Lino Galiana Remove some badges from python (#476)
4cd44f3 2023-12-11 17:37:50 Antoine Palazzolo Relecture NLP (#474)
889a71b 2023-11-10 11:40:51 Antoine Palazzolo Modification TP 3 (#443)
a771183 2023-10-09 11:27:45 Antoine Palazzolo Relecture TD2 par Antoine (#418)
154f09e 2023-09-26 14:59:11 Antoine Palazzolo Des typos corrigées par Antoine (#411)
3bdf3b0 2023-08-25 11:23:02 Lino Galiana Simplification de la structure 🤓 (#393)
78ea2cb 2023-07-20 20:27:31 Lino Galiana Change titles levels (#381)
d2a2773 2023-07-07 15:59:36 Lino Galiana Retour du wordcloud (#372)
29ff3f5 2023-07-07 14:17:53 linogaliana description everywhere
f21a24d 2023-07-02 10:58:15 Lino Galiana Pipeline Quarto & Pages 🚀 (#365)
934149d 2023-02-13 11:45:23 Lino Galiana get_feature_names is deprecated in scikit 1.0.X versions (#351)
f10815b 2022-08-25 16:00:03 Lino Galiana Notebooks should now look more beautiful (#260)
d201e3c 2022-08-03 15:50:34 Lino Galiana Pimp la homepage ✨ (#249)
bb38643 2022-06-08 16:59:40 Lino Galiana Répare bug leaflet (#234)
299cff3 2022-06-08 13:19:03 Lino Galiana Problème code JS suite (#233)
12965ba 2022-05-25 15:53:27 Lino Galiana :launch: Bascule vers quarto (#226)
9c71d6e 2022-03-08 10:34:26 Lino Galiana Plus d’éléments sur S3 (#218)
e94c1c5 2021-12-23 21:34:46 Lino Galiana Un tutoriel sur les pipelines :tada: (#203)
09b60a1 2021-12-21 19:58:58 Lino Galiana Relecture suite du NLP (#205)
495599d 2021-12-19 18:33:05 Lino Galiana Des éléments supplémentaires dans la partie NLP (#202)
2a8809f 2021-10-27 12:05:34 Lino Galiana Simplification des hooks pour gagner en flexibilité et clarté (#166)
2e4d586 2021-09-02 12:03:39 Lino Galiana Simplify badges generation (#130)
4cdb759 2021-05-12 10:37:23 Lino Galiana :sparkles: :star2: Nouveau thème hugo :snake: :fire: (#105)
7f9f97b 2021-04-30 21:44:04 Lino Galiana 🐳 + 🐍 New workflow (docker 🐳) and new dataset for modelization (2020 🇺🇸 elections) (#99)
f4e61ed 2020-12-10 15:00:12 Lino Galiana Ajout partie LDA (#88)
d164635 2020-12-08 16:22:00 Lino Galiana :books: Première partie NLP (#87)
Back to top

Citation

BibTeX citation:
@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}
}
For attribution, please cite this work as:
Galiana, Lino. 2023. Python Pour La Data Science. https://doi.org/10.5281/zenodo.8229676.