GRATÍCULAinstrumento de maestría
BancoRTX 5090 · GB202
Rev2026.06
Entrar
N2 · Post-training + RL/L3

Datos y SFT: el 80% del resultado está aquí

Objetivo de maestría

dominar lo que de verdad decide la calidad de un fine-tune — los datos y su formato — no los hiperparámetros. Un dataset bien construido con LoRA básico bate a un dataset mediocre con la mejor receta.


3.1La verdad incómoda del fine-tuning

La mayoría de fine-tunes fallidos no fallan por hiperparámetros: fallan por datos mal formateados, máscaras mal puestas o ejemplos inconsistentes. El orden de impacto es: calidad de los datos ≫ formato/máscaras ≫ hiperparámetros ≫ método PEFT. Esta lección ataca los tres primeros.


3.2Formatos de datos

Tres formatos que verás:

  • ChatML / formato de chat del modelo: mensajes con roles (system, user, assistant) y tokens especiales. Es el estándar moderno y el que produce apply_chat_template. Úsalo siempre que entrenes un modelo de chat.
  • ShareGPT: conversaciones multi-turno como lista de turnos ({"from": "human"/"gpt", "value": ...}). Común en datasets; conviértelo a chat template.
  • Alpaca: {instruction, input, output}. Simple, single-turn. Lo conviertes a chat.

Regla: internamente todo acaba pasando por el chat template del modelo objetivo. Cada modelo tiene su propia plantilla (sus tokens de rol); usar la equivocada degrada o rompe el entrenamiento.


3.3Máscaras de pérdida: no entrenes sobre el prompt

Idea crítica y a menudo ignorada: en SFT solo quieres que el modelo aprenda a generar la respuesta, no a "predecir el prompt". Por eso los tokens del prompt (system + user) deben tener la pérdida enmascarada (label = -100, que PyTorch ignora en cross-entropy); solo los tokens de la respuesta del assistant contribuyen a la pérdida.

Por qué importa: si entrenas también sobre el prompt, el modelo gasta capacidad aprendiendo a reproducir instrucciones (inútil) y diluye la señal de lo que importa (responder). En tareas con prompts largos y respuestas cortas, esto degrada notablemente.

python
1# Concepto de máscara (TRL/Unsloth lo hacen por ti con train_on_responses_only,
2# pero debes entender qué pasa por debajo):
3# labels = input_ids.clone()
4# labels[: len(prompt_tokens)] = -100   # ignora el prompt en la loss
5# loss = cross_entropy(logits, labels, ignore_index=-100)

En la práctica, con Unsloth usas el helper:

python
1from unsloth.chat_templates import train_on_responses_only
2trainer = train_on_responses_only(
3    trainer,
4    instruction_part="<|im_start|>user\n",
5    response_part="<|im_start|>assistant\n",
6)  # enmascara todo menos la parte del assistant

3.4Packing: aprovechar cada token de cómputo

Las secuencias tienen longitudes distintas; si rellenas con padding hasta max_seq, desperdicias cómputo en tokens de relleno. Packing concatena varios ejemplos cortos en una sola secuencia de longitud max_seq (con separadores y máscara de atención que evita que un ejemplo "vea" al siguiente). Sube el throughput de entrenamiento sin cambiar el resultado.

python
1SFTConfig(..., packing=True)   # TRL empaqueta automáticamente

3.5Preservar el razonamiento (al fine-tunear reasoners)

Si tu base es un modelo con razonamiento (genera <think>...), un SFT con respuestas directas puede matar esa capacidad (le enseñas a no razonar). Regla empírica (Unsloth/Qwen3): mantén ≥75% de ejemplos con cadena de pensamiento y ≤25% directos si quieres conservar el razonamiento. O sepáralos por un flag de control.


3.6Laboratorio L3.1 — Construir un dataset de SFT verificable (genérico)

Construiremos un dataset texto→SQL, porque es una tarea verificable (podrás darle recompensa en L7/L8) y genérica. El patrón sirve para cualquier tarea con respuesta comprobable.

