Aller au contenu

~/teaching · machine learning

Le mécanisme d'attention et le transformer, pas à pas

Une phrase simple, puis les maths, puis les chiffres à la main : plongements, requêtes, clés, valeurs, têtes, positions, encodeur, décodeur

Résumé

Un modèle de langue a besoin du sens d'un mot, et le sens d'un mot dépend des mots qui l'entourent. L'attention est l'opération qui permet à chaque mot d'une phrase d'emprunter aux autres, dans des proportions que le modèle apprend. Cet article la construit depuis la première idée jusqu'au transformer complet, dans l'ordre dont un étudiant a besoin : les plongements (embeddings), la moyenne pondérée, les requêtes, clés et valeurs, les têtes multiples, la position des mots, puis l'encodeur, le décodeur et la prédiction du mot suivant.

Chaque section s'ouvre par une phrase simple, donne ensuite les mathématiques, puis refait le calcul sur une phrase de quatre mots en deux dimensions, assez petite pour être vérifiée au crayon. Rien n'est caché derrière une bibliothèque : les douze lignes de NumPy de la fin reproduisent chaque nombre du texte.

Comment lire

  • Vous avez dix minutes : lisez les phrases simples et regardez les figures, elles racontent toute l'histoire.
  • Vous préparez un examen ou un entretien : suivez les listes « pas à pas » et refaites l'exemple sur papier ; les tableaux donnent les nombres à retrouver.
  • Vous voulez l'implémenter : allez au code de la fin, puis revenez aux sections pour les raisons de chaque ligne.

Cet article accompagne le cours de machine learning de Centrale Méditerranée.

Les plongements : un mot est un point dans l'espace

Un plongement transforme un mot en une liste de nombres, placée de sorte que les mots de sens proche soient voisins.

Un ordinateur ne peut pas comparer les mots actions et titres comme du texte : les deux chaînes n'ont aucune lettre commune. Il peut comparer deux listes de nombres. Un plongement (embedding) est une fonction qui donne à chaque jeton (token), un mot ou un morceau de mot, un vecteur de Rd\R^d, avec dd de quelques centaines à quelques milliers, entraînée pour que la géométrie reflète le sens : vecteurs proches pour des mots liés, éloignés pour des mots sans rapport.

Pas à pas

  1. Découper. Couper le texte en jetons. Dans cet article un jeton est un mot ; les vrais modèles coupent en sous-mots, pour que les mots rares soient couverts aussi.
  2. Chercher dans la table. Chaque jeton est une ligne d'une grande table E∈R∣V∣×dE \in \R^{|V| \times d}, où ∣V∣|V| est la taille du vocabulaire. Le plongement du jeton ii est simplement la ligne EiE_i.
  3. Comparer. Deux vecteurs se comparent par le cosinus de leur angle, qui ignore leur longueur : cos⁡θ=a⃗⋅b⃗∥a⃗∥ ∥b⃗∥\cos\theta = \dfrac{\vv{a}\cdot\vv{b}}{\lVert\vv{a}\rVert\,\lVert\vv{b}\rVert}. Proche de 1, même direction ; proche de 0, sans rapport ; proche de −1-1, opposés.
cos⁡θab=a⃗⋅b⃗∥a⃗∥ ∥b⃗∥=∑k=1dak bk∑kak2 ∑kbk2\cos\theta_{ab} = \frac{\vv{a}\cdot\vv{b}}{\lVert\vv{a}\rVert\,\lVert\vv{b}\rVert} = \frac{\sum_{k=1}^{d} a_k\, b_k}{\sqrt{\sum_k a_k^2}\,\sqrt{\sum_k b_k^2}}
(1)

La table EE est apprise, pas écrite à la main : on entraîne un modèle sur un grand corpus à prédire les mots à partir de leurs voisins, et les lignes de EE se placent là où la prédiction marche le mieux. Les régularités célèbres en découlent : dans un bon espace, le vecteur qui va de roi à reine est presque le même que celui qui va d'homme à femme.

L'exemple suivi dans tout l'article

Prenons la phrase Apple shares fell sharply (« les actions Apple ont chuté »), quatre jetons, et un espace jouet de dimension d=2d = 2. Lisons l'axe 1 comme « nourriture » et l'axe 2 comme « finance ». La table de plongements, entraînée sur du texte général, a placé Apple près du fruit, parce que c'est ce que le mot veut dire le plus souvent :

X=[x⃗Applex⃗sharesx⃗fellx⃗sharply]=[1.000.200.101.000.300.600.200.50]X = \begin{bmatrix} \vv{x}_{\text{Apple}} \\ \vv{x}_{\text{shares}} \\ \vv{x}_{\text{fell}} \\ \vv{x}_{\text{sharply}} \end{bmatrix} = \begin{bmatrix}1.00 & 0.20 \\ 0.10 & 1.00 \\ 0.30 & 0.60 \\ 0.20 & 0.50\end{bmatrix}
(2)
0.250.250.50.50.750.7511axe 1 : nourritureaxe 2 : financeApplesharesfellsharplyApple, après attention[0.36, 0.62]
Figure 1 : Les quatre jetons dans le plan jouet. Apple pointe vers la nourriture ; shares, fell et sharply vers la finance. La flèche en pointillé est l'endroit où l'attention déplacera Apple à la section 3.

