Ce que cet article va vous apporter :

Nous allons parler de Word Embedding. Cette méthode a permis une amélioration majeure des algorithmes de machine learning appliqués au langage.

Vous en avez probablement entendu parler si vous travaillez dans l’intelligence artificielle.

Mais les articles en français sont rares et souvent trop vagues ou trop succincts.

Ici, nous allons parcourir ensemble les principes linguistiques à l’œuvre derrière le Word Embedding.

Et nous allons surtout les mettre en pratique immédiatement, avec quelques lignes de code que vous pourrez faire tourner sur votre ordinateur.

C’est parti.

Cet article est rédigé par Raphaël de la chaine YouTube Machine Linguist.

Savez-vous que Google comprend le sens des mots ?

Par exemple, Google sait que rue et route, ce sont des mots qui ont un sens proche.

D’ailleurs, il peut réaliser des opérations mathématiques telles que : Roi plus Femme moins Homme est égal à Reine.

N’est-ce pas surprenant ?

Regardez ces quelques lignes de codes ci-dessous.

Nous allons réaliser le calcul dont nous venons de parler.

Je vous conseille d’exécuter ce code dans Google Colab si vous êtes débutant.

import gensim.downloader as api

model = api.load("word2vec-google-news-300")

model.most_similar(positive=['king', 'woman'], negative=['man'], topn=1)

Et voici le résultat que l’on obtient :

[('queen', 0.7118192911148071)]

Ainsi, pour le modèle, roi est à reine, ce que homme est vis-à-vis de femme.

Voici un autre exemple :

Nous allons ici demander à notre modèle de trouver un mot qui soit proche de Paris et d’Allemagne, mais éloigné de France.

model.most_similar(positive=['paris', 'germany'], negative=['france'], topn=1)

Et le résultat est…

[('berlin', 0.48413652181625366)]

Et nous pouvons réaliser la même expérience avec les animaux ; nous allons ajouter chat à chiot, et soustraire chien.

model.most_similar(positive=['puppy', 'cat'], negative=['dog'], topn=1)

Et voici ce que nous obtenons :

[('kitten', 0.7634989619255066)]

C’est plutôt sympa, non ?

Mais comment est-ce possible de faire des mathématiques avec des mots ?

En fait, ce que vous voyez ici, c’est une technologie développée par Google en 2013.

Elle s’appelle Word2Vec, et ça appartient aux algorithmes de Word Embedding.

Nous allons voir comment cela fonctionne plus loin dans cet article.

Avant cela, retenez que Word2Vec permet de comprendre ce que les mots veulent dire.

Et à la fin de cet article, vous comprendrez le mécanisme,

Et vous allez même pouvoir le coder vous-même !

Mais avant tout, à quoi ça sert le word embedding ?

L’une des utilisations les plus évidentes, c’est d’améliorer la recherche d’information.

Est-ce que vous vous souvenez, au début d’internet, il n’était pas rare que certains moteurs de recherche ne trouvent aucun résultat.

Et nous recevions un message du genre : aucun résultat n’a été trouvé, veuillez essayer avec d’autres mots-clés.

Dans la recherche d’information, cela s’appelle le silence : c’est le fait de ne trouver aucun résultat dans un moteur de recherche.

Le word embedding permet d’éviter le silence dans la recherche d’information.

Imaginez que vous êtes devant un moteur de recherche, et que vous souhaitez trouver un restaurant qui sert des hamburgers.

Un moteur de recherche classique ne nous renverrait que les sites Web qui contiennent le mot exact : hamburger.

Pourtant, un restaurant qui vendrait des cheeseburger, cela nous conviendrait, non ?

Eh bien, c’est cela que permet le Word embedding.

Il va étendre les termes de notre recherche en ajoutant les mots qui ont un sens proche.

Dans le cas des hamburgers, voici comment demander les neuf termes qui sont les plus proches :

model.most_similar('hamburger', topn=9)

Et voici le résultat.

[('burger', 0.7784732580184937),
 ('hamburgers', 0.7297240495681763),
 ('cheeseburger', 0.7115724086761475),
 ('burgers', 0.7085813879966736),
 ('sandwich', 0.666914165019989),
 ('hotdog', 0.6552070379257202),
 ('taco', 0.647402822971344),
 ('fries', 0.6414111852645874),
 ('burrito', 0.6399032473564148)]

