Chapitre 8 · 18 min
Une tête d’attention à la main
Écris Q, K, V, les scores causaux, le softmax et les valeurs pondérées, puis ajoute le cœur de l’attention à ton modèle local.
Jusqu’au chapitre 7, le modèle savait ajuster des fonctions sur des exemples individuels. Les courbes de descendaient, les résolvaient XOR, les faisaient leur travail. Le problème restant : un n’a aucun moyen de regarder les autres . Le ne voyait que le mot précédent ; un appliqué à un fixe ne voit que ce vecteur.
L’ est ce mécanisme, et elle porte tous les de pointe depuis 2017. Le papier d’origine la disait « tout ce dont vous avez besoin » — une exagération qui s’est révélée approximativement juste.
La mécanique est mécanique. Une fois que tu as exécuté chaque morceau et inspecté les formes, tu peux expliquer l’ à un ami au comptoir. C’est exactement ce qu’on va faire : quatre cellules exécutables qui construisent une tête d’ de l’intérieur, sur une phrase jouet de 5 avec des en 4 dimensions, pour que les matrices tiennent à l’écran. Ensuite, tu sauvegarderas une fonction d’ causale minimale en local.
Phrase : the cat sat on mat. Cinq , 4D faits à la main.
1. Projeter en Q, K, V
Une seule tête d’ a trois matrices apprises : W_Q, W_K, W_V, chacune de taille [d × d_head]. Multiplier l’entrée X (de forme [seq_len × d]) par chacune produit les queries, les keys et les values — trois vues de la même entrée, calculées chacune par un jeu de poids différent :
Intuition :
- Queries : “qu’est-ce que je cherche ?”
- Keys : “voilà ce que j’ai”
- Values : “voilà ce que j’apporterai si tu me choisis”
Écris la pour Q. (On réutilise la même routine pour K et V — une fois que tu l’as écrite une fois, c’est fini.)
À toi de jouer · JavaScript
Le résultat est [seq_len × d_head], la même forme que l’entrée dans notre cas jouet. Chaque ligne est le vecteur query d’un de la phrase.
2. Scorer chaque paire
On demande maintenant : à quel point chaque query s’intéresse à chaque key ? La réponse standard est un produit scalaire — grand quand deux vecteurs pointent dans la même direction, petit quand ils sont orthogonaux, négatif quand ils s’opposent.
S est une matrice [seq_len × seq_len]. S[i][j] signifie : « à quel point le j est-il pertinent pour le i ? » La ligne i est la ligne des scores vus depuis le i.
À toi de jouer · JavaScript
La heatmap montre les scores bruts. La matrice n’est pas symétrique en général : i qui interroge j n’est pas la même question que j qui interroge i, parce qu’elles passent par des W_Q et W_K différents. C’est cette asymétrie qui permet à l’ d’exprimer des relations orientées, du type « ce verbe est gouverné par ce sujet ».
3. Échelle et softmax
Les scores ne sont pas encore des probabilités : ils peuvent avoir n’importe quel signe, n’importe quelle magnitude. Deux transformations les convertissent en une distribution de probabilité, ligne par ligne :
- Mettre à l’échelle chaque entrée par
1/√d_k. Sans ça, les produits scalaires de vecteurs en grande dimension grandiraient assez pour saturer le et faire disparaître les à l’. - Appliquer le à chaque ligne. Chaque ligne somme alors à 1.
À toi de jouer · JavaScript
( par ligne, appliqué indépendamment à chacune.)
Regarde la ligne i dans la heatmap. Ses cellules te disent, pour le i, quelle part de sa représentation mise à jour viendra de chaque autre . Si le 2 a une valeur forte en colonne 4, le modèle considère le 4 comme très pertinent en tant que source pour la mise à jour du 2. Les lignes somment à 1 : ce sont de vraies .
4. Mélanger les values
Dernière étape : la sortie de chaque est la somme des vecteurs value pondérée par l’. Le i prend une moyenne pondérée des values de tous les , avec les poids de sa ligne d’ A[i].
Sous forme matricielle, output = A · V, de forme [seq_len × d]. La même forme que l’entrée X — la tête réarrange la représentation de chaque en y tirant des morceaux des autres.
À toi de jouer · JavaScript
C’est tout le calcul d’une à une tête. Cinq lignes de maths, quatre opérations matricielles. Empile la même machinerie quelques dizaines de fois (c’est ce que font les chapitres suivants), entraîne-la sur un milliard de de texte, et tu obtiens GPT.
Pourquoi ça marche
Le mécanisme n’est pas profond, mais ses implications le sont. Une seule tête d’ peut implémenter, selon les poids qu’elle a appris :
- Une copie — chaque regarde le précédent, la sortie est la value du d’avant. (Utile pour la répétition.)
- Un lookup — pour chaque « the », regarder le nom qui suit. (Fréquent en modélisation du langage.)
- Un accord — pour chaque verbe, regarder son sujet. (Dépendance à longue portée.)
- Un résumé — chaque regarde à peu près également tous les autres, ce qui moyenne la séquence. (Utile dans la dernière couche.)
La procédure d’ ( sur la next-) découvre quels motifs le réseau a besoin d’avoir. On ne les lui dit jamais. Un moderne a des dizaines de ces têtes par couche et des dizaines de couches ; chaque tête se spécialise pendant l’.
5. Ajouter l’attention causale localement
Crée llm/attention.py :
"""Readable attention helpers before the PyTorch version."""
from __future__ import annotations
import math
Vector = list[float]
Matrix = list[Vector]
def dot(a: Vector, b: Vector) -> float:
return sum(x * y for x, y in zip(a, b))
def softmax(values: Vector) -> Vector:
m = max(values)
exps = [math.exp(v - m) for v in values]
total = sum(exps)
return [v / total for v in exps]
def matmul(x: Matrix, w: Matrix) -> Matrix:
columns = list(zip(*w))
return [[dot(row, list(col)) for col in columns] for row in x]
def causal_attention(x: Matrix, wq: Matrix, wk: Matrix, wv: Matrix) -> Matrix:
# [1]
q = matmul(x, wq)
k = matmul(x, wk)
v = matmul(x, wv)
scale = math.sqrt(len(k[0]))
out: Matrix = []
for i, query in enumerate(q):
# [2]
scores = [
dot(query, key) / scale if j <= i else -1e9
for j, key in enumerate(k)
]
# [3]
weights = softmax(scores)
# [4]
out.append([
sum(weight * value[d] for weight, value in zip(weights, v))
for d in range(len(v[0]))
])
return outLis-le comme quatre passages sur la même séquence :
- [1]
q,ketvsont trois vues apprises dex. Mêmes , questions différentes. - [2]
scorescompare la query duià chaque key.j <= iest le : les passés et courant sont visibles, les futurs reçoivent-1e9. - [3]
softmax(scores)fabrique une pour lei. - [4] construit une moyenne pondérée des vecteurs value. C’est la nouvelle représentation du
i.
L’ajout important, c’est le : le i ne peut lire que les 0..i. Sans lui, le modèle pourrait tricher pendant l’ next- en regardant la réponse.
Recap
- Trois projections de la même entrée — Q (queries), K (keys), V (values).
- Les scores sont les produits scalaires par paires entre queries et keys : à quel point chaque autre est-il pertinent ?
- Mise à l’échelle et transforment les scores en une par .
- La sortie est une somme pondérée des vecteurs value, pondérée par l’.
- Ton projet local a maintenant
llm/attention.pyavec l’ causale. - Une tête est un unique motif de routage d’information. Plusieurs têtes (chapitre suivant) permettent à plusieurs routes de coexister.
Pour aller plus loin
- The Illustrated Transformer, par Jay Alammar — l’explication visuelle de l’ la plus reprise sur internet.
- Karpathy, « Let’s build GPT from scratch » — deux heures d’ codée en direct.
- Step by Token, chapitre 4 traite l’ sous l’angle de la compréhension.
- La référence mathématique de chaque cellule vit dans
components/chapter/ch08/_shared.ts.
Prochaine étape : multi-têtes et résidus — une tête ne suffit pas, et il faut une connexion pour en empiler beaucoup.
Questions fréquentes
Que signifient query, key et value dans l'attention ?
Trois vues différentes de la même entrée, chacune produite par sa propre matrice apprise. La query demande « qu'est-ce que je cherche », la key annonce « voilà ce que j'ai », et la value porte « voilà ce que j'apporterai si tu me choisis ». Les poids d'attention viennent de la correspondance entre queries et keys ; ce qui circule réellement, ce sont les values.
Pourquoi diviser le score d'attention par la racine de d_k ?
Parce que la variance d'un produit scalaire croît linéairement avec sa dimension. À d_k = 64, les scores bruts atteignent des dizaines voire des centaines, ce qui pousse le softmax en saturation : presque toute la masse va sur un seul token et le gradient s'annule. Diviser par √d_k ramène les scores à une échelle unitaire et garde le softmax dans le régime où il peut encore apprendre.
Que fait concrètement le masque causal ?
Il empêche le token i de regarder les tokens qui le suivent, en mettant ces scores à moins l'infini avant le softmax pour qu'ils en ressortent à probabilité exactement nulle. Sans lui, un modèle entraîné à prédire le token suivant pourrait simplement lire la réponse. C'est la seule contrainte qui fait d'un transformer un décodeur plutôt qu'un encodeur.
La matrice d'attention est-elle symétrique ?
Non, et c'est important. Le token i qui interroge le token j passe par W_Q et W_K, tandis que j qui interroge i passe par les mêmes matrices dans les rôles inverses — calcul différent, score différent. C'est cette asymétrie qui permet à l'attention d'exprimer des relations orientées comme « ce verbe est gouverné par ce sujet ».