Accueil / Articles

Entraînement d’un petit GPT sur des contes populaires indiens : tokeniseur, masque et perte.

Un GPT à 9 millions de paramètres créé de zéro : pourquoi le tokeniseur de GPT-2 échoue avec les mots des contes populaires, comment fonctionnent le BPE et la masque causale, et comment un seul lot génère 4 096 pertes de token suivant.

Éditeur : REACTAPP.TOP16 min de lecture3644 mots

Sources: Building LLM, Part 1 — Training: Predicting the Next Token

Un petit GPT entraîné sur des contes populaires indiens

Tout ceux qui ont déjà développé un Vision Transformer connaissent approximativement 70 pour cent des composants d’un modèle de langage. Les embeddings, l’attention, la pile résiduelle ainsi que le cycle d’entraînement sont identiques dans les deux cas. Ce qui reste, c’est la différence spécifique, et cette présentation l’explique en construisant un petit GPT à partir de principes fondamentaux et en l’entraînant depuis zéro sur la GPU d’un ordinateur portable. Le corpus utilisé est constitué de contes populaires indiens : des histoires Jataka, des fables de style Panchatantra ainsi que des récits du Pendjab et du Bengale.

Déjà au début apparaît un véritable problème. Le tokeniseur de base de GPT-2 divise des mots tels que Brahman et Rajah en fragments, car il ne les a jamais rencontrés ; c’est pourquoi un tokeniseur est entraîné à partir du corpus. En fin de compte, on obtient un modèle de 9 millions de paramètres qui est passé du bruit aléatoire à des poids entraînés, et n’importe qui peut reproduire ces résultats en exécutant une séance d’entraînement d’environ dix minutes.

Pourquoi la vision a précédé

Considérer les images avant le texte peut sembler être dans le mauvais ordre, puisque les chatbots travaillent avec des mots. Pourtant, un transformateur est indifférent au fait que son entrée soit des fragments d’images ou des parties de phrases. Les mécanismes d’attention, LayerNorm, les connexions sautées, l’MLP et le cycle d’optimisation ne changent pas. La meilleure question est de savoir où ce moteur commun est le plus facile à apprendre, et les images l’emportent pour quatre raisons.

  • Un token n’est rien d’autre qu’un patch d’image, une grille fixe découpée dans l’image ; il n’y a donc ni vocabulaire, ni outil de tokenisation, ni problème de mots inconnus à résoudre en premier.
  • Il n’y a pas de masque causal. Un ViT perçoit l’image entière en même temps, ce qui fait que la simple attention prend le pas sur la règle supplémentaire du langage interdisant de regarder à l’avance.
  • Le comportement est visible : les patches, les cartes d’attention et les régions sur lesquelles le modèle se concentre peuvent tous être représentés graphiquement, et les pixels semblent plus tangibles que le texte.
  • Il s’entraîne à partir de zéro sur une seule GPU en quelques minutes (sur CIFAR-10) ; ainsi, chaque chiffre correspond à une valeur que vous avez générée vous-même et non à quelque chose de pris pour acquis.

En résumé, le transformateur complet existe déjà une fois que ViT a été construit, il est simplement adapté à la vision. Un modèle de langage ne fait qu’ajouter la manière dont le texte devient des tokens (une recherche plutôt qu’un calcul), la masque causale et la prédiction du token suivant. La partie difficile et abstraite, le langage lui-même, devient bien plus facile à comprendre une fois que la machine en a pris connaissance.

L’entraînement, c’est simple : deviner le token suivant

Pendant l’entraînement, tout ce que fait un modèle de langage se résume à lire les tokens déjà présents et à prédire celui qui vient ensuite.

La figure illustre la prédiction d’un token suivant à partir des tokens qui le précèdent. En lui fournissant l’incipit « Dans un certain village vivait un pauvre Brahmane », on obtient une probabilité pour chacun des tokens susceptibles de suivre. Un point, une virgule ou un mot comme qui ou nommé sont tous grammaticalement possibles. Personne n’a besoin d’annoter quoi que ce soit : la bonne réponse est simplement le token qui figurait ensuite dans le texte. C’est pourquoi du texte brut seul peut enseigner à un modèle de langue, et c’est lui qui constitue tout le signal d’apprentissage. Tous les autres éléments existent pour soutenir cette seule prédiction, et ViT en fournit déjà la plupart.

