Curso / HF LLM Course / Capítulo 3
● piloto de formato

Hugging Face · LLM Course · Capítulo 3

Fine-tuning: de usar un modelo a entrenarlo vos

Todo lo del Capítulo 2 asumía un modelo ya entrenado. Acá lo entrenás vos — primero con la forma clásica (ajustar todos los pesos), después con la que de verdad vas a usar en 2026: tocar una fracción mínima de ellos.

0. De usar un modelo a entrenarlo

El ejemplo del capítulo original sigue siendo el correcto para aprender la mecánica: tomar BERT (un encoder-only, Capítulo 1) y ajustarlo para una tarea concreta — detectar si dos frases dicen lo mismo (el dataset MRPC, 5.801 pares de oraciones). Es chico, rápido de entrenar, y expone cada pieza del proceso sin la complejidad de un LLM de 30B parámetros de por medio.

Vas a ver tres formas de llegar al mismo resultado, de más alto nivel a más bajo nivel:

Trainer API

Unas pocas líneas, la librería maneja el resto — lo que usás el 95% del tiempo.

Training loop manual

Cada paso explícito en PyTorch puro — para cuando necesitás control que la API no te da.

🤗 Accelerate

El mismo loop manual, portable a multi-GPU/TPU con casi ningún cambio de código.

1. Preparar los datos

Con datasets (la librería, no el concepto) cargar un dataset del Hub es una línea:

from datasets import load_dataset

raw_datasets = load_dataset("glue", "mrpc")
# DatasetDict con splits train (3668) / validation (408) / test (1725)

Tokenizar un par de oraciones (no una sola, como en el Capítulo 2) agrega un campo nuevo:

inputs = tokenizer("Esta es la primera oración.", "Esta es la segunda.")
# input_ids, attention_mask, y ahora también token_type_ids:
# [0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1] — 0 = primera oración, 1 = segunda

token_type_ids le dice al modelo dónde termina una oración y empieza la otra — BERT lo necesita porque se preentrenó con un objetivo llamado next sentence prediction (predecir si una oración sigue a otra en el texto original).

🟢 Actualización 2026

token_type_ids es una particularidad de BERT y su familia — ni DistilBERT lo devuelve. Los LLM decoder-only de hoy (Capítulo 2) no tienen equivalente: la noción de "dos segmentos" la resuelve el chat template, con roles (system/user/ assistant) en vez de un ID binario de segmento. Es la misma necesidad — decirle al modelo qué parte del input es qué — resuelta de forma completamente distinta una generación después.

Para tokenizar el dataset completo sin cargarlo todo en RAM, Dataset.map(batched=True) aplica la función en lotes, aprovechando que el tokenizer de tokenizers está escrito en Rust y paraleliza internamente:

def tokenize_function(example):
    return tokenizer(example["sentence1"], example["sentence2"], truncation=True)

tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)

Notá que acá no se pasa padding=True. La razón es padding dinámico: rellenar cada lote hasta su propio máximo (no el máximo de todo el dataset) ahorra cómputo real cuando las longitudes varían mucho. DataCollatorWithPadding hace ese relleno al momento de armar cada batch, no antes.

2. Fine-tunear con la Trainer API

Con los datos listos, definir el entrenamiento son tres objetos:

from transformers import TrainingArguments, AutoModelForSequenceClassification, Trainer

training_args = TrainingArguments("test-trainer", eval_strategy="epoch")
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)

trainer = Trainer(
    model, training_args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
    data_collator=data_collator,
    processing_class=tokenizer,
    compute_metrics=compute_metrics,
)
trainer.train()

num_labels=2 importa: BERT nunca vio esta tarea en su preentrenamiento, así que AutoModelForSequenceClassification descarta el head original y le pone uno nuevo, inicializado al azar — el warning que ves al cargar el modelo es exactamente eso, no un error. compute_metrics es la función que convierte logits en accuracy/F1 usando la librería evaluate; sin ella, Trainer solo te va a reportar la loss.

Tres flags de TrainingArguments que valen la pena conocer de memoria:

FlagQué hace
fp16=TrueEntrena en precisión mixta (16 bits) — más rápido, menos memoria, casi sin pérdida de calidad
gradient_accumulation_steps=NAcumula gradientes de N batches chicos antes de actualizar — simula un batch N veces más grande sin más VRAM
lr_scheduler_type="cosine"Cambia cómo decae el learning rate durante el entrenamiento (el default es lineal)

3. Un training loop a mano

Todo lo que Trainer hace por vos, en PyTorch puro, es esto — vale la pena leerlo una vez para saber qué se esconde detrás del .train():

model.train()
for epoch in range(num_epochs):
    for batch in train_dataloader:
        batch = {k: v.to(device) for k, v in batch.items()}
        outputs = model(**batch)
        loss = outputs.loss
        loss.backward()

        optimizer.step()
        lr_scheduler.step()
        optimizer.zero_grad()

Cinco pasos, siempre en este orden: forward (calcular la salida y la loss), backward (propagar el gradiente), optimizer.step() (actualizar pesos con ese gradiente), scheduler.step() (ajustar el learning rate), zero_grad() (limpiar el gradiente para el próximo batch — si te olvidás este paso, los gradientes se acumulan silenciosamente entre batches).

El optimizador es AdamW — Adam con weight decay desacoplado, el estándar para entrenar transformers. Para correr el mismo loop en varias GPUs sin reescribirlo, Accelerate envuelve los mismos objetos:

