Nous parlerons ici du Topic Modeling, qui est une méthode d’analyse textuelle, nous aborderons un peu la théorie mais surtout la réalisation avec R. Si en soit la méthode n’est pas très compliquée en soit, elle suppose malgré tout de maitriser un minimum les bases langage R que nous aborderons pas ici.
Cette méthode est intéressante car permet de découvrir les spécificités des différents textes qui constituent un corpus et d’opérer toutes sortes d’analyses que nous développerons ci-dessous.
Le topic model fait référence à un modèle probabiliste qui permet de définir l’appartenance de documents à des topics, ou thèmes. L’idée de base repose sur l’axiome qu’un auteur va produire des textes, plus ou moins longs, avec une ou plusieurs thématiques mais qu’à chaque fois il choisira des mots précis pour parler de sa ou ses thématiques. Très simplement, un texte donné va aborder un ou plusieurs thèmes donnés avec un vocabulaire donné. Autrement dit vous avez plus de probabilité de retrouver les termes Alice, chat, lapin et chapelier dans un livre de Lewis Caroll (par exemple : Les aventures d’Alice au pays des merveilles) que dans un ouvrage sur le langage de programmation R (hypothèse à vérifier !). L’idée qui se cache derrière le topic modeling c’est finalement qu’un corpus c’est un regroupement de textes, qui sont eux-mêmes une collection de thématiques, et qu’une thématique est un ensemble de mots.
Pour calculer les modèles probabilistes, il existe de très nombreux algorithmes, le plus fréquemment utilisé est l’Allocation de Dirichlet Latente (LDA).
En fait il s’agit d’un “modèle probabiliste expliquant des observations à l’aide de groupes (non observés) de données similaires. Il s’agit d’un algorithme d’apprentissage automatique non supervisé, les classes de sortie n’étant pas étiquetées.” En français, cet algorithme identifie des thèmes mais n’est pas en mesure de vous dire à quoi ils correspondent en terme de sens. Il va par contre indiquer quels textes semblent appartenir à tel thème et quels mots y sont associés. C’est finalement un processus de catégorisation de nos textes.
Il sera nécessaire d’indiquer le nombre de thèmes puis l’algorithme ira partitionner les documents, et vous répondra de deux manières :
Pour ce faire l’algorithme :
La force de cette méthode c’est qu’elle accepte un chevauchement des topics d’un texte à l’autre. Ce qui permet une catégorisation plus souple et donc finalement plus fine.
Les ligne de code précédée d’un “#” sont des lignes de commentaires qui vise à vous aider à comprendre le code R. Il est inutile de les lancer dans la console de R. Par ailleurs si vous avez des interrogations sur certaines fonctions n’hésitez surtout pas à lire la doc accessible en tapant ?nom_fonction() dans R.
Avant tout on commence par charger les packages dont nous aurons besoin!
# Pour installer les packages utiliser la fonction install.package("le package à installer")
library(tidyverse)
library(rvest)
library(dplyr)
library(stringr)
library(tidytext)
library(tidyr)
library(knitr)
library(proustr)
library(topicmodels)
library(ldatuning)
library(broom)
library(wesanderson)
Dans le cadre de cette démonstration, nous allons constituer un corpus de textes très différents via un web scraping de 4 pages Wikipédia sur l’immense culture bretonne:
# Le code ci-dessous permet de réaliser le webscraping pour constituer notre base de données
# Le sujet n'étant pas le webscraping je ne commenterai pas le code ci-dessous
# Pour plus d'info vous pouvez lire la doc du package rvest : https://cran.r-project.org/web/packages/rvest/rvest.pdf
scrape_wiki <- function(url){
url <- read_html(url)
df <- tibble(text = url %>% html_nodes("p+ ul li , #mw-content-text p") %>% html_text(),
name = url %>% html_nodes("h1") %>% html_text())
return(df)
}
url_list <- c("https://fr.wikipedia.org/wiki/Galette-saucisse",
"https://fr.wikipedia.org/wiki/Pays_Bigouden",
"https://fr.wikipedia.org/wiki/Festival_des_Vieilles_Charrues",
"https://fr.wikipedia.org/wiki/Pouldreuzic")
data <- map_df(.x = url_list, .f = scrape_wiki)
Le corpus sera donc représenté sous la forme d’un data frame, avec comme première variable le texte et notre seconde variable est l’identifiant du texte (ici le titre).
Ici les premières lignes de notre corpus:
head(data)
## # A tibble: 6 x 2
## text name
## <chr> <chr>
## 1 "\n" Galette-sauc~
## 2 "La galette-saucisse[N 1] est un en-cas composé d'une sauc~ Galette-sauc~
## 3 "Créée et popularisée au cours du XIXe siècle, elle réunit~ Galette-sauc~
## 4 "La galette-saucisse devient, dès lors, un en-cas populair~ Galette-sauc~
## 5 "La galette-saucisse est composée :\n" Galette-sauc~
## 6 d'une galette de sarrasin, obtenue par la cuisson d'une pâ~ Galette-sauc~
# Pour visualiser complétement vos données dans R ou rstudio utilisez la commande View(data)
Par ailleurs, il est tout à fait possible de charger son propre jeu de donnée qui aurait été préalablement nettoyé. Par exemple un corpus des occurrences qui se compose des mots, de leur source et du nombre d’occurrences qui auraient été sorties d’Iramuteq.
Il est important de noter qu’une variable identifiant est indispensable pour différencier chaque texte, quel que soit le corpus.
Voici à quoi devrait ressembler ce jeu de données :
## name word nb
## 1 Pouldreuzic pouldreuzic 51
## 2 Pouldreuzic penhors 47
## 3 Pouldreuzic hénaff 22
## 4 Pouldreuzic lababan 16
## 5 Pouldreuzic audierne 16
## 6 Pouldreuzic pâté 14
Une fois que vous avez votre corpus, il va être nécessaire de le transformer en une matrice de mots, un dtm pour les initiés (DocumentTermMatrix). C’est également lors de cette étape dans ce tutoriel que nous allons procéder au nettoyage de notre corpus. Le nettoyage du corpus devra être faite avant le passage en dtm.
dtm <- data %>%
unnest_tokens(word, text) %>% #Pour récupérer chaque mot
count(name, word, sort = TRUE) %>% # pour compter le nombre des occurences
anti_join(proustr::proust_stopwords()) %>% # permet de supprimer les mots outils en français, si votre corpus avait été en anglais remplacé par tidytext::stop_words
# c'est cette dernière ligne qui permet la transformation en dtm
cast_dtm(name, term = word, value = n)
Pour l’alternative de mise en forme des données vue il y a quelques lignes que nous appelerons data_02 voici les lignes de commandes à utiliser pour la conversion en dtm :
# Il est inutile de lancer cette ligne de commande c'est à titre d'exemple
# D'autant que nous avez les données data_02. Moi je dis ça juste pour vous éviter un message d'erreur.
# Rassurez vous, vous en aurez plein !
dtm_02<- data_02 %>%
cast_dtm(name, term = word, value = nb)
Après avoir constitué notre jeu de donnée, l’avoir nettoyé et transformé en dtm, identifié le nombre “idéal” de topics, réjouissez-vous ! Nous allons enfin pour voir lancer notre topic model. Il existe de nombreux packages mais ici nous utiliserons la fonction LDA
du package {topicmodels}.
res_lda <- LDA(dtm, k = 4, method = "Gibbs", # k = correspond au nombre de topic
control = list(seed = 1149)) #permet de reproduire les mêmes résultats d'une session à l'autre, le topic 1 restera le topic numéro 1... Quand à 1988 c'est simplement un nombre qui fait office de clés. Personnellement j'utilise l'heure qu'il est quand j'écrit la fonction
Et voilà vous avez votre modèle!
On peut commencer très rapidement par observer quels sont les mots les plus fortement associé aux topics retenus.
terms(res_lda, 15)# ici les 15 premiers
## Topic 1 Topic 2 Topic 3 Topic 4
## [1,] "pouldreuzic" "festival" "galette" "bigouden"
## [2,] "penhors" "scène" "saucisse" "pays"
## [3,] "hénaff" "000" "d'une" "saint"
## [4,] "d'audierne" "the" "bretagne" "pont"
## [5,] "port" "charrues" "également" "siècle"
## [6,] "quimper" "jours" "2" "penmarc'h"
## [7,] "maison" "vieilles" "siècle" "l'abbé"
## [8,] "baie" "artistes" "galettes" "coiffe"
## [9,] "commune" "carhaix" "6" "sud"
## [10,] "jean" "glenmor" "1" "guénolé"
## [11,] "galets" "festivaliers" "noir" "guilvinec"
## [12,] "plage" "juillet" "20" "communes"
## [13,] "mer" "rock" "partie" "breton"
## [14,] "lababan" "fête" "rennes" "années"
## [15,] "pierre" "grande" "pain" "bigoudène"
On peut également regarder à quel topic est préférentiellement associé tel ou tel texte.
posterior(res_lda)$topics
## 1 2 3 4
## Pays Bigouden 0.19862764 0.03625213 0.14094000 0.62418023
## Festival des Vieilles Charrues 0.03830472 0.79431330 0.15075107 0.01663090
## Galette-saucisse 0.02354740 0.03516820 0.90733945 0.03394495
## Pouldreuzic 0.79163331 0.02301841 0.08186549 0.10348279
On obtient dans ce tableau les probabilités d’appartenances des textes aux différents topics. Et on se rends compte que certains textes sont plus ou moins associé à tel ou tel topic. l’attribution du topic sur les textes associés à “Galettes-saucisses” est très clair c’est un peu moins clair pour les textes liés au “Pays bigouden”.
Et non! Le meilleur moyen d’étudier notre modèle c’est de le faire revenir dans le tidyverse grâce à la fonction tidy
du package {broom}. Le tidyverse étant une manière de coder en R, qui globalement simplifie la compréhension du code et facilite les opérations compliquées
#La matrice beta donne la probabilité que chaque mot soit généré par un des quatre topics.
res_lda_beta <- tidy(res_lda,matrix = "beta")
res_lda_beta
## # A tibble: 27,412 x 3
## topic term beta
## <int> <chr> <dbl>
## 1 1 bigouden 0.000246
## 2 2 bigouden 0.0000211
## 3 3 bigouden 0.0000239
## 4 4 bigouden 0.0156
## 5 1 pays 0.0000224
## 6 2 pays 0.0000211
## 7 3 pays 0.000263
## 8 4 pays 0.0153
## 9 1 festival 0.0000224
## 10 2 festival 0.0177
## # ... with 27,402 more rows
On peut ainsi réaliser plusieurs opérations, comme chercher le résultat d’un mot bien précis, par exemple ici le mot saucisse.
res_lda_beta %>%
filter(term == "saucisse")
## # A tibble: 4 x 3
## topic term beta
## <int> <chr> <dbl>
## 1 1 saucisse 0.0000224
## 2 2 saucisse 0.0000211
## 3 3 saucisse 0.0158
## 4 4 saucisse 0.0000162
On voit ainsi que le mot saucisse à une plus forte probabilité de se retrouver dans le topic 1.
On peut également créer des sous-corpus qui se composent des termes les plus fortement associés à chaque topic.
# Pour stocker les termes les plus spécifiques à chaque topic, ici les 10 premiers
top_terms <- res_lda_beta %>%
group_by(topic) %>%
top_n(10, beta) %>%
ungroup() %>%
arrange(topic, -beta)
top_terms
## # A tibble: 41 x 3
## topic term beta
## <int> <chr> <dbl>
## 1 1 pouldreuzic 0.0132
## 2 1 penhors 0.0110
## 3 1 hénaff 0.00652
## 4 1 d'audierne 0.00517
## 5 1 port 0.00495
## 6 1 quimper 0.00495
## 7 1 maison 0.00495
## 8 1 baie 0.00473
## 9 1 commune 0.00473
## 10 1 jean 0.00473
## # ... with 31 more rows
Pour que ce soit plus lisible, on peut les représenter à l’aide d’histogrammes.
library(ggplot2)
#Top 10 des mots de chaque topic
res_lda_beta %>%
group_by(topic) %>%
top_n(10, beta) %>%
ungroup() %>%
arrange(topic, -beta) %>%
ggplot(aes(reorder(term, beta), beta, fill = factor(topic))) +
geom_col(show.legend = FALSE) +
facet_wrap(~ topic, scales = "free") +
scale_fill_manual(values = wes_palette("GrandBudapest2"))+
coord_flip() +
labs(x = "Topic",
y = "beta score",
title = "Les 10 mots les plus fortement associés à chaque topic")
Comme il est possible de faire une recherche par mot, il est également possible de représenter l’appartenance du mot recherché à chaque topic .
res_lda_beta %>%
filter(term == "saucisse") %>%
ggplot(aes(topic, beta, fill = factor(topic))) +
geom_col() +
scale_fill_manual(values = wes_palette("GrandBudapest2")) + #pour définir la palette de couleur
coord_flip() +
labs(x = "Topic",
y = "beta score",
title = "Topic modeling du mot saucisse ") +
theme_minimal()
Maintenant que nous avons un peu exploré les mots, que peut-on faire des documents? Et bien exactement la même chose! On commence par faire revenir les probabilités d’appartenance de chaque document à un topic dans le tidyverse.
# La matrice gamma contient la probabilité de chaque document d'appartenir à un topic.
res_lda_gamma <- tidy(res_lda,matrix = "gamma")
res_lda_gamma
## # A tibble: 16 x 3
## document topic gamma
## <chr> <int> <dbl>
## 1 Pays Bigouden 1 0.199
## 2 Festival des Vieilles Charrues 1 0.0383
## 3 Galette-saucisse 1 0.0235
## 4 Pouldreuzic 1 0.792
## 5 Pays Bigouden 2 0.0363
## 6 Festival des Vieilles Charrues 2 0.794
## 7 Galette-saucisse 2 0.0352
## 8 Pouldreuzic 2 0.0230
## 9 Pays Bigouden 3 0.141
## 10 Festival des Vieilles Charrues 3 0.151
## 11 Galette-saucisse 3 0.907
## 12 Pouldreuzic 3 0.0819
## 13 Pays Bigouden 4 0.624
## 14 Festival des Vieilles Charrues 4 0.0166
## 15 Galette-saucisse 4 0.0339
## 16 Pouldreuzic 4 0.103
Comme pour les mots on peut ensuite identifier les topics principaux de chaque document.
# Pour identifier le topic principal des textes
res_lda_gamma %>%
group_by(topic) %>%
arrange(desc(gamma)) %>%
top_n(1)
## Selecting by gamma
## # A tibble: 4 x 3
## # Groups: topic [4]
## document topic gamma
## <chr> <int> <dbl>
## 1 Galette-saucisse 3 0.907
## 2 Festival des Vieilles Charrues 2 0.794
## 3 Pouldreuzic 1 0.792
## 4 Pays Bigouden 4 0.624
Le visualiser :
# Visualiser avec ggplot les topics principaux de chaque texte
res_lda_gamma %>%
ggplot(aes(document, gamma, fill = factor(topic))) +
geom_col() +
scale_fill_manual(values = wes_palette("GrandBudapest2")) +
coord_flip() +
labs(x = "Corpus",
y = "gamma score",
title = "Topic modeling de 4 textes fondamentaux portant sur la Bretagne")
On se rend compte que même si la répartition des documents dans chaque topic ne fait pas vraiment débat, les choses sont malgré tout un peu plus compliquées. En effet, chaque document a bien un topic principal auquel il est plus fortement associé mais on voit que chaque texte se projette aussi dans les autres topics. Ce qui n’est pas du tout un problème et traduit simplement une certaine proximité entre les textes, même si la thématique de chaque texte est différente ils renvoient tous les 4 à la Bretagne.
On peut d’ailleurs regarder plus précisément pour un texte la répartition des topics.
res_lda_gamma %>%
filter(document == "Galette-saucisse") %>%
ggplot(aes(topic, gamma, fill = factor(topic))) +
geom_col() +
scale_fill_manual(values = wes_palette("GrandBudapest2")) +
coord_flip() +
labs(x = "Topic",
y = "gamma score",
title = "Topic modeling du texte Galette-saucisse ") +
theme_minimal()
Pour un document ou la répartition est un peu plus ambigüe :
res_lda_gamma %>%
filter(document == "Pays Bigouden") %>%
ggplot(aes(topic, gamma, fill = factor(topic))) +
geom_col() +
scale_fill_manual(values = wes_palette("GrandBudapest2")) +
coord_flip() +
labs(x = "Topic",
y = "gamma score",
title = "Topic modeling du texte Pays Bigouden ") +
theme_minimal()
La matrice gamma peut également être très intéressante dans le cas de corpus plus importants notamment pour voir si notre modèle est bien “tranché”. C’est-à-dire voir si notre modèle arrive à déterminer quel texte génère quel topic. En effet, nous l’avons vue chaque document génère un pourcentage de participation à plusieurs topics (en l’occurrence ici à chacun des 4 topics).
res_lda_gamma %>%
ggplot(aes(gamma)) +
geom_histogram(bins = 100) +
scale_y_log10() +
labs(x = "Gamma score",
y = "Count",
title = "Score Gamma du topic Model sur les traditions bretonnes") +
theme_minimal()
Lorsqu’un topic model offre un découpage bien tranché, c’est à dire que les différents textes, documents sont clairement attribué à tel ou tel topic, les deux modes (les valeurs le splus fréquentes) sont autour de 0 et de 1. Ici on est plutôt bon!
Il existe de nombreux package pour faire du topic modeling sur R, et notamment des visualisations. Les packages topicApp (https://github.com/wesslen/topicApp) et LDAvis (https://github.com/cpsievert/LDAvis) offrent des représentations intéressantes et complémentaires avec les représentations réalisées un peu plus haut.
Comment définir le nombre de topics idéals?
C’est effectivement la question centrale du topic modeling. Malheureusement il n’y a pas de réponses claires, car comme souvent en analyse textuelle il est parfois (toujours!) nécessaire de relancer ses analyses en changeant le paramètre et comparer les résultats. Cependant, il y a tout de même deux méthodes qui se complètent assez bien. Il y a d’abord le bon sens et la connaissance de son corpus. Ici nous avons constitué un corpus de 4 sources différentes :
Les 4 sources portant sur des sujets assez différents, il serait tout fait cohérent d’envisager 4 topics. En sachant que certain termes seront transverse à nos 4 topics. Ici pour l’exercice, nous ferons ce choix.
L’autre méthode est statistique, son objectif est d’identifier le meilleur nombre possible de topics. Le package {ldatuning} propose la fonction
FindTopicNumber
qui implémente 4 méthodes différentes pour identifier ce nombre “idéal” : “Griffiths2004”, “CaoJuan2009”, “Arun2010” et “Deveaud2014”.Le calcul de cette fonction peut être très long. Ici nous nous limitons à 15 topics, mais plus ce nombre augmente plus le temps de calcul va augmenter, et cela peut aller jusqu’à plusieurs heures si en plus d’un nombre élevé de topics on a un corpus important.
Une fois le calcul efectué l’identification du nombre de topic adéquat se fait via l’analyse d’un graph.
“Griffiths” et “Deveaud” suivent un principe de maximisation alors que “CaoJuan” et “Arun” obéissent à un principe de minimisation. Je vous épargne les détails techniques, mais l’idée ici est d’identifier l’endroit où simultanément “Griffiths” et “Deveaud” se rejoignent le plus et où c’est également le cas pour “CaoJuan” et “Arun”. Tout est histoire de compromis, trouver l’endroit ou l’écart entre les courbes est minimal en haut et en bas ! Ici, nous aurions tendance à retenir 5 topics.