Aller au contenu
English

Position et masquage

  • Leçon 19
  • 50 min
  • Module 6 : Assembler un transformeur

Ce que vous saurez faire

Donnez à un transformeur l’ordre des mots et contrôlez ce que chaque jeton (token) peut lire. Vous pourrez ensuite expliquer les deux masques utilisés dans les transformeurs. Vous comprendrez aussi la différence entre les modèles qui lisent un texte entier et ceux qui en écrivent un.

Carnet JupyterFaites cette leçon dans un carnet JupyterColab · Kaggle · GitHub · en anglais

Le carnet (notebook) Jupyter exécute le test de mélange et les deux masques sur de vrais tenseurs. Il entraîne d’abord son propre petit transformeur pendant 8 époques (une à trois minutes). Ses scores diffèrent donc un peu de ceux de cette page. Environ 35 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 mainEnviron 50 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur (seulement pour le parcours local)

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

Écrivez chaque étape d’une recette sur une fiche, puis laissez tomber les fiches. La personne qui les ramasse a toutes les étapes, mais pas leur ordre. « Cuire pendant 20 minutes » peut maintenant précéder « mélanger la farine ». Tous les mots sont là, mais le sens dépend de la place de chaque fiche.

Pourquoi l’ordre des mots et le contexte comptent a montré un problème du sac de mots. « Endommagé à l’arrivée, pas après utilisation » et « endommagé après utilisation, pas à l’arrivée » reçoivent la même entrée. L’attention fait-elle mieux? Exécutez le test avec deux phrases inventées contenant les mêmes 7 jetons dans un ordre différent. Une couche d’attention lit chaque phrase. La moyenne de ses sorties forme un résumé.

positions.py
import torch
from torch import nn
words = ["damaged", "on", "arrival", ",", "not", "after", "use"]
a = [0, 1, 2, 3, 4, 5, 6] # damaged on arrival , not after use
b = [0, 5, 6, 3, 4, 1, 2] # damaged after use , not on arrival
torch.manual_seed(0)
table = nn.Embedding(len(words), 4) # one vector of 4 numbers per word
attention = nn.MultiheadAttention(4, 1, batch_first=True)
def read(order):
x = table(torch.tensor([order])) # (1, 7, 4)
out, _ = attention(x, x, x) # (1, 7, 4)
return out.mean(dim=1).detach() # one summary per sentence: (1, 4)
print(read(a))
print(read(b))
print(torch.allclose(read(a), read(b), atol=1e-6))

La sortie :

tensor([[-0.1657, -0.0738, 0.3466, -0.1725]])
tensor([[-0.1657, -0.0738, 0.3466, -0.1725]])
True

Les deux phrases opposées obtiennent le même résumé. Regardez à l’intérieur, la sortie pour « use » (utilisation), à la position 6 dans la phrase a et 2 dans b :

x_a, x_b = table(torch.tensor([a])), table(torch.tensor([b]))
out_a = attention(x_a, x_a, x_a)[0].detach()
out_b = attention(x_b, x_b, x_b)[0].detach()
print("'use' in a, position 6:", out_a[0, 6])
print("'use' in b, position 2:", out_b[0, 2])

La sortie :

'use' in a, position 6: tensor([-0.0194, -0.0040, 0.2147, -0.0818])
'use' in b, position 2: tensor([-0.0194, -0.0040, 0.2147, -0.0818])

Les mêmes nombres. L’attention compare chaque requête à chaque clé, peu importe leur place. « Use » lit donc le même ensemble de mots dans les deux phrases. Il obtient les mêmes poids et donne la même sortie. Mélangez l’entrée, et l’attention mélange simplement la sortie de la même façon. L’ordre n’entre jamais dans le calcul.

Donnez à chaque place dans le texte son propre motif de nombres, puis ajoutez-le au vecteur du jeton. « Use » à la position 6 et « use » à la position 2 deviennent alors des entrées différentes. C’est un plongement de position : une table apprise avec une ligne par position. Elle s’utilise exactement comme la table de plongements (embeddings) des jetons. Le nom général est encodage positionnel : toute méthode qui intègre la position aux vecteurs.