from accelerate import Accelerator

accelerator = Accelerator()
train_dl, eval_dl, model, optimizer = accelerator.prepare(train_dataloader, eval_dataloader, model, optimizer)
# reemplazar loss.backward() por accelerator.backward(loss), nada más

4. Leer las curvas de entrenamiento

Un número de loss aislado no te dice nada — la forma de la curva en el tiempo sí. Esta parte no estaba en el curso original de 2022; se agregó porque interpretar mal una curva es de los errores más comunes y más caros (entrenás horas para nada, o peor, publicás un modelo roto).

Curva sana vs. overfitting Loss de entrenamiento y validación: en la curva sana ambas bajan y convergen; en overfitting la de validación empieza a subir mientras la de entrenamiento sigue bajando. Sana épocas → — train ┄ val Overfitting épocas →
Izquierda: train y validación bajan juntas y convergen. Derecha: la de entrenamiento sigue bajando pero la de validación empieza a subir — el modelo está memorizando, no aprendiendo.
PatrónSíntomaQué probar
OverfittingLoss de train sigue bajando, la de validación sube o se estanca; accuracy de train mucho más alta que la de validaciónEarly stopping, más regularización (dropout, weight decay), más datos, un modelo más chico
UnderfittingAmbas losses se quedan altas y se estancan tempranoMás capacidad, más épocas, ajustar el learning rate, revisar la calidad de los datos
ErráticaFluctuaciones fuertes, sin tendencia claraBajar el learning rate, subir el batch size, gradient clipping

Un detalle que confunde la primera vez: la curva de accuracy avanza en escalones, no suave como la loss. La loss puede mejorar aunque la predicción siga siendo incorrecta (0.3 → 0.4 en un clasificador binario sigue siendo "0"); la accuracy solo salta cuando la predicción cruza el umbral que la vuelve correcta.

5. Lo que cambió: fine-tuning completo ya no es la norma

Todo lo de arriba ajusta los 110 millones de parámetros de BERT, todos. Para un modelo de ese tamaño está perfecto — es rápido, es simple, y no hay ninguna razón para complicarlo. Para un LLM de 8B, 30B o 70B parámetros, hacer lo mismo significa guardar en memoria el estado del optimizador (Adam guarda dos números extra por parámetro) para cada uno de esos parámetros — varias veces el tamaño del modelo, solo en overhead de entrenamiento.

LoRA (Low-Rank Adaptation) ataca esto desde otro ángulo: en vez de tocar la matriz de pesos original W, la congelás, y entrenás dos matrices mucho más chicas (A y B) cuyo producto se suma a la salida de W. El modelo base nunca cambia; lo único que se entrena y se guarda es ese par de matrices de bajo rango — típicamente menos del 1% de los parámetros originales.

from peft import LoraConfig, get_peft_model

config = LoraConfig(r=16, lora_alpha=32, target_modules=["q_proj", "v_proj"])
model = get_peft_model(base_model, config)
model.print_trainable_parameters()
# trainable params: 4,194,304 || all params: 7,000,000,000 || trainable%: 0.06%

🟢 Esto ya lo viste correr en tus propios proyectos

El ciclo generacional de interpretante-lacaniano entrena exactamente así: CPT inicial sobre el corpus, y después cada generación se ajusta con LoRA vía Unsloth sobre la producción curada — nunca se reentrena el modelo de 31B completo. Y en la página de NVFP4 en difusión viste el caso límite: cuando el modelo base ya está cuantizado a 4 bits, ni siquiera podés sumarle A·B a los pesos — hay que fusionar esa suma dentro del kernel en cada forward, porque no existe un "peso intermedio" al que sumarle algo en un formato de 16 valores por bloque.

QLoRA combina las dos ideas: el modelo base se carga cuantizado (originalmente en NF4, un formato de 4 bits distinto al NVFP4 de Blackwell que ya conocés) y congelado, y las matrices LoRA se entrenan encima en precisión más alta — la razón por la que hoy podés fine-tunear un modelo de 70B en una sola GPU de consumo.

Fine-tuning completoLoRAQLoRA
Parámetros entrenados100%<1%<1%
Peso baseSe modificaCongeladoCongelado y cuantizado (4 bits)
Mejor paraModelos chicos, máxima calidadLLMs grandes, hardware limitadoLLMs grandes, hardware muy limitado

Nada de esto invalida las secciones anteriores — Trainer, el training loop manual, y las curvas de aprendizaje funcionan exactamente igual con LoRA. Lo único que cambia es qué parámetros tienen requires_grad=True.

6. Resumen

  1. Fine-tuning parte de datos tokenizados en lotes con padding dinámico — nunca rellenes al máximo del dataset entero si podés rellenar al máximo del batch.
  2. Trainer resuelve el 95% de los casos en pocas líneas; el training loop manual existe para cuando necesitás control fino, y Accelerate lo escala a multi-GPU casi gratis.
  3. Las curvas de loss y accuracy tienen formas reconocibles — overfitting, underfitting, entrenamiento errático — cada una con su propio remedio.
  4. Ajustar el 100% de los parámetros es la excepción hoy, no la regla: LoRA y QLoRA son el camino por defecto para cualquier LLM que no te entre cómodo en memoria para un fine-tune completo — y ya los viste corriendo en tus propios proyectos.