Aller au contenu
Tutoriel

Comment on crée une IA, explique avec un vrai modèle de 10 millions de paramètres

ChatGPT semble sorti d'une boîte noire. Sa recette de base tient pourtant en un peu plus de 200 lignes de code. Tokenisation, attention, entraînement, génération : comment un modèle de langage est vraiment construit, avec le code sous les yeux.

Attuoman Prince Josias

Auteur : Attuoman Prince Josias

Publié le

Comment on crée une IA, explique avec un vrai modèle de 10 millions de paramètres

ChatGPT semble sorti d'une boîte noire inaccessible. Pourtant, la recette qui le fait fonctionner tient, dans ses grandes lignes, en un peu plus de 200 lignes de code. La différence entre ce petit modèle et un assistant IA grand public n'est pas la recette : c'est l'échelle. Voici comment on construit, de zéro, un vrai modèle de langage, avec le code exact sous les yeux.

Dans cet article :

Les données : comment un texte devient des chiffres

Un modèle de langage ne comprend pas les lettres, il ne travaille qu'avec des nombres. La toute première étape consiste donc à donner un numéro à chaque caractère unique présent dans un texte d'exemple : "a" devient peut-être 1, "b" devient 2, un espace devient 3, et ainsi de suite. Le texte entier se transforme alors en une longue suite de chiffres.

chars = sorted(list(set(text)))       # tous les caractères uniques du texte
vocab_size = len(chars)
stoi = {ch: i for i, ch in enumerate(chars)}   # lettre -> chiffre
itos = {i: ch for i, ch in enumerate(chars)}   # chiffre -> lettre
encode = lambda s: [stoi[c] for c in s]
decode = lambda l: ''.join([itos[i] for i in l])

Ce modèle-ci travaille caractère par caractère, la version la plus simple possible. Les grands modèles utilisent des unités un peu plus grandes (des morceaux de mots), mais le principe de départ, transformer du texte en chiffres avant tout calcul, reste rigoureusement identique.

Le cerveau : ce qu'est vraiment "l'attention"

Le cœur du modèle s'appelle un transformeur, popularisé par une équipe de chercheurs de Google en 2017. Son idée centrale, "l'attention", peut se résumer simplement : pour deviner le mot suivant, l'IA ne regarde pas seulement le mot juste avant, elle balaie tous les mots précédents et décide, à chaque fois, lesquels comptent vraiment pour la suite.

class Head(nn.Module):
    """Une 'tête d'attention' : regarde le texte et décide quels mots
    passés sont importants pour deviner le mot suivant."""
    def __init__(self, head_size):
        super().__init__()
        self.key = nn.Linear(n_embd, head_size, bias=False)
        self.query = nn.Linear(n_embd, head_size, bias=False)
        self.value = nn.Linear(n_embd, head_size, bias=False)

    def forward(self, x):
        k = self.key(x)      # "voici ce que je propose"
        q = self.query(x)    # "voici ce que je cherche"
        wei = q @ k.transpose(-2, -1)   # qui correspond à qui
        wei = F.softmax(wei, dim=-1)    # transforme en poids d'importance
        v = self.value(x)    # "voici mon contenu réel"
        return wei @ v       # contenu pondéré par l'importance

Dans le code, chaque mot pose une question ("query" : qu'est-ce qui m'intéresse ?), chaque mot propose aussi une réponse ("key" : voici ce que je représente), et plus la question et la réponse se correspondent, plus le contenu réel de ce mot ("value") pèse dans la décision finale. Empilez plusieurs de ces "têtes d'attention" en parallèle, puis plusieurs couches de ce mécanisme, et vous obtenez l'architecture qui fait tourner la quasi-totalité des assistants IA actuels.

L'entraînement : apprendre de ses erreurs

Un modèle qui vient d'être créé ne sait littéralement rien : ses réglages internes démarrent au hasard. L'entraînement consiste à lui montrer, des milliers de fois de suite, un morceau de texte, à lui demander de deviner le caractère suivant, puis à mesurer à quel point il s'est trompé.

