Aller au contenu
English

Le transformeur original, étape par étape

  • Leçon facultative 4
  • 60 min
  • Module 8 : Sous le capot : les mathématiques · Facultative, non requise pour le certificat

Ce que vous saurez faire

Lisez l’article de 2017 à l’origine du transformeur, une étape à la fois. Vous verrez chaque formule expliquée en mots, avec un petit nombre que vous pouvez vérifier. Puis vous suivrez un tout petit exemple dans toute la machine, avec ses formes. Vous la verrez apprendre, puis découvrirez ce que les modèles suivants ont changé.

Carnet JupyterConstruire le transformeur original dans un carnet JupyterColab · Kaggle · GitHub · en anglais

Le carnet (notebook) Jupyter construit l’encodage positionnel et une couche d’encodeur à partir des formules, puis les vérifie. Ensuite, il entraîne le petit modèle d’inversion pendant 1 500 étapes (cette leçon en utilise 3 000). Il décode un exemple étape par étape, trace l’attention croisée et compte les paramètres du modèle de base. Prévoyez environ 40 minutes, dont environ 1 minute d’entraînement sur un processeur gratuit de Colab.

Le carnet est en anglais. Colab et Kaggle ouvrent votre propre copie. Il faut un compte Google ou Kaggle gratuit. Vous êtes bloqué ? Consultez le carnet de solution.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 60 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

  • Rien pour cette page. Le carnet s’ouvre dans Colab ou Kaggle avec PyTorch déjà installé; aucun téléchargement de données n’est nécessaire.

Testé avec Python 3.12 et PyTorch 2.14.1 sur le processeur (macOS).

Cette leçon est facultative : le certificat ne l’exige pas.

Vous avez vu le principe dans Les blocs du transformeur et Position et masquage. Un bloc lit les autres jetons grâce à l’attention, puis traite chaque jeton avec une couche à propagation avant. Les connexions résiduelles et la normalisation par couche assurent sa stabilité; les positions et les masques contrôlent ce que chaque jeton voit. SmolLM2-135M, dans Entraînement et inférence, empile 30 blocs de ce type.

Tout cela remonte à un article : Attention Is All You Need, de Vaswani, Shazeer, Parmar, Uszkoreit, Jones, Gomez, Kaiser et Polosukhin. Il a été publié à NeurIPS 2017 (arXiv:1706.03762). Cette page présente les mathématiques. La question à laquelle elle répond : que calcule exactement le transformeur original, du premier jeton à la dernière probabilité? Et lesquels de ses choix les modèles utilisent-ils encore aujourd’hui?

L’article a construit un traducteur, pas un modèle de conversation. Son principal test utilisait les données anglais–allemand de WMT 2014. Les deux langues partageaient un vocabulaire d’environ 37 000 jetons représentant des parties de mots. La phrase source est complète; la cible s’écrit un jeton à la fois.

Le modèle comporte donc deux moitiés. L’encodeur lit toute la phrase source, sans masque causal, et produit un vecteur par jeton source : la mémoire. Le décodeur écrit la cible. Il lit les jetons déjà écrits, avec un masque causal, et lit aussi la mémoire à chaque couche. Cette lecture de l’autre texte est l’attention croisée, la seule partie que vous n’avez pas encore vue.

Pour montrer chaque étape avec des nombres réels, cette page utilise un tout petit modèle suivant exactement la recette de l’article. Sa tâche simplifiée : inverser une séquence de lettres. La source « c a b e d » doit devenir « d e b a c », suivie d’un jeton de fin </s>. Le vocabulaire compte 13 jetons : les lettres a à j, un jeton de remplissage, un jeton de début <s> et </s>. Le modèle possède 2 couches d’encodeur et 2 couches de décodeur, d_model = 64, 4 têtes et une dimension de propagation avant de 256 : 234 304 paramètres. (d_model est le nom donné dans l’article à d, le nombre de nombres par jeton dans Les mathématiques de l’attention.) Le modèle de base de l’article possède 6 + 6 couches, d_model = 512, 8 têtes et une dimension de 2 048.

Le schéma reprend la figure d’architecture de l’article, avec la forme sur chaque flèche pour cet exemple.

