Introduction : Pipeline de préparation des données
Avant même de songer à l’architecture ou à l’entraînement d’un LLM, l’étape la plus cruciale est la préparation des données d’entraînement. C’est l’objectif de la toute première étape de la phase 1 du développement d’un modèle de langage.
Pour pouvoir traiter du texte, un LLM ne lit pas des phrases brutes, mais convertit le texte suivant un pipeline bien précis :
- La tokenisation : Le texte brut est découpé en petites unités appelées tokens (qui peuvent être des mots entiers, ou des sous-mots). Des méthodes avancées comme le Byte Pair Encoding (BPE) sont généralement utilisées sur les modèles récents comme GPT.
- L’échantillonnage : Grâce à une approche par « fenêtre glissante » (sliding window), on extrait des paires d’entrées et de sorties permettant d’entraîner le modèle à la tâche de prédiction du mot suivant.
- La vectorisation : Les tokens extraits sont ensuite convertis en vecteurs de nombres (embeddings) que le réseau de neurones va pouvoir ingérer et traiter mathématiquement.

Comprendre les embeddings de mots
Les réseaux de neurones ne peuvent pas traiter directement le texte brut, car les algorithmes nécessitent des valeurs numériques continues pour fonctionner. La méthode pour accomplir cela s’appelle l’embedding (ou plongement vectoriel).
Un embedding consiste à projeter des objets discrets (mots, phrases, audios, images) vers des points dans un format que la machine peut traiter : un espace vectoriel continu (une suite de nombres). Il est essentiel de comprendre qu’il existe un type de modèle d’embedding pour chaque type de format de données (on ne peut pas utiliser un modèle textuel sur de la vidéo).

Dans ce livre, nous nous intéressons uniquement aux embeddings de mots puisque la génération des LLM opère un mot à la fois. Historiquement, on utilisait des modèles tiers comme Word2Vec pour transformer chaque mot en vecteur. La logique mathématique est élégante : les mots partageant des contextes similaires acquièrent des valeurs mathématiques similaires et se retrouvent par conséquent regroupés (clustered) géométriquement lorsqu’on les affiche dans un espace en deux dimensions.

Cependant, les LLM modernes ne s’appuient pas sur Word2Vec. Ils utilisent leur propre couche d’embedding intégrée, qui s’entraîne et s’optimise en même temps que le reste du modèle, permettant d’adapter les vecteurs aux spécificités exactes des données cibles.
Enfin, la dimensionnalité de ces vecteurs est clé : un espace à deux dimensions est utile pour enseigner et visualiser (comme la figure 2.3), mais dans un modèle réel, le nombre de dimensions chiffre très vite pour capturer toute l’expressivité et la nuance de la langue. Plus il y a de dimensions, plus la précision augmente, au détriment de l’efficacité calculatoire. Un petit GPT-2 utilise 768 dimensions pour un seul mot, alors que l’immense GPT-3 en requiert 12 288 !
La tokenization du texte (Tokenizing text)
La tokenization (ou segmentation en lexèmes) est une étape de prétraitement indispensable avant la création d’embeddings pour un LLM. Elle consiste à diviser le texte d’entrée en tokens individuels, qui peuvent être des mots isolés ou des caractères spéciaux, y compris la ponctuation.

