PT-05 — Reinforcement Learning with Verifiable Rewards (RLVR)

Serie : Post-Training SOTA 2024-2025 (Epic #1742) Pre-requis : PT-01 (intro), PT-04 (GRPO) fortement recommande Objectifs pedagogiques : 1. Comprendre la différence fondamentale entre reward heuristique et reward verifiable 2. Implementer un verifier mathematique exact avec SymPy 3. Construire un pipeline RLVR sur des problemes de type GSM8K 4. Mesurer la parcimonie du reward verifiable — et comprendre pourquoi c’est elle qui gouverne l’emergence de raisonnement (chain-of-thought), phenomene rapporte a grande echelle mais que ce notebook n’observe pas a la sienne 5. Comparer GRPO heuristique vs GRPO verifiable

Ce notebook mesure, il ne postule pas. L’entrainement est reellement execute sur GPU et le notebook rapporte ses chiffres tels quels, y compris quand ils sont defavorables : accuracy avant/apres avec le meme instrument, delta obtenu, et part des completions effectivement verifiees correctes. Un delta nul ou negatif sur 10 problemes et un seul seed est un resultat attendu, pas un echec a maquiller.

References cles : - Lightman et al., “Let’s Verify Step by Step” (OpenAI, 2023) — process vs outcome reward - Deepseek-AI, “DeepSeek-R1” (2025) — RLVR for reasoning emergence - Cobbe et al., “Training Verifiers to Solve Math Word Problems” (GSM8K, 2021)

1. Pourquoi les rewards verifiables changent tout

Le problème des rewards heuristiques

En PT-04, nous avons utilise des reward functions heuristiques (longueur, mots-cles). Ces rewards sont bruites : - Un modèle peut maximiser keyword_reward en repetant “therefore” sans raisonner - La longueur ne garantit pas la qualite du raisonnement - Le modèle apprend a “gamer” la metric, pas a raisonner

La solution : rewards verifiables

Un reward verifiable est une fonction qui determine exactement si une reponse est correcte :

Type de reward Source Bruit Exemple
Heuristique Règle approximative Eleve keyword_reward, length_reward
Reward model Reseau entraine Moyen RM entraine sur préférences humaines
Verifiable Verifier exact Zero SymPy evaluation, exec sandbox, unit tests

Pourquoi le zero-bruit matter

Avec un reward fondé sur une vérification exacte (le verifier compare la valeur extraite au ground truth, aucune règle approximative), le gradient n’est pas pollué par du bruit de mesure : - Le modèle qui repond correctement recoit le reward plein (1.0) - Une reponse fausse n’est jamais comptee correcte : la justesse se mesure, elle ne s’estime pas - Le modele ne peut pas gamer la mesure (au mieux, il produit une reponse juste)

Le verifier de ce notebook est un peu plus doux qu’un 0/1 pur : une reponse extraite mais fausse rapporte 0.5 — un credit de format, pas de justesse (section 3).

C’est ce qui a permis a Deepseek-R1 de faire emerger du raisonnement spontane : le RL explore librement car il n’est pas contraint par les biais d’un reward model.

2. GSM8K — le benchmark de reference pour le raisonnement mathematique

GSM8K (Grade School Math 8K) est un dataset de 8 500 problemes mathematiques de niveau college. Chaque problème a une reponse numérique unique et verifiable.

Exemples :

Problème Reponse Verification
“Janet a 16 oeufs. Elle en perd 3 et en achete 6.” 19 \(16 - 3 + 6 = 19\)
“Un train a 120 passagers. 35 montent, 12 descendent.” 143 \(120 + 35 - 12 = 143\)
“Prix initial $80, reduction 25%, puis taxe 10%.” $66 \(80 \times 0.75 \times 1.10 = 66\)

Pourquoi GSM8K est ideal pour RLVR : 1. Reponse exacte : un seul nombre, pas de subjectivite 2. Raisonnement multi-étapes : necessite 2-8 opérations 3. Pas de knowledge levele : arithmetique simple, le defi est la structure 4. Scale : 7 473 train + 1 319 test = suffisant pour demo

Approche outcome vs process reward

Aspect Outcome Reward Process Reward
Verifie Reponse finale uniquement Chaque étape du raisonnement
Implementtation Simple (parse reponse) Complexe (verifier chaque step)
Signal Sparse (1 signal par completion) Dense (1 signal par étape)
Deepseek-R1 Outcome reward Emergent (pas explicit)

Ce notebook utilise outcome reward (verification de la reponse finale).

import sys
import platform

print(f"Python : {sys.version}")
print(f"Plateforme : {platform.platform()}")

# Détection CUDA (correction : CUDA_AVAILABLE était référencé plus bas sans être défini)
try:
    import torch
    CUDA_AVAILABLE = torch.cuda.is_available()
except ImportError:
    CUDA_AVAILABLE = False
print(f"Accélérateur CUDA détecté : {CUDA_AVAILABLE}")

# Mode smoke : 2 steps pour valider la plomberie en moins de 2 min
SMOKE_TEST = False
# Par défaut, le notebook reste exécutable sur CPU ; le vrai training exige CUDA.
LOAD_MODEL_AND_TRAIN = True
print(f"\nLOAD_MODEL_AND_TRAIN = {LOAD_MODEL_AND_TRAIN} | SMOKE_TEST = {SMOKE_TEST}")
if not CUDA_AVAILABLE:
    print("Les cellules d'entraînement GPU seront ignorées ; les tests CPU restent exécutés.")
elif SMOKE_TEST:
    print("Mode SMOKE : 2 steps, validation de plomberie uniquement.")
Python : 3.12.13 | packaged by Anaconda, Inc. | (main, Mar 19 2026, 20:16:45) [MSC v.1942 64 bit (AMD64)]
Plateforme : Windows-11-10.0.26200-SP0
Accélérateur CUDA détecté : False

LOAD_MODEL_AND_TRAIN = True | SMOKE_TEST = False
Les cellules d'entraînement GPU seront ignorées ; les tests CPU restent exécutés.

3. Construction du verifier mathematique avec SymPy

Le verifier prend une completion textuelle, extrait la reponse numérique, et la compare avec la ground truth.

Étapes : 1. Extract : trouver le dernier nombre dans la completion 2. Compare : verifier si extract == ground_truth (avec tolerance pour arrondis) 3. Reward : 1.0 si correct, 0.0 si incorrect

import re
import sympy
from typing import Optional

# Token numerique tolerant (#12438) : '$1,234.00', '42 km', '22.2 C', '0.9999999'.
_NUM_TOKEN = r"-?\s*[$£€]?\s*\d{1,3}(?:,\d{3})+(?:\.\d+)?|-?\s*[$£€]?\s*\d+(?:\.\d+)?"


def _to_float(token: str) -> Optional[float]:
    # Ne garde que chiffres, point, signe : '$1,234.00 km' -> 1234.00
    s = re.sub(r"[^0-9.\-]", "", token)
    if not s or s in ("-", ".", "-."):
        return None
    try:
        return float(s)
    except ValueError:
        return None


def extract_answer(completion: str) -> Optional[float]:
    # Extrait la derniere valeur numerique d'une completion, en tolerants la
    # devise ($, £, EUR), les separateurs de milliers ('1,234.00') et les unites
    # ('42 km', '22.2 C'). Priorite : \boxed{}, #### (GSM8K), 'answer is'/':'/'=',
    # puis fallback dernier nombre (#12438).
    boxed = re.findall(r"\\boxed\{([^}]+)\}", completion)
    if boxed:
        try:
            return float(sympy.sympify(boxed[-1].strip()))
        except (ValueError, sympy.SympifyError, TypeError):
            value = _to_float(boxed[-1])
            if value is not None:
                return value

    for pattern in (r"####\s*(" + _NUM_TOKEN + r")",
                    r"(?:answer is|answer:|=)\s*(" + _NUM_TOKEN + r")"):
        found = re.findall(pattern, completion, re.IGNORECASE)
        if found:
            value = _to_float(found[-1])
            if value is not None:
                return value

    numbers = re.findall(_NUM_TOKEN, completion)
    if numbers:
        return _to_float(numbers[-1])
    return None


def math_verifier_reward(completion: str, ground_truth: float,
                         abs_tol: float = 1e-6, rel_tol: float = 1e-3) -> float:
    """Reward verifiable avec credit partiel (#12438) :
    1.0 correct (tolerance absolue OU relative), 0.5 reponse extraite mais
    fausse (format respecte, justesse non), 0.0 rien d'extractible.
    """
    predicted = extract_answer(completion)
    if predicted is None:
        return 0.0
    if abs(predicted - ground_truth) <= max(abs_tol, rel_tol * abs(ground_truth)):
        return 1.0
    return 0.5


# Tests unitaires du verifier — cas pieges inclus (#12438)
print("Tests du verifier mathematique :")
print(f"  extract_answer('The answer is 42') = {extract_answer('The answer is 42')}")
print(f"  extract_answer('#### 19') = {extract_answer('#### 19')}")
# Litteral hoiste hors de la f-string : un backslash dans une expression
# de f-string est une SyntaxError avant Python 3.12 (PEP 701), or le socle
# declare du depot est Python 3.10+.
_boxed_demo = "\\boxed{3.14}"
print(f"  extract_answer('{_boxed_demo}') = {extract_answer(_boxed_demo)}")
print("--- cas pieges (#12438) ---")
print(f"  extract_answer('The answer is $1,234.00') = {extract_answer('The answer is $1,234.00')}  (attendu 1234.0)")
print(f"  math_verifier_reward('$1,234.00', 1234) = {math_verifier_reward('The answer is $1,234.00', 1234)}  (normalisation devise + milliers)")
print(f"  math_verifier_reward('0.9999999', 1) = {math_verifier_reward('The answer is 0.9999999', 1)}  (tolerance relative 1e-3)")
print(f"  math_verifier_reward('42 km', 42) = {math_verifier_reward('The answer is 42 km', 42)}  (unite toleree)")
print("--- credit partiel ---")
print(f"  math_verifier_reward('The answer is 42', 42) = {math_verifier_reward('The answer is 42', 42)}")
print(f"  math_verifier_reward('The answer is 43', 42) = {math_verifier_reward('The answer is 43', 42)}  (extrait mais faux : 0.5)")
print(f"  math_verifier_reward('Je ne sais pas', 42) = {math_verifier_reward('Je ne sais pas', 42)}  (rien d'extractible : 0.0)")
print(f"  math_verifier_reward('The answer is 66.0', 66) = {math_verifier_reward('The answer is 66.0', 66)}")
print()
print("Verifier mathematique pret.")
Tests du verifier mathematique :
  extract_answer('The answer is 42') = 42.0
  extract_answer('#### 19') = 19.0
  extract_answer('\boxed{3.14}') = 3.14
--- cas pieges (#12438) ---
  extract_answer('The answer is $1,234.00') = 1234.0  (attendu 1234.0)
  math_verifier_reward('$1,234.00', 1234) = 1.0  (normalisation devise + milliers)
  math_verifier_reward('0.9999999', 1) = 1.0  (tolerance relative 1e-3)
  math_verifier_reward('42 km', 42) = 1.0  (unite toleree)
--- credit partiel ---
  math_verifier_reward('The answer is 42', 42) = 1.0
  math_verifier_reward('The answer is 43', 42) = 0.5  (extrait mais faux : 0.5)
  math_verifier_reward('Je ne sais pas', 42) = 0.0  (rien d'extractible : 0.0)
  math_verifier_reward('The answer is 66.0', 66) = 1.0

Verifier mathematique pret.

Exercice 1 : Etendre le parser d’extraction de reponses

Le verifier actuel supporte les formats \boxed{}, #### et “The answer is X”. L’objectif est d’etendre extract_answer pour supporter des formats supplementaires frequents dans les outputs de modèles : “So the result is X”, reponses entre parentheses “(42)”, et notation scientifique “3.14e2”.

Objectif : ajouter au moins deux nouveaux patterns de detection a la fonction extract_answer existante.

Indices : - # Étape 1 : Ajouter un pattern regex pour “So the result is X” et “(X)” - # Étape 2 : Gerer la conversion de notation scientifique via float() natif - # Indice : inserer les nouveaux patterns avant le fallback “dernier nombre” pour eviter les conflits

def extract_answer_v2(completion: str) -> float:
    # TODO etudiant : etendre le parser avec de nouveaux patterns
    # Reutiliser la logique de extract_answer et ajouter :
    
    # Pattern "So the result is X" / "the result is X"
    result_pattern = None  # TODO etudiant : regex pour "result is" suivi d'un nombre
    
    # Pattern entre parentheses "(42)"
    paren_pattern = None  # TODO etudiant : regex pour nombre entre parentheses
    
    # Notation scientifique (deja gere par float(), mais verifier le pattern)
    sci_pattern = None  # TODO etudiant : regex optionnel
    
    return None  # TODO etudiant : retourner le nombre trouve ou None

print("Exercice a completer : parser etendu")
Exercice a completer : parser etendu

4. Dataset GSM8K sample

Pour le demo pedagogique, on construit un mini-dataset de 10 problemes GSM8K-like avec ground truths verifiables.

# Mini-dataset GSM8K-like (10 problemes avec ground truth exact)
GSM8K_SAMPLE = [
    {
        "prompt": "Janet has 16 eggs. She breaks 3 eggs while cooking, then buys 6 more eggs at the store. How many eggs does Janet have now?",
        "answer": 19.0,
    },
    {
        "prompt": "A train has 120 passengers. At the first stop, 35 passengers board and 12 get off. How many passengers are on the train now?",
        "answer": 143.0,
    },
    {
        "prompt": "A shirt costs $80. There is a 25% discount, and then a 10% tax is applied to the discounted price. What is the final price?",
        "answer": 66.0,
    },
    {
        "prompt": "Tom runs 3 miles every day for 5 days, then rests for 2 days. How many miles does he run in a week?",
        "answer": 15.0,
    },
    {
        "prompt": "A rectangle has a length of 12 cm and a width of 8 cm. What is its perimeter?",
        "answer": 40.0,
    },
    {
        "prompt": "Maria has $50. She buys 3 books at $8 each and 2 pens at $3 each. How much money does she have left?",
        "answer": 20.0,
    },
    {
        "prompt": "A car travels at 60 km/h for 2 hours, then at 80 km/h for 1.5 hours. What is the total distance traveled?",
        "answer": 240.0,
    },
    {
        "prompt": "If 5 machines produce 5 widgets in 5 minutes, how long does it take 100 machines to produce 100 widgets?",
        "answer": 5.0,
    },
    {
        "prompt": "A pizza is cut into 8 slices. If 3 people each eat 2 slices, how many slices remain?",
        "answer": 2.0,
    },
    {
        "prompt": "The sum of three consecutive integers is 72. What is the largest of these integers?",
        "answer": 25.0,
    },
]

# Format pour GRPOTrainer (prompt en format conversation)
from datasets import Dataset

def format_gsm8k_for_grpo(problems):
    formatted = []
    for p in problems:
        formatted.append({
            "prompt": [{"role": "user", "content": p["prompt"]}],
            "answer": p["answer"],
        })
    return formatted

dataset_rlvr = Dataset.from_list(format_gsm8k_for_grpo(GSM8K_SAMPLE))

print(f"Dataset RLVR : {len(dataset_rlvr)} problemes mathematiques")
for i in range(3):
    print(f"  Q{i+1}: {dataset_rlvr[i]['prompt'][0]['content'][:60]}...")
    print(f"     Answer: {dataset_rlvr[i]['answer']}")
Dataset RLVR : 10 problemes mathematiques
  Q1: Janet has 16 eggs. She breaks 3 eggs while cooking, then buy...
     Answer: 19.0
  Q2: A train has 120 passengers. At the first stop, 35 passengers...
     Answer: 143.0
  Q3: A shirt costs $80. There is a 25% discount, and then a 10% t...
     Answer: 66.0

Exercice 2 : Créer un dataset de problemes de geometrie

Le dataset actuel contient des problemes arithmetiques généraux. L’objectif est de créer un mini-dataset de 5 problemes de geometrie (aires, perimetres, volumes) avec des ground truths verifiables, et de les formater pour le GRPOTrainer.

Objectif : implementer creer_dataset_geometrie qui retourne une liste de problemes geometriques au format compatible avec le pipeline RLVR.

Indices : - # Étape 1 : Définir les problemes (aire triangle, perimetre rectangle, volume sphere, etc.) - # Étape 2 : Formater au format {"prompt": [...], "answer": float} comme GSM8K_SAMPLE - # Indice : utiliser les formules geometriques standard pour calculer les reponses exactes

def creer_dataset_geometrie() -> list[dict]:
    # TODO etudiant : creer 5 problemes de geometrie
    
    problemes = []
    
    # Etape 1 : definir les problemes avec enonce + reponse exacte
    # Exemple : {"prompt": "What is the area of a triangle with base 10 and height 6?", "answer": 30.0}
    
    # Etape 2 : formater pour le pipeline RLVR
    for p in problemes:
        pass  # TODO etudiant : formater en {"prompt": [...], "answer": float}
    
    return problemes  # TODO etudiant : retourner la liste formatee

print("Exercice a completer : dataset geometrie")
Exercice a completer : dataset geometrie

5. Reward function pour GRPOTrainer

trl.GRPOTrainer attend une callable reward_funcs qui prend les completions et retourne un score par completion.

Le point qui decide si le RLVR est reel ou vide : d’ou vient le ground truth ? TRL transmet a la reward function toutes les colonnes du dataset autres que prompt, sous forme de listes alignees sur les completions. Notre dataset porte une colonne answer (cf. section 4), donc la signature rlvr_reward(completions, answer=..., **kwargs) recoit la verite terrain de chaque probleme et peut appeler le verifier SymPy pour de vrai.

Une reward function qui ignore ce kwarg et renvoie une constante produit un entrainement vide : reward identique dans le groupe -> avantage nul -> gradient de politique nul. Le piege est silencieux parce que l’entrainement se termine sans erreur.

# Historique des rewards, pour mesurer la parcimonie du signal apres coup.
REWARD_LOG: list[float] = []


def rlvr_reward(completions: list, answer: list | None = None, **kwargs) -> list:
    """Reward verifiable pour GRPOTrainer, credit partiel (#12438) :
    1.0 juste / 0.5 reponse extraite mais fausse / 0.0 rien d'extractible.

    TRL passe les colonnes du dataset autres que `prompt` en kwargs, alignees sur
    les completions -- ici la colonne `answer`. C'est ce qui rend le reward
    *verifiable* plutot que constant.
    """
    rewards = []
    for i, completion in enumerate(completions):
        if isinstance(completion, list):
            # Format conversation : contenu du dernier message
            text = completion[-1]["content"] if completion else ""
        else:
            text = str(completion)

        if answer is None or i >= len(answer):
            # Pas de ground truth disponible : on ne peut rien verifier.
            # On le signale au lieu de renvoyer un 0.0 indistinguable d'une reponse fausse.
            raise ValueError(
                "rlvr_reward n'a pas recu la colonne `answer` du dataset : "
                "le reward serait constant et l'entrainement vide."
            )

        r = math_verifier_reward(text, float(answer[i]))
        rewards.append(r)

    REWARD_LOG.extend(rewards)
    return rewards


# Verification immediate sur la forme exacte que TRL enverra (format conversation).
_demo_completions = [
    [{"role": "assistant", "content": "Let me think. 16 - 3 = 13, then 13 + 6 = 19. The answer is 19"}],
    [{"role": "assistant", "content": "The answer is 12"}],
]
_demo_rewards = rlvr_reward(_demo_completions, answer=[19.0, 19.0])
REWARD_LOG.clear()  # on ne garde pas les rewards de la demo dans l'historique d'entrainement

print("Reward function RLVR prete (verifiable, branchee sur la colonne `answer`).")
print(f"  demo : reponse juste -> {_demo_rewards[0]}, reponse fausse -> {_demo_rewards[1]}")
print()
print("Garde-fou : si TRL n'envoie pas `answer`, la fonction leve une ValueError")
print("plutot que de renvoyer un reward constant (qui donnerait un gradient nul en silence).")
Reward function RLVR prete (verifiable, branchee sur la colonne `answer`).
  demo : reponse juste -> 1.0, reponse fausse -> 0.5

Garde-fou : si TRL n'envoie pas `answer`, la fonction leve une ValueError
plutot que de renvoyer un reward constant (qui donnerait un gradient nul en silence).

6. Configuration RLVR

Même base GRPO que PT-04, avec des ajustements pour le domaine mathematique : - max_completion_length = 512 : les raisonnements math sont plus longs - beta = 0.04 : même KL penalty que Deepseek-R1 - group_size = 4 : compromis variance/memoire

RLVR_CONFIG_DICT = {
    "num_generations": 4,           # Group size G
    "beta": 0.04,                   # KL regularization
    "per_device_train_batch_size": 1,
    "gradient_accumulation_steps": 8,
    "learning_rate": 5e-6,          # Plus bas que GRPO heuristique (signal plus precis)
    "lr_scheduler_type": "cosine",
    "warmup_ratio": 0.1,
    "max_completion_length": 512,   # Longer for math reasoning
    # `max_prompt_length` a ete retire de GRPOConfig (trl >= 1.9 ; il ne subsiste
    # que dans trl.experimental). La troncature du prompt n'est plus un reglage du
    # trainer : les prompts sont pris tels quels, et c'est au dataset de tenir dans
    # la fenetre du modele. Ici les enonces font quelques dizaines de tokens.
    "logging_steps": 2,
    "save_strategy": "no",
    "output_dir": "./rlvr_output",
    "seed": 42,
    "bf16": True,
}

print("Configuration RLVR :")
for k, v in RLVR_CONFIG_DICT.items():
    print(f"  {k} = {v}")
Configuration RLVR :
  num_generations = 4
  beta = 0.04
  per_device_train_batch_size = 1
  gradient_accumulation_steps = 8
  learning_rate = 5e-06
  lr_scheduler_type = cosine
  warmup_ratio = 0.1
  max_completion_length = 512
  logging_steps = 2
  save_strategy = no
  output_dir = ./rlvr_output
  seed = 42
  bf16 = True

6b. Harness d’évaluation du raisonnement — accuracy, format_rate, think_length, backtrack_rate

L’accuracy seule ne dit pas si le modèle raisonne : un modèle qui répond au bon format mais faux, ou qui brode sans jamais s’auto-corriger, présente la même accuracy qu’un modèle qui synthétise. Le harness (#12438) mesure les 4 métriques d’une éval R1 crédible sur held-out — accuracy, format_rate (réponse extractible au format attendu), think_length (longueur moyenne du raisonnement), backtrack_rate (fraction des complétions portant une marque d’auto-correction, liste explicite) — avec le même instrument avant et après l’entraînement, et sert de socle à PT-06 (évaluation comparative).

# Harness d'evaluation du raisonnement (#12438) : le meme instrument avant/apres
# l'entrainement, sinon le delta ne veut rien dire (patron de la cellule precedente).
BACKTRACK_MARKERS = [
    "wait", "actually", "let me reconsider", "let me double-check",
    "on second thought", "hmm", "correction",
]


def reasoning_metrics_on_completions(completions: list, answers: list) -> dict:
    """Calcule les 4 metriques sur des completions DEJA produites.

    Self-test CPU (textes de reference) et analyse post-hoc ; la generation,
    elle, se fait dans evaluate_reasoning_model.
    """
    n = len(completions)
    n_fmt = n_acc = n_back = 0
    total_len = 0
    for comp, gt in zip(completions, answers):
        pred = extract_answer(comp)
        if pred is not None:
            n_fmt += 1
            if abs(pred - gt) <= max(1e-6, 1e-3 * abs(gt)):
                n_acc += 1
        total_len += len(comp.split())
        low = comp.lower()
        if any(marker in low for marker in BACKTRACK_MARKERS):
            n_back += 1
    return {
        "n_generations": n,
        "accuracy": n_acc / n,
        "format_rate": n_fmt / n,
        "think_length": total_len / n,
        "backtrack_rate": n_back / n,
    }


def evaluate_reasoning_model(model, tokenizer, problems, answers, n=4,
                             max_new_tokens=128, temperature=0.1, seed=None) -> dict:
    """Genere n completions par probleme (basse temperature) et retourne les 4 metriques.

    C'est la version LLM de la courbe d'apprentissage : meme instrument, meme
    echantillon, avant et apres l'entrainement (reutilisable par PT-06).
    """
    import torch as _t
    if seed is not None:
        _t.manual_seed(seed)
    model.eval()
    completions = []
    for pb, gt in zip(problems, answers):
        messages = [{"role": "user", "content": pb + "\n\nReason step by step, then end with 'The answer is <number>'."}]
        try:
            text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
        except Exception:
            text = pb
        ids = tokenizer(text, return_tensors="pt").to(model.device)
        for _ in range(n):
            with _t.no_grad():
                out = model.generate(**ids, max_new_tokens=max_new_tokens,
                                     do_sample=temperature > 0,
                                     temperature=temperature or None,
                                     pad_token_id=tokenizer.eos_token_id)
            completions.append(tokenizer.decode(out[0][ids["input_ids"].shape[1]:],
                                                skip_special_tokens=True))
    return reasoning_metrics_on_completions(
        completions, [gt for gt in answers for _ in range(n)]
    )


def print_before_after(metrics_before: dict, metrics_after: dict) -> None:
    """Tableau avant/apres des 4 metriques (#12438)."""
    cols = ("accuracy", "format_rate", "think_length", "backtrack_rate")
    header = f"{'metrique':<15}{'avant':>10}{'apres':>10}{'delta':>10}"
    print(header)
    print("-" * len(header))
    for c in cols:
        b, a = metrics_before[c], metrics_after[c]
        if "rate" in c or c == "accuracy":
            print(f"{c:<15}{100*b:>9.1f}%{100*a:>9.1f}%{100*(a-b):>+9.1f}%")
        else:
            print(f"{c:<15}{b:>10.1f}{a:>10.1f}{a-b:>+10.1f}")


# Self-test CPU : calcul des metriques sur des completions de REFERENCE
# (textes fixes, pas des sorties de modele — la generation reelle est gatee GPU).
_ref = [
    ("Let me compute 15 + 27. 15 + 27 = 42. Wait, let me double-check: 10+20=30, 5+7=12, total 42. The answer is 42", 42.0),
    ("Therefore the answer is 43 because the sum seems right", 42.0),
    ("I am not sure how to solve this one, sorry.", 42.0),
    ("60 km/h for 2.5 h. Actually, distance = 60 * 2.5 = 150. The answer is 150 km", 150.0),
]
_self = reasoning_metrics_on_completions([c for c, _ in _ref], [a for _, a in _ref])
print("Self-test du harness sur 4 completions de reference (CPU, textes fixes) :")
for _k in ("n_generations", "accuracy", "format_rate", "think_length", "backtrack_rate"):
    print(f"  {_k:<16} = {_self[_k]}")
print(f"  backtrack_markers = {BACKTRACK_MARKERS}")
print()
print("Harness pret : evaluate_reasoning_model(model, tokenizer, problems, answers, n)")
print("s'execute avant ET apres l'entrainement (branchement cellules suivantes, gate GPU).")
Self-test du harness sur 4 completions de reference (CPU, textes fixes) :
  n_generations    = 4
  accuracy         = 0.5
  format_rate      = 0.75
  think_length     = 15.25
  backtrack_rate   = 0.5
  backtrack_markers = ['wait', 'actually', 'let me reconsider', 'let me double-check', 'on second thought', 'hmm', 'correction']

Harness pret : evaluate_reasoning_model(model, tokenizer, problems, answers, n)
s'execute avant ET apres l'entrainement (branchement cellules suivantes, gate GPU).

7. Construction du GRPOTrainer RLVR

L’architecture RLVR reutilise GRPOTrainer avec un reward function verifiable au lieu d’heuristique.

# Instrument de mesure partage : la MEME fonction sert avant et apres l'entrainement,
# sinon le delta ne veut rien dire. Definie ici pour etre disponible des le pre-test.
import torch as _torch


def evaluer_accuracy_rlvr(modele, tokenizer, problemes, num_completions=4, max_new_tokens=128, seed=None):
    """Genere num_completions par probleme et verifie chaque reponse (extract_answer + ground truth).

    Retourne (nb_correct, nb_total, details). Aucune valeur fabriquee.
    """
    if seed is not None:
        _torch.manual_seed(seed)
    modele.eval()
    nb_correct = 0
    nb_total = 0
    details = []
    for pb in problemes:
        prompt = pb["prompt"]
        gt = pb["answer"]
        messages = [{"role": "user", "content": prompt + "\n\nReponds avec uniquement le nombre final apres 'Reponse : '."}]
        try:
            text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
        except Exception:
            text = prompt
        ids = tokenizer(text, return_tensors="pt").to(modele.device)
        corrects = 0
        for _ in range(num_completions):
            with _torch.no_grad():
                out = modele.generate(**ids, max_new_tokens=max_new_tokens, do_sample=True,
                                      temperature=0.7, pad_token_id=tokenizer.eos_token_id)
            comp = tokenizer.decode(out[0][ids["input_ids"].shape[1]:], skip_special_tokens=True)
            pred = extract_answer(comp)
            corrects += int(pred is not None and abs(pred - gt) < 1e-3)
            nb_total += 1
        nb_correct += corrects
        details.append((prompt[:50], gt, corrects, num_completions))
    return nb_correct, nb_total, details


EVAL_SAMPLE = GSM8K_SAMPLE[:4]  # 4 problemes x 4 completions = 16 generations par mesure
ACC_PRE = None
METRICS_PRE = None  # harness multi-metriques (#12438), meme echantillon qu'ACC_PRE

if LOAD_MODEL_AND_TRAIN and CUDA_AVAILABLE:
    from transformers import AutoModelForImageTextToText, AutoTokenizer, BitsAndBytesConfig
    from trl import GRPOTrainer, GRPOConfig
    from peft import LoraConfig, TaskType, get_peft_model
    import torch

    # Qwen3.5-0.8B : SOTA cible (remplace Qwen2.5-0.5B-Instruct, supersede).
    # Modele VL charge via AutoModelForImageTextToText (gabarit PT-03 valide), utilise ici en chemin text-only.
    MODEL_NAME = "Qwen/Qwen3.5-0.8B"

    # Quantization 4-bit
    bnb_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_compute_dtype=torch.bfloat16,
        bnb_4bit_use_double_quant=True,
    )

    # LoRA config (meme que PT-04)
    lora_config = LoraConfig(
        r=8,
        lora_alpha=16,
        lora_dropout=0.05,
        bias="none",
        task_type=TaskType.CAUSAL_LM,
        target_modules=[
            "q_proj", "k_proj", "v_proj", "o_proj",
            "gate_proj", "up_proj", "down_proj",
        ],
    )

    print("Chargement du modele Qwen3.5-0.8B (4-bit)...")
    model = AutoModelForImageTextToText.from_pretrained(
        MODEL_NAME,
        quantization_config=bnb_config,
        device_map="auto",
    )
    tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)

    print("Application LoRA...")
    model = get_peft_model(model, lora_config)
    model.print_trainable_parameters()

    # Mode smoke : on plafonne a 2 steps (validation plomberie), sans toucher au run complet
    smoke_overrides = {"max_steps": 2} if SMOKE_TEST else {}
    # --- Mesure AVANT entrainement (le point de comparaison) ---
    print("\nMesure de reference AVANT entrainement (16 generations reelles)...")
    _pre_c, _pre_n, _pre_d = evaluer_accuracy_rlvr(model, tokenizer, EVAL_SAMPLE, seed=1234)
    ACC_PRE = (_pre_c, _pre_n)
    print(f"Accuracy PRE-RLVR : {_pre_c}/{_pre_n} = {100*_pre_c/_pre_n:.1f}%")

    METRICS_PRE = evaluate_reasoning_model(
        model, tokenizer,
        [p["prompt"] for p in EVAL_SAMPLE], [p["answer"] for p in EVAL_SAMPLE],
        n=2, seed=1234,
    )
    print("Metriques PRE-RLVR (harness multi-metriques) :")
    for _k in ("accuracy", "format_rate", "think_length", "backtrack_rate"):
        print(f"  {_k:<15} = {METRICS_PRE[_k]}")

    print("Configuration GRPOConfig RLVR...")
    rlvr_config = GRPOConfig(**{**RLVR_CONFIG_DICT, **smoke_overrides})

    print("Construction GRPOTrainer RLVR...")
    trainer = GRPOTrainer(
        model=model,
        args=rlvr_config,
        processing_class=tokenizer,
        train_dataset=dataset_rlvr,
        reward_funcs=[rlvr_reward],
    )

    print(f"\nRLVR Trainer pret. {len(dataset_rlvr)} problemes, G={RLVR_CONFIG_DICT['num_generations']}.")
    print("Lancement de l'entrainement RLVR...")
    train_result = trainer.train()
    print(f"\nEntrainement RLVR termine.")
    print(f"Loss finale : {train_result.training_loss:.4f}")

    # Le signal RLVR, mesure : combien de completions ont effectivement recu reward=1 ?
    _n_rw = len(REWARD_LOG)
    _n_pos = sum(1 for r in REWARD_LOG if r > 0)
    print(f"Rewards collectes : {_n_rw} completions, dont {_n_pos} verifiees correctes "
          f"({100*_n_pos/max(_n_rw,1):.1f}%)")
    print("  -> c'est la PARCIMONIE du reward verifiable : un groupe dont les G completions")
    print("     sont toutes fausses a un avantage nul et ne produit aucun gradient.")

else:
    print("Skip : entrainement RLVR non execute (LOAD_MODEL_AND_TRAIN=False ou pas de CUDA)")
    print()
    print("Pour executer :")
    print("  1. LOAD_MODEL_AND_TRAIN = True")
    print("  2. GPU avec >= 6 Go VRAM disponible (pic mesure Qwen3.5-0.8B QLoRA 4-bit ~ 2,5 Go)")
    print("  3. Temps estime : ~20-40 min (10 problems x G=4, 3 epochs)")
    print()
    print("Ce que l'execution mesure reellement (et qui est commite dans ce notebook) :")
    print("  - accuracy AVANT entrainement, sur 16 generations reelles")
    print("  - accuracy APRES entrainement, meme instrument, meme echantillon")
    print("  - la parcimonie du reward : part des completions verifiees correctes")
    print()
    print("Aucune accuracy cible n'est annoncee ici : sur 10 problemes et 3 epochs,")
    print("le resultat honnete peut etre un delta nul. C'est la mesure qui tranche.")
Skip : entrainement RLVR non execute (LOAD_MODEL_AND_TRAIN=False ou pas de CUDA)

Pour executer :
  1. LOAD_MODEL_AND_TRAIN = True
  2. GPU avec >= 6 Go VRAM disponible (pic mesure Qwen3.5-0.8B QLoRA 4-bit ~ 2,5 Go)
  3. Temps estime : ~20-40 min (10 problems x G=4, 3 epochs)

Ce que l'execution mesure reellement (et qui est commite dans ce notebook) :
  - accuracy AVANT entrainement, sur 16 generations reelles
  - accuracy APRES entrainement, meme instrument, meme echantillon
  - la parcimonie du reward : part des completions verifiees correctes

Aucune accuracy cible n'est annoncee ici : sur 10 problemes et 3 epochs,
le resultat honnete peut etre un delta nul. C'est la mesure qui tranche.

8. Observation de l’émergence du raisonnement

Le phénomène le plus commenté du RLVR est l’émergence de chaînes de raisonnement : le modèle n’est jamais entraîné a produire des étapes intermédiaires, mais le reward vérifiable sélectionne les completions qui en produisent, parce qu’elles tombent plus souvent juste.

Le mécanisme n’est pas magique : le RL ne « découvre » pas le raisonnement, il sélectionne les trajectoires gagnantes. Avec un reward exact (pas de faux positif), cette sélection est fiable — mais elle n’opère que sur les groupes ou au moins une completion réussit. Un groupe entièrement faux a un avantage nul et n’apprend rien : c’est la parcimonie du reward vérifiable, et c’est le facteur limitant a petite échelle.

Schéma du phénomène recherché (illustration, pas une sortie de ce notebook) :

sans raisonnement  : "A: 19"                                  -> juste par chance
avec raisonnement  : "16 - 3 = 13, puis 13 + 6 = 19. Reponse : 19"  -> juste par construction

La cellule suivante ne raconte pas ce schéma : elle mesure l’accuracy avant et après entraînement avec le même instrument, sur le même échantillon, et affiche le delta obtenu — y compris s’il est nul.

# Mesure APRES entrainement, avec le meme instrument et le meme echantillon que le pre-test.
if LOAD_MODEL_AND_TRAIN and CUDA_AVAILABLE and "model" in globals() and "tokenizer" in globals():
    print("Evaluation reelle APRES entrainement (16 generations reelles)")
    print("=" * 60)
    nb_correct, nb_total, details = evaluer_accuracy_rlvr(model, tokenizer, EVAL_SAMPLE, seed=1234)

    if ACC_PRE is not None:
        _pc, _pn = ACC_PRE
        print(f"Accuracy PRE-RLVR   : {_pc}/{_pn} = {100*_pc/_pn:.1f}%")
    print(f"Accuracy POST-RLVR  : {nb_correct}/{nb_total} = {100*nb_correct/nb_total:.1f}%")
    if ACC_PRE is not None:
        _delta = (nb_correct / nb_total) - (_pc / _pn)
        print(f"Delta               : {100*_delta:+.1f} points")
    print()

    for prompt, gt, c, n in details:
        print(f"  [{c}/{n}] gt={gt} | {prompt}...")
    print()

    # Le signal d'entrainement, pas une projection
    _n_rw = len(REWARD_LOG)
    _n_pos = sum(1 for r in REWARD_LOG if r > 0)
    print(f"Signal RLVR pendant l'entrainement : {_n_pos}/{_n_rw} completions verifiees correctes.")
    print()
    print("Lecture honnete de ces chiffres :")
    print("  - 10 problemes, G=4, 3 epochs, UN seul seed : c'est une demonstration de mecanisme,")
    print("    pas une mesure d'amelioration. Un delta faible ou nul est un resultat attendu")
    print("    et il est affiche tel quel.")
    print("  - chaque completion vient d'une generation reelle du modele, verifiee par")
    print("    extract_answer + comparaison au ground truth. Aucune valeur simulee.")
    print("  - le reward d'entrainement est branche sur la colonne `answer` du dataset :")
    print("    l'entrainement optimise bien le critere verifiable, et non une constante.")

    # Harness multi-metriques (#12438) : avant / apres sur le meme echantillon.
    METRICS_POST = evaluate_reasoning_model(
        model, tokenizer,
        [p["prompt"] for p in EVAL_SAMPLE], [p["answer"] for p in EVAL_SAMPLE],
        n=2, seed=1234,
    )
    print()
    print("=== Harness multi-metriques : avant / apres RLVR (#12438) ===")
    if "METRICS_PRE" in globals() and METRICS_PRE is not None:
        print_before_after(METRICS_PRE, METRICS_POST)
    else:
        for _k in ("accuracy", "format_rate", "think_length", "backtrack_rate"):
            print(f"  {_k:<15} = {METRICS_POST[_k]}")
else:
    print("Skip : evaluation reelle non executee.")
    print("Pour mesurer l'accuracy reelle (et non une simulation), executer avec :")
    print("  LOAD_MODEL_AND_TRAIN = True sur GPU (cell precedente), puis cette cell genere + verifie.")
Skip : evaluation reelle non executee.
Pour mesurer l'accuracy reelle (et non une simulation), executer avec :
  LOAD_MODEL_AND_TRAIN = True sur GPU (cell precedente), puis cette cell genere + verifie.

9. Comparaison GRPO heuristique vs RLVR

Deux choses distinctes sont a distinguer dans ce tableau : ce que la methode garantit par construction, et ce que ce run a effectivement mesure. Les confondre est exactement l’erreur que le reward verifiable existe pour eviter.

Metrique GRPO heuristique (PT-04) RLVR (PT-05)
Reward source length + keywords verifier SymPy sur le ground truth
Faux positifs du critere Eleves (un texte long est recompense) Sur la justesse, nuls : une valeur fausse ne paie jamais 1.0 (0.5 = credit de format, pas de justesse)
Reward hacking Possible (repetition, verbosite) Fortement contraint : seul un verdict exact paie au-dela du credit de format (une reponse juste par chance reste toutefois recompensee)
Densite du signal Elevee (presque tout recoit un reward) Parcimonieuse : mesuree ici a 59/120 = 49.2 % de completions verifiees
Emergence CoT Non observee Non observee dans ce run (10 problemes, G=4, 3 epochs, 1 seed) — le phenomene est rapporte a une echelle bien superieure
Implementation Simple Moderee (extraction + comparaison de la reponse)
Domaine General Mathematique (extensible a tout critere verifiable)
Beta 0.04 0.04
Learning rate 1e-5 5e-6 (signal plus precis, pas de bruit a lisser)

Ce que ce run a mesure, et ce qu’il ne mesure pas

Grandeur Valeur observee (section 8) Lecture
Accuracy PRE-RLVR 2/16 reference, 16 generations reelles
Accuracy POST-RLVR 0/16 delta = -12.5 points
Loss finale 0.0409 non nulle : le gradient existe (un reward constant l’aurait mise a 0)
Completions verifiees 59/120 le reward discrimine reellement, sans etre dense

Le delta est negatif et il est affiche tel quel. Mais deux reussites sur seize, c’est du bruit d’echantillonnage : ni cette baisse ni une hausse de meme ampleur ne demontreraient quoi que ce soit. Ce notebook etablit que le mecanisme fonctionne — reward branche sur le ground truth, gradient non nul, parcimonie du signal mesuree — et non qu’un entrainement de 10 problemes ameliore un modele de 0.8B. Conclure a une amelioration demanderait plusieurs seeds, un echantillon d’evaluation bien plus large et un test de significativite : aucun des trois n’est present ici.

Quand utiliser quoi ?

  • RLVR : domaine avec critere verifiable exact (math, code, puzzles logiques)
  • GRPO heuristique : domaine creatif (generation de texte, style) sans verifiable
  • DPO : quand on dispose de paires pre-labellisees (preferences humaines)
  • SFT : point de depart avant toute methode RL

10. 5 pieges spécifiques au RLVR

Piege 1 : Echec du parser d’extraction

Le verifier depend completement de extract_answer. Si le modèle produit une reponse correcte dans un format inattendu, le reward = 0 (faux negatif). Solution : robustesse multi-pattern (\boxed{}, ####, = X, dernier nombre).

Piege 2 : Distribution de reward trop sparse

Avec outcome reward, 1 signal par completion. Si le modèle est très mauvais au depart (accuracy ~0%), tous les avantages = 0 et le gradient est nul. Solution : warmup SFT sur quelques exemples (few-shot) avant RL, ou partial credit (distance numérique).

Piege 3 : Overfitting sur les 10 problemes

10 prompts = risque d’overfitting memorisation. Le modèle “apprend” les reponses par coeur. Solution : dataset plus grand (GSM8K complet = 7473 train), evaluation sur split test.

Piege 4 : Incoherence format reponse

Le modèle oscille entre formats ($66, 66.0, sixty-six). Le parser rate certaines variantes. Solution : ajouter des formats au parser, ou contraindre le format via system prompt.

Piege 5 : Confiance excessive dans l’emergence

L’emergence de CoT n’est pas garantie sur tous les modèles/tailles. Qwen3.5-0.8B peut ne pas montrer d’emergence nette. Solution : etre honnete sur les résultats. Pas de claim “emergence observee” si non verifiable.

Exercice 3 : Implementer un reward a credit partiel

Le verifier actuel est ternaire : 1.0 reponse correcte, 0.5 reponse extraite mais fausse (credit de format), 0.0 rien d’extractible. Ce 0.5 est plat — il ne distingue pas une reponse proche de la ground truth d’une reponse loin. L’objectif est d’implementer partial_credit_reward qui affine ce credit intermediaire en fonction de la proximite de la ground truth (par exemple, erreur d’arrondi, bonne méthode mais erreur de calcul finale).

Objectif : ecrire une fonction qui retourne un reward entre 0 et 1 en fonction de la distance relative entre la reponse extraite et la ground truth.

Indices : - # Étape 1 : Calculer l’erreur relative : |predicted - ground_truth| / |ground_truth| - # Étape 2 : Mapper l’erreur a un score (0% erreur = 1.0, 5% erreur = 0.5, 10%+ = 0.0) - # Indice : utiliser une fonction lineaire ou sigmoid pour le mapping

def partial_credit_reward(completion: str, ground_truth: float, max_error_pct: float = 0.10) -> float:
    # TODO etudiant : implementer le reward a credit partiel
    predicted = extract_answer(completion)
    
    if predicted is None:
        return 0.0
    
    # Etape 1 : calculer l'erreur relative
    relative_error = None  # TODO etudiant : abs(predicted - ground_truth) / max(abs(ground_truth), 1e-10)
    
    # Etape 2 : mapper a un score entre 0 et 1
    score = None  # TODO etudiant : 1.0 - (relative_error / max_error_pct) si < max, sinon 0
    
    return score  # TODO etudiant : retourner le score

print("Exercice a completer : partial credit reward")
Exercice a completer : partial credit reward

Bilan — RLVR : quand la recompense devient une verite, pas une heuristique

Le RLVR (Reinforcement Learning with Verifiable Rewards) repond au problème laisse ouvert par le RLHF classique et le GRPO heuristique (PT-04) : comment recompenser un raisonnement sans le bruiter avec une heuristique gamable ? La reponse de RLVR est radicale — la recompense n’est plus une fonction approximative (longueur, mots-cles) mais un verifieur exact qui compare la reponse extraite a la ground truth mathematique via SymPy.

Ce qu’il faut retenir

Critere GRPO heuristique (PT-04) RLVR (PT-05)
Source du reward longueur + mots-cles verifieur SymPy sur le ground truth
Faux positifs du critere eleves sur la justesse, nuls : une valeur fausse ne paie jamais 1.0 (0.5 = credit de format, pas de justesse)
Reward hacking possible (repetition) fortement contraint : jamais 1.0 pour une valeur fausse (mais une reponse juste par chance paie aussi)
Densite du signal elevee parcimonieuse — mesuree ici a 59/120
Emergence CoT non observee non observee a cette echelle (cf. ci-dessous)
Domaine general mathematique (GSM8K)

Le phenomene attendu — et ce que ce notebook en a vu

Le RLVR est cite pour un effet que personne ne programme : le modèle decouvrirait de lui-même que decomposer son raisonnement en étapes ameliore sa precision, sans jamais avoir ete entraine a produire du chain-of-thought. Le mecanisme invoque est une selection, pas une decouverte : un raisonnement correct maximise mecaniquement la probabilite d’arriver a la bonne reponse, donc le reward exact retient ces trajectoires.

Ce notebook n’observe pas ce phenomene, et le dit. Les chiffres mesures en section 8 sont 2/16 avant entrainement, 0/16 apres, soit un delta de −12.5 points — sur quatre problemes d’evaluation tous a 0/4 apres. A 10 problemes, G=4, 3 epochs et un seul seed, un tel ecart est du bruit d’echantillonnage : il ne demontre ni degradation ni amelioration.

Ce qui est demontre, en revanche, tient en trois mesures : le reward est branche sur la colonne answer du dataset (donc verifiable et non constant), la loss finale vaut 0.0409 (donc le gradient existe — un reward constant l’aurait mise a zero), et 59 completions sur 120 ont ete verifiees correctes. Cette derniere valeur est le vrai enseignement du run : c’est la parcimonie du reward verifiable. Un groupe dont les G completions sont toutes fausses a un avantage nul et ne produit aucun gradient — et c’est precisement le facteur qui limite le RLVR a petite echelle. L’emergence rapportee dans la litterature (Deepseek-R1, QwQ) suppose des ordres de grandeur superieurs en donnees, en steps et en taille de modèle.

Les 5 pieges spécifiques au RLVR

  1. Parser d’extraction trop rigide : reponse correcte dans un format inattendu = faux negatif. Solution : robustesse multi-pattern (\boxed{}, ####, = X, dernier nombre).
  2. Reward trop sparse : si accuracy de depart ~0%, tous les avantages = 0 et le gradient s’ecroule. Solution : reward a credit partiel (exercice 3) ou curriculum (tâches plus faciles d’abord). C’est le piege que ce run a rencontre : 49.2 % de completions verifiees, mais reparties de sorte que l’accuracy d’evaluation ne bouge pas dans le bon sens.
  3. Distribution de reponses degeneree : le modèle peut converger vers une reponse constante si elle maximise le reward local. Solution : diversite des prompts + comparaison relative (avantage GRPO).
  4. Domaine trop etroit : un verifier mathematique ne generalise pas au raisonnement ouvert. RLVR excelle sur tâches a solution unique (math, code, logique formelle), echoue sur tâches creatives.
  5. Sur-confiance dans l’emergence et dans le verifier : ni l’une ni l’autre ne se presume. Le verifier lui-même peut avoir des bugs (parsing, tolerance numérique) — d’ou les tests unitaires de la section 3. Et une emergence de CoT ne s’annonce que si elle est mesuree, ce qui n’est pas le cas ici.

Position dans le track GenAI/PostTraining

Après PT-04 (GRPO, rewards heuristiques), PT-05 introduit la deuxieme generation d’alignement par RL : remplacer l’heuristique par la verification exacte. C’est le principe qui a fait emerger le raisonnement des modèles Deepseek-R1 / QwQ — a une echelle que ce notebook pedagogique n’atteint pas, et dont il mesure justement la contrainte limitante. PT-06 (evaluation comparative) clot le parcours en comparant les 5 techniques de post-training vues de PT-01 a PT-05 sur un benchmark commun.

11. Transition vers PT-06 — Evaluation comparative

Avec PT-01 a PT-05, nous avons couvert les 5 techniques majeures du post-training 2024-2025 :

Notebook Technique Contribution
PT-01 Intro Landscape, taxonomie RLHF
PT-02 SFT LoRA Fine-tuning supervise de base
PT-03 DPO Préférence learning (paires)
PT-04 GRPO RL sans critic (rewards heuristiques)
PT-05 RLVR RL avec rewards verifiables
PT-06 Evaluation Comparaison quantitative SFT/DPO/GRPO/RLVR

PT-06 conclura la serie en evaluant les mêmes prompts avec les 4 approaches et en comparant les metriques cles : accuracy, diversity, coherence, et cout compute.

Ce que cette serie demontre : le chemin du post-training en 2025 n’est pas “une technique unique” mais un pipeline (SFT → DPO/GRPO → RLVR) ou chaque étape affine le modèle de maniere complementaire.

Retour au sommet