torch.manual_seed(1)
position = nn.Embedding(7, 4) # one vector of 4 numbers per position
steps = torch.arange(7) # positions 0, 1, ..., 6
def read_in_order(order):
x = table(torch.tensor([order])) + position(steps) # word vector + position vector
out, _ = attention(x, x, x)
return out.mean(dim=1).detach()
print(read_in_order(a))
print(read_in_order(b))
print(torch.allclose(read_in_order(a), read_in_order(b), atol=1e-6))

La sortie :

tensor([[-0.2461, -0.0334, 0.7288, -0.4205]])
tensor([[-0.1802, 0.0059, 0.7413, -0.4107]])
False

Les deux phrases diffèrent maintenant. Ces vecteurs de position sont encore aléatoires, donc la différence ne signifie encore rien. L’entraînement enseigne à la table ce que chaque position doit ajouter.

TinyTransformer contient la table self.position = nn.Embedding(120, 64) : 120 positions, car MAX_TOKENS limite les billets à 120 jetons. Chargez le modèle entraîné dans Les blocs du transformeur. Regardez ses 4 premiers nombres pour « parcel » (colis) à deux endroits :

from ticketnet import (TinyTransformer, Vocab, labels, load_split, token_ids, evaluate, tokenize,
TEAMS, PAD)
train_rows, valid_rows = load_split("data/train.csv"), load_split("data/valid.csv")
vocab = Vocab([row["text"] for row in train_rows])
model = TinyTransformer(len(vocab))
model.load_state_dict(torch.load("tiny_transformer.pt"))
model.eval()
print(tuple(model.position.weight.shape), model.position.weight.numel(), "numbers")
parcel = vocab.stoi["parcel"]
print("token 'parcel' ", model.embed.weight[parcel, :4].detach().numpy().round(2))
print("position 0 ", model.position.weight[0, :4].detach().numpy().round(2))
print("position 5 ", model.position.weight[5, :4].detach().numpy().round(2))
print("'parcel' at 0 ", (model.embed.weight[parcel, :4] + model.position.weight[0, :4]).detach().numpy().round(2))
print("'parcel' at 5 ", (model.embed.weight[parcel, :4] + model.position.weight[5, :4]).detach().numpy().round(2))

La sortie :

(120, 64) 7680 numbers
token 'parcel' [-1.35 -0.68 -1.01 -0.75]
position 0 [ 0.13 -0.41 -0.04 1.04]
position 5 [ 1.76 1.14 0.25 -0.61]
'parcel' at 0 [-1.21 -1.09 -1.06 0.29]
'parcel' at 5 [ 0.42 0.46 -0.77 -1.36]

torch.load() lit les nombres enregistrés, et load_state_dict() les place dans un nouveau modèle de même forme. Vérifiez le premier nombre à la main : −1,35 + 0,13 = −1,22. Il s’affiche comme −1,21, car la page arrondit chaque nombre séparément. « Parcel » à la position 0 et à la position 5 entrent maintenant dans les blocs sous forme de vecteurs différents.

Mélangez les 92 jetons du billet T-64697, un billet de paiement, puis comparez les 5 scores du modèle entraîné :

import random
ticket = valid_rows[0]["text"]
tokens = tokenize(ticket)
random.seed(0)
shuffled = random.sample(tokens, len(tokens))
print(" ".join(tokens[:10]), "...")
print(" ".join(shuffled[:10]), "...")
def scores(text):
ids = torch.tensor([vocab.encode(text)])
with torch.no_grad():
return [round(v, 3) for v in model(ids)[0].tolist()]
print("in order:", scores(ticket))
print("shuffled:", scores(" ".join(shuffled)))

La sortie :

i am writing regarding order lk - 714258 . the ...
i refund lk delivery process i receiving week so as ...
in order: [-3.104, -3.135, 10.019, -1.814, -1.48]
shuffled: [-3.283, -1.909, 10.309, -2.007, -2.905]

Les scores changent, donc le modèle remarque l’ordre. Mettez maintenant la table de positions à zéro pour que le modèle n’ait plus de positions :

saved = model.position.weight.data.clone()
model.position.weight.data.zero_() # remove the positions
print("in order:", scores(ticket))
print("shuffled:", scores(" ".join(shuffled)))
model.position.weight.data = saved # put them back

