Fine-tuning — Módulo 8: evaluar si de verdad sirvió
Octavo módulo del Curso de Fine-tuning y cierre del bloque intermedio: la validation loss no te dice si el modelo sirve. Holdout intocado, comparación A/B honesta contra el base, métricas por tipo de tarea, LLM-as-judge y cómo detectar catastrophic forgetting con una suite de regresión de capacidades generales.
☰ Índice del contenido
- La tesis del módulo
- Por qué la loss no alcanza
- El holdout que nunca se tocó
- La comparación A/B honesta
- La trampa del base mal formateado
- Métricas según el tipo de tarea
- LLM-as-judge para salidas libres
- Catastrophic forgetting
- La tabla que hay que producir
- Errores comunes
- Cierre del bloque intermedio
- La frase para llevarse
- Qué viene
- Material relacionado
El módulo 7 terminó con una fila incómoda en la tabla de diagnóstico: “val loss buena pero el modelo responde mal en la práctica”, y la nota de que ese caso no tiene arreglo de hiperparámetro. Es hora de enfrentarlo. Tienes un adapter de 80 MB, curvas sanas y una validation loss de 0.87 que bajó desde 1.4. Todo indica éxito. Y todavía no tienes ninguna evidencia de que tu modelo sea mejor que el que descargaste. Este módulo cierra el bloque intermedio construyendo esa evidencia — o, con la misma frecuencia, demostrando que no existe.
La tesis del módulo
La validation loss mide qué tan bien predice el modelo el siguiente token de tu dataset; no mide si el modelo sirve. Son dos cosas distintas y se pueden mover en direcciones opuestas. La única evaluación que vale es comparar el modelo afinado contra el base, en tareas reales, sobre datos que ninguno de los dos vio nunca.
Por qué la loss no alcanza
La val loss es una métrica de verosimilitud: promedia, sobre cada token de tu split de validación, cuánta sorpresa le produce al modelo el token que efectivamente venía. Que ese número baje significa una cosa muy concreta: el modelo se volvió mejor prediciendo texto que se parece al de tu dataset.
Eso no es lo mismo que ser útil, por cuatro razones.
Premia imitar el estilo, no acertar. Si tus respuestas de referencia empiezan todas con “Estimado cliente,”, el modelo aprende esa apertura y baja la loss de forma medible sin haber mejorado en nada sustantivo.
Promedia sobre todos los tokens por igual. En “El plazo de garantía es de 6 meses”, el token que decide si la respuesta es correcta o desinformación es uno solo. Pesa lo mismo que “de”. Un modelo puede bajar su loss promedio mientras empeora justo en los tokens que importan.
No dice nada fuera de tu distribución. La val loss se calcula sobre datos que se parecen al entrenamiento por construcción. El usuario real pregunta cosas raras, y ahí la métrica es ciega.
Es incomparable entre modelos. No puedes contrastar la val loss de tu fine-tune con la del base para decidir si mejoraste: el base ni siquiera fue entrenado con tu formato, así que su loss sobre tus datos es alta por razones que no tienen que ver con la calidad. Comparar esos dos números es un error categórico y se ve seguido.
LO QUE MIDE CADA COSA
val loss ──► ¿predice bien el siguiente token
en textos como los del train?
│
│ NO implica
▼
evaluación ──► ¿resuelve la tarea mejor
que el modelo base?
│
│ NO implica
▼
regresión ──► ¿sigue sabiendo hacer
todo lo que sabía antes?
Las tres se miden por separado.
Bajar la primera no garantiza ninguna de las otras dos.
El holdout que nunca se tocó
Antes de medir nada, necesitas datos limpios. Y “limpios” tiene un significado estricto: el holdout se aparta antes de empezar el proyecto y no se mira hasta el final.
PARTICIÓN DE LOS DATOS (se hace UNA vez, al inicio)
dataset completo
│
├──► TRAIN (~85%) el modelo entrena aquí
│
├──► VAL (~5%) curvas y early stopping (M7)
│ el modelo lo VE indirectamente
│
└──► HOLDOUT (~10%) ┌────────────────────────┐
│ NO se toca. │
│ NO se usa para elegir │
│ hiperparámetros. │
│ Se abre UNA vez, al │
│ final, y se reporta. │
└────────────────────────┘
La distinción entre validación y holdout confunde y es importante. El split de validación sí influye en el modelo: lo usaste para early stopping y para elegir entre v2 y v3 en la tabla del módulo 7. Después de veinte corridas eligiendo por val loss, ese split ya está contaminado — no por entrenamiento directo, sino porque tú, el humano, optimizaste contra él. El holdout es el que se mantiene virgen para que el número final signifique algo.
Cuidado con la contaminación. Si tu dataset salió de tickets de soporte y el mismo cliente preguntó lo mismo tres veces, esas tres variantes casi idénticas pueden repartirse entre train y holdout. Tu modelo “acierta” porque memorizó, no porque generalice. Antes de particionar, deduplica por similitud — no por igualdad exacta, que casi nunca se da. Y si tus datos tienen orden temporal, particiona por fecha: entrena con lo viejo, evalúa con lo nuevo, que es la situación real de producción.
La comparación A/B honesta
La única pregunta que importa es: ¿es mejor que el base? Y para responderla con validez, todo salvo el modelo tiene que ser idéntico.
"""A/B entre el modelo base y el fine-tuned. La clave es que ambos
reciban EXACTAMENTE el mismo tratamiento salvo el adapter."""
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import PeftModel
MODELO_BASE = "Qwen/Qwen2.5-7B-Instruct"
ADAPTER = "./qwen7b-soporte-lora/final"
bnb = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16,
)
tok = AutoTokenizer.from_pretrained(MODELO_BASE)
base = AutoModelForCausalLM.from_pretrained(
MODELO_BASE, quantization_config=bnb, device_map="auto"
)
# Truco de PEFT: un solo modelo en memoria. disable_adapter() apaga el
# adapter temporalmente, así el "base" es literalmente el mismo objeto
# con los mismos pesos cuantizados. Evita diferencias espurias por
# cuantización distinta entre las dos cargas.
modelo = PeftModel.from_pretrained(base, ADAPTER)
modelo.eval()
def responder(mensajes: list[dict], con_adapter: bool) -> str:
entrada = tok.apply_chat_template(
mensajes, add_generation_prompt=True, return_tensors="pt"
).to(modelo.device)
def _gen():
with torch.no_grad():
return modelo.generate(
entrada,
max_new_tokens=256,
do_sample=False, # determinista: sin esto no hay comparación
pad_token_id=tok.pad_token_id,
)
if con_adapter:
salida = _gen()
else:
with modelo.disable_adapter(): # el base, mismos pesos base
salida = _gen()
return tok.decode(salida[0][entrada.shape[-1]:], skip_special_tokens=True)
Tres condiciones para que el A/B sea válido:
do_sample=Falseen ambos. Con muestreo, la diferencia que observes puede ser azar. Si necesitas evaluar con temperatura porque así vas a servir, corre cada caso 3-5 veces y promedia.- La misma cuantización.
disable_adapter()lo garantiza gratis, porque hay un solo conjunto de pesos base en memoria. - El mismo prompt exacto, incluido el system prompt. Y acá está la trampa que más resultados infla.
La trampa del base mal formateado
Este es el fraude metodológico más común del fine-tuning, y casi siempre es involuntario.
Tu modelo afinado aprendió a responder al chat template de tu dataset. Si evalúas el base pasándole texto plano concatenado a mano, o sin add_generation_prompt=True, o con el template de otra familia, el base va a responder mal — no porque sea peor, sino porque le hablaste en un idioma que no es el suyo. Comparas tu modelo bien formateado contra un base saboteado y concluyes que ganaste por goleada.
Las tres reglas para evitarlo:
- Usa
tok.apply_chat_template(..., add_generation_prompt=True)para ambos, nunca strings armados a mano. - Si el base es un modelo instruct, dale un system prompt razonable que describa la tarea. Tu fine-tune aprendió la tarea del entrenamiento; el base solo puede saberla si se la cuentas. Compararlo sin instrucciones es hacerle trampa.
- Agrega una tercera columna: el base con few-shot. Dos o tres ejemplos en el prompt es lo que cualquiera haría antes de invertir en fine-tuning, y es el baseline honesto. Si tu fine-tune no le gana claramente, la respuesta correcta puede ser no hacer fine-tuning.
Ojo: ese tercer contendiente es incómodo a propósito. El módulo 1 planteó cuándo conviene afinar; este módulo es donde esa decisión se somete a evidencia. Y evaluar contra un baseline débil para que tu trabajo se vea bien es la forma más silenciosa de perder meses.
Métricas según el tipo de tarea
No hay una métrica universal. Lo primero es clasificar tu tarea.
| Tipo de tarea | Métrica principal | Métricas de apoyo |
|---|---|---|
| Clasificación (intención, sentimiento, ruteo) | Accuracy, F1 macro | Matriz de confusión, F1 por clase |
| Extracción estructurada (JSON, campos) | % de JSON válido + exact match por campo | Precision/recall por campo |
| Formato / adherencia (esquema, longitud, idioma) | % que cumple la regla | Validación con pydantic o JSON Schema |
| Traducción / paráfrasis | Similitud semántica (embeddings) | chrF, BLEU como referencia secundaria |
| Resumen | LLM-as-judge con rúbrica | ROUGE (débil), fidelidad al original |
| Respuesta libre / asistente | LLM-as-judge pareado (win rate) | Preferencia humana en una muestra |
| Código | Tests que pasan (pass@1) | Compila / parsea |
La regla de oro: si tu tarea admite una métrica determinista, úsala. Es gratis, es reproducible y no tiene sesgos de juez. La mayoría de los fine-tunes de negocio son clasificación o extracción disfrazadas de generación, y para esos casos un assert bien puesto vale más que cualquier LLM juez.
"""Evaluación de una tarea de extracción estructurada.
Determinista, sin llamadas a ningún LLM juez, corre en segundos."""
import json
from statistics import mean
def evaluar_extraccion(casos: list[dict], con_adapter: bool) -> dict:
json_valido, campos_ok, exactos = [], [], []
for caso in casos:
salida = responder(caso["messages"][:-1], con_adapter=con_adapter)
esperado = json.loads(caso["messages"][-1]["content"])
# 1) ¿Es JSON parseable? Métrica base de cualquier salida estructurada.
try:
obtenido = json.loads(salida)
json_valido.append(1.0)
except json.JSONDecodeError:
json_valido.append(0.0)
campos_ok.append(0.0)
exactos.append(0.0)
continue
# 2) Acierto campo por campo: localiza QUÉ campo falla.
aciertos = [obtenido.get(k) == v for k, v in esperado.items()]
campos_ok.append(mean(aciertos) if aciertos else 0.0)
# 3) Exact match: el objeto completo correcto.
exactos.append(1.0 if obtenido == esperado else 0.0)
return {
"json_valido": round(mean(json_valido), 3),
"campos_correctos": round(mean(campos_ok), 3),
"exact_match": round(mean(exactos), 3),
"n": len(casos),
}
holdout = [json.loads(l) for l in open("datos/holdout.jsonl", encoding="utf-8")]
print("base :", evaluar_extraccion(holdout, con_adapter=False))
print("fine-tuned :", evaluar_extraccion(holdout, con_adapter=True))
Ese desglose en tres niveles es deliberado. Si json_valido sube de 0.71 a 0.99 pero exact_match no se mueve, aprendiste formato y no contenido — un resultado real y valioso, pero distinto del que creías tener.
LLM-as-judge para salidas libres
Cuando la salida es texto libre no hay exact match posible, y ahí entra el LLM-as-judge (glosar: usar un modelo de lenguaje como evaluador, entregándole la pregunta, las respuestas y una rúbrica para que emita un juicio). El tratamiento completo está en evals como tests y en la investigación sobre evaluación de agentes; acá va lo específico del fine-tuning.
Para comparar dos modelos, la comparación pareada (elegir cuál de dos respuestas es mejor) es más confiable que puntuar cada una por separado: a los jueces les cuesta mucho menos comparar que calibrar una escala absoluta.
"""Juez pareado con mitigación de sesgo de posición.
La métrica resultante es el WIN RATE del fine-tune contra el base."""
import json
import random
from openai import OpenAI
client = OpenAI()
RUBRICA = """Eres un evaluador estricto de respuestas de soporte al cliente.
Compara las dos respuestas a la misma consulta y elige la mejor según,
en este orden de prioridad:
1. CORRECCION: ¿los datos son correctos según el CONTEXTO de referencia?
Una respuesta con un dato inventado pierde, aunque suene mejor.
2. COMPLETITUD: ¿responde todo lo que se preguntó?
3. FORMATO: ¿sigue el formato pedido (tono, largo, estructura)?
4. CONCISION: a igualdad de lo anterior, gana la más breve.
Razona antes de decidir. Si son equivalentes, responde "empate".
Devuelve JSON: {"razon": "...", "ganadora": "A" | "B" | "empate"}
CONSULTA: {consulta}
CONTEXTO DE REFERENCIA: {referencia}
RESPUESTA A:
{a}
RESPUESTA B:
{b}"""
def juzgar(consulta: str, referencia: str, r_base: str, r_ft: str) -> str:
"""Devuelve 'base', 'ft' o 'empate'.
Sesgo de posición: los jueces tienden a preferir la primera opción.
Se mitiga aleatorizando el orden y traduciendo de vuelta.
"""
ft_es_a = random.random() < 0.5
a, b = (r_ft, r_base) if ft_es_a else (r_base, r_ft)
r = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": RUBRICA.format(
consulta=consulta, referencia=referencia, a=a, b=b)}],
response_format={"type": "json_object"},
temperature=0, # determinismo: sin esto el juez es ruido
)
ganadora = json.loads(r.choices[0].message.content)["ganadora"]
if ganadora == "empate":
return "empate"
gano_a = ganadora == "A"
return "ft" if gano_a == ft_es_a else "base"
def win_rate(casos: list[dict]) -> dict:
votos = {"ft": 0, "base": 0, "empate": 0}
for c in casos:
votos[juzgar(
c["consulta"], c["respuesta_ideal"],
responder(c["messages"], con_adapter=False),
responder(c["messages"], con_adapter=True),
)] += 1
decisivos = votos["ft"] + votos["base"]
return {
**votos,
# Win rate sobre casos decisivos. > 0.5 = el fine-tune gana.
"win_rate_ft": round(votos["ft"] / decisivos, 3) if decisivos else None,
}
Dos detalles hacen la diferencia entre un juez que informa y uno que decora: aleatorizar el orden (el sesgo de posición está bien documentado y puede valer varios puntos porcentuales) y entregar el contexto de referencia para que el juez pueda verificar los hechos en vez de premiar la respuesta que suena más segura.
Ojo: un win rate de 0.55 sobre 40 casos no es una victoria. Es ruido. Para diferencias de esa magnitud necesitas del orden de 200-300 casos, o aceptar que no puedes distinguir los dos modelos. Cuando el resultado queda cerca del 50 %, la conclusión honesta no es “empatamos”: es que el fine-tune no aportó lo suficiente como para ser detectable, que a efectos de decisión significa lo mismo que no aportar.
Catastrophic forgetting
Acá está el riesgo que la val loss jamás va a mostrarte, y el que más sorpresas produce en producción.
El catastrophic forgetting (glosar: olvido catastrófico; cuando un modelo, al ser entrenado en una tarea nueva, degrada o pierde capacidades que ya tenía) ocurre porque tu adapter modifica el comportamiento del modelo en todas las entradas, no solo en las de tu tarea. Entrenaste con 3.000 ejemplos de clasificación de tickets en una línea; el modelo aprendió que ante cualquier entrada, la respuesta correcta es corta y con formato de etiqueta. Ahora le pides que redacte un correo y devuelve categoria: correo.
LO QUE MIDES vs LO QUE PASA
capacidad
│
alta│═══════════╗ generales
│ ║ ── ── ── ── ── (razonar, redactar,
│ ╚═════════════ código, otros idiomas)
│
media│ NADIE MIDE ESTO
│ ╱─────────
│ ╱
│ ╱ tu tarea
│ ╱ (lo único que mides)
baja│──────╱
└──────────────────────────────► entrenamiento
inicio fin
El fine-tune sube tu tarea y hunde el resto.
Sin suite de regresión, la caída es invisible
hasta que un usuario la encuentra.
Los factores que lo agravan, en orden de impacto: muchas épocas (el módulo 7 ya lo advirtió), learning rate alto, rank alto sobre un dataset chico y datos poco diversos (si todos tus ejemplos son de un solo tipo, el modelo colapsa a ese tipo).
La defensa es una suite de regresión de capacidades generales: un conjunto chico y fijo de tareas que tu modelo debería seguir sabiendo hacer aunque no tengan nada que ver con tu fine-tune. No necesitas MMLU completo; necesitas 40-60 casos que corran en minutos.
"""Suite de regresión de capacidades generales.
Vive en el repo, corre después de CADA fine-tune, y compara base vs FT.
El objetivo no es que el FT gane: es que NO PIERDA."""
REGRESION = [
# -- razonamiento aritmético básico --
{"id": "mat_01", "cap": "razonamiento",
"pregunta": "Si un producto cuesta $12.500 y tiene 15% de descuento, "
"¿cuánto se paga? Responde solo el monto.",
"esperado": "10625"},
# -- seguir instrucciones de formato ajenas a la tarea entrenada --
{"id": "fmt_01", "cap": "instrucciones",
"pregunta": "Lista exactamente 3 colores primarios, uno por línea, "
"sin numerar y sin texto adicional.",
"esperado": None}, # validado con una función, no con string exacto
# -- conocimiento general --
{"id": "gen_01", "cap": "conocimiento",
"pregunta": "¿En qué océano está la isla de Pascua? Una palabra.",
"esperado": "pacífico"},
# -- capacidad de decir que no sabe --
{"id": "abs_01", "cap": "abstención",
"pregunta": "¿Cuál fue la facturación de mi empresa en marzo?",
"esperado": None}, # debe indicar que no tiene esa información
# -- redacción libre: el primer síntoma del colapso de formato --
{"id": "red_01", "cap": "redacción",
"pregunta": "Escribe un párrafo de 3 oraciones sobre el otoño.",
"esperado": None}, # se valida: >= 3 oraciones, sin formato de etiqueta
# -- idioma: que no se te olvide responder en español --
{"id": "idi_01", "cap": "idioma",
"pregunta": "Traduce al inglés: 'la reunión se movió para el jueves'.",
"esperado": None},
]
def correr_regresion(con_adapter: bool) -> dict:
resultados = {}
for caso in REGRESION:
salida = responder(
[{"role": "user", "content": caso["pregunta"]}],
con_adapter=con_adapter,
).strip()
if caso["esperado"] is not None:
ok = caso["esperado"].lower() in salida.lower()
else:
# Heurísticas baratas que atrapan el colapso de formato:
# respuestas anormalmente cortas o con pinta de etiqueta.
ok = len(salida) > 20 and not salida.startswith("categoria:")
resultados[caso["id"]] = {"cap": caso["cap"], "ok": ok, "salida": salida}
return resultados
antes = correr_regresion(con_adapter=False)
despues = correr_regresion(con_adapter=True)
# Lo único que importa: qué se ROMPIÓ que antes funcionaba.
regresiones = [
cid for cid in antes
if antes[cid]["ok"] and not despues[cid]["ok"]
]
print(f"Capacidades perdidas: {len(regresiones)} / {len(REGRESION)}")
for cid in regresiones:
print(f" {cid} ({antes[cid]['cap']}): {despues[cid]['salida'][:80]!r}")
La lógica del cierre es lo importante: no mides el puntaje absoluto, mides qué se rompió. Un caso que el base también fallaba no es una regresión tuya. Y ese conjunto regresiones, si no está vacío, es un motivo legítimo para descartar el adapter aunque haya ganado en tu tarea.
Las mitigaciones, cuando aparece el problema:
| Mitigación | Cómo | Costo |
|---|---|---|
| Menos épocas / LR más bajo | Lo primero que hay que probar | Gratis |
| Rank más bajo | r=8 en vez de r=32: menos capacidad de sobrescribir | Gratis |
| Mezclar datos generales | 5-15 % de ejemplos de instrucciones variadas en el train | Curar esos datos |
| Bajar el escalado del adapter | lora_alpha menor: el adapter influye menos | Gratis, cuesta desempeño en tu tarea |
| Apagar el adapter cuando no aplique | disable_adapter() para consultas fuera de tarea | Requiere un router |
La de mezclar datos generales es la que mejor funciona y la que más gente omite. Un 10 % de ejemplos de conversación variada actúa como ancla y le recuerda al modelo que sus otras capacidades siguen siendo válidas.
La tabla que hay que producir
Todo lo anterior converge en una sola tabla. Este es un ejemplo ilustrativo de un caso de clasificación de tickets — con la conclusión incómoda que sale más seguido de lo que se admite:
| Métrica (holdout, n=300) | Base | Base + few-shot (3 ej.) | Fine-tuned |
|---|---|---|---|
| Accuracy de clasificación | 0.71 | 0.89 | 0.91 |
| F1 macro (clases raras) | 0.58 | 0.79 | 0.82 |
| Formato válido | 0.62 | 0.97 | 1.00 |
| Win rate vs base (juez) | — | 0.68 | 0.71 |
| Regresión de capacidades | 0/6 | 0/6 | 2/6 perdidas |
| Latencia p95 | 890 ms | 1.340 ms | 850 ms |
| Tokens de prompt por consulta | 180 | 620 | 180 |
| Costo de mantención | Ninguno | Ninguno | Reentrenar por cada cambio |
Léela con calma, porque la conclusión no es la obvia.
El fine-tune gana en todas las métricas de calidad. Pero le gana al few-shot por dos puntos de accuracy — dentro del margen de ruido para n=300 — y a cambio perdió dos de seis capacidades generales. El few-shot llega al 89 % sin entrenar nada, sin GPU, sin adapter que versionar y sin riesgo de olvido.
¿Dónde gana de verdad el fine-tune? En las dos filas de abajo: 440 tokens menos de prompt por consulta y 490 ms menos de latencia. Si atiendes 100.000 consultas al mes, eso es una reducción de costo y latencia perfectamente real.
Entonces la respuesta correcta depende del volumen, y esa es exactamente la clase de decisión que solo se puede tomar con la tabla completa a la vista:
- Volumen bajo o medio → el few-shot gana. El fine-tune no justifica su costo de mantención ni el riesgo de las capacidades perdidas.
- Volumen alto → el fine-tune gana por economía, pero primero hay que arreglar las regresiones (menos épocas, mezclar datos generales) y volver a correr la tabla.
Cuidado: la fila de regresión de capacidades es la que casi nadie incluye, y es la única que puede convertir un “mejoramos 20 puntos” en “no lo desplieguen”. Ponla en la tabla desde el primer día, cuando todavía no duele, no cuando ya defendiste el proyecto en una reunión.
Errores comunes
- Reportar la val loss como resultado. No es comparable entre modelos, no mide utilidad y no significa nada para quien decide. Reporta métricas de tarea sobre el holdout.
- Comparar contra un base mal formateado. Sin chat template correcto, sin
add_generation_prompto sin system prompt, el base pierde por razones de forma. Es la trampa que infla más resultados en la industria. - Omitir el baseline de few-shot. Es lo que cualquiera haría sin fine-tuning y muchas veces alcanza. Si tu tabla no lo tiene, tu tabla no responde la pregunta que el negocio hace.
- Evaluar con
do_sample=True. Cualquier diferencia observada puede ser azar. Determinista para comparar; con muestreo solo si promedias varias corridas. - No medir regresión de capacidades. Es el daño que aparece en producción tres semanas después, por una consulta que nadie anticipó, y que nadie conecta con el fine-tune.
- Tocar el holdout durante el desarrollo. Si lo usaste para elegir hiperparámetros, dejó de ser holdout y tu número final está inflado. Aparta uno nuevo o acepta que el resultado es optimista.
- Concluir con 30 casos. Las diferencias de pocos puntos porcentuales necesitan cientos de casos para ser distinguibles del ruido. Con muestras chicas la conclusión honesta es “no puedo distinguirlos”.
- No versionar el set de evaluación. Si el archivo cambia entre corridas, la comparación con la semana pasada es ficción. Va en git, con nombre versionado.
Cierre del bloque intermedio
Con estos cuatro módulos tienes el ciclo completo y honesto: entiendes qué hace LoRA por dentro y por qué sus hiperparámetros son los que son, sabes correr un SFT de punta a punta con las verificaciones que evitan los desastres silenciosos, puedes leer las curvas para saber si el entrenamiento está roto, y ahora puedes demostrar con evidencia si el resultado sirve. Ese último paso es el que convierte el fine-tuning de un experimento entretenido en una decisión de ingeniería defendible.
La frase para llevarse
“Un fine-tune que no supera al modelo base con few-shot es un modelo que gastó GPU para empatar. Y uno que gana en tu tarea pero perdió dos capacidades generales no ganó: cambió un problema visible por uno que va a aparecer en producción.”
Qué viene
Módulo 9: DPO en la práctica, y arranca el bloque avanzado. El SFT le enseña al modelo qué responder, pero solo puede mostrarle respuestas buenas — nunca le dice qué evitar. El Direct Preference Optimization entrena sobre pares de preferencia (una respuesta elegida y una rechazada para el mismo prompt) y le enseña la diferencia entre ambas, que es una señal que el SFT no puede transmitir. Vamos a ver de dónde salen esos pares, por qué DPO reemplazó a PPO en la mayoría de los casos, el DPOTrainer con el beta que controla cuánto se aleja del modelo de referencia, y por qué DPO siempre va después de un SFT y nunca en su lugar.
Material relacionado
Preguntas sobre este curso
Regístrate gratis para hacer preguntas y guardar tu progreso.
✉ Entérate de lo nuevo
Recibe un aviso cuando publique nuevos cursos, workshops o investigaciones. Sin spam.