for iter in range(max_iters):
    if iter % eval_interval == 0:
        losses = estimate_loss()
        print(f"étape {iter}: erreur train {losses['train']:.4f}, erreur val {losses['val']:.4f}")

    xb, yb = get_batch('train')          # un lot d'exemples
    logits, loss = model(xb, yb)         # le modèle devine, on mesure l'erreur
    optimizer.zero_grad(set_to_none=True)
    loss.backward()                      # on calcule comment corriger
    optimizer.step()                     # on corrige un tout petit peu

Cette mesure d'erreur permet de corriger, à chaque tour, très légèrement les réglages internes du modèle dans la direction qui réduit l'erreur. Répétée trois mille fois dans ce script, cette correction minuscule et répétée est, très concrètement, tout ce qu'on appelle "l'apprentissage" d'une IA.

La génération : comment il invente la suite

Une fois entraîné, le modèle ne "sait" toujours qu'une seule chose : pour une suite de caractères donnée, quelle est la probabilité de chaque caractère suivant possible. Écrire du texte consiste alors à tirer un caractère selon ces probabilités, l'ajouter à la suite, puis recommencer le calcul sur cette nouvelle suite, encore et encore.

def generate(self, idx, max_new_tokens):
    for _ in range(max_new_tokens):
        logits, _ = self(idx)                     # probabilité de chaque caractère suivant
        probs = F.softmax(logits[:, -1, :], dim=-1)
        idx_next = torch.multinomial(probs, num_samples=1)  # tirage pondéré, pas le plus probable à coup sûr
        idx = torch.cat((idx, idx_next), dim=1)    # on l'ajoute et on recommence
    return idx

Le tirage n'est pas toujours le choix le plus probable : c'est un tirage pondéré, comme une loterie où les numéros les plus probables ont plus de tickets, mais où un numéro moins probable peut quand même sortir. C'est cette petite part de hasard contrôlé qui permet à une IA de ne jamais répondre exactement de la même façon deux fois.

Pour faire tourner ce script vous-même : mettez un fichier texte nommé "input.txt" dans le même dossier (par exemple des articles collés en français), lancez python mini_gpt.py, et regardez le texte généré s'améliorer au fil de l'entraînement. Un GPU gratuit (Google Colab, modèle T4) accélère largement l'opération par rapport à un simple processeur.

Ce que ce mini-modèle ne pourra jamais faire

Ce modèle compte environ 10 millions de réglages internes et s'entraîne en quelques minutes sur une carte graphique gratuite. Les modèles derrière les grands assistants IA en comptent plusieurs centaines de milliards, entraînés pendant des mois sur des dizaines de milliers de puces spécialisées, pour un coût qui se chiffre en dizaines, parfois en centaines de millions de dollars.

La recette de base, la partie que vous venez de lire, est rigoureusement la même dans les deux cas. Ce qui sépare ce script d'un assistant IA grand public n'est donc pas un secret caché dans l'algorithme, c'est une question d'échelle : la quantité de données, la puissance de calcul, et le budget que ça représente. Notre article sur le coût réel de l'inférence IA détaille justement cette partie de l'histoire.

Le code complet, prêt à lancer

Afficher le script Python complet (mini_gpt.py)
"""
MINI-GPT — un vrai modèle de langage, codé de zéro
=====================================================
Inspiré de nanoGPT (Andrej Karpathy). Environ 10 millions de paramètres.
Tourne sur CPU (lent) ou GPU (rapide, ex: Google Colab gratuit avec T4).

Comment l'utiliser :
1. Mets un fichier texte nommé "input.txt" dans le même dossier
   (par exemple des articles de lecodeia.com collés dans un .txt, en français).
2. Lance : python mini_gpt.py
3. Regarde la génération de texte s'améliorer au fil de l'entraînement.
"""

import torch
import torch.nn as nn
from torch.nn import functional as F

# ---------------------------------------------------------------------------
# 1. RÉGLAGES (hyperparamètres) — les "boutons" du modèle
# ---------------------------------------------------------------------------
batch_size = 64        # combien de morceaux de texte traités en même temps
block_size = 256       # combien de caractères l'IA regarde d'un coup ("mémoire")
max_iters = 3000       # nombre de fois où on corrige le modèle
eval_interval = 300    # tous les combien on affiche le niveau d'erreur
learning_rate = 3e-4   # vitesse d'apprentissage
device = 'cuda' if torch.cuda.is_available() else 'cpu'
eval_iters = 50
n_embd = 384           # taille du "sens" attribué à chaque caractère
n_head = 6             # nombre de "têtes d'attention" (angles de lecture)
n_layer = 6            # nombre de couches empilées (profondeur du cerveau)
dropout = 0.2          # anti-par-cœur (évite que l'IA récite bêtement)

