Curso / HF LLM Course / Capítulo 12
HF LLM Course · 12 de 12

Hugging Face · LLM Course · Capítulo 12 (último)

Construir modelos de razonamiento: RL, DeepSeek R1 y GRPO

El capítulo más nuevo del curso — agregado cuando DeepSeek R1 demostró en enero de 2025 que se podía entrenar razonamiento explícito con RL puro, sin la maquinaria pesada de RLHF clásico. Es el cierre natural: todo lo anterior (fine-tuning, LoRA, evaluación) confluye acá en la técnica que hoy separa un modelo "instruct" de uno que realmente piensa antes de responder.

Antes de leercap. 11 §2, SFT · cap. 3 §3, el training loop · fast.ai L3 §2, gradientes

🎯 Al terminar vas a poder

  • Explicar con tus palabras qué reemplaza GRPO respecto de PPO (el critic por la comparación dentro del grupo) y elegir entre RLHF/PPO, DPO y GRPO según los datos que tenés y la memoria disponible.
  • Calcular a mano la ventaja normalizada de un grupo de respuestas y leer el objetivo de GRPO término por término: ratio por token, clip, penalización KL con β.
  • Escribir funciones de reward para GRPOTrainer que reciban correctamente las columnas del dataset y funcionen con completions en formato conversacional.
  • Diagnosticar un entrenamiento GRPO que no aprende (grupo idéntico, reward hacking, batch no divisible, KL apagado) a partir de los síntomas.

1. RL aplicado a LLMs: de RLHF a GRPO

Reinforcement Learning tiene un vocabulario propio que vale la pena fijar una vez: un agente (acá, el LLM) actúa sobre un entorno (el prompt más lo que ya generó), elige una acción (el siguiente token, o la respuesta completa) guiado por una policy (los pesos del modelo), y recibe una reward (una señal numérica de qué tan buena fue esa acción). El ciclo — observar, actuar, recibir señal, ajustar la policy, repetir — es el mismo que enseñar un truco a un perro con premios, solo que la "policy" son billones de parámetros en vez de neuronas caninas.

RLHF (Reinforcement Learning from Human Feedback) fue durante años el estándar para ir de un modelo instruct a uno alineado: se entrena un reward model a partir de preferencias humanas (¿cuál de estas dos respuestas preferís?), y después se usa ese reward model para ajustar la policy con un algoritmo como PPO. Funciona, pero es caro — hay que mantener cuatro modelos en memoria simultáneamente (policy, reward model, value model/critic, y una copia de referencia para la penalización KL) y el pipeline de recolectar preferencias humanas es lento y costoso.

🟢 Por qué el curso le da tanto espacio a GRPO

GRPO (Group Relative Policy Optimization), la innovación central de DeepSeek, elimina el value model/critic por completo: en vez de que un modelo separado estime "qué tan buena es esta respuesta en abstracto", GRPO genera varias respuestas al mismo prompt y las compara entre sí — la ventaja de cada una es directamente qué tan por encima o por debajo del promedio del grupo quedó su reward. Menos modelos en memoria, menos cómputo por paso, y —crucialmente— funciona muy bien en tareas donde la reward se puede calcular con una regla verificable (¿el resultado matemático es correcto?, ¿el formato pedido se respetó?) en vez de necesitar un reward model entrenado aparte.

📐 Deducción algebraica de DPO a partir del modelo de preferencias Bradley-Terry

En alineación por pares de preferencias (x,yw,yl) (donde yw es la respuesta ganadora y yl la perdedora), el modelo de Bradley-Terry define la probabilidad de preferencia humana bajo una función de recompensa r*(x,y):

P(yw≻yl∣x)=σ(r*(x,yw)−r*(x,yl))=11+exp(−(r*(x,yw)−r*(x,yl)))

El problema de RL con regularización KL busca maximizar la recompensa esperada penalizando la divergencia respecto al modelo de referencia πref: maxπE[r(x,y)]−βDKL(π∥πref). Rafailov et al. (2023) demostraron que la política óptima tiene forma analítica cerrada:

π*(y∣x)=1Z(x)πref(y∣x)exp(1βr*(x,y))⟹r*(x,y)=βlogπ*(y∣x)πref(y∣x)+βlogZ(x)

Al sustituir esta expresión en la función de verosimilitud de Bradley-Terry, la función de partición desconocida Z(x) se cancela exactamente:

LDPO(θ;πref)=−E(x,yw,yl)[logσ(βlogπθ(yw∣x)πref(yw∣x)−βlogπθ(yl∣x)πref(yl∣x))]

Esto permite optimizar las preferencias humanas directamente sobre el LLM sin entrenar un reward model separado y sin muestreo dinámico de PPO.

Qué hay en memoria: RLHF con PPO contra GRPO Con PPO conviven cuatro modelos: la policy que se entrena, una copia de referencia para la penalización KL, un reward model que puntúa cada respuesta y un critic que estima el valor. GRPO conserva policy y referencia, reemplaza el reward model por una función de reward escrita en código y reemplaza al critic por la comparación entre las G respuestas del mismo prompt. RLHF con PPO · 4 modelos en memoria policy se entrena · genera 1 respuesta referencia congelada · penalización KL reward model entrenado con preferencias critic / value model estima cuánto vale la respuesta caro, inestable, y hay que anotar preferencias humanas GRPO · 2 modelos (1 si β = 0) policy se entrena · genera G respuestas referencia congelada · penalización KL reward function código: regla, tests, formato sin critic lo reemplaza comparar las G la ventaja de cada respuesta: cuánto supera al promedio de su grupo DPO, en el medio: dos modelos y ningún RL, pero necesita los pares chosen / rejected fijos de antemano
Lo que GRPO saca de la mesa. El reward model se vuelve una función de código cuando la calidad es verificable, y el critic desaparece porque «qué tan buena es esta respuesta» pasa a ser una pregunta relativa dentro del grupo generado en el momento.