Dans le code, cette idée se résume en deux lignes : exécuter le modèle, puis normaliser ses scores pour obtenir une distribution des tokens possibles suivants.

logits, _ = model(ids)                     # ids = the tokens so far, shape (1, T)
probs = F.softmax(logits[0, -1], dim=-1)   # one probability per vocabulary token

Le reste explique comment le modèle est construit et entraîné afin que ces probabilités soient fiables.

Pourquoi le tokeniseur est important

Un ViT divise une image en patches, tandis qu’un modèle de langage divise le texte en tokens. Un patch est calculé à partir des valeurs des pixels, mais un token de texte est tiré d’un vocabulaire fixe. Quelqu’un doit d’abord créer ce vocabulaire, et c’est cette personne qui décide quels éléments le modèle trouve faciles à traiter.

La première tentative a réutilisé r50k_base, le tokeniseur prêt à l’emploi de GPT-2, dont le vocabulaire était basé sur des textes web occidentaux datant d’environ 2019. La figure suivante montre son effet sur les mots des contes.

Brahman se divise en Brah et man, Rajah en Raj et ah, et Panchatantra se brise en cinq parties. Aucun de ces termes n’est obscur. Rajah apparaît 709 fois dans le corpus, mais l’outil de tokenisation n’a jamais rencontré ces mots assez souvent dans ses propres données d’entraînement pour leur attribuer un token chacun. La fragmentation allonge les séquences et force le modèle à reconstituer Brahman à partir de Brah et de man, où man est également partagé avec woman et human. Avec seulement environ un million de tokens de données d’entraînement, c’est un véritable handicap.

Le remède consiste en un tokeniseur entraîné sur ces récits, utilisant l’encodage par paires de bytes (BPE). GPT emploie le même algorithme, mais adapté à un corpus mieux correspondant. Le matériel de base provient de douze collections de contes populaires indiens en domaine public provenant du Project Gutenberg, incluant deux livres de Jataka, deux volumes de contes de fées indiens, Old Deccan Days, Folk-Tales of Bengal, The Talking Thrush, Tales of the Punjab, Twenty-Two Goblins, deux collections de contes Santal ainsi que Tales of the Sun from Southern India, pour un total d’environ 809 000 mots.

Le schéma illustre les fusions de paires de bytes utilisées pour entraîner le tokeniseur. Le BPE commence avec les 256 valeurs de bytes brutes, de sorte qu’il n’y a rien d’inconnu, puis fusionne répétitivement la paire voisine la plus fréquente pour former un nouveau token. Après 7 935 fusions sur les récits, le vocabulaire compte 8 192 entrées et inclut des unités entières telles que Rajah, Brahman et jackal.

Une décision de conception mérite une explication claire : le tokeniseur de GPT-2 a été complètement remplacé, et non étendu.

Le graphique montre le vocabulaire personnalisé composé de 8 192 tokens. Ajouter des tokens aux 50 257 existants dans r50k constitue une approche sensée lorsque l’on souhaite conserver les capacités en langue anglaise d’un grand modèle préentraîné, comme le font des projets tels qu’OpenHathi. Entraîner à partir de zéro sur un corpus petit et spécialisé est une autre situation : le vocabulaire issu du Web occidental de r50k ne sert qu’à prendre de l’espace, et les contes n’en couvrent que 37 pour cent. Le nouveau vocabulaire de 8 000 tokens est plus petit, utilisé à 96,6 pour cent, et adapté aux contes, ce qui fait de son remplacement l’option la plus appropriée.

Entraîner le tokeniseur ne nécessite que quelques lignes de code avec la bibliothèque tokenizers d’HuggingFace. Le script lit chaque livre de texte brut, supprime les en-têtes et pieds de page de Project Gutenberg, puis entraîne un modèle BPE.

import glob
from tokenizers import Tokenizer, models, trainers, pre_tokenizers
from llm_transformer.prep_tales import strip_gutenberg, RAW_DIR

# read every raw .txt book and strip its Project Gutenberg header/footer,
# leaving just the story text - one big string per book
tales_texts = [strip_gutenberg(open(p, encoding="utf-8", errors="ignore").read())
               for p in sorted(glob.glob(f"{RAW_DIR}/*.txt"))]   # 12 books, ~809k words
tok = Tokenizer(models.BPE())
tok.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)   # start from bytes
trainer = trainers.BpeTrainer(vocab_size=8192, special_tokens=["<|endoftext|>"],
                              initial_alphabet=pre_tokenizers.ByteLevel.alphabet())