Cela permet de trouver ce que l’on ne cherche pas.

C’est-à-dire que nous pouvons trouver du contenu proche de ce que nous recherchons, même si nous ne l’avons pas écrit explicitement.

C’est ainsi que le word embedding améliore notre recherche d’information.

Et c’est vrai pour les moteurs de recherche — tels que Google, YouTube, etc. — mais c’est également un outil indispensable pour que l’on puisse converser avec les machines.

Et là, nous parlons des assistants virtuels tels que Alexa, Siri, etc.

D’une manière générale, cette technologie permet en quelque sorte aux machines de comprendre ce qu’on leur dit ou écrit.

Et cela, indépendant des termes qui sont utilisés.

De plus, de nombreux modèles de machine learning utilisent le word embedding pour prendre des décisions sur base de texte ou de notre voix, pour des tâches telles que la classification de texte, la détection d’intention, etc.

Mais le word embedding n’a pas été inventé en un jour.

Et en réalité, il y a deux grandes étapes qui ont rendu possible cette technologie.

La première étape se situe en 1954, lorsque Zellig Harris, linguiste américain, a publié Distributional Structure.

Dans cette publication, il pose cette hypothèse : Difference of meaning correlates with difference of distribution.

Cette phrase peut sembler un peu mystérieuse.

Mais nous allons voir ensemble sa signification.

En fait, John Rupert Firth, un autre linguiste américain, a formulé cela autrement quelques années plus tard : « You shall know a word by the company it keeps. »

Ces deux citations parlent de l’hypothèse distributionnelle.

Cette hypothèse pose l’idée que les mots ayant un même contexte linguistique ont un sens qui est proche.

Mais qu’est-ce que le contexte linguistique ?

Pour faire simple, ce sont les mots qui se trouvent avant et après un mot observé.

Regardez l’illustration ci-dessous : en bleu vous avez le mot observé — on parle parfois de center word — ici c’est le mot anglais cat.
Et en rouge, vous avez les mots qui constituent le contexte du mot observé : ici c’est cute, jumps, etc.

Source: cbail.github.io

Et donc, un contexte similaire signifierait un sens similaire.

C’est-à-dire que si le mot chien est généralement entouré par les mêmes mots que le mot chat, alors ces deux mots ont un sens proche.

Mais  cette hypothèse distributionnelle elle n’a pas été liée directement à l’intelligence artificielle

Car à l’époque où cette hypothèse a été posée, l’informatique n’en était qu’à ses balbutiements.

IBM lançait ses premiers ordinateurs, qui étaient énormes, et les disques durs venaient à peine d’être inventés.

Et jusqu’aux années 80, il ne s’est pas passé grand-chose.

La linguistique et l’informatique se limitaient à l’application de moteur de règles, et on utilisait peu de statistiques et encore moins de machine learning à l’époque.

La seconde grande étape a été franchie par Gerard Salon en 1983.

Dans sa publication Introduction to modern information retrieval, il a proposé le concept de modèle vectoriel.

Il s‘agit d’une méthode qui représente du texte sous la forme de vecteurs ; c’est-à-dire une liste de valeurs numériques.

Mais comment est-ce qu’on transforme du texte en vecteur ?

La manière la plus simple, c’est de compter les mots.

Pour comprendre comment cela fonctionne, nous allons vectoriser les trois phrases ci-dessous.

phrases = [
"le chat mange ses croquettes",
"le chien aime ses croquettes",
"le chat ronronne et mange"
]

Nous allons utiliser un CountVectorizer de Scikit-Learn pour vectoriser ces phrases.

import numpy as np
import pandas as pd

from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer


vectorizer = CountVectorizer()
vector = vectorizer.fit_transform(phrases)

df = pd.DataFrame(
  vector.toarray(),
  columns=vectorizer.get_feature_names(),
  index=[f"phrase_{i+1}" for i in range(len(phrases))]
  )

print(df)

Voici le résultat que l’on obtient.

          aime  chat  chien  croquettes  et  le  mange  ronronne  ses