Entre RLHF clásico y GRPO hay un tercer camino que el Capítulo 11 mencionó: DPO, que se salta el RL entero y optimiza directamente sobre pares de respuestas preferida/rechazada. Los tres resuelven "mejorar el modelo más allá de imitar ejemplos", pero piden cosas distintas:

RLHF con PPODPOGRPO
Qué necesita como datosPreferencias humanas para entrenar un reward model, más promptsPares chosen/rejected por prompt, fijos de antemanoSolo prompts, más una función de reward (una regla, tests, o un reward model)
Genera durante el entrenamientoSí (una respuesta por prompt, puntuada por el reward model)No — es "offline", todo está en el datasetSí: G respuestas por prompt, comparadas entre sí
Modelos en memoriaCuatro: policy, referencia, reward model, criticDos: policy y referencia (la referencia se puede precomputar y bajar a uno)Dos: policy y referencia — uno solo si apagás la KL; más el reward model si usás uno
Cuándo usarloTenés un reward model bueno, presupuesto, y equipo para estabilizarloTenés (o podés anotar, Capítulo 10) preferencias; alineación de estilo, tono, seguridadLa calidad se puede verificar con código: matemática, programación, formato, extracción estructurada
Limitación típicaInestable, caro, difícil de depurarNo mejora más allá de lo que hay en los pares; sensible a la calidad de los rechazadosCada paso genera mucho texto (caro); si no hay reward verificable, hereda los problemas del reward model

2. El paper de DeepSeek R1: el momento "Aha"

El hallazgo que hizo ruido en enero de 2025: entrenando un modelo base con RL puro — sin ningún fine-tuning supervisado previo, solo una reward de "¿la respuesta final es correcta?" y otra de "¿el formato con las etiquetas <think>...</think> se respetó?" — el modelo (DeepSeek-R1-Zero) empezó espontáneamente a generar cadenas de razonamiento cada vez más largas, a reconsiderar pasos intermedios, y en un caso documentado en el paper, a escribir literalmente "Wait, let me re-examine this step..." en medio de una respuesta — sin que nadie le hubiera mostrado ese patrón explícitamente. Emergió solo, como subproducto de optimizar la reward correcta.

DeepSeek-R1-Zero funcionaba, pero su output era desprolijo (mezclaba idiomas, formato inconsistente). El modelo final, DeepSeek-R1, agrega cuatro fases de entrenamiento sobre esa base:

  1. Cold start: un puñado de ejemplos de razonamiento de alta calidad, curados a mano, para darle al modelo un punto de partida legible antes de soltarlo a RL puro.
  2. Reasoning RL: la fase de GRPO propiamente dicha, optimizando corrección y formato sobre problemas verificables (matemática, código).
  3. Rejection sampling: generar muchas respuestas con el modelo ya entrenado, quedarse solo con las mejores, y usarlas como nuevo dataset de fine-tuning supervisado — control de calidad antes de la fase final.
  4. Diverse RL: una ronda final de RL sobre un conjunto más amplio de tareas (no solo verificables) para alinear también helpfulness y harmlessness, cerrando el círculo con RLHF clásico.

El resultado: rendimiento comparable a modelos de frontera cerrados en matemática y código, publicado con pesos abiertos — el evento que disparó, entre otras cosas, todo el ecosistema de reproducciones open-source ("Open R1") sobre el que está construido este mismo capítulo.

Notá que las cuatro fases son, una por una, cosas que ya viste: la 1 y la 3 son SFT del Capítulo 11 sobre datos curados como en el Capítulo 10 (la 3 es literalmente "generar → filtrar → reentrenar", el ciclo de interpretante-lacaniano, con la reward como filtro); la 2 y la 4 son GRPO con rewards distintas. Lo nuevo no es ninguna pieza, es el orden y la insistencia en que la reward sea verificable donde se pueda.

Cómo se entrenó DeepSeek-R1: RL puro para R1-Zero, cuatro fases para R1 Arriba, del modelo base con RL puro vía GRPO sobre rewards de corrección y formato sale R1-Zero, que razona pero es desprolijo. Abajo, cuatro fases: cold start con SFT sobre pocos ejemplos curados, reasoning RL con GRPO, rejection sampling que genera, filtra y hace SFT, y diverse RL con GRPO sobre tareas más amplias, hasta DeepSeek-R1. modelo base DeepSeek-V3 RL puro · GRPO reward: ¿respuesta correcta? + ¿formato <think>? R1-Zero razona («Wait, let me re-examine…») pero mezcla idiomas y formatos 1 · cold start SFT, ejemplos curados 2 · reasoning RL GRPO, verificable 3 · rejection sampling generar → filtrar → SFT 4 · diverse RL GRPO, helpfulness DeepSeek-R1 pesos abiertos, enero de 2025 SFT sobre datos curados (caps. 10 y 11) GRPO con reward (este capítulo) la fase 3 es el ciclo «generar → filtrar → reentrenar» con la reward como filtro
Las dos rutas del paper. R1-Zero muestra que el razonamiento emerge de RL puro; R1 alterna SFT sobre datos curados con rondas de GRPO para que además sea legible y útil. Ninguna pieza es nueva: lo nuevo es el orden, y que la reward sea verificable donde se pueda.

