Introduction
En 2026, aligner les grands modèles de langage (LLM) avec les préférences humaines est essentiel pour des réponses utiles et sûres. Direct Preference Optimization (DPO) révolutionne cela : contrairement à RLHF (Reinforcement Learning from Human Feedback), DPO évite l'entraînement d'un modèle de récompense séparé, rendant le processus plus simple, stable et efficace.
Imaginez : au lieu d'optimiser une récompense proxy, DPO optimise directement la politique du LLM via une fonction de perte basée sur des paires 'choisie/rejetée'. Cela réduit les coûts computationnels de 50-70% tout en surpassant souvent RLHF sur des benchmarks comme MT-Bench.
Ce tutoriel beginner vous guide pour implémenter DPO sur GPT-2 avec un dataset réel (stack-exchange-paired). À la fin, vous aurez un modèle aligné, prêt pour l'inférence. Parfait pour les développeurs IA voulant maîtriser l'alignement sans PhD. Temps estimé : 30min sur GPU, 2h sur CPU.
Prérequis
- Python 3.10+ installé (utilisez pyenv pour la gestion).
- Environnement virtuel :
python -m venv dpo-env && source dpo-env/bin/activate. - GPU NVIDIA recommandé (CUDA 12+), sinon CPU ok pour ce demo petit.
- Accès internet pour télécharger datasets/modèles Hugging Face.
- Connaissances basiques en Python et PyTorch (pas de ML avancé requis).
Installation des dépendances
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
pip install transformers==4.44.0 datasets==2.21.0
pip install trl==0.9.6 accelerate==0.33.0 peft==0.12.0
pip install bitsandbytes==0.43.1 safetensors==0.4.3Ce script installe PyTorch avec CUDA (adaptez l'index-url pour CPU si besoin), puis les libs essentielles : Transformers pour les modèles, Datasets pour les données, TRL pour DPOTrainer, et Accelerate/PEFT pour l'optimisation. Bitsandbytes active la quantization 8-bit pour économiser la VRAM. Lancez-le une seule fois ; vérifiez avec pip list.
Comprendre les bases de DPO
DPO repose sur une astuce mathématique élégante : la perte est dérivée directement de la préférence humaine, sans reward model. Pour une paire (prompt, réponse_choisie, réponse_rejetée), la formule est :
$$\mathcal{L}_{DPO} = -\mathbb{E} \log \sigma \left( \beta \log \frac{\pi_\theta (y_w | x)}{\pi_{ref} (y_w | x)} - \beta \log \frac{\pi_\theta (y_l | x)}{\pi_{ref} (y_l | x)} \right)$$
Où $\pi_\theta$ est votre modèle, $\pi_{ref}$ un modèle de référence (souvent un clone), $\beta$ un hyperparamètre (0.1-1.0), $y_w$ choisie, $y_l$ rejetée. Analogie : comme choisir le meilleur chemin sans carte des récompenses, juste en comparant deux routes.
Nous utilisons le dataset 'lvwerra/stack-exchange-paired' : 3.3k paires prompt/choisi/rejeté de Stack Exchange, parfait pour beginner.
Chargement et préparation du dataset
from datasets import load_dataset
import torch
# Charger le dataset DPO prêt-à-l'emploi
dataset = load_dataset("lvwerra/stack-exchange-paired", split="train")
# Prendre un petit sous-ensemble pour test rapide (1000 exemples)
dataset = dataset.shuffle(seed=42).select(range(1000))
# Vérifier la structure
dataset[0]
print(f"Dataset size: {len(dataset)}")
print("Exemple prompt:", dataset[0]['prompt'])
print("Exemple choisi:", dataset[0]['chosen'])
print("Exemple rejeté:", dataset[0]['rejected'])
# Sauvegarder localement pour réutilisation
dataset.save_to_disk("./dpo_dataset")
print("Dataset prêt ! Exécutez ce script d'abord.")Ce code charge un dataset spécialisé DPO avec des paires naturelles issues de Stack Exchange. On limite à 1000 exemples pour un training rapide (5-10min sur GPU). La structure est standard : 'prompt', 'chosen', 'rejected'. Exécutez-le en premier ; il sauvegarde localement pour éviter re-téléchargements.
Chargement du modèle et tokenizer
Pourquoi GPT-2 ? Modèle lightweight (124M params), rapide pour débutants, mais DPO scale à des milliards. Le tokenizer gère le padding ; on set pad_token à eos_token pour éviter les warnings.
Astuce : Utilisez device_map="auto" avec Accelerate pour multi-GPU/CPU transparent.
Initialisation du modèle de base
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
from datasets import load_from_disk
# Charger tokenizer et modèle base (GPT-2 petit)
model_name = "gpt2"
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "left"
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.bfloat16,
device_map="auto",
trust_remote_code=True
)
# Charger dataset préparé
dataset = load_from_disk("./dpo_dataset")
print("Modèle et dataset chargés. Prêt pour DPO.")
print(f"Modèle params: {model.num_parameters():,}")
print(f"Vocab size: {tokenizer.vocab_size}")Charge GPT-2 en bfloat16 pour précision/économie mémoire. device_map="auto" déplace automatiquement sur GPU/CPU. Padding à gauche est crucial pour causal LM en DPO (prompt avant réponse). Exécutez après prepare_dataset.py.
Configuration du trainer DPO
DPOTrainer (de TRL) gère tout : perte, référence (approximée via beta), LoRA via PEFT pour fine-tuning efficace (seulement 1-5% params entraînés). Hyperparams : beta=0.1 (standard), epochs=1 (pour demo), batch=4 (adaptez à VRAM).
Setup du DPOTrainer avec LoRA
from trl import DPOConfig, DPOTrainer
from transformers import TrainingArguments, AutoTokenizer
from peft import LoraConfig, get_peft_model, TaskType
from datasets import load_from_disk
import torch
# Charger tokenizer, model, dataset (de scripts précédents)
model_name = "gpt2"
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "left"
model = AutoModelForCausalLM.from_pretrained(
model_name, torch_dtype=torch.bfloat16, device_map="auto"
)
dataset = load_from_disk("./dpo_dataset")
# Config LoRA pour efficiency (r=16, alpha=32)
peft_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16,
lora_alpha=32,
lora_dropout=0.1,
target_modules=["c_attn", "c_proj"]
)
model = get_peft_model(model, peft_config)
# Config DPO
beta = 0.1
dpo_config = DPOConfig(
beta=beta,
output_dir="./dpo-gpt2",
max_length=256,
max_prompt_length=128,
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
num_train_epochs=1,
learning_rate=1e-5,
logging_steps=10,
save_steps=100,
remove_unused_columns=False,
)
trainer = DPOTrainer(
model=model,
ref_model=None, # Auto-approximation
args=dpo_config,
train_dataset=dataset,
tokenizer=tokenizer,
)
print("Trainer configuré. Lancez trainer.train() ensuite.")
trainer.model.print_trainable_parameters()LoRA réduit la VRAM (de 1GB à 200MB). DPOConfig définit les hyperparams ; ref_model=None utilise l'approximation beta pour simplicité (pas de clone). Batch effectif=8 avec accumulation. print_trainable_parameters() confirme : ~1% params actifs.
Lancement de l'entraînement DPO
from setup_trainer import trainer # Import du trainer configuré
# Lancer l'entraînement
train_result = trainer.train()
# Sauvegarder le modèle fine-tuné
trainer.save_model("./dpo-gpt2-final")
trainer.tokenizer.save_pretrained("./dpo-gpt2-final")
print("Entraînement terminé ! Modèle sauvé dans ./dpo-gpt2-final")
print(f"Résultats: {train_result.metrics}")Ce script final lance le training (1 epoch ~5min sur A10G). Sauvegarde automatique + manuelle. Metrics incluent loss DPO descendante. Importez depuis setup_trainer.py (ou copiez le code complet dans un seul fichier pour prod).
Test et inférence sur le modèle DPO
Après training, testez avec un prompt Stack Exchange-like. Comparez avant/après pour voir l'alignement : réponses plus utiles, moins verbeuses.
Inférence et comparaison modèles
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
# Charger modèle DPO fine-tuné
model_path = "./dpo-gpt2-final"
tokenizer = AutoTokenizer.from_pretrained(model_path)
model = AutoModelForCausalLM.from_pretrained(
model_path, torch_dtype=torch.bfloat16, device_map="auto"
)
prompt = "Question: What is the best way to learn Python? Answer:"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
outputs = model.generate(**inputs, max_new_tokens=50, do_sample=True, temperature=0.7)
response_dpo = tokenizer.decode(outputs[0], skip_special_tokens=True)
print("Réponse DPO:", response_dpo[len(prompt):])
# Comparaison base (téléchargez gpt2 si besoin)
model_base = AutoModelForCausalLM.from_pretrained("gpt2", torch_dtype=torch.bfloat16, device_map="auto")
outputs_base = model_base.generate(**inputs, max_new_tokens=50, do_sample=True, temperature=0.7)
response_base = tokenizer.decode(outputs_base[0], skip_special_tokens=True)
print("Réponse base:", response_base[len(prompt):])Génère 50 tokens avec sampling. Comparez : DPO favorise réponses concises/pertinentes. do_sample=True pour variabilité ; ajustez temperature. Fonctionnel standalone après training.
Bonnes pratiques
- Choisissez beta judicieusement : 0.1 pour datasets propres, 0.5+ si noisy (testez sur val set).
- Utilisez toujours LoRA/QLORA : Économisez 90% VRAM, scale à Llama-7B sur 16GB.
- Validez dataset : Vérifiez len(chosen) > len(rejected) pour cohérence préférentielle.
- Monitor loss : Si >1.0 après epoch1, réduisez LR ou augmentez beta.
- Push HF Hub :
trainer.push_to_hub("votre-nom/dpo-gpt2")pour sharing.
Erreurs courantes à éviter
- Padding mal configuré : Oublier
padding_side="left"cause logits erronés (perte infinie). - Ref model manquant : Sans beta ou ref=None, approximation foire ; clonez model pour prod.
- Batch trop grand : OOM error ? Réduisez per_device_batch_size à 1 + accumulation_steps=8.
- Dataset non-formaté : Assurez 'prompt','chosen','rejected' exacts, sans NaN.
Pour aller plus loin
- Papier original : Direct Preference Optimization (Rafailov et al., 2023).
- Repo TRL : huggingface/trl pour exemples avancés (SFT+DPO).
- Datasets : UltraFeedback.
- Scalez à Llama : Remplacez "gpt2" par "meta-llama/Llama-3-8B" + login HF.