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.
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.
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.
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.
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.
# 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)
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.
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:
from trl import GRPOConfig, GRPOTrainer
def reward_formato(completions, **kwargs):
"""1.0 si respeta <think>...</think> seguido de la respuesta, 0.0 si no."""
import re
patron = re.compile(r"^<think>.*?</think>\s*\S+", re.DOTALL)
return [1.0 if patron.match(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(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, # solo prompts — las respuestas las genera GRPO
)
trainer.train()
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.
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.
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).
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, tokenizer=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)
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.
🔧 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. 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.
- 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. - 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.
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.- Unsloth + vLLM hacen viable correr esto en una sola GPU — 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á.