3. GRPO en detalle: sampling, ventaja, clipping y KL

El algoritmo, paso a paso, para un prompt dado:

  1. Group sampling: generar un grupo de G respuestas (típicamente 8–16) al mismo prompt, con la policy actual y algo de temperatura para que no sean todas idénticas.
  2. Reward por respuesta: evaluar cada una de las G respuestas con una o más funciones de reward (¿el resultado es correcto?, ¿siguió el formato?, ¿la longitud es razonable?).
  3. Ventaja normalizada: para cada respuesta i, la ventaja es (reward_i − media_del_grupo) / desviación_estándar_del_grupo — no cuánto vale en absoluto, sino cuánto mejor o peor fue que sus pares en el mismo grupo. Esto es literalmente lo que reemplaza al value model/critic de PPO.
  4. Policy update: subir la probabilidad de los tokens de las respuestas con ventaja positiva, bajarla en las de ventaja negativa — con dos frenos de seguridad tomados directo de PPO.

Un ejemplo con números, G = 4, reward = formato (1 punto) + corrección (2 puntos):

RespuestaFormatoCorrectaRewardVentaja ≈
1sísí3,0+0,96
2síno1,0−0,58
3nono0,0−1,35
4sísí3,0+0,96

Media del grupo 1,75; desviación estándar ≈ 1,30. Las dos respuestas correctas reciben la misma ventaja positiva, la que solo acertó el formato una negativa suave, y la que no hizo nada una negativa fuerte. Fijate qué pasa si las cuatro respuestas hubieran sido correctas: media 3,0, desviación 0, ventaja 0 para todas — ese prompt no enseña nada en este paso. Lo mismo si las cuatro fallan. GRPO aprende solo de los prompts donde el grupo es heterogéneo; esa observación es el origen de varias de las variantes de más abajo.

📐 Función objetivo formal de GRPO (Shao et al., DeepSeek-Math / R1)

Para un grupo de G salidas generadas {o1,…,oG}∼πθold(q), la ventaja normalizada de la salida i es:

A^i=ri−mean({r1,…,rG})std({r1,…,rG})+ϵ

El objetivo maximiza la ventaja recortada token a token restando la divergencia KL insesgada (estimador de Schulman):

JGRPO(θ)=E[1G∑i=1G1|oi|∑t=1|oi|(min(πθ(oi,t)πθold(oi,t)A^i,clip(πθ(oi,t)πθold(oi,t),1−ϵ,1+ϵ)A^i)−βDKL(πθ∥πref))]

donde la penalización KL por token utiliza la aproximación asintóticamente insesgada de mínima varianza:

DKL(πθ∥πref)=πref(oi,t)πθ(oi,t)−logπref(oi,t)πθ(oi,t)−1
Un paso de GRPO con números: cuatro respuestas, cuatro ventajas Un prompt genera cuatro respuestas. Cada una recibe reward por formato, un punto, y por corrección, dos puntos: 3, 1, 0 y 3. La media del grupo es 1,75 y la desviación 1,30; las ventajas son más 0,96, menos 0,58, menos 1,35 y más 0,96. La policy sube la probabilidad de los tokens de las respuestas con ventaja positiva y baja la de las negativas, con el ratio recortado y una penalización KL contra la referencia. 1 · GROUP SAMPLING (G = 4) 2 · REWARD 3 · VENTAJA 4 · UPDATE prompt «48 clips en abril, la mitad en mayo…» 1 · <think>48 en abril, 24 en mayo…</think> 72 formato ✓ 1,0 · correcta ✓ 2,0 3,0 +0,96 ↑ sube p 2 · <think>48 + 24 = 84</think> 84 formato ✓ 1,0 · correcta ✗ 0 1,0 -0,58 ↓ baja p 3 · La respuesta es 72, creo. formato ✗ 0 · correcta ✗ 0 0,0 -1,35 ↓ baja p 4 · <think>la mitad de 48 es 24; 48+24</think> 72 formato ✓ 1,0 · correcta ✓ 2,0 3,0 +0,96 ↑ sube p 0 media 1,75 · desv. 1,30 ventaja = (r − media) / desv. igual para todos los tokens de esa respuesta clip del ratio a [1−ε, 1+ε] · − β·KL contra la referencia
La tabla de §3 como mecanismo. Las dos respuestas correctas reciben la misma ventaja positiva, la que solo acertó el formato una negativa suave, la que no hizo nada una negativa fuerte. Si las cuatro hubieran acertado, la desviación sería 0, la ventaja 0 para todas, y ese prompt no enseñaría nada.

Los dos frenos son necesarios porque sin ellos un solo paso de gradiente grande puede destruir un modelo que ya funciona razonablemente bien:

  • Clipping: el ratio entre la probabilidad nueva y la vieja para cada token se recorta a un rango (ej. [0.8, 1.2] con ε=0.2) — evita que un solo update cambie demasiado la policy de una sola pasada.
  • Penalización KL: un término que castiga alejarse demasiado de una policy de referencia (usualmente el modelo antes de esta ronda de RL) — sin esto el modelo puede converger a explotar la reward de formas degeneradas (reward hacking) que técnicamente maximizan la métrica pero destruyen la calidad real.

