De la prédiction du token suivant à l’exécution d’instructions : affinage d’un modèle GPT
Comment un GPT préentraîné apprend à suivre des instructions : le formatage des enregistrements, le décalage des étiquettes, les masques de remplissage, le cycle d’entraînement, les limites d’évaluation et l’avancée vers les préférences DPO.
Sources: From Next-Token Prediction to ChatGPT <> Build an LLM From Scratch [6]
Le pré-entraînement confère déjà à un modèle de langage la capacité d’écrire. Ce qui lui manque, c’est toute notion de ce que l’utilisateur souhaite obtenir. L’ajustement par instructions comble cette lacune. En donnant aux deux modèles la même demande, par exemple expliquer la récursivité à un enfant de dix ans, la différence est évidente. Le modèle de base a tendance à répéter la demande et à s’engager dans une introduction de style manuel sur les cours d’informatique. Il ne répond pas du tout, il se contente d’allonger le texte. Le modèle ajusté, quant à lui, propose une image : une boîte contenant une boîte plus petite, elle-même contenant une boîte encore plus petite, la récursivité étant décrite comme un problème qui se renvoie constamment une version plus petite jusqu’à ce que la plus petite soit facile à résoudre.
En réalité, les deux modèles font la même chose : prédire le token suivant, mais ils se comportent de manière très différente. Les produits de chat font en sorte que l’exécution des instructions paraisse naturelle, ce qui masque cette différence. Les questions trouvent des réponses, les demandes de code génèrent du code, et des résumés sont écrits. Rien de tout cela ne résulte gratuitement de la simple lecture d’une montagne de texte. Le préentraînement inculque plutôt quelque chose de plus fondamental : étant donné tout ce qui a été vu jusqu’à présent, quel token est le plus probable ensuite ? Rien dans cela ne stipule qu’une demande d’utilisateur doive être exécutée. Combler cette lacune est le sujet abordé ici.
Acte 1 : le préentraînement enseigne la complétion, pas l’obéissance
La tâche de préentraînement consiste simplement en la prédiction du token suivant. Cela seul suffit à apprendre la grammaire, les faits, la syntaxe du code, les styles d’écriture ainsi que certains schémas surprenamment complexes, mais l’obéissance aux instructions n’en fait pas partie. Donnez ce prompt à un modèle de base :
Explain why the sky appears blue.
et cela pourrait se poursuivre par :
Explain why the sky appears blue.
The following questions are commonly asked in introductory physics...
Cela n’indique pas nécessairement un modèle défectueux. Il fait précisément ce que le préentraînement lui a demandé, à savoir continuer le texte. Le comportement souhaité ressemble plutôt à ceci :
The sky appears blue because Earth's atmosphere scatters
shorter wavelengths of sunlight more strongly than longer
wavelengths...
Le modèle de base est le même, mais le comportement attendu diffère. La question devient la suivante :
What text probably comes next?
vers ceci :
When the text looks like an instruction,
what kind of continuation should I produce?
Apprendre au modèle à répondre à cette deuxième question, c’est précisément l’objectif du fine-tuning par instructions.
Acte 2 : formatage des données d’instructions
La manière la plus directe d’enseigner au modèle à suivre des instructions consiste à lui montrer de nombreuses instructions accompagnées de bonnes réponses, et c’est vraiment aussi simple que cela. Un enregistrement d’entraînement peut ressembler à ceci :
example = {
"instruction": "Convert the following sentence to passive voice.",
"input": "The developer fixed the bug.",
"output": "The bug was fixed by the developer."
}
Un transformateur ne peut pas traiter des dictionnaires Python, donc le enregistrement est d’abord sérialisé en texte brut, à peu près comme ceci :
Below is an instruction that describes a task.
### Instruction:
Convert the following sentence to passive voice.
### Input:
The developer fixed the bug.
### Response:
The bug was fixed by the developer.
Le formatage est important car le modèle voit à plusieurs reprises le même schéma :
instruction
↓
optional context
↓
response
En bref, chaque enregistrement devient du texte formaté, puis des tokens, puis des lots complétés, et ces lots permettent d’affiner le modèle GPT préentraîné.
La vue des tokens
Une fois tokenisé, l’exemple se transforme en une liste d’ID, à peu près comme ceci :
[21106, 318, 281, 12064, ... 4435, 257, 3126, ...]
Ces identifiants de tokens forment la séquence d’entraînement. Pour un modèle autoregressif, les étiquettes ne sont rien d’autre que la séquence décalée d’une position :
Tokens:
[A, B, C, D, E]
Input:
[A, B, C, D]
Labels:
[B, C, D, E]
Ainsi, l’ajustement des instructions n’apporte aucun objectif d’apprentissage entièrement nouveau. Le modèle prédit toujours le token suivant. Ce qui change, c’est la distribution de texte qu’on lui demande de prédire. Au lieu de documents principalement ordinaires, il voit désormais cette correspondance encore et encore :
instruction → useful response
C’est l’idée centrale.
Le remplissage nécessite un traitement particulier
Les exemples d’entraînement diffèrent en longueur, mais les lots exigent des tenseurs aux formes compatibles. Prenons deux exemples :
Example 1:
[10, 20, 30, 40]
Example 2:
[11, 21]
On peut les rembourrer :
Example 1:
[10, 20, 30, 40]
Example 2:
[11, 21, PAD, PAD]
Les tokens de remplissage ne constituent pas des cibles significatives. Le modèle ne devrait pas être récompensé pour apprendre cela :
PAD → PAD → PAD → PAD
Ces positions sont généralement remplacées par une valeur d’ignorance :
ignore_index = -100
La perte de croisement d’entropie de PyTorch les ignore alors. Conceptuellement :
loss = cross_entropy(
logits.reshape(-1, vocab_size),
targets.reshape(-1),
ignore_index=-100,
)
Les détails sont importants. Sans masquage, une partie du signal d’entraînement est gaspillée à cause d’artefacts générés par le regroupement des données plutôt que pour le langage lui-même.
Acte 3 : affiner le comportement
Le cycle d’entraînement en lui-même n’est pas particulièrement complexe ici, puisque l’optimisation d’un modèle de token suivant est déjà une pratique courante. Ce qui compte, c’est quels exemples alimentent ce cycle. En termes simplifiés :
for batch in train_loader:
optimizer.zero_grad()
input_ids = batch[:, :-1]
targets = batch[:, 1:]
logits = model(input_ids)
loss = cross_entropy(
logits.flatten(0, 1),
targets.flatten(),
ignore_index=-100,
)
loss.backward()
optimizer.step()
Rien dans ce code ne dit quoi que ce soit de ce genre :
model.learn_to_be_helpful()
Le comportement provient des données. Supposons que des milliers d’exemples aient cet aspect :
### Instruction:
Summarize this paragraph.
### Response:
<clear concise summary>
Les mises à jour graduelles poussent constamment le modèle à produire des réponses de ce type après des prompts de ce type. Supposons que d’autres exemples ressemblent à ceci :
### Instruction:
Write Python code that reverses a string.
### Response:
def reverse_string(s):
return s[::-1]
Il apprend une autre correspondance entre un schéma d’instruction et une continuation adaptée. Vue au niveau des tokens, le fine-tuning des instructions n’est pas si magique : le contexte entourant les prédictions a été délibérément modifié. Avant :
The CPU cache is...
Après le formatage de l’instruction :
### Instruction:
Explain CPU caching to a beginner.
### Response:
A CPU cache is...
Aucun objectif tel que « comprendre les utilisateurs » n’a été introduit. Au lieu de cela, la distribution du texte d’entraînement a été modifiée de manière à ce que les réponses utiles deviennent les continuations statistiquement les plus probables après une instruction. C’est ce changement qui produit le nouveau comportement. Dans l’exemple donné, un modèle GPT-2-medium préentraîné est affiné grâce à des instructions, génère des réponses et enregistre le modèle résultant.
Acte 4 : l’évaluation devient problématique
La classification offrait une méthode d’évaluation simple. Pour la détection de spam :
Prediction: SPAM
Label: SPAM
Correct.
La mise en œuvre des instructions ne permet pas de telles facilités. Prenons l’exemple de la demande d’explication simple de la récursivité. Une réponse possible :
Recursion is when a function solves a problem by calling
itself on a smaller version of the same problem.
Autre :
Recursion is a technique where a problem is reduced into
smaller instances of itself until a stopping condition is reached.
Les deux peuvent être corrects, et aucun n’a besoin de correspondre mot pour mot à un texte de référence. Cela rend la précision de la correspondance exacte presque inutile pour les réponses ouvertes.
Comparaison avec un texte de référence
Une option consiste à fournir un texte de référence :
{
"instruction": "Explain recursion simply.",
"reference": "Recursion solves a problem by repeatedly reducing it...",
"model_response": "A recursive function calls itself..."
}
et comparer la réponse générée avec celle-ci. Mais la similarité n’est pas synonyme de qualité, et un modèle peut fournir une excellente réponse en utilisant des mots complètement différents. Cela mène à une autre approche.
Demander à un autre LLM de juger
L’évaluateur reçoit l’instruction, la réponse de référence ainsi que la réponse à évaluer :
Instruction:
Explain recursion simply.
Reference answer:
...
Model answer:
...
Rate the model answer from 0 to 100 based on correctness,
relevance, and clarity.
Un juge local peut utiliser Ollama, ce qui est pratique, mais cela pose un nouveau problème : le juge lui-même est un modèle. Son score peut dépendre du prompt d’évaluation, du choix de l’évaluateur, de l’ordre des réponses, du degré de détail, des préférences stylistiques ainsi que des erreurs du juge lui-même. Certains exemples montrent que des juges locaux attribuent des scores étonnamment élevés à des réponses issues de modèles de base médiocres, ce qui rappelle que les scores obtenus avec des LLM en tant que juges ne correspondent pas à la vérité absolue.
Un petit projet peut combiner trois couches :
1. Inspect representative outputs manually
2. Compare against reference answers where useful
3. Use an LLM judge for aggregate comparison
Aucun de ces éléments n’est parfait en soi. Ensemble, ils offrent une image plus claire qu’un seul chiffre présenté comme vérité.
Acte 5 : un problème plus difficile, les préférences
L’ajustement des instructions montre au modèle à quoi ressemble une réponse raisonnable. Comparons maintenant deux réponses. Réponse A :
Recursion is when a function calls itself.
Réponse B :
Recursion is a technique where a function solves a problem
by calling itself on a smaller version of that problem.
The process stops when it reaches a base case.
Les deux sont valables, mais B peut être préféré. Le signal d’entraînement n’est plus le suivant :
correct vs incorrect
C’est cela :
chosen vs rejected
Un enregistrement de préférence peut ressembler à ceci :
{
"prompt": "Explain recursion simply.",
"chosen": """
Recursion solves a problem by repeatedly reducing it
until reaching a base case.
""",
"rejected": """
Recursion is when recursion happens recursively.
"""
}
C’est un type de supervision différent. Au lieu de dire « voici une réponse à imiter », on dit « parmi ces deux réponses, préférez celle-ci ». C’est là que des techniques telles que l’Optimisation Directe des Préférences (DPO) entrent en jeu. Au niveau général :
Prompt
↓
Chosen response ─────┐
├── preference objective
Rejected response ───┘
↓
model update
Le DPO mérite un traitement distinct, donc ses mécanismes ne font pas l’objet de cette discussion. Le lien utile concerne la progression globale :
Pretraining
↓
learn language patterns
------------------------------
Instruction fine-tuning
↓
learn instruction → response behavior
------------------------------
Preference tuning
↓
learn which acceptable responses we prefer
La génération de jeux de données de préférences à l’aide de Llama 3.1 via Ollama ainsi que la mise en œuvre de DPO depuis zéro sont des sujets à aborder plus tard. Une précaution s’impose : le fait que DPO soit disponible en tant que matériel supplémentaire ne signifie pas pour autant qu’il s’agisse de la prochaine étape appropriée pour tous les modèles ou jeux de données. Certains essais montrent des limites dans ses résultats, ce qui constitue précisément le scepticisme à garder en tête lors de l’ajustement des préférences.
Qu’est-ce qui a vraiment changé
Le point de départ était un modèle préentraîné capable de continuer un texte. Le transformateur n’a pas été remplacé par une architecture d’assistant spécial. Les exemples liés au problème du token suivant ont changé, ce qui a donné naissance à un profil de comportement différent. Lorsque le modèle voit maintenant :
### Instruction:
On a rencontré suffisamment de cas où la continuation n’était pas un autre paragraphe tiré d’Internet, mais une réponse. Vu de cette manière, il est plus facile de comprendre la progression :
Pretraining:
What token should come next?
Instruction tuning:
What does a good answer look like after an instruction?
Preference tuning:
Of several reasonable answers, which behavior should we prefer?
L’architecture, l’optimiseur et le taux d’apprentissage sont tous importants, mais dans l’ajustement par instruction, les données jouent un rôle particulier, car elles définissent le comportement à imiter. Lorsqu’il existe plusieurs réponses acceptables, un résultat valide n’est plus l’objectif ultime. La question devient alors de savoir quelle réponse acceptable mérite le plus d’importance, et c’est là que le DPO entre en jeu.
Le préentraînement enseigne quel texte a tendance à suivre. L’ajustement par instruction détermine quelles continuations constituent des réponses utiles aux instructions. L’ajustement par préférence va plus loin en décidant quelles réponses utiles doivent être privilégiées.
Lectures complémentaires
- Entraîner un petit GPT sur des contes populaires indiens : tokeniseur, masque et perte — Un GPT de 9 millions de paramètres construit de zéro : pourquoi le tokeniseur de GPT-2 échoue avec les mots des contes populaires, comment fonctionnent BPE et le masque causal, et comment un seul lot génère 4 096 valeurs de perte pour le prochain token.
- À l’intérieur d’un LLM : inférence en Python pur pour Qwen3-0.6B sur MLX — Construction étape par étape de l’inférence d’un LLM en environ 200 lignes de Python avec MLX : tokeniseur, RMSNorm, RoPE, attention groupée aux requêtes, SwiGLU, cache KV et échantillonnage.