Le transformeur original pour l'exemple c a b e d. Encodeur, à gauche : jetons sources (1, 5); plongement multiplié par 8, tiré d'une table partagée de 13 par 64, donnant (1, 5, 64); ajout de l'encodage positionnel sinusoïdal, (1, 5, 64); puis une couche d'encodeur répétée 2 fois (6 dans l'article) : autoattention multitête avec 4 têtes de 16, poids (1, 4, 5, 5), addition et LayerNorm, propagation avant de 64 à 256 à 64 avec ReLU, addition et LayerNorm, chaque sortie étant de forme (1, 5, 64); la mémoire (1, 5, 64). Décodeur, à droite : cible écrite jusqu'ici, jeton de début puis d e b a c, (1, 6); plongement multiplié par 8, tiré de la même table, (1, 6, 64); ajout de l'encodage positionnel; une couche de décodeur répétée 2 fois : autoattention masquée, poids (1, 4, 6, 6), addition et LayerNorm, attention croisée avec les requêtes du décodeur et les clés et valeurs de la mémoire, poids (1, 4, 6, 5), addition et LayerNorm, propagation avant, addition et LayerNorm, chaque sortie étant de forme (1, 6, 64); puis une couche linéaire qui multiplie par la table de plongements transposée, logits (1, 6, 13); softmax; le jeton suivant à chaque position, d e b a c et le jeton de fin. Les lignes pointillées montrent les connexions résiduelles.
L'encodeur-décodeur de l'article de 2017, avec la forme sur chaque liaison pour un exemple du petit modèle. La mémoire alimente l'attention croisée de chaque couche du décodeur.
Description textuelle du diagramme

Deux colonnes. L’encodeur, à gauche, commence par les jetons sources, de forme (1, 5) : une séquence de 5 jetons. Le plongement (embedding) multiplie la ligne de chaque jeton par 8, la racine carrée de 64. La table partagée compte 13 lignes de 64 nombres. La forme est maintenant (1, 5, 64). On ajoute l’encodage positionnel sinusoïdal, sans changer la forme. Vient ensuite une couche d’encodeur, répétée 2 fois ici et 6 fois dans l’article. Elle contient une autoattention multitête avec 4 têtes de 16 nombres, dont les poids ont la forme (1, 4, 5, 5). Puis viennent l’addition et LayerNorm. Ensuite, une couche à propagation avant passe de 64 à 256 nombres, puis revient à 64, avec ReLU. Elle est suivie à nouveau de l’addition et de LayerNorm. Chaque flèche dans la couche porte la forme (1, 5, 64). Les lignes pointillées font passer l’entrée de chaque sous-couche autour de celle-ci jusqu’à la case d’addition et de LayerNorm : ce sont les connexions résiduelles. La sortie de l’encodeur est la mémoire, (1, 5, 64).

Le décodeur, à droite, commence par la cible écrite jusqu’ici : le jeton de début et d e b a c, de forme (1, 6). Il utilise la même table de plongements, multipliée par 8, et le même encodage positionnel : (1, 6, 64). Une couche de décodeur est répétée 2 fois. Elle contient une autoattention masquée avec des poids (1, 4, 6, 6), puis une attention croisée, puis la couche à propagation avant. Chacune est suivie de l’addition et de LayerNorm. Dans l’attention croisée, les requêtes viennent du décodeur, et les clés et valeurs viennent de la mémoire. Ses poids ont la forme (1, 4, 6, 5). Une flèche amène la mémoire de l’encodeur à cette case. Après les couches du décodeur, une couche linéaire multiplie par la table de plongements transposée, sans nouveaux poids. Elle donne des logits (scores avant softmax) de forme (1, 6, 13). Softmax donne le jeton suivant à chaque position : d e b a c et le jeton de fin. Une note compare les dimensions : ce petit modèle a d_model 64, 4 têtes et d_ff 256. Le modèle de base de l’article a 512, 8 têtes, 2 048 et 6 + 6 couches.

Les prochaines sections suivent les flèches : la formule de l’article, son sens, un nombre à vérifier à la main, puis une vérification en code.

Étape 1 : plongements de jetons, multipliés par √d_model

Section intitulée « Étape 1 : plongements de jetons, multipliés par √d_model »

Chaque identifiant de jeton sélectionne sa ligne dans une table de plongements, comme dans Jetons et plongements. L’article ajoute un détail (§3.4) : « Dans les couches de plongement, nous multiplions ces poids par √d_model. »

xpos=dmodel  E[tokenpos]+PE(pos)x_{\text{pos}} = \sqrt{d_{\text{model}}}\; E[\text{token}_{\text{pos}}] + PE(\text{pos})

Ici, E est la table de plongements, avec une ligne de d_model nombres par jeton. token_pos est le jeton à la position pos, et PE(pos) est l’encodage positionnel de l’étape 2. En mots : trouvez la ligne, multipliez-la par √d_model et ajoutez le motif de la position.

Pourquoi multiplier? La table du petit modèle commence avec une dispersion de 1/√64 = 0,125, tandis que les positions vont de −1 à 1. Sans ce facteur, les positions domineraient le jeton. Le jeton « c » commence par −0,023, −0,006, −0,027 et 0,023. Multipliés par 8, ces nombres donnent environ −0,186, −0,045, −0,219 et 0,187.

L’article utilise une seule table pour la source, la cible et la couche de sortie de l’étape 5. Les deux langues partagent en effet un vocabulaire.

Position et masquage donnait à chaque position un vecteur appris. L’article utilise plutôt un motif fixe d’ondes (§3.5). Pour la position pos et la paire de colonnes i (i va donc de 0 à d_model/2 − 1) :

PE(pos,2i)=sin⁡(pos100002i/dmodel)PE(pos,2i+1)=cos⁡(pos100002i/dmodel)PE(\text{pos}, 2i) = \sin\left(\frac{\text{pos}}{10000^{2i/d_{\text{model}}}}\right) \qquad PE(\text{pos}, 2i+1) = \cos\left(\frac{\text{pos}}{10000^{2i/d_{\text{model}}}}\right)

En mots : la paire de colonnes i contient un sinus et un cosinus de la position, ralentis par le facteur 10000^(2i/d_model). La première paire tourne vite, et chaque paire suivante tourne plus lentement.

Vérifiez à la main à la position 1. Pour i = 0, le diviseur est 10000⁰ = 1. Les deux premiers nombres sont donc sin 1 = 0,841 et cos 1 = 0,540. Pour i = 1 et d_model = 64, le diviseur est 10000^(2/64) = 1,334. L’angle est donc 1 / 1,334 = 0,750. Les deux nombres suivants sont sin 0,750 = 0,682 et cos 0,750 = 0,732.

positions.py
import math
import torch
def sinusoidal_positions(n, d):
pos = torch.arange(n, dtype=torch.float).unsqueeze(1) # (n, 1): positions 0, 1, 2, ...
two_i = torch.arange(0, d, 2, dtype=torch.float) # 2i = 0, 2, 4, ..., d - 2
angle = pos / torch.pow(10000.0, two_i / d) # (n, d/2): one angle per pair
pe = torch.zeros(n, d)
pe[:, 0::2] = torch.sin(angle) # even columns 2i: sine
pe[:, 1::2] = torch.cos(angle) # odd columns 2i + 1: cosine
return pe
pe = sinusoidal_positions(50, 64)
print(tuple(pe.shape))
print(pe[1, :4].round(decimals=3))
print(round(math.sin(1), 3), round(math.cos(1), 3), round(math.sin(1 / 10000 ** (2 / 64)), 3))

La sortie :

(50, 64)
tensor([0.8410, 0.5400, 0.6820, 0.7320])
0.841 0.54 0.682

0::2 prend une colonne sur deux à partir de la colonne 0, et 1::2 à partir de la colonne 1. La table ne dépend pas des jetons; on la calcule donc une seule fois.

  • Positif
  • Négatif
L'encodage positionnel sinusoïdal pour 50 positions et d_model = 64, calculé avec la formule de l'article. Les colonnes de gauche changent vite d'une ligne à l'autre; celles de droite changent lentement.
Description textuelle du graphique

Une grille de 50 lignes, les positions 0 à 49, et de 64 colonnes. Les cellules bleues sont positives et les cellules orange sont négatives. Plus la couleur est intense, plus la valeur est proche de +1 ou de −1. Dans les premières colonnes, la couleur change d’une ligne à l’autre en courtes bandes, car ces sinus et cosinus tournent vite. Plus à droite, les bandes s’allongent. Dans les quelque 20 dernières colonnes, la couleur change à peine sur les 50 positions. Les colonnes paires restent près de 0, et les colonnes impaires, les cosinus, restent près de +1 (bleu intense). Chaque ligne forme un motif différent; chaque position reçoit donc sa propre empreinte.

Chaque colonne est une onde. Sa longueur d’onde, le nombre de positions pour un tour complet, est 2π · 10000^(2i/d_model). L’article dit (§3.5) : « Les longueurs d’onde forment une progression géométrique de 2π à 10000 · 2π. »

for d in (64, 512):
lengths = [2 * math.pi * 10000 ** (2 * i / d) for i in (0, 1, d // 4, d // 2 - 1)]
print(d, [f"{v:,.1f}" for v in lengths])

La sortie :

64 ['6.3', '8.4', '628.3', '47,117.2']
512 ['6.3', '6.5', '628.3', '60,611.5']

Les colonnes rapides distinguent les positions proches, comme l’aiguille des secondes d’une horloge. Les colonnes lentes distinguent les positions éloignées, comme l’aiguille des heures.

  • column 0 (wavelength 6.3)
  • column 8 (wavelength 19.9)
  • column 20 (wavelength 111.7)
Colonnes 0, 8 et 20 de l'encodage positionnel (d_model = 64), calculées avec la formule. Le modèle utilise les valeurs aux positions entières; les courbes les relient.
Description textuelle du graphique

Trois courbes sinusoïdales entre −1 et +1 sur les positions 0 à 60. La colonne 0 a une longueur d’onde de 6,3 positions; elle monte et descend donc presque dix fois. La colonne 8 a une longueur d’onde de 19,9 positions et fait trois tours. La colonne 20 a une longueur d’onde de 111,7 positions. Elle monte lentement depuis 0 et atteint son sommet seulement après environ 28 positions.

Pourquoi des ondes? Les positions relatives deviennent une rotation. L’article a choisi ce motif « parce que nous supposions qu’il permettrait au modèle d’apprendre facilement à diriger l’attention selon les positions relatives. Pour tout décalage fixe k, PE_pos+k peut en effet être représenté comme une fonction linéaire de PE_pos. » Prenez une paire de colonnes de fréquence ω = 1/10000^(2i/d_model). Selon les règles d’addition des angles du sinus et du cosinus :

(sin⁡(ω(pos+k))cos⁡(ω(pos+k)))=(cos⁡ωksin⁡ωk−sin⁡ωkcos⁡ωk)(sin⁡ω poscos⁡ω pos)\begin{pmatrix} \sin(\omega(\text{pos}+k)) \\ \cos(\omega(\text{pos}+k)) \end{pmatrix} = \begin{pmatrix} \cos \omega k & \sin \omega k \\ -\sin \omega k & \cos \omega k \end{pmatrix} \begin{pmatrix} \sin \omega\,\text{pos} \\ \cos \omega\,\text{pos} \end{pmatrix}

La matrice 2 × 2 est une rotation d’angle ωk. Elle dépend seulement du décalage k, pas de pos. Donc, « 3 positions plus loin » correspond à la même matrice à la position 0, 10 ou 40 :

k, i = 3, 5 # move 3 positions; look at the pair i = 5 (columns 10, 11)
w = 1 / 10000 ** (2 * i / 64) # the frequency of this pair
turn = torch.tensor([[math.cos(w * k), math.sin(w * k)],
[-math.sin(w * k), math.cos(w * k)]])
for pos in (0, 10, 40):
print(pos, (turn @ pe[pos, 10:12]).round(decimals=4), pe[pos + k, 10:12].round(decimals=4))

La sortie :

0 tensor([0.6529, 0.7574]) tensor([0.6529, 0.7574])
10 tensor([ 0.0588, -0.9983]) tensor([ 0.0588, -0.9983])
40 tensor([-0.6977, -0.7164]) tensor([-0.6977, -0.7164])

La paire après rotation égale la paire située 3 positions plus loin, à chaque position. L’attention peut donc apprendre à regarder « k jetons en arrière ». L’article a aussi essayé des positions apprises et « constaté que les deux versions produisaient des résultats presque identiques ». Il a conservé les sinusoïdes parce qu’elles « pourraient permettre au modèle d’extrapoler à des séquences plus longues que celles rencontrées pendant l’entraînement ».

À la position 0, chaque sinus vaut 0 et chaque cosinus vaut 1. L’entrée de l’encodeur pour « c » est donc le plongement multiplié par le facteur, plus (0, 1, 0, 1, …) : −0,186, 0,955, −0,219, 1,187.

Une couche d’encodeur comporte deux sous-couches : l’autoattention multitête, puis un réseau à propagation avant appliqué à chaque position. L’article entoure chacune de la même façon (§3.1) : « la sortie de chaque sous-couche est LayerNorm(x + Sublayer(x)) ». Avec le dropout (désactivation aléatoire), utilisé pendant l’entraînement (§5.4) :

output=LayerNorm(x+Dropout(Sublayer(x)))\text{output} = \text{LayerNorm}\big(x + \text{Dropout}(\text{Sublayer}(x))\big)

En mots : calculez le changement, désactivez-en 10 % pendant l’entraînement, ajoutez l’entrée (la connexion résiduelle), puis normalisez. La normalisation vient après l’addition : c’est la post-normalisation.

La première sous-couche est l’attention multitête de Les mathématiques de l’attention. Q, K et V proviennent tous de la même entrée X, avec une ligne par jeton :

MultiHead(X)=[head1,…,headh] WO,headi=softmax ⁣(XWQ(i) (XWK(i))⊤dk)XWV(i)\text{MultiHead}(X) = [\text{head}_1, \ldots, \text{head}_h]\, W_O, \qquad \text{head}_i = \text{softmax}\!\left(\frac{X W_Q^{(i)} \,(X W_K^{(i)})^\top}{\sqrt{d_k}}\right) X W_V^{(i)}

W_Q^(i), W_K^(i) et W_V^(i) sont les matrices de poids de la tête i, chacune de forme d_model × d_k. W_O mélange les têtes réunies pour revenir à d_model nombres. (L’article écrit W_i^Q et W^O; ce module conserve la notation de la leçon précédente.) Avec h têtes, d_k = d_model / h : 512 / 8 = 64 dans l’article, 64 / 4 = 16 dans le petit modèle.

LayerNorm remet à l’échelle les d_model nombres de chaque jeton séparément :

LayerNorm(x)=γ⊙x−μσ2+ϵ+β,μ=1d∑i=1dxi,σ2=1d∑i=1d(xi−μ)2\text{LayerNorm}(x) = \gamma \odot \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} + \beta, \qquad \mu = \frac{1}{d}\sum_{i=1}^{d} x_i, \quad \sigma^2 = \frac{1}{d}\sum_{i=1}^{d} (x_i - \mu)^2

μ est la moyenne des nombres du jeton et σ² leur variance (divisée par d, pas par d − 1). ε (10⁻⁵ dans PyTorch) évite de diviser par 0. γ et β sont un facteur d’échelle et un décalage appris, initialisés à 1 et à 0; ⊙ multiplie nombre par nombre.

Vérifiez à la main. Prenez x = (1, 2, 3, 6). La moyenne est 12 / 4 = 3. La variance est (4 + 1 + 0 + 9) / 4 = 3,5, et √3,5 = 1,871. Donc, 1 devient (1 − 3) / 1,871 = −1,069, et 6 devient 3 / 1,871 = 1,604.

x = torch.tensor([1.0, 2.0, 3.0, 6.0])
mu = x.mean()
var = ((x - mu) ** 2).mean() # divide by d, not by d - 1
by_hand = (x - mu) / torch.sqrt(var + 1e-5) # then times gamma (1 at the start) plus beta (0)
print(mu.item(), var.item())
print(by_hand.round(decimals=3))
print(torch.nn.LayerNorm(4)(x).detach().round(decimals=3))

La sortie :

3.0 3.5
tensor([-1.0690, -0.5350, 0.0000, 1.6040])
tensor([-1.0690, -0.5350, 0.0000, 1.6040])

La deuxième sous-couche est le réseau à propagation avant (§3.3), appliqué séparément et de façon identique à chaque position :

FFN(x)=max⁡(0,  xW1+b1) W2+b2\text{FFN}(x) = \max(0,\; x W_1 + b_1)\, W_2 + b_2

W₁ est de forme d_model × d_ff et W₂ de forme d_ff × d_model; b₁ et b₂ sont leurs biais. max(0, ·) est ReLU. Dans l’article, 512 → 2 048 → 512. Dans le petit modèle, 64 → 256 → 64; le tenseur caché a donc la forme (1, 5, 256). Après l’entraînement, ReLU met à 0 92,6 % des 256 nombres cachés du jeton « c » dans la couche d’encodeur 1. Chaque jeton utilise une petite partie de la couche.

Voici maintenant la couche entière construite avec ces formules, comparée à la couche post-normalisée nn.TransformerEncoderLayer(norm_first=False) de PyTorch. La fonction lit les poids de PyTorch; in_proj_weight contient W_Q, W_K et W_V empilés.

encoder_layer.py
from torch import nn
torch.manual_seed(0)
d, h, d_ff = 64, 4, 256
ref = nn.TransformerEncoderLayer(d, h, d_ff, dropout=0.1, activation="relu",
batch_first=True, norm_first=False).eval()
def layer_norm(x, norm): # (x - mean) / sqrt(var + eps) * gamma + beta
mu = x.mean(-1, keepdim=True)
var = x.var(-1, unbiased=False, keepdim=True)
return (x - mu) / torch.sqrt(var + norm.eps) * norm.weight + norm.bias
def encoder_layer(x, L): # L: PyTorch's layer, read only for its weights
n = x.shape[1]
Q, K, V = (x @ L.self_attn.in_proj_weight.T + L.self_attn.in_proj_bias).split(d, dim=-1)
Q, K, V = (t.view(1, n, h, d // h).transpose(1, 2) for t in (Q, K, V)) # (1, 4, n, 16)
weights = torch.softmax(Q @ K.transpose(-2, -1) / math.sqrt(d // h), dim=-1)
heads = (weights @ V).transpose(1, 2).reshape(1, n, d) # concatenate
attn = heads @ L.self_attn.out_proj.weight.T + L.self_attn.out_proj.bias # W_O
x = layer_norm(x + attn, L.norm1) # add, then norm
ffn = torch.relu(x @ L.linear1.weight.T + L.linear1.bias) @ L.linear2.weight.T + L.linear2.bias
return layer_norm(x + ffn, L.norm2) # add, then norm
x = torch.randn(1, 5, d)
with torch.no_grad():
mine, theirs = encoder_layer(x, ref), ref(x)
print(tuple(mine.shape), f"largest difference {(mine - theirs).abs().max().item():.1e}")

La sortie :

(1, 5, 64) largest difference 3.6e-07

3,6 × 10⁻⁷ est une erreur d’arrondi : les formules correspondent à la couche. (.eval() désactive le dropout.)

La plupart des modèles actuels placent la normalisation avant la sous-couche. La dernière section explique pourquoi.

Deux sous-couches côte à côte. À gauche, la post-normalisation de l'article de 2017, LayerNorm(x + Sublayer(x)) : x entre dans la sous-couche (attention ou propagation avant), un signe plus ajoute x par une ligne résiduelle pointillée, puis LayerNorm donne la sortie. La normalisation se trouve sur le chemin principal. À droite, la pré-normalisation de la plupart des modèles actuels, x + Sublayer(Norm(x)) : x entre dans RMSNorm ou LayerNorm, puis dans la sous-couche, puis un signe plus ajoute x par la ligne résiduelle pointillée, donnant la sortie. Le chemin résiduel de l'entrée à la sortie ne comporte aucune normalisation.
La post-normalisation, comme dans l'article, et la pré-normalisation, comme dans la plupart des modèles suivants. Seul l'emplacement de la normalisation change.
Description textuelle du diagramme

À gauche : la post-normalisation, l’ordre de l’article de 2017, écrit LayerNorm(x + Sublayer(x)). L’entrée x entre dans la sous-couche, qui est l’attention ou la couche à propagation avant. Un signe plus additionne la sortie de la sous-couche et x. Une ligne pointillée fait passer x autour de la sous-couche : c’est la connexion résiduelle. Puis LayerNorm donne la sortie. La note indique que la normalisation se trouve sur le chemin principal; la sortie de chaque couche est donc remise à l’échelle.

À droite : la pré-normalisation, l’ordre de la plupart des modèles actuels, écrit x + Sublayer(Norm(x)). L’entrée x entre dans RMSNorm ou LayerNorm, puis dans la sous-couche. Un signe plus ajoute x, que la ligne résiduelle pointillée fait passer autour des deux cases. La note indique que le chemin résiduel de l’entrée à la sortie ne comporte aucune normalisation : une voie directe sans obstacle.

Étape 4 : la couche du décodeur et l’attention croisée

Section intitulée « Étape 4 : la couche du décodeur et l’attention croisée »

Une couche du décodeur a trois sous-couches, chacune entourée des mêmes opérations d’addition et de normalisation.

  1. Autoattention masquée. Les jetons du décodeur se lisent entre eux, avec le masque causal M des Les mathématiques de l’attention, ajouté aux scores avant softmax. La position 1, le jeton de début, ne lit qu’elle-même.
  2. Attention encodeur-décodeur, ou attention croisée. Les requêtes viennent du décodeur, et les clés et valeurs viennent de la mémoire M_enc de l’encodeur (voir la formule ci-dessous). M_enc a une ligne par jeton source; ce n’est pas le masque M.
  3. Le réseau à propagation avant, avec la même formule que dans l’encodeur.
CrossAttention(Y,Menc)=softmax ⁣((YWQ)(MencWK)⊤dk)MencWV\text{CrossAttention}(Y, M_{\text{enc}}) = \text{softmax}\!\left(\frac{(Y W_Q)(M_{\text{enc}} W_K)^\top}{\sqrt{d_k}}\right) M_{\text{enc}} W_V

Ici, Y contient les vecteurs du décodeur après l’autoattention masquée, avec une ligne par position cible. La formule montre une tête. En mots : chaque position cible pose une question (sa requête) et cherche la réponse dans la phrase source (les clés). Elle prend ensuite ce que la source offre (les valeurs). Avec 6 positions cibles et 5 jetons sources, les scores ont une forme de 6 × 5 par tête. Aucun masque causal n’est nécessaire : toute la source est connue.

torch.manual_seed(0)
cross = nn.MultiheadAttention(64, 4, batch_first=True)
memory = torch.randn(1, 5, 64) # the encoder's output: 5 source tokens
y = torch.randn(1, 6, 64) # the decoder: 6 target positions
out, w = cross(query=y, key=memory, value=memory, average_attn_weights=False)
print("output ", tuple(out.shape))
print("weights", tuple(w.shape))
print(w[0, 0, 0].detach().round(decimals=3), "sum", round(w[0, 0, 0].sum().item(), 3))

La sortie :

output (1, 6, 64)
weights (1, 4, 6, 5)
tensor([0.1440, 0.2300, 0.1400, 0.3070, 0.1800]) sum 1.0

Une ligne de sortie par position du décodeur, remplie d’information provenant de la source. Voilà comment la phrase source intervient à chaque étape de l’écriture.

Après la dernière couche du décodeur, chaque position contient d_model nombres. Une couche linéaire les transforme en un score (un logit) par jeton du vocabulaire. Softmax transforme ensuite les scores en probabilités. L’article partage la matrice de cette couche avec la table de plongements (§3.4) :

P(next token)=softmax(ypos E⊤)P(\text{next token}) = \text{softmax}\big(y_{\text{pos}}\, E^\top\big)

y_pos est le vecteur de la dernière couche du décodeur à une position, soit une ligne de d_model nombres. E est la même table n_vocab × d_model qu’à l’étape 1, ici 13 × 64. n_vocab est le nombre de jetons du vocabulaire, et Eᵀ est la transposée de E. Donc (1, 6, 64) @ (64, 13) → (1, 6, 13). En mots : le score d’un jeton est le produit scalaire du vecteur du décodeur avec le plongement de ce jeton. À la position 1 de l’exemple, le score de « d » est 5,22. Après softmax, sa probabilité est 0,916.

Réunissons maintenant les cinq étapes. L’explorateur suit « c a b e d » dans le modèle miniature entraîné : l’entrée, l’encodeur, le décodeur et la sortie. Chaque étape montre la forme réelle et quelques nombres réels.

Explorer

Un exemple dans le transformeur original

Étape 1 sur 14 : Jetons de la source, forme (1, 5).

    1. Entrée (cette étape)
    2. Encodeur
    3. Décodeur
    4. Sortie

    Étape 1 sur 14 Jetons de la source

    Forme (1, 5)

    La source c a b e d devient 5 identifiants de jetons du vocabulaire commun de 13 jetons.

    Jeton et son ID
    cabed
    53476
    1. Entrée (cette étape)
    2. Encodeur
    3. Décodeur
    4. Sortie

    Étape 2 sur 14 Recherche du plongement

    Forme (1, 5, 64)

    Chaque identifiant prend sa ligne de 64 nombres dans la table commune. Voici les 4 premiers nombres de c. Ils sont petits : la table a commencé avec un écart de 1/√64 = 0,125.

    Les 4 premiers des 64 nombres
    c, plongement−0,023−0,006−0,0270,023…
    1. Entrée (cette étape)
    2. Encodeur
    3. Décodeur
    4. Sortie

    Étape 3 sur 14 Multiplier par √d_model = 8

    Forme (1, 5, 64)

    L’article multiplie les plongements par √d_model, pour qu’ils ne soient pas noyés par l’encodage positionnel, dont les nombres sont entre −1 et 1.

    Les 4 premiers des 64 nombres
    c, plongement−0,023−0,006−0,0270,023…
    c, × 8−0,186−0,045−0,2190,187…
    1. Entrée (cette étape)
    2. Encodeur
    3. Décodeur
    4. Sortie

    Étape 4 sur 14 Ajouter l’encodage positionnel

    Forme (1, 5, 64)

    c est à la position 0, où l’encodage est sin 0 = 0 et cos 0 = 1 dans chaque paire. La position 1 commence par sin 1 = 0,841 et cos 1 = 0,540.

    Les 4 premiers des 64 nombres
    c, × 8−0,186−0,045−0,2190,187…
    PE, position 00,0001,0000,0001,000…
    entrée de l’encodeur−0,1860,955−0,2191,187…
    PE, position 10,8410,5400,6820,732…
    1. Entrée
    2. Encodeur (cette étape)
    3. Décodeur
    4. Sortie

    Étape 5 sur 14 Requêtes, clés et valeurs

    Forme (1, 4, 5, 16)

    Trois couches linéaires créent Q, K et V à partir de l’entrée de l’encodeur, et chacune est divisée en 4 têtes de 16 nombres.

    1. Entrée
    2. Encodeur (cette étape)
    3. Décodeur
    4. Sortie

    Étape 6 sur 14 Autoattention de l’encodeur (couche 1, tête 1)

    Forme (1, 4, 5, 5)

    Chaque lettre de la source lit toutes les lettres de la source. Dans la couche 1, les poids sont presque égaux, entre 0,15 et 0,23 : cette tête fait surtout une moyenne.

    Lignes : la position qui lit. Colonnes : ce qu’elle lit. Chaque ligne donne 1 au total.
    cabed
    c0,170,200,180,220,23
    a0,190,200,150,220,23
    b0,210,200,210,190,19
    e0,180,220,160,220,22
    d0,200,200,170,200,23
    1. Entrée
    2. Encodeur (cette étape)
    3. Décodeur
    4. Sortie

    Étape 7 sur 14 Addition et normalisation

    Forme (1, 5, 64)

    Les 4 têtes sont réunies (4 × 16 = 64 nombres), mélangées par W_O, ajoutées à l’entrée et normalisées : LayerNorm(x + Sublayer(x)).

    Les 4 premiers des 64 nombres
    c, entrée de l’encodeur−0,1860,955−0,2191,187…
    c, après addition et normalisation−0,4691,800−0,5291,894…
    1. Entrée
    2. Encodeur (cette étape)
    3. Décodeur
    4. Sortie

    Étape 8 sur 14 Couche à propagation avant, puis addition et normalisation

    Forme (1, 5, 64)

    Chaque jeton passe seul par 64 → 256 → 64. La couche cachée a la forme (1, 5, 256); ReLU met à 0 92,6 % des 256 nombres cachés de c. Puis encore une addition et une normalisation.

    Les 4 premiers des 64 nombres
    c, après addition et normalisation−0,4691,800−0,5291,894…
    c, après la couche 1−0,5371,412−0,6201,863…
    1. Entrée
    2. Encodeur (cette étape)
    3. Décodeur
    4. Sortie

    Étape 9 sur 14 Sortie de l’encodeur : la mémoire

    Forme (1, 5, 64)

    Après 2 couches d’encodeur, chaque lettre de la source a 64 nombres qui portent aussi de l’information sur les autres lettres. Le décodeur lit cette mémoire dans chaque couche.

    Les 4 premiers des 64 nombres
    c, après la couche 1−0,5371,412−0,6201,863…
    c, mémoire−0,6701,164−0,3032,715…
    1. Entrée
    2. Encodeur
    3. Décodeur (cette étape)
    4. Sortie

    Étape 10 sur 14 Entrée du décodeur, décalée vers la droite

    Forme (1, 6)

    Le décodeur lit <s>, puis la réponse jusqu’ici. À l’entraînement, la réponse entière d e b a c est donnée d’un coup, décalée d’une place vers la droite.

    Jeton et son ID
    <s>debac
    167435
    1. Entrée
    2. Encodeur
    3. Décodeur (cette étape)
    4. Sortie

    Étape 11 sur 14 Autoattention masquée (couche 1, tête 1)

    Forme (1, 4, 6, 6)

    Le masque causal cache chaque position suivante. <s> ne peut lire que lui-même (poids 1); d lit <s> et lui-même (0,5 et 0,5).

    Lignes : la position qui lit. Colonnes : ce qu’elle lit. Chaque ligne donne 1 au total.
    <s>debac
    <s>1,00masquémasquémasquémasquémasqué
    d0,500,50masquémasquémasquémasqué
    e0,290,370,34masquémasquémasqué
    b0,260,240,240,26masquémasqué
    a0,180,200,190,170,25masqué
    c0,180,140,150,190,150,19
    1. Entrée
    2. Encodeur
    3. Décodeur (cette étape)
    4. Sortie

    Étape 12 sur 14 Attention croisée (couche 2, moyenne de 4 têtes)

    Forme (1, 4, 6, 5)

    Les requêtes viennent du décodeur, les clés et les valeurs de la mémoire. Chaque position du décodeur lit une lettre de la source : la position 1 lit la lettre 5 (d), la position 2 lit la lettre 4 (e), et ainsi de suite. Une antidiagonale : le modèle a appris où regarder.

    Lignes : la position qui lit. Colonnes : ce qu’elle lit. Chaque ligne donne 1 au total.
    cabed
    <s> → d0,000,000,000,001,00
    d → e0,000,000,001,000,00
    e → b0,010,040,940,010,00
    b → a0,030,960,010,000,00
    a → c1,000,000,000,000,00
    c → </s>0,440,010,230,070,26
    1. Entrée
    2. Encodeur
    3. Décodeur
    4. Sortie (cette étape)

    Étape 13 sur 14 Couche de sortie, liée au plongement

    Forme (1, 6, 13)

    Les 64 nombres de chaque position sont multipliés par la table de plongements transposée : un score pour chacun des 13 jetons. À la position 1, le score de d est 5,223, bien au-dessus des autres.

    Les 4 premiers des 64 nombres
    position 1, 4 premiers scores0,3710,3260,3170,370…
    1. Entrée
    2. Encodeur
    3. Décodeur
    4. Sortie (cette étape)

    Étape 14 sur 14 Softmax : le jeton suivant à chaque position

    Forme (1, 6, 13)

    Softmax transforme chaque ligne de 13 scores en probabilités. Les jetons les plus probables sont d e b a c </s> : la source inversée. Aucune probabilité ne dépasse 0,93, à cause du lissage des étiquettes.

    Jeton suivant le plus probable à chaque position du décodeur
    123456
    jetondebac</s>
    probabilité0,9160,9160,9290,9270,9240,927
Le modèle miniature entraîné sur la source c a b e d, en mode évaluation (sans dropout). Les formes sont (lot, jetons, nombres) ou (lot, têtes, requêtes, clés). Chaque nombre vient du modèle réel.
Description textuelle de l’explorateur

L’explorateur comporte 14 étapes, accessibles avec un curseur et deux boutons. Chaque étape nomme sa phase (entrée, encodeur, décodeur ou sortie), la forme du tenseur et les nombres réels.

Étape 1, les jetons sources, forme (1, 5) : c, a, b, e et d ont les identifiants 5, 3, 4, 7 et 6. Étape 2, recherche des plongements, (1, 5, 64) : les 4 premiers nombres de c sont −0,023, −0,006, −0,027 et 0,023. Étape 3, multiplication par 8 : −0,186, −0,045, −0,219 et 0,187. Étape 4, ajout de l’encodage positionnel de la position 0, soit 0, 1, 0, 1 : l’entrée de l’encodeur devient −0,186, 0,955, −0,219 et 1,187. L’encodage de la position 1 commence par 0,841, 0,540, 0,682 et 0,732.

Étape 5, requêtes, clés et valeurs, (1, 4, 5, 16). Étape 6, poids d’autoattention de l’encodeur, couche 1, tête 1, (1, 4, 5, 5) : chaque poids se situe entre 0,15 et 0,23. Cette tête calcule donc surtout une moyenne. Étape 7, addition et normalisation, (1, 5, 64) : c devient −0,469, 1,800, −0,529 et 1,894. Étape 8, la couche à propagation avant, avec une forme cachée de (1, 5, 256). ReLU met à 0 92,6 % des nombres cachés de c. Après addition et normalisation, c vaut −0,537, 1,412, −0,620 et 1,863. Étape 9, la mémoire après 2 couches d’encodeur, (1, 5, 64) : −0,670, 1,164, −0,303 et 2,715.

Étape 10, l’entrée du décodeur, décalée vers la droite, (1, 6) : le jeton de début, d, e, b, a et c, avec les identifiants 1, 6, 7, 4, 3 et 5. Étape 11, autoattention masquée du décodeur, couche 1, tête 1, (1, 4, 6, 6) : le triangle supérieur est masqué. Le jeton de début ne lit que lui-même avec un poids de 1. d lit le jeton de début et lui-même avec un poids de 0,5 chacun. Étape 12, attention croisée du décodeur, couche 2, moyenne des 4 têtes, (1, 4, 6, 5). La ligne du jeton de début attribue 0,999 à la lettre source 5 (d); celle de d, 0,999 à la lettre 4 (e); celle de e, 0,938 à la lettre 3 (b); celle de b, 0,962 à la lettre 2 (a); celle de a, 1,000 à la lettre 1 (c). La dernière ligne, qui doit écrire le jeton de fin, répartit ses poids : 0,44, 0,01, 0,23, 0,07 et 0,26. Étape 13, la couche de sortie liée au plongement, (1, 6, 13) : à la position 1, le score de d est 5,223. Étape 14, softmax : les jetons les plus probables sont d, e, b, a, c et le jeton de fin. Leurs probabilités sont 0,916, 0,916, 0,929, 0,927, 0,924 et 0,927.

L’étape 12 répond à la question de prédiction. Pour écrire la première lettre du texte inversé, la position 1 doit lire la dernière lettre source, d, à la position 5. Elle le fait, avec un poids de 0,999. La position suivante lit la lettre 4, puis 3, 2 et 1. Personne n’a dit au modèle de faire cela. L’entraînement a trouvé que lire la position symétrique permet d’inverser le texte.

L’article entraîne le modèle avec quatre choix (§5.3 et §5.4).

Adam avec des réglages inhabituels. β₁ = 0,9, β₂ = 0,98 et ε = 10⁻⁹. L’optimiseur est l’Adam de Taux d’apprentissage et optimiseurs; β₂ = 0,98 au lieu de 0,999 lui donne une mémoire plus courte.

Un calendrier de montée progressive. Le taux d’apprentissage n’est pas fixe. Il change à chaque étape :

lrate=dmodel−0.5⋅min⁡(step−0.5,  step⋅warmup_steps−1.5)\text{lrate} = d_{\text{model}}^{-0.5} \cdot \min\left(\text{step}^{-0.5},\; \text{step} \cdot \text{warmup\_steps}^{-1.5}\right)

En mots : le taux augmente en ligne droite pendant warmup_steps étapes, puis diminue selon 1/√step. Les deux termes sont égaux au sommet, quand step = warmup_steps. L’article utilise warmup_steps = 4 000. Vérifiez à la main : 512^−0,5 = 0,0442 et 4000^−0,5 = 0,0158. Le sommet est donc 0,0442 × 0,0158 = 6,99 × 10⁻⁴.

def lrate(step, d_model=512, warmup=4000):
return d_model ** -0.5 * min(step ** -0.5, step * warmup ** -1.5)
for step in (1, 100, 1000, 4000, 10000, 100000):
print(f"step {step:>7,} lrate {lrate(step):.3e}")

La sortie :

step 1 lrate 1.747e-07
step 100 lrate 1.747e-05
step 1,000 lrate 1.747e-04
step 4,000 lrate 6.988e-04
step 10,000 lrate 4.419e-04
step 100,000 lrate 1.398e-04
Le calendrier du taux d'apprentissage de l'article pour d_model = 512 et 4 000 étapes de montée progressive. La formule donne les 40 000 premières étapes sur 100 000.
Description textuelle du graphique

Un graphique linéaire du taux d’apprentissage selon l’étape d’entraînement, de 0 à 40 000. La ligne monte tout droit de presque 0 à son sommet de 6,99 × 10⁻⁴ à l’étape 4 000. Elle descend ensuite, rapidement au début, puis de plus en plus lentement. Elle atteint 4,42 × 10⁻⁴ à l’étape 10 000 et environ 2,2 × 10⁻⁴ à l’étape 40 000. À l’étape 100 000, la fin de l’entraînement de l’article, elle vaut 1,40 × 10⁻⁴.

Pourquoi cette montée progressive? Au début, Adam estime la taille des gradients à partir de très peu d’étapes. De grands pas peuvent pousser un modèle à normalisation après les sous-couches dans une région dont il ne revient pas.

Dropout de 0,1, soit la désactivation aléatoire, sur la sortie de chaque sous-couche et sur les plongements additionnés aux positions. C’est le même principe que dans Limiter le surapprentissage.

Lissage des étiquettes de 0,1. La cible n’est pas « 100 % pour le bon jeton », mais une version moins catégorique. PyTorch répartit ε = 0,1 sur les n_vocab jetons et attribue le reste au bon jeton :

q(k)=(1−ϵ) [k=right]+ϵnvocabq(k) = (1 - \epsilon)\,[k = \text{right}] + \frac{\epsilon}{n_{\text{vocab}}}

Ici, k est un jeton quelconque du vocabulaire. [k = right] vaut 1 pour le bon jeton et 0 pour les autres. Avec n_vocab = 13, le bon jeton reçoit 0,9 + 0,1 / 13 = 0,908. Chaque autre jeton reçoit 0,1 / 13 = 0,0077. La perte est l’entropie croisée par rapport à q. Elle est minimale quand le modèle prédit exactement q. Cette valeur minimale est l’entropie de q, et non 0 :

n_vocab, eps, right = 13, 0.1, 5
target = torch.full((n_vocab,), eps / n_vocab) # PyTorch spreads eps over all tokens
target[right] += 1 - eps # and puts the rest on the right one
print(round(target[right].item(), 4), round(target[0].item(), 4))
loss_fn = nn.CrossEntropyLoss(label_smoothing=eps)
best = loss_fn(target.log().unsqueeze(0), torch.tensor([right])) # predict exactly the target
sure = loss_fn(torch.tensor([[0.0] * right + [20.0] + [0.0] * (n_vocab - right - 1)]), torch.tensor([right]))
print(f"best possible loss {best.item():.3f}; a model 100% sure: {sure.item():.3f}")

La sortie :

0.9077 0.0077
best possible loss 0.537; a model 100% sure: 1.846

Même un modèle miniature parfait a donc une perte lissée de 0,537. Un modèle certain à 100 % paie plus (1,846) qu’un modèle certain à 91 %. Voilà pourquoi les probabilités maximales du modèle miniature restent près de 0,92 et n’atteignent jamais 0,99. L’article décrit honnêtement le compromis (§5.4) : « Cela nuit à la perplexité, car le modèle apprend à être moins certain, mais améliore l’exactitude et le score BLEU. »

Le tableau 3 de l’article donne au modèle de base 65 × 10⁶ paramètres. Ses réglages sont N = 6, d_model = 512, h = 8, d_k = d_v = 64, d_ff = 2 048 et un dropout de 0,1. Comptez-les à partir des formules : 4d² + 4d pour l’attention, deux matrices et leurs biais pour le FFN, 2d pour chaque LayerNorm. Une couche d’encodeur a 1 attention et 2 normalisations; une couche de décodeur en a 2 et 3. Comme PyTorch, le calcul donne un biais à chaque matrice d’attention. Les formules d’attention de l’article n’en montrent aucun, ce qui ne retirerait que 36 864 paramètres.

d, d_ff, N, vocab = 512, 2048, 6, 37000
attention = 4 * d * d + 4 * d # W_Q, W_K, W_V, W_O and their biases
ffn = d * d_ff + d_ff + d_ff * d + d # W1, b1, W2, b2
norm = 2 * d # gamma and beta
encoder_layer_n = attention + ffn + 2 * norm
decoder_layer_n = 2 * attention + ffn + 3 * norm
embedding = vocab * d # one matrix: source, target and output
total = embedding + N * (encoder_layer_n + decoder_layer_n)
print(f"attention {attention:,} ffn {ffn:,} encoder layer {encoder_layer_n:,} decoder layer {decoder_layer_n:,}")
print(f"total {total:,}; with a separate output matrix {total + vocab * d:,}")

La sortie :

attention 1,050,624 ffn 2,099,712 encoder layer 3,152,384 decoder layer 4,204,032
total 63,082,496; with a separate output matrix 82,026,496

Notre total est 63 082 496, environ 63 millions, contre les 65 millions arrondis de l’article. L’article décrit seulement le vocabulaire comme contenant « environ 37 000 » jetons, donc un petit écart est normal. Une matrice de sortie distincte donnerait 82,0 millions, loin de 65 : le partage est bien réel. Notez où se trouvent les paramètres. Le plongement partagé en contient 18,9 millions. Chaque couche de décodeur est un tiers plus grande qu’une couche d’encodeur à cause de son attention croisée. L’article a entraîné ce modèle pendant 100 000 étapes, soit environ 12 heures sur 8 processeurs graphiques NVIDIA P100.

Entraînez maintenant le modèle miniature avec la recette complète. Le seul changement est la montée progressive : 400 étapes au lieu de 4 000. L’essai compte en effet 3 000 étapes au lieu de 100 000. Le taux d’apprentissage maximal est alors 64^−0,5 × 400^−0,5 = 0,00625. Chaque étape utilise 64 séquences aléatoires de 3 à 8 lettres. exact_match (correspondance exacte) compte les séquences entièrement correctes parmi les 500 séquences de test. Le carnet (notebook) Jupyter contient les classes du modèle et batch().

train_reversal.py
seed_all(0)
gen = torch.Generator().manual_seed(0) # the training batches
test_src, test_in, test_out = batch(500, torch.Generator().manual_seed(1234))
model = Transformer(vocab=13, d=64, h=4, d_ff=256, n_layers=2, p=0.1)
opt = torch.optim.Adam(model.parameters(), lr=1.0, betas=(0.9, 0.98), eps=1e-9)
sched = torch.optim.lr_scheduler.LambdaLR(opt, lambda s: lrate(s + 1, 64, 400))
loss_fn = nn.CrossEntropyLoss(ignore_index=PAD, label_smoothing=0.1)
for step in range(1, 3001):
model.train()
src, tgt_in, tgt_out = batch(64, gen) # 64 random sequences
loss = loss_fn(model(src, tgt_in).reshape(-1, 13), tgt_out.reshape(-1))
opt.zero_grad()
loss.backward()
opt.step()
sched.step()
if step in (1, 500, 1000, 2000, 3000):
print(f"step {step:>5} lr {opt.param_groups[0]['lr']:.2e} train loss {loss.item():.3f}"
f" exact match {exact_match(model, test_src, test_out):.1%}")

La sortie :

step 1 lr 3.13e-05 train loss 5.254 exact match 0.0%
step 500 lr 5.58e-03 train loss 0.741 exact match 88.8%
step 1000 lr 3.95e-03 train loss 0.572 exact match 98.2%
step 2000 lr 2.79e-03 train loss 0.558 exact match 99.6%
step 3000 lr 2.28e-03 train loss 0.540 exact match 100.0%

LambdaLR multiplie le taux de base 1,0 par lrate() à chaque étape. Le décodeur reçoit la bonne réponse décalée d’une position (teacher forcing, ou guidage par la réponse correcte). Les 3 000 étapes ont pris 34 secondes sur 2 fils d’exécution du processeur d’un ordinateur partagé.

  • Perte de test (entropie croisée simple)
  • Perte d'entraînement (avec lissage des étiquettes)
  • Plancher de la perte lissée, 0,537
L'essai miniature, mesuré toutes les 100 étapes à partir de l'étape 100. À l'étape 1, les deux pertes dépassent 5, hors du graphique. Les annotations donnent la correspondance exacte sur les 500 séquences de test.
Description textuelle du graphique

Un graphique linéaire avec l’étape d’entraînement, de 100 à 3 000, sur l’axe horizontal, et la perte, de 0 à 2, sur l’axe vertical. La perte de test, l’entropie croisée simple, descend de 1,68 à l’étape 100 à 0,31 à l’étape 200. Elle atteint 0,13 à l’étape 500, puis descend lentement à 0,079 à l’étape 3 000. La perte d’entraînement, avec lissage des étiquettes, descend de 1,90 à l’étape 100 à environ 0,7 à l’étape 300. Elle se stabilise ensuite juste au-dessus d’une ligne pointillée à 0,537, le plancher fixé par le lissage des étiquettes. Elle termine à 0,540. Les annotations sur la perte de test donnent la correspondance exacte : 88,8 % à l’étape 500, 98,2 % à l’étape 1 000 et 100,0 % à l’étape 3 000.

La perte d’entraînement lissée ne peut pas descendre sous 0,537. Sa fin plate est donc une réussite, pas un problème.

Décodage glouton. À l’inférence, aucune réponse n’est disponible pour être décalée. L’encodeur s’exécute une fois. Le décodeur commence ensuite avec <s> et ajoute le jeton le plus probable à chaque étape. C’est la même démarche que la boucle de génération d’Entraînement et inférence.

model.eval()
src = torch.tensor([[STOI[c] for c in "cabed"]])
with torch.no_grad():
memory = model.encode(src) # the encoder runs once
ys = torch.tensor([[BOS]]) # the decoder starts with <s>
for _ in range(10):
p = model.decode(ys, memory, src)[0, -1].softmax(-1) # the last position's prediction
nxt = int(p.argmax())
print(f"{' '.join(ITOS[i] for i in ys[0].tolist()):<14} -> {ITOS[nxt]:<5} {p[nxt].item():.3f}")
ys = torch.cat([ys, torch.tensor([[nxt]])], dim=1)
if nxt == EOS:
break

La sortie :

<s> -> d 0.916
<s> d -> e 0.916
<s> d e -> b 0.929
<s> d e b -> a 0.927
<s> d e b a -> c 0.924
<s> d e b a c -> </s> 0.927

Les autres meilleurs candidats de chaque étape ont une probabilité inférieure à 0,01 :

Entrée du décodeur jusqu’iciChoixLes 3 meilleurs (probabilité)
<s>dd 0.916 · c 0.008 · f 0.007
<s> dee 0.916 · j 0.008 · g 0.008
<s> d ebb 0.929 · c 0.006 · d 0.006
<s> d e baa 0.927 · d 0.007 · j 0.006
<s> d e b acc 0.924 · </s> 0.007 · <s> 0.007
<s> d e b a c</s></s> 0.927 · c 0.006 · <s> 0.006

Où le modèle regarde. L’attention croisée des deux couches du décodeur pour cet exemple, moyennée sur les 4 têtes :

Poids d'attention croisée du modèle miniature entraîné pour c a b e d, moyenne des 4 têtes. Dans la couche 2, le poids maximal de chaque ligne (encerclé) forme une antidiagonale : la position 1 lit la lettre source 5, la position 2 lit la lettre 4, et ainsi de suite.
Description textuelle du graphique

Deux grilles de 6 lignes et 5 colonnes. Les lignes sont les positions du décodeur, chacune avec le jeton qu’elle doit écrire. Le jeton de début écrit d, d écrit e, e écrit b, b écrit a, a écrit c et c écrit le jeton de fin. Les colonnes sont les lettres sources c, a, b, e et d, aux positions 1 à 5.

La couche 1 est floue. Le poids maximal de chaque ligne n’est que de 0,28 à 0,47. La ligne du jeton de début attribue 0,37 à d et 0,32 à c. Les autres lignes répartissent leurs poids sur deux ou trois lettres.

La couche 2 est nette. La ligne du jeton de début attribue 0,999 à d (position 5). La ligne de d attribue 0,999 à e (position 4). La ligne de e attribue 0,938 à b (position 3). La ligne de b attribue 0,962 à a (position 2). La ligne de a attribue 1,000 à c (position 1). Ces cellules forment une ligne du coin supérieur droit au coin inférieur gauche : l’antidiagonale. La dernière ligne, qui doit écrire le jeton de fin, répartit son poids : 0,44 sur c, 0,23 sur b et 0,26 sur d.

La couche 1 a une idée approximative; la couche 2 lit exactement une lettre à la position symétrique pour chaque position. La ligne </s> n’a aucune lettre à copier, donc elle répartit ses poids. Comme le rappelait L’attention étape par étape, les poids montrent d’où vient l’information, pas pourquoi. Sur cette tâche simple, les deux concordent.

Avec norm_first=False, ReLU, le dropout désactivé et nos poids copiés, nn.Transformer donne la même sortie que notre modèle à 9,5 × 10⁻⁷ près. Le test utilise un lot avec remplissage et les deux masques. Deux pièges ont d’abord fait échouer la vérification. Le premier est la LayerNorm finale supplémentaire de PyTorch après chaque pile (voir le cas d’échec ci-dessous). Le second est un chemin rapide en mode évaluation pour les lots avec remplissage, qui renvoie 0 aux positions de remplissage. Désactivez-le avec use_nested_tensor = False.

Le routeur de Larkfield lit un billet de soutien et choisit une équipe. C’est uniquement le travail de l’encodeur : tout le billet est connu, et rien ne doit être écrit. Le TinyTransformer du cours est donc un encodeur seulement, comme BERT. Il compte 291 397 paramètres et donne 94,3 % de bonnes réponses sur les billets de validation. Il diffère de l’encodeur de l’article par deux choix : il apprend ses positions et normalise avant chaque sous-couche (norm_first=True). Un décodeur serait nécessaire seulement pour écrire, par exemple une réponse.

Les composants de l’article se retrouvent dans chaque grand modèle de langage, mais presque tous les détails ont changé. Le tableau compare le modèle de base à SmolLM2-135M d’Entraînement et inférence. Il utilise sa configuration et les faits mesurés dans le cours.

ComposantTransformeur (2017, base)SmolLM2-135MPourquoi cela a changé
shapeencoder + decoder (6 + 6 layers)decoder only (30 layers)A model that only continues text needs no separate encoder: the prompt and the answer are one sequence, read with a causal mask.
width d_model512576A choice of the model's makers: many thin layers (30) instead of a few wide ones (6 + 6).
normalizationLayerNorm after each residual addition (post-norm)RMSNorm before each sublayer (pre-norm), eps 1e-5, plus a final RMSNormWith the norm before each sublayer, the residual additions form a clean path from the output back to the input, so deep stacks train steadily. RMSNorm skips the mean: less work, a similar effect.
positionssinusoidal, added to the embeddingsrotary (RoPE, theta 100,000), applied to queries and keys in every layerRotary positions turn the queries and keys, so the score q · k depends on how far apart two tokens are, in every layer, not only at the input.
attention heads8 heads, each with its own keys and values (d_k = 64)9 query heads sharing 3 key-value heads (grouped-query attention, head size 64)Three query heads share each key-value head, so the KV cache that generation keeps is 3 times smaller.
feed-forwardReLU, 512 -> 2048 -> 512SiLU-gated (SwiGLU): down(SiLU(gate(x)) * up(x)), 576 -> 1536 -> 576A gate, SiLU(gate(x)), decides number by number how much of up(x) passes. Later experiments found gated layers better at the same size.
biasesin the feed-forward layer (the paper's attention formulas have none)noneThey add little, and leaving them out keeps the layers simpler.
embeddingsone shared matrix (source, target, output), x sqrt(d_model)input embedding tied to the output layer, not scaledTying is kept: in SmolLM2 it saves 28.3 million parameters. The × √d_model scaling is gone.
vocabularyabout 37,000 BPE tokens (shared)49,152 BPE tokensOne tokenizer for many kinds of text, including code.
contextno fixed limit (sinusoids extend), trained on sentences8,192 tokensThe model is trained and tested on texts up to this length; rotary positions make longer windows work.
dropout0.10.0Pretraining reads trillions of tokens, mostly once, so overfitting is not the main risk; dropout would only slow learning.
parametersabout 65 M (our count 63.1 M)134.5 MAbout twice as many, in one stack instead of two.
training tasktranslation (supervised sentence pairs)next-token prediction on raw text (pretraining)Next-token prediction needs no human labels, so the text itself is the answer key, and far more text can be used.

Trois changements comptent surtout. Décodeur seulement : la requête (prompt) et la réponse d’un modèle de conversation forment une seule séquence. Le masque causal de Position et masquage remplace donc l’encodeur et l’attention croisée. Normalisation avant les sous-couches : les piles profondes s’entraînent plus régulièrement avec un chemin résiduel dégagé. Attention à requêtes groupées : un cache KV plus petit, le coût principal décrit dans Les coûts de l’inférence.

  • Une tâche miniature. Inverser des lettres est beaucoup plus facile que traduire. Cela montre comment les composants s’assemblent, pas la qualité de traduction.
  • Une recette réduite. La montée progressive a été réduite à 400 étapes pour un essai de 3 000 étapes. L’article utilisait 4 000 étapes sur 100 000.
  • Un total estimé. Notre total de 63 082 496 dépend du vocabulaire supposé (37 000) et du partage des poids. L’article arrondit à 65 millions.
  • La ligne de SmolLM2 vient de sa configuration, pas des essais de cette page. Les durées sont approximatives (ordinateur partagé).

Exercice guidé : suivre les formes d’un exemple plus grand

Section intitulée « Exercice guidé : suivre les formes d’un exemple plus grand »
  1. Utilisez le modèle miniature (d_model 64, 4 têtes, d_ff 256, vocabulaire 13). Prenez une source de 7 lettres et une entrée du décodeur de 8 jetons : le jeton de début et 7 lettres. Écrivez la forme après le plongement et les positions, pour la source et pour la cible.
  2. Écrivez la forme des poids d’autoattention de l’encodeur, du tenseur caché du FFN et de la mémoire.
  3. Écrivez la forme des poids d’autoattention masquée du décodeur, des poids d’attention croisée et des logits.
  4. Calculez PE(2, 0) et PE(2, 1) à la main, pour n’importe quelle valeur de d_model.
  5. Calculez le taux d’apprentissage du calendrier miniature (d_model 64, montée progressive 400) aux étapes 100 et 1 600.

Résultat attendu : une des formes d’attention n’est pas carrée.

Afficher les réponses
  1. Source (1, 7, 64); cible (1, 8, 64).
  2. Poids d’autoattention (1, 4, 7, 7); tenseur caché du FFN (1, 7, 256); mémoire (1, 7, 64).
  3. Autoattention masquée (1, 4, 8, 8), avec 28 cellules cachées par tête au-dessus de la diagonale; attention croisée (1, 4, 8, 7), sans masque causal; logits (1, 8, 13).
  4. Pour i = 0, le diviseur vaut 1 : PE(2, 0) = sin 2 = 0,909 et PE(2, 1) = cos 2 = −0,416. Cela ne dépend pas de d_model.
  5. L’étape 100 se trouve dans la montée progressive : 64^−0,5 × 100 × 400^−1,5 = 0,125 × 100 / 8 000 = 0,00156. (Un journal affiché après sched.step() indique 1,58e-03, le taux de l’étape suivante.) L’étape 1 600 se trouve après le sommet : 0,125 × 1 600^−0,5 = 0,125 / 40 = 0,003125 : la moitié du sommet, car 1 600 = 4 × 400 et le taux diminue selon 1/√étape.

Omar veut traduire les réponses de Larkfield en allemand. Prenez le modèle de base avec des vocabulaires séparés : 32 000 jetons anglais et 32 000 jetons allemands. Seuls le plongement allemand et la couche de sortie partagent une matrice. Comptez les paramètres avec le code de l’étape 7 et indiquez l’écart par rapport à 65 millions.

Afficher la réponse

Les couches ne changent pas : 6 × (3 152 384 + 4 204 032) = 44 138 496. Les plongements comptent maintenant deux matrices de 32 000 × 512 = 16 384 000 chacune : 32 768 000. Le total est de 76 906 496, soit environ 77 millions : 12 millions de plus que les 65 millions de l’article. Pourtant, chaque vocabulaire compte moins de 37 000 jetons. Le partage d’un vocabulaire et d’une matrice a permis une grande économie.

Erreur courante

nn.Transformer ne correspond pas à l'article : une LayerNorm supplémentaire à la fin

Symptôme. Vous comparez votre implémentation de l’article à nn.Transformer(norm_first=False) avec des poids copiés. Avant l’entraînement, elles concordent à 1,7 × 10⁻⁶ près, et vous faites confiance à cette vérification. Après l’entraînement, la même comparaison révèle un écart allant jusqu’à 2,14 dans les logits du modèle miniature. Pourtant, chaque couche correspond encore lorsqu’on la compare seule.

Cause. nn.Transformer crée une LayerNorm finale après la pile de l’encodeur et une autre après celle du décodeur. Le modèle à normalisation après les sous-couches de l’article n’en a aucune : chaque sous-couche se termine déjà par une LayerNorm. Avant l’entraînement, chaque γ vaut 1 et chaque β vaut 0. Une deuxième normalisation de nombres déjà normalisés ne change alors presque rien, ce qui cache le bogue. L’entraînement change γ et β, puis la deuxième normalisation change les nombres. (Un modèle à normalisation avant les sous-couches a besoin d’une normalisation finale, comme SmolLM2; d’où le choix par défaut de PyTorch.) Un lot avec remplissage en mode évaluation peut aussi emprunter un chemin rapide qui renvoie 0 aux positions de remplissage. L’écart y atteint 3,06.

Correction. Pour un modèle à normalisation après les sous-couches, définissez model.encoder.norm = None et model.decoder.norm = None. Définissez aussi model.encoder.use_nested_tensor = False avant de comparer les lignes avec remplissage. Comparez ensuite avec (mine - theirs).abs().max() : 9,5 × 10⁻⁷, avant et après l’entraînement. Testez avec des poids entraînés, pas seulement avec des poids fraîchement initialisés.

Vérifiez votre compréhension

Dans l'attention croisée, d'où viennent les requêtes, les clés et les valeurs? Quelle est la forme des poids pour 6 positions cibles, 5 jetons sources et 4 têtes?

Afficher la réponse

Les requêtes viennent du décodeur : chaque position cible pose sa question. Les clés et les valeurs viennent de la sortie de l’encodeur, la mémoire : la phrase source fournit son information. Les poids ont la forme (lot, têtes, positions cibles, jetons sources) = (1, 4, 6, 5). La matrice n’est pas carrée, et il n’y a aucun masque causal, car toute la source est connue. Chaque ligne a une somme de 1 sur les 5 jetons sources. Dans le modèle miniature, la couche 2 a appris une antidiagonale : la position 1 lit le jeton source 5, la position 2 lit le jeton 4, et ainsi de suite.

Vérifiez votre compréhension

Prédisez : pourquoi la perte d'entraînement lissée du modèle miniature s'arrête-t-elle près de 0,54 plutôt que d'atteindre 0? Pourquoi ses probabilités les plus élevées sont-elles d'environ 0,92 et non de 0,99?

Afficher la réponse

Le lissage des étiquettes avec ε = 0,1 change la cible : le bon jeton reçoit 0,9 + 0,1/13 = 0,908, et chaque autre jeton reçoit 0,0077. L’entropie croisée par rapport à cette cible est minimale quand le modèle prédit exactement la cible. Cette valeur minimale est l’entropie de la cible, soit 0,537. Même un modèle parfait a donc une perte d’environ 0,54. La meilleure prédiction est la cible elle-même. Le modèle apprend donc à donner au bon jeton environ 0,91, et non 0,99. Un modèle sûr à 100 % a une perte lissée plus élevée (1,846 dans la vérification). L’article accepte ce compromis : il « nuit à la perplexité », mais améliore l’exactitude et BLEU.

Vérifiez votre compréhension

Nommez deux choix du transformeur de 2017 que SmolLM2-135M a modifiés, et expliquez pourquoi chacun a changé.

Afficher la réponse

Deux choix au choix, par exemple. La normalisation après les sous-couches est devenue une normalisation avant les sous-couches avec RMSNorm. Avec la normalisation avant chaque sous-couche, le chemin résiduel de l’entrée à la sortie ne contient aucune normalisation. Les piles profondes (30 couches) s’entraînent donc de façon plus stable. RMSNorm ne calcule pas la moyenne : moins de travail pour un effet semblable. Les positions sinusoïdales ajoutées à l’entrée sont devenues des positions rotatives sur les requêtes et les clés de chaque couche. Le score dépend alors de la distance entre les jetons. Les clés et valeurs séparées pour chacune des 8 têtes ont cédé la place à l’attention à requêtes groupées. Elle compte 9 têtes de requêtes et 3 têtes de clés et valeurs. Cela réduit la taille du cache KV d’un facteur de 3. L’encodeur-décodeur est devenu un décodeur seul, car un modèle qui poursuit un texte n’a pas besoin d’une source séparée. Le dropout (désactivation aléatoire) de 0,1 est devenu 0,0, car le préentraînement lit des milliers de milliards de jetons, généralement une seule fois.

Récapitulatif

  • Le transformeur de 2017 traduit : un encodeur crée une mémoire de la source; un décodeur écrit avec l’autoattention masquée, l’attention croisée (requêtes du décodeur, clés et valeurs de la mémoire) et une couche à propagation avant.
  • Entrée : plongement × √d_model + positions sinusoïdales; un décalage de k positions correspond à une rotation de chaque paire.
  • Chaque sous-couche : LayerNorm(x + Dropout(Sublayer(x))); FFN(x) = max(0, xW₁ + b₁)W₂ + b₂; sortie liée au plongement.
  • Entraînement : Adam (0,9, 0,98, 10⁻⁹), montée progressive jusqu’à 6,99 × 10⁻⁴ à l’étape 4 000, dropout 0,1, lissage des étiquettes 0,1.
  • Modèle de base : 63 082 496 selon notre calcul, 65 millions dans l’article. Le modèle miniature a atteint 100 % de correspondance exacte avec une attention croisée antidiagonale.
  • Aujourd’hui : décodeur seul, RMSNorm avant les sous-couches, positions rotatives, couches à propagation avant avec portes, attention à requêtes groupées.

Le module facultatif de mathématiques se termine ici. Les études de cas viennent ensuite, en commençant par les images.

Leçon suivante : Classer des images de vêtements