El objetivo, en palabras

Lo que se maximiza, para cada token t de cada respuesta i del grupo, se arma con tres piezas:

  1. El ratio por token: la probabilidad que la policy actual le asigna a ese token (dado el prompt y los tokens anteriores) dividida por la que le asignaba la policy que generó la respuesta. Vale 1 exacto en el primer paso de optimización después de generar; se aleja de 1 a medida que la policy se mueve.
  2. La ventaja de la respuesta, A_i, que es la misma para todos los tokens de esa respuesta — GRPO no reparte crédito entre tokens; toda la respuesta sube o baja junta.
  3. El clip: se toma el mínimo entre ratio × A_i y ratio recortado a [1−ε, 1+ε] × A_i. El mínimo hace que, cuando la ventaja es positiva, subir el ratio más allá de 1+ε no dé más objetivo (no hay incentivo para pasarse), y cuando es negativa, bajarlo más allá de 1−ε tampoco.
Ai=ri−mean(r1,…,rG)std(r1,…,rG), ρi,t=πθ(oi,t|q,oi,<t)πvieja(oi,t|q,oi,<t)

Cómo se lee. La ventaja de la respuesta i es su reward menos el promedio del grupo, dividido por la desviación del grupo: no hay value model, el grupo es la referencia. El ratio por token compara la probabilidad que la policy actual le da al token t de la respuesta i con la que le daba la policy que generó esa respuesta.

J(θ)=𝔼grupo[1G∑i=1G1|oi|∑t=1|oi|min(ρi,tAi, clip(ρi,t,1−ε,1+ε)Ai)−β DKL(πθ‖πref)]

Cómo se lee. Promedio sobre las G respuestas y sobre los tokens de cada una del término recortado (el freno de PPO), menos β veces la distancia KL a la policy de referencia. Los tres frenos del texto, en una línea: la ventaja por grupo, el clip y la KL. Las variantes de 2025 (Dr. GRPO, DAPO) cambian exactamente cómo se promedia sobre |oi| y si se divide por la desviación.

Ai=ri−mean(r1,…,rG)std(r1,…,rG)ρi,t=πθ(oi,t|q,oi,<t)πvieja(oi,t|q,oi,<t)

Cómo se lee. La ventaja de la respuesta i es su reward menos el promedio del grupo, dividido por la desviación del grupo: no hay value model, el grupo es la referencia. El ratio por token compara la probabilidad que la policy actual le da al token t de la respuesta i con la que le daba la policy que generó esa respuesta.

J(θ)=𝔼grupo[1G∑i=1G1|oi|∑t=1|oi|min(ρi,tAi, clip(ρi,t,1−ε,1+ε)Ai)−β DKL(πθ‖πref)]

Cómo se lee. Promedio sobre las G respuestas y sobre los tokens de cada una del término recortado (el freno de PPO), menos β veces la distancia KL a la policy de referencia. Los tres frenos del texto, en una línea: la ventaja por grupo, el clip y la KL. Las variantes de 2025 (Dr. GRPO, DAPO) cambian exactamente cómo se promedia sobre |oi| y si se divide por la desviación.

A eso se le resta β × KL: una estimación por token de cuánto se aleja la policy actual de la de referencia (la que había antes de empezar RL), con un estimador que es siempre no negativo y que vale 0 cuando ambas coinciden. β es el dial: alto, el modelo casi no se mueve; cero, no hay freno y el modelo puede irse a donde la reward lo lleve. El resultado se promedia sobre los tokens y sobre las G respuestas, y el gradiente de eso es el paso de entrenamiento. En pseudocódigo:

Qué hace el clip al objetivo de GRPO según el signo de la ventaja Dos paneles con el objetivo por token en función del ratio entre la policy nueva y la vieja. Con ventaja positiva el objetivo crece con el ratio hasta 1 más épsilon y después queda plano: no hay incentivo para pasarse. Con ventaja negativa el objetivo cae con el ratio, pero por debajo de 1 menos épsilon queda plano: no hay incentivo para bajar más. La línea punteada es el objetivo sin clip. ventaja > 0: conviene subir el ratio 0,5 1−ε 1 1+ε 1,5 ratio = p_nueva(token) / p_vieja(token) más allá de 1+ε no suma objetivo ↑ ventaja < 0: conviene bajar el ratio 0,5 1−ε 1 1+ε 1,5 ratio = p_nueva(token) / p_vieja(token) por debajo de 1−ε no resta objetivo ↑
El freno de PPO que GRPO hereda. La línea llena es min(ratio·A, clip(ratio)·A); la punteada, el objetivo sin recortar. En la franja [1−ε, 1+ε] son iguales; afuera, el clip vuelve plano el objetivo justo en la dirección en la que la ventaja empujaría, y un solo update no puede mover demasiado la policy.
# Pseudocódigo de un paso de GRPO
for prompt in batch:
    respuestas = policy.generate(prompt, n=G)              # 1. group sampling
    rewards    = [reward_fn(r) for r in respuestas]   # 2. reward por respuesta
    ventajas   = normalizar(rewards)                        # 3. (reward - media) / std del grupo

    for r, ventaja in zip(respuestas, ventajas):
        ratio = policy.prob(r) / policy_vieja.prob(r)      # por token, en realidad
        objetivo = min(ratio * ventaja, clip(ratio, 1-eps, 1+eps) * ventaja)
        loss = -objetivo + beta * kl_divergence(policy, policy_referencia)
        policy.step(loss)                                   # 4. policy update