Le cosinus entre Apple et shares vaut 0,29 : les plongements bruts disent que les deux mots ont peu à voir. Dans cette phrase, ils ont tout à voir. Cet écart est le problème que l'attention résout.

Retour au sommaire.

Le problème : un vecteur par mot, quelle que soit la phrase

Un plongement fixe donne le même vecteur à un mot dans toutes les phrases : il ne distingue pas un fruit d'une entreprise.

La table EE a une ligne par jeton. Apple reçoit le même vecteur dans an apple a day et dans Apple shares fell sharply ; intérêt reçoit le même vecteur qu'il soit un sentiment ou un taux ; obligation qu'elle soit morale ou financière. Les linguistes parlent de mots polysémiques ; pour un modèle, c'est un seul point qui devrait en être deux.

Ce que nous voulons, c'est une seconde suite y⃗1,…,y⃗L\vv{y}_1, \dots, \vv{y}_L, un vecteur par jeton, où y⃗i\vv{y}_i est le vecteur du jeton ii dans cette phrase : le même mot, repositionné par ses voisins. Cette représentation doit se construire à partir de la phrase elle-même, au moment où on la lit, pas se lire dans une table.

Remarque (Pourquoi une moyenne pondérée). La façon la plus simple, pour un jeton, de tenir compte de son contexte est de se mélanger un peu de chacun des autres. Un mélange est une moyenne pondérée, et toute la question devient : combien de chaque voisin, et qui décide.

Retour au sommaire.

Sitraka FORLERarchitecte systèmes, enseignant en machine learning
L'attention, pas à pas

L'attention comme moyenne pondérée

Chaque mot devient une moyenne pondérée de tous les mots de la phrase, avec plus de poids sur les mots qui comptent pour lui.

Soit la phrase, suite x⃗1,…,x⃗L\vv{x}_1, \dots, \vv{x}_L de plongements, LL jetons de dimension dd. Le vecteur contextuel du jeton ii est une somme pondérée de tous les jetons :

y⃗i=∑j=1Lwij x⃗j,wij≥0,∑j=1Lwij=1\vv{y}_i = \sum_{j=1}^{L} w_{ij}\,\vv{x}_j, \qquad w_{ij} \ge 0,\quad \sum_{j=1}^{L} w_{ij} = 1
(3)

Le poids wijw_{ij} dit combien le jeton jj compte dans la nouvelle version du jeton ii. Les contraintes font des poids une distribution de probabilité sur la phrase, donc y⃗i\vv{y}_i reste dans la même région de l'espace que les entrées : une vraie moyenne, pas une explosion.

Pas à pas

  1. Noter. Mesurer la pertinence de jj pour ii. La mesure la moins chère est le produit scalaire (dot product) sij=x⃗i⋅x⃗js_{ij} = \vv{x}_i\cdot\vv{x}_j : grand quand les deux vecteurs pointent dans la même direction, nul quand ils sont perpendiculaires.
  2. Normaliser. Transformer les LL notes de la ligne ii en poids positifs de somme 1 avec la fonction softmax : wij=esij∑ℓ=1Lesiℓw_{ij} = \dfrac{e^{s_{ij}}}{\sum_{\ell=1}^{L} e^{s_{i\ell}}}.
  3. Mélanger. Calculer y⃗i\vv{y}_i par la somme pondérée de l'équation (3).
  4. Recommencer pour chaque jeton. Les lignes n'interagissent pas : les LL calculs sont indépendants et se font en parallèle, ce qui explique pourquoi cette opération convient si bien aux GPU.
wij=softmax⁡j(si1,…,siL)=exp⁡(x⃗i⋅x⃗j)∑ℓ=1Lexp⁡(x⃗i⋅x⃗ℓ)w_{ij} = \softmax_j\big(s_{i1}, \dots, s_{iL}\big) = \frac{\exp(\vv{x}_i\cdot\vv{x}_j)}{\sum_{\ell=1}^{L}\exp(\vv{x}_i\cdot\vv{x}_\ell)}
(4)

En empilant les lignes, on obtient la forme matricielle. Avec X∈RL×dX \in \R^{L\times d}, la matrice des notes est S=XXTS = XX\T (l'entrée ijij vaut x⃗i⋅x⃗j\vv{x}_i\cdot\vv{x}_j), le softmax s'applique ligne par ligne, et la nouvelle suite est le produit des poids par les entrées :

Y=softmax⁡ ⁣(XXT)X,Y∈RL×dY = \softmax\!\left(XX\T\right) X, \qquad Y \in \R^{L\times d}
(5)

À la main, sur l'exemple

La ligne 1 est Apple. Ses produits scalaires avec les quatre jetons valent x⃗1⋅x⃗1=1,0×1,0+0,2×0,2=1,04\vv{x}_1\cdot\vv{x}_1 = 1{,}0\times1{,}0 + 0{,}2\times0{,}2 = 1,04, puis 0,30 avec shares, 0,42 avec fell et 0,30 avec sharply. Les exponentielles, divisées par leur somme, donnent les poids de la première ligne du tableau 1.

Tableau 1 : Poids d'attention wijw_{ij} avec le produit scalaire brut (ni projection ni mise à l'échelle). Chaque ligne somme à 1. Apple garde 40 % de lui-même et ne donne pas plus à shares qu'à sharply.
i\ji \backslash jApplesharesfellsharply
Apple0,4010,1910,2160,191
shares0,1760,3590,2450,220
fell0,2380,2930,2450,224
sharply0,2330,2900,2470,230

