Nettoyage du texte en NLP : moins de vocabulaire, moins de bruit

le 14/04/2022 par Sofiene Alouini
Tags: Data & AI, Data Science

Cet article est le deuxième de la série Analyse de tendances des réseaux sociaux.

Dans l'article précédent, nous avons présenté les bases méthodologiques pour analyser des tendances à partir de données de réseaux sociaux. Nous avons notamment expliqué l’importance de bien identifier la population de référence sur laquelle porte notre étude, et de bien choisir la fonction d’extrapolation pour que nos observations soient réellement représentatives.

À présent, il est temps de commencer à collecter des tweets et de se lancer dans la détection de tendances ! La collecte est relativement facile à réaliser grâce à l’API Twitter. Une fois la base de données constituée, on dispose a priori d’une mine d’informations qui ne demandent qu’à être modélisées pour une meilleure compréhension des tendances politiques, économiques, sociales…

Cependant, on se rend rapidement compte des spécificités des données de réseaux sociaux. Il s’agit de texte libre, qui vient avec son lot de fautes d’orthographe, d’abréviations, de néologismes, de caractères spéciaux comme les emojis 💥, de mentions d’autres utilisateurs, ou de références plus ou moins explicites à d’autres sujets d’actualité.

Il va donc falloir bien préparer cette donnée non-structurée, afin de faciliter les analyses et modélisations qui vont suivre. Le nettoyage de texte est souvent la première étape de tout projet de Natural Language Processing (NLP). Cet article présente une approche possible de nettoyage des tweets, en se focalisant sur la réduction du vocabulaire. On utilisera pour cela le package spacy, l'un des plus plus populaires pour le traitement du langage naturel en Python.

Un peu de terminologie

Commençons par définir certains concepts importants en NLP et qui nous serviront dans toute la suite de l’article.

On appelle corpus l’ensemble des textes d’un jeu de données.

Chaque texte du corpus est appelé document. Par exemple, dans un corpus de tweets, on fera référence à chaque tweet comme étant un document.

Chaque document peut être découpé en unités de texte, qu’on appelle des tokens. Souvent, les tokens sont assimilés aux mots du document, mais selon le problème qu’on cherche à résoudre, les tokens peuvent être des caractères individuels (des “lettres”), voire des morceaux de mots (des subwords).

La tokenisation est l'opération consistant à découper un document en tokens. Dans cet article, on adopte une tokenisation à la maille des mots. Ce niveau de granularité est le plus intuitif, et celui qui bénéficie le plus du nettoyage de texte. Une tokenisation à la maille des caractères serait trop abstraite et peu exploitable pour capturer une sémantique. Une tokenisation en subwords serait intéressante pour réduire le vocabulaire, mais nécessite un apprentissage statistique préalable pour trouver les découpages pertinents.

Enfin, on appelle vocabulaire l’ensemble des tokens distincts présents dans un corpus.

Comment structurer le texte ?

Pour effectuer des analyses statistiques, ou construire un modèle de Machine Learning, il est nécessaire de se ramener à des données structurées (un “tableau” de données).

Comment faire pour apporter une structure aux textes non structurés que sont les tweets?

Bag-of-words est une approche possible pour structurer du texte. Cette approche consiste en deux étapes :

  1. On détermine le vocabulaire du corpus.
  2. Chaque document est transformé en un vecteur, dont les composantes sont les occurrences de chaque token du vocabulaire dans le document en question.

Illustrons cela par un exemple. Si notre corpus est constitué des deux documents suivants :

Vous lisez le blog d'OCTO Technology.

Octo Technology ❤️ NLP.

On obtient un vocabulaire de taille N = 10 :

["Vous", "lisez", "le", "blog", "d'", "OCTO", "Technology", ".", "Octo", "❤️", "NLP"]

Chaque document est alors représenté par N attributs comme suit :

Vouslisezleblogd'OCTOTechnology.Octo❤️NLP
11111111000
00000011111

Dans un corpus non nettoyé de quelques milliers de tweets, tokenisés à la maille des mots, la taille du vocabulaire N atteint rapidement plusieurs dizaines de milliers de tokens “uniques”.

Si cette dimension N est grande, c’est en partie à cause de tokens très proches, comme les mots OCTO et Octo dans l’exemple précédent. Ces derniers sont considérés comme étant différents à cause de la différence de typographie. C’est l’une des situations où le nettoyage de texte est utile, car il permet d’uniformiser les tokens similaires, réduisant ainsi la taille du vocabulaire.

NB: l’approche Bag-of-words n’est pas la seule façon de représenter numériquement du texte, mais elle a l’avantage d’être simple à comprendre, et bénéficie grandement du nettoyage de texte qui fait l’objet de cet article ! Il existe d’autres approches basées sur la notion d’Embedding, elles seront abordées dans une future publication 😊.

Réduire la taille du vocabulaire

Pour illustrer la problématique liée à la taille du vocabulaire, considérons un cas d’usage simple: la classification des tweets selon leurs thématiques. Pour classifier nos documents, on peut entraîner un modèle de Machine Learning (une régression logistique par exemple) sur le tableau structuré issu du Bag-of-words.

La régression va construire une pondération optimale des N attributs, en ajustant ses N+1 paramètres pendant la phase d’apprentissage. La complexité du modèle (en nombre de paramètres à apprendre) croît donc linéairement avec la taille du vocabulaire. Cette complexité croît encore plus rapidement si l’on opte pour des modèles plus avancés que la régression logistique.

