Chapitre 4 · 16 min
Donner du sens aux mots
Transforme les ids de tokens en vecteurs, entraîne de petits embeddings skip-gram et ajoute les premiers poids appris à ton projet local.
Au chapitre 3, tu as entraîné un . Il te rend des entiers : par exemple 17 pour king, 248 pour queen. Le modèle ne sait toujours pas que ces deux entiers ont un rapport.
La façon naïve de donner ces entiers au modèle est le : le 17 devient un vecteur de 50 000 zéros avec un seul 1 à la position 17. Ça marche mécaniquement, mais c’est gaspilleur, et le produit scalaire entre deux vecteurs différents vaut toujours zéro. Pour le modèle, king et queen ne sont pas plus proches que king et bicycle.
On veut que chaque vive dans un espace continu, et que la géométrie de cet espace signifie quelque chose. La technique qui rend ça possible est : pour chaque mot, on pousse son vecteur et ceux de ses voisins à se rapprocher au sens du produit scalaire.
1. Construire les paires (centre, contexte)
s’entraîne sur des paires : pour chaque position du , chaque voisin dans une fenêtre fixe devient un exemple positif.
Code · JavaScript
Cette liste est tout le dataset . Une fenêtre plus large donne plus de contexte à chaque mot.
2. Une étape de SGD
Pour une paire positive (center, context), on veut que le produit scalaire u · v augmente, avec u = E[center] et v = E[context]. On pousse les deux vecteurs l’un vers l’autre.
Code · JavaScript
La barre du produit scalaire doit grandir après l’update. C’est tout le signal d’ : prendre une paire que le dit proche, et rapprocher ses vecteurs.
3. Entraîner et visualiser
On chaîne : construire les paires, initialiser des aléatoires, lancer des milliers de pas . À chaque pas, on prend une paire au hasard. Après assez d’itérations, les mots utilisés dans des contextes similaires finissent proches en 2D.
Code · JavaScript
Regarde le scatter. Avec 3000 itérations sur un petit , tu devrais voir des regroupements : man/woman, boy/girl, king/queen. C’est bruité, parce que le est minuscule, mais la forme est la bonne : les mots utilisés dans des contextes similaires ont des vecteurs similaires.
4. Similarité cosinus
Une fois les vecteurs appris, “à quel point ces mots se ressemblent” devient “à quel point leurs vecteurs sont alignés”. La mesure standard est la :
1 signifie même direction, 0 orthogonal, -1 opposé. On ignore souvent la magnitude : c’est la direction qui porte le sens.
Code · JavaScript
Essaie quelques mots. Avec assez d’itérations, tu devrais voir des voisins plausibles. Ne t’attends pas à l’analogie magique king - man + woman ≈ queen sur ce jouet : il faut des milliards de pour ça.
Ce que ça corrige
On a corrigé “tous les mots distincts sont sans rapport”. Le modèle peut maintenant dire : “ce vit dans le même voisinage que ceux-là”.
Mais tout n’est pas réglé :
- Vecteurs statiques.
bankrivière etbankbanque partagent le même vecteur. - Pas d’ordre des mots. agrège des voisinages.
- Pas encore de prédiction. On a décoré les avec des vecteurs ; on n’a pas encore construit un modèle qui les utilise pour prédire.
5. Ajouter les embeddings à my-llm/
Crée llm/embeddings.py :
"""Tiny embedding helpers before we switch to PyTorch."""
from __future__ import annotations
import math
import random
Vector = list[float]
Matrix = list[Vector]
# [1]
def init_embeddings(vocab_size: int, dim: int, scale: float = 0.01) -> Matrix:
return [
[random.uniform(-scale, scale) for _ in range(dim)]
for _ in range(vocab_size)
]
# [2]
def dot(a: Vector, b: Vector) -> float:
return sum(x * y for x, y in zip(a, b))
def sigmoid(x: float) -> float:
return 1.0 / (1.0 + math.exp(-x))
def skipgram_step(E: Matrix, center: int, context: int, lr: float = 0.1) -> float:
u = E[center][:]
v = E[context][:]
# [3]
pred = sigmoid(dot(u, v))
# [4]
grad = pred - 1.0
# [5]
for i in range(len(u)):
E[center][i] -= lr * grad * v[i]
E[context][i] -= lr * grad * u[i]
return -math.log(max(pred, 1e-9))
# [6]
def cosine(a: Vector, b: Vector) -> float:
denom = math.sqrt(dot(a, a)) * math.sqrt(dot(b, b))
return 0.0 if denom == 0 else dot(a, b) / denomLis ce fichier comme le plus petit système apprenant possible :
- [1]
init_embeddingscrée une table : une ligne par . - [2]
dotscore l’alignement de deux vecteurs. - [3]
sigmoid(dot(u, v))transforme ce score en nombre entre 0 et 1. - [4]
grad = pred - 1.0est l’erreur sur une paire positive. - [5] rapproche les deux vecteurs.
- [6]
cosinesert à inspecter, pas à .
Plus tard, PyTorch portera la table et les . Pour l’instant, ce fichier fixe le contrat : id de en entrée, vecteur dense en sortie.
Recap
- Les vecteurs gaspillent de la place et déclarent tous les mots différents comme sans
rapport. - Les sont des vecteurs denses par . - les entraîne en
rapprochant les paires
(centre, contexte). - La mesure l’alignement des . - Ton projet local a maintenantllm/embeddings.py, le premier fichier avec des appris. - Les seuls ne prédisent rien. Les prochains chapitres les branchent dans une fonction qui apprend.
Pour aller plus loin
- Mikolov et al., “Efficient Estimation of Word Representations”.
- The Illustrated Word2vec.
- TensorFlow Embedding Projector.
- L’implémentation de référence vit dans
lib/ml/embedding/.
Prochaine étape : un neurone qui apprend — les nous donnent des entrées. Il faut maintenant une fonction qui transforme ces entrées en sorties et s’améliore.