Le nouveau vecteur d'Apple est y⃗1=0.401 x⃗1+0.191 x⃗2+0.216 x⃗3+0.191 x⃗4=[0.523,  0.497]\vv{y}_1 = 0.401\,\vv{x}_1 + 0.191\,\vv{x}_2 + 0.216\,\vv{x}_3 + 0.191\,\vv{x}_4 = [0.523,\;0.497]. Il a quitté [1.00,  0.20][1.00,\;0.20] vers l'axe finance, ce qui est la bonne direction, mais surtout parce que trois mots sur quatre s'y trouvent. Rien dans le calcul ne savait que shares était le mot qui comptait.

produit scalaire brutApplesharesfellsharply0.400.190.220.190.180.360.250.220.240.290.250.220.230.290.250.23Applesharesfellsharplyavec la projection W^QApplesharesfellsharply0.210.320.250.230.360.200.230.210.300.240.240.220.290.240.240.23Applesharesfellsharplylignes : le jeton qui regarde ; colonnes : le jeton regardé
Figure 2 : Les mêmes poids en carte de chaleur, à gauche avec le produit scalaire brut, à droite après les projections apprises de la section 4. Plus foncé, plus lourd. À droite, la ligne d'Apple a déplacé son poids sur shares.

Remarque (Où la méthode cale). Le produit scalaire récompense les vecteurs qui pointent déjà dans la même direction. Si Apple et shares étaient perpendiculaires dans l'espace des plongements, leur note vaudrait 0 et shares recevrait le plus petit poids, exactement quand il devrait recevoir le plus grand. Les poids doivent être calculés dans un espace où les bonnes paires sont alignées, et cet espace doit être appris. C'est le travail de la section suivante.

Retour au sommaire.

Requêtes, clés et valeurs

Avant la comparaison, chaque mot est projeté trois fois : en une question qu'il pose, une étiquette à laquelle il répond, et un contenu qu'il transmet.

Dans la moyenne pondérée, un seul vecteur x⃗i\vv{x}_i jouait trois rôles à la fois : il demandait du contexte, il servait de point de comparaison, et il entrait dans le mélange. Séparer les trois rôles donne au modèle la liberté d'apprendre chacun. Imaginez une salle de lecture : un lecteur dépose une requête (query), ce que je cherche ; chaque livre porte une clé (key) sur sa tranche, ce dont je parle ; et le livre lui-même est la valeur (value), ce que vous obtenez en l'ouvrant. La pertinence se mesure entre la requête et les clés ; ce qui est rendu est un mélange des valeurs.

Pas à pas

  1. Projeter. Trois matrices apprises transforment l'entrée en requêtes, clés et valeurs : Q=XWQQ = XW^Q, K=XWKK = XW^K, V=XWVV = XW^V, avec WQ,WK∈Rd×dkW^Q, W^K \in \R^{d\times d_k} et WV∈Rd×dvW^V \in \R^{d\times d_v}.
  2. Noter. La note du jeton jj pour le jeton ii est le produit scalaire de la requête de ii avec la clé de jj : sij=q⃗i⋅k⃗js_{ij} = \vv{q}_i\cdot\vv{k}_j, soit S=QKTS = QK\T.
  3. Mettre à l'échelle. Diviser les notes par dk\sqrt{d_k} pour qu'elles restent dans la plage où le softmax a encore un gradient (remarque ci-dessous).
  4. Normaliser et mélanger. Softmax sur chaque ligne, puis produit par les valeurs.
Attention⁡(Q,K,V)=softmax⁡ ⁣(QKTdk)V\Attention(Q, K, V) = \softmax\!\left(\frac{QK\T}{\sqrt{d_k}}\right) V
(6)

C'est l'attention par produit scalaire mis à l'échelle (scaled dot-product attention) de l'article de 2017, et c'est tout le mécanisme. Tout ce qui suit (têtes, positions, encodeur, décodeur) se construit autour de cette ligne. Les matrices WQW^Q, WKW^K, WVW^V sont les paramètres que l'entraînement ajuste : la perte sur la prédiction du jeton suivant les pousse pour que les requêtes d'un jeton s'alignent sur les clés des jetons qui aident à prédire la suite.

pour tous les jetons à la fois : softmax(QKᵀ / √dₖ) Vxᵢjeton iW^QW^KW^Vqᵢkᵢvᵢclés de tous les jetonsvaleurs de tous les jetonsnotes qᵢ·kⱼsoftmax → poidssomme pondéréeΣⱼ wᵢⱼ vⱼyᵢvecteur contextuel
Figure 3 : Les trois projections d'un jeton, puis la note contre chaque clé, le softmax et le mélange des valeurs. La boîte grise s'applique à tous les jetons de la phrase à la fois, en un seul produit matriciel.

Remarque (Pourquoi diviser par la racine de la dimension des clés). Si les coordonnées de q⃗\vv{q} et k⃗\vv{k} sont indépendantes, de moyenne 0 et de variance 1, leur produit scalaire est une somme de dkd_k termes de variance 1 : sa variance vaut dkd_k et sa taille typique dk\sqrt{d_k}. Avec dk=64d_k = 64, les notes brutes s'étalent sur des dizaines d'unités, le softmax met toute sa masse sur un seul jeton, et son gradient s'évanouit. Diviser par dk\sqrt{d_k} ramène la variance à 1 quelle que soit la dimension.

