Chapitre 1 · 18 min
Le modèle le plus idiot qui existe
Démarre ton projet LLM local, puis construis le modèle de langage le plus simple possible : un générateur bigramme que tu peux lancer sur ta machine.
Ouvre un livre. Lis la première ligne. Peux-tu deviner le mot suivant ? Probablement, parce que tu l’as déjà vu mille fois ailleurs.
Construisons un modèle qui ne sait faire que ça. À la fin du chapitre, tu auras lancé quatre petites fonctions, vu comment elles se combinent en générateur, et sauvegardé le premier fichier de ton propre projet my-llm/.
Le utilisé ici est un fragment de comptine. Il est volontairement petit et répétitif : assez petit pour vérifier le code à l’œil, assez répétitif pour que le modèle dise quelque chose de presque cohérent.
the cat sat on the mat the dog sat on the rug the cat watched the dog the dog watched the cat
0. Démarre ton projet local
Les cellules dans le navigateur sont le banc d’essai rapide. Le dossier local est ce que tu gardes.
Crée-le maintenant :
mkdir -p my-llm/llm my-llm/scripts my-llm/data && cd my-llm && python3 -m venv .venv && source .venv/bin/activatemkdir my-llm; mkdir my-llm\\llm,my-llm\\scripts,my-llm\\data; cd my-llm; py -m venv .venv; .\\.venv\\Scripts\\Activate.ps1mkdir -p my-llm/llm my-llm/scripts my-llm/data && cd my-llm && python3 -m venv .venv && source .venv/bin/activateAjoute ensuite le marqueur de package vide :
touch llm/__init__.pyNew-Item llm\\__init__.py -ItemType Filetouch llm/__init__.pyTu utiliseras ce dossier pendant tout le livre. Chaque chapitre y ajoute une brique.
1. Découper le texte en tokens
Avant de modéliser quoi que ce soit, il faut couper le texte en unités manipulables. Le choix le plus simple : chaque mot séparé par un espace est un . On met tout en minuscules pour que The et the comptent comme le même mot.
Lance la cellule. Elle retourne le tableau de , et la visualisation montre exactement ce que le modèle voit.
Code · JavaScript
C’est tout. C’est la préparation des données d’ dans sa forme minimale. Les vrais font quelque chose de bien plus malin, ce qu’on verra au chapitre 3, mais pour l’instant espaces + minuscules suffit.
2. Compter les paires
Tout le “modèle” sera une table de comptage : pour chaque paire de adjacents (a, b) dans le , combien de fois b a suivi a ? Cette table est le modèle. L’ consiste simplement à la remplir.
Le est l’ensemble des uniques, dans leur ordre d’apparition. La table de comptage est une matrice carrée de taille vocab.length × vocab.length, où counts[i][j] est le nombre de fois où vocab[i] a été immédiatement suivi par vocab[j].
Lance la boucle et inspecte la matrice qu’elle construit.
Code · JavaScript
La matrice produite est le modèle entier. La plupart des cellules sont à zéro : la plupart des paires de mots ne sont jamais apparues côte à côte. Les cellules non nulles disent ce que le modèle a appris. La ligne de the est probablement la plus remplie ; la ligne de mat est très pauvre, car mat n’est apparu qu’une fois, suivi par the.
En une ligne, les maths sont :
La probabilité que le mot suivant soit , sachant que le mot précédent était , est le nombre de fois où tu as vu divisé par le nombre total de fois où tu as vu . Chaque ligne de la matrice, divisée par sa somme, est exactement une .
3. Échantillonner une distribution
Nous avons des probabilités. Il faut les transformer en choix concrets. Si une ligne dit que the est suivi de cat 40 % du temps et de dog 60 % du temps, on veut tirer dog environ six fois sur dix.
L’astuce standard : tirer un nombre aléatoire dans [0, 1), parcourir la somme cumulée des probabilités, puis choisir le premier index dont la somme cumulée dépasse le tirage.
Code · JavaScript
Distribution
Empirical counts
Click Run several times to fill this side.
Lance plusieurs fois. Regarde le graphe des comptages empiriques se remplir. Après quelques dizaines de tirages, la droite devrait commencer à ressembler à la gauche. C’est la loi des grands nombres qui s’invite discrètement dans le chapitre.
4. Tout assembler : générer
On chaîne maintenant les pièces. On part d’un . On trouve sa ligne dans la matrice. On normalise cette ligne en . On le suivant. Celui-ci devient le nouveau . Et on répète.
La cellule met maintenant le , le , la longueur voulue et les trois fonctions auxiliaires directement dans le script. La boucle s’arrête tôt si elle tombe sur une impasse : un dont la ligne est entièrement à zéro.
Code · JavaScript
Clique sur Run plusieurs fois. Tu obtiendras des séquences différentes, parce que chaque étape est un , pas un choix déterministe. Certaines seront cohérentes, d’autres absurdes. Elles ne sont pas de simples répétitions du , sauf hasard.
Structurellement, tous les modernes font la même chose : des précédents entrent, une probabilité sur les suivants sort, puis on . Le mécanisme qui produit la est infiniment plus expressif que counts[i] / row_sum[i], mais l’interface est identique.
Tu viens de construire un . Il est très mauvais. Le reste du livre consiste à remplacer chaque partie par quelque chose qui l’est moins.
5. Mettre la baseline dans ton repo
Sauvegarde maintenant la même idée localement. Crée llm/bigram.py :
"""A tiny bigram language model."""
from __future__ import annotations
import random
from collections import Counter, defaultdict
# [1]
def tokenize(text: str) -> list[str]:
return [part for part in text.lower().split() if part]
# [2]
def train(tokens: list[str]) -> dict[str, Counter[str]]:
counts: dict[str, Counter[str]] = defaultdict(Counter)
# [3]
for left, right in zip(tokens, tokens[1:]):
counts[left][right] += 1
return counts
# [4]
def sample_next(row: Counter[str]) -> str | None:
total = sum(row.values())
if total == 0:
return None
roll = random.random() * total
acc = 0.0
for token, count in row.items():
acc += count
if roll <= acc:
return token
return next(reversed(row))
# [5]
def generate(model: dict[str, Counter[str]], seed: str, steps: int) -> list[str]:
out = [seed]
for _ in range(steps - 1):
nxt = sample_next(model.get(out[-1], Counter()))
if nxt is None:
break
out.append(nxt)
return outLis ce fichier par petits morceaux :
- [1]
tokenizeest volontairement ennuyeuse : minuscules, split, suppression des morceaux vides. Sa sortie est la seule chose que le modèle peut voir. - [2]
trainconstruit un dictionnaire de dictionnaires : précédent à l’extérieur, suivants possibles à l’intérieur. - [3]
zip(tokens, tokens[1:])est la boucle d’ en miniature :(the, cat), puis(cat, sat), puis(sat, on). - [4]
sample_nextne prend pas le plus grand comptage. Il tire un nombre aléatoire et parcourt la ligne : les fréquents gagnent souvent, pas toujours. - [5]
generaterépète ce pas d’un . Le dernier devient l’entrée de la prédiction suivante.
Crée scripts/train_bigram.py :
from llm.bigram import generate, tokenize, train
TEXT = "the cat sat on the mat the dog sat on the rug the cat watched the dog the dog watched the cat"
# [1]
tokens = tokenize(TEXT)
# [2]
model = train(tokens)
# [3]
for _ in range(5):
print(" ".join(generate(model, seed="the", steps=12)))Ce script est volontairement fin :
TEXTest la donnée d’.- [1] transforme le texte brut en unités du modèle.
- [2] remplit la table de comptage.
- [3] cinq continuations pour montrer que la est stochastique.
Lance-le :
python -m scripts.train_bigrampython -m scripts.train_bigrampython -m scripts.train_bigramTu as maintenant le premier artefact réel du cours : un local avec et . Minuscule, brut, mais à toi.
Recap
- Tu as lancé quatre fonctions :
tokenize,buildCounts,sampleNext, puis une bouclegenerate. - Tu as démarrémy-llm/et sauvegardé le premier module réutilisable :llm/bigram.py. - Le modèle est la table de comptage. L’ la remplit ; l’ choisit une ligne, la normalise et . - La est un répété. Différents runs donnent différentes sorties. - C’est quand même un . Mauvais, sans mémoire au-delà d’un , sans généralisation. Mais il a la même forme entrée/sortie qu’un vrai .
Pour aller plus loin
- makemore, épisode 1, par Karpathy construit le même modèle en Python, en version caractère.
- Step by Token, chapitre 1 couvre les mêmes idées depuis l’angle compréhension.
- L’implémentation de référence complète vit dans
lib/ml/bigram/.
Prochaine étape : compter ne suffit pas — que se passe-t-il quand ton modèle local rencontre une continuation qu’il n’a jamais vue ?