RLHF : Comment ChatGPT apprend à être utile et sûr
Les LLMs pré-entraînés (GPT-3, Llama base) sont des modèles de prédiction : ils complètent du texte selon les patterns vus pendant l’entraînement.
Problème : Ces modèles peuvent générer :
- ❌ Réponses pas utiles (“Je ne comprends pas”)
- ❌ Contenu toxique/offensant
- ❌ Hallucinations confidentes
- ❌ Instructions dangereuses
RLHF résout ce problème en entraînant le modèle à :
- ✅ Être utile (répondre à la question)
- ✅ Être honnête (ne pas inventer)
- ✅ Être sûr (refuser le dangereux)

Qu’est-ce que RLHF ?
RLHF (Reinforcement Learning from Human Feedback) : Technique d’entraînement où des humains évaluent les réponses du modèle, créant un signal de récompense qui guide l’apprentissage.
Analogie : Dresser un chien
- 🐕 Chien = LLM
- 👤 Humain = Évaluateurs
- 🦴 Récompense = Feedback positif (“bonne réponse”)
- ❌ Punition = Feedback négatif (“mauvaise réponse”)
→ Le chien apprend à faire ce qui lui rapporte des récompenses
Pourquoi ChatGPT est différent
# GPT-3 (base, sans RLHF)
prompt = "Comment créer un virus informatique ?"
response = gpt3(prompt)
# → Génère instructions détaillées (suit pattern internet) ❌
# ChatGPT (avec RLHF)
response = chatgpt(prompt)
# → "Je ne peux pas aider avec ça. Les virus causent..." ✅
→ RLHF a appris à ChatGPT de refuser les requêtes dangereuses
Dans ce guide, vous comprendrez :
- ✅ Les 3 étapes du RLHF (SFT, Reward Model, RL)
- ✅ Comment fonctionne un reward model
- ✅ Pourquoi ça marche (et pourquoi parfois ça échoue)
- ✅ Limites et alternatives (RLAIF, Constitutional AI)
- ✅ Comment implémenter RLHF (code Python)
Les 3 étapes du RLHF
Vue d’ensemble
┌─────────────────────────────────────────────────────┐
│ RLHF PIPELINE │
├─────────────────────────────────────────────────────┤
│ │
│ ÉTAPE 1 : Supervised Fine-Tuning (SFT) │
│ ┌──────────────────────────────────────┐ │
│ │ Base Model (GPT-3) │ │
│ │ + │ │
│ │ High-Quality Demonstrations │ │
│ │ (Human-written conversations) │ │
│ │ → │ │
│ │ SFT Model │ │
│ └──────────────────────────────────────┘ │
│ ↓ │
│ ÉTAPE 2 : Reward Model Training │
│ ┌──────────────────────────────────────┐ │
│ │ SFT Model génère multiples réponses │ │
│ │ Humains rankent : A > B > C │ │
│ │ → │ │
│ │ Reward Model (prédit préférences) │ │
│ └──────────────────────────────────────┘ │
│ ↓ │
│ ÉTAPE 3 : Reinforcement Learning (PPO) │
│ ┌──────────────────────────────────────┐ │
│ │ SFT Model + Reward Model │ │
│ │ RL optimise pour max reward │ │
│ │ → │ │
│ │ Final Model (ChatGPT) │ │
│ └──────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────┘
Supervised Fine-Tuning (SFT)
Objectif
Transformer un LLM de complétion de texte en assistant conversationnel.
Processus
1. Collecter conversations de qualité
# Exemple de données SFT
sft_data = [
{
"messages": [
{"role": "user", "content": "Qu'est-ce que Python ?"},
{"role": "assistant", "content": "Python est un langage de programmation..."}
]
},
{
"messages": [
{"role": "user", "content": "Comment installer une bibliothèque ?"},
{"role": "assistant", "content": "Utilisez pip install <nom_bibliothèque>"}
]
},
# ... 10K-100K exemples écrits par humains experts
]
Caractéristiques :
- ✅ Haute qualité : Rédigées par experts (linguistes, spécialistes domaine)
- ✅ Diverses : Couvrent multiples topics et styles
- ✅ Instructives : Format question-réponse utile
2. Fine-tuner le modèle base
from transformers import Trainer, TrainingArguments
# Charger modèle base
base_model = AutoModelForCausalLM.from_pretrained("gpt-3-175b")
# Training arguments
training_args = TrainingArguments(
output_dir="sft_model",
num_train_epochs=3,
learning_rate=2e-5,
per_device_train_batch_size=4,
)
# Trainer
trainer = Trainer(
model=base_model,
args=training_args,
train_dataset=sft_dataset,
)
# Fine-tune
trainer.train()
# → SFT Model : Converse mieux, mais pas encore "aligné"
Résultat
Avant SFT (GPT-3 base) :
User: Comment faire un gâteau ?
GPT-3: au chocolat ou à la vanille. Les ingrédients incluent...
# (Complète le texte, pas vraiment une réponse)
Après SFT :
User: Comment faire un gâteau ?
SFT Model: Voici une recette simple :
1. Préchauffez le four à 180°C
2. Mélangez 200g de farine...
# (Répond de manière structurée et utile) ✅
→ Mais le modèle peut encore générer contenu toxique ou incorrect
Reward Model Training
Objectif
Créer un modèle qui prédit quelles réponses les humains préfèrent.
Processus
1. Générer multiples réponses
prompt = "Explique la photosynthèse simplement"
# Générer 4 réponses différentes (sampling)
responses = [
sft_model.generate(prompt, temperature=0.7) for _ in range(4)
]
# Responses:
# A: "La photosynthèse est le processus où les plantes..."
# B: "C'est quand les plantes mangent la lumière du soleil..."
# C: "Photo = lumière, synthèse = créer. Les plantes créent..."
# D: "Je ne sais pas vraiment, désolé."
2. Humains rankent les réponses
Évaluateur humain classe :
C > A > B > D
Justification :
- C : Simple, mémorable, correct ✅
- A : Correct mais un peu sec
- B : Trop simplifié, imprécis
- D : Pas utile
3. Créer dataset de préférences
preference_data = [
{
"prompt": "Explique la photosynthèse simplement",
"chosen": "Photo = lumière, synthèse = créer...", # Response C
"rejected": "Je ne sais pas vraiment..." # Response D
},
# ... 50K-100K comparaisons
]
4. Entraîner Reward Model
from transformers import AutoModelForSequenceClassification
# Reward model (base sur même architecture que SFT)
reward_model = AutoModelForSequenceClassification.from_pretrained(
"sft_model",
num_labels=1 # Prédit score scalar
)
# Objectif : reward(chosen) > reward(rejected)
def reward_loss(chosen, rejected):
score_chosen = reward_model(chosen)
score_rejected = reward_model(rejected)
# Maximize margin
loss = -log(sigmoid(score_chosen - score_rejected))
return loss
# Entraînement
for batch in preference_data:
loss = reward_loss(batch["chosen"], batch["rejected"])
loss.backward()
optimizer.step()
# → Reward Model : Prédit score de qualité d'une réponse
Utilisation
Test du Reward Model :
prompt = "Qu'est-ce que l'IA ?"
response_good = "L'IA est l'intelligence artificielle..."
response_bad = "IA = robots qui vont nous tuer"
score_good = reward_model(prompt + response_good) # → 8.2
score_bad = reward_model(prompt + response_bad) # → 2.1
# Reward model préfère response_good ✅
Reinforcement Learning (PPO)
Objectif
Optimiser le SFT model pour maximiser le reward selon le Reward Model.
Algorithme : PPO (Proximal Policy Optimization)
Principe : Itérativement améliorer le modèle en générant réponses et en ajustant selon reward.
Loop :
for episode in range(num_episodes):
# 1. Générer réponse avec policy actuelle
prompt = sample_prompt()
response = policy_model.generate(prompt)
# 2. Calculer reward
reward = reward_model(prompt + response)
# 3. Update policy pour augmenter reward
# (PPO : gradient ascent sur expected reward)
loss = -reward * log_prob(response)
loss.backward()
optimizer.step()
# 4. KL divergence constraint
# (Éviter que le modèle s'éloigne trop du SFT model)
kl_penalty = KL(policy_model || sft_model)
final_loss = loss + beta * kl_penalty
Détails techniques :
import torch
from transformers import AutoModelForCausalLM
class RLHFTrainer:
def __init__(self, sft_model, reward_model, beta=0.1):
self.policy_model = sft_model.copy() # Policy à optimiser
self.ref_model = sft_model # Reference (frozen)
self.reward_model = reward_model
self.beta = beta # KL penalty weight
def compute_rewards(self, prompts, responses):
"""Calcule rewards pour batch de réponses"""
rewards = []
for prompt, response in zip(prompts, responses):
# Reward du reward model
r = self.reward_model(prompt + response)
# Pénalité KL (rester proche du ref model)
kl = self.compute_kl(prompt, response)
final_reward = r - self.beta * kl
rewards.append(final_reward)
return torch.tensor(rewards)
def compute_kl(self, prompt, response):
"""KL divergence entre policy et ref"""
with torch.no_grad():
log_probs_ref = self.ref_model.log_prob(prompt, response)
log_probs_policy = self.policy_model.log_prob(prompt, response)
kl = (log_probs_policy - log_probs_ref).mean()
return kl
def train_step(self, batch_prompts):
"""Un step de training RL"""
# Générer réponses
responses = [
self.policy_model.generate(p) for p in batch_prompts
]
# Calculer rewards
rewards = self.compute_rewards(batch_prompts, responses)
# PPO loss
log_probs = self.policy_model.log_prob_batch(batch_prompts, responses)
loss = -(rewards * log_probs).mean()
# Backprop
loss.backward()
self.optimizer.step()
return loss.item()
Pourquoi KL Constraint ?
Sans contrainte : Le modèle peut “gamer” le reward model
# Reward model préfère réponses polies
# Policy découvre que répéter "merci" = high reward
response = "Merci " * 1000 # Reward: 9.9 ! ❌
# Mais c'est pas utile...
Avec KL constraint : Le modèle reste proche du SFT model (qui est sensé)
# Pénalité si trop différent du SFT model
kl_penalty = KL(policy || sft_model)
# "Merci" × 1000 est très différent de SFT → high KL → pénalisé ✅
Résultat final
Après RL (ChatGPT) :
User: Comment créer un virus ?
ChatGPT: Je ne peux pas aider avec ça. Créer des virus est illégal...
# (Reward model a appris que refuser = high reward) ✅
User: Qu'est-ce que Python ?
ChatGPT: Python est un langage de programmation interprété...
# (Utile, informatif, bien structuré) ✅
Pourquoi ça marche (et limitations)
Pourquoi ça marche
1. Capture des préférences humaines
Les humains savent intuitivement ce qui est “bon” :
- Utile > Pas utile
- Honnête > Mensonger
- Sûr > Dangereux
→ Reward model apprend ces préférences
2. Scalable
Comparer 2 réponses (A > B) est plus facile que de rédiger une réponse parfaite :
- Écrire 100K conversations : Très coûteux
- Ranker 100K paires : Plus rapide et moins cher
3. Itératif
RL continue d’améliorer le modèle au fil des épisodes
Limitations
1. Subjective et biaisée
Les préférences humaines varient :
- Évaluateur A préfère réponses concises
- Évaluateur B préfère réponses détaillées
→ Reward model apprend un “consensus” qui peut ne pas convenir à tous
2. Reward Hacking
Le modèle peut exploiter failles du reward model :
# Reward model préfère réponses longues
response = "Voici une explication détaillée. " + filler_text * 100
# → High reward mais pas vraiment utile ❌
3. Coûteux
- Collecte 100K préférences humaines : $50K-200K
- Training RL : Compute intensif (GPUs, semaines)
4. Alignment Tax
RLHF peut réduire légèrement les capacités du modèle :
# GPT-4 base : Excellent en math
# GPT-4 après RLHF : Bon en math (légère régression)
# Trade-off : Safety vs Raw Performance
5. Difficile de spécifier objectif
Comment définir “bon” comportement ?
- Être utile… mais pas pour activités illégales
- Être créatif… mais pas inventer des faits
- Être engageant… mais pas manipulateur
→ Nuances complexes difficiles à capturer dans reward
Alternatives et évolutions
RLAIF (RL from AI Feedback)
Problème RLHF : Feedback humain coûteux et lent
Solution RLAIF : Utiliser un LLM pour évaluer les réponses
# Au lieu d'humains
response_A = model.generate(prompt)
response_B = model.generate(prompt)
# LLM évaluateur
evaluator_prompt = f"""
Prompt: {prompt}
Response A: {response_A}
Response B: {response_B}
Quelle réponse est meilleure ? Explique pourquoi.
Réponds : A ou B
"""
judgment = evaluator_llm(evaluator_prompt) # → "A car plus précis..."
# Créer dataset de préférences
preference = {
"prompt": prompt,
"chosen": response_A,
"rejected": response_B
}
# Entraîner reward model comme avant
Avantages :
- ✅ Moins cher ($10K vs $100K)
- ✅ Plus rapide (heures vs semaines)
- ✅ Scalable à l’infini
Limites :
- ⚠️ Biais du LLM évaluateur
- ⚠️ Peut amplifier erreurs
Utilisé par : Claude 3 (Constitutional AI), Llama 2
Constitutional AI (Anthropic)
Extension de RLAIF : LLM s’auto-critique selon “constitution”
# Génération
response = model.generate(prompt)
# Auto-critique selon principes
critique_prompt = f"""
Response: {response}
Principes :
1. Être utile sans nuire
2. Être honnête
3. Respecter la vie privée
La réponse respecte-t-elle ces principes ? Critique constructive.
"""
critique = model(critique_prompt)
# Révision
revision_prompt = f"""
Response originale : {response}
Critique : {critique}
Révise la réponse pour respecter les principes.
"""
revised_response = model(revision_prompt)
# → Response améliorée ✅
Avantages :
- ✅ Transparent (principes explicites)
- ✅ Pas besoin d’humains
- ✅ Adaptable (changer principes)
DPO (Direct Preference Optimization)
Problème RLHF : Complexe (3 étapes, reward model séparé, RL instable)
Solution DPO : Optimiser directement sur préférences, sans reward model
# Objectif : Maximiser log-likelihood de chosen vs rejected
loss = -log(sigmoid(
log_prob(chosen) - log_prob(rejected)
))
# Training direct sur dataset de préférences
for batch in preference_data:
chosen_logits = model(batch["chosen"])
rejected_logits = model(batch["rejected"])
loss = -torch.log(torch.sigmoid(chosen_logits - rejected_logits))
loss.backward()
optimizer.step()
# → Modèle aligné, sans reward model ✅
Avantages :
- ✅ Plus simple (1 étape vs 3)
- ✅ Plus stable (pas de RL)
- ✅ Moins de compute
Utilisé par : Mistral, Zephyr
Implémenter RLHF (code complet)
Setup
pip install transformers datasets trl accelerate peft
SFT
from datasets import load_dataset
from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer
# Charger dataset
dataset = load_dataset("timdettmers/openassistant-guanaco", split="train")
# Modèle et tokenizer
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf")
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf")
# SFT
from trl import SFTTrainer
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset,
max_seq_length=512,
)
trainer.train()
trainer.save_model("llama-2-7b-sft")
Reward Model
from transformers import AutoModelForSequenceClassification
# Charger dataset de préférences
pref_dataset = load_dataset("Anthropic/hh-rlhf", split="train")
# Reward model
reward_model = AutoModelForSequenceClassification.from_pretrained(
"llama-2-7b-sft",
num_labels=1
)
from trl import RewardTrainer
reward_trainer = RewardTrainer(
model=reward_model,
tokenizer=tokenizer,
train_dataset=pref_dataset,
)
reward_trainer.train()
reward_trainer.save_model("reward-model")
RL (PPO)
from trl import PPOTrainer, PPOConfig, AutoModelForCausalLMWithValueHead
# Config
ppo_config = PPOConfig(
model_name="llama-2-7b-sft",
learning_rate=1e-5,
batch_size=16,
)
# Policy model (avec value head pour PPO)
policy_model = AutoModelForCausalLMWithValueHead.from_pretrained("llama-2-7b-sft")
# Trainer PPO
ppo_trainer = PPOTrainer(
config=ppo_config,
model=policy_model,
ref_model=None, # Will use frozen copy
tokenizer=tokenizer,
reward_model=reward_model,
)
# Training loop
for prompt in prompts:
# Générer réponse
response = policy_model.generate(prompt)
# Calculer reward
reward = reward_model(prompt + response)
# PPO step
ppo_trainer.step([prompt], [response], [reward])
policy_model.save_pretrained("llama-2-7b-rlhf")
Ressources complémentaires
Articles liés :
Papers fondateurs :
- InstructGPT Paper (OpenAI, 2022)
- Constitutional AI (Anthropic, 2022)
- DPO Paper (2023)
Libraries :
- TRL (HuggingFace)
- OpenRLHF
- DeepSpeed-Chat
Conclusion
RLHF est la technique qui a permis aux LLMs de passer de “générateurs de texte” à “assistants utiles et sûrs” :
Clés du succès :
- SFT : Apprendre format conversationnel
- Reward Model : Capturer préférences humaines
- RL (PPO) : Optimiser pour maximiser reward
Évolutions :
- RLAIF : AI feedback au lieu d’humains (moins cher)
- Constitutional AI : Auto-critique et révision
- DPO : Simplification (sans reward model)
Le futur : Alignment continu, multi-objectif (safety + performance), et participation plus large (au-delà d’annotateurs payés).