À la main, sur l'exemple

D'abord la mise à l'échelle seule. Avec dk=2d_k = 2, les notes de la ligne 1 sont divisées par 2\sqrt{2} : 0,735, 0,212, 0,297, 0,212, et les poids deviennent 0,353, 0,209, 0,228, 0,209 : plus plats, comme attendu, puisque le softmax de nombres plus petits est plus proche de l'uniforme.

Puis une projection. Nous choisissons WQW^Q à la main pour montrer ce que l'entraînement pourrait découvrir : une matrice qui échange les deux axes. La requête d'un jeton demande alors l'autre lecture de lui-même. Apple, qui vit sur l'axe nourriture, demande la finance. WKW^K et WVW^V restent l'identité.

WQ=[0110],Q=XWQ=[0.201.001.000.100.600.300.500.20],K=V=XW^Q = \begin{bmatrix} 0 & 1 \\ 1 & 0 \end{bmatrix}, \qquad Q = XW^Q = \begin{bmatrix}0.20 & 1.00 \\ 1.00 & 0.10 \\ 0.60 & 0.30 \\ 0.50 & 0.20\end{bmatrix}, \qquad K = V = X
(7)

La requête d'Apple vaut maintenant [0.20,  1.00][0.20,\;1.00]. Ses notes mises à l'échelle contre les quatre clés sont 0,283, 0,721, 0,467, 0,382 : shares est désormais la meilleure correspondance, comme le montre le tableau 2.

Tableau 2 : Poids d'attention après la projection WQW^Q et la mise à l'échelle par dk\sqrt{d_k}. La ligne d'Apple met maintenant son plus grand poids sur shares ; la ligne de shares regarde vers Apple.
i\ji \backslash jApplesharesfellsharply
Apple0,2060,3190,2470,227
shares0,3610,2020,2270,210
fell0,2960,2400,2400,225
sharply0,2920,2380,2410,229

La sortie pour Apple devient y⃗1=[0.358,  0.623]\vv{y}_1 = [0.358,\;0.623], plus loin sur l'axe finance qu'avant, et cette fois parce que le bon mot l'a tirée. Une matrice 2×22\times2, choisie exprès, a fait ce qu'aucun produit scalaire sur les plongements bruts ne pouvait faire. Un modèle entraîné possède des milliers de telles matrices et les choisit à partir des données.

Retour au sommaire.

L'attention multi-têtes

Plusieurs attentions tournent côte à côte, chacune avec ses projections, pour que l'une suive le sens pendant qu'une autre suit la grammaire.

Un jeu de projections capte une sorte de relation. La matrice d'échange ci-dessus règle Apple ; elle ne dit rien sur le verbe qui va avec quel sujet, ni sur le mot situé deux places à gauche. Plutôt que de demander tout à une projection, le transformer fait tourner hh attentions en parallèle, les têtes (heads), chacune avec ses WiQW^Q_i, WiKW^K_i, WiVW^V_i de dimension réduite dk=dv=d/hd_k = d_v = d / h, puis recolle leurs sorties.

Pas à pas

  1. Séparer. La tête ii projette l'entrée dans son petit espace : headi=Attention⁡(XWiQ,  XWiK,  XWiV)\text{head}_i = \Attention(XW^Q_i,\; XW^K_i,\; XW^V_i), chaque sortie dans RL×dv\R^{L\times d_v}.
  2. Concaténer. Les hh sorties sont placées côte à côte, ce qui donne une matrice de RL×hdv=RL×d\R^{L\times h d_v} = \R^{L\times d}.
  3. Reprojeter. Une dernière matrice WO∈Rhdv×dW^O \in \R^{h d_v\times d} mélange les têtes en une représentation par jeton.
MultiHead⁡(X)=Concat⁡(head1,…,headh) WOheadi=Attention⁡ ⁣(XWiQ,  XWiK,  XWiV)\begin{aligned} \MultiHead(X) &= \Concat(\text{head}_1, \dots, \text{head}_h)\,W^O \\ \text{head}_i &= \Attention\!\left(XW^Q_i,\; XW^K_i,\; XW^V_i\right) \end{aligned}
(8)

Le coût est celui d'une seule tête de dimension pleine : hh têtes de dimension d/hd/h font autant d'arithmétique qu'une tête de dimension dd. Ce qu'on gagne, c'est la diversité. Dans les modèles entraînés, les sondes trouvent des têtes qui suivent la syntaxe (un verbe regarde son sujet), des têtes qui regardent le jeton précédent, des têtes qui recopient un mot rare vu plus tôt, et des têtes dont personne n'a nommé le rôle.

X (4 jetons, d = 2)tête 1 : axe financepoids d'Apple:0.23 0.27 0.25 0.25tête 2 : axe nourriturepoids d'Apple:0.43 0.17 0.21 0.19concaténerW^OApple, 2 têtes :[0.591, 0.544]
Figure 4 : Deux têtes sur l'exemple. Chaque tête voit un axe de l'espace jouet ; leurs sorties sont concaténées et projetées par WOW^O. Dans un vrai modèle, chaque tête voit une tranche apprise de dimension d/hd/h.