La sortie :

in order: [-3.571, -2.937, 10.644, -2.797, 0.005]
shuffled: [-3.571, -2.937, 10.644, -2.797, 0.005]

Sans positions, le transformeur entraîné entier donne exactement les mêmes scores pour le billet et pour les mots mélangés. Les positions sont le seul endroit où l’ordre intervient.

À quel point le modèle entraîné utilise-t-il l’ordre? Quand tous les billets de validation sont mélangés, son exactitude est de 93.3%, contre 94.3% dans l’ordre : un point de moins. Le réseau à sac de mots obtient 98.0% sur les billets mélangés, car il ne peut pas remarquer le changement. Pour ces billets, les mots déterminent l’équipe, donc le transformeur utilise peu l’ordre. Il paie quand même tout le coût de son apprentissage.

Une table apprise est la méthode la plus simple, mais elle a une limite. Elle contient seulement 120 lignes de positions. Les positions rares pendant l’entraînement restent presque aléatoires. Deux autres méthodes sont courantes :

  • Positions sinusoïdales, utilisées dans le premier transformeur de 2017 : un motif fixe d’ondes de différentes longueurs pour chaque position. Une formule le calcule, donc rien n’est appris.
  • Positions rotatives (RoPE) : au lieu d’ajouter un vecteur, elles tournent chaque requête et chaque clé selon un angle qui augmente avec la position. L’attention voit ainsi la distance entre deux jetons. SmolLM2-135M, au module 7, utilise des positions rotatives.

Le masque de remplissage : ne lisez pas les lignes vides

Section intitulée « Le masque de remplissage : ne lisez pas les lignes vides »

La situation familière. Un formulaire contient dix lignes de réponse, et une réponse courte en laisse sept vides. Personne ne lit les lignes vides comme une partie de la réponse.

Le problème. Un lot forme un seul tenseur, donc tous ses billets doivent avoir la même longueur. token_ids() complète les billets courts avec <pad> (remplissage), d’identifiant 0, jusqu’à la longueur du plus long billet du lot. Prenez un court billet de livraison et placez-le dans un lot avec le plus long billet de validation :

short = next(row for row in valid_rows if row["style"] == "short")
long = max(valid_rows, key=lambda row: len(vocab.encode(row["text"])))
print(short["ticket_id"], short["team"], "|", short["text"])
pair = token_ids([short, long], vocab)
print(tuple(pair.shape))
print(pair[0, :12].tolist(), "...", pair[0, -4:].tolist())
print("padding in the short ticket:", int((pair[0] == PAD).sum()))

La sortie :

T-64715 delivery | Ugh, need to change trampoline delivery slot. Order LK-640933
(2, 120)
[2653, 8, 1664, 2591, 618, 2620, 832, 2338, 10, 1749, 1486, 9] ... [0, 0, 0, 0]
padding in the short ticket: 107

Le billet court contient 13 vrais jetons et 107 jetons de remplissage. Sans masque, l’attention lirait le remplissage comme des mots. Exécutez les étapes du modèle à la main, une fois avec le masque et une fois sans :

def team_scores(ids, use_mask):
with torch.no_grad():
h = model.embed(ids) + model.position(torch.arange(ids.shape[1]))
mask = ids == PAD if use_mask else None
h = model.blocks(h, src_key_padding_mask=mask)
keep = (ids != PAD).unsqueeze(-1).float()
mean = (model.norm(h) * keep).sum(1) / keep.sum(1)
return [round(v, 3) for v in model.out(mean)[0].tolist()]
alone = token_ids([short], vocab)
print("alone, no padding: ", team_scores(alone, use_mask=True))
print("padded, with mask: ", team_scores(pair, use_mask=True))
print("padded, without mask:", team_scores(pair, use_mask=False))

La sortie :

alone, no padding: [5.223, -1.598, -3.307, -4.811, 4.613]
padded, with mask: [5.223, -1.598, -3.307, -4.811, 4.613]
padded, without mask: [1.351, -6.228, -1.896, 0.008, 8.922]

Les équipes sont dans l’ordre : livraison, retours, paiement, garantie, compte. Seul, le billet va à la livraison (5,223), ce qui est correct. Avec remplissage, sans masque, il va à l’équipe compte (8,922) : 107 lignes vides ont changé la réponse.

