Les blocs du transformeur
- Leçon 18
- 55 min
- Module 6 : Assembler un transformeur
Leçon 18 sur 23 · L'apprentissage profond et les transformeurs expliquésLes blocs du transformeur
1. Ce qu'est un réseau de neurones
2. Les tenseurs et PyTorch
3. Comment les réseaux de neurones apprennent
4. Diagnostiquer les problèmes d'entraînement
5. Représenter les séquences et le sens
6. Assembler un transformeur
7. Utiliser des modèles préentraînés
8. Sous le capot : les mathématiquesFacultatif
Ce que vous saurez faire
Construisez le bloc du transformeur, l’unité que chaque modèle de langage moderne répète de nombreuses fois. Empilez ensuite deux blocs, entraînez-les sur les billets de soutien de Larkfield et évaluez honnêtement le résultat. Comparez-le aux modèles de routage que vous avez déjà.
Carnet JupyterFaites cette leçon dans un carnet Jupyter
Le carnet (notebook) Jupyter contient le code de cette leçon, avec des tâches et des vérifications. Il entraîne le tout petit transformeur pendant 8 époques plutôt que 20. Il termine ainsi en quelques minutes sur un processeur gratuit. Environ 40 minutes.
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 main
Ce que vous devez savoir
- Les requêtes, les clés, les valeurs et une tête d’attention, dans L’attention étape par étape. Cette leçon reprend là où celle-là se termine.
- Les jetons (tokens), leurs identifiants, le remplissage et la table de plongements (embeddings), dans Jetons et plongements.
- Les cinq étapes de l’entraînement et
train()dansticketnet.py, dans La boucle d’entraînement; la meilleure époque selon la perte de validation, dans Lots, époques et validation. - Le dropout (désactivation aléatoire), dans Limiter le surapprentissage, et la disparition des gradients, dans Déboguer l’expérience.
Ce qu’il vous faut sur votre ordinateur (seulement pour le parcours local; le carnet n’en a pas besoin)
- Le dossier
ticket-routeravec son environnement.venvactif,ticketnet.pyetdata/, dans Installer PyTorch : parcours 2. S’il manque ou ne fonctionne plus, suivez Réinitialiser la configuration.
Testé avec Python 3.12 et PyTorch 2.14.1 sur le processeur (macOS).
D’une tête d’attention à un bloc
Section intitulée « D’une tête d’attention à un bloc »Dans la dernière leçon, vous avez calculé une tête d’attention. Chaque jeton envoyait une requête, la comparait à chaque clé et calculait une moyenne pondérée des valeurs. Le résultat était un nouveau vecteur pour chaque jeton, formé à partir des jetons importants pour lui.
Une tête ne constitue pas encore un modèle. Un transformeur est une pile de blocs identiques. Chaque bloc comporte quatre parties autour de l’attention. Cette leçon les ajoute une à une. Pour chaque partie, vous voyez d’abord le problème qu’elle résout :
- L’attention multitête : plusieurs têtes, chacune avec sa propre question.
- Une couche à propagation avant : un petit réseau qui travaille sur chaque jeton séparément.
- Des connexions résiduelles : conserver l’entrée et ajouter le changement.
- La normalisation de couche : maintenir les nombres dans une plage stable.
L’attention multitête : plusieurs personnes à l’écoute
Section intitulée « L’attention multitête : plusieurs personnes à l’écoute »La situation familière. Dans une réunion sur une lampe brisée, un collègue écoute pour savoir ce qui est brisé. Un autre écoute pour savoir quand c’est arrivé. Un troisième écoute pour savoir ce que le client veut. Chaque personne entend les mêmes mots, mais porte attention à des mots différents.
Le problème. Une tête donne à chaque jeton une ligne de poids dont la somme est 1. Prenons « la lampe que j’ai retournée est arrivée brisée ». Le jeton « brisée » doit lire « lampe » pour savoir ce qui est brisé. Il doit aussi lire « retournée » pour savoir quelle équipe est concernée. Une seule ligne de softmax (normalisation en probabilités) doit répartir son poids entre les deux. Chaque signal devient donc plus faible.
L’idée. Utilisez plusieurs petites têtes côte à côte. Chaque tête a ses propres poids de requête, de clé et de valeur. Chacune apprend donc sa propre question. On joint leurs sorties, puis une dernière couche linéaire les mélange. C’est l’attention multitête : « multitête » parce qu’il y a plusieurs têtes, chacune effectuant un calcul d’attention complet.
Voici deux têtes sur 3 jetons de 8 nombres chacun. nn.MultiheadAttention est la couche d’attention de PyTorch. batch_first=True indique que le lot est la première dimension, comme partout dans ce cours. average_attn_weights=False garde les poids de chaque tête séparés.
import torchfrom torch import nn
torch.manual_seed(0)x = torch.randn(1, 3, 8) # 1 ticket, 3 tokens, 8 numbers per tokenattention = nn.MultiheadAttention(embed_dim=8, num_heads=2, batch_first=True)out, weights = attention(x, x, x, average_attn_weights=False)print("input ", tuple(x.shape))print("output ", tuple(out.shape))print("weights", tuple(weights.shape))print(weights[0].detach().numpy().round(2))La sortie :
input (1, 3, 8)output (1, 3, 8)weights (1, 2, 3, 3)[[[0.4 0.3 0.3 ] [0.33 0.33 0.34] [0.19 0.41 0.4 ]] [[0.26 0.34 0.4 ] [0.21 0.28 0.51] [0.29 0.34 0.37]]]Lisez la forme des poids (1, 2, 3, 3) à partir de la gauche. Il y a 1 billet et 2 têtes. Chaque tête a un tableau de 3 × 3, avec une ligne pour chaque jeton qui lit. La somme de chaque ligne est 1. Les deux têtes lisent différemment : le jeton 3 donne 0,19 au jeton 1 dans la tête 1, mais 0,29 dans la tête 2. La sortie a la même forme que l’entrée, (1, 3, 8).
Chaque tête travaille avec une portion des nombres : 2 têtes × 4 nombres = 8. Ajouter des têtes n’ajoute donc pas de paramètres. Elles divisent les mêmes nombres en portions plus nombreuses et plus petites :
for heads in (1, 4, 8): layer = nn.MultiheadAttention(embed_dim=64, num_heads=heads, batch_first=True) print(heads, "heads:", sum(p.numel() for p in layer.parameters()), "parameters")La sortie :
1 heads: 16640 parameters4 heads: 16640 parameters8 heads: 16640 parametersLes 16 640 paramètres comprennent les poids de requête, de clé et de valeur (3 × 64 × 64). Ils comprennent aussi le mélange de sortie (64 × 64) et leurs biais. Le tout petit transformeur de Larkfield utilise 4 têtes de 16 nombres chacune.
La couche à propagation avant : travailler sur ce qu’on a entendu
Section intitulée « La couche à propagation avant : travailler sur ce qu’on a entendu »La situation familière. Après la réunion, chaque personne retourne à son bureau et travaille seule sur ce qu’elle a entendu.
Le problème. L’attention ne fait que mélanger. Chaque sortie est une moyenne pondérée de vecteurs de valeurs. Un jeton peut donc recueillir de l’information, mais ne peut pas en faire grand-chose. Rappelez-vous Pourquoi les couches ont besoin d’activations : les moyennes et les autres étapes linéaires ne peuvent pas créer de courbes.
L’idée. Donnez à chaque jeton un petit réseau : une couche linéaire vers 256 nombres, ReLU, puis une couche linéaire vers 64. Chaque jeton utilise les mêmes poids, mais ici, aucun jeton ne regarde les autres. C’est pourquoi on l’appelle une couche à propagation avant, par jeton. Les nombres avancent seulement, dans le petit réseau propre à un jeton.
torch.manual_seed(0)feed_forward = nn.Sequential(nn.Linear(64, 256), nn.ReLU(), nn.Linear(256, 64))h = torch.randn(1, 5, 64) # 1 ticket, 5 tokenstogether = feed_forward(h) # all 5 tokens at oncealone = feed_forward(h[:, 2]) # token 2 on its ownprint(tuple(together.shape), tuple(alone.shape))print(torch.allclose(together[:, 2], alone, atol=1e-6))print(sum(p.numel() for p in feed_forward.parameters()), "parameters")La sortie :
(1, 5, 64) (1, 64)True33088 parametersnn.Linear travaille sur la dernière dimension et conserve toutes les dimensions précédentes. Les 5 jetons passent donc ensemble. Le jeton 2 obtient le même résultat que lorsqu’il passe seul : True (vrai). La couche à propagation avant contient 33 088 paramètres, deux fois plus que l’attention.
Les connexions résiduelles : conserver l’original, ajouter le changement
Section intitulée « Les connexions résiduelles : conserver l’original, ajouter le changement »La situation familière. Vous modifiez le rapport d’un collègue avec le « suivi des modifications ». Le texte original reste, et vos modifications s’y ajoutent. Si une modification est mauvaise, l’original est toujours là.
Le problème. Dans une pile profonde, chaque couche remplace son entrée par sa sortie. Chaque couche doit transmettre tout ce qui compte. Le gradient doit aussi revenir à travers chaque couche. Dans Déboguer l’expérience, la première couche d’une pile profonde de couches sigmoïdes recevait un gradient de 1,45 × 10⁻⁸. Elle ne pouvait pas apprendre.
L’idée. Ne remplacez pas l’entrée : ajoutez-y la sortie de la couche. Voici un petit exemple avec 3 nombres :
x = torch.tensor([1.0, -2.0, 0.5])change = torch.tensor([0.1, 0.3, -0.2])print(x + change)La sortie :
tensor([ 1.1000, -1.7000, 0.3000])C’est une connexion résiduelle. « Résiduel » signifie « ce qui reste ». La couche doit seulement apprendre la différence entre son entrée et une meilleure version de celle-ci. Les nombres originaux traversent directement l’addition, tout comme le gradient au retour. Voici une pile de 20 couches sigmoïdes, avec et sans connexion résiduelle :
def first_layer_gradient(residual, depth=20): torch.manual_seed(0) layers = [nn.Sequential(nn.Linear(64, 64), nn.Sigmoid()) for _ in range(depth)] h = torch.randn(32, 64) for layer in layers: h = h + layer(h) if residual else layer(h) # keep h and add the change, or replace h h.pow(2).mean().backward() return layers[0][0].weight.grad.norm().item()
print(f"without residual connections: {first_layer_gradient(False):.2e}")print(f"with residual connections: {first_layer_gradient(True):.2e}")La sortie :
without residual connections: 3.44e-18with residual connections: 9.55e-01Sans connexions résiduelles, le gradient de la première couche est de 3,44 × 10⁻¹⁸. Cette couche n’apprend donc jamais. Avec ces connexions, il est de 0,955. Les mêmes couches, avec un seul + ajouté, peuvent apprendre de la première à la dernière.
La normalisation de couche : maintenir les nombres dans une plage stable
Section intitulée « La normalisation de couche : maintenir les nombres dans une plage stable »La situation familière. Avant un concert, un ingénieur du son règle chaque microphone au même niveau. Une voix douce et un tambour puissant arrivent alors à la console de mixage dans la même plage.
Le problème. Les connexions résiduelles ajoutent un changement à chaque couche. Les nombres peuvent donc grandir. Voici vingt additions d’une couche linéaire aléatoire :
torch.manual_seed(0)h = torch.randn(1, 64)for step in range(1, 21): h = h + nn.Linear(64, 64)(h) # add a change, twenty times if step in (1, 5, 10, 20): print(f"after {step:2} additions: spread {h.std().item():10.2f}")La sortie :
after 1 additions: spread 1.16after 5 additions: spread 1.95after 10 additions: spread 3.92after 20 additions: spread 14.42La dispersion (l’écart-type) passe d’environ 1 à 14. Les grandes entrées poussent softmax et les couches suivantes vers des plages où l’apprentissage est lent et instable.
L’idée. Avant chaque partie du bloc, remettez les nombres de chaque jeton à l’échelle. Soustrayez leur moyenne et divisez par leur dispersion. Un facteur d’échelle et un décalage appris redonnent ensuite le contrôle au modèle. C’est la normalisation de couche : elle normalise les nombres de la sortie d’une couche, un jeton à la fois. Pour les 4 nombres 2, 4, 6 et 8, la moyenne est 5 et la dispersion est √5 ≈ 2,236. Ainsi, 2 devient (2 − 5) ÷ 2,236 = −1,342 :
row = torch.tensor([[2.0, 4.0, 6.0, 8.0]])print(nn.LayerNorm(4)(row).detach())big = row * 1000print(nn.LayerNorm(4)(big).detach())La sortie :
tensor([[-1.3416, -0.4472, 0.4472, 1.3416]])tensor([[-1.3416, -0.4472, 0.4472, 1.3416]])La ligne multipliée par 1 000 donne le même résultat : le niveau disparaît, mais la structure reste. Une nouvelle nn.LayerNorm commence avec un facteur d’échelle de 1 et un décalage de 0. Vous voyez donc les nombres normalisés sans autre transformation.
Le bloc : quatre parties, une seule forme
Section intitulée « Le bloc : quatre parties, une seule forme »Assemblez maintenant les quatre parties. Le schéma montre le bloc du tout petit transformeur de Larkfield. Il indique la forme sur chaque flèche et les paramètres de chaque partie.
Description textuelle du diagramme
Le schéma forme une chaîne de gauche à droite. L’entrée du bloc a la forme (8, T, 64) : 8 billets, T jetons et 64 nombres par jeton. Elle traverse une LayerNorm (128 paramètres), puis l’attention multitête avec 4 têtes de 16 nombres (16 640 paramètres). Une ligne transporte l’entrée du bloc autour des deux cases jusqu’à un cercle avec un signe plus. Ce cercle l’ajoute à la sortie de l’attention : c’est la première connexion résiduelle. La somme traverse une deuxième LayerNorm (128 paramètres), puis la couche à propagation avant. Cette couche travaille sur chaque jeton séparément : de 64 à 256 nombres, ReLU, puis retour à 64 (33 088 paramètres). Un deuxième signe plus ajoute sa sortie à son entrée, que la deuxième connexion résiduelle transporte autour de ces éléments. La sortie du bloc a la même forme, (8, T, 64). Une note indique qu’un bloc contient 49 984 paramètres et que le tout petit transformeur empile 2 blocs.
PyTorch propose le bloc entier sous forme d’une seule couche, nn.TransformerEncoderLayer. norm_first=True place chaque LayerNorm avant sa partie, comme dans le schéma. Le premier transformeur, en 2017, plaçait la normalisation après l’addition (« addition et normalisation »). Aujourd’hui, on normalise généralement d’abord, car les piles profondes s’entraînent alors de façon plus stable. dropout=0.1 désactive 10 % des nombres pendant l’entraînement, comme dans Limiter le surapprentissage.
torch.manual_seed(0)block = nn.TransformerEncoderLayer(d_model=64, nhead=4, dim_feedforward=256, dropout=0.1, batch_first=True, norm_first=True)x = torch.randn(8, 120, 64) # 8 tickets, 120 tokens, 64 numbers per tokenprint(tuple(block(x).shape))for name, p in block.named_parameters(): print(f"{name:<24} {str(tuple(p.shape)):<10} {p.numel():>6,}")print("in all", f"{sum(p.numel() for p in block.parameters()):,}")La sortie :
(8, 120, 64)self_attn.in_proj_weight (192, 64) 12,288self_attn.in_proj_bias (192,) 192self_attn.out_proj.weight (64, 64) 4,096self_attn.out_proj.bias (64,) 64linear1.weight (256, 64) 16,384linear1.bias (256,) 256linear2.weight (64, 256) 16,384linear2.bias (64,) 64norm1.weight (64,) 64norm1.bias (64,) 64norm2.weight (64,) 64norm2.bias (64,) 64in all 49,984(8, 120, 64) à l’entrée, (8, 120, 64) à la sortie. Chaque partie du schéma figure dans la liste. in_proj_weight regroupe les poids de requête, de clé et de valeur (3 × 64 = 192 lignes). out_proj mélange les têtes. linear1 et linear2 constituent la couche à propagation avant. norm1 et norm2 contiennent les facteurs d’échelle et les décalages appris. Comme la forme ne change pas, les blocs s’empilent comme des briques. Les grands modèles en empilent davantage : SmolLM2-135M, dans le module 7, en a 30.
Le tout petit transformeur sur les billets
Section intitulée « Le tout petit transformeur sur les billets »Le fichier ticketnet.py du cours contient TinyTransformer. Il ajoute des vecteurs de position aux vecteurs des jetons et applique 2 blocs, puis une LayerNorm finale. Il calcule ensuite la moyenne des jetons réels de chaque billet. Une couche linéaire donne alors 5 scores. La prochaine leçon explique les vecteurs de position et le masque de remplissage. Voici les formes pour 8 billets de validation; le plus long contient 93 jetons, donc T = 93 :
from ticketnet import (TinyTransformer, Vocab, labels, load_split, set_seed, token_ids, train, evaluate, count_parameters, TEAMS, PAD)
train_rows, valid_rows, test_rows = (load_split(f"data/{s}.csv") for s in ("train", "valid", "test"))vocab = Vocab([row["text"] for row in train_rows])ids_train, ids_valid = token_ids(train_rows, vocab), token_ids(valid_rows, vocab)y_train, y_valid = labels(train_rows), labels(valid_rows)
set_seed(0)model = TinyTransformer(len(vocab))model.eval()ids = token_ids(valid_rows[:8], vocab)with torch.no_grad(): positions = torch.arange(ids.shape[1]) h = model.embed(ids); print("token vectors ", tuple(h.shape)) h = h + model.position(positions); print("+ position vectors", tuple(h.shape)) h = model.blocks(h, src_key_padding_mask=ids == PAD) print("after 2 blocks ", tuple(h.shape)) keep = (ids != PAD).unsqueeze(-1).float() mean = (model.norm(h) * keep).sum(1) / keep.sum(1) print("average per ticket", tuple(mean.shape)) print("scores ", tuple(model.out(mean).shape))La sortie :
token vectors (8, 93, 64)+ position vectors (8, 93, 64)after 2 blocks (8, 93, 64)average per ticket (8, 64)scores (8, 5)Les blocs conservent (8, 93, 64). Seule la moyenne retire la dimension des jetons. La dernière couche transforme les 64 nombres en 5 scores d’équipe. Où se trouvent les paramètres?
for name, part in model.named_children(): print(f"{name:<9} {count_parameters(part):>8,}")print(f"{'in all':<9} {count_parameters(model):>8,}")La sortie :
embed 183,296position 7,680blocks 99,968norm 128out 325in all 291,397La table de plongements, 2 864 jetons × 64 nombres, contient 63 % des 291 397 paramètres. Les deux blocs, 2 × 49 984, contiennent la majeure partie du reste.
Entraînez-le
Section intitulée « Entraînez-le »L’entraînement utilise la même fonction train() que le réseau à sac de mots. Il utilise Adam, un taux d’apprentissage de 0,001, des lots de 64, 20 époques et une graine de 0. log=None le rend silencieux, et la boucle affiche six des époques.
import time
set_seed(0)model = TinyTransformer(len(vocab))start = time.time()history = train(model, ids_train, y_train, ids_valid, y_valid, epochs=20, lr=1e-3, log=None)print(f"{time.time() - start:.0f} seconds")for row in history: if row["epoch"] in (1, 2, 5, 10, 15, 20): print(row)best = min(history, key=lambda row: row["valid_loss"])print("best epoch", best["epoch"], "| valid loss", best["valid_loss"], "| valid accuracy", best["valid_acc"])La sortie :
119 seconds{'epoch': 1, 'train_loss': 0.9262, 'train_acc': 0.6767, 'valid_loss': 0.9591, 'valid_acc': 0.6717}{'epoch': 2, 'train_loss': 0.4359, 'train_acc': 0.8524, 'valid_loss': 0.4562, 'valid_acc': 0.8533}{'epoch': 5, 'train_loss': 0.1381, 'train_acc': 0.9678, 'valid_loss': 0.2343, 'valid_acc': 0.91}{'epoch': 10, 'train_loss': 0.0317, 'train_acc': 0.9951, 'valid_loss': 0.2239, 'valid_acc': 0.925}{'epoch': 15, 'train_loss': 0.0034, 'train_acc': 0.9998, 'valid_loss': 0.2091, 'valid_acc': 0.9417}{'epoch': 20, 'train_loss': 0.0005, 'train_acc': 1.0, 'valid_loss': 0.2266, 'valid_acc': 0.9433}best epoch 15 | valid loss 0.2091 | valid accuracy 0.9417Cela prend environ deux minutes sur le processeur d’un Mac mini; le réseau à sac de mots prend quelques secondes. Vos nombres peuvent différer légèrement sur un autre ordinateur. La perte d’entraînement descend à 0,0005, et l’exactitude d’entraînement atteint 100 %. La perte de validation est au plus bas à l’époque 15 : 0,209, avec 94.2% des billets de validation correctement classés.
La prochaine leçon et L’attention étape par étape chargent ce modèle entraîné. Enregistrez donc ses poids. torch.save() les écrit dans un fichier; Enregistrer et examiner les résultats l’explique en détail. La ligne n’affiche rien.
torch.save(model.state_dict(), "tiny_transformer.pt")La comparaison honnête
Section intitulée « La comparaison honnête »Priya compare le nouveau modèle aux modèles de routage que Larkfield utilise déjà, sur les mêmes 600 billets de validation.
- Tout petit transformeur
- Réseau à sac de mots
- TF-IDF + régression logistique
Description textuelle du graphique
Un graphique en courbes avec l’époque, de 1 à 20, sur l’axe horizontal et l’exactitude de validation, de 60 % à 100 %, sur l’axe vertical. Le réseau à sac de mots commence à 97,2 % après l’époque 1, atteint un maximum de 99,2 % à l’époque 4 et termine à 97,3 %. Le modèle TF-IDF avec régression logistique du cours d’apprentissage automatique forme une ligne horizontale pointillée à 98,8 %. Le tout petit transformeur commence à 67,2 % et atteint 91,0 % à l’époque 5. Il atteint 94,2 % à sa meilleure époque, la 15e, et termine à 94,3 %. Il ne s’approche jamais des deux autres courbes.
Globalement, le tout petit transformeur donne la bonne réponse dans 94,2 % des cas à sa meilleure époque. Le réseau à sac de mots atteint 98,3 % à sa meilleure époque, et TF-IDF avec régression logistique, 98,8 %. Par style (style), à la dernière époque de chaque exécution, le tout petit transformeur est plus faible presque partout :
| Style | Sac de mots | Tout petit transformeur | TF-IDF + régression logistique |
|---|---|---|---|
| two_topics | 90,4 % | 84,6 % | 88,5 % |
| long | 81,0 % | 76,2 % | 100 % |
| negation | 94,7 % | 92,1 % | 100 % |
| short | 97,9 % | 89,6 % | 100 % |
| typos | 100 % | 92,5 % | 97,5 % |
| boundary | 97,7 % | 96,9 % | 100 % |
| plain | 100 % | 97,9 % | 100 % |
Même sur les billets avec une négation ou deux sujets, où l’ordre des mots devrait aider, le transformeur ne gagne pas.
Est-ce une question de réglages? Priya mène une recherche équitable de réglages : 8 exécutions de 25 époques chacune. Les exécutions essaient 1 ou 2 blocs, un dropout (désactivation aléatoire) de 0,1 ou 0,3 et un taux d’apprentissage de 0,0005 ou 0,001. Chaque exécution conserve l’époque dont la perte de validation est la plus faible. Les huit exécutions atteignent 92,8 % à 94,7 %. Les réglages ne comblent pas l’écart. Enfin, chaque modèle traite les billets de test une seule fois :
Description textuelle du graphique
Un diagramme à colonnes de l’exactitude de test de quatre modèles de routage. TF-IDF avec régression logistique : 99,2 %. Le réseau à sac de mots : 96,5 %. Le réseau à moyenne de plongements : 95,2 %. Le tout petit transformeur : 91,2 %.
Sur les billets de test, le tout petit transformeur donne la bonne réponse dans 91,2 % des cas. C’est moins que le sac de mots (96,5 %) et bien moins que TF-IDF avec régression logistique (99,2 %). Larkfield garde le modèle linéaire. C’est un résultat, pas un échec, et les raisons expliquent la suite du cours :
- Trop peu de données pour apprendre la langue. Les 4 696 billets d’entraînement contiennent environ 269 000 jetons. À partir de ces seuls billets, le modèle doit apprendre le sens de chaque mot et le fonctionnement de leur ordre.
- Plus de paramètres à ajuster. Les 291 397 nombres commencent avec des valeurs aléatoires et sont ajustés sur 4 696 exemples. L’exactitude d’entraînement de 100 % et la perte de validation croissante après l’époque 15 montrent que le modèle mémorise les billets d’entraînement.
- Rien n’est préentraîné. Le vecteur de « refund » (remboursement) commence avec 64 nombres aléatoires. Un modèle qui a déjà lu des milliards de mots commence avec des vecteurs et des blocs utiles.
Sur ces billets courts, les mots déterminent l’équipe, donc compter les mots suffit. Les blocs deviennent utiles quand un modèle a d’abord lu beaucoup de texte. C’est le préentraînement, le sujet de la leçon Les objectifs d’entraînement. Le module 7 exécute un modèle préentraîné. L’étude de cas Faire le réglage fin d’un petit transformeur mesure ce que le préentraînement apporte à une tâche textuelle.
Exercice guidé : construire et entraîner le bloc dans le carnet
Section intitulée « Exercice guidé : construire et entraîner le bloc dans le carnet »- Ouvrez le carnet depuis l’encadré Faites cette leçon dans un carnet en haut de cette page, puis exécutez les cellules de préparation. La cellule de données termine avec
Ready: train 4696 tickets, valid 600, test 600.(prêt : 4 696 billets d’entraînement, 600 de validation, 600 de test). - Dans la section 1, exécutez les deux têtes. Vous voyez la forme des poids
(1, 2, 3, 3). - Dans la section 2, exécutez la couche à propagation avant et le test des connexions résiduelles. Vous voyez
True(vrai) et3.44e-18. - Dans la section 3, construisez un bloc. Dans la cellule À vous de jouer, placez son nombre de paramètres dans
block_parameters, puis exécutez la cellule de vérification. Elle afficheCheck passed.(vérification réussie). - Dans la section 4, entraînez le tout petit transformeur pendant 8 époques. Cela prend une à trois minutes. Vous voyez une exactitude de validation d’environ 92 %.
- Calculez l’exactitude par style. Vous voyez que
long(long) ettwo_topics(deux sujets) figurent parmi les styles les plus faibles.
Afficher un exemple de réponse à l’étape 4
block_parameters = sum(p.numel() for p in block.parameters())print(block_parameters)49984L’attention compte 16 640 paramètres, la couche à propagation avant, 33 088, et les deux LayerNorm, 128 chacune.
À vous de jouer : huit têtes au lieu de quatre
Section intitulée « À vous de jouer : huit têtes au lieu de quatre »Changez une seule chose : TinyTransformer(len(vocab), heads=8). Avant de l’exécuter, prédisez le nombre de paramètres et la forme des scores pour les 8 billets.
Afficher la réponse
set_seed(0)eight = TinyTransformer(len(vocab), heads=8)print(count_parameters(eight))with torch.no_grad(): print(tuple(eight.eval()(ids).shape))291397(8, 5)Le nombre de paramètres reste à 291 397 et les scores gardent la forme (8, 5). Huit têtes divisent les 64 nombres en groupes de 8 plutôt que de 16, et les poids gardent leur taille. Le nombre de têtes change ce que le modèle peut apprendre, pas sa taille en mémoire.
Erreur courante
« PyTorch trouvera quelle dimension représente le lot »
Symptôme : Tomás construit un bloc sans batch_first=True. Il s’exécute sans erreur et renvoie la bonne forme. Mais la sortie d’un billet change quand les autres billets de son lot changent :
torch.manual_seed(0)wrong = nn.TransformerEncoderLayer(64, 4, 256, dropout=0.0) # batch_first is Falsewrong.eval()batch = torch.randn(8, 10, 64) # 8 tickets, 10 tokensother = batch.clone()other[1:] = torch.randn(7, 10, 64) # the same ticket 0, seven other ticketswith torch.no_grad(): a, b = wrong(batch)[0], wrong(other)[0]print(tuple(wrong(batch).shape))print("ticket 0 changed:", not torch.allclose(a, b, atol=1e-6))(8, 10, 64)ticket 0 changed: TrueCause : sans batch_first=True, PyTorch lit la forme comme (jetons, lot, nombres). Il voit 10 séquences de 8 jetons. L’attention mélange donc les 8 billets entre eux, et le billet 0 lit les autres billets. La forme (8, 10, 64) semble correcte, donc aucune erreur n’apparaît.
Correction : définissez batch_first=True sur chaque couche d’attention et chaque bloc, comme le fait ticketnet.py. Testez ensuite : la sortie d’un billet ne doit pas dépendre de ses voisins.
torch.manual_seed(0)right_layer = nn.TransformerEncoderLayer(64, 4, 256, dropout=0.0, batch_first=True)right_layer.eval()with torch.no_grad(): a, b = right_layer(batch)[0], right_layer(other)[0]print("ticket 0 changed:", not torch.allclose(a, b, atol=1e-6))ticket 0 changed: FalseCe que cela signifie pour un vrai système d’IA : au service de soutien, Amira envoie les billets par lots. Avec ce bogue, un même billet pourrait être envoyé à une autre équipe selon les billets arrivés avec lui. Vérifiez le sens de chaque dimension, pas seulement la forme.
Vérification des connaissances
Section intitulée « Vérification des connaissances »Vérifiez votre compréhension
Nommez les quatre parties d'un bloc de transformeur et le problème que chacune résout.
Afficher la réponse
L’attention multitête permet à chaque jeton de recueillir de l’information auprès des autres jetons. Elle possède plusieurs têtes qui apprennent chacune leur propre question. Une seule ligne de softmax n’a donc pas à répondre à tous les besoins. La couche à propagation avant traite chaque jeton séparément, avec ReLU, pour que le bloc transforme ce que l’attention a recueilli. L’attention seule ne calcule que des moyennes pondérées. Les connexions résiduelles ajoutent la sortie de chaque partie à son entrée. Elles conservent l’original, et le gradient atteint les premières couches (3,44 × 10⁻¹⁸ sans elles, 0,955 avec elles, dans le test à 20 couches). La normalisation par couche ajuste les nombres de chaque jeton pour obtenir une moyenne de 0 et une dispersion de 1 avant chaque partie. Les nombres ne grossissent donc pas à mesure que les changements s’accumulent.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Omar demande : le transformeur est le modèle le plus récent, alors pourquoi Larkfield garde-t-elle le modèle linéaire? Répondez-lui avec les chiffres et les raisons.
Afficher la réponse
Sur les mêmes billets, le tout petit transformeur est le modèle de routage le plus faible. Il obtient 94,2 % en validation à sa meilleure époque et de 92,8 % à 94,7 % après une recherche équitable de réglages. Sur les billets de test, il obtient 91,2 %. TF-IDF avec régression logistique obtient 98,8 % en validation et 99,2 % au test. Le réseau à sac de mots obtient 96,5 % au test. Le transformeur doit apprendre le sens de chaque mot et le fonctionnement de leur ordre à partir de seulement 4 696 billets. Ses 291 397 paramètres commencent avec des valeurs aléatoires, et il mémorise les billets d’entraînement (exactitude d’entraînement de 100 %). Sur ces billets courts, les mots déterminent l’équipe, donc compter les mots suffit. Un transformeur devient utile quand il part d’un préentraînement sur beaucoup plus de texte.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Récapitulatif
- L’attention multitête exécute plusieurs petites têtes côte à côte, chacune avec sa propre question. Ajouter des têtes n’ajoute pas de paramètres.
- La couche à propagation avant est un petit réseau qui traite chaque jeton séparément : 64 → 256 → ReLU → 64.
- Une connexion résiduelle conserve l’entrée et ajoute le changement, pour que les gradients atteignent les premières couches.
- La normalisation par couche maintient les nombres de chaque jeton à une moyenne de 0 et une dispersion de 1.
- Un bloc garde la même forme en entrée et en sortie, (lot, jetons, 64), donc les blocs s’empilent. Le tout petit transformeur compte 2 blocs et 291 397 paramètres.
- Sur les billets de Larkfield, c’est le modèle de routage le plus faible : 91,2 % au test, contre 96,5 % pour le sac de mots et 99,2 % pour le modèle linéaire. Il a trop peu de données, et rien n’est préentraîné.
À suivre : les deux éléments que cette leçon a utilisés sans les expliquer, les vecteurs de position et le masque de remplissage.
Leçon suivante : Position et masquage