Ainsi, plus le vocabulaire est riche, plus le modèle est complexe, et plus il nécessite un volume important de données d’entraînement.

L’une des façons de simplifier le problème est donc de réduire la taille du vocabulaire en amont de la modélisation. Dans cet article, nous présentons les différentes étapes pour nettoyer le texte dans le but de réduire le vocabulaire. Pour illustrer cela, nous utilisons le package Python spacy.

La tokenisation avec spacy

spacy est l’un des packages Python les plus populaires en NLP. Il présente une API intuitive permettant de traiter du texte de façon efficace, quelle que soit la langue du texte utilisé. Il permet d’implémenter des opérations basiques sur du texte, mais également de tirer parti de modèles pré-entraînés, utiles pour faire des traitements avancés.

On peut installer spacy en utilisant pip, puis télécharger un modèle pré-entraîné pour la langue française :

pip install spacy
python -m spacy download fr_core_news_lg

Les modèles pré-entraînés de spacy suivent une convention de nommage particulière de la forme <langue>_<type>_<corpus_d’apprentissage>_<taille>.

Ainsi, le modèle fr_core_news_lg a les propriétés suivantes :

  • fr : modèle pour la langue française
  • core : modèle généraliste ayant des composants pour différentes tâches (NER, lemmatisation…)
  • news : modèle entraîné sur un corpus d’articles de presse
  • lg : modèle volumineux, ayant un grand nombre de paramètres (lg = large, md = medium...)

Prenons comme fil rouge ce tweet publié par le compte Twitter @OCTOTechnology, qui nous permettra d’illustrer de nombreux traitements utiles en NLP :

Dans une fenêtre interactive Python, on stocke le texte du tweet dans une variable text :

>>> text = """[#MachineLearning] 🧠 Que signifie “passer à l’échelle” quand on fait du Machine Learning ? Que se passe-t-il après le PoC ? Par quel chantier est-ce que je dois commencer ?
 
💡Toutes les réponses à vos questions avec @SofieneAlouini et Mehdi ! 
 
➡️ https://bit.ly/3Gr7XXa"""

On importe ensuite le package spacy, et on crée une instance de modèle en français, qu’on stocke dans une variable nlp :

>>> import spacy

>>> nlp = spacy.load("fr_core_news_lg")

Essayons de comprendre l’objet nlp :

>>> type(nlp)

<class 'spacy.lang.fr.French'>

>>> nlp.lang

'fr'

>>> nlp.component_names

['tok2vec', 'morphologizer', 'parser', 'senter', 'attribute_ruler', 'lemmatizer', 'ner']

>>> nlp.max_length

1000000

L’objet nlp est une instance de la classe spacy.lang.fr.French, créée par l’opération .load() qui permet de charger un modèle pré-entraîné. Dans spacy, les modèles pré-entraînés sont des modèles “tout-en-un”, constitués par défaut de plusieurs composants permettant chacun de réaliser une tâche de NLP spécifique (senter pour la reconnaissance de phrases, ner pour la reconnaissance d’entités nommées…).

Un modèle pré-entraîné vient aussi avec une certaine configuration standard. Par exemple, la longueur maximale par défaut des textes que peut gérer ce modèle est de 1 000 000 de caractères. Il est possible de changer cette valeur si besoin.

On peut “appliquer” ce modèle à notre texte en l’appelant comme une fonction :

>>> doc = nlp(text)

>>> type(doc)

<class 'spacy.tokens.doc.Doc'>

On récupère alors un objet Doc. Cet objet est un itérable Python représentant un document au sens de la définition donnée plus haut. Il est donc composé de tokens, comme on peut le voir en examinant le premier élément de l’itérable :

>>> type(doc[0])

<class 'spacy.tokens.token.Token'>

On peut visualiser l’ensemble des tokens de doc grâce à l’attribut .text :

>>> [token.text for token in doc]

['[', '#', 'MachineLearning', ']', '🧠', 'Que', 'signifie', '“', 'passer', 'à', 'l’', 'échelle', '”', 'quand', 'on', 'fait', 'du', 'Machine', 'Learning', '?', 'Que', 'se', 'passe', '-t', '-il', 'après', 'le', 'PoC', '?', 'Par', 'quel', 'chantier', 'est', '-ce', 'que', 'je', 'dois', 'commencer', '?', '\n\n', '💡', 'Toutes', 'les', 'réponses', 'à', 'vos', 'questions', 'avec', '@SofieneAlouini', 'et', 'Mehdi', '!', ' \n\n', '➡', '️', 'https://bit.ly/3Gr7XXa']