phrase_1     0     1      0           1   0   1      1         0    1
phrase_2     1     0      1           1   0   1      0         0    1
phrase_3     0     1      0           0   1   1      1         1    0

Le problème avec cette manière de créer un vecteur en comptant les mots, c’est que les mots ont tous le même poids.

Je m’explique.

Regardez la troisième phrase, le mot le a autant de poids que le mont ronronne.

Pourtant intuitivement, on sait bien que ronronne contient plus d’information sémantique que le déterminant le.

Pour résoudre ce problème, Salton propose d’utiliser une autre valeur numérique que le nombre de mots.

Cela s’appelle le TF-IDF, pour Term Frequency inverse Document Frequency.

Term Frequency, c’est la fréquence d’un mot dans un document spécifique.

Par exemple, dans la phrase 3, il y a 4 mots, et le mot ronronne apparaît 1 fois, sa Term Frequency est donc de 25%

Et la Document Frequency, c’est la fréquence des documents qui contiennent ce mot. 

Par exemple, il y a un document sur trois qui contient le mot « ronronne », sa Document Frequency est donc de 33%

Et le TF-IDF est un score qui  fait un arbitrage entre ces deux mesures.

Un mot qui apparaît dans peu de documents, mais qui est très présent dans un document spécifique, il aura un score plus élevé.

Alors qu’un mot qui apparaît dans tous les documents — des mots comme  le, la, les, etc. qui n’ont pas beaucoup de valeur sémantique — il aura une Document Frequency plus élevée, et donc un TF-IDF plutôt bas

Si ça vous intéresse, vous trouverez la formule sur la page Wikipedia.

Voici comment vectoriser nos phrases avec un TF-IDF.

Voici le code.

tfidf_vectorizer = TfidfVectorizer()
tfidf_vector = tfidf_vectorizer.fit_transform(phrases)

df = pd.DataFrame(
  np.around(tfidf_vector.toarray(), 2),
  columns=vectorizer.get_feature_names(),
  index=[f"phrase_{i+1}" for i in range(len(phrases))]
  )

print(df)

Et voici le résultat.

          aime  chat  chien  croquettes    et    le  mange  ronronne   ses
phrase_1  0.00  0.47   0.00        0.47  0.00  0.36   0.47      0.00  0.47
phrase_2  0.53  0.00   0.53        0.41  0.00  0.32   0.00      0.00  0.41
phrase_3  0.00  0.41   0.00        0.00  0.53  0.32   0.41      0.53  0.00

On peut comparer ces vecteurs de TF-IDF avec les vecteurs qui comptaient simplement les mots.

Nous voyons que pour la troisième phrase, le mot ronronne a maintenant un score plus élevé que le déterminant le.

Grâce à quoi ?

Tout simplement parce qu’il apparaît dans moins de documents, et qu’il a donc potentiellement plus de valeur sémantique.

Bref, maintenant vous voyez comment on peut représenter une phrase, ou un document, avec des vecteurs.

Et ce qui est sympa, c’est qu’on peut maintenant calculer la distance entre deux phrases.

Comment est-ce-possible ?

Nous allons le voir maintenant.

Avant tout, rappelons-nous que les vecteurs que nous avons imprimés un peu plus haut contiennent 3 phrases, qui sont les lignes, et neuf 9 mots, qui sont les colonnes.

Le truc, c’est que ces 9 colonnes sont en quelque sorte les dimensions d’un espace vectoriel.

Ainsi, les trois phrases peuvent être considérées comme des points dans cet espace à 9 dimensions.

Et du coup, il y a une certaine distance entre ces points, une distance dans un espace vectoriel à neuf dimensions.

Et cette distance, elle s’appelle la distance cosine, et elle est facile à calculer. 

Calculons cette distance avec quelques lignes de code.

Vérifions à quel point, la phrase 3 est proche de la phrase 1.

from scipy.spatial.distance import cosine
cosine(
  tfidf_vectorizer.transform(['le chat ronronne et mange']).toarray(),
  tfidf_vectorizer.transform(['le chat mange ses croquettes']).toarray(),
)

Toutes les deux contiennent le mot chat et mange, nous pouvons donc nous attendre à ce qu’elles soient proches.

