Buckets:

rtrm's picture
|
download
raw
9.08 kB

Depurar el pipeline de entrenamiento[[debugging-the-training-pipeline]]

Has escrito un script para entrenar o hacer ajuste fino (fine-tuning) de un modelo para una tarea concreta, siguiendo lo aprendido en el Capítulo 7. Pero al lanzar trainer.train(), algo sale mal: aparece un error, o peor, el entrenamiento termina pero el modelo final da resultados pobres. En esta sección vamos a recorrer un método práctico para depurar ese tipo de problemas.

Depurar el pipeline de entrenamiento[[debugging-the-training-pipeline]]

Cuando aparece un error dentro de trainer.train(), puede venir de varias partes distintas del pipeline: los datos, el data collator, el modelo, el optimizador o incluso la métrica de evaluación. La forma más fiable de depurarlo es separar el pipeline en pasos y comprobarlos uno por uno.

Usaremos como ejemplo un script que intenta ajustar DistilBERT sobre el dataset MNLI:

from datasets import load_dataset
import evaluate
from transformers import (
    AutoTokenizer,
    AutoModelForSequenceClassification,
    TrainingArguments,
    Trainer,
)

raw_datasets = load_dataset("glue", "mnli")

model_checkpoint = "distilbert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)

def preprocess_function(examples):
    return tokenizer(examples["premise"], examples["hypothesis"], truncation=True)

tokenized_datasets = raw_datasets.map(preprocess_function, batched=True)
model = AutoModelForSequenceClassification.from_pretrained(model_checkpoint)

args = TrainingArguments(
    f"distilbert-finetuned-mnli",
    evaluation_strategy="epoch",
    save_strategy="epoch",
    learning_rate=2e-5,
    num_train_epochs=3,
    weight_decay=0.01,
)

metric = evaluate.load("glue", "mnli")

def compute_metrics(eval_pred):
    predictions, labels = eval_pred
    return metric.compute(predictions=predictions, references=labels)

trainer = Trainer(
    model,
    args,
    train_dataset=raw_datasets["train"],
    eval_dataset=raw_datasets["validation_matched"],
    compute_metrics=compute_metrics,
)
trainer.train()

Este código falla con un error críptico:

'ValueError: You have to specify either input_ids or inputs_embeds'

Revisa tus datos[[check-your-data]]

El primer paso siempre es revisar el dataset que realmente recibe el Trainer:

trainer.train_dataset[0]
{'hypothesis': 'Product and geography are what make cream skimming work. ',
 'idx': 0,
 'label': 1,
 'premise': 'Conceptually cream skimming has two basic dimensions - product and geography.'}

Aquí ya se ve el problema: estamos pasando texto sin tokenizar al Trainer. Aunque antes habíamos aplicado map() sobre el dataset, luego le pasamos raw_datasets en vez de tokenized_datasets.

La primera corrección es esta:

trainer = Trainer(
    model,
    args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation_matched"],
    compute_metrics=compute_metrics,
)

Con eso aparece un error nuevo. Eso es una buena señal: hemos avanzado.

Además, cuando depuras entrenamiento, conviene inspeccionar los input_ids ya decodificados para comprobar que representen exactamente el texto esperado:

tokenizer.decode(trainer.train_dataset[0]["input_ids"])

También conviene revisar la máscara de atención y las etiquetas:

trainer.train_dataset[0]["attention_mask"]
trainer.train_dataset[0]["label"]
trainer.train_dataset.features["label"].names

De datasets a dataloaders[[from-datasets-to-dataloaders]]

El siguiente punto donde suelen romperse las cosas es la creación de lotes. Puedes comprobarlo generando manualmente un batch:

for batch in trainer.get_train_dataloader():
    break

Si aquí aparece un error, casi siempre vale la pena inspeccionar el collate_fn:

data_collator = trainer.get_train_dataloader().collate_fn
data_collator

En este ejemplo, el problema es que el Trainer termina usando default_data_collator en lugar de DataCollatorWithPadding, porque no le pasamos el tokenizador. La corrección adecuada es crear el data collator explícitamente:

from transformers import DataCollatorWithPadding

data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

trainer = Trainer(
    model,
    args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation_matched"],
    compute_metrics=compute_metrics,
    data_collator=data_collator,
    tokenizer=tokenizer,
)

Si quieres replicar con precisión lo que hace Trainer tras bambalinas, también puedes eliminar columnas no usadas con:

actual_train_set = trainer._remove_unused_columns(trainer.train_dataset)
batch = data_collator([actual_train_set[i] for i in range(4)])

Pasar un batch por el modelo[[going-through-the-model]]

Una vez que puedes crear un batch, el siguiente paso es comprobar que el modelo lo procese bien:

for batch in trainer.get_train_dataloader():
    break

outputs = trainer.model.cpu()(**batch)

Depurar en CPU suele ser mucho más informativo que depurar errores de CUDA, porque muchos errores en GPU se reportan lejos del punto real donde se originaron.

En este ejemplo, al ejecutar en CPU aparece:

IndexError: Target 2 is out of bounds.

Eso indica que el número de etiquetas del modelo no coincide con el dataset. Si lo comprobamos:

trainer.model.config.num_labels

vemos que el modelo tiene 2 etiquetas, pero MNLI tiene 3. Hay que reinstanciarlo así:

model = AutoModelForSequenceClassification.from_pretrained(model_checkpoint, num_labels=3)

Después de eso, conviene volver a probar el paso por CPU y luego mover el batch a la GPU:

import torch

device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
batch = {k: v.to(device) for k, v in batch.items()}
outputs = trainer.model.to(device)(**batch)

Ejecutar un paso de optimización[[performing-one-optimization-step]]

Si el modelo ya procesa el batch correctamente, el siguiente paso es comprobar la retropropagación y el optimizador:

loss = outputs.loss
loss.backward()

trainer.create_optimizer()
trainer.optimizer.step()

Si algo falla aquí, vuelve a la CPU para obtener un mensaje de error útil.

Errores de memoria CUDA[[dealing-with-cuda-out-of-memory-errors]]

Cuando ves un error que empieza por RuntimeError: CUDA out of memory, no suele significar que tu código esté lógicamente mal, sino que estás intentando usar más memoria de GPU de la disponible.

Las medidas más habituales son:

  • Reducir el tamaño del batch
  • Usar un modelo más pequeño
  • Asegurarte de no tener dos modelos cargados a la vez en GPU
  • Reiniciar el kernel si el entorno quedó en mal estado tras el error

Evaluar el modelo[[evaluating-the-model]]

Aunque el entrenamiento parezca arrancar bien, aún puede romperse al evaluar. Por eso conviene probar trainer.evaluate() antes de lanzar un entrenamiento largo.

En este ejemplo, el problema está en compute_metrics(): estamos pasando logits en bruto a la métrica, en vez de las predicciones finales. La corrección es aplicar argmax:

import numpy as np

def compute_metrics(eval_pred):
    predictions, labels = eval_pred
    predictions = np.argmax(predictions, axis=1)
    return metric.compute(predictions=predictions, references=labels)

Errores silenciosos durante el entrenamiento[[debugging-silent-errors-during-training]]

La situación más difícil es cuando el entrenamiento termina sin error, pero los resultados son malos. En ese caso, conviene revisar varias cosas.

Revisa los datos otra vez[[check-your-data-again]]

Pregúntate siempre:

  • ¿Las entradas decodificadas tienen sentido?
  • ¿Las etiquetas parecen correctas?
  • ¿Hay una clase mucho más frecuente que las otras?
  • ¿Qué valor de pérdida o métrica esperarías de un modelo aleatorio?

Sobreajustar un solo batch[[overfit-your-model-on-one-batch]]

Una prueba muy útil consiste en entrenar repetidamente sobre un solo batch pequeño. Si el modelo ni siquiera puede llevar la pérdida casi a cero en ese escenario, es muy probable que haya un fallo en el pipeline, en los labels o en la configuración del entrenamiento.

for batch in trainer.get_train_dataloader():
    break

batch = {k: v.to(device) for k, v in batch.items()}
trainer.create_optimizer()

for _ in range(20):
    outputs = trainer.model(**batch)
    loss = outputs.loss
    loss.backward()
    trainer.optimizer.step()
    trainer.optimizer.zero_grad()

Si esta prueba funciona, ya sabes que el modelo es capaz de aprender al menos un caso trivial y puedes seguir depurando el resto del pipeline con más confianza.

Si usas un bucle manual en lugar de Trainer, las mismas comprobaciones siguen siendo válidas. Solo asegúrate también de no olvidarte de model.train(), model.eval() y zero_grad() donde corresponda.

Xet Storage Details

Size:
9.08 kB
·
Xet hash:
0a267e0772389e860daaf4c9e443ab4832a13bdb0676dae18819aa3f74c8a302

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.