No hace falta memorizar la fórmula — el punto conceptual que sobrevive todo el resto del capítulo es este: GRPO convierte "qué tan buena es una respuesta" en una pregunta relativa dentro de un grupo generado en el momento, no en un valor absoluto que otro modelo tiene que aprender a predecir de antemano.

🟢 Las variantes de 2025 que refinan GRPO

Dos trabajos de marzo de 2025 revisaron detalles de la fórmula original y hoy están incorporados como opciones en TRL. Dr. GRPO ("GRPO Done Right", del paper Understanding R1-Zero-Like Training) señala dos sesgos: promediar la loss de cada respuesta por su propia longitud hace que las respuestas incorrectas largas se penalicen menos por token que las cortas — empujando al modelo a alargar cuando falla — y dividir la ventaja por la desviación estándar del grupo sobrepondera los prompts muy fáciles o muy difíciles. Propone quitar ambas normalizaciones. DAPO (ByteDance Seed) agrega, entre otras cosas, un clip asimétrico con el límite superior más alto que el inferior (para que tokens poco probables pero buenos puedan crecer), el dynamic sampling que descarta los prompts cuyo grupo salió todo correcto o todo incorrecto (ventaja 0, gradiente nulo — el caso de la tabla de arriba) y los reemplaza hasta llenar el batch, y una loss promediada por token sobre todo el batch en vez de por respuesta; también elimina el término KL. En GRPOConfig vas a encontrar opciones que corresponden a estas ideas (un tipo de loss alternativo, un ε superior distinto del inferior); los nombres exactos dependen de la versión, así que fijate en la documentación de la tuya.

4. Implementando GRPO con TRL

La misma librería trl del Capítulo 11 (donde vimos SFTTrainer) trae GRPOTrainer. La pieza que hay que escribir vos mismo, siempre, es la función de reward — es donde vive todo el criterio de qué es una buena respuesta para tu tarea:

import re
from trl import GRPOConfig, GRPOTrainer

def texto(c):
    """Las completions llegan como str, o como lista de mensajes si el prompt era conversacional."""
    return c[0]["content"] if isinstance(c, list) else c

def extraer_respuesta(s):
    m = re.search(r"</think>\s*(.+?)\s*$", s, re.DOTALL)
    return m.group(1).strip() if m else None

def reward_formato(completions, **kwargs):
    """1.0 si respeta <think>...</think> seguido de la respuesta, 0.0 si no."""
    patron = re.compile(r"^<think>.*?</think>\s*\S+", re.DOTALL)
    return [1.0 if patron.match(texto(c)) else 0.0 for c in completions]

def reward_correctitud(completions, respuesta_correcta, **kwargs):
    """2.0 si el resultado final coincide con la respuesta esperada."""
    return [2.0 if extraer_respuesta(texto(c)) == esperada else 0.0
            for c, esperada in zip(completions, respuesta_correcta)]

trainer = GRPOTrainer(
    model="HuggingFaceTB/SmolLM2-1.7B-Instruct",
    reward_funcs=[reward_formato, reward_correctitud],
    args=GRPOConfig(num_generations=8, output_dir="grpo-razonamiento"),
    train_dataset=dataset,  # columnas: "prompt" y "respuesta_correcta" — las respuestas las genera GRPO
)
trainer.train()

Dos detalles de la firma de las funciones que no son obvios y que rompen silenciosamente si los ignorás. Primero: GRPOTrainer llama a cada reward function con completions más todas las demás columnas del dataset como argumentos con nombre. Por eso reward_correctitud recibe respuesta_correcta: porque el dataset tiene una columna que se llama exactamente así. Si tu columna se llama answer, el parámetro tiene que llamarse answer; el **kwargs está para absorber las columnas que esa función no usa (y prompts, que también llega). Segundo: el formato de completions depende del dataset. Si la columna prompt es un string, cada completion es un string; si es una lista de mensajes {"role", "content"} (formato conversacional, el mismo del Capítulo 11), cada completion es también una lista de mensajes — en general uno solo, del asistente — y el texto está en c[0]["content"]. La función texto() de arriba cubre los dos casos.

Un detalle que rompe la intuición si venís del Capítulo 11: el dataset para GRPO no lleva respuestas — solo prompts. Las respuestas las genera la policy misma durante el entrenamiento (eso es el "group sampling"), y las funciones de reward son las que le dan sentido a esos outputs generados. Es fine-tuning donde el "target" no está fijado de antemano, sino que se construye en cada paso. (La columna respuesta_correcta no es un target para imitar: es un dato para la reward.)

Tres familias de reward function cubren la mayoría de los casos: por longitud (penalizar respuestas absurdamente cortas o largas), basadas en reglas para tareas verificables (matemática exacta, tests que pasan o no), y de formato (¿respetó la estructura pedida?, como en el ejemplo de arriba). Combinar varias con distintos pesos es la norma, no la excepción — y la escala relativa importa: en el ejemplo, corrección vale el doble que formato a propósito, para que el modelo no se conforme con abrir y cerrar <think>. Los parámetros que más vas a tocar en GRPOConfig: num_generations (el G), max_completion_length (cuánto puede razonar), beta (el peso de la KL — fijate cuál es el valor por defecto en tu versión, porque cambió con el tiempo y en versiones recientes puede ser cero), y temperature del sampling, que tiene que ser claramente mayor que cero.