Dans cette section, nous utiliserons la courte nouvelle “The Verdict” d’Edith Wharton, disponible dans le domaine public.
Vous pouvez télécharger et lire ce texte avec le code Python suivant :
import urllib.request
url = ("https://raw.githubusercontent.com/rasbt/"
"LLMs-from-scratch/main/ch02/01_main-chapter-code/"
"the-verdict.txt")
file_path = "the-verdict.txt"
urllib.request.urlretrieve(url, file_path)
with open("the-verdict.txt", "r", encoding="utf-8") as f:
raw_text = f.read()
print("Total number of character:", len(raw_text))
print(raw_text[:99])
Résultat :
Total number of character: 20479
I HAD always thought Jack Gisburn rather a cheap genius--though a good fellow
enough--so it was no
Bien que l’entraînement de vrais LLMs implique souvent des millions d’articles (des gigaoctets de texte), nous utiliserons cet échantillon de 20 479 caractères à des fins éducatives pour pouvoir exécuter le code en temps raisonnable sur du matériel grand public.
Création d’un tokenizer de base avec Python
Afin de diviser le texte et d’obtenir une liste de tokens, nous faisons une courte incursion dans la bibliothèque d’expressions régulières de Python (re).
Nous évitons de convertir tout le texte en minuscules, car la capitalisation aide les LLMs à :
- Distinguer les noms propres des noms communs.
- Comprendre la structure des phrases.
- Apprendre à générer du texte avec une capitalisation correcte.
Voici une première tentative de séparation basée uniquement sur les espaces :
import re
text = "Hello, world. This, is a test."
result = re.split(r'(\s)', text)
print(result)
Résultat :
['Hello,', ' ', 'world.', ' ', 'This,', ' ', 'is', ' ', 'a', ' ', 'test.']
Ce schéma fonctionne en grande partie, mais la ponctuation reste collée aux mots ("Hello,"). Pour corriger cela, séparons également sur les virgules et les points (r'([,.]|\s)') :
result = re.split(r'([,.]|\s)', text)
print(result)
Résultat :
['Hello', ',', '', ' ', 'world', '.', '', ' ', 'This', ',', '', ' ', 'is', ' ', 'a', ' ', 'test', '.', '']
Un petit problème subsiste : la liste inclut encore les espaces et des chaînes vides. Nous pouvons supprimer ces caractères superflus avec .strip() :
result = [item for item in result if item.strip()]
print(result)
Résultat :
['Hello', ',', 'world', '.', 'This', ',', 'is', 'a', 'test', '.']
Note sur les espaces : Lors du développement d’un tokenizer simple, le fait d’encoder les espaces comme des caractères séparés ou de les supprimer (via
.strip()) dépend de votre application. Leurs suppressions réduisent les besoins en mémoire et calcul. Cependant, conserver les espaces est utile pour les modèles sensibles à la structure exacte du texte (comme le code Python, sensible à l’indentation). Ici, nous les supprimons pour simplifier.
Complexifions l’expression régulière pour gérer d’autres signes de ponctuation et les double-tirets, similaires à ceux rencontrés dans “The Verdict” :
text = "Hello, world. Is this-- a test?"
result = re.split(r'([,.:;?_!"()\']|--|\s)', text)
result = [item.strip() for item in result if item.strip()]
print(result)
Résultat :
['Hello', ',', 'world', '.', 'Is', 'this', '--', 'a', 'test', '?']

Maintenant que nous avons un tokenizer de base fonctionnel, appliquons-le à l’entièreté de la nouvelle d’Edith Wharton :
preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', raw_text)
preprocessed = [item.strip() for item in preprocessed if item.strip()]
print(len(preprocessed))
print(preprocessed[:30])
Résultat :
4690
['I', 'HAD', 'always', 'thought', 'Jack', 'Gisburn', 'rather', 'a', 'cheap', 'genius', '--', 'though', 'a', 'good', 'fellow', 'enough', '--', 'so', 'it', 'was', 'no', 'great', 'surprise', 'to', 'me', 'to', 'hear', 'that', ',', 'in']
Convertir les tokens en identifiants (Token IDs)
Une fois le texte divisé en tokens (chaînes de caractères), l’étape suivante consiste à les convertir en nombres entiers (Token IDs). C’est une étape intermédiaire obligatoire avant de générer les vecteurs d’embeddings.
Pour transformer les tokens en identifiants, nous devons d’abord construire un vocabulaire. Ce vocabulaire mappe chaque mot et caractère spécial unique à un nombre entier de façon unique.

Créons la liste de tous les tokens uniques et trions-les pour déterminer la taille de notre vocabulaire :
all_words = sorted(set(preprocessed))
vocab_size = len(all_words)
print(vocab_size)
Résultat :
1130
Le vocabulaire contient 1 130 tokens différents. Nous pouvons maintenant créer le dictionnaire qui associe chaque token à un numéro :
vocab = {token:integer for integer,token in enumerate(all_words)}
for i, item in enumerate(vocab.items()):
print(item)
if i >= 50:
break
Résultat :
('!', 0)
('"', 1)
("'", 2)
...
('Her', 49)
('Hermia', 50)

Implémentation d’une classe Tokenizer
Pour automatiser ce processus, nous allons implémenter une classe Python SimpleTokenizerV1. Elle comprendra :
- Une méthode
encode()pour diviser un texte en tokens et le transformer en IDs. - Une méthode
decode()pour effectuer l’opération inverse (de Token IDs vers textes), indispensable pour lire la sortie générée par le modèle.
class SimpleTokenizerV1:
def __init__(self, vocab):
self.str_to_int = vocab # Stocke le vocabulaire pour l'encodage
self.int_to_str = {i:s for s,i in vocab.items()} # Vocabulaire inversé pour le décodage
def encode(self, text):
preprocessed = re.split(r'([,.?_!"()\']|--|\s)', text)
preprocessed = [item.strip() for item in preprocessed if item.strip()]
ids = [self.str_to_int[s] for s in preprocessed]
return ids
def decode(self, ids):
text = " ".join([self.int_to_str[i] for i in ids])
# Supprime les espaces insérés avant la ponctuation
text = re.sub(r'\s+([,.?!"()\'])', r'\1', text)
return text