Voici la distance mesurée : elles sont à une distance cosine de 50% dans cet espace vectoriel à 9 dimensions.

0.5071716593555834

Faisons la même chose avec une autre phrase.

Nous allons voir que la phrase 1 est plus éloignée de la phrase 2.

Calculons leur distance cosine.

cosine(
  tfidf_vectorizer.transform(['le chat ronronne et mange']).toarray(),
  tfidf_vectorizer.transform(['le chien aime ses croquettes']).toarray(),
)

Nous constatons que ces deux phrases sont plus éloignées, elles ont une distance cosine supérieure à 90%.

0.9004949875798397

D’accord, mais en quoi tout cela permet de comprendre le sens des mots ?

Eh bien en fait c’est assez simple.

Si le sens d’un mot est lié à son contexte — c’est l’hypothèse distributionnelle ;

Et si le contexte peut être représenté sous la forme d’un vecteur — avec le modèle vectoriel  ;

Et si nous pouvons calculer la distance entre les vecteurs — avec la distance cosine ;

Alors nous pouvons mesurer la distance sémantique entre les mots.

Codons un peu, et vous allez comprendre.

D’abord, nous allons prendre une liste de phrases un peu plus grande.

Nous allons prendre 6 phrases.

phrases = [
  "le chat mange ses croquettes",
  "le chien dévore ses croquettes",
  "le chat dévore son paté",
  "jean va travailler",
  "le chat mange son repas",
  "jacque aime quand son chien mange"
]

Pour chacun des mots de ces phrases, nous allons extraire leur contexte, c’est-à-dire les mots qui se trouvent juste avant et après.

Cela peut se faire avec le code ci-après.

contexts = defaultdict(list)

for phrase in phrases:
words = phrase.split()

for i, word in enumerate(words):
  candidate = [i+j for j in range (-1, 2) if j != 0]
  actual = [c for c in candidate if 0 <= c < len(words)]
  contexts[word] = contexts[word] + [words[a] for a in actual]

Voici la valeur du dictionnaire contexts que nous venons de créer.

defaultdict(list,
            {'aime': ['jacque', 'quand'],
             'chat': ['le', 'mange', 'le', 'dévore', 'le', 'mange'],
             'chien': ['le', 'dévore', 'son', 'mange'],
             'croquettes': ['ses', 'ses'],
             'dévore': ['chien', 'ses', 'chat', 'son'],
             'jacque': ['aime'],
             'jean': ['va'],
             'le': ['chat', 'chien', 'chat', 'chat'],
             'mange': ['chat', 'ses', 'chat', 'son', 'chien'],
             'paté': ['son'],
             'quand': ['aime', 'son'],
             'repas': ['son'],
             'ses': ['mange', 'croquettes', 'dévore', 'croquettes'],
             'son': ['dévore', 'paté', 'mange', 'repas', 'quand', 'chien'],
             'travailler': ['va'],
             'va': ['jean', 'travailler']})

Maintenant, nous allons vectoriser ces contextes avec un TfidfVectorizer, de la même manière que nous avons vectorisé les phrases un peu plus haut dans l’article.

vectorizer = TfidfVectorizer()
vectorizer.fit(phrases)

vectors = {}

for k, v in dict(contexts).items():
str_ = ' '.join(v)
vectors[k] = np.around(vectorizer.transform([str_]).toarray(), 2)[0]

Nous venons de créer un dictionnaire appelé vectors qui contient pour chaque mot observé, un vecteur qui représente son contexte.

{'aime': array([0.  , 0.  , 0.  , 0.  , 0.  , 0.71, 0.  , 0.  , 0.  , 0.  , 0.71,
        0.  , 0.  , 0.  , 0.  , 0.  ]),
'chat': array([0.  , 0.  , 0.  , 0.  , 0.34, 0.  , 0.  , 0.74, 0.58, 0.  , 0.  ,
        0.  , 0.  , 0.  , 0.  , 0.  ]),
'chien': array([0.  , 0.  , 0.  , 0.  , 0.58, 0.  , 0.  , 0.42, 0.49, 0.  , 0.  ,
        0.  , 0.  , 0.49, 0.  , 0.  ]),
'croquettes': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.]),
'dévore': array([0.  , 0.46, 0.54, 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  ,
        0.  , 0.54, 0.46, 0.  , 0.  ]),