tok.train_from_iterator(tales_texts, trainer)   # learn the 7,935 merges
tok.save("tales_bpe.json")

Le fichier de sortie .json représente le tokeniseur final, une liste fixe de fusions. Il n’est pas entraîné à nouveau. Il est chargé et utilisé pour convertir du texte en identifiants, puis ces derniers en texte. Une seule appel à .encode() relie le texte brut au réseau neuronal.

tok = Tokenizer.from_file("tales_bpe.json")   # reload the trained tokenizer
ids = tok.encode("Once upon a time").ids      # -> [412, 987, 15, 733]   (integers)
idx = torch.tensor(ids).unsqueeze(0)          # (1, 4) — a batch of one sequence

vocab_size = tok.get_vocab_size()             # 8192 - sets the NUMBER OF ROWS below

Rien d’autre que ces entiers n’atteint le réseau, et les mots originaux n’y existent plus. Chaque identifiant n’est rien de plus qu’une position dans un tableau, et vocab_size définit le nombre d’entrées que la couche d’incorporation réserve.

Des identifiants aux vecteurs

Lorsque le texte est une liste d’identifiants, chacun d’eux se transforme en un vecteur. C’est là que l’LLM est plus simple que le ViT.

Le schéma relie un index de tokeniseur à un vecteur d’incorporation. Dans le ViT, une petite multiplication matricielle permet de calculer le token correspondant à chaque patch à partir des pixels. Dans un LLM, le token est un identifiant discret ; par conséquent, le vecteur n’est rien d’autre que le numéro de ligne correspondant dans un tableau appris, ce qui constitue une simple recherche. Le token 789 récupère la ligne 789, qui est un vecteur de 256 dimensions ; l’ajout d’une incorporation positionnelle, comme dans le ViT, termine la phase d’entrée.

Dans le code, une couche nn.Embedding représente ce tableau, et l’appel à cette couche effectue une recherche par indexation, et non par multiplication. Son attribut .weight est un tenseur apprenable de 8192 par 256 contenant 2,1 millions de poids qui sont entraînés via la rétropropagation ; une ligne donnée ne change que lors des étapes où son token apparaît.

self.token_embed = nn.Embedding(vocab_size, n_embed)   # the 8,192 x 256 table
# .weight IS a learnable (8192, 256) parameter tensor — 2.1M weights, trained by backprop;
# each id's row is nudged only on steps where that token appears in the batch
...
tok_emb = self.token_embed(idx)     # idx (B, T)  ->  vectors (B, T, 256), one row per id

Tout ce qui suit, à savoir l’attention, le flux résiduel et les blocs MLP, est identique à celui du Vision Transformer, avec une exception.

Le masque causal

Un ViT permet à chaque patch d’accorder de l’attention à tous les autres. Un modèle de langage ne peut pas faire cela, car sa fonction est de deviner ce qui vient ensuite. Si la position t voyait la position t+1, elle copierait simplement la réponse. La solution consiste à masquer l’attention afin que chaque token ne voie que lui-même et les tokens précédents.

Le masque représenté dans la figure est un triangle inférieur. Avant l’application de softmax, toutes les positions futures reçoivent une valeur négative infinie, que softmax transforme en un poids égal à zéro ; ainsi, les tokens masqués n’ajoutent rien. Ce triangle constitue la seule différence architecturale entre l’attention du ViT et celle des LLM. Les produits scalaires, le scaling par la racine carrée de la taille du bloc, softmax ainsi que la somme pondérée des valeurs restent tous identiques.

Dans le code, il s’agit d’une ligne supplémentaire par rapport à l’attention ordinaire. Une matrice triangulaire inférieure composée de 1 est créée une fois, les scores sont calculés comme d’habitude, et les positions zéroées sont remplies par l’infini négatif avant l’application de softmax.

# tril = torch.tril(torch.ones(T, T))  — a lower-triangular matrix of 1s, made once
attn = q @ k.transpose(-2, -1) / math.sqrt(head_size)          # scores, (B, T, T)
attn = attn.masked_fill(self.tril[:T, :T] == 0, float('-inf')) # blank out the future
attn = F.softmax(attn, dim=-1)                                  # -inf -> weight 0
out  = attn @ v

Une seule passe entraîne toutes les longueurs de contexte