L’idée. Indiquez à l’attention quelles clés ignorer. src_key_padding_mask a la forme (lot, jetons) et vaut True (vrai) lorsqu’un jeton doit être masqué : ids == PAD. Les clés masquées ne reçoivent aucun poids, donc les vrais jetons ne lisent jamais le remplissage. Avec le masque, le billet rempli obtient exactement les mêmes scores que lorsqu’il est seul. C’est le masque de remplissage. TinyTransformer l’utilise à chaque propagation avant, et sa moyenne ignore aussi les lignes de remplissage.

La situation familière. Dans un examen, les réponses sont imprimées sur la même feuille, sous chaque question. Un élève honnête couvre tout ce qui se trouve sous la ligne actuelle avec une feuille de papier.

Le problème. La prochaine leçon entraîne un modèle à prédire le jeton suivant à chaque position. Si la position 3 peut lire la position 4, elle lit la réponse au lieu de la prédire. Le modèle échoue alors dès qu’il doit écrire un nouveau texte, où le prochain jeton n’existe pas encore.

L’idée. Pour chaque jeton, masquez tous les jetons qui le suivent. Dans le tableau d’attention, cela correspond à toutes les cellules à droite de la diagonale : un triangle.

Une grille de 6 sur 6 pour les jetons my, parcel, arrived, broken, virgule, please. Les lignes représentent le jeton qui lit, les colonnes celui qu'il peut lire. Les cellules sur la diagonale et en dessous sont bleues : un jeton peut se lire lui-même et lire les jetons précédents. Les cellules au-dessus de la diagonale sont grises et portent la mention hidden (masqué). La ligne broken peut lire my, parcel, arrived et broken, mais pas la virgule ni please. 15 des 36 cellules sont masquées.
Le masque causal sur six jetons : un jeton peut se lire lui-même et lire les jetons précédents, jamais les suivants.
Description textuelle du diagramme

Le schéma est une grille de 6 lignes et 6 colonnes pour les jetons « my » (mon), « parcel », « arrived » (arrivé), « broken » (brisé), une virgule et « please » (s’il vous plaît). Chaque ligne représente un jeton qui lit, et chaque colonne un jeton qu’il peut lire. Les cellules sur la diagonale et en dessous sont bleues : un jeton peut se lire lui-même et lire tous les jetons précédents. Les cellules au-dessus de la diagonale sont grises et portent la mention « hidden » (masqué). Ce sont les jetons suivants, les réponses que le modèle doit prédire. Une note indique que la ligne « broken » peut lire « my », « parcel », « arrived » et « broken », mais pas la virgule ni « please ». 15 des 36 cellules sont masquées.

C’est le masque causal. « Causal », car le passé peut influencer un jeton, mais pas le futur. PyTorch le crée pour vous :

causal = nn.Transformer.generate_square_subsequent_mask(6)
print(causal)

La sortie :

tensor([[0., -inf, -inf, -inf, -inf, -inf],
[0., 0., -inf, -inf, -inf, -inf],
[0., 0., 0., -inf, -inf, -inf],
[0., 0., 0., 0., -inf, -inf],
[0., 0., 0., 0., 0., -inf],
[0., 0., 0., 0., 0., 0.]])

Le masque s’ajoute aux scores avant la fonction softmax (normalisation en poids dont la somme vaut 1). Un score de moins l’infini devient un poids d’exactement 0, car e à la puissance moins l’infini vaut 0. Le voici sur une tête d’attention, avec 6 vecteurs de jetons aléatoires :

torch.manual_seed(0)
x = torch.randn(1, 6, 8) # 6 tokens, 8 numbers each
head = nn.MultiheadAttention(8, 1, batch_first=True)
_, weights = head(x, x, x, attn_mask=causal)
print(weights[0].detach().numpy().round(2))

La sortie :

[[1. 0. 0. 0. 0. 0. ]
[0.6 0.4 0. 0. 0. 0. ]
[0.38 0.2 0.42 0. 0. 0. ]
[0.29 0.23 0.22 0.26 0. 0. ]
[0.22 0.17 0.21 0.18 0.22 0. ]
[0.12 0.16 0.26 0.15 0.15 0.16]]

