El primer método de adaptación: reentrenar todos los pesos del modelo con tus propios datos. Entendemos qué cambia internamente, cuánta memoria necesita, y lo implementamos desde cero.
El fine-tuning completo (full fine-tuning en inglés) hace exactamente lo mismo que aprendiste en el curso de redes neuronales: forward pass, calcular el loss, backward pass, actualizar pesos. La diferencia es que en vez de empezar con pesos aleatorios, empiezas con los pesos ya entrenados de un modelo grande.
Los pesos del Transformer preentrenado se ajustan, y la cabeza nueva se entrena desde cero.
DistilBERT-base tiene: · 6 bloques Transformer (versión comprimida de BERT) · 66.362.880 parámetros en total (66 millones) · Cada parámetro es un float32 = 4 bytes Cabeza de clasificación NUEVA (para 2 clases): · Una capa Linear: 768 entradas × 2 salidas = 1.536 parámetros · Más bias: 2 parámetros · Total cabeza: 1.538 parámetros (~0.002% del modelo) Durante fine-tuning se actualizan: 66.362.880 + 1.538 = 66.364.418 parámetros ← TODOS (la cabeza nueva + todos los pesos del Transformer)
Aquí está la razón por la que el fine-tuning completo de modelos grandes es caro. Durante el entrenamiento, la GPU necesita guardar mucho más que solo los pesos del modelo:
Para DistilBERT (66M parámetros) con fine-tuning completo: 1) LOS PESOS DEL MODELO (model weights): 66M params × 4 bytes/param = ~252 MB 2) LOS GRADIENTES (uno por cada peso): 66M params × 4 bytes/param = ~252 MB 3) EL ESTADO DEL OPTIMIZADOR (Adam guarda 2 valores por peso): 66M params × 8 bytes = ~504 MB (Adam guarda momentum y variance — esto es lo más caro) 4) LAS ACTIVACIONES INTERMEDIAS (para poder calcular los gradientes): Depende del tamaño del lote. Con batch_size=16, texto de 128 tokens: ~400 MB TOTAL APROXIMADO: 252 + 252 + 504 + 400 = ~1.4 GB de GPU (DistilBERT es pequeño; BERT-large (340M params) necesita ~6 GB) (LLaMA-7B (7B params) necesita ~60 GB sin ninguna optimización)
Vamos a hacer fine-tuning de DistilBERT para clasificar sentimiento de reseñas de películas (dataset IMDB: 25.000 reseñas de entrenamiento etiquetadas como positivas o negativas). Este código funciona en Google Colab con GPU gratuita o en cualquier máquina con GPU.
pip install transformers datasets evaluate accelerate
import torch
from transformers import (
AutoTokenizer,
AutoModelForSequenceClassification,
TrainingArguments,
Trainer,
)
from datasets import load_dataset
import evaluate
import numpy as np
print("GPU disponible:", torch.cuda.is_available())
print("Dispositivo:", torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU")
# Cargar IMDB: 25k train + 25k test, etiquetas 0=negativo, 1=positivo
dataset = load_dataset("imdb")
print(dataset)
# Ver un ejemplo
print(dataset["train"][0]["text"][:200]) # primeros 200 caracteres
print("Etiqueta:", dataset["train"][0]["label"]) # 0 o 1
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
def tokenize_batch(batch):
# Aplicamos el tokenizer a cada texto del lote
# truncation=True: cortar si pasa de 512 tokens (límite de DistilBERT)
return tokenizer(
batch["text"],
padding="max_length", # rellenar hasta 512 (o max_length especificado)
truncation=True,
max_length=256, # usamos 256 para ahorrar memoria (no 512)
)
# Aplicar la tokenización a todo el dataset de una vez (eficiente)
tokenized = dataset.map(tokenize_batch, batched=True)
print(tokenized)
# num_labels=2: dos clases (negativo y positivo)
model = AutoModelForSequenceClassification.from_pretrained(
"distilbert-base-uncased",
num_labels=2
)
# Contar los parámetros
total = sum(p.numel() for p in model.parameters())
print(f"Parámetros totales: {total:,}")
print(f"Memoria aproximada (solo pesos): {total * 4 / 1e6:.1f} MB")
El mensaje de advertencia es completamente normal — nos dice que la cabeza de clasificación se inicializó con pesos aleatorios (porque es nueva) y que debemos entrenar antes de usar. Eso es exactamente lo que vamos a hacer.
metric = evaluate.load("accuracy")
def compute_metrics(eval_pred):
# eval_pred es una tupla: (logits, labels)
logits, labels = eval_pred
# argmax sobre los logits → el índice con mayor puntuación = predicción
predictions = np.argmax(logits, axis=-1)
return metric.compute(predictions=predictions, references=labels)
training_args = TrainingArguments(
output_dir="./resultados-imdb", # dónde guardar checkpoints y logs
num_train_epochs=3, # 3 pasadas por el dataset
per_device_train_batch_size=16, # 16 ejemplos por paso de entrenamiento
per_device_eval_batch_size=32, # 32 en evaluación (sin gradientes, cabe más)
learning_rate=2e-5, # tasa de aprendizaje (2 × 10⁻⁵)
weight_decay=0.01, # regularización L2 (previene sobreajuste)
evaluation_strategy="epoch", # evaluar al final de cada época
save_strategy="epoch", # guardar checkpoint al final de cada época
load_best_model_at_end=True, # al final, cargar el mejor checkpoint
logging_dir="./logs",
logging_steps=100, # mostrar log cada 100 pasos
report_to="none", # no enviar a wandb/tensorboard
)
num_train_epochs=3
→ El modelo verá cada ejemplo de entrenamiento 3 veces.
→ Con 25.000 ejemplos y batch_size=16: 25.000/16 ≈ 1.562 pasos por época
→ Total: 3 × 1.562 = 4.687 pasos de actualización de pesos
learning_rate=2e-5 (es decir, 0.00002)
→ El tamaño del "paso" con el que se ajustan los pesos en cada actualización.
→ Para fine-tuning de modelos preentrenados, valores pequeños (1e-5 a 5e-5)
funcionan mejor que los valores típicos de entrenamiento desde cero (1e-3).
→ Si es demasiado grande, el modelo "olvida" lo que aprendió (catastrophic forgetting).
→ Si es demasiado pequeño, el aprendizaje es lentísimo.
weight_decay=0.01
→ Una regularización que penaliza pesos muy grandes.
→ Equivalente a lo que en el curso de redes llamamos "L2 regularization".
→ Ayuda a evitar que el modelo memorice los ejemplos de entrenamiento.
per_device_train_batch_size=16
→ 16 ejemplos se procesan juntos antes de actualizar los pesos.
→ Más grande = más estable (pero más memoria).
→ En una GPU de 16GB con textos de 256 tokens, 16-32 es lo típico.
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized["train"],
eval_dataset=tokenized["test"],
tokenizer=tokenizer,
compute_metrics=compute_metrics,
)
# ¡A entrenar!
trainer.train()
El modelo partió con accuracy ~50% (aleatoria para 2 clases) y llegó al 93% en 3 épocas.
Vemos que el loss de entrenamiento baja consistentemente (de 0.34 a 0.11), pero el loss
de validación sube un poco en la época 3 — señal de que el modelo empieza a sobreajustar
ligeramente al dataset de entrenamiento. Por eso load_best_model_at_end=True
carga la época 3 (la de mayor accuracy).
# Guardar el modelo y tokenizer en disco
trainer.save_model("./modelo-imdb-finetuned")
tokenizer.save_pretrained("./modelo-imdb-finetuned")
print("Modelo guardado en ./modelo-imdb-finetuned")
# Para cargarlo más adelante:
modelo_cargado = AutoModelForSequenceClassification.from_pretrained(
"./modelo-imdb-finetuned"
)
# Usarlo con pipeline
from transformers import pipeline
clf = pipeline("text-classification", model="./modelo-imdb-finetuned")
print(clf("This movie was absolutely fantastic, loved every minute!"))
./modelo-imdb-finetuned
encontrarás varios archivos. El más importante es pytorch_model.bin (o archivos
.safetensors) — ese archivo contiene los 66 millones de pesos ajustados.
Pesará unos 260 MB. Junto a él, config.json dice la arquitectura y
vocab.txt es el vocabulario del tokenizer.
| Usa fine-tuning completo si… | Usa otra estrategia si… |
|---|---|
| Tienes +1.000 ejemplos etiquetados | Tienes menos de 100 ejemplos (usa prompting) |
| El modelo es BERT-size (hasta 340M params) | El modelo es LLaMA-7B o mayor (usa LoRA) |
| Tienes GPU con ≥8 GB VRAM | Solo tienes CPU o GPU pequeña (usa LoRA/QLoRA) |
| La tarea es estable y no cambia | La tarea cambia frecuentemente |
| Necesitas máxima precisión posible | La precisión "suficientemente buena" es aceptable |
Observa cómo evoluciona el loss y la accuracy durante las 3 épocas de fine-tuning:
En la próxima lección: Dataset y preparación — cómo preparar tus propios datos para fine-tuning, incluyendo formatos de instrucción, DataCollators y las trampas más comunes.