Testons notre classe sur un extrait de la nouvelle :
tokenizer = SimpleTokenizerV1(vocab)
text = """"It's the last he painted, you know,"
Mrs. Gisburn said with pardonable pride."""
ids = tokenizer.encode(text)
print(ids)
Résultat :
[1, 56, 2, 850, 988, 602, 533, 746, 5, 1126, 596, 5, 1, 67, 7, 38, 851, 1108, 754, 793, 7]
Décodons maintenant cette liste d’IDs pour vérifier si nous retrouvons la phrase originale :
print(tokenizer.decode(ids))
Résultat :
'" It\' s the last he painted, you know," Mrs. Gisburn said with pardonable pride.'
Le décodeur fonctionne bien ! Essayons maintenant un nouveau texte qui n’est pas issu de la nouvelle d’Edith Wharton :
text = "Hello, do you like tea?"
print(tokenizer.encode(text))
Résultat :
KeyError: 'Hello'
Problème : Le mot “Hello” n’apparaît pas dans la nouvelle “The Verdict”, il est donc absent de notre vocabulaire. Cela démontre pourquoi les LLMs doivent être entraînés sur de gigantesques ensembles de données diversifiés pour étendre leur vocabulaire (et pourquoi nous aurons besoin de tokens spéciaux pour gérer les mots inconnus).
Ajouter des tokens de contexte spéciaux (Adding special context tokens)
Il est indispensable de modifier le tokenizer pour gérer les mots inconnus. De plus, l’ajout de tokens de contexte spéciaux permet d’améliorer la compréhension du modèle, par exemple pour marquer la fin ou le début d’un document. Nous allons ajouter deux nouveaux tokens : <|unk|> pour les mots inconnus, et <|endoftext|> pour séparer des documents textuels indépendants.

<|unk|> (pour les mots inconnus) et <|endoftext|> (pour séparer deux sources de texte non liées) au vocabulaire.L’ajout du token <|endoftext|> est crucial lorsqu’on entraîne des LLMs de type GPT sur de multiples documents ou livres indépendants. Cela aide le modèle à comprendre que, bien que concaténés à la chaîne pour l’entraînement, ces textes n’ont aucun lien contextuel entre eux.