Le triangle supérieur ne contient que des 0. Le premier jeton peut seulement se lire lui-même, donc son poids vaut 1. La somme de chaque ligne vaut toujours 1, répartie entre les jetons qu’elle peut lire.

Essayez les deux masques sur le petit transformeur entraîné. Faites d’abord une prédiction, puis choisissez un masque.

Explorer

Quelles cellules chaque masque cache-t-il?

Cellules masquées
0 sur 36
Cellules visibles
36
« my » lit
6 jetons

Aucune cellule n’est masquée. Chaque jeton lit tous les jetons, y compris les 2 jetons de remplissage : « late » donne 0,37 de son attention au remplissage. 0 cellule sur 36 est masquée; les poids visibles de chaque ligne totalisent 1.

Lignes : le jeton qui lit. Colonnes : le jeton qu’il lit.
myparcelislatepadpad
my0,100,130,260,140,230,14
parcel0,200,110,280,150,140,11
is0,040,460,080,170,110,13
late0,130,240,090,160,320,05
pad0,240,040,230,120,150,22
pad0,210,020,140,140,130,36
Les vrais poids d'attention du petit transformeur entraîné (bloc 1, moyenne de ses 4 têtes) pour « my parcel is late » (mon colis est en retard) et 2 jetons de remplissage, calculés avec chaque masque.
Description textuelle de l’explorateur

L’explorateur montre un tableau de 6 lignes et 6 colonnes pour les jetons « my », « parcel », « is » (est), « late » (en retard) et deux jetons de remplissage. Les lignes représentent le jeton qui lit; les colonnes représentent celui qu’il lit. Une rangée de boutons permet de choisir le masque. Sans masque : aucune cellule n’est masquée, et « late » accorde 0,37 de son attention aux deux jetons de remplissage. Masque de remplissage : les deux colonnes de remplissage sont masquées, soit 12 cellules sur 36. Les poids des vrais jetons augmentent pour combler l’écart, et « late » accorde maintenant 0,46 à « parcel ». Masque causal : les 15 cellules au-dessus de la diagonale sont masquées. « My » ne lit que lui-même, avec un poids de 1,00. « Parcel » accorde 0,51 à « my » et 0,49 à lui-même. Les deux masques : 18 cellules sont masquées. Les vrais jetons lisent comme avec le masque causal, et seules les lignes de remplissage changent. La somme des poids visibles de chaque ligne vaut 1.

Deux détails méritent votre attention. Les lignes de remplissage reçoivent encore des poids : le modèle les calcule. TinyTransformer les élimine lorsqu’il calcule la moyenne des vrais jetons. Avec le remplissage à la fin, le masque causal cache déjà le remplissage aux vrais jetons. L’option « les deux » ne change donc que les lignes de remplissage.

Les masques déterminent le type de modèle. Un modèle qui lit un texte entier à la fois, avec seulement un masque de remplissage, est un encodeur. Un modèle avec un masque causal, où chaque jeton ne lit que le passé, est un décodeur. Il peut écrire un texte un jeton à la fois.

Encodeur (de type BERT) Décodeur (de type GPT)
Chaque jeton lit tous les jetons, avant et après lui-même et les jetons précédents
Masques remplissage causal (et remplissage dans les lots)
Utile pour lire : classer, chercher, trouver des noms écrire : continuer un texte, répondre, traduire
Dans ce cours TinyTransformer (nn.TransformerEncoder) SmolLM2-135M au module 7