5. Práctica: GRPO con Unsloth

GRPO es más pesado en cómputo que SFT porque cada paso implica generar G respuestas completas antes de poder calcular un solo gradiente — por eso el ejercicio práctico del curso usa Unsloth en vez de TRL puro: kernels optimizados que reducen memoria y aceleran tanto la generación como el training step, haciendo viable correr esto en una sola GPU de consumo (el ejemplo oficial usa Gemma 3 1B + LoRA sobre GSM8K, el dataset de problemas de matemática de escuela primaria que ya vimos como benchmark en el Capítulo 11; hoy el reemplazo directo es google/gemma-4-E2B-it, Apache 2.0 y con soporte de Unsloth desde el lanzamiento, cargado con FastModel porque el checkpoint es multimodal).

from unsloth import FastLanguageModel
from trl import GRPOConfig, GRPOTrainer

model, tokenizer = FastLanguageModel.from_pretrained(
    "unsloth/gemma-3-1b-it", max_seq_length=1024, load_in_4bit=True,
)
model = FastLanguageModel.get_peft_model(model, r=16, target_modules=["q_proj", "v_proj"])

trainer = GRPOTrainer(
    model=model, processing_class=tokenizer,
    reward_funcs=[reward_formato, reward_correctitud],
    args=GRPOConfig(num_generations=8, use_vllm=True),  # vLLM acelera la fase de generación
    train_dataset=dataset_gsm8k,
)
trainer.train()
model.push_to_hub_merged("tu-usuario/gemma-3-razonador", tokenizer)

🟢 Actualización 2026

El tokenizer se pasa como processing_class=tokenizer, no tokenizer=tokenizer: los trainers de TRL (y el Trainer de transformers del que heredan) unificaron el nombre porque el mismo argumento puede ser un tokenizer, un processor multimodal o un feature extractor. El nombre viejo aparece en tutoriales de 2024 y, según la versión, da error de argumento inesperado.

Notá el use_vllm=True: el mismo motor de inferencia del Capítulo 2 (y de tus propios fixes en vLLM) se usa acá no para servir el modelo final, sino para acelerar el paso de "generar G respuestas" dentro del loop de entrenamiento — la parte más cara de todo el ciclo GRPO. Tiene sentido: con G = 8 y max_completion_length = 1024, cada prompt del batch puede producir hasta ocho mil tokens de generación autoregresiva antes de un solo backward. Sin un motor de inferencia rápido, el entrenamiento pasa la mayor parte del tiempo generando.

🔧 Este es, literalmente, tu training stack

interpretante-lacaniano ya usa Unsloth para el ciclo de CPT + SFT generacional del modelo Gemma-4-31B-lacan — mismo FastLanguageModel, mismo get_peft_model con LoRA, misma razón de fondo (correr entrenamiento pesado en tu propia GPU sin la factura de memoria de un fine-tuning completo). Lo único que agrega GRPO sobre lo que ya tenés corriendo es el reemplazo del dataset de pares instrucción/respuesta fijos por una función de reward — y ahí sí hay una conexión directa: tu curaduria_delegada.py ya calcula una señal de "qué tan buena es esta generación" (el rating 👍/👎 más el filtro de novedad) que, en espíritu, es exactamente lo que una reward_fn de GRPO necesita — solo que hoy la usás para filtrar datos de SFT en vez de para una ventaja de política en el momento. Es la frontera natural si algún día quisieras que el modelo mejore su propio razonamiento generacional con RL en vez de solo con SFT sobre generaciones filtradas.

6. Errores comunes

En GRPO los errores casi nunca son excepciones: son curvas de reward que suben mientras el modelo empeora, o que no se mueven en absoluto.

SíntomaCausa probableQué hacer
La reward de formato llega a 1,0 en pocos pasos, la de corrección no se mueve; las respuestas son <think></think> 42 o razonamientos vacíos.Reward hacking: el formato es fácil de satisfacer y pesa lo mismo (o más) que el contenido, así que el modelo optimiza lo barato.Dar más peso a la reward verificable, exigir contenido mínimo dentro de <think>, o apagar la reward de formato una vez que el modelo ya la cumple. Leer respuestas reales cada N pasos, no solo curvas (§4).
Error al arrancar: el batch no es divisible por num_generations.Cada prompt ocupa G lugares del batch, así que el batch global (per_device_train_batch_size × procesos, y según la versión × acumulación de gradiente) tiene que ser múltiplo de G.Ajustar el batch a un múltiplo de num_generations (por ejemplo batch 8 con G = 8 es un prompt por paso; batch 16, dos) (§4).
La reward promedio no cambia nunca; la loss es exactamente 0 o casi.Sampling con temperatura 0 (o muy baja): las G respuestas son idénticas, la desviación del grupo es 0 y la ventaja de todas es 0 — no hay gradiente.Temperatura claramente positiva (del orden de 0,7–1,0) y comprobar en un batch que las respuestas de un mismo prompt difieren (§3).
Un prompt fácil (todos aciertan) o imposible (todos fallan) domina el batch y el modelo no aprende de él.Mismo mecanismo: grupo homogéneo → ventaja 0. No es un bug, es una propiedad de GRPO.Filtrar el dataset a prompts de dificultad media para la policy actual, o usar el dynamic sampling de DAPO (§3).
El entrenamiento es inestable: la reward salta, la loss explota, o una sola reward function tapa a las demás.Escalas descontroladas: una reward devuelve valores en [0, 1], otra en [0, 100], o una devuelve None/NaN en algunos casos.Mantener cada reward en un rango chico y conocido, ponderar explícitamente, y comprobar que devuelven un float por cada completion, siempre (§4).
La reward sube de forma sostenida y el modelo se vuelve incoherente, repite frases o cambia de idioma.Sin freno KL (beta = 0) y con muchos pasos, la policy se aleja de la referencia hasta donde la reward la lleve.Activar beta (verificá el default de tu versión), o al menos monitorear la KL y parar cuando crece sin que la calidad real mejore (§3).
Todas las rewards dan 0 aunque las respuestas se ven bien.El nombre del parámetro no coincide con la columna del dataset (llega en kwargs y nadie lo lee), o completions viene como lista de mensajes y el regex corre sobre una lista.Renombrar el parámetro a la columna exacta; imprimir type(completions[0]) en la primera llamada y usar c[0]["content"] en el caso conversacional (§4).

