Fine-tune de modelos pequeños con Hugging Face Trainer
1. Resumen general
Esta guía te muestra cómo hacer fine-tune a un modelo pequeño (DistilBERT) para clasificar la polaridad de reseñas de productos, usando la API Trainer de la librería transformers. Al terminar, sabrás cómo preparar un dataset, tokenizar, configurar TrainingArguments, entrenar, evaluar y guardar el modelo para usarlo en inference.
El ejemplo que recorre toda la guía: OrderReview – la reseña de un cliente (Customer) sobre un pedido (Order), etiquetada como positive/negative.
2. Preparar el entorno
Instala las librerías necesarias. No necesitas una GPU potente; modelos pequeños como DistilBERT corren bien en CPU con datasets pequeños, aunque más lentamente.
pip install transformers datasets evaluate accelerate torch scikit-learn
Nota: Si tu máquina tiene GPU, instala la versión correspondiente de torch con CUDA para entrenar mucho más rápido. Con CPU, es recomendable limitar el número de filas de datos y epochs para que la demostración no tarde demasiado.
3. Preparar el dataset
En lugar de descargar un dataset enorme, crearemos un conjunto pequeño que simule reseñas de pedidos para que sea fácil de entender. En la práctica, puedes reemplazarlo con load_dataset("imdb") o tus propios datos CSV.
from datasets import Dataset
reviews = [
{"text": "Pedido entregado rápido, empaque cuidadoso, muy satisfecho", "label": 1},
{"text": "Producto defectuoso, entrega lenta, no coincide con la descripción", "label": 0},
{"text": "Buena calidad, precio justo, volvería a comprar", "label": 1},
{"text": "Servicio terrible, atención al cliente muy lenta", "label": 0},
{"text": "Muy contento con este pedido, 5 estrellas", "label": 1},
{"text": "El producto llegó dañado, muy decepcionado", "label": 0},
]
dataset = Dataset.from_list(reviews)
dataset = dataset.train_test_split(test_size=0.33, seed=42)
print(dataset)
DatasetDict({
train: Dataset({features: ['text', 'label'], num_rows: 4})
test: Dataset({features: ['text', 'label'], num_rows: 2})
})
Dataset.from_list convierte una lista de diccionarios en un Dataset de Hugging Face. train_test_split divide en conjuntos train/test — con datos reales deberías tener varios miles de filas o más, esto es solo un ejemplo que ilustra el flujo de procesamiento.
4. Tokenizar los datos
El modelo no entiende texto directamente; necesitas un tokenizer para convertirlo a input_ids y attention_mask.
from transformers import AutoTokenizer
checkpoint = "distilbert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
def tokenize_fn(batch):
return tokenizer(batch["text"], truncation=True, padding="max_length", max_length=32)
tokenized_dataset = dataset.map(tokenize_fn, batched=True)
print(tokenized_dataset["train"][0].keys())
dict_keys(['text', 'label', 'input_ids', 'attention_mask'])
truncation=True: recorta frases más largas quemax_length.padding="max_length": rellena con tokens adicionales para que todas las frases tengan la misma longitud, lo que facilita el procesamiento paralelo en lotes.map(..., batched=True): aplica la función de tokenización por lotes, mucho más rápido que procesar fila por fila.
Nota: DistilBERT en inglés no es ideal para fine-tunear reseñas en vietnamita en producción. Lo usamos aquí para demostrar la técnica; con datos en vietnamita, elige checkpoints como
vinai/phobert-base.
5. Cargar el modelo y definir las etiquetas
from transformers import AutoModelForSequenceClassification
label2id = {"negative": 0, "positive": 1}
id2label = {v: k for k, v in label2id.items()}
model = AutoModelForSequenceClassification.from_pretrained(
checkpoint,
num_labels=2,
id2label=id2label,
label2id=label2id,
)
num_labels=2 crea una cabeza de clasificación nueva con 2 salidas (positive/negative) que se añade al encoder preentrenado. id2label/label2id hacen que la salida del modelo devuelva etiquetas legibles en lugar de solo números 0/1.
6. Configurar TrainingArguments
TrainingArguments agrupa todos los hiperparámetros y la configuración del bucle de entrenamiento.
from transformers import TrainingArguments
training_args = TrainingArguments(
output_dir="./order-review-model",
eval_strategy="epoch",
save_strategy="epoch",
learning_rate=2e-5,
per_device_train_batch_size=4,
per_device_eval_batch_size=4,
num_train_epochs=3,
weight_decay=0.01,
load_best_model_at_end=True,
metric_for_best_model="f1",
logging_steps=1,
)
eval_strategy="epoch": evalúa después de cada epoch en lugar de cada paso.load_best_model_at_end=True: después de entrenar, Trainer recarga automáticamente el checkpoint mejor segúnmetric_for_best_model.per_device_train_batch_size=4: tamaño de lote pequeño porque el dataset demo también es pequeño; con datos reales, aumenta a 16/32 según tu VRAM.
7. Definir la función de cálculo de métricas
Trainer necesita una función compute_metrics para saber cómo evaluar el modelo.
import numpy as np
import evaluate
accuracy = evaluate.load("accuracy")
f1 = evaluate.load("f1")
def compute_metrics(eval_pred):
logits, labels = eval_pred
predictions = np.argmax(logits, axis=-1)
acc = accuracy.compute(predictions=predictions, references=labels)
f1_score = f1.compute(predictions=predictions, references=labels)
return {"accuracy": acc["accuracy"], "f1": f1_score["f1"]}
eval_pred es una tupla (logits, labels) que Trainer pasa automáticamente después de cada evaluación. np.argmax selecciona la etiqueta con la probabilidad (logit) más alta como predicción.
8. Inicializar Trainer y entrenar
from transformers import Trainer, DataCollatorWithPadding
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset["train"],
eval_dataset=tokenized_dataset["test"],
data_collator=data_collator,
compute_metrics=compute_metrics,
)
trainer.train()
Epoch Training Loss Validation Loss Accuracy F1
1 0.693100 0.671204 0.500000 0.500000
2 0.654200 0.598311 1.000000 1.000000
3 0.581900 0.520447 1.000000 1.000000
DataCollatorWithPadding gestiona el relleno dinámico por lote (más eficiente que rellenar fijo con max_length en el paso de tokenización, aunque aquí ya hemos rellenado así que el collator principalmente prepara los tensores del lote). trainer.train() ejecuta el bucle forward → backward → optimizer step → log loss, y evalúa automáticamente según eval_strategy.
8.1. Monitorear el progreso del entrenamiento
Si quieres registrar más detalles en TensorBoard, añade report_to="tensorboard" en TrainingArguments y luego ejecuta tensorboard --logdir ./order-review-model/runs. Con datasets pequeños como la demostración, el registro en consola es suficiente para verificar que la pérdida disminuya gradualmente.
9. Evaluar y guardar el modelo
metrics = trainer.evaluate()
print(metrics)
trainer.save_model("./order-review-model/final")
tokenizer.save_pretrained("./order-review-model/final")
{'eval_loss': 0.5204, 'eval_accuracy': 1.0, 'eval_f1': 1.0, 'eval_runtime': 0.12, ...}
trainer.evaluate() ejecuta de nuevo en eval_dataset y devuelve un diccionario de métricas. save_model guarda la configuración y pesos; recuerda guardar también el tokenizer con save_pretrained para que cuando lo recargues después no haya inconsistencias de vocabulario.
10. Inference con el modelo fine-tuneado
Usa pipeline para probar rápidamente el modelo que acabas de entrenar.
from transformers import pipeline
classifier = pipeline(
"text-classification",
model="./order-review-model/final",
tokenizer="./order-review-model/final",
)
result = classifier("Entrega a tiempo, producto mejor de lo esperado")
print(result)
[{'label': 'positive', 'score': 0.9123}]
pipeline se encarga de tokenizar, forward, y softmax para dar label con score (probabilidad de confianza). Esta es la forma más rápida de integrar el modelo en un servicio pequeño, por ejemplo, un PaymentService que necesita clasificar automáticamente comentarios de clientes.
11. Escribir tests para verificar el comportamiento del modelo
Con el dataset demo, puedes escribir tests simples con pytest para asegurar que el pipeline funciona correctamente y no hay regresiones al cambiar checkpoints.
import pytest
from transformers import pipeline
@pytest.fixture(scope="module")
def classifier():
return pipeline(
"text-classification",
model="./order-review-model/final",
tokenizer="./order-review-model/final",
)
def test_positive_review_is_classified_correctly(classifier):
result = classifier("Producto excelente, envío rápido")[0]
assert result["label"] == "positive"
assert result["score"] > 0.5
def test_negative_review_is_classified_correctly(classifier):
result = classifier("Producto defectuoso, envío lento, muy decepcionado")[0]
assert result["label"] == "negative"
assert result["score"] > 0.5
scope="module" hace que el pipeline solo cargue el modelo una vez para todos los tests del archivo, evitando gastar tiempo recargándolo en cada test. Este test solo verifica el comportamiento de alto nivel (etiqueta correcta), no debe usarse para evaluar la calidad del modelo — eso es responsabilidad de la etapa de evaluación anterior.
12. Errores comunes
- Olvidar
padding/truncation: frases más largas quemax_lengthcausarán error en runtime porque los tensores no tendrán el mismo tamaño en el lote. - Confundir
num_labels: si tu dataset tiene 3 clases pero declarasnum_labels=2, Trainer seguirá ejecutándose pero los resultados serán completamente incorrectos porque la cabeza de clasificación no tiene suficientes salidas. - Dataset muy pequeño y esperar F1 alto en datos reales: el ejemplo en esta guía solo sirve para ilustrar el código, 4 filas de entrenamiento no son suficientes para que el modelo generalice. No tomes accuracy=1.0 aquí como estándar.
- No establecer
seed: los resultados variarán entre ejecuciones, dificultando la comparación al ajustar hiperparámetros. Usaset_seed(42)detransformersantes de entrenar. - Usar modelos grandes (base/large) en máquinas débiles: comienza con versiones
distilbert/tinypara tener ciclos de desarrollo rápidos, solo cambia a modelos más grandes cuando hayas consolidado el pipeline.
Cuándo NO usar Trainer: si necesitas personalizar completamente el bucle de entrenamiento (ej. pérdida multi-tarea compleja, scheduler personalizado difícil de expresar a través de callbacks), escribir un loop PyTorch puro o usar
acceleratedirectamente será más flexible que intentar forzarlo enTrainer.
13. Conclusión
La API Trainer te permite hacer fine-tune a modelos pequeños sin escribir tu propio bucle train/eval; solo necesitas preparar el dataset, tokenizer, modelo y TrainingArguments. Para tareas simples de clasificación como reseñas de pedidos, el flujo estándar es: tokenizar → configurar TrainingArguments → definir compute_metrics → instanciar Trainer → train() → evaluate() → save_model(). Cuando trabajes con datos más complejos, solo necesitas cambiar el dataset y checkpoint; el resto del pipeline permanece prácticamente igual.