Le masque à une ligne cache quelque chose qui mérite un examen plus approfondi. Les gens ont tendance à imaginer qu’un modèle de langage fonctionne comme s’il générait du texte : en ajoutant un token à un contexte qui s’agrandit et en recalculant l’attention à chaque étape. C’est vrai lors de l’inference, mais pas pendant l’entraînement. Lors de l’entraînement, la phrase est entièrement connue, donc Q, K et V pour tous les tokens T sont calculés ensemble, formant ainsi toute la matrice d’attention T par T en une seule étape.

Cette unique passe n’est valide que grâce au masque causal. Considérez la ligne t de la matrice masquée : toutes les entrées situées à droite de la diagonale sont égales à l’infini négatif, ce qui les fait devenir zéro après application de softmax. Le calcul out = attn @ V permet alors à la ligne t d’obtenir une somme pondérée uniquement des premiers t vecteurs de valeurs. Chaque longueur de contexte est donc traitée en même temps.

Le graphique montre différentes itérations ainsi que la forme du masque pendant l’entraînement. Lisez-le ligne par ligne :

  • La ligne 1 ne prend en compte que le token 1, son output prédit donc le token 2 à partir d’un contexte de un seul token.
  • La ligne 3 prend en compte les tokens 1 à 3 et prédit le token 4 à partir de trois tokens du contexte.
  • La ligne 6 prend en compte les six tokens et prédit le token 7 à partir du contexte complet.

Voici un autre point de vue. Le contexte visible de chaque ligne forme une matrice triangulaire inférieure qui s’agrandit d’une unité à chaque fois : 1 par 1 pour le premier token, puis 2 par 2 et 3 par 3. Il s’agit des blocs imbriqués en haut à gauche de la grande matrice, dessinés en orange ; ainsi, une seule passe de 6 par 6 contient toutes les séquences plus courtes, et la bande en bas les déroule. Toutes sont générées à l’intérieur d’un seul attn @ V, sans utilisation de boucles sur les positions en Python. Cela explique l’efficacité d’entraînement des transformateurs : une séquence comportant T tokens correspond à T exemples d’entraînement, avec des contextes allant de la longueur 1 à T, évalués en parallèle. La même masque triangulaire qui fournit ces exemples empêche également toute position de regarder à l’avant pendant la génération, qui se fait ligne par ligne et reste économique grâce au stockage en mémoire des clés et valeurs précédentes (le KV-cache).

Nombres réels grâce à l’attention

Jusqu’à présent, chaque case ne signifiait que « assister » ou « ne pas assister ». En affichant les valeurs multiples, on obtient les chiffres réels pour ces mêmes trois exécutions, tirés du bloc 0, tête 0 du modèle entraîné sur la phrase « Once there lived a clever jackal ».

