Du sac de mots à word2vec
Prérequis : MATH 2.1 (similarité cosinus), DL 5.1 (régression logistique), MATH 13.1 (entropie croisée) · Notions : sac de mots, n-grammes, TF-IDF, hypothèse distributionnelle, matrice de cooccurrence, word2vec (skip-gram, CBOW), échantillonnage négatif, analogies, GloVe, limites des embeddings statiques
Le sac de mots
Avant les réseaux, on représentait un document par le sac de mots (bag of words) : un vecteur de la taille du vocabulaire
TF-IDF : pondérer par la rareté
Les mots fréquents partout (« le », « de ») dominent les comptes sans rien dire du sujet. TF-IDF (term frequency – inverse document frequency) les écrase :
où
Limite fondamentale : « voiture » et « automobile » sont deux cases différentes, orthogonales. Le sac de mots ne sait rien du sens.
L'hypothèse distributionnelle
« On connaît un mot à ses fréquentations » (J. R. Firth, 1957). Deux mots qui apparaissent dans les mêmes contextes ont un sens proche : « guitare » et « basse » apparaissent tous deux après « joue de la ». On peut donc apprendre un vecteur dense par mot (dimension 100 à 300) tel que les mots de contextes similaires aient des vecteurs proches : un embedding de mot.
word2vec, skip-gram
Mikolov et al. (2013). On parcourt le texte ; pour chaque mot central
L'échantillonnage négatif (negative sampling, SGNS) remplace la softmax par
C'est une régression logistique (DL 5.1) dont on apprend aussi les entrées. Le gradient sur chaque score est « prédiction moins cible »,
Levy et Goldberg (2014) ont montré que SGNS factorise implicitement une matrice de cooccurrence décalée (PMI − ln k) : les deux familles, comptage et prédiction, apprennent au fond la même chose. GloVe (2014) factorise explicitement le logarithme des cooccurrences.
Analogies et limites
Les embeddings appris capturent des régularités linéaires :
Limite qui a mené aux Transformers : un mot a un seul vecteur, quel que soit le contexte. « La batterie du téléphone » et « la batterie du groupe » partagent le même embedding. Les modèles contextuels (ELMo 2018, puis BERT, NLP 6.1) produisent un vecteur par occurrence, qui dépend de la phrase entière.
Quatre documents : (1) « le chat dort sur le canapé », (2) « le chien dort sur le tapis », (3) « la bourse chute et le dollar monte », (4) « le dollar chute à la bourse ». « le » figure dans les quatre : idf
Les mots d'un corpus de l'OIF. Sur des rapports de la Francophonie, un word2vec placera « coopération », « partenariat » et « appui » côte à côte, et « Sénégal », « Côte d'Ivoire », « Bénin » dans un même amas. Mais « Conseil » (le Conseil permanent, le conseil municipal, un conseil pratique) n'aura qu'un vecteur, moyenne de tous ses sens. Pour un moteur de recherche thématique, c'est rédhibitoire : il faudra des embeddings contextuels de phrases (NLP 9.1), et un modèle multilingue si le corpus mélange français, anglais et arabe (NLP 10.1).
- Calcule la matrice TF-IDF des quatre documents de l'exemple et vérifie les similarités.
- Implémente skip-gram avec échantillonnage négatif sur un corpus jouet qui mélange deux thèmes (musique, cuisine) avec la même syntaxe.
- Vérifie que « guitare » est plus proche de « basse » que de « tomates », et affiche les plus proches voisins.
Solution commentée
import numpy as np, math
docs = ["le chat dort sur le canapé", "le chien dort sur le tapis",
"la bourse chute et le dollar monte", "le dollar chute à la bourse"]
toks = [d.split() for d in docs]
vocab = sorted({w for t in toks for w in t}); idx = {w: i for i, w in enumerate(vocab)}
tf = np.zeros((len(docs), len(vocab)))
for i, t in enumerate(toks):
for w in t: tf[i, idx[w]] += 1
tf /= tf.sum(1, keepdims=True) # fréquence relative dans le document
df = (tf > 0).sum(0) # nombre de documents contenant le mot
idf = np.log(len(docs) / df) # « le » apparaît partout : idf = 0
X = tf * idf
X /= np.linalg.norm(X, axis=1, keepdims=True)
S = X @ X.T
print(np.round(S, 2))
assert idf[idx["le"]] == 0
assert S[0, 1] > S[0, 2] and S[2, 3] > S[1, 3] # chat~chien, finance~finance
import numpy as np, re, collections
rng = np.random.default_rng(0)
# corpus jouet : deux « mondes » (musique, cuisine) qui partagent la syntaxe
gabarits = ["le {a} joue de la {i} sur scène", "la {a} accorde sa {i} avant le concert",
"le {c} coupe les {l} dans la cuisine", "la {c} fait cuire les {l} au four"]
mots = {"a": ["musicien", "pianiste", "guitariste"], "i": ["guitare", "basse", "batterie"],
"c": ["cuisinier", "chef", "pâtissier"], "l": ["carottes", "pommes", "tomates"]}
phrases = []
for _ in range(3000):
g = gabarits[rng.integers(4)]
phrases.append(g.format(**{k: v[rng.integers(3)] for k, v in mots.items()}).split())
vocab = sorted({w for p in phrases for w in p}); idx = {w: i for i, w in enumerate(vocab)}; V = len(vocab)
freq = np.bincount([idx[w] for p in phrases for w in p], minlength=V).astype(float)
pneg = freq ** 0.75; pneg /= pneg.sum() # loi de bruit unigramme^0.75
paires = [(idx[p[t]], idx[p[c]]) for p in phrases for t in range(len(p))
for c in range(max(0, t - 2), min(len(p), t + 3)) if c != t]
paires = np.array(paires)
D, k, lr = 16, 5, 0.05
W = rng.normal(0, 0.1, (V, D)); C = np.zeros((V, D)) # embeddings centre / contexte
sig = lambda x: 1 / (1 + np.exp(-x))
for ep in range(3):
rng.shuffle(paires)
for t, c in paires:
negs = rng.choice(V, k, p=pneg)
ctx = np.concatenate([[c], negs]); lab = np.r_[1.0, np.zeros(k)]
s = sig(C[ctx] @ W[t]) # (k+1,)
g = s - lab # gradient de la BCE sur les scores
gw = g @ C[ctx]
C[ctx] -= lr * np.outer(g, W[t]); W[t] -= lr * gw
E = W / np.linalg.norm(W, axis=1, keepdims=True)
def voisins(w, n=3):
s = E @ E[idx[w]]; o = np.argsort(-s)
return [vocab[i] for i in o if vocab[i] != w][:n]
for w in ["guitare", "chef", "pommes"]: print(w, "→", voisins(w))
def cos(a, b): return float(E[idx[a]] @ E[idx[b]])
print("guitare~basse", round(cos("guitare", "basse"), 2), " guitare~tomates", round(cos("guitare", "tomates"), 2))
assert cos("guitare", "basse") > cos("guitare", "tomates") + 0.3
En 3 époques (une dizaine de secondes), les instruments se regroupent entre eux, les aliments aussi. Les deux thèmes ont exactement la même syntaxe : seul le contexte de cooccurrence les sépare, c'est l'hypothèse distributionnelle à l'œuvre. Essaie une fenêtre de ±1 au lieu de ±2 et regarde ce qui change.
À retenir
- Sac de mots et TF-IDF : simples, efficaces pour la recherche, aveugles au sens.
- Hypothèse distributionnelle : le sens d'un mot vient de ses contextes.
- Skip-gram + échantillonnage négatif = régression logistique sur des paires (mot, contexte), dont on apprend les vecteurs.
- Un vecteur par mot, pas par occurrence : la limite que lèvent les modèles contextuels.