BERT (2018) est l’encodeur classique, et les modèles GPT sont les décodeurs classiques. Pour acheminer les billets, un encodeur convient : le billet entier est disponible. « Not » (pas) à la fin peut changer le sens de « delivery » (livraison) au début. Pour écrire une réponse, un modèle doit fonctionner comme un décodeur, car le prochain mot de la réponse n’existe pas encore.

  • Les positions sont nécessaires dès que l’ordre peut changer le sens. Une table apprise est simple, mais s’arrête à sa longueur maximale; celle de Larkfield s’arrête à 120 jetons. Les positions rotatives gèrent mieux les textes longs.
  • Le masque de remplissage est nécessaire dès qu’un lot contient des textes de longueurs différentes. C’est presque toujours le cas. Son calcul ne coûte rien; l’oublier change les réponses sans provoquer d’erreur.
  • Le masque causal est nécessaire dans tout modèle qui apprend à prédire le prochain jeton. Il limite aussi le modèle : un jeton ne peut jamais utiliser ce qui suit, même si le texte entier est disponible.
  1. Ouvrez le carnet dans l’encadré en haut de cette page et exécutez les cellules de préparation. La cellule d’entraînement termine après 8 époques avec une exactitude de validation d’environ 92 %.
  2. Dans la section 1, exécutez le test de mélange avec une couche d’attention. Vous voyez True : le même résumé pour les deux phrases.
  3. Ajoutez la table de positions et exécutez le test à nouveau. Vous voyez False (faux).
  4. Dans la section 2, mettez à zéro la table de positions du modèle entraîné. Comparez les scores de T-64697 dans l’ordre et mélangé. Vous voyez les mêmes 5 scores deux fois.
  5. Dans la section 3, exécutez le billet court seul, avec remplissage et masque, puis avec remplissage sans masque. Les deux premières lignes sont égales.
  6. Dans la cellule Your turn (à vous de jouer), créez causal pour 6 jetons et comptez ses cellules masquées dans hidden_cells. Exécutez la cellule de vérification. Elle affiche Check passed. (vérification réussie).
Afficher un exemple de réponse à l’étape 6
causal = nn.Transformer.generate_square_subsequent_mask(6)
hidden_cells = int(torch.isinf(causal).sum())
print(hidden_cells)
15

torch.isinf() vaut True lorsqu’une valeur est infinie. Le triangle supérieur d’un tableau de 6 × 6 contient 5 + 4 + 3 + 2 + 1 = 15 cellules.

Changez une condition : un texte de 4 jetons. Avant de l’exécuter, dessinez le masque causal sur papier et comptez ses cellules masquées.

Afficher la réponse
four = nn.Transformer.generate_square_subsequent_mask(4)
print(four)
print("hidden cells:", int(torch.isinf(four).sum()))
tensor([[0., -inf, -inf, -inf],
[0., 0., -inf, -inf],
[0., 0., 0., -inf],
[0., 0., 0., 0.]])
hidden cells: 6

3 + 2 + 1 = 6 cellules, le triangle au-dessus de la diagonale. Pour n jetons, le masque causal cache n × (n − 1) ÷ 2 cellules, soit presque la moitié du tableau.

Erreur courante

Un masque de remplissage inversé

Symptôme : Tomás écrit le masque comme « les jetons à garder », ids != PAD. Rien ne s’arrête avec une erreur, mais l’exactitude de validation baisse. Certains billets obtiennent nan (pas un nombre) comme scores :

ids_valid = token_ids(valid_rows, vocab)
y_valid = labels(valid_rows)
def scores_with(mask):
with torch.no_grad():
h = model.embed(ids_valid) + model.position(torch.arange(ids_valid.shape[1]))
h = model.blocks(h, src_key_padding_mask=mask)
keep = (ids_valid != PAD).unsqueeze(-1).float()
return model.out((model.norm(h) * keep).sum(1) / keep.sum(1))
wrong = scores_with(ids_valid != PAD) # True on the real tokens: they are hidden
right = scores_with(ids_valid == PAD) # True on the padding: it is hidden
print("wrong mask: accuracy", round((wrong.argmax(1) == y_valid).float().mean().item(), 4),
"| tickets with nan scores:", int(torch.isnan(wrong).any(1).sum()))
print("right mask: accuracy", round((right.argmax(1) == y_valid).float().mean().item(), 4))
first = int(torch.isnan(wrong).any(1).nonzero()[0])
print(valid_rows[first]["ticket_id"], len(vocab.encode(valid_rows[first]["text"])), "tokens:", wrong[first])
wrong mask: accuracy 0.155 | tickets with nan scores: 23
right mask: accuracy 0.9433
T-64707 120 tokens: tensor([nan, nan, nan, nan, nan])

Cause : dans PyTorch, True dans un masque signifie masquer. Le masque de Tomás cache les vrais mots, donc chaque billet ne lit que son remplissage : 15,5 %, pire que le hasard. Un billet de 120 jetons n’a aucun remplissage, donc toutes les clés sont masquées. Softmax divise alors 0 par 0, ce qui donne nan.

