Quelques limites du Traitement Automatique des Langues (NLP)

1 Introduction

Avec le developpement des assistants vocaux et chatbots, on pourrait croire que le traitement automatique des langues (ou NLP, “Natural Language Processing”) est une technologie desormais efficace et rapide a mettre en oeuvre. De nombreux articles de blog mettent d’ailleurs en avant les cas d’usage reussis du NLP. Il y a pourtant encore beaucoup de situations frustrantes pour le DataScientist qui s’initie a cette discipline. On expose dans les paragraphes suivants quelques-une de ces situations et de potentielles solutions, en utilisant deux des modules les plus frequemment utilises pour le NLP sous Python, a savoir nltk et spacy.

On peut executer la suite de commandes ci-dessous dans Anaconda Prompt pour creer un environnement virtuel conda et executer les codes Python presents dans ce document (la version imposee au module wasabi permet d’eviter un bug lors de l’utilisation de spacy).

conda create --name env_spacy
conda activate env_spacy
conda install -c conda-forge wasabi=0.9.1 spacy spacy-model-fr_core_news_md python-levenshtein
conda install -c anaconda nltk scipy
pip install textblob-fr vaderSentiment_fr

Enfin on charge les modules utiles.

import spacy
import nltk

# on charge le modele francais de taille moyenne
nlp = spacy.load("fr_core_news_md")

from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
from nltk.tokenize import RegexpTokenizer
# pour la racinisation
from nltk.stem.snowball import FrenchStemmer
stemmer = FrenchStemmer()

nltk.download('stopwords');nltk.download('punkt')
spacy.__version__, nltk.__version__
('3.4.2', '3.7')
from scipy.spatial.distance import cosine
# pour la liste des symboles de ponctuation
import string
# pour supprimer les accents
import unicodedata
# pour la correction des fautes d'orthographe
# https://maxbachmann.github.io/Levenshtein/levenshtein.html#distance
from Levenshtein import distance
## modules d'analyse de sentiments

# la version francaise date de 2021
# https://pypi.org/project/vaderSentiment-fr
from vaderSentiment_fr.vaderSentiment import SentimentIntensityAnalyzer
SIA = SentimentIntensityAnalyzer()

# la version francaise date de 2013
# https://github.com/sloria/textblob-fr
# https://textblob.readthedocs.io/en/dev/quickstart.html#sentiment-analysis
from textblob import Blobber
from textblob_fr import PatternTagger, PatternAnalyzer
tb = Blobber(pos_tagger=PatternTagger(), analyzer=PatternAnalyzer())

2 Nettoyage de textes

Une des approches classiques consiste a “nettoyer” le texte pour faciliter son analyse (par exemple la determination des mots les plus frequents et le nuage de mots correspondant). Ce nettoyage s’effectue en plusieurs etapes :

  • la tokenisation consiste a decouper le texte en sous-unites, le plus souvent en mots, auxquelles on applique les traitements qui suivent
  • la suppression des mots vides ou “stopwords”, des mots consideres comme peu informatifs : la, le, du, pas, …
  • la normalisation des mots, pour reconnaitre comme identiques des mots de la meme famille : leger/legere, etait/sera, …
    • la racinisation ou “stemmatisation” est la version la plus rapide et brutale
    • la lemmatisation est plus longue a s’executer mais plus fine
  • la correction des fautes d’orthographe est aussi une option a envisager

2.1 Tokenisation

La tokenisation transforme la phrase en une liste de mots et ponctuations.

x = "La recette d'un classique : le gratin de chou-fleur (4 personnes)"
word_tokenize(x, language = 'french')
['La', 'recette', "d'un", 'classique', ':', 'le', 'gratin', 'de', 'chou-fleur', '(', '4', 'personnes', ')']

Avec nltk, meme en precisant la langue, on voit que l’apostrophe n’est pas geree correctement.

' * '.join(word_tokenize(x, language = 'french'))
"La * recette * d'un * classique * : * le * gratin * de * chou-fleur * ( * 4 * personnes * )"

Une approche par expression reguliere regle ce probleme, mais cette fois c’est le trait d’union qui n’est pas bien gere.

tokenizer = RegexpTokenizer("[\w]+")
' * '.join(tokenizer.tokenize(x))
'La * recette * d * un * classique * le * gratin * de * chou * fleur * 4 * personnes'