'jacque': array([1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]),
'jean': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]),
'le': array([0.  , 0.93, 0.37, 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  ,
        0.  , 0.  , 0.  , 0.  , 0.  ]),
'mange': array([0.  , 0.72, 0.42, 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  ,
        0.  , 0.42, 0.36, 0.  , 0.  ]),
'paté': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0.]),
'quand': array([0.82, 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.  ,
        0.  , 0.  , 0.57, 0.  , 0.  ]),
'repas': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0.]),
'ses': array([0.  , 0.  , 0.  , 0.84, 0.42, 0.  , 0.  , 0.  , 0.35, 0.  , 0.  ,
        0.  , 0.  , 0.  , 0.  , 0.  ]),
'son': array([0.  , 0.  , 0.37, 0.  , 0.37, 0.  , 0.  , 0.  , 0.32, 0.46, 0.46,
        0.46, 0.  , 0.  , 0.  , 0.  ]),
'travailler': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]),
'va': array([0.  , 0.  , 0.  , 0.  , 0.  , 0.  , 0.71, 0.  , 0.  , 0.  , 0.  ,
        0.  , 0.  , 0.  , 0.71, 0.  ])}

Voyez-vous où nous voulons en venir ?

Si l’hypothèse distributionnelle est exacte, ces vecteurs de contexte peuvent être considérés comme des vecteurs de sens.

Vérifions cela immédiatement.

Voici une petite fonction qui va nous imprimer pour chaque mot, quel est le mot qui a le contexte le plus proche.

def most_similar(word):
scores = []
keys_ = [k for k in vectors.keys() if k != word]
for k in keys_:
  scores.append(
      1-cosine(
          vectors[word],
          vectors[k]
      )
  )
return pd.Series(
    scores,
    index=keys_
  ).sort_values(ascending=False).head(1)

La fonctionne calcule la distance cosine entre les vecteurs de contexte d’un mot et de tous les autres contextes de mots du corpus.

Elle nous renvoie ensuite le mot dont le contexte est le plus similaire.

Et si l’hypothèse distributionnelle est exacte, ce mot sera aussi celui dont le sens est le plus proche.

Essayons immédiatement.

Quel est le mot le plus proche du mot chat sur base de nos six phrases ?

most_similar('chat')

Il s’agit du mot chien.

chien    0.795146
dtype: float64

Et quel est le mot le plus proche du verbe manger ?

most_similar('mange')

Il s’agit du verbe dévorer.

dévore    0.946995
dtype: float64

Enfin, trouvons le terme le plus proche de pâté.

most_similar('paté')

Il s’agit du mot repas.

repas    1.0
dtype: float64

Les résultats sont clairs : le sens d’un mot semble en effet lié à son contexte.

Et pourtant, nous ne sommes pas arrivés au bout de notre l’histoire

Car cela n’a aucun sens de faire l’exercice que nous venons de faire sur six phrases.

Pour obtenir des statistiques valables, nous devrions réaliser cette opération sur des millions d’exemples.

Mais nous allons avoir un problème.

Car si nous faisons cela à l’échelle d’internet, comme le fait le moteur de recherche Google, nous aurions des millions, voire des milliards de dimensions. 

Du coup, calculer la distance entre des milliards de vecteurs ayant des milliards de dimensions, cela reviendrait à réaliser des milliards de milliards d’opérations.

Et une telle solution serait quasiment inutilisable tellement elle serait lente.

Cette problématique est connue, on l’appelle parfois le fléau de la dimension.

Et il y a des solutions pour cela, mais nous le verrons dans un prochain article.

Ici, nous avons vu ce qu’était le Word Embedding.

Nous avons vu comment l’hypothèse distributionnelle et les modèles vectoriels permettent de calculer la distance sémantique entre les mots.

Dans un prochain article, nous verrons comment la réduction de dimensions et les réseaux de neurones permettent d’appliquer cette technologie à l’échelle du Web.

Crédit de l’image de couverture : Raphaël Hubain – Tous droits réservés