torch.manual_seed(1337)

# ---------------------------------------------------------------------------
# 2. DONNÉES — on charge le texte et on l'encode en chiffres
# ---------------------------------------------------------------------------
with open('input.txt', 'r', encoding='utf-8') as f:
    text = f.read()

chars = sorted(list(set(text)))       # tous les caractères uniques du texte
vocab_size = len(chars)
stoi = {ch: i for i, ch in enumerate(chars)}   # lettre -> chiffre
itos = {i: ch for i, ch in enumerate(chars)}   # chiffre -> lettre
encode = lambda s: [stoi[c] for c in s]
decode = lambda l: ''.join([itos[i] for i in l])

data = torch.tensor(encode(text), dtype=torch.long)
n = int(0.9 * len(data))
train_data = data[:n]
val_data = data[n:]

def get_batch(split):
    d = train_data if split == 'train' else val_data
    ix = torch.randint(len(d) - block_size, (batch_size,))
    x = torch.stack([d[i:i+block_size] for i in ix])
    y = torch.stack([d[i+1:i+block_size+1] for i in ix])
    return x.to(device), y.to(device)

@torch.no_grad()
def estimate_loss():
    out = {}
    model.eval()
    for split in ['train', 'val']:
        losses = torch.zeros(eval_iters)
        for k in range(eval_iters):
            X, Y = get_batch(split)
            _, loss = model(X, Y)
            losses[k] = loss.item()
        out[split] = losses.mean()
    model.train()
    return out

# ---------------------------------------------------------------------------
# 3. LE CERVEAU — architecture "transformer" (self-attention)
# ---------------------------------------------------------------------------
class Head(nn.Module):
    """Une 'tête d'attention' : regarde le texte et décide quels mots
    passés sont importants pour deviner le mot suivant."""
    def __init__(self, head_size):
        super().__init__()
        self.key = nn.Linear(n_embd, head_size, bias=False)
        self.query = nn.Linear(n_embd, head_size, bias=False)
        self.value = nn.Linear(n_embd, head_size, bias=False)
        self.register_buffer('tril', torch.tril(torch.ones(block_size, block_size)))
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        B, T, C = x.shape
        k = self.key(x)
        q = self.query(x)
        wei = q @ k.transpose(-2, -1) * C**-0.5
        wei = wei.masked_fill(self.tril[:T, :T] == 0, float('-inf'))  # ne pas tricher en regardant le futur
        wei = F.softmax(wei, dim=-1)
        wei = self.dropout(wei)
        v = self.value(x)
        return wei @ v

class MultiHeadAttention(nn.Module):
    """Plusieurs têtes d'attention en parallèle, combinées ensuite."""
    def __init__(self, num_heads, head_size):
        super().__init__()
        self.heads = nn.ModuleList([Head(head_size) for _ in range(num_heads)])
        self.proj = nn.Linear(n_embd, n_embd)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        out = torch.cat([h(x) for h in self.heads], dim=-1)
        return self.dropout(self.proj(out))

class FeedForward(nn.Module):
    """Petit réseau qui 'digère' l'information après l'attention."""
    def __init__(self, n_embd):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(n_embd, 4 * n_embd),
            nn.ReLU(),
            nn.Linear(4 * n_embd, n_embd),
            nn.Dropout(dropout),
        )

    def forward(self, x):
        return self.net(x)

class Block(nn.Module):
    """Un bloc complet : attention + digestion. On en empile plusieurs (n_layer)."""
    def __init__(self, n_embd, n_head):
        super().__init__()
        head_size = n_embd // n_head
        self.sa = MultiHeadAttention(n_head, head_size)
        self.ffwd = FeedForward(n_embd)
        self.ln1 = nn.LayerNorm(n_embd)
        self.ln2 = nn.LayerNorm(n_embd)

    def forward(self, x):
        x = x + self.sa(self.ln1(x))
        x = x + self.ffwd(self.ln2(x))
        return x