Le graphique présente les calculs de Q, K et V au cours des différentes exécutions. Au sein d’une même ligne, les poids d’attention sont positifs et s’additionnent à 1 ; par conséquent, la sortie représente un mélange convexe des vecteurs de valeurs visibles, ce qui correspond à une moyenne pondérée.

  • L’exécution 1 présente un seul token avec un poids de 1,00, donc out1 = V1. La première position copie sa propre valeur, puisqu’il n’y a rien à mixer pour l’instant.
  • Dans l’exécution 3, la sortie mélange trois valeurs selon des poids d’environ 0,20 pour le premier token, 0,06 pour le deuxième et 0,74 pour le troisième. Elle s’appuie principalement sur son propre token, lived, mais intègre également un cinquième de Once ; il s’agit donc d’un véritable mélange.
  • Dans la 6e itération, le poids est réparti sur les six valeurs, avec 0,26 pour chacun de there et jackal. Ce vecteur mixte à 32 dimensions résume l’ensemble du préfixe correspondant à la tête, et les couches suivantes le transforment en une prédiction.
  • Cela couvre tout ce que fait l’attention : les scores provenant des requêtes et des clés déterminent les poids, et ces derniers mélangent les valeurs. Le masquage garantit que ce mélange ne dépasse jamais le token actuel, de sorte que chaque ligne constitue un résumé équitable de tout ce qui a été lu jusqu’à présent. Les têtes réelles disposent de 32 dimensions de valeurs, et le modèle compte huit têtes dans chacune des six couches. Les bandes affichent les 8 premières dimensions d’une seule tête, bien que le même mécanisme s’applique partout ailleurs.

    Predictions T, pertes T

    La prédiction du token suivant apporte un avantage que le ViT ne possédait pas. Le ViT effectuait une seule prédiction par image, à savoir une étiquette de classe. La masque causale permet à un modèle de langage de deviner le token suivant en même temps pour toutes les positions, de sorte qu’une séquence fournit autant de signaux d’apprentissage que de tokens qu’elle contient.

    Le graphique présente la moyenne des prédictions T et des valeurs de perte pour une entrée de T tokens à chaque exécution. Chaque position reçoit une perte de croisement d’entropie, qui mesure le degré de surprise du modèle face au vrai token suivant. Certaines prédictions sont simples, comme a après lived, avec une valeur de seulement 0,58. D’autres sont complexes, comme l’adjectif clever avec 7,17, car de nombreux mots auraient pu convenir. La moyenne des six valeurs donne 3,59, et la rétropropagation fait baisser ce chiffre en utilisant le même mécanisme de règle de chaîne que le ViT.

    Dans le code, toutes les pertes T proviennent d’une seule appel à cross_entropy. La grille de positions (B, T) est aplatie en une seule liste longue, et chaque élément est évalué par rapport à son token suivant réel.

    logits = self.lm_head(x)                       # (B, T, vocab)
    B, T, C = logits.shape
    loss = F.cross_entropy(logits.reshape(B*T, C), # every position ...
                           targets.reshape(B*T))   # ... vs the token that actually followed
    

    Chaque perte correspond à une classification de vocabulaire

    Il est important d’être précis quant à ce que compare cross_entropy, car tant la prédiction que la cible sont des vecteurs, et non des mots individuels. À chaque position, lm_head génère un vecteur de logits composé de 8 192 éléments, un score par token du vocabulaire, et softmax le transforme en une distribution de probabilités sur l’ensemble de ces tokens. La cible est un vecteur one-hot de même taille, contenant 1 pour le mot qui a réellement suivi et 0 partout ailleurs. Prédire le prochain token revient donc à résoudre un problème de classification où les mots du vocabulaire constituent les classes.

    Le graphique compare les logits d’un LLM et d’un ViT. Il met en évidence un point subtil : les barres sont classées par probabilité uniquement pour faciliter la lecture, tandis que le résultat réel est un vecteur à structure fixe, où la case i appartient toujours à l’identifiant de token i. Le mot « son » reste à l’index 498 lors de chaque itération, que ce soit pendant l’entraînement ou la déduction, et le softmax ne modifie que les hauteurs des cases, jamais leur ordre. Cela explique pourquoi le seul 1 de la cible one-hot se trouve à l’index 498 et non en première position : le tokeniseur a attribué à chaque token son identité une fois, en tant que position correspondante.

    C’est précisément la tête de classification de ViT, qui applique softmax aux scores de classe et les évalue par entropie croisée par rapport à une étiquette one-hot. Deux éléments diffèrent : le nombre de classes (8 192 tokens du vocabulaire contre environ 10 étiquettes d’image) et la fréquence (à toutes les positions T contre une fois par image). Face à une cible one-hot, l’entropie croisée se réduit également à moins du logarithme de la probabilité attribuée au mot correct, c’est pourquoi la perte de chaque position ressemble à un score de surprise. « son » à 9,8 % coûte 2,32, un mot que le modèle s’attendait fortement à voir coûter beaucoup moins, tandis qu’un véritable choc coûterait beaucoup plus.

    Comment est construit un lot

    La commande reshape(B*T) indique simplement qu’un lot contient B séquences, chacune contribuant des pertes pour T positions. D’où viennent donc ces B séquences, et ont-elles toutes la même longueur ?

    Les gens se heurtent souvent à un détail : les séquences de pré-entraînement ne sont pas complétées et ont toutes exactement la même longueur de contexte. Le corpus est regroupé en un seul flux de tokens prolongé, avec un token <|endoftext|> entre les documents, et des fenêtres de longueur fixe sont extraites à des positions aléatoires. Toutes ces fenêtres proviennent d’un flux continu, de sorte qu’aucune ne peut être courte, et aucun token de complétion n’est nécessaire.

    Le schéma suit le parcours allant du corpus d’entraînement vers les lots, puis vers la perte. Le seul token spécial dans ce flux est <|endoftext|> (EOT), qui sert de séparateur plutôt que de remplissage et indique où s’arrête un conte populaire et où en commence un autre. Une fenêtre peut traverser une telle frontière et contenir un EOT en cours de route, c’est ainsi que le modèle apprend qu’une histoire s’est terminée et qu’une nouvelle commence. Le remplissage apparaît plus tard, lors du fine-tuning, lorsque les exemples diffèrent en longueur et que les positions remplies sont masquées dans le calcul de la perte, ce qui constitue un mode différent de l’entraînement préalable avec remplissage.

    La perte représente le deuxième aspect où le LLM diffère du ViT, et cette différence est positive. Le ViT produisait une prédiction et une perte par image. Une séquence de 128 tokens génère donc 128 pertes, dont la moyenne constitue la perte de cet échantillon (3,78 pour l’échantillon 1 dans la figure) ; les 32 pertes d’échantillons d’un lot donnent en moyenne une perte de lot de 3,92 que la rétropropagation cherche à minimiser. Grâce à ces deux étapes de moyennage, une seule itération permet de réduire la moyenne de 32 fois 128, soit 4 096 pertes liées au token suivant, ce qui représente bien plus de signal par itération que une seule perte par image.

    Voir la perte diminuer

    Reliez cette perte à un optimiseur et répétez l’opération. C’est là que se déroule réellement l’entraînement sur le corpus des contes.

    Le graphique montre la perte de formation et la perte de validation sur des époches. La formation commence à 9,2, ce qui correspond à ln(8192), c’est-à-dire la perte liée à des devinements aléatoires sur un vocabulaire de 8 192 tokens, et la meilleure perte de validation atteint 4,73. Les courbes restent proches, séparées d’environ 1,0, ce qui indique que, grâce à un domaine restreint, le modèle généralise plutôt que de simplement mémoriser. Le modèle lui-même est de petite taille.

    Le graphique relie le processus d’apprentissage au nombre de poids apprenables. Avec 9,0 millions de paramètres, le modèle s’entraîne en environ dix minutes sur une GPU de laptop de 6 GB. Un peu plus de la moitié se trouve dans les six blocs transformateurs, et le reste appartient à la table d’incorporation et au module de sortie. L’entraînement consiste uniquement à ajuster répétitivement ces 9 millions de valeurs, de sorte que les probabilités du token suivant accordent plus d’importance au token qui a réellement suivi.

    Dans le code, la boucle est celle à laquelle on s’attend, et elle a la même structure que celle d’un ViT : on sélectionne aléatoirement des contextes et des fenêtres de tokens suivants, on exécute le passage en avant pour obtenir une perte moyenne, on met les gradients à zéro, on propage la dérivée en arrière et on fait un pas.

    for step in range(train_steps):
        xb, yb = get_batch(train_data)     # random (context, next-token) windows
        _, loss = model(xb, yb)            # forward: T predictions -> one mean loss
        opt.zero_grad(); loss.backward()   # backprop the loss into all 9M parameters
        opt.step()                         # nudge them downhill
    

    Le script complet ajoute un calendrier de taux d’apprentissage basé sur le cosinus ainsi que des mécanismes d’évaluation.

    Quoi de nouveau et quoi qui reste inchangé

    Par rapport au Vision Transformer, seules de petites modifications ont été apportées au LLM : une embedding de tokens fonctionnant comme un tableau de recherche, un masque causal, ainsi qu’une perte liée au token suivant à chaque position qui classe par rapport au vocabulaire complet. Une leçon pratique ressort également : le tokenizer fait partie intégrante du modèle, donc en utiliser un adapté au domaine en vaut la peine. L’attention, les embeddings positionnels, le flux résiduel, LayerNorm, les blocs MLP et le cycle complet de backpropagation et d’optimiseur sont conservés sans modification.

    Les nombres réels ont révélé le fonctionnement interne : comment le masque causal permet à une seule passe en avant d’évaluer l’ensemble des longueurs de contexte, ce que produit attn @ V (chaque ligne de sortie mélange les vecteurs de valeurs visibles pour ce token), comment les lots d’entraînement proviennent du regroupement plutôt que du remplissage, et comment la perte d’une position, une classification à l’intérieur du vocabulaire, se joint aux autres pour former une moyenne unique.

    Les poids sont prêts, mais un modèle entraîné ne fait rien tant qu’il n’est pas exécuté. Viennent ensuite les étapes d’inférence : la boucle de génération, le cache KV qui en accélère le fonctionnement, et ce que le petit modèle écrit lorsqu’on lui donne le début d’une histoire.