Antes de entrenar necesitas datos propios bien preparados. Esta lección enseña a crear un dataset desde cero, dividirlo correctamente, empaquetarlo para el modelo y evitar las trampas más comunes.
En el curso de PyTorch usaste Dataset y DataLoader de PyTorch.
HuggingFace tiene su propia clase Dataset (del paquete datasets)
que hace lo mismo pero con superpoderes: es mucho más eficiente con texto, sabe cargar
datos de muchos formatos (CSV, JSON, Parquet, Arrow) sin cargar todo en RAM a la vez,
y se integra perfectamente con el Trainer.
Hay dos maneras de obtener un Dataset:
load_dataset("imdb"), load_dataset("squad"), etc. (más de 50.000 datasets en HuggingFace Hub).La forma más rápida para prototipar: tienes listas Python con los textos y etiquetas, las metes en un diccionario, y listo.
from datasets import Dataset
import pandas as pd
# Ejemplo: reseñas de un restaurante con etiqueta de sentimiento
# 0 = negativo, 1 = positivo
datos = {
"texto": [
"La comida estuvo deliciosa, el servicio excelente.",
"Esperé 45 minutos y la comida llegó fría.",
"Precios razonables y buena atención.",
"El mesero fue grosero y el baño estaba sucio.",
"Me encantó el ambiente, regresaré pronto.",
"Pésima experiencia, no lo recomiendo a nadie.",
],
"etiqueta": [1, 0, 1, 0, 1, 0]
}
# Crear el Dataset directamente del diccionario
ds = Dataset.from_dict(datos)
print(ds)
print("\nPrimer ejemplo:")
print(ds[0])
Lo más común en proyectos reales: tienes un archivo mis_datos.csv con columnas
texto y etiqueta. Dos opciones:
# OPCIÓN A: desde la librería datasets directamente
ds_csv = Dataset.from_csv("mis_datos.csv")
# OPCIÓN B: cargar con pandas primero y luego convertir
# (útil si necesitas limpiar o filtrar antes)
df = pd.read_csv("mis_datos.csv")
df = df.dropna() # eliminar filas con valores nulos
df = df[df["texto"].str.len() > 10] # eliminar textos muy cortos
ds_desde_df = Dataset.from_pandas(df)
print(ds_desde_df)
# Para un archivo JSONL (un JSON por línea — formato muy común en NLP)
# Ejemplo de línea en el JSON: {"texto": "...", "etiqueta": 1}
ds_json = Dataset.from_json("mis_datos.jsonl")
# Para un JSON normal con una lista de objetos
import json
with open("datos.json") as f:
lista = json.load(f) # lista de dicts: [{"texto":..., "etiqueta":...}, ...]
ds_json2 = Dataset.from_list(lista)
print(ds_json2)
Cuando tienes un dataset, no puedes usar todos los datos para entrenar. Necesitas guardar una parte para saber si el modelo está aprendiendo de verdad o solo memorizando. Y otra parte para la evaluación final honesta. Eso es lo que hace dividir (en inglés, split) el dataset en tres partes.
La proporción más común: 80% entrenar, 10% validar, 10% probar. En datasets pequeños puede ser 70/15/15.
Expliquemos para qué sirve cada parte con una analogía de examen:
from datasets import Dataset
# Suponemos que tenemos un dataset grande ya cargado
# Primer split: separar test del resto (10% para test)
ds_train_val, ds_test = ds.train_test_split(
test_size=0.1, # 10% para test
seed=42 # semilla para reproducibilidad (siempre el mismo split)
).values()
# Segundo split: separar validation del conjunto train_val
# Ahora el "test_size" es 10% de lo que queda (≈11.1% del total, ajustado a 10/90)
splits = ds_train_val.train_test_split(test_size=0.111, seed=42)
ds_train = splits["train"]
ds_val = splits["test"] # la función lo llama "test" pero es nuestro validation
print(f"Train: {len(ds_train):>6} ejemplos ({len(ds_train)/len(ds)*100:.1f}%)")
print(f"Validation: {len(ds_val):>6} ejemplos ({len(ds_val)/len(ds)*100:.1f}%)")
print(f"Test: {len(ds_test):>6} ejemplos ({len(ds_test)/len(ds)*100:.1f}%)")
También puedes empaquetar los tres en un DatasetDict — un diccionario especial
que guarda los tres splits juntos, igual que el IMDB que descargaste en la Lección 5:
from datasets import DatasetDict
dataset_completo = DatasetDict({
"train": ds_train,
"validation": ds_val,
"test": ds_test,
})
print(dataset_completo)
Aquí viene un concepto que muchos tutoriales saltan sin explicar. Cuando el Trainer entrena el modelo, no le da ejemplos de uno en uno — los agrupa en lotes (batches) de, digamos, 16 ejemplos a la vez. Recuerda del curso de PyTorch que esto es más eficiente porque la GPU puede procesar muchos en paralelo.
El problema es que los textos tienen longitudes distintas. "La comida estuvo rica"
tiene 4 tokens. "Me sentí muy decepcionado con la calidad del servicio, especialmente
considerando el precio" tiene 18 tokens. Si quieres meterlos en el mismo lote
(que debe ser un tensor rectangular — todos del mismo tamaño), necesitas
rellenar los cortos con un token especial de relleno llamado
[PAD] (o su ID numérico, típicamente 0).
El DataCollator es exactamente eso: una función que toma una lista de ejemplos individuales y los convierte en un lote rectangular rellenado, listo para entrar al modelo.
El DataCollator añade [PAD] hasta que todos los ejemplos del lote tengan la misma longitud — la del más largo del lote.
Para tareas de clasificación con modelos tipo BERT, el DataCollator correcto es
DataCollatorWithPadding. Solo necesita el tokenizer para saber qué ID
usar como [PAD] y cuál es la longitud máxima permitida.
from transformers import AutoTokenizer, DataCollatorWithPadding
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
# Tokenizar el dataset (sin padding fijo — lo hará el collator)
def tokenize(batch):
return tokenizer(
batch["texto"],
truncation=True,
max_length=128,
# NO ponemos padding aquí — lo dejamos al collator
)
ds_tokenizado = dataset_completo.map(tokenize, batched=True)
# Crear el DataCollator — solo necesita el tokenizer
collator = DataCollatorWithPadding(tokenizer=tokenizer)
print("DataCollator listo")
# Demostración: simular lo que hace con 3 ejemplos distintos
ejemplos = [ds_tokenizado["train"][i] for i in [0, 1, 2]]
longitudes_antes = [len(e["input_ids"]) for e in ejemplos]
print(f"Longitudes antes del collator: {longitudes_antes}")
lote = collator(ejemplos)
print(f"Forma del lote (input_ids): {lote['input_ids'].shape}")
print(f"Forma del lote (attention_mask): {lote['attention_mask'].shape}")
Los tres ejemplos tenían 12, 7 y 18 tokens. El collator rellenó los dos primeros con
tokens [PAD] hasta 18 (la longitud del más largo del lote). El tensor resultante es
3×18 — rectangular, listo para la GPU. El attention_mask es 1 donde hay
texto real y 0 donde hay [PAD], para que el modelo ignore el relleno.
Lo que viste hasta ahora sirve para fine-tuning de clasificación (BERT-style). Pero si estás haciendo fine-tuning de un modelo generativo (como Llama, Mistral, GPT), el formato del dataset es diferente. En vez de pares (texto, etiqueta), necesitas triples de (instrucción, entrada, respuesta).
Stanford Alpaca popularizó este formato en 2023. Cada ejemplo se convierte en un texto único con marcadores especiales:
# Ejemplo de un dato de entrenamiento en formato Alpaca
ejemplo_alpaca = {
"instruction": "Clasifica el sentimiento del siguiente texto en Positivo, Negativo o Neutro.",
"input": "Me encantó la película, aunque el final fue un poco predecible.",
"output": "Positivo"
}
# Función para convertir al formato de texto que ve el modelo
def formato_alpaca(ejemplo):
if ejemplo["input"]: # si hay entrada
texto = (
f"### Instrucción:\n{ejemplo['instruction']}\n\n"
f"### Entrada:\n{ejemplo['input']}\n\n"
f"### Respuesta:\n{ejemplo['output']}"
)
else: # si no hay entrada (la instrucción es suficiente)
texto = (
f"### Instrucción:\n{ejemplo['instruction']}\n\n"
f"### Respuesta:\n{ejemplo['output']}"
)
return {"text": texto}
print(formato_alpaca(ejemplo_alpaca)["text"])
# Dataset con ejemplos en formato instrucción
datos_instruccion = [
{"instruction": "Resume en una oración.",
"input": "El cambio climático es causado principalmente por la quema de combustibles fósiles...",
"output": "El cambio climático se debe al uso de combustibles fósiles."},
{"instruction": "Traduce al inglés.",
"input": "La inteligencia artificial está cambiando el mundo.",
"output": "Artificial intelligence is changing the world."},
# ... más ejemplos
]
ds_instruccion = Dataset.from_list(datos_instruccion)
# Aplicar el formato a todos los ejemplos
ds_formateado = ds_instruccion.map(formato_alpaca)
print(ds_formateado[0]["text"])
DataCollatorForLanguageModeling.
Imagina que tienes un dataset para detectar spam: 900 correos normales y 100 correos spam. Si entrenas el modelo así, aprende rápidamente a decir siempre "no es spam" — obtendrá 90% de accuracy sin aprender nada útil. Esto es el desbalance de clases (en inglés, class imbalance).
Dataset: 900 correos normales + 100 correos spam = 1000 total Un modelo que SIEMPRE dice "no es spam": · Acierta en los 900 correos normales → 900 correctos · Falla en los 100 spam → 100 incorrectos · Accuracy = 900 / 1000 = 90% ← parece bueno, pero... · Recall de spam = 0% ← no detecta NINGÚN spam Un modelo que SÍ aprende: · Accuracy = 85% (parece peor) · Pero detecta 70 de los 100 spam → Recall de spam = 70% · El segundo modelo es MUY superior para la tarea real
Hay cuatro estrategias principales para manejar el desbalance:
| Estrategia | Qué hace | Cuándo usarla |
|---|---|---|
| Oversampling | Duplicar ejemplos de la clase minoritaria | Cuando tienes pocos datos en la clase rara |
| Undersampling | Reducir ejemplos de la clase mayoritaria | Cuando tienes muchos datos en total |
| Class weights | Penalizar más los errores en la clase minoritaria | La más práctica — no cambia el dataset |
| Métricas correctas | Usar F1, Precision, Recall en vez de Accuracy | Siempre, junto a cualquier estrategia |
from datasets import concatenate_datasets
# Separar por clase
ds_normal = ds_train.filter(lambda x: x["etiqueta"] == 0) # 900 ejemplos
ds_spam = ds_train.filter(lambda x: x["etiqueta"] == 1) # 100 ejemplos
print(f"Normal: {len(ds_normal)}, Spam: {len(ds_spam)}")
# Duplicar el spam 9 veces para igualar las clases
ds_spam_x9 = concatenate_datasets([ds_spam] * 9) # 9 × 100 = 900 ejemplos
# Combinar y mezclar
ds_balanceado = concatenate_datasets([ds_normal, ds_spam_x9])
ds_balanceado = ds_balanceado.shuffle(seed=42)
print(f"Dataset balanceado: {len(ds_balanceado)} ejemplos")
# Verificar distribución
from collections import Counter
etiquetas = ds_balanceado["etiqueta"]
print("Distribución:", Counter(etiquetas))
import torch
from sklearn.utils.class_weight import compute_class_weight
import numpy as np
etiquetas_numpy = np.array(ds_train["etiqueta"])
# Calcular pesos: la clase rara recibe un peso mayor
pesos = compute_class_weight(
class_weight="balanced",
classes=np.unique(etiquetas_numpy),
y=etiquetas_numpy
)
pesos_tensor = torch.tensor(pesos, dtype=torch.float)
print(f"Peso clase 0 (normal): {pesos_tensor[0]:.4f}")
print(f"Peso clase 1 (spam): {pesos_tensor[1]:.4f}")
print("→ Un error en spam penaliza 9 veces más que un error en normal")
Juntamos todo lo que aprendiste en este flujo completo. Supón que tienes un archivo
resenas.csv con columnas texto y etiqueta
(0 = negativo, 1 = positivo):
import pandas as pd
from datasets import Dataset, DatasetDict
from transformers import AutoTokenizer, DataCollatorWithPadding
## ── PASO 1: Cargar y limpiar ──────────────────────────
df = pd.read_csv("resenas.csv")
df = df.dropna() # eliminar filas vacías
df = df[df["texto"].str.len() >= 10] # descartar textos muy cortos
df = df.reset_index(drop=True)
print(f"Total después de limpiar: {len(df)} filas")
print(df["etiqueta"].value_counts())
## ── PASO 2: Crear Dataset y hacer splits ──────────────
ds_completo = Dataset.from_pandas(df)
split1 = ds_completo.train_test_split(test_size=0.1, seed=42)
split2 = split1["train"].train_test_split(test_size=0.111, seed=42)
dataset = DatasetDict({
"train": split2["train"],
"validation": split2["test"],
"test": split1["test"],
})
print(dataset)
## ── PASO 3: Tokenizar ─────────────────────────────────
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
def tokenize_fn(batch):
return tokenizer(
batch["texto"],
truncation=True,
max_length=128,
)
ds_tok = dataset.map(tokenize_fn, batched=True, remove_columns=["texto"])
print("Columnas después de tokenizar:", ds_tok["train"].column_names)
## ── PASO 4: Renombrar etiqueta a 'labels' (lo espera el Trainer) ─
ds_tok = ds_tok.rename_column("etiqueta", "labels")
ds_tok = ds_tok.with_format("torch") # convertir a tensores PyTorch
## ── PASO 5: Crear el DataCollator ─────────────────────
collator = DataCollatorWithPadding(tokenizer=tokenizer)
print("\n¡Dataset listo para el Trainer!")
print(f"Train: {len(ds_tok['train'])} | Val: {len(ds_tok['validation'])} | Test: {len(ds_tok['test'])}")
Escribe hasta 4 textos cortos y observa cómo el DataCollator los convierte en un tensor rectangular. Verás los IDs de tokens (simulados), el padding añadido y el attention mask.
En la próxima lección: Trainer API — el objeto que orquesta el entrenamiento completo: cómo configurar los hiperparámetros, qué hace en cada paso y cómo detenerlo automáticamente cuando deja de mejorar.