Text mining & n-gramme avec R

Wow, vous êtes encore en train de nous perdre avec un truc compliqué ! Mais non, lecteurs, rassurez-vous : malgré un nom tiré par les cheveux, les n-gramme sont faciles à comprendre… et simples à mettre en place avec R ! Démonstration en bonne et due forme.

Alors, c’est quoi un n-gramme

Tout simplement, un n-gramme est “une sous-séquence de n éléments construite à partir d’une séquence donnée.” (merci Wikipédia). En clair, il s’agit d’une séquence de taille n, piochée dans une séquence de taille plus grande que n. Un exemple ? de est un bigramme de demain, mai un trigramme et main un quadrigramme.

Une découpe en n-gramme ne s’applique pas qu’aux lettres, et nous pouvons faire la même chose avec des mots : “Je suis” est un bigramme de “Je suis en train de programmer en R”, “en train de” un trigramme, “je suis en train” un quadrigramme, et ainsi de suite…

À quoi ça sert ? En pratique, une découpe en n-gramme permet de créer un modèle probabiliste, pour anticiper le prochain élément d’une suite… Il s’agit par exemple la technologie utilisée par votre smartphone pour effectuer de la prédiction de texte lorsque vous commencez à taper des mots : l’appareil construit des phrases par n-gramme, c’est-à-dire en vous proposant la combinaison suivante la plus probable, en fonction de votre entrée. C’est également cette technique qui est utilisée par un moteur de recherche lorsqu’il vous suggère de terminer la requête que vous avez commencé à taper.

N-gramme et text-mining

Et pour le sujet qui nous intéresse ? En text-mining “pur”, la recherche par n-gramme permet d’aller plus loin que l’analyse de fréquence par unigramme (celle que nous avons vu dans notre premier billet). En effet, les subtilités d’un texte vont souvent apparaitre en allant plus loin qu’un simple “ce mot-là est le plus présent”. D’autant plus si l’on décide d’indexer des documents automatiquement : se baser sur un seul terme pourra fausser le résultat. Comment ? Par exemple, si on lie sous un terme “transport” (parce que tous vos éléments en entrée contiennent ce terme), nous indexerons ensemble des textes sur le transport en commun, le transport de marchandises, le transport de fret… mais aussi de potentiels documents scientifiques sur le transport au cerveau.

L’autre importance du n-gramme ? Permettre de perfectionner l’analyse de sentiment ! En effet, là où l’unigramme classerait “aime” ou “content” dans une catégorie positive, son couplage avec “pas” (aime pas / pas content) offre une vision plus précise de la phrase.

n-gramme avec R

Bref, vous l’aurez compris, travailler avec des n-grammes est indispensable à un text-mining plus avancé. Et si nous vous en parlons aujourd’hui, c’est pour vous expliquer comment le faire en R.

Charger le texte

Dans notre premier billet, nous analysions The Philosophy of Mathematics. Aujourd’hui, changeons un peu d’air avec Flatland: A Romance of Many Dimensions

library(tidyverse)
## Loading tidyverse: ggplot2
library(tidytext)
library(gutenbergr)
book <- gutenberg_works(title == "Flatland: A Romance of Many Dimensions")$gutenberg_id %>%
  gutenberg_download() %>% 
  gutenberg_strip()

bigramme, trigramme et quadrigramme

Si l’on souhaite réaliser des n-grammes avec la méthode “classique” (avec le package tm), vous devrez… utiliser un autre package (par exemple ngram), ou rédiger votre propre fonction. Mais parce qu’il n’est pas nécessaire de se compliquer la tâche pour réaliser un n-gramme, nous allons nous concentrer sur les possibilités offertes par le package tidytext, et sa fonction unnest_tokens, désormais agrémentée de deux paramètres — token = "ngrams", et n=x, où x est le volume de votre n-gramme.

library(tidytext)
#Bigram
book_bigram <- data_frame(line = 1:nrow(book), text = book$text)  %>%
  unnest_tokens(bigram, text, token = "ngrams", n = 2) %>%
  count(bigram, sort = TRUE)
#Trigram
book_trigram <- data_frame(line = 1:nrow(book), text = book$text)  %>%
  unnest_tokens(bigram, text, token = "ngrams", n = 3)%>%
  count(bigram, sort = TRUE)
#Four-gram 
book_fourgram <- data_frame(line = 1:nrow(book), text = book$text)  %>%
  unnest_tokens(bigram, text, token = "ngrams", n = 4)%>%
  count(bigram, sort = TRUE)
#et ainsi de suite ! 

Et voilà, c’est aussi simple que ça !

Nettoyer le texte

Seul hic, nous retrouvons avec un data_frame contenant “of the”, “of a”, et cie… Bref, un petit nettoyage s’impose :

library(tidytext)
#Bigram
book_bigram_clean <- book_bigram %>%
  separate(bigram, c("word1", "word2"), sep = " ") %>%
  filter(!word1 %in% stop_words$word) %>%
  filter(!word2 %in% stop_words$word) %>%
  unite(bigram, word1, word2, sep = " ")
#Trigram
book_trigram_clean <- book_trigram  %>%
  separate(bigram, c("word1", "word2", "word3"), sep = " ") %>%
  filter(!word1 %in% stop_words$word) %>%
  filter(!word3 %in% stop_words$word) %>%
  unite(bigram, word1, word2, word3, sep = " ")

Jetons un œil un instant à cette méthode de nettoyage, en affichant les 10 premières entrées de chaque tableau, complétées des résultats d’une analyse de fréquence simple.

data.frame(unigram = book %>%
             unnest_tokens(word, text) %>%
             count(word, sort = TRUE) %>%
             head(10),
           classic_clean = book %>%
             unnest_tokens(word, text) %>%
             anti_join(stop_words) %>%
             count(word, sort = TRUE) %>%
             head(10),
           bigram = head(book_bigram, 10), 
           bigram_clean = head(book_bigram_clean, 10), 
           trigram = head(book_trigram, 10), 
           trigram_clean = head(book_trigram_clean, 10))

Tidy text mining with R

N’hésitez pas à cliquer sur l’image pour zoomer.

Débrief

C’est bien beau tout cela, mais quelle méthode doit-on privilégier ? Malheureusement, ici… pas de bonne réponse ! Nos n-grammes se complètent les uns après les autres, offrant une vision de plus en plus précise sur le contenu du texte. À noter que pour nettoyer les données, nous avons pour ce modèle utilisé la liste de stop_words (ou mots vides) fournie dans le package tidytext. Cependant, cet usage n’est pas neutre : par exemple, et comme nous le disions plus haut, si nous cherchons à qualifier un sentiment en prenant en compte le “not” , un filtre systématique avec les stop_words ferait disparaitre toutes les occurrences de la négation. Même chose pour les trigrammes et plus: nous avons choisi ici de filtrer les trigrammes contenant un mot neutre en position 1 ou 3. Effectuer un filtre complet supprimerait sense of sight, law of nature et consorts…

La morale ? Tester, tester, toujours tester… et ne pas foncer tête baissée !


À propos de l'auteur

Colin Fay

Colin Fay

Data scientist & R Hacker


Commentaires


À lire également