7. Resumen — y cierre del curso de HF

  1. GRPO elimina el value model/critic de RLHF clásico comparando varias respuestas al mismo prompt entre sí, no contra una estimación absoluta. Frente a PPO (cuatro modelos, reward model) y DPO (pares fijos, offline), es la opción cuando la calidad se puede verificar con código.
  2. DeepSeek R1 demostró que razonamiento explícito (cadenas <think> cada vez más largas, autocorrección) puede emerger de RL puro sobre rewards verificables, sin supervisión de ese comportamiento específico — y su receta final alterna SFT sobre datos curados con rondas de GRPO.
  3. El algoritmo en código es corto: group sampling → ventaja normalizada por grupo → policy update con clipping + penalización KL, igual que PPO pero sin el critic. La ventaja es la misma para todos los tokens de una respuesta; un grupo homogéneo tiene ventaja 0 y no enseña nada. Dr. GRPO y DAPO retocan las normalizaciones, el clip y el sampling.
  4. GRPOTrainer de TRL espera solo prompts, no pares prompt/respuesta — las respuestas las genera la policy, y el criterio de calidad vive en las funciones de reward que escribís vos. Esas funciones reciben las columnas del dataset por nombre, y las completions en el mismo formato (string o mensajes) que el prompt.
  5. Unsloth + vLLM hacen viable correr esto en una sola GPU — con processing_class=tokenizer en versiones actuales — y es, con una pieza de diferencia, el mismo stack que ya corre en interpretante-lacaniano.

Con esto se cierran los 12 capítulos del LLM Course de Hugging Face: de "qué es un transformer" a "cómo entrenar razonamiento con RL", pasando por tokenización, datasets, fine-tuning clásico y con LoRA, compartir en el Hub, demos, curaduría de datos, y SFT moderno — con una conexión real a algo tuyo en casi cada parada. Sigue fast.ai: el enfoque top-down (entrenar algo real primero, teoría después) como contraste deliberado con el enfoque bottom-up que acabamos de recorrer acá.

8. Ponete a prueba

Respondé antes de abrir cada respuesta. Si fallás una, volvé a la sección indicada.

1. Tenés 5.000 pares de respuestas "preferida / rechazada" anotados en Argilla y una sola GPU de 24 GB. ¿PPO, DPO o GRPO? ¿Por qué?

DPO: los datos ya tienen la forma exacta que necesita (pares), es offline (no genera durante el entrenamiento) y cabe en memoria con policy + referencia. PPO pediría entrenar un reward model y cuatro modelos en memoria; GRPO pide una función de reward, no pares. (§1)

2. Un grupo de G = 4 tiene rewards [1, 1, 1, 1]. ¿Qué ventaja recibe cada respuesta y qué aprende el modelo de ese prompt?

Ventaja 0 para las cuatro (media 1, desviación 0) y no aprende nada de ese prompt en este paso: GRPO solo tiene señal cuando el grupo es heterogéneo. Es lo que DAPO ataca con dynamic sampling. (§3)

3. ¿Qué pasa si generás las G respuestas con temperatura 0?

Son todas idénticas (greedy), así que el caso anterior ocurre en todos los prompts: ventaja 0 en todo el batch, gradiente nulo, reward que nunca cambia. Hace falta temperatura claramente positiva para que el grupo tenga variedad. (§3, §6)

4. Tu dataset tiene columnas prompt y answer. Escribís def reward(completions, respuesta_correcta, **kwargs). ¿Qué pasa?

Falla: el trainer pasa las columnas por nombre, así que llega answer=... (absorbido por kwargs) y respuesta_correcta no llega nunca — error de argumento faltante. El parámetro tiene que llamarse answer. (§4)

5. La reward de formato pesa 1,0 y la de corrección 2,0. Cambiás el formato a 5,0 "para que aprenda rápido la estructura". ¿Qué riesgo aparece?

Reward hacking: el formato es trivial de satisfacer y ahora vale más que acertar, así que el modelo converge a <think></think> + cualquier cosa. La curva de reward sube y la utilidad real no. La reward verificable tiene que dominar. (§4, §6)

6. ¿En qué se parece la fase de rejection sampling de DeepSeek-R1 a algo que ya construiste, y en qué se diferencia de la fase de GRPO?

Es "generar muchas respuestas → filtrar las buenas → SFT sobre ellas": el ciclo generacional de interpretante-lacaniano con la reward como filtro. Se diferencia de GRPO en que el modelo no recibe gradiente de las malas (solo imita las buenas) y en que es un paso offline, no un update relativo dentro de un grupo. (§2, §5)