On comprend que le modèle nlp effectue une tokenisation un peu plus élaborée qu'un simple découpage au niveau des espaces ou des sauts de ligne. Par exemple, [#MachineLearning] a été tokenisé en ['[', '#', 'MachineLearning', ']'], et “passer à l’échelle” a été tokenisé en ['“', 'passer', 'à', 'l’', 'échelle', '”'].

Le nettoyage du texte avec spacy

Maintenant qu’on a compris les notions basiques de l’API de spacy, voyons comment on peut l’utiliser pour nettoyer du texte. Pour rappel, le nettoyage du texte est l’opération consistant à réduire la taille du vocabulaire afin de diminuer la dimension de l’espace d’entrée de nos modèles de Machine Learning.

La ponctuation

Souvent dans les modèles NLP, la ponctuation a peu d’importance. Par exemple, si l’on souhaite classer des articles de blog selon leur thématique (Data Science, Software Craftsmanship, OPS…), il y a peu de chances que la ponctuation porte du signal. Un premier nettoyage possible serait donc d’ignorer tous les tokens qui sont des signes de ponctuation. Avec spacy, cela se fait simplement dans une list comprehension grâce à l’attribut .is_punct de la classe Token :

>>> [token.text for token in doc if (not token.is_punct)]

['MachineLearning', '🧠', 'Que', 'signifie', 'passer', 'à', 'l’', 'échelle', 'quand', 'on', 'fait', 'du', 'Machine', 'Learning', 'Que', 'se', 'passe', '-t', '-il', 'après', 'le', 'PoC', 'Par', 'quel', 'chantier', 'est', '-ce', 'que', 'je', 'dois', 'commencer', '\n\n', '💡', 'Toutes', 'les', 'réponses', 'à', 'vos', 'questions', 'avec', '@SofieneAlouini', 'et', 'Mehdi', ' \n\n', '➡', '️', 'https://bit.ly/3Gr7XXa']

On voit que des tokens comme ?, !, [ et ] ont bien été filtrés.

Attention toutefois, pour certains cas de figure comme l’analyse de sentiments, la ponctuation pourrait s’avérer pertinente. On a alors intérêt à la garder dans ces cas-là !

Les espaces

Des espaces multiples ou des retours à la ligne peuvent s’insérer dans le texte pour des raisons de mise en forme, mais ils sont rarement utiles pour la modélisation. On peut donc les enlever de façon similaire à la ponctuation, en utilisant l’attribut .is_space. Dans notre exemple, cela permet de se débarrasser des doubles sauts de ligne \n\n.

>>> [token.text for token in doc if (not token.is_punct) and (not token.is_space)]

['MachineLearning', '🧠', 'Que', 'signifie', 'passer', 'à', 'l’', 'échelle', 'quand', 'on', 'fait', 'du', 'Machine', 'Learning', 'Que', 'se', 'passe', '-t', '-il', 'après', 'le', 'PoC', 'Par', 'quel', 'chantier', 'est', '-ce', 'que', 'je', 'dois', 'commencer', '💡', 'Toutes', 'les', 'réponses', 'à', 'vos', 'questions', 'avec', '@SofieneAlouini', 'et', 'Mehdi', '➡', '️', 'https://bit.ly/3Gr7XXa']

Les URL

spacy propose aussi un attribut .like_url qui permet de savoir directement si un token a une forme d’URL :

>>> [token.text for token in doc if (not token.is_punct) and (not token.is_space) and (not token.like_url)]

['MachineLearning', '🧠', 'Que', 'signifie', 'passer', 'à', 'l’', 'échelle', 'quand', 'on', 'fait', 'du', 'Machine', 'Learning', 'Que', 'se', 'passe', '-t', '-il', 'après', 'le', 'PoC', 'Par', 'quel', 'chantier', 'est', '-ce', 'que', 'je', 'dois', 'commencer', '💡', 'Toutes', 'les', 'réponses', 'à', 'vos', 'questions', 'avec', '@SofieneAlouini', 'et', 'Mehdi', '➡', '️']

Cela nous permet d’éliminer le lien https://bit.ly/3Gr7XXa. Dans d’autres cas de figure, on pourrait se servir de ce même attribut .like_url pour, au contraire, extraire et stocker toutes les URL présentes dans un corpus.

Les stop words

En NLP, les stop words sont les tokens très fréquents dans une langue donnée. Ces tokens étant présents dans la plupart des textes, ils ont un pouvoir discriminant assez faible. On a donc tendance à les supprimer pour réduire encore plus la taille du vocabulaire. Un modèle spacy pré-entraîné contient déjà une liste de stop words pour la langue utilisée. Voici un aperçu des stop words par défaut du modèle fr_core_news_lg :

>>> nlp.Defaults.stop_words

{'on', 'or', 'celle', 'semble', 'avoir', 'pourquoi'...}

Il est alors possible d’ignorer ces stop words dans notre document, grâce à l’attribut .is_stop de la classe Token :

>>> [token.text for token in doc if (not token.is_punct) and (not token.is_space) and (not token.like_url) and (not token.is_stop)]

['MachineLearning', '🧠', 'signifie', 'passer', 'échelle', 'Machine', 'Learning', 'passe', '-t', '-il', 'PoC', 'chantier', '-ce', 'dois', 'commencer', '💡', 'réponses', 'questions', '@SofieneAlouini', 'Mehdi', '➡', '️']

On voit que de nombreux tokens ont été supprimés : Que, à, l', quand, on… Si on souhaite garder certains stop words que l’on juge utiles pour la modélisation (des éléments de négation par exemple), ou si l’on souhaite en ignorer d’autres que le modèle pré-entraîné ne connaît pas, on peut tout à fait modifier l’ensemble nlp.Defaults.stop_words (qui est un simple objet de type set) pour y ajouter ou en enlever des éléments.

Les emojis et les tokens “vides”

Sur les réseaux sociaux, les utilisateurs ont tendance à employer des emojis pour illustrer leurs propos. Ces emojis sont des caractères spéciaux, et forment donc des tokens à 1 caractère. Dans notre exemple, on a 3 emojis 🧠, 💡 et ➡.

Bien qu’utiles pour certaines modélisations (comme l’analyse de sentiments), on peut choisir de les supprimer pour d’autres cas d’usage. Pour cela, on peut se baser sur la longueur des tokens, pour ne garder que ceux qui font au moins 2 caractères. Cela supprimera par la même occasion les tokens “vides” qui peuvent apparaître.

>>> [token.text for token in doc if (not token.is_punct) and (not token.is_space) and (not token.like_url) and (not token.is_stop) and len(token) > 1]

['MachineLearning', 'signifie', 'passer', 'échelle', 'Machine', 'Learning', 'passe', '-t', '-il', 'PoC', 'chantier', '-ce', 'dois', 'commencer', 'réponses', 'questions', '@SofieneAlouini', 'Mehdi']

Les entités nommées

Le tweet contient un token représentant le prénom d’une personne, Mehdi. Si notre but est de faire de la classification par thématique, on pourrait vouloir filtrer ce type de tokens.

En effet, il est peu probable que le prénom d’une personne ait un pouvoir prédictif important dans une telle classification (à moins que les Mehdi soit particulièrement sur-représentés dans la communauté MLOps 🤔. Et même si c’est le cas, ce serait risqué de baser nos modèles là-dessus, cela pourrait introduire certains biais, donc désolé Mehdi on va filtrer 😋).

Comment faire alors pour identifier les prénoms ou noms propres en vue de les filtrer ? Une règle simple serait de vérifier que le token est constitué d’une lettre majuscule suivie de plusieurs lettres minuscules. La méthode str.istitle() en Python permet justement de vérifier qu’une chaîne de caractère est de la forme "Aaaaa Bbbbb Ccccc", ce qui ferait l’affaire ici.

>>> "Mehdi".istitle()

True

Il suffirait alors que l’on rajoute une condition (not token.text.istitle()) dans notre list comprehension. Le souci avec cette approche est que l’on risque d’éliminer plusieurs autres tokens, dont certains seraient utiles pour la modélisation. Dans notre exemple, cela éliminerait des tokens comme Machine et Learning, ce qu’on aimerait bien éviter.

Une alternative plus judicieuse serait d’utiliser le composant ner de notre modèle pré-entraîné. Il s’agit d’un modèle de Named Entity Recognition dont le rôle est de détecter des entités particulières (personnes, organisations, dates, pays…) dans le texte. Vérifions quelles sont les entités détectées par notre modèle, avec une belle visualisation dans le navigateur :

>>> from spacy import displacy

>>> displacy.serve(doc, style="ent", options={"colors":{"PER": "cyan"}})

Le modèle détecte deux types d’entités: plusieurs entités MISC (entités n’appartenant pas à un type particulier) et une entité de type PER (comme “person”) qui correspond au token Mehdi. Cela tombe bien, c’est ce qu’on cherchait à détecter !

On ajoute alors une condition (not token.ent_type_ == "PER") à notre pipeline de nettoyage :

>>> [token.text for token in doc if (not token.is_punct) and (not token.is_space) and (not token.like_url) and (not token.is_stop) and len(token) > 1 and (not token.ent_type_ == "PER")]

['MachineLearning', 'signifie', 'passer', 'échelle', 'Machine', 'Learning', 'passe', '-t', '-il', 'PoC', 'chantier', '-ce', 'dois', 'commencer', 'réponses', 'questions', '@SofieneAlouini']

Ajouter des règles de tokenisation

Jusqu’ici, quelques traitements basiques ont permis de commencer à bien réduire la taille du vocabulaire. Grâce aux fonctionnalités de spacy, on a pu implémenter ces traitement en une ligne de code. Toutefois, il est évident qu’on pourrait aller plus loin, et que certains tokens ne sont pas encore gérés correctement.

Dans la suite, on va personnaliser le modèle pour gérer différents cas particuliers.

Les mentions

Le tweet contient une mention (un nom d’utilisateur, ou handle en anglais) : @SofieneAlouini.

De manière analogue aux URL, on a envie de pouvoir identifier ces mentions, pour les filtrer ou au contraire les extraire, selon la problématique que l’on cherche à résoudre. Toutefois, contrairement aux URL facilement détectables grâce à l’attribut .like_url, il n’existe pas d’attribut .like_handle par défaut.

Comme il s’agit du nom d’utilisateur Twitter d’une personne, cela aurait pu être détecté comme une entité de type PER, à l’instar du token Mehdi. Malheureusement il n’est pas identifié comme tel par le modèle qu’on a choisi.

Une option possible est alors de définir un attribut personnalisé ._.like_handle, en utilisant par exemple des expressions régulières :

>>> import re

>>> from spacy.tokens import Token

>>> handle_regex = r"@[\w\d_]+"

>>> like_handle = lambda token: re.fullmatch(handle_regex, token.text)

>>> Token.set_extension("like_handle", getter=like_handle)

>>> [token for token in nlp(text) if token._.like_handle]

['@SofieneAlouini']

En mettant à jour le pipeline de nettoyage avec cette nouvelle fonctionnalité, on vérifie que la mention @SofieneAlouini est bien filtrée :

>>> [token.text for token in doc if (not token.is_punct) and (not token.is_space) and (not token.like_url) and (not token.is_stop) and len(token) > 1 and (not token.ent_type_ == "PER") and (not token._.like_handle)]

['MachineLearning', 'signifie', 'passer', 'échelle', 'Machine', 'Learning', 'passe', '-t', '-il', 'PoC', 'chantier', '-ce', 'dois', 'commencer', 'réponses', 'questions']

Les tokens en CamelCase

Il arrive que certaines expressions soient écrites en CamelCase, sous forme d’un seul token. C’est souvent le cas des tokens issus d’un hashtag, car les hashtags n’autorisent pas les espaces. Dans notre exemple, le MachineLearning provenant de #MachineLearning est un token à part, qui sera considéré comme étant différent de la paire (Machine, Learning), alors que ces deux découpages font référence à la même notion.

On peut forcer le découpage des tokens CamelCase au niveau des majuscules intermédiaires. Il faut indiquer à spacy qu’il s’agit là d’un nouveau pattern infixe du modèle.

>>> default_infixes = list(nlp.Defaults.infixes)

>>> default_infixes.append('[A-Z][a-z0-9]+')

>>> infix_regex = spacy.util.compile_infix_regex(default_infixes)

>>> nlp.tokenizer.infix_finditer = infix_regex.finditer

Toutefois, si on redécoupe notre texte tout de suite, on risque également de découper le token @SofieneAlouini en ['@', 'Sofiene', 'Alouini'], et celui-ci ne serait plus capturé par la condition ._.like_handle.

Ce comportement est prévisible, car les opérations sur le texte s’effectuent dans un ordre bien précis. L’ajout d’un pattern infixe modifie le découpage initial du texte. L’attribut ._.like_handle est un attribut du token, c’est-à-dire qu’il s’applique sur les tokens une fois le découpage réalisé.

Comment faire alors pour concilier le découpage des mots en CamelCase avec le filtrage des mentions ? Une solution possible est d’ajouter la regex décrivant les mentions comme une exception du tokenizer, avant de définir le nouveau pattern infixe :

>>> nlp.tokenizer.token_match = re.compile(handle_regex).match
 
>>> default_infixes = list(nlp.Defaults.infixes)

>>> default_infixes.append('[A-Z][a-z0-9]+')

>>> infix_regex = spacy.util.compile_infix_regex(default_infixes)

>>> nlp.tokenizer.infix_finditer = infix_regex.finditer

Les règles de précédence de spacy font que le modèle va d’abord identifier ces exceptions, ensuite faire la tokenisation (en tenant compte également des patterns infixes), et enfin appliquer les différents filtres .is_stop, ._.like_handle… En ré-appliquant le modèle mis à jour avec cette nouvelle règle, on obtient la tokenisation suivante :

>>> doc = nlp(text)

>>> [token.text for token in doc if (not token.is_punct) and (not token.is_space) and (not token.like_url) and (not token.is_stop) and len(token) > 1 and (not token.ent_type_ == "PER") and (not token._.like_handle)]

['Machine', 'Learning', 'signifie', 'passer', 'échelle', 'Machine', 'Learning', 'passe', '-t', '-il', 'PoC', 'chantier', '-ce', 'dois', 'commencer', 'réponses', 'questions']

Les exceptions de tokenisation

Poursuivons notre opération nettoyage. Les tokens -t, -il ou -ce pourraient être considérés comme des stop words. On remarque cependant qu’ils ont été maintenus dans le document, préfixés par un -.

Chaque modèle pré-entraîné vient avec son lot d’exceptions de tokenisation. Il s’agit d’un dictionnaire d’expressions auxquelles sont associés des découpages spécifiques.

>>> nlp.Defaults.tokenizer_exceptions

{
	...,
	'quelques-uns': [{65: 'quelques-uns'}],
	'rendez-vous': [{65: 'rendez-vous'}],
	...,
	'passe-t-il': [{65: 'passe'}, {65: '-t'}, {65: '-il'}],
	...
}

Des expressions comme "quelques-uns" et "rendez-vous" sont explicitement configurées pour ne pas être découpées.

L’expression "passe-t-il" qui nous intéresse est elle aussi configurée pour avoir un découpage spécial en ['passe', '-t', '-il']. De même, pour l’expression "est-ce", une règle spéciale est définie pour la découper en ["est", "-ce"]. Ces exceptions sont propres aux modèles en français, et ont été définies ainsi car jugées plus pertinentes dans la plupart des cas.

Toutefois, rien ne nous empêche de modifier ces exceptions ou d’en ajouter d’autres si l’on juge cela pertinent pour notre cas particulier. En l’occurrence, on peut surcharger ces cas d’exception, pour forcer un découpage au niveau du -. On retombe ainsi sur des stop words qui seront éliminés grâce à la propriété .is_stop, ce qui aidera à réduire la taille du vocabulaire :

>>> from spacy.attrs import ORTH

>>> nlp.tokenizer.add_special_case("passe-t-il", [{ORTH: "passe"}, {ORTH: "-"}, {ORTH: "t"}, {ORTH: "-"}, {ORTH: "il"}])

>>> nlp.tokenizer.add_special_case("est-ce", [{ORTH: "est"}, {ORTH: "-"}, {ORTH: "ce"}])

En appliquant à nouveau le nettoyage après cette modification, on remarque que les tokens ['t', 'il', 'ce'] ont bien été éliminés car la tokenisation a été corrigée, ce qui a permis de bien les détecter comme étant des stop words :

>>> doc = nlp(text)

>>> [token.text for token in doc if (not token.is_punct) and (not token.is_space) and (not token.like_url) and (not token.is_stop) and len(token) > 1 and (not token.ent_type_ == "PER") and (not token._.like_handle)]

['Machine', 'Learning', 'signifie', 'passer', 'échelle', 'Machine', 'Learning', 'passe', 'PoC', 'chantier', 'dois', 'commencer', 'réponses', 'questions']

La lemmatisation

Toujours dans le but de réduire la taille du vocabulaire, on peut se dire que les différentes “formes” d’un même mot font référence à la même notion, et qu'on souhaite donc les regrouper. Par exemple, on peut vouloir mettre à l'infinitif toutes les conjugaisons possibles d’un verbe, ou encore transformer les formes plurielles vers le singulier.

Ce type de transformation basé sur des règles linguistiques est appelé lemmatisation, et spacy permet de le faire simplement avec un modèle pré-entraîné, en utilisant l’attribut .lemma_ de la classe Token :

>>> [token.lemma_ for token in doc if (not token.is_punct) and (not token.is_space) and (not token.like_url) and (not token.is_stop) and len(token) > 1 and (not token.ent_type_ == "PER") and (not token._.like_handle)]

['machine', 'Learning', 'signifier', 'passer', 'échelle', 'Machine', 'Learning', 'passer', 'poc', 'chantier', 'devoir', 'commencer', 'réponse', 'question']

On observe les transformations suivantes :

  • signifie ⇒ signifier
  • passe ⇒ passer
  • dois ⇒ devoir
  • réponses ⇒ réponse
  • questions ⇒ question

Dans le cas de ce seul tweet, la lemmatisation a permis de réduire la taille du vocabulaire de 1, car à présent on voit apparaître 2 fois le même token passer, alors qu’on avait initialement deux tokens différents passer et passe.

La réduction de la taille du vocabulaire serait encore plus importante si l’on traitait tout un corpus, et non un document unique.

Les majuscules et les accents

Enfin, une façon de réduire encore plus le vocabulaire est d’éliminer les majuscules et les accents. Cela nous permet de gérer de petites différences au niveau de l’orthographe. La méthode str.lower() permet de passer le texte en minuscules, et le package unidecode permet d’éliminer facilement tous les accents.

On installe d’abord unidecode avec pip :

pip install unidecode

Puis on met à jour notre pipeline de tokenisation et de nettoyage de la manière suivante :

>>> from unidecode import unidecode

>>> [unidecode(token.lemma_.lower()) for token in doc if (not token.is_punct) and (not token.is_space) and (not token.like_url) and (not token.is_stop) and len(token) > 1 and (not token.ent_type_ == "PER") and (not token._.like_handle)]

['machine', 'learning', 'signifier', 'passer', 'echelle', 'machine', 'learning', 'passer', 'poc', 'chantier', 'devoir', 'commencer', 'reponse', 'question']

Grâce à cette légère modification, on a pu mutualiser les tokens Machine et machine, mais encore une fois, l’effet de réduction du vocabulaire serait encore plus flagrant si l’on traitait un corpus entier de tweets plutôt qu’un seul. C’est d’ailleurs l’objet du dernier chapitre !

Nettoyer un corpus entier

Ayant identifié les principales étapes de nettoyage d’un tweet, on peut généraliser ce traitement à tout un corpus. Commençons par créer une fonction qui applique les filtres et transformations vus plus haut :

def clean(doc: spacy.tokens.doc.Doc) -> list[str]:
    return [
        unidecode(token.lemma_.lower()) for token in doc 
        if (not token.is_punct) 
        and (not token.is_space) 
        and (not token.like_url) 
        and (not token.is_stop) 
        and len(token) > 1 
        and (not token.ent_type_ == "PER") 
        and (not token._.like_handle)
    ]

Prenons ensuite comme exemple le corpus suivant:

>>> text_1 = """[#MachineLearning] 🧠 Que signifie “passer à l’échelle” quand on fait du Machine Learning ? Que se passe-t-il après le PoC ? Par quel chantier est-ce que je dois commencer ?
 
💡Toutes les réponses à vos questions avec @SofieneAlouini et Mehdi ! 
 
➡️ https://bit.ly/3Gr7XXa"""
 
>>> text_2 = """[#LeComptoir] Quels sont les artefacts à gérer dans le cadre d'un projet de #MachineLearning ?
 
🏃‍♀️ Le service d'entraînement
💪 Le service d'inférence
🖥 Le modèle
💡 La configuration
📉 La donnée
🤔 L'infrastructure
 
#OCTOEvents"""
 
>>> text_3 = """[#MachineLearning] Pourquoi est-ce encore plus important de bien maîtriser l’expérimentation dans un contexte de Machine Learning ? 🤔
 
💡 Notre experte Capucine vous explique tout dans son nouvel article
 
➡️ https://bit.ly/3vEEkA2"""
 
>>> texts = [text_1, text_2, text_3]

Afin de nettoyer ce corpus, on peut appliquer le modèle nlp à tous les textes avec la méthode nlp.pipe(). Contrairement à une boucle for où on appliquerait nlp() à chaque texte individuellement, l’utilisation de nlp.pipe() est conseillée car elle permet d’appliquer le modèle à un batch de textes, ce qui réduit le temps de traitement d’un corpus volumineux.

>>> docs = nlp.pipe(texts)

>>> tokens = []

>>> for doc in docs:
... 	tokens.append(clean(doc))

>>> tokens

[['machine', 'learning', 'signifier', 'passer', 'echelle', 'machine', 'learning', 'passer', 'poc', 'chantier', 'devoir', 'commencer', 'reponse', 'question'], ['comptoir', 'artefact', 'gerer', 'cadre', 'projet', 'machine', 'learning', 'service', 'entrainement', 'service', 'inference', 'modele', 'configuration', 'donnee', 'infrastructure', 'octo', 'event'], ['machine', 'learning', 'important', 'bien', 'maitriser', 'experimentation', 'contexte', 'machine', 'learning', 'experte', 'explique', 'nouveau', 'article']]

Conclusion

Récapitulons tout ce qui a été expliqué dans cet article.

Les dépendances

On commence par installer les dépendances spacy et unidecode:

pip install unidecode
pip install spacy
python -m spacy download fr_core_news_lg

Le nettoyage du texte

Voici l’ensemble des étapes de nettoyage regroupées en un seul script Python :

# Import des dépendances
import re
import spacy
from spacy.tokens import Token
from spacy.attrs import ORTH
from unidecode import unidecode
from itertools import chain
 
# Corpus de textes à nettoyer
text_1 = """[#MachineLearning] 🧠 Que signifie “passer à l’échelle” quand on fait du Machine Learning ? Que se passe-t-il après le PoC ? Par quel chantier est-ce que je dois commencer ?
 
💡Toutes les réponses à vos questions avec @SofieneAlouini et Mehdi ! 
 
➡️ https://bit.ly/3Gr7XXa"""
 
text_2 = """[#LeComptoir] Quels sont les artefacts à gérer dans le cadre d'un projet de #MachineLearning ?
 
🏃‍♀️ Le service d'entraînement
💪 Le service d'inférence
🖥 Le modèle
💡 La configuration
📉 La donnée
🤔 L'infrastructure
 
#OCTOEvents"""
 
text_3 = """[#MachineLearning] Pourquoi est-ce encore plus important de bien maîtriser l’expérimentation dans un contexte de Machine Learning ? 🤔
 
💡 Notre experte Capucine vous explique tout dans son nouvel article
 
➡️ https://bit.ly/3vEEkA2"""
 
texts = [text_1, text_2, text_3]
 
# Chargement du modèle pré-entraîné
nlp = spacy.load("fr_core_news_lg")
 
# Création de l'attribut personnalisé pour les mentions
handle_regex = r"@[\w\d_]+"
like_handle = lambda token: re.fullmatch(handle_regex, token.text)
Token.set_extension("like_handle", getter=like_handle)
 
# Ajout des mentions comme exceptions à ignorer lors de la recherche de patterns infixe
nlp.tokenizer.token_match = re.compile(r"@[\w\d_]+").match
 
# Ajout d'un pattern infixe pour découper les mots écrits en CamelCase
default_infixes = list(nlp.Defaults.infixes)
default_infixes.append('[A-Z][a-z0-9]+')
infix_regex = spacy.util.compile_infix_regex(default_infixes)
nlp.tokenizer.infix_finditer = infix_regex.finditer
 
# Surcharge explicite de certaines exceptions de tokenisation
nlp.tokenizer.add_special_case("passe-t-il", [{ORTH: "passe"}, {ORTH: "-"}, {ORTH: "t"}, {ORTH: "-"}, {ORTH: "il"}])
nlp.tokenizer.add_special_case("est-ce", [{ORTH: "est"}, {ORTH: "-"}, {ORTH: "ce"}])

# Définition de la fonction de nettoyage
def clean(doc: spacy.tokens.doc.Doc) -> list[str]:
    return [
        unidecode(token.lemma_.lower()) for token in doc 
        if (not token.is_punct) 
        and (not token.is_space) 
        and (not token.like_url) 
        and (not token.is_stop) 
        and len(token) > 1 
        and (not token.ent_type_ == "PER") 
        and (not token._.like_handle)
    ]

# Tokenisation et nettoyage du corpus
docs = nlp.pipe(texts)
tokens = []
for doc in docs:
    tokens.append(clean(doc))
 
# Affichage des résultats et de la taille du vocabulaire
for i, tokenized_text in enumerate(tokens):
    print(f"TEXTE {i + 1}:", tokenized_text, "\n")
 
print("TAILLE DU VOCABULAIRE:", len(set(chain(*tokens))))

En exécutant ce script, on obtient les résultats suivants:

TEXTE 1: ['machine', 'learning', 'signifier', 'passer', 'echelle', 'machine', 'learning', 'passer', 'poc', 'chantier', 'devoir', 'commencer', 'reponse', 'question']

TEXTE 2: ['comptoir', 'artefact', 'gerer', 'cadre', 'projet', 'machine', 'learning', 'service', 'entrainement', 'service', 'inference', 'modele', 'configuration', 'donnee', 'infrastructure', 'octo', 'event']

TEXTE 3: ['machine', 'learning', 'important', 'bien', 'maitriser', 'experimentation', 'contexte', 'machine', 'learning', 'experte', 'explique', 'nouveau', 'article']

TAILLE DU VOCABULAIRE: 34

À la lecture de ces résultats, les thématiques abordées dans les tweets restent clairement identifiables, même après un nettoyage important. Le plus intéressant est que la taille du vocabulaire a été réduite à 34 tokens distincts seulement. Dans une représentation Bag-of-words, chaque tweet serait donc représenté par un vecteur de taille 34.

À titre de comparaison, ce deuxième script permet d’effectuer une tokenisation simple (sans tous les filtres et toutes les règles qu’on a ajoutés) :

# Import des dépendances
import re
import spacy
from spacy.tokens import Token
from spacy.attrs import ORTH
from unidecode import unidecode
from itertools import chain

# Corpus de textes à nettoyer
text_1 = """[#MachineLearning] 🧠 Que signifie “passer à l’échelle” quand on fait du Machine Learning ? Que se passe-t-il après le PoC ? Par quel chantier est-ce que je dois commencer ?

💡Toutes les réponses à vos questions avec @SofieneAlouini et Mehdi !

➡️ https://bit.ly/3Gr7XXa"""

text_2 = """[#LeComptoir] Quels sont les artefacts à gérer dans le cadre d'un projet de #MachineLearning ?

🏃‍♀️ Le service d'entraînement
💪 Le service d'inférence
🖥 Le modèle
💡 La configuration
📉 La donnée
🤔 L'infrastructure

#OCTOEvents"""

text_3 = """[#MachineLearning] Pourquoi est-ce encore plus important de bien maîtriser l’expérimentation dans un contexte de Machine Learning ? 🤔

💡 Notre experte Capucine vous explique tout dans son nouvel article

➡️ https://bit.ly/3vEEkA2"""

texts = [text_1, text_2, text_3]

# Chargement du modèle pré-entraîné
nlp = spacy.load("fr_core_news_lg")

# Tokenisation et nettoyage du corpus
docs = nlp.pipe(texts)
tokens = []
for doc in docs:
    tokens.append([token.text for token in doc])

# Affichage des résultats et de la taille du vocabulaire
for i, tokenized_text in enumerate(tokens):
    print(f"TEXTE {i + 1}:", tokenized_text, "\n")

print("TAILLE DU VOCABULAIRE:", len(set(chain(*tokens))))

En l’exécutant, on obtient les résultats suivants :

TEXTE 1: ['[', '#', 'MachineLearning', ']', '🧠', 'Que', 'signifie', '“', 'passer', 'à', 'l’', 'échelle', '”', 'quand', 'on', 'fait', 'du', 'Machine', 'Learning', '?', 'Que', 'se', 'passe', '-t', '-il', 'après', 'le', 'PoC', '?', 'Par', 'quel', 'chantier', 'est', '-ce', 'que', 'je', 'dois', 'commencer', '?', '\n \n', '💡', 'Toutes', 'les', 'réponses', 'à', 'vos', 'questions', 'avec', '@SofieneAlouini', 'et', 'Mehdi', '!', '\n \n', '➡', '️', 'https://bit.ly/3Gr7XXa']

TEXTE 2: ['[', '#', 'LeComptoir', ']', 'Quels', 'sont', 'les', 'artefacts', 'à', 'gérer', 'dans', 'le', 'cadre', "d'", 'un', 'projet', 'de', '#', 'MachineLearning', '?', '\n \n', '🏃', '\u200d', '♀', '️', 'Le', 'service', "d'", 'entraînement', '\n', '💪', 'Le', 'service', "d'", 'inférence', '\n', '🖥', 'Le', 'modèle', '\n', '💡', 'La', 'configuration', '\n', '📉', 'La', 'donnée', '\n', '🤔', "L'", 'infrastructure', '\n \n', '#', 'OCTOEvents']

TEXTE 3: ['[', '#', 'MachineLearning', ']', 'Pourquoi', 'est', '-ce', 'encore', 'plus', 'important', 'de', 'bien', 'maîtriser', 'l’', 'expérimentation', 'dans', 'un', 'contexte', 'de', 'Machine', 'Learning', '?', '🤔', '\n \n', '💡', 'Notre', 'experte', 'Capucine', 'vous', 'explique', 'tout', 'dans', 'son', 'nouvel', 'article', '\n \n', '➡', '️', 'https://bit.ly/3vEEkA2']

TAILLE DU VOCABULAIRE: 99

Les traitements implémentés nous ont permis de réduire la taille du vocabulaire de ce petit corpus de presque 66%, ce qui simplifie grandement les représentations Bag-of-words qui suivent, et a fortiori les modèles qui vont être entraînés dessus.

En conclusion, le nettoyage de texte est une étape essentielle dans les projets de NLP, surtout quand il s’agit d’analyse de contenu de réseaux sociaux dont la qualité est très variable. Cette étape vise à réduire le vocabulaire du corpus, afin de diminuer la quantité de bruit, dans le but de simplifier l’analyse et l’entraînement des modèles.

Attention toutefois à choisir les bonnes étapes de nettoyage selon le problème que l’on cherche à résoudre ! À titre d'exemple, dans cet article, nous avons présenté une façon de supprimer les entités nommées de type PER, car on cherche à identifier les thématiques des tweets et non les individus. Pour d’autres usages, ce sont précisément les entités PER qu’on voudra conserver, pour suivre par exemple des tendances électorales.

Nous avons voulu montrer un large éventail de techniques possibles pour nettoyer du texte, qui apportent beaucoup de valeur si l’on souhaite faire du Machine Learning “classique” par la suite. D’autres approches plus récentes, basées sur du Deep Learning, accordent moins d’importance à ce nettoyage exhaustif, et se contentent de tokeniser le texte en subwords, puis de laisser la magie du Deep Learning opérer. L’apprentissage des modèles est alors moins dépendant des pré-traitements, mais demande en contrepartie un volume de données et des ressources de calcul beaucoup plus importants pour obtenir de bons résultats, surtout si on s’intéresse à un domaine de connaissances particulier.

Dans les prochains articles, nous reviendrons sur les différentes modélisations possibles pour faire de l’analyse de tendances sur les réseaux sociaux.