On peut tout simplement ajouter un tiret dans la classe qui definit nos mots.

tokenizer = RegexpTokenizer("[\w-]+")
' * '.join(tokenizer.tokenize(x))
'La * recette * d * un * classique * le * gratin * de * chou-fleur * 4 * personnes'

On peut aussi specifier tout ce qui ne fait pas partie des mots admissibles : les symboles de ponctuation (hors tiret), les chiffres, les espaces, et on peut imposer des mots d’au moins 2 caracteres.

tokenizer = RegexpTokenizer("[^"+string.punctuation.replace("-", "")+"\d\s]{2,}")
' * '.join(tokenizer.tokenize(x))
'La * recette * un * classique * le * gratin * de * chou-fleur * personnes'

Avec spacy le traitement initial fonctionne bien mais il reste les ponctuations.

' * '.join([token.text for token in nlp(x)])
"La * recette * d' * un * classique * : * le * gratin * de * chou-fleur * ( * 4 * personnes * )"

On peut les enlever facilement car spacy permet d’identifier les differentes caracteristiques d’un mot : ponctuation, verbe, url, emoji, …

' * '.join([token.text for token in nlp(x) if not token.is_punct])
"La * recette * d' * un * classique * le * gratin * de * chou-fleur * 4 * personnes"

2.2 Suppression des mots vides

Avec nltk la liste des mots vides est minimale, on peut vouloir y ajouter des lettres suivies d’apostrophes.

# nltk
stop = sorted((stopwords.words('french')))
stop
['ai', 'aie', 'aient', 'aies', 'ait', 'as', 'au', 'aura', 'aurai', 'auraient', 'aurais', 'aurait', 'auras', 'aurez', 'auriez', 'aurions', 'aurons', 'auront', 'aux', 'avaient', 'avais', 'avait', 'avec', 'avez', 'aviez', 'avions', 'avons', 'ayant', 'ayante', 'ayantes', 'ayants', 'ayez', 'ayons', 'c', 'ce', 'ces', 'd', 'dans', 'de', 'des', 'du', 'elle', 'en', 'es', 'est', 'et', 'eu', 'eue', 'eues', 'eurent', 'eus', 'eusse', 'eussent', 'eusses', 'eussiez', 'eussions', 'eut', 'eux', 'eûmes', 'eût', 'eûtes', 'furent', 'fus', 'fusse', 'fussent', 'fusses', 'fussiez', 'fussions', 'fut', 'fûmes', 'fût', 'fûtes', 'il', 'ils', 'j', 'je', 'l', 'la', 'le', 'les', 'leur', 'lui', 'm', 'ma', 'mais', 'me', 'mes', 'moi', 'mon', 'même', 'n', 'ne', 'nos', 'notre', 'nous', 'on', 'ont', 'ou', 'par', 'pas', 'pour', 'qu', 'que', 'qui', 's', 'sa', 'se', 'sera', 'serai', 'seraient', 'serais', 'serait', 'seras', 'serez', 'seriez', 'serions', 'serons', 'seront', 'ses', 'soient', 'sois', 'soit', 'sommes', 'son', 'sont', 'soyez', 'soyons', 'suis', 'sur', 't', 'ta', 'te', 'tes', 'toi', 'ton', 'tu', 'un', 'une', 'vos', 'votre', 'vous', 'y', 'à', 'étaient', 'étais', 'était', 'étant', 'étante', 'étantes', 'étants', 'étiez', 'étions', 'été', 'étée', 'étées', 'étés', 'êtes']
other_stop = ["a", "c'", "d'", "j'", "l'", "m'", "n'", "qu'", "s'", "t'"]
stop.extend(other_stop)
stop = sorted(stop)
stop
['a', 'ai', 'aie', 'aient', 'aies', 'ait', 'as', 'au', 'aura', 'aurai', 'auraient', 'aurais', 'aurait', 'auras', 'aurez', 'auriez', 'aurions', 'aurons', 'auront', 'aux', 'avaient', 'avais', 'avait', 'avec', 'avez', 'aviez', 'avions', 'avons', 'ayant', 'ayante', 'ayantes', 'ayants', 'ayez', 'ayons', 'c', "c'", 'ce', 'ces', 'd', "d'", 'dans', 'de', 'des', 'du', 'elle', 'en', 'es', 'est', 'et', 'eu', 'eue', 'eues', 'eurent', 'eus', 'eusse', 'eussent', 'eusses', 'eussiez', 'eussions', 'eut', 'eux', 'eûmes', 'eût', 'eûtes', 'furent', 'fus', 'fusse', 'fussent', 'fusses', 'fussiez', 'fussions', 'fut', 'fûmes', 'fût', 'fûtes', 'il', 'ils', 'j', "j'", 'je', 'l', "l'", 'la', 'le', 'les', 'leur', 'lui', 'm', "m'", 'ma', 'mais', 'me', 'mes', 'moi', 'mon', 'même', 'n', "n'", 'ne', 'nos', 'notre', 'nous', 'on', 'ont', 'ou', 'par', 'pas', 'pour', 'qu', "qu'", 'que', 'qui', 's', "s'", 'sa', 'se', 'sera', 'serai', 'seraient', 'serais', 'serait', 'seras', 'serez', 'seriez', 'serions', 'serons', 'seront', 'ses', 'soient', 'sois', 'soit', 'sommes', 'son', 'sont', 'soyez', 'soyons', 'suis', 'sur', 't', "t'", 'ta', 'te', 'tes', 'toi', 'ton', 'tu', 'un', 'une', 'vos', 'votre', 'vous', 'y', 'à', 'étaient', 'étais', 'était', 'étant', 'étante', 'étantes', 'étants', 'étiez', 'étions', 'été', 'étée', 'étées', 'étés', 'êtes']