9. Para practicar

✏️ Ejercicio 1 — La ventaja de un grupo, a mano

Reproducí la tabla de §3 y después probá grupos homogéneos y un grupo con una sola respuesta correcta. Solo Python estándar. Observá que la ventaja no depende de la escala absoluta de la reward — multiplicá todas por 10 y mirá qué cambia.

import statistics as st

def ventajas(rewards, eps=1e-4):
    media = st.mean(rewards)
    std = st.pstdev(rewards)             # desviación poblacional del grupo
    return [round((r - media) / (std + eps), 2) for r in rewards]

for grupo in ([3.0, 1.0, 0.0, 3.0], [3.0] * 4, [0.0] * 4, [3.0, 0.0, 0.0, 0.0], [30.0, 10.0, 0.0, 30.0]):
    print(grupo, "→", ventajas(grupo))
→ [3.0, 1.0, 0.0, 3.0] → [0.96, -0.58, -1.35, 0.96] … [3.0, 3.0, 3.0, 3.0] → [0.0, 0.0, 0.0, 0.0]
Pista / qué esperar

Los grupos homogéneos dan 0 en todo (el eps evita la división por cero). El grupo con una sola correcta le da a esa respuesta una ventaja grande (+1,73) y a las otras tres una negativa chica: una respuesta rara y buena recibe mucho crédito. Multiplicar por 10 no cambia nada — la normalización por desviación hace la ventaja invariante a la escala dentro de un grupo; lo que sí cambia con la escala es cómo se suman rewards distintas entre sí (§6).

✏️ Ejercicio 2 — Testear las reward functions antes de entrenar

Nunca entrenes con una reward que no probaste a mano. Corré las funciones de §4 contra completions inventadas, en los dos formatos (string y conversacional), e incluí un caso de reward hacking para ver que la de formato lo premia.

# pegá acá texto(), extraer_respuesta(), reward_formato() y reward_correctitud() de §4

casos = [
    "<think>48 en abril, la mitad en mayo: 24. Total 72.</think> 72",   # bien
    "<think></think> 72",                                            # reward hacking: formato sin contenido
    "La respuesta es 72",                                              # correcta, sin formato
    "<think>48 + 24</think> 84",                                       # formato ok, incorrecta
]
esperadas = ["72"] * len(casos)

print("formato:   ", reward_formato(casos))
print("correccion:", reward_correctitud(casos, respuesta_correcta=esperadas))

# mismo test en formato conversacional: cada completion es una lista de mensajes
conv = [[{"role": "assistant", "content": c}] for c in casos]
assert reward_formato(conv) == reward_formato(casos)
assert reward_correctitud(conv, respuesta_correcta=esperadas) == reward_correctitud(casos, respuesta_correcta=esperadas)
print("ok: mismo resultado en ambos formatos")
→ formato: [1.0, 1.0, 0.0, 1.0] · correccion: [2.0, 2.0, 0.0, 0.0]
Pista / qué esperar

El segundo caso —<think></think> 72— recibe reward completa (3,0) igual que el primero: la reward de formato tal como está escrita no exige contenido. Modificá el regex para pedir al menos N caracteres dentro de <think> y volvé a correr. El tercer caso muestra el otro lado: "La respuesta es 72" es correcta pero extraer_respuesta devuelve None porque no hay </think> — decidí si eso es lo que querés.

✏️ Ejercicio 3 — Un paso de GRPO real, chiquito

Con SmolLM2-135M-Instruct, G = 4, completions de 48 tokens y un dataset de cuatro cuentas, corré cinco pasos de GRPOTrainer y mirá los logs de reward. En CPU tarda varios minutos; con GPU, segundos. pip install trl. Reusá las funciones de §4 (con una columna llamada respuesta_correcta, como pide la firma).

from datasets import Dataset
from trl import GRPOConfig, GRPOTrainer

SISTEMA = "Pensá dentro de <think>...</think> y después escribí solo el número."
cuentas = [("¿Cuánto es 7 × 6?", "42"), ("¿Cuánto es 15 + 27?", "42"),
           ("¿Cuánto es 100 − 58?", "42"), ("¿Cuánto es 84 ÷ 2?", "42")]
ds = Dataset.from_list([
    {"prompt": [{"role": "system", "content": SISTEMA}, {"role": "user", "content": q}],
     "respuesta_correcta": a}
    for q, a in cuentas
])

trainer = GRPOTrainer(
    model="HuggingFaceTB/SmolLM2-135M-Instruct",
    reward_funcs=[reward_formato, reward_correctitud],
    train_dataset=ds,
    args=GRPOConfig(output_dir="mini-grpo", num_generations=4, per_device_train_batch_size=4,
                    max_completion_length=48, max_steps=5, logging_steps=1,
                    temperature=1.0, report_to="none"),
)
trainer.train()
Pista / qué esperar

Con cinco pasos no vas a ver aprendizaje: el objetivo es ver el mecanismo. En los logs aparecen las rewards por función y la media del grupo; un modelo de 135M probablemente falle la mayoría de las cuentas, así que la reward de corrección va a ser baja y la de formato variable. Cambiá per_device_train_batch_size a 6 y mirá el error de divisibilidad de §6; poné temperature=0.01 y fijate cómo la desviación del grupo (y la ventaja) se va a cero.

10. Fuentes y para seguir