Chapitre 3 · 16 min
Entraîne tes propres tokens
Entraîne un petit tokenizer BPE dans le navigateur, puis sauvegarde le code du tokenizer et ses merges dans ton projet LLM local.
Jusqu’ici, nous découpions le texte sur les espaces. Chaque mot devenait un . C’est ainsi que le du chapitre 1 définissait son , et ça semblait innocent.
Ça ne l’est pas. C’est mauvais.
Un par espaces traite running, runs et run comme trois mots sans rapport. Pareil pour cat, cats et cat's. Pire : dès que le modèle rencontre un mot jamais vu, comme lowest alors que l’ ne contenait que low et lower, il n’a aucune information. Le est fermé.
Découper en caractères ne règle pas tout. Le devient minuscule et ouvert, mais running devient sept : r u n n i n g. Les séquences explosent.
Il existe un milieu presque embarrassant de simplicité : (). À l’origine, c’est un algorithme de compression de 1994, devenu très utile pour le langage. Tu vas l’inspecter depuis zéro : quatre fonctions, puis la même idée sauvegardée dans llm/tokenizer.py.
1. Initialiser : chaque mot comme liste de caractères
L’état de départ est le plus simple possible : chaque mot est découpé en caractères. Les espaces séparent les mots, mais à l’intérieur tout est lettre par lettre.
Code · JavaScript
C’est l’unité de départ de . Chaque étape choisit la paire adjacente la plus fréquente et la fusionne en un nouveau . Répète assez longtemps, et tu récupères quelque chose qui ressemble à des mots.
2. Trouver la paire la plus commune
Le doit demander : quelle paire de adjacents apparaît le plus souvent ? Parcours le , compte chaque (a, b) côte à côte, retourne le gagnant.
Code · JavaScript
Top adjacent pairs in the corpus
Dans notre , la paire gagnante sera une séquence de lettres banale comme e + s ou n + e. C’est toute l’histoire : redécouvre de la morphologie en poursuivant ce qui apparaît souvent ensemble.
3. Appliquer un merge
Une fois la paire gagnante connue, on parcourt le et on remplace chaque occurrence de (a, b) par un nouveau ab. Le est juste la concaténation. Rien de magique.
Code · JavaScript
Compare l’avant et l’après. Partout où a + b apparaissait, tu vois maintenant une seule puce ab. Le nombre total de baisse ; le augmente d’une entrée.
4. Entraîner et encoder
On assemble : “trouver la paire la plus fréquente + appliquer le merge”, N fois, en accumulant les merges dans l’ordre. Puis on applique ces merges à un nouveau texte, lowest newer.
Le script répète explicitement les quatre briques : bpeInit, countPairs, applyMerge et encode.
Code · JavaScript
Regarde le texte encodé. Le mot lowest n’est jamais apparu dans le d’. Pourtant il se décompose en morceaux du style low + est, parce que ces ont été appris ailleurs. Voilà le résultat important : généralise aux mots jamais vus en composant des .
Ce que tu perds
est glouton et irréversible. Les premiers merges influencent tous les suivants. Si ton est petit ou biaisé, les découverts le seront aussi. Un entraîné sur de l’anglais mal le français, et inversement.
Les merges sont aussi aveugles à la position : est peut servir dans newest et dans Eastern alors que ces mots n’ont rien à voir. Le ne sait pas. Il ne voit que des paires fréquentes.
5. Sauvegarder ton tokenizer localement
Crée llm/tokenizer.py :
"""A tiny BPE-style tokenizer for the teaching project."""
from __future__ import annotations
import json
from collections import Counter
from pathlib import Path
# [1]
def words(text: str) -> list[list[str]]:
return [list(word) for word in text.lower().split()]
# [2]
def count_pairs(corpus: list[list[str]]) -> Counter[tuple[str, str]]:
pairs: Counter[tuple[str, str]] = Counter()
for word in corpus:
for left, right in zip(word, word[1:]):
pairs[(left, right)] += 1
return pairs
# [3]
def apply_merge(corpus: list[list[str]], pair: tuple[str, str]) -> list[list[str]]:
merged = "".join(pair)
out: list[list[str]] = []
for word in corpus:
next_word: list[str] = []
i = 0
while i < len(word):
if i < len(word) - 1 and (word[i], word[i + 1]) == pair:
next_word.append(merged)
# [4]
i += 2
else:
next_word.append(word[i])
i += 1
out.append(next_word)
return out
# [5]
def train_bpe(text: str, steps: int) -> list[tuple[str, str]]:
corpus = words(text)
merges: list[tuple[str, str]] = []
for _ in range(steps):
pairs = count_pairs(corpus)
if not pairs:
break
pair, _ = pairs.most_common(1)[0]
merges.append(pair)
corpus = apply_merge(corpus, pair)
return merges
# [6]
def save_merges(merges: list[tuple[str, str]], path: str = "data/merges.json") -> None:
Path(path).parent.mkdir(parents=True, exist_ok=True)
Path(path).write_text(json.dumps(merges, indent=2), encoding="utf-8")Lis ce comme une boucle de compression :
- [1]
wordspart du niveau caractère. - [2]
count_pairscompte les symboles adjacents. - [3]
apply_mergeremplace la paire gagnante par un unique. - [4] saute de 2 après un merge pour ne pas réutiliser le même caractère.
- [5]
train_bperépète “compte, choisit, fusionne”. - [6]
save_mergesécrit la recette sur disque. L’ordre des merges est important.
Crée scripts/train_tokenizer.py :
from llm.tokenizer import save_merges, train_bpe
TEXT = "low lower newest widest low lower newer"
# [1]
merges = train_bpe(TEXT, steps=20)
# [2]
save_merges(merges)
# [3]
print(f"saved {len(merges)} merges to data/merges.json")
print(merges[:10])Lance-le :
python -m scripts.train_tokenizerpython -m scripts.train_tokenizerpython -m scripts.train_tokenizerLe reste minuscule, mais il existe maintenant comme pièce réutilisable de ton modèle local. Au chapitre 11, on remplacera ce jouet par un de production ; la forme restera la même.
Recap
- La par espaces est trop grossière : fermé, pas de lien entre mots
apparentés. - La caractère est trop fine : minuscule, séquences trop
longues. - commence avec les caractères et fusionne avidement la paire adjacente la plus
fréquente. - Les merges découvrent de la morphologie sans qu’on la donne au modèle. -
généralise aux mots jamais vus en composant des appris. - Ton projet local a
maintenant
llm/tokenizer.pyetdata/merges.json.
Pour aller plus loin
- Sennrich, Haddow, Birch — “Neural Machine Translation of Rare Words with Subword Units”.
- Karpathy, “Let’s build the GPT Tokenizer”.
- Tiktokenizer.
- L’implémentation de référence vit dans
lib/ml/tokenizer/bpe.ts.
Prochaine étape : donner du sens aux mots — un n’est qu’un nombre, mais on peut lui donner un vecteur.