À la main, sur l'exemple

Prenons h=2h = 2 têtes de dimension dk=dv=1d_k = d_v = 1. La tête 1 ne garde que la coordonnée finance de chaque jeton (W1Q=W1K=W1V=[0,1]TW^Q_1 = W^K_1 = W^V_1 = [0, 1]\T), la tête 2 que la coordonnée nourriture ([1,0]T[1, 0]\T). Dans la tête 2, la ligne d'Apple vaut 0,425, 0,173, 0,211, 0,191 : Apple, seul mot de nourriture, se regarde surtout lui-même. Dans la tête 1, elle vaut 0,232, 0,272, 0,251, 0,246 : sur l'axe finance, les quatre jetons se ressemblent et les poids sont presque uniformes. La concaténation des deux sorties pour Apple donne [0.591,  0.544][0.591,\;0.544] ; avec WOW^O l'identité, c'est la sortie multi-têtes.

Remarque (Ce que disent les deux têtes). La tête 2 répond à qui suis-je, la tête 1 à quel est le marché autour de moi. Une seule tête de dimension 2 aurait dû fondre les deux questions en une seule note. C'est tout l'intérêt des têtes : des questions séparées, des poids séparés, une réponse combinée.

Retour au sommaire.

L'encodage de position : donner un ordre aux mots

L'attention traite la phrase comme un sac de mots : on ajoute donc à chaque jeton un vecteur qui décrit sa position, avant toute autre chose.

Regardez à nouveau l'équation (6) : rien n'y dépend de l'indice jj sauf à travers x⃗j\vv{x}_j. Permutez les jetons, les sorties sont permutées de la même façon, inchangées. Le fonds a racheté la banque et La banque a racheté le fonds donnent le même ensemble de vecteurs. L'ordre porte du sens et le mécanisme le jette.

Le remède consiste à ajouter à chaque plongement un vecteur qui ne dépend que de la position pp du jeton dans la suite, avant la première couche d'attention :

x⃗p←x⃗p+PE⁡(p),PE⁡(p)∈Rd\vv{x}_p \leftarrow \vv{x}_p + \PE(p), \qquad \PE(p) \in \R^d
(9)

Pas à pas

  1. Choisir un code par position. L'article de 2017 utilise des sinus et cosinus de fréquences espacées géométriquement : la coordonnée 2i2i de PE⁡(p)\PE(p) vaut sin⁡(p/100002i/d)\sin(p / 10000^{2i/d}), la coordonnée 2i+12i+1 vaut cos⁡(p/100002i/d)\cos(p / 10000^{2i/d}). Les coordonnées basses oscillent vite et distinguent les positions voisines ; les hautes oscillent lentement et ordonnent toute la suite.
  2. Ajouter, pas concaténer. L'addition conserve la dimension dd et laisse les projections WQW^Q, WKW^K lire position et contenu ensemble, ce qui permet à une tête d'apprendre à regarder le jeton juste avant moi.
  3. Laisser les couches s'en servir. À partir de là, chaque requête et chaque clé porte la position, et les notes peuvent dépendre de la distance autant que du sens.
PE⁡(p)2i=sin⁡ ⁣(p100002i/d),PE⁡(p)2i+1=cos⁡ ⁣(p100002i/d)\PE(p)_{2i} = \sin\!\left(\frac{p}{10000^{2i/d}}\right), \qquad \PE(p)_{2i+1} = \cos\!\left(\frac{p}{10000^{2i/d}}\right)
(10)

Remarque (Pourquoi des sinus et des cosinus). Pour un décalage kk fixé, le couple (sin⁡ωp,cos⁡ωp)(\sin\omega p, \cos\omega p) à la position p+kp+k est une rotation du couple à la position pp d'un angle ωk\omega k : une fonction linéaire, indépendante de pp. Une tête peut donc apprendre k positions à gauche comme une application linéaire fixe, quelle que soit la position absolue. Les encodages sont aussi bornés, et ils s'étendent à des longueurs jamais vues à l'entraînement. Beaucoup de modèles récents utilisent une variante rotative qui applique la même idée de rotation à l'intérieur des requêtes et des clés plutôt que de l'ajouter à l'entrée.

sin pcos pp = 0Applep = 1sharesp = 2fellp = 3sharplyPE(p) = [sin p, cos p] ; un pas = une rotation d'un radian
Figure 5 : L'encodage sinusoïdal pour d=2d = 2 : les deux coordonnées de PE⁡(p)\PE(p) pour les positions 0 à 3 sont des points du cercle unité aux angles 0, 1, 2 et 3 radians. Avancer d'une position est toujours la même rotation.

À la main, sur l'exemple

Avec d=2d = 2 il n'y a qu'une fréquence, 1/100000=11/10000^0 = 1, donc PE⁡(p)=[sin⁡p,cos⁡p]\PE(p) = [\sin p, \cos p] en radians. Le tableau 3 l'ajoute aux quatre plongements. Les deux phrases Apple shares fell sharply et Shares fell sharply, Apple donnent maintenant à Apple deux entrées différentes, [1.00,  1.20][1.00,\;1.20] en position 0 et [1.14,  −0.79][1.14,\;-0.79] en position 3, et tous les poids d'attention changent avec elles.