Solution : écrivez le masque comme les jetons à cacher, ids == PAD, comme le fait ticketnet.py. Testez-le avec un billet seul et le même billet avec remplissage : les scores doivent être égaux.

Ce que cela signifie pour un vrai système d’IA : chaque bibliothèque a sa propre règle pour les masques. Dans transformers de Hugging Face, attention_mask fonctionne à l’inverse : 1 signifie garder. Lisez la documentation de chaque masque. Gardez un test qui compare un billet seul au même billet dans un lot avec remplissage.

Vérifiez votre compréhension

Sans vecteurs de position, pourquoi l'attention donne-t-elle le même résumé pour deux phrases contenant les mêmes mots dans un ordre différent? Que changent les vecteurs de position?

Afficher la réponse

L’attention compare la requête de chaque jeton à chaque clé et fait la moyenne des valeurs. Rien dans ce calcul ne dépend de la position d’un jeton. Chaque mot reçoit donc la même sortie dans les deux phrases, mais à une autre position. La moyenne des jetons est alors exactement la même (True, ou vrai, dans la leçon). Le tout petit transformeur entraîné, avec sa table de positions mise à zéro, a donné des scores identiques pour un billet et ses mots mélangés. Les vecteurs de position ajoutent un motif appris différent pour chaque position. Le même mot à deux positions devient donc deux entrées différentes, et l’ordre peut influencer le résultat.

Vérifiez votre compréhension

Faites une prédiction : un court billet reçoit 107 jetons de remplissage dans un lot, sans masque de remplissage. Sa réponse peut-elle changer? Que fait le masque? Quelle est la forme de src_key_padding_mask pour un lot de 2 billets de 120 jetons?

Afficher la réponse

Oui. Sans masque, l’attention lit les jetons de remplissage comme des mots, et ils changent la sortie de chaque vrai jeton. Dans la leçon, le court billet de livraison T-64715 était classé dans la catégorie compte (8,922) avec remplissage, sans masque. Seul, il était classé dans la catégorie livraison (5,223). Le masque de remplissage vaut True (vrai) aux positions des jetons de remplissage. Aucun jeton ne leur donne donc de poids. Avec le masque, le billet rempli reçoit exactement les mêmes scores que lorsqu’il est seul. Le masque contient une valeur pour chaque jeton de chaque billet : sa forme est (2, 120).

Vérifiez votre compréhension

Larkfield veut deux modèles : l'un dirige un billet complet, l'autre écrit les premiers mots d'une réponse. Lequel devrait être un encodeur et lequel un décodeur? De quels masques chacun a-t-il besoin?

Afficher la réponse

Le modèle qui dirige les billets devrait être un encodeur, comme TinyTransformer. Le billet entier est disponible. Chaque jeton peut donc lire tous les autres jetons, avant et après lui. Il a seulement besoin du masque de remplissage. Le modèle qui rédige les réponses doit être un décodeur. Il écrit un jeton à la fois, et chaque jeton peut dépendre seulement des jetons qui le précèdent. Il a besoin du masque causal, ainsi que du masque de remplissage lorsqu’il s’entraîne sur des lots avec remplissage.

Récapitulatif

  • L’attention seule ignore l’ordre des mots : des mots mélangés donnent une sortie mélangée et le même résumé.
  • Un plongement de position ajoute un vecteur appris pour chaque position. TinyTransformer en a 120; les positions sinusoïdales et rotatives sont d’autres méthodes.
  • Le masque de remplissage cache les jetons de remplissage. Sans lui, la réponse pour un court billet peut changer selon son lot.
  • Le masque causal cache tous les jetons suivants, soit le triangle au-dessus de la diagonale. Le modèle ne peut donc pas lire la réponse qu’il doit prédire.
  • Un encodeur (de type BERT) lit tout; un décodeur (de type GPT) lit seulement le passé et peut écrire du texte.

À suivre : comment un décodeur apprend à partir de texte brut sans étiquettes, et pourquoi Larkfield ne peut pas le faire à grande échelle.

Leçon suivante : Les objectifs d'entraînement