python
1# lab_n2l3_dataset.py — prepara un dataset SFT texto->SQL en formato chat con máscara
2from datasets import load_dataset
3
4raw = load_dataset("gretelai/synthetic_text_to_sql", split="train[:8000]")
5
6SYSTEM = ("Eres un asistente que convierte preguntas en lenguaje natural a SQL. "
7          "Responde SOLO con la consulta SQL, sin explicación.")
8
9def to_chat(ex, tokenizer):
10    msgs = [
11        {"role":"system","content": SYSTEM},
12        {"role":"user","content": f"Esquema:\n{ex['sql_context']}\n\nPregunta: {ex['sql_prompt']}"},
13        {"role":"assistant","content": ex["sql"]},
14    ]
15    return {"text": tokenizer.apply_chat_template(msgs, tokenize=False),
16            "gold_sql": ex["sql"], "schema": ex["sql_context"]}  # guardamos gold para evaluar/recompensar
17
18# Buenas prácticas de calidad ANTES de entrenar:
19# 1) deduplicar prompts casi idénticos
20# 2) filtrar respuestas vacías o malformadas
21# 3) verificar que el SQL parsea (sqlglot.parse_one) -> descarta ejemplos rotos
22import sqlglot
23def is_valid_sql(ex):
24    try: sqlglot.parse_one(ex["sql"]); return True
25    except Exception: return False
26raw = raw.filter(is_valid_sql)
27print(f"Ejemplos válidos: {len(raw)}")

Líneas no triviales:

  • Guardamos gold_sql y schema: los necesitarás como verificador de recompensa en L8 (¿la SQL generada produce el mismo resultado que la gold?). Diseña los datos pensando ya en cómo verificarás.
  • is_valid_sql: filtrar ejemplos rotos antes de entrenar evita enseñarle basura. La limpieza de datos es trabajo de ingeniería, no opcional.
  • system consistente: un system prompt fijo y claro reduce la varianza de comportamiento.

3.7Laboratorio L3.2 — SFT con máscara de respuestas

python
1# lab_n2l3_sft.py — entrena el SFT texto->SQL con máscara correcta
2from unsloth import FastLanguageModel
3from unsloth.chat_templates import train_on_responses_only
4from trl import SFTTrainer, SFTConfig
5
6model, tok = FastLanguageModel.from_pretrained("unsloth/Qwen3-4B-Instruct", max_seq_length=2048, load_in_4bit=True)
7model = FastLanguageModel.get_peft_model(model, r=16, lora_alpha=16,
8    target_modules=["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"],
9    use_gradient_checkpointing="unsloth")
10
11# (usa el dataset de L3.1, mapeado con este tokenizer)
12trainer = SFTTrainer(model=model, tokenizer=tok, train_dataset=ds,
13    args=SFTConfig(per_device_train_batch_size=4, gradient_accumulation_steps=2,
14                   num_train_epochs=2, learning_rate=2e-4, packing=False,
15                   logging_steps=20, optim="paged_adamw_8bit", output_dir="./sft_sql"))
16trainer = train_on_responses_only(trainer,
17    instruction_part="<|im_start|>user\n", response_part="<|im_start|>assistant\n")
18trainer.train()
19model.save_pretrained("adapters/sql-sft")   # tu baseline SFT para comparar contra RL en L8

Este adaptador sql-sft es tu baseline. En L8, el agente entrenado con RL deberá batirlo — ese es el checkpoint C2b(b).


3.8Ejercicios

E1. Entrena el mismo SFT con y sin train_on_responses_only. Evalúa exact-match en un holdout. ¿Cuánto mejora enmascarar el prompt?

E2. Activa packing=True y mide el throughput de entrenamiento (tok/s) vs sin packing. ¿Cuánto subió?

E3. Introduce a propósito 500 ejemplos con SQL malformada (sin filtrar) y entrena. ¿Cómo se degrada el exact-match? (Lección sobre calidad de datos.)

3.9Trampas comunes

  • No enmascarar el prompt → el modelo "aprende" a repetir instrucciones.
  • Chat template equivocado (de otro modelo) → tokens de rol incorrectos.
  • No limpiar/validar datos → enseñas errores.
  • Matar el razonamiento al hacer SFT directo sobre un reasoner.

3.10Referencias

  • HF LLM Course (fine-tuning), docs Unsloth (chat templates, train_on_responses_only), TRL SFTTrainer (packing).