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
GRPOTrainerque 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 (donde es la respuesta ganadora y la perdedora), el modelo de Bradley-Terry define la probabilidad de preferencia humana bajo una función de recompensa :
El problema de RL con regularización KL busca maximizar la recompensa esperada penalizando la divergencia respecto al modelo de referencia : . Rafailov et al. (2023) demostraron que la política óptima tiene forma analítica cerrada:
Al sustituir esta expresión en la función de verosimilitud de Bradley-Terry, la función de partición desconocida se cancela exactamente:
Esto permite optimizar las preferencias humanas directamente sobre el LLM sin entrenar un reward model separado y sin muestreo dinámico de PPO.
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 PPO | DPO | GRPO | |
|---|---|---|---|
| Qué necesita como datos | Preferencias humanas para entrenar un reward model, más prompts | Pares chosen/rejected por prompt, fijos de antemano | Solo prompts, más una función de reward (una regla, tests, o un reward model) |
| Genera durante el entrenamiento | Sí (una respuesta por prompt, puntuada por el reward model) | No — es "offline", todo está en el dataset | Sí: G respuestas por prompt, comparadas entre sí |
| Modelos en memoria | Cuatro: policy, referencia, reward model, critic | Dos: 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 usarlo | Tenés un reward model bueno, presupuesto, y equipo para estabilizarlo | Tenés (o podés anotar, Capítulo 10) preferencias; alineación de estilo, tono, seguridad | La calidad se puede verificar con código: matemática, programación, formato, extracción estructurada |
| Limitación típica | Inestable, caro, difícil de depurar | No mejora más allá de lo que hay en los pares; sensible a la calidad de los rechazados | Cada 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:
- 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.
- Reasoning RL: la fase de GRPO propiamente dicha, optimizando corrección y formato sobre problemas verificables (matemática, código).
- 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.
- 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.
3. GRPO en detalle: sampling, ventaja, clipping y KL
El algoritmo, paso a paso, para un prompt dado:
- Group sampling: generar un grupo de
Grespuestas (típicamente 8–16) al mismo prompt, con la policy actual y algo de temperatura para que no sean todas idénticas. - Reward por respuesta: evaluar cada una de las
Grespuestas con una o más funciones de reward (¿el resultado es correcto?, ¿siguió el formato?, ¿la longitud es razonable?). - 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. - 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):
| Respuesta | Formato | Correcta | Reward | Ventaja ≈ |
|---|---|---|---|---|
| 1 | sí | sí | 3,0 | +0,96 |
| 2 | sí | no | 1,0 | −0,58 |
| 3 | no | no | 0,0 | −1,35 |
| 4 | sí | 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 salidas generadas , la ventaja normalizada de la salida es:
El objetivo maximiza la ventaja recortada token a token restando la divergencia KL insesgada (estimador de Schulman):
donde la penalización KL por token utiliza la aproximación asintóticamente insesgada de mínima varianza:
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:
- 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.
- 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. - El clip: se toma el mínimo entre
ratio × A_iyratio recortado a [1−ε, 1+ε] × A_i. El mínimo hace que, cuando la ventaja es positiva, subir el ratio más allá de1+εno dé más objetivo (no hay incentivo para pasarse), y cuando es negativa, bajarlo más allá de1−εtampoco.
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.
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.
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.
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:
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íntoma | Causa probable | Qué 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
- 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.
- 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. - 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.
GRPOTrainerde 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.- Unsloth + vLLM hacen viable correr esto en una sola GPU — con
processing_class=tokenizeren 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
- LLM Course, Chapter 12 — Build reasoning models — el capítulo original, con el notebook de GRPO + Unsloth sobre GSM8K y la explicación interactiva del algoritmo.
- DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning (arXiv 2501.12948) — el paper de §2: R1-Zero, el momento "Aha" y las cuatro fases de R1.
- DeepSeekMath (arXiv 2402.03300) — donde GRPO se presentó por primera vez, un año antes de R1; la sección 4 tiene la fórmula de §3 y la comparación con PPO.
- DAPO: An Open-Source LLM Reinforcement Learning System at Scale (arXiv 2503.14476) — clip asimétrico, dynamic sampling y loss por token; el sistema completo, con código.
- Understanding R1-Zero-Like Training: A Critical Perspective (arXiv 2503.20783) — el análisis de los sesgos de longitud y de normalización que da lugar a Dr. GRPO.
- TRL — GRPOTrainer — la firma exacta de las reward functions, el formato de
completions, y cada opción deGRPOConfig(β, ε, tipo de loss, vLLM) en la versión que tengas instalada.