class MiniGPT(nn.Module):
    def __init__(self):
        super().__init__()
        self.token_embedding_table = nn.Embedding(vocab_size, n_embd)
        self.position_embedding_table = nn.Embedding(block_size, n_embd)
        self.blocks = nn.Sequential(*[Block(n_embd, n_head) for _ in range(n_layer)])
        self.ln_f = nn.LayerNorm(n_embd)
        self.lm_head = nn.Linear(n_embd, vocab_size)

    def forward(self, idx, targets=None):
        B, T = idx.shape
        tok_emb = self.token_embedding_table(idx)
        pos_emb = self.position_embedding_table(torch.arange(T, device=device))
        x = tok_emb + pos_emb
        x = self.blocks(x)
        x = self.ln_f(x)
        logits = self.lm_head(x)

        if targets is None:
            loss = None
        else:
            B, T, C = logits.shape
            logits = logits.view(B*T, C)
            targets = targets.view(B*T)
            loss = F.cross_entropy(logits, targets)
        return logits, loss

    def generate(self, idx, max_new_tokens):
        for _ in range(max_new_tokens):
            idx_cond = idx[:, -block_size:]
            logits, _ = self(idx_cond)
            logits = logits[:, -1, :]
            probs = F.softmax(logits, dim=-1)
            idx_next = torch.multinomial(probs, num_samples=1)
            idx = torch.cat((idx, idx_next), dim=1)
        return idx

# ---------------------------------------------------------------------------
# 4. ENTRAÎNEMENT
# ---------------------------------------------------------------------------
model = MiniGPT().to(device)
print(f"Nombre de paramètres : {sum(p.numel() for p in model.parameters())/1e6:.2f}M")

optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate)

for iter in range(max_iters):
    if iter % eval_interval == 0:
        losses = estimate_loss()
        print(f"étape {iter}: erreur train {losses['train']:.4f}, erreur val {losses['val']:.4f}")

    xb, yb = get_batch('train')
    logits, loss = model(xb, yb)
    optimizer.zero_grad(set_to_none=True)
    loss.backward()
    optimizer.step()

# ---------------------------------------------------------------------------
# 5. GÉNÉRATION — on fait "parler" le modèle entraîné
# ---------------------------------------------------------------------------
context = torch.zeros((1, 1), dtype=torch.long, device=device)
print(decode(model.generate(context, max_new_tokens=500)[0].tolist()))

Questions fréquentes

Faut-il un GPU puissant pour faire tourner ce script ?

Non. Il tourne sur un simple processeur, lentement, ou beaucoup plus vite sur un GPU gratuit comme celui proposé par Google Colab.

Que dois-je mettre dans le fichier input.txt ?

N'importe quel texte assez long dans la langue de votre choix : plus il y en a, mieux le modèle apprend les régularités de cette langue.

Le texte généré sera-t-il cohérent, comme ChatGPT ?

Non, pas à cette échelle. Avec seulement 10 millions de réglages, le résultat imitera le style et l'orthographe du texte fourni, sans construire de phrases toujours sensées. C'est la différence d'échelle qui manque, pas la méthode.

Est-ce que ce mini-modèle utilise la même technique que ChatGPT ?

Oui, dans ses grandes lignes : la même architecture "transformeur" à base d'attention, le même principe d'entraînement par correction d'erreur répétée. La différence tient à l'échelle, pas à la recette.

Sources principales :

  • Vaswani et al. : "Attention Is All You Need" (2017)
    Article de recherche original ayant introduit l'architecture transformeur utilisée ici. (arxiv.org)
  • Andrej Karpathy : nanoGPT
    Projet éducatif open source dont ce script s'inspire directement. (github.com)

Une newsletter par semaine, gratuite.

Actus IA, tutoriels et outils. Zero spam, desabonnement en 1 clic.

création IA transformeur attention nanoGPT entraînement modèle code Python vulgarisation

Notez cet article

5,0/5 (1 vote)

Commentaires 0

Aucun commentaire pour l'instant.

Tu peux être le premier.

Connectez-vous pour laisser un commentaire

Compte gratuit, 30 secondes.