<|endoftext|> agit comme un marqueur signalant le début ou la fin d’un segment.Mise à jour du vocabulaire
Ajoutons ces deux tokens spéciaux à la suite de notre liste de mots uniques, puis vérifions la nouvelle taille du vocabulaire :
all_tokens = sorted(list(set(preprocessed)))
all_tokens.extend(["<|endoftext|>", "<|unk|>"])
vocab = {token:integer for integer,token in enumerate(all_tokens)}
print(len(vocab.items()))
Résultat :
1132
Le vocabulaire compte bien à présent 1 132 valeurs (au lieu de 1 130). Imprimons les cinq dernières entrées du dictionnaire pour le confirmer :
for i, item in enumerate(list(vocab.items())[-5:]):
print(item)
Résultat :
('younger', 1127)
('your', 1128)
('yourself', 1129)
('<|endoftext|>', 1130)
('<|unk|>', 1131)
Le Tokenizer V2 gérant les mots inconnus
Nous pouvons ajuster la méthode encode de notre classe précédente. Désormais, si un mot rencontré dans le texte fourni n’est pas présent dans la base de données de notre self.str_to_int, nous lui associons d’office le token <|unk|>.
class SimpleTokenizerV2:
def __init__(self, vocab):
self.str_to_int = vocab
self.int_to_str = { i:s for s,i in vocab.items()}
def encode(self, text):
preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', text)
preprocessed = [item.strip() for item in preprocessed if item.strip()]
# Filtre de sécurité pour les mots inconnus
preprocessed = [item if item in self.str_to_int else "<|unk|>" for item in preprocessed]
ids = [self.str_to_int[s] for s in preprocessed]
return ids
def decode(self, ids):
text = " ".join([self.int_to_str[i] for i in ids])
text = re.sub(r'\s+([,.:;?!"()\'])', r'\1', text)
return text
Testons cette nouvelle version sur un échantillon composé de deux phrases indépendantes concaténées avec notre fameux marqueur, pour vérifier la mise en place du texte de test :
text1 = "Hello, do you like tea?"
text2 = "In the sunlit terraces of the palace."
text = " <|endoftext|> ".join((text1, text2))
print(text)
Résultat :
Hello, do you like tea? <|endoftext|> In the sunlit terraces of the palace.
Maintenant, encodons ce texte complet et regardons les identifiants générés :
tokenizer = SimpleTokenizerV2(vocab)
print(tokenizer.encode(text))
Résultat :
[1131, 5, 355, 1126, 628, 975, 10, 1130, 55, 988, 956, 984, 722, 988, 1131, 7]
(On observe bien la présence du token 1130 correspondant au <|endoftext|> et deux compteurs 1131 pour <|unk|> correspondant aux mots hors-vocabulaire).
Décryptons les identités pour le voir de nos propres yeux :
print(tokenizer.decode(tokenizer.encode(text)))
Résultat :
<|unk|>, do you like tea? <|endoftext|> In the sunlit terraces of the <|unk|>.
En comparant ce texte détokenisé avec le texte d’entrée original, nous pouvons en déduire que le dataset d’entraînement, l’histoire courte d’Edith Wharton “The Verdict”, ne contient pas les mots “Hello” et “palace”. Ces mots ont donc été remplacés par <|unk|>.
Autres types de tokens contextuels
Selon le LLM, certains chercheurs considèrent également d’autres tokens spéciaux supplémentaires tels que :
[BOS](Beginning of sequence) : Ce token marque le début d’un texte. Il indique au LLM où commence un fragment de contenu.[EOS](End of sequence) : Ce token est positionné à la fin d’un texte et est particulièrement utile lorsque l’on concatène plusieurs textes non liés, de manière similaire à<|endoftext|>. Par exemple, lors de la combinaison de deux articles Wikipédia ou livres différents, le token[EOS]indique où l’un se termine et où le suivant commence.[PAD](Padding) : Lors de l’entraînement de LLMs avec des tailles de lots (batch sizes) supérieures à un, le batch peut contenir des textes de longueurs variables. Pour s’assurer que tous les textes ont la même longueur, les textes plus courts sont prolongés ou “rembourrés” (padded) en utilisant le token[PAD], jusqu’à atteindre la longueur du texte le plus long du lot.
La particularité du tokenizer des modèles GPT
Le tokenizer des modèles GPT se distingue par sa simplicité. Au lieu de multiplier les tokens spéciaux, il n’utilise que
<|endoftext|>comme équivalent à[BOS]et[EOS].Ce token
<|endoftext|>sert également pour le padding. Lors de l’entraînement par lots (batchs), un masque (mask) est appliqué pour que le modèle ignore simplement ces tokens de remplissage. Le choix du token spécifique pour le padding n’a donc aucune importance.Enfin, ce tokenizer n’utilise pas de token
<|unk|>pour les mots hors-vocabulaire (out-of-vocabulary). Il utilise plutôt un algorithme appelé encodage par paire d’octets (Byte Pair Encoding ouBPE), qui décompose les mots inconnus en sous-mots (subword units), comme nous le verrons dans la section suivante.
L’Encodage par paire d’octets (Byte Pair Encoding - BPE)
Examinons un schéma de tokenisation plus sophistiqué basé sur un concept appelé encodage par paire d’octets (Byte Pair Encoding - BPE). Le tokenizer BPE a été utilisé pour entraîner des LLMs tels que GPT-2, GPT-3 et le modèle original utilisé dans ChatGPT.
Ici, nous allons utiliser une bibliothèque open-source Python existante appelée tiktoken (https://github.com/openai/tiktoken), qui implémente l’algorithme BPE de manière très. De même que pour d’autres bibliothèques Python, nous pouvons installer la bibliothèque tiktoken via l’installateur de paquets pip depuis le terminal :
pip install tiktoken
Vérification de la version :
from importlib.metadata import version
import tiktoken
print("tiktoken version:", version("tiktoken"))
Résultat :
tiktoken version: 0.7.0
Initialisation d’un tokenizer BPE (modèle “gpt2”) :
tokenizer = tiktoken.get_encoding("gpt2")
Encodage d’un texte (y compris les tokens de contrôle contextuel) :
text = (
"Hello, do you like tea? <|endoftext|> In the sunlit terraces"
"of someunknownPlace."
)
# Le paramètre allowed_special évite que le tokenizer lève une erreur en voyant le token <|endoftext|>
integers = tokenizer.encode(text, allowed_special={"<|endoftext|>"})
print(integers)
Résultat :
[15496, 11, 466, 345, 588, 8887, 30, 220, 50256, 554, 262, 4252, 18250, 8812, 2114, 286, 617, 34680, 27271, 13]
Décodage inverse :
strings = tokenizer.decode(integers)
print(strings)
Résultat :
Hello, do you like tea? <|endoftext|> In the sunlit terraces of someunknownPlace.
Observations fondamentales sur le Tokenizer BPE
Cette expérimentation met en évidence deux faits remarquables inhérents au tokenizer BPE de la famille GPT :
- La position du token
<|endoftext|>: Son ID attribué est très grand (50256). C’est logique : le vocabulaire total des modèles type GPT-2 ou GPT-3 est limité à 50 257 tokens, avec<|endoftext|>occupant l’ultime position à la fin. - La gestion robuste des mots “hors-vocabulaire” (OOV) : Le tokenizer parvient à re-transformer et décoder parfaitement
someunknownPlacesans avoir à effectuer de “fallback” vers un token aveugle comme<|unk|>.
Comment le BPE se passe-t-il totalement du token <|unk|> ?
La force du BPE réside dans sa capacité à fractionner un mot totalement inconnu. Au lieu de lever une erreur ou de le remplacer brutalement par <|unk|>, le BPE segmente le mot en fragments plus petits : des syllabes / sous-mots (“subwords”) connus ou, s’il le faut en dernier recours, en caractères ou octets individuels.
Figure 2.11 : Les tokenizers BPE décomposent les mots inconnus en sous-mots et en caractères individuels. Ainsi, un tokenizer BPE peut analyser n’importe quel mot et n’a pas besoin de remplacer les mots inconnus par des tokens spéciaux, tels que
<|unk|>.
Cette capacité de décomposer les mots inconnus en caractères individuels garantit que le tokenizer, et par conséquent le LLM, peut traiter n’importe quel texte, même s’il contient des mots absents de ses données d’entraînement
Exercice 2.1 : Encodage par paire d’octets de mots inconnus
Essayez le tokenizer BPE de la bibliothèque
tiktokensur le mot inconnu “Akwirw ier” et affichez les identifiants de tokens (token IDs) individuels. Ensuite, appelez la fonction de décodage sur chacun des entiers obtenus dans cette liste pour reproduire le mappage logiquement dicté par le BPE. Pour finir, appelez la méthode de décodage sur l’ensemble final des identifiants de tokens pour vérifier s’il parvient à reconstruire l’entrée d’origine de la figure 2.11.
Solution :
En passant manuellement chaque sous-fragment perçu du mot de la figure 2.11 au tokenizer :
print(tokenizer.encode("Ak"))
print(tokenizer.encode("w"))
# ...
Résultat :
[33901]
[86]
# ...
Une fois assemblés, un unique appel repasse la liste d’IDs au tokenizer, reconstituant fidèlement la chaîne de départ :
print(tokenizer.decode([33901, 86, 343, 86, 220, 959]))
Résultat :
Akwirw ier
En bref, le BPE construit son vocabulaire de manière progressive, en partant de la plus petite unité. Il initialise d’abord son vocabulaire avec tous les caractères individuels (par exemple “a”, “b”, etc.). Ensuite, il repère les caractères qui apparaissent le plus souvent côte à côte pour les fusionner en sous-mots. Par exemple, si “d” et “e” sont très souvent adjacents, ils sont fusionnés pour créer le sous-mot “de” (très courant dans des mots comme “define” ou “made”). Ce mécanisme se répète itérativement, fusionnant les sous-mots les plus fréquents en mots entiers, uniquement sur la base de leur fréquence d’apparition.
Échantillonnage des données avec une fenêtre glissante (Sliding window)
Pour entraîner un LLM, on génère des paires entrée-cible (input-target pairs). La tâche du modèle étant de prédire le mot suivant, la séquence cible (y) correspond exactement à la séquence d’entrée (x), mais décalée d’une position vers la droite.
Figure 2.12 : Extraction de blocs d’entrée (input blocks) à partir d’un échantillon de texte pour l’entraînement du LLM. La tâche consiste à prédire le mot suivant le bloc d’entrée, en masquant les mots qui suivent la cible. (La tokenisation est omise ici pour plus de clarté).
Pour créer ces paires, on fait glisser une fenêtre sur le texte tokenisé. La taille du contexte (context_size ou max_length) détermine le nombre de tokens de l’entrée.
# Exemple de décalage d'une position pour créer la cible
context_size = 4
x = enc_sample[:context_size]
y = enc_sample[1:context_size+1]
print(f"x: {x}") # [290, 4920, 2241, 287]
print(f"y: {y}") # [4920, 2241, 287, 257]
Implémentation du pipeline de données avec PyTorch
Pour l’entraînement, les données doivent être converties en tenseurs et organisées en lots. On utilise pour cela deux classes standard de PyTorch : Dataset et DataLoader.
Figure 2.13 : Pour une efficacité maximale, les entrées sont regroupées dans un tenseur
x, où chaque ligne représente le contexte d’entrée. Un second tenseurycontient les cibles prédictives correspondantes (les mots suivants), créées en décalant l’entrée d’une position.
1. La classe Dataset (GPTDatasetV1) :
Cette classe définit comment découper le texte en séquences individuelles. Elle divise le texte en blocs de la taille de max_length pour les entrées, et crée les blocs cibles correspondants en les décalant d’un token.
import torch
from torch.utils.data import Dataset, DataLoader
class GPTDatasetV1(Dataset):
def __init__(self, txt, tokenizer, max_length, stride):
self.input_ids = []
self.target_ids = []
# Tokenisation de tout le texte
token_ids = tokenizer.encode(txt)
# Utilisation d'une fenêtre glissante pour créer les séquences
for i in range(0, len(token_ids) - max_length, stride):
input_chunk = token_ids[i:i + max_length]
target_chunk = token_ids[i + 1: i + max_length + 1]
self.input_ids.append(torch.tensor(input_chunk))
self.target_ids.append(torch.tensor(target_chunk))
def __len__(self):
return len(self.input_ids)
def __getitem__(self, idx):
return self.input_ids[idx], self.target_ids[idx]
2. La classe DataLoader :
Le DataLoader regroupe les séquences du Dataset en lots (batches). Cela permet au modèle de traiter plusieurs exemples en parallèle.
import tiktoken
def create_dataloader_v1(txt, batch_size=4, max_length=256, stride=128, shuffle=True, drop_last=True, num_workers=0):
tokenizer = tiktoken.get_encoding("gpt2")
dataset = GPTDatasetV1(txt, tokenizer, max_length, stride)
dataloader = DataLoader(
dataset,
batch_size=batch_size,
shuffle=shuffle,
drop_last=drop_last,
num_workers=num_workers
)
return dataloader
Paramètres importants du DataLoader
- Batch Size (Taille du lot) : Le nombre de séquences traitées simultanément. Une taille de 1 est utile pour l’illustration, mais en apprentissage profond, on utilise des lots plus grands pour stabiliser les mises à jour du modèle.
- Drop Last (
drop_last=True) : Si le nombre total de séquences n’est pas divisible par la taille du lot, le dernier lot sera incomplet. L’ignorer (le supprimer) permet d’éviter des instabilités (pics de perte) de l’entraînement. - Stride (Le pas) : Détermine de combien de positions la fenêtre glissante avance pour extraire la séquence suivante.
- Un
stridede 1 fait avancer la fenêtre d’un seul token, créant beaucoup de chevauchements entre les séquences consécutives (pratique pour visualiser le mécanisme). - En pratique lors de l’entraînement, on définit souvent le
strideà la même valeur quemax_length. Cela empêche les séquences de se chevaucher, limitant ainsi le surapprentissage (overfitting).
- Un
Figure 2.14 : En définissant un pas (stride) égal à la taille de la fenêtre d’entrée (input window size), on évite tout chevauchement entre les lots.
Exercice 2.2 : Data loaders avec différents strides et context sizes
Essayez le data loader avec d’autres paramètres comme
max_length=2etstride=2, ou encoremax_length=8etstride=2pour développer votre intuition de la mécanique de la fenêtre glissante.
Création des embeddings de tokens (Token embeddings)
La dernière étape de la préparation des données consiste à convertir les identifiants de tokens (token IDs) en vecteurs d’intégration continus (embedding vectors). Cette représentation vectorielle est requise car les LLMs sont des réseaux de neurones profonds entraînés par l’algorithme de rétropropagation (backpropagation).
Les poids de la matrice d’embeddings sont initialisés avec de petites valeurs aléatoires qui seront optimisées pendant l’entraînement du modèle.
Figure 2.15 : La préparation du texte passe par la tokenisation, la conversion en identifiants (token IDs), et enfin la projection de ces identifiants en vecteurs continus via une couche d’embedding.
Fonctionnement d’une couche d’embedding avec PyTorch
On instancie une telle couche avec torch.nn.Embedding. Ses dimensions dépendent de deux paramètres :
vocab_size: La taille du vocabulaire (le nombre de lignes).output_dim: Le nombre de dimensions de chaque vecteur d’embedding (le nombre de colonnes).
L’application d’un token ID à cette couche effectue une opération de recherche (lookup operation) : elle récupère directement la ligne de la matrice correspondant à cet identifiant.
# Exemple d'extraction de vecteurs pour input_ids = [2, 3, 5, 1]
# vocab_size = 6, output_dim = 3
print(embedding_layer(input_ids))
tensor([[ 1.2753, -0.2010, -0.1606], # Ligne d'index 2
[-0.4015, 0.9666, -1.1481], # Ligne d'index 3
[-2.8400, -0.7849, -1.4096], # Ligne d'index 5
[ 0.9178, 1.5810, 1.3010]], # Ligne d'index 1
grad_fn=<EmbeddingBackward0>)
Figure 2.16 : L’extraction de vecteurs d’embedding. Chaque token ID sert d’index pour extraire la ligne correspondante depuis la matrice de poids de la couche d’embedding.
Note sur le One-hot encoding : Utiliser une couche d’embedding est mathématiquement et fondamentalement équivalent à appliquer un encodage “one-hot” suivi d’une multiplication matricielle (couche fully connected). La couche d’embedding est cependant une implémentation beaucoup plus efficace en termes de calculs, tout en restant un composant différentiable pour la rétropropagation.
Encodage de la position des mots
Les embeddings de tokens présentent un défaut structurel majeur : peu importe qu’un mot apparaisse en début, au milieu ou en fin de phrase, son vecteur d’embedding reste rigoureusement identique. Pire, le mécanisme de self-attention des LLMs (abordé au chapitre 3) est, par construction, invariant à l’ordre : il traite la séquence d’entrée comme un simple ensemble non ordonné de tokens, sans aucune notion de précédence ou de proximité.

token_id 5, qu’il soit en
première ou en quatrième position dans le vecteur d’entrée, produira toujours le même vecteur
d’embedding.Il est donc indispensable d’injecter une information de position dans ces embeddings pour que le réseau comprenne l’ordre des mots.
Il existe deux grandes familles d’encodages positionnels :
- Encodages de position absolue : Un vecteur de position distinct est associé à chaque indice de la séquence (le token à la position 0 reçoit un vecteur fixe, celui en position 1 un autre, etc.). Ces vecteurs ont la même dimension que les embeddings de tokens, et leur sont additionnés terme à terme. C’est l’approche retenue par OpenAI pour ses modèles GPT : les vecteurs de position absolue sont des paramètres appris et optimisés lors de l’entraînement, au même titre que les poids du réseau.
⚠️ Limite à l’inférence : La couche
pos_embedding_layerest une matrice de poids de dimensions(context_length, output_dim). Aveccontext_length = 1024, cette matrice possède exactement 1 024 lignes — une par position, de l’indice 0 à l’indice 1 023. Si, à l’inférence, on soumet une séquence de 1 500 tokens, le modèle tente d’accéder aux lignes 1 024 à 1 499, qui n’existent pas. PyTorch lève immédiatement une erreur :IndexError: index out of range in self. Le modèle ne se dégrade pas progressivement : il plante. C’est pourquoi toute séquence dépassantcontext_lengthdoit être tronquée avant d’être soumise au modèle.
- Encodages de position relative : Plutôt que d’encoder la position absolue de chaque token, ce type d’encodage modélise la distance entre deux tokens — c’est-à-dire leur écart relatif dans la séquence. Concrètement, au lieu d’injecter un vecteur de position dans l’embedding d’entrée, l’information positionnelle est intégrée directement au sein du calcul du score d’attention entre deux tokens et : ce score dépend alors de plutôt que de et séparément.
L’avantage principal est une meilleure généralisation hors-distribution : le modèle n’a jamais mémorisé de vecteur fixe par position absolue — il a appris à raisonner en termes d’écarts entre tokens, qui restent valables quelle que soit la longueur totale de la séquence.

Implémentation en PyTorch
Dans l’approche GPT (position absolue), on crée deux couches torch.nn.Embedding distinctes, toutes deux de même dimension de sortie :
- Une couche pour encoder le sens des tokens (indexée sur le vocabulaire).
- Une couche pour encoder la position des tokens dans la séquence.
Nous travaillons ici avec une dimension d’embedding de 256 — inférieure aux 12 288 dimension de GPT-3, mais suffisante pour l’expérimentation — et le tokenizer BPE introduit précédemment, qui couvre un vocabulaire de 50 257 tokens.
Étape 1 — Créer la couche d’embedding des tokens
vocab_size = 50257
output_dim = 256
token_embedding_layer = torch.nn.Embedding(vocab_size, output_dim)
Étape 2 — Charger un batch depuis le DataLoader
Instancions d’abord le DataLoader (voir section 2.6) pour récupérer un batch d’exemples :
max_length = 4
dataloader = create_dataloader_v1(
raw_text, batch_size=8, max_length=max_length,
stride=max_length, shuffle=False
)
data_iter = iter(dataloader)
inputs, targets = next(data_iter)
print("Token IDs:\n", inputs)
print("\nInputs shape:\n", inputs.shape)
Token IDs:
tensor([[ 40, 367, 2885, 1464],
[1807, 3619, 402, 271],
[10899, 2138, 257, 7026],
[15632, 438, 2016, 257],
[ 922, 5891, 1576, 438],
[ 568, 340, 373, 645],
[ 1049, 5975, 284, 502],
[ 284, 3285, 326, 11]])
Inputs shape:
torch.Size([8, 4])
Le tenseur inputs est de dimension (8, 4) : 8 exemples textuels, chacun composé de 4 tokens.
Étape 3 — Appliquer l’embedding des tokens
token_embeddings = token_embedding_layer(inputs)
print(token_embeddings.shape)
torch.Size([8, 4, 256])
Chaque token_id est désormais représenté par un vecteur de 256 dimensions.
Étape 4 — Créer la couche d’embedding des positions
context_length = max_length # Longueur maximale de la séquence d'entrée
pos_embedding_layer = torch.nn.Embedding(context_length, output_dim)
pos_embeddings = pos_embedding_layer(torch.arange(context_length))
print(pos_embeddings.shape)
torch.Size([4, 256])
torch.arange(context_length)génère simplement[0, 1, 2, ..., context_length - 1].
C’est la liste de tous les indices de position, passée à la couche pour en récupérer toutes les lignes d’un coup — le vecteur de la position 0, puis celui de la position 1, etc.
Étape 5 — Additionner pour former l’entrée finale
input_embeddings = token_embeddings + pos_embeddings
print(input_embeddings.shape)
torch.Size([8, 4, 256])
Grâce au mécanisme de broadcasting de PyTorch, le tenseur pos_embeddings de dimensions (4, 256) — identique pour chaque exemple du batch — s’additionne automatiquement à chacun des 8 exemples dans token_embeddings de dimensions (8, 4, 256). C’est ce tenseur final input_embeddings qui est transmis aux couches profondes du modèle.

token_ids, transformé en embeddings de tokens, auxquels on additionne les embeddings positionnels pour former les input_embeddings transmis aux couches principales du LLM.Ce qu’il faut retenir de ce chapitre
-
Les LLMs ne traitent pas le texte brut : Le texte doit d’abord être segmenté en tokens (mots ou sous-mots), puis transformé en entiers appelés
token_ids. -
Les tokens spéciaux : Des balises comme
<|unk|>ou<|endoftext|>servent à structurer les données et à délimiter des textes indépendants lors de l’entraînement. -
Le tokenizer BPE (Byte Pair Encoding) : Il permet aux LLMs comme GPT-2 ou GPT-3 de gérer tout mot inconnu en le décomposant en sous-mots ou en caractères.
-
Échantillonnage par fenêtre glissante (sliding window) : Lors de l’entraînement, les cibles (labels) sont extraites par un simple décalage de +1, pour apprendre au modèle à prédire systématiquement le token suivant.
-
La couche
Embeddingen PyTorch : Elle se comporte comme une table de correspondance (lookup table) : étant donné untoken_identier, elle retourne instantanément le vecteur correspondant. Mathématiquement, c’est équivalent à encoder le token en vecteur one-hot puis à le multiplier par une matrice de projection linéaire — mais l’implémentation directe est drastiquement plus efficace. -
Plongement sémantique : Transformer un token abstrait en vecteur continu permet de disposer les mots dans un espace vectoriel de grande dimension, au sein duquel les relations sémantiques peuvent être optimisées numériquement.
-
Encodage de position : Le mécanisme de
self-attentionlit une séquence comme un ensemble non ordonné de tokens. Pour lui restituer la notion d’ordre, GPT additionne à chaque embedding de token un vecteur de position absolue appris — un vecteur par indice de la séquence, stocké dans une matrice de taille(context_length, output_dim). Cette matrice impose une contrainte dure : toute séquence dépassantcontext_lengthtokens doit être tronquée, sous peine d’une erreur d’indice à l’inférence. Les architectures plus récentes préfèrent des encodages relatifs, intégrés dans le calcul des scores d’attention plutôt qu’additionnés aux embeddings, ce qui s’affranchit de cette limite de longueur.