Tableau 3 : Encodage de position sinusoïdal pour d=2d = 2 et entrée positionnée X+PE⁡X + \PE. L'encodage ne dépend que de la position, jamais du mot.
jetonppsin⁡p\sin pcos⁡p\cos px1+sin⁡px_1 + \sin px2+cos⁡px_2 + \cos p
Apple00,0001,0001,0001,200
shares10,8410,5400,9411,540
fell20,909−0,4161,2090,184
sharply30,141−0,9900,341−0,490

Retour au sommaire.

Sitraka FORLERarchitecte systèmes, enseignant en machine learning
L'attention, pas à pas

Le bloc encodeur

Un bloc encodeur laisse les mots se parler (l'attention), puis laisse chaque mot réfléchir seul (un petit réseau de neurones), avec des raccourcis pour que l'entraînement reste stable.

Le transformer de Vaswani et al. (2017) a été construit pour transformer une suite en une autre, une phrase française en une phrase anglaise. Son encodeur (encoder) lit toute l'entrée et produit un vecteur riche en contexte par jeton d'entrée. C'est une pile de NN blocs identiques (6 dans l'article), et chaque bloc est fait de deux sous-couches enveloppées des deux mêmes astuces.

Un bloc encodeur, entrée X∈RL×dX \in \R^{L\times d}

  1. Auto-attention multi-têtes. A=MultiHead⁡(X)A = \MultiHead(X) : requêtes, clés et valeurs viennent toutes de XX, d'où le nom d'auto-attention (self-attention). L'information passe entre les jetons ici, et seulement ici.
  2. Connexion résiduelle et normalisation. X′=LayerNorm⁡(X+A)X' = \LayerNorm(X + A). Rajouter l'entrée (le résidu) signifie qu'un bloc n'a qu'une correction à apprendre, et que les gradients traversent la pile tout droit ; normaliser chaque vecteur de jeton à moyenne 0 et variance 1 garde l'échelle stable de bloc en bloc.
  3. Réseau à propagation avant. F=FFN⁡(X′)F = \FFN(X'), le même réseau à deux couches (feed-forward network) appliqué à chaque jeton indépendamment : FFN⁡(x⃗)=W2 max⁡(0,W1x⃗+b⃗1)+b⃗2\FFN(\vv{x}) = W_2\,\max(0, W_1\vv{x} + \vv{b}_1) + \vv{b}_2, avec une largeur cachée de 4d4d dans l'article. Aucune information ne passe entre les jetons ici ; chaque vecteur est transformé seul.
  4. Résidu et normalisation à nouveau. Z=LayerNorm⁡(X′+F)Z = \LayerNorm(X' + F) est la sortie du bloc et l'entrée du suivant.
X′=LayerNorm⁡(X+MultiHead⁡(X))Z=LayerNorm⁡(X′+FFN⁡(X′))\begin{aligned} X' &= \LayerNorm\big(X + \MultiHead(X)\big) \\ Z &= \LayerNorm\big(X' + \FFN(X')\big) \end{aligned}
(11)
LayerNorm⁡(x⃗)=γ⊙x⃗−μσ2+ϵ+βμ=1d∑k=1dxk,σ2=1d∑k=1d(xk−μ)2\begin{aligned} \LayerNorm(\vv{x}) &= \gamma \odot \frac{\vv{x} - \mu}{\sqrt{\sigma^2 + \epsilon}} + \beta \\ \mu &= \frac{1}{d}\sum_{k=1}^{d} x_k, \qquad \sigma^2 = \frac{1}{d}\sum_{k=1}^{d} (x_k - \mu)^2 \end{aligned}
(12)

L'attention mélange à travers la phrase ; le réseau à propagation avant travaille à l'intérieur de chaque jeton. La répartition est voulue : l'attention décide où regarder, le réseau décide quoi en faire. Empiler NN blocs permet à un jeton de rassembler du contexte sur le contexte : après deux blocs, Apple peut utiliser ce que shares a lui-même appris de fell.

bloc encodeur (× N)entrée + positionsauto-attention multi-têtesrésidu & norm.réseau avantrésidu & norm.Z : un vecteur par jeton d'entréebloc décodeur (× N)sortie déjà écrite + positionsauto-attention masquéerésidu & norm.attention croiséerésidu & norm.réseau avantrésidu & norm.clés, valeurslinéaire + softmax → jeton suivant
Figure 6 : Le bloc encodeur (à gauche) et le bloc décodeur (à droite). Chaque flèche qui contourne une sous-couche est une connexion résiduelle suivie d'une normalisation de couche. La sous-couche du milieu du décodeur prend ses clés et ses valeurs dans la sortie de l'encodeur.

Retour au sommaire.

Le bloc décodeur

Le décodeur écrit la sortie un jeton à la fois, en regardant ce qu'il a déjà écrit et la lecture de l'entrée faite par l'encodeur.

Le décodeur (decoder) produit la suite de sortie jeton par jeton. À l'étape tt, il a déjà produit t−1t-1 jetons, plongés et positionnés dans Y<t∈R(t−1)×dY_{<t} \in \R^{(t-1)\times d}, et il dispose de la sortie de l'encodeur Z∈RL×dZ \in \R^{L\times d}. Chacun de ses NN blocs a trois sous-couches, chacune enveloppée de la connexion résiduelle et de la normalisation de l'encodeur.

Un bloc décodeur

  1. Auto-attention masquée. Attention multi-têtes sur les jetons déjà écrits, avec un changement : un jeton ne peut regarder que les positions avant lui. Dans la matrice des notes, chaque entrée sijs_{ij} avec j>ij > i est mise à −∞-\infty avant le softmax, et son poids devient exactement 0. Sans ce masque, l'entraînement laisserait le modèle lire la réponse qu'il est censé prédire.
  2. Attention croisée. Les requêtes viennent des vecteurs du décodeur ; les clés et les valeurs viennent de la sortie de l'encodeur ZZ. C'est là que la sortie regarde l'entrée : en écrivant le troisième mot anglais, le décodeur demande quels mots français lui sont pertinents. C'est le seul endroit où les deux moitiés du modèle se rencontrent.
  3. Réseau à propagation avant. Le même réseau par jeton que dans l'encodeur.
Attention⁡masqueˊe(Q,K,V)=softmax⁡ ⁣(QKTdk+M)VMij={0j≤i−∞j>i\begin{aligned} \Attention_{\text{masquée}}(Q, K, V) &= \softmax\!\left(\frac{QK\T}{\sqrt{d_k}} + M\right) V \\ M_{ij} &= \begin{cases} 0 & j \le i \\ -\infty & j > i \end{cases} \end{aligned}
(13)
Croiseˊe(Y,Z)=Attention⁡ ⁣(YWQ,  ZWK,  ZWV)\text{Croisée}(Y, Z) = \Attention\!\left(YW^Q,\; ZW^K,\; ZW^V\right)
(14)

Remarque (Encodeur-décodeur, encodeur seul, décodeur seul). Le modèle de 2017 a les deux moitiés parce qu'il traduit. Les modèles qui classent ou plongent du texte ne gardent que l'encodeur (BERT en est un). Les grands modèles de langue qui écrivent du texte ne gardent que le décodeur : il n'y a pas d'entrée séparée à regarder, la consigne et la réponse forment une seule suite, et le masque est ce qui rend la génération de gauche à droite.

Retour au sommaire.

Prédire le jeton suivant

Le dernier vecteur du décodeur est transformé en une note pour chaque mot du vocabulaire, les notes deviennent des probabilités, et un mot est choisi.

Après le dernier bloc du décodeur, le vecteur à la dernière position, h⃗t∈Rd\vv{h}_t \in \R^d, résume tout ce qui a été écrit et tout ce qui a été lu dans l'entrée. Une couche linéaire le transforme en une note par entrée du vocabulaire, et un softmax transforme les ∣V∣|V| notes en une distribution de probabilité :

P(jeton suivant=v∣contexte)=softmax⁡ ⁣(Wout h⃗t+b⃗)vWout∈R∣V∣×d\begin{aligned} P(\text{jeton suivant} = v \mid \text{contexte}) &= \softmax\!\left(W^{\text{out}}\,\vv{h}_t + \vv{b}\right)_v \\ W^{\text{out}} &\in \R^{|V|\times d} \end{aligned}
(15)

La boucle de génération

  1. Noter le vocabulaire. Calculer la distribution ci-dessus pour le préfixe courant.
  2. Choisir un jeton. Le plus probable (décodage glouton), ou un tirage selon la distribution, souvent après l'avoir accentuée ou aplatie par une température, ou en ne gardant que les meilleurs candidats.
  3. Ajouter et recommencer. Le jeton choisi rejoint le préfixe, reçoit son plongement et sa position, et le décodeur tourne à nouveau. On s'arrête au jeton de fin de séquence ou à une longueur maximale.

À l'entraînement, il n'y a pas de boucle : toute la phrase cible est donnée au décodeur d'un coup, le masque cache l'avenir, et la perte est l'entropie croisée entre la distribution prédite à chaque position et le vrai jeton suivant, sommée sur la phrase. On appelle cela le forçage par l'enseignant (teacher forcing), et c'est pourquoi l'entraînement d'un transformer se parallélise sur la séquence alors que la génération ne le peut pas.

Remarque (Coût). L'auto-attention compare chaque jeton à tous les autres : L2L^2 notes de dkd_k multiplications chacune, soit un temps en O(L2d)O(L^2 d) et une mémoire en O(L2)O(L^2) par tête et par couche. Ce terme quadratique est la raison pour laquelle les longs contextes coûtent cher, et la raison d'être de toute une littérature sur les attentions creuses ou approchées.

Retour au sommaire.

Sitraka FORLERarchitecte systèmes, enseignant en machine learning
L'attention, pas à pas

Toute la passe avant, dans l'ordre

  1. Découper et plonger. Couper le texte en jetons ; chercher chacun dans la table EE pour obtenir X∈RL×dX \in \R^{L\times d}.
  2. Ajouter les positions. X←X+PE⁡X \leftarrow X + \PE, pour que l'ordre des mots survive à l'étape suivante.
  3. Projeter. Pour chaque tête, Q=XWQQ = XW^Q, K=XWKK = XW^K, V=XWVV = XW^V.
  4. Noter, mettre à l'échelle, masquer. S=QKT/dkS = QK\T / \sqrt{d_k}, plus −∞-\infty au-dessus de la diagonale dans un décodeur.
  5. Normaliser. Softmax sur chaque ligne : chaque jeton obtient une distribution sur la phrase.
  6. Mélanger. Multiplier les poids par VV ; concaténer les têtes ; projeter par WOW^O.
  7. Résidu, normalisation, réseau avant, résidu, normalisation. Un bloc est fini ; répéter NN fois.
  8. Lire le dernier vecteur. Couche linéaire vers le vocabulaire, softmax, choisir un jeton, l'ajouter, revenir à l'étape 1 pour le nouveau préfixe.

La même arithmétique en douze lignes de NumPy

Douze lignes qui refont la section 4 sur la matrice XX de l'exemple, avec la projection WQW^Q qui échange les axes. Les deux lignes imprimées sont la première ligne du tableau 2 et la sortie d'Apple.

import numpy as np

def attention(Q, K, V, mask=None):
    """Scaled dot-product attention, one line per step of section 4."""
    d_k = K.shape[-1]
    S = Q @ K.T / np.sqrt(d_k)            # scores, scaled
    if mask is not None:
        S = S + mask                      # -inf above the diagonal
    W = np.exp(S - S.max(axis=-1, keepdims=True))
    W = W / W.sum(axis=-1, keepdims=True) # softmax, row by row
    return W @ V, W                       # mix of the values, and the weights

# Apple, shares, fell, sharply
X = np.array([[1.0, 0.2], [0.1, 1.0], [0.3, 0.6], [0.2, 0.5]])
W_Q = np.array([[0.0, 1.0], [1.0, 0.0]])   # the axis swap of section 4
Y, W = attention(X @ W_Q, X, X)
print(W[0].round(3))   # [0.206 0.319 0.247 0.227]: Apple looks at shares
print(Y[0].round(3))   # [0.358 0.623]

Glossaire

Plongement (embedding)
Une application apprise des jetons vers des vecteurs, entraînée pour que la distance reflète le sens. Une ligne par jeton dans une table EE.
Jeton (token)
L'unité que lit le modèle : un mot, un sous-mot ou un caractère, selon le découpeur.
Attention
Une moyenne pondérée des valeurs d'une suite, avec des poids calculés d'après l'accord entre une requête et toutes les clés.
Requête, clé, valeur (query, key, value)
Trois projections du même jeton : ce qu'il cherche, ce à quoi il répond, ce qu'il transmet.
Tête (head)
Une attention avec ses propres projections ; plusieurs tournent en parallèle et sont concaténées.
Encodage de position (positional encoding)
Un vecteur ajouté à chaque plongement, qui code la position du jeton pour que l'ordre ne soit pas perdu.
Connexion résiduelle (residual connection)
Ajouter l'entrée d'une sous-couche à sa sortie, pour que la sous-couche apprenne une correction et que les gradients passent.
Normalisation de couche (layer normalisation)
Ramener chaque vecteur de jeton à moyenne 0 et variance 1, puis une échelle et un décalage appris.
Encodeur (encoder)
La pile de blocs qui lit la suite d'entrée et produit un vecteur contextuel par jeton.
Décodeur (decoder)
La pile de blocs qui écrit la sortie un jeton à la fois, avec un masque causal et une attention croisée vers l'encodeur.
Softmax
La fonction qui transforme une liste de notes en poids positifs de somme 1, par exponentielle puis division par le total.

Références

  1. A. Vaswani, N. Shazeer, N. Parmar, J. Uszkoreit, L. Jones, A. N. Gomez, Ł. Kaiser, I. Polosukhin, Attention Is All You Need, NeurIPS 2017. arXiv:1706.03762.
  2. D. Bahdanau, K. Cho, Y. Bengio, Neural Machine Translation by Jointly Learning to Align and Translate, ICLR 2015. arXiv:1409.0473. Le premier mécanisme d'attention, additif plutôt que par produit scalaire.
  3. J. L. Ba, J. R. Kiros, G. E. Hinton, Layer Normalization, 2016. arXiv:1607.06450.
  4. J. Su, Y. Lu, S. Pan, A. Murtadha, B. Wen, Y. Liu, RoFormer: Enhanced Transformer with Rotary Position Embedding, 2021. arXiv:2104.09864. La variante rotative citée à la section 6.
  5. J. Alammar, The Illustrated Transformer, 2018. jalammar.github.io.
  6. P. Moskwa, Attention, please! Demystifying the math behind the attention mechanism and the transformer model, 2025. paulinamoskwa.github.io. L'article dont celui-ci suit le chemin, avec une implémentation PyTorch.
  7. A. Rush et al., The Annotated Transformer, Harvard NLP, 2018, révisé 2022. nlp.seas.harvard.edu. L'article de 2017 réimplémenté ligne par ligne.

À propos de cet article

Écrit pour les élèves de l'option machine learning de Centrale Méditerranée, et pour quiconque a lu un tutoriel sur les transformers sans parvenir à refaire les chiffres. Le chemin des plongements au modèle complet est le chemin classique ; la phrase, l'exemple en deux dimensions, la projection choisie à la main et le code sont propres à cet article. Les mathématiques sont celles de Vaswani et al. (2017), avec la normalisation de couche de Ba et al. (2016).

Rédigé avec l'aide de Claude Code, un produit d'Anthropic ; chaque formule a été vérifiée sur les articles cités et chaque nombre est recalculé par le code de la page à chaque construction.

Révision : . Première publication le même jour.