Avec spacy on peut trouver la liste trop longue ! En particulier le mot “nul” pourrait etre conserve si on veut detecter de l’insatisfaction client par exemple.

stopwords_spacy = sorted(nlp.Defaults.stop_words)
stopwords_spacy
['a', 'abord', 'afin', 'ah', 'ai', 'aie', 'ainsi', 'ait', 'allaient', 'allons', 'alors', 'anterieur', 'anterieure', 'anterieures', 'antérieur', 'antérieure', 'antérieures', 'apres', 'après', 'as', 'assez', 'attendu', 'au', 'aupres', 'auquel', 'aura', 'auraient', 'aurait', 'auront', 'aussi', 'autre', 'autrement', 'autres', 'autrui', 'aux', 'auxquelles', 'auxquels', 'avaient', 'avais', 'avait', 'avant', 'avec', 'avoir', 'avons', 'ayant', 'bas', 'basee', 'bat', "c'", 'car', 'ce', 'ceci', 'cela', 'celle', 'celle-ci', 'celle-la', 'celle-là', 'celles', 'celles-ci', 'celles-la', 'celles-là', 'celui', 'celui-ci', 'celui-la', 'celui-là', 'cent', 'cependant', 'certain', 'certaine', 'certaines', 'certains', 'certes', 'ces', 'cet', 'cette', 'ceux', 'ceux-ci', 'ceux-là', 'chacun', 'chacune', 'chaque', 'chez', 'ci', 'cinq', 'cinquantaine', 'cinquante', 'cinquantième', 'cinquième', 'combien', 'comme', 'comment', 'compris', 'concernant', 'c’', "d'", 'da', 'dans', 'de', 'debout', 'dedans', 'dehors', 'deja', 'dejà', 'delà', 'depuis', 'derriere', 'derrière', 'des', 'desormais', 'desquelles', 'desquels', 'dessous', 'dessus', 'deux', 'deuxième', 'deuxièmement', 'devant', 'devers', 'devra', 'different', 'differente', 'differentes', 'differents', 'différent', 'différente', 'différentes', 'différents', 'dire', 'directe', 'directement', 'dit', 'dite', 'dits', 'divers', 'diverse', 'diverses', 'dix', 'dix-huit', 'dix-neuf', 'dix-sept', 'dixième', 'doit', 'doivent', 'donc', 'dont', 'douze', 'douzième', 'du', 'duquel', 'durant', 'dès', 'déja', 'déjà', 'désormais', 'd’', 'effet', 'egalement', 'eh', 'elle', 'elle-meme', 'elle-même', 'elles', 'elles-memes', 'elles-mêmes', 'en', 'encore', 'enfin', 'entre', 'envers', 'environ', 'es', 'est', 'et', 'etaient', 'etais', 'etait', 'etant', 'etc', 'etre', 'eu', 'eux', 'eux-mêmes', 'exactement', 'excepté', 'facon', 'fais', 'faisaient', 'faisant', 'fait', 'façon', 'feront', 'font', 'gens', 'ha', 'hem', 'hep', 'hi', 'ho', 'hormis', 'hors', 'hou', 'houp', 'hue', 'hui', 'huit', 'huitième', 'hé', 'i', 'il', 'ils', 'importe', "j'", 'je', 'jusqu', 'jusque', 'juste', 'j’', "l'", 'la', 'laisser', 'laquelle', 'le', 'lequel', 'les', 'lesquelles', 'lesquels', 'leur', 'leurs', 'longtemps', 'lors', 'lorsque', 'lui', 'lui-meme', 'lui-même', 'là', 'lès', 'l’', "m'", 'ma', 'maint', 'maintenant', 'mais', 'malgre', 'malgré', 'me', 'meme', 'memes', 'merci', 'mes', 'mien', 'mienne', 'miennes', 'miens', 'mille', 'moi', 'moi-meme', 'moi-même', 'moindres', 'moins', 'mon', 'même', 'mêmes', 'm’', "n'", 'na', 'ne', 'neanmoins', 'neuvième', 'ni', 'nombreuses', 'nombreux', 'nos', 'notamment', 'notre', 'nous', 'nous-mêmes', 'nouveau', 'nul', 'néanmoins', 'nôtre', 'nôtres', 'n’', 'o', 'on', 'ont', 'onze', 'onzième', 'or', 'ou', 'ouias', 'ouste', 'outre', 'ouvert', 'ouverte', 'ouverts', 'où', 'par', 'parce', 'parfois', 'parle', 'parlent', 'parler', 'parmi', 'partant', 'pas', 'pendant', 'pense', 'permet', 'personne', 'peu', 'peut', 'peuvent', 'peux', 'plus', 'plusieurs', 'plutot', 'plutôt', 'possible', 'possibles', 'pour', 'pourquoi', 'pourrais', 'pourrait', 'pouvait', 'prealable', 'precisement', 'premier', 'première', 'premièrement', 'pres', 'procedant', 'proche', 'près', 'préalable', 'précisement', 'pu', 'puis', 'puisque', "qu'", 'quand', 'quant', 'quant-à-soi', 'quarante', 'quatorze', 'quatre', 'quatre-vingt', 'quatrième', 'quatrièmement', 'que', 'quel', 'quelconque', 'quelle', 'quelles', "quelqu'un", 'quelque', 'quelques', 'quels', 'qui', 'quiconque', 'quinze', 'quoi', 'quoique', 'qu’', 'relative', 'relativement', 'rend', 'rendre', 'restant', 'reste', 'restent', 'retour', 'revoici', 'revoila', 'revoilà', "s'", 'sa', 'sait', 'sans', 'sauf', 'se', 'seize', 'selon', 'semblable', 'semblaient', 'semble', 'semblent', 'sent', 'sept', 'septième', 'sera', 'seraient', 'serait', 'seront', 'ses', 'seul', 'seule', 'seulement', 'seules', 'seuls', 'si', 'sien', 'sienne', 'siennes', 'siens', 'sinon', 'six', 'sixième', 'soi', 'soi-meme', 'soi-même', 'soit', 'soixante', 'son', 'sont', 'sous', 'souvent', 'specifique', 'specifiques', 'spécifique', 'spécifiques', 'stop', 'suffisant', 'suffisante', 'suffit', 'suis', 'suit', 'suivant', 'suivante', 'suivantes', 'suivants', 'suivre', 'sur', 'surtout', 's’', "t'", 'ta', 'tant', 'te', 'tel', 'telle', 'tellement', 'telles', 'tels', 'tenant', 'tend', 'tenir', 'tente', 'tes', 'tien', 'tienne', 'tiennes', 'tiens', 'toi', 'toi-meme', 'toi-même', 'ton', 'touchant', 'toujours', 'tous', 'tout', 'toute', 'toutes', 'treize', 'trente', 'tres', 'trois', 'troisième', 'troisièmement', 'très', 'tu', 'té', 't’', 'un', 'une', 'unes', 'uns', 'va', 'vais', 'vas', 'vers', 'via', 'vingt', 'voici', 'voila', 'voilà', 'vont', 'vos', 'votre', 'votres', 'vous', 'vous-mêmes', 'vu', 'vé', 'vôtre', 'vôtres', 'y', 'à', 'â', 'ça', 'ès', 'également', 'étaient', 'étais', 'était', 'étant', 'être', 'ô']

Et on peut regrouper toutes les operations dans une fonction, sans oublier de tout mettre en minuscules.

def nettoyage(x, tokenizer = RegexpTokenizer("[\w-]+")): 
  return ' '.join([word for word in tokenizer.tokenize(x) if word not in stop])
nettoyage(x.lower())
'recette classique gratin chou-fleur 4 personnes'
def nettoyage_spacy(x): 
  return ' '.join([token.text for token in nlp(x) if not token.is_punct and token.text not in stop])
nettoyage_spacy(x.lower())
'recette classique gratin chou-fleur 4 personnes'

On pourrait aussi ajouter a la liste des mots vides sa version sans accents, a l’aide de la fonction suivante :

def simplify(text):
    try:
        text = unicode(text, 'utf-8')
    except NameError:
        pass
    text = unicodedata.normalize('NFD', text).encode('ascii', 'ignore').decode("utf-8")
    return str(text)
print(simplify('çàéêö@ôhello'))
caeeo@ohello

2.3 Racinisation et lemmatisation

2.3.1 Racinisation

La racinisation est plutot deconseillee : on obtient en general des mots qui n’existent pas en francais, ce qui peut gener des traitements ulterieurs. On peut meme ne pas reconnaitre que deux mots font partie de la meme famille car leurs racines peuvent etre differentes.

stemmer.stem("satisfaction"), stemmer.stem("satisfait")
('satisfact', 'satisf')

L’exemple suivant avec nltk montre qu’on peut meme obtenir une racine avec un sens different du mot initial.

stemmer.stem("amerique")
'amer'

2.3.2 Lemmatisation

La lemmatisation permet de normaliser les noms et adjectifs au masculin singulier, les verbes a l’infinitif, …

[token.lemma_ for token in nlp("Les nouvelles étudiantes sont motivées")]
['le', 'nouveau', 'étudiant', 'être', 'motiver']

La lemmatisation peut rencontrer les memes difficultes que la racinisation : avec des mots de la meme famille mais qui ont des natures differentes, on obtient des lemmes differents.

[(token.text, token.pos_, token.lemma_) for token in nlp("satisfaction satisfait")]
[('satisfaction', 'NOUN', 'satisfaction'), ('satisfait', 'ADJ', 'satisfait')]

On doit aussi etre attentif a appliquer la lemmatisation au texte initial, avant toute transfomation ou suppression de certains mots, sous peine de voir des absurdites.

Ici le mot “amerique” tout seul est pris pour un adjectif et il est mis au singulier …

[(token.lemma_ , token.pos_) for token in nlp("amerique")]
[('ameriqu', 'ADJ')]

Dans une phrase entiere tout se passe mieux.

[token.lemma_ for token in nlp("Il était une fois en Amérique")]
['il', 'être', 'un', 'fois', 'en', 'Amérique']

Mais l’absence d’un accent peut aussi causer des problemes.

[token.lemma_  for token in nlp("Il etait une fois en amerique")]
['il', 'eter', 'un', 'fois', 'en', 'amerique']

On peut enfin avoir une phrase simple et correcte mais une lemmatisation aberrante.

[token.lemma_ for token in nlp("je suis patient")]
['je', 'être', 'patier']

2.4 Corrections manuelles

On l’a vu, les techniques de nettoyage et de normalisation du texte ont quelquefois des rates. Il y a aussi les problemes poses par les sigles comme “HLM” ou des abreviations telles que “rdv”. On peut corriger certaines de ces erreurs manuellement et apres coup en utilisant un dictionnaire.

dico_corrections = {'patier' : 'patient',
                    'eter' : 'être',
                    'rdv': 'rendez-vous'}

Une premiere approche trop simple ne convient pas : des que le dictionnaire s’applique a un mot qui ne fait pas partie de ses cles, une erreur se declenche.

def nettoyage(x, dico = dico_corrections, mots_vides = stop, tokenizer = RegexpTokenizer("[\w-]+")): 
  return ' '.join([dico[token.lemma_] for token in nlp(x) if not token.is_punct and token.text not in stop])
y = "Il etait pour une fois en retard au rdv, mais je suis patient"
try:
  nettoyage(y)
except Exception as e:
  e
KeyError('il')

Il faut creer une classe speciale de dictionnaire qui, si on l’applique a un mot qui n’est pas une cle, renvoie le mot.

class smart_dict(dict):
    @staticmethod
    def __missing__(key):
        return key
    
smart_dico = smart_dict()
 
for cle, valeur in dico_corrections.items():
  smart_dico[cle] = valeur
smart_dico
{'patier': 'patient', 'eter': 'être', 'rdv': 'rendez-vous'}
smart_dico["Il"]
'Il'
y;nettoyage(y, dico = smart_dico)
'Il etait pour une fois en retard au rdv, mais je suis patient'
'il être fois retard rendez-vous patient'

2.5 Fautes d’orthographe

On peut utiliser la distance de Levenshtein pour corriger les erreurs d’orthographe, en suivant par exemple la methode suivante :

  • on se procure un lexique de la langue francaise le plus complet possible, par exemple http://www.lexique.org/
  • on controle si le mot w qu’on veut traiter est dans le lexique
  • si ce n’est pas le cas, on cherche dans le lexique les mots a une distance faible de w

Mais il faut etre prudent car la correction peut etre erronee : ainsi exonère et exagère sont a distance 2 mais exonère n’est pas dans le lexique indique plus haut, il sera donc pris pour un mot mal orthographie et sera remplace a tort par exagere.

distance("exonère", "exagère")
2

Une regle de bon sens peut etre de n’accepter qu’une erreur pour des mots courts et deux erreurs pour des mots longs, ce qui reduit le risque de remplacer un mot correct absent du lexique par un autre.

3 Analyse de sentiments

L’analyse de sentiments avec deux modules differents donne des resultats parfois convaincants, parfois beaucoup moins. Dans les deux cas les algorithmes s’appuient sur des dictionnaires et des regles (cf https://github.com/sloria/TextBlob/issues/344 et https://github.com/cjhutto/vaderSentiment). Les exemples ci-dessous montrent que meme sur des phrases limitees a un mot, le sentiment mesure par l’algorithme peut etre tout a fait errone.

3.1 Avec VADER

SIA.polarity_scores("Quelle belle matinée")
{'neg': 0.0, 'neu': 0.541, 'pos': 0.459, 'compound': 0.1779}
SIA.polarity_scores("Quelle journée horrible")
{'neg': 0.459, 'neu': 0.541, 'pos': 0.0, 'compound': -0.1779}

La “nouveaute” est jugee negativement, ce qui est tres discutable.

SIA.polarity_scores("Une nouvelle journée commence !")
{'neg': 0.272, 'neu': 0.728, 'pos': 0.0, 'compound': -0.126}
SIA.polarity_scores("nouveau")
{'neg': 1.0, 'neu': 0.0, 'pos': 0.0, 'compound': -0.0516}

La phrase suivante est correctement jugee negativement.

SIA.polarity_scores("Ce spectacle est vraiment décevant")
{'neg': 0.444, 'neu': 0.556, 'pos': 0.0, 'compound': -0.4939}

3.2 Avec TextBlob

Les deux composantes du couple ‘sentiment’ sont la polarite entre -1 et 1 et la subjectivite entre 0 et 1 si on garde l’analyseur par defaut, cf https://textblob.readthedocs.io/en/dev/advanced_usage.html#sentiment-analyzers

blob1 = tb("Quelle belle matinée")
blob1.sentiment
(0.8, 0.8)
blob2 = tb("Quelle journée horrible")
blob2.sentiment
(-1.0, 1.0)

Cette fois la nouveaute est jugee positivement.

tb("Une nouvelle journée commence !").sentiment
(0.125, 0.25)

Par contre cette phrase est anormalement jugee neutre.

tb("Ce spectacle est vraiment décevant").sentiment
(0.0, 0.5)

4 Vectorisation de mots

4.1 Generalites

La documentation de spacy sur le sujet : https://spacy.io/usage/linguistic-features#vectors-similarity

Cette approche utilise des reseaux de neurones pre-entraines qui associent a chaque mot un vecteur fixe de longueur 300. La couche “Embedding” des reseaux de neurones donne son nom a cette technique de “word embedding”.

Une phrase va etre remplacee par la moyenne des vecteurs associes a ses mots, et on peut ensuite appliquer toutes les techniques usuelles a ces vecteurs, par exemple une classification par Kmeans. C’est une approche a la “bag of words” ou “sac de mots” puisque l’ordre des mots dans la phrase n’est pas pris en compte.

Un exemple de vecteur.

nlp("je").vector[:5]
array([  6.7919,  -3.3221,  -6.2118, -12.733 ,  17.073 ], dtype=float32)
len(nlp("je").vector)
300

On verifie que le vecteur d’une phrase est bien la moyenne des vecteurs de ses mots.

(nlp("je").vector[:5] + nlp("suis").vector[:5] + nlp("content").vector[:5])/3
array([ 1.9008666 ,  0.24163334, -4.4582667 , -3.6277363 ,  5.5526333 ],
      dtype=float32)
nlp("je suis content").vector[:5]
array([ 1.9008666 ,  0.24163334, -4.4582667 , -3.6277363 ,  5.5526333 ],
      dtype=float32)

Un mot inconnu sera remplace par le vecteur nul.

nlp("sayonara").vector[:5]
array([0., 0., 0., 0., 0.], dtype=float32)

4.2 Calculs de similarite

4.2.1 Similarite et sens des mots

Des mots de sens proche sont similaires.

x = "rendement"
y = "rentabilité"
round(nlp(x).similarity(nlp(y)),2)
0.77

La similarite utilisee est la “similarite cosinus” entre les vecteurs.

similarite = 1 - cosine(nlp(x).vector, nlp(y).vector)
similarite.round(2)
0.77

Mais on ne peut pas esperer faire de l’analyse de sentiments avec ces vecteurs : “bon” et “mauvais” ne sont pas negativement lies.

x = "bon"
y = "mauvais"
round(nlp(x).similarity(nlp(y)),2)
0.66

4.2.2 Similarite et orthographe des mots

Les majuscules et minuscules ne donnent pas le meme vecteur mais ils sont tres similaires.

nlp("je").vector[:5]
array([  6.7919,  -3.3221,  -6.2118, -12.733 ,  17.073 ], dtype=float32)
nlp("Je").vector[:5]
array([ 5.8945, -4.2558, -1.83  , -9.8364, 16.448 ], dtype=float32)
x = "Je"
y = "je"
round(nlp(x).similarity(nlp(y)),2)
0.89

Les versions avec et sans accent d’un meme mot peuvent etre absolument identiques. C’est une bonne surprise qui regle le probleme de la gestion des accents manquants.

x = "simplicite"
y = "simplicité"
nlp(x).vector[:5]
array([ 1.7565 , -0.35576, -2.8536 ,  1.386  , -0.79974], dtype=float32)
nlp(y).vector[:5]
array([ 1.7565 , -0.35576, -2.8536 ,  1.386  , -0.79974], dtype=float32)
round(nlp(x).similarity(nlp(y)),2)
1.0

Mais dans d’autres cas les vecteurs ne sont pas forcement aussi similaires qu’on l’attendrait. Difficile de comprendre pourquoi quand on ne sait pas comment le réseau de neurones a ete entraine et ce qui a ete fait pour gerer les accents.

x = "facilite"
y = "facilité"
round(nlp(x).similarity(nlp(y)),2)
0.69

5 Conclusion

On a vu a travers tous les exemples precedents qu’il y a un certain nombre de “trous dans la raquette du NLP”. L’essentiel est de le savoir, on peut alors appliquer differents correctifs :

  • ajuster la tokenisation
  • controler et modifier la liste des mots vides
  • se poser la question de la gestion/suppression des accents
  • eviter la racinisation
  • corriger les erreurs les plus genantes de la lemmatisation
  • traiter ou non les sigles et abreviations
  • envisager de corriger certaines fautes d’orthographe

L’analyse de sentiments peut etre tentee mais il faudra estimer le taux d’erreur et juger s’il est acceptable ou non. Enfin pour la vectorisation les resultats sont assez satisfaisants meme pour les quelques cas bizarres rencontres.

retour au debut du document