Depuración de modelos
Has escrito un hermoso script para entrenar o ajustar un modelo en una tarea determinada, siguiendo diligentemente los consejos del Capítulo 7. Pero cuando lanzas el comando trainer.train(), sucede algo horrible: ¡obtienes un error 😱! O peor aún, todo parece estar bien y el entrenamiento se ejecuta sin errores, pero el modelo resultante es de mala calidad. En esta sección, te mostraremos qué puedes hacer para depurar este tipo de problemas.
Depuración del pipeline de entrenamiento[[debugging-the-training-pipeline]]
El problema cuando encuentras un error en trainer.train() es que podría provenir de múltiples fuentes, ya que el Trainer generalmente junta muchas cosas. Convierte conjuntos de datos en dataloaders, por lo que el problema podría ser algo incorrecto en tu conjunto de datos, o algún problema al intentar agrupar elementos de los conjuntos de datos. Luego toma un lote de datos y lo alimenta al modelo, por lo que el problema podría estar en el código del modelo. Después de eso, calcula los gradientes y realiza el paso de optimización, por lo que el problema también podría estar en tu optimizador. E incluso si todo va bien para el entrenamiento, algo podría salir mal durante la evaluación si hay un problema con tu métrica.
La mejor manera de depurar un error que surge en trainer.train() es recorrer manualmente todo este pipeline para ver dónde salieron mal las cosas. El error suele ser muy fácil de resolver.
Para demostrar esto, usaremos el siguiente script que (intenta) ajustar un modelo DistilBERT en el conjunto de datos MNLI:
from datasets import load_dataset
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()
Si intentas ejecutarlo, te encontrarás con un error bastante críptico:
'ValueError: You have to specify either input_ids or inputs_embeds'
Verifica tus datos[[check-your-data]]
Esto se da por sentado, pero si tus datos están corruptos, el Trainer no podrá formar lotes, y mucho menos entrenar tu modelo. Así que, lo primero es lo primero, debes echar un vistazo a lo que hay dentro de tu conjunto de entrenamiento.
Para evitar innumerables horas dedicadas a intentar arreglar algo que no es la fuente del error, te recomendamos que uses trainer.train_dataset para tus comprobaciones y nada más. Así que hagámoslo aquí:
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.'}
¿Notas algo mal? Esto, junto con el mensaje de error sobre la falta de input_ids, debería hacerte darte cuenta de que son textos, no números que el modelo pueda entender. Aquí, el error original es muy engañoso porque el Trainer elimina automáticamente las columnas que no coinciden con la firma del modelo (es decir, los argumentos esperados por el modelo). Eso significa que aquí, todo excepto las etiquetas fue descartado. Por lo tanto, no hubo ningún problema al crear lotes y luego enviarlos al modelo, que a su vez se quejó de que no recibió la entrada adecuada.
¿Por qué no se procesaron los datos? Usamos el método Dataset.map() en los conjuntos de datos para aplicar el tokenizer en cada muestra. Pero si miras de cerca el código, verás que cometimos un error al pasar los conjuntos de entrenamiento y evaluación al Trainer. En lugar de usar tokenized_datasets aquí, usamos raw_datasets 🤦. ¡Así que arreglemos esto!
from datasets import load_dataset
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=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation_matched"],
compute_metrics=compute_metrics,
)
trainer.train()
Este nuevo código ahora dará un error diferente (¡progreso!):
'ValueError: expected sequence of length 43 at dim 1 (got 37)'
Mirando el traceback, podemos ver que el error ocurre en el paso de colación de datos:
~/git/transformers/src/transformers/data/data_collator.py in torch_default_data_collator(features)
105 batch[k] = torch.stack([f[k] for f in features])
106 else:
--> 107 batch[k] = torch.tensor([f[k] for f in features])
108
109 return batch
Así que, deberíamos pasar a eso. Antes de hacerlo, sin embargo, terminemos de inspeccionar nuestros datos, solo para estar 100% seguros de que son correctos.
Una cosa que siempre debes hacer al depurar una sesión de entrenamiento es echar un vistazo a las entradas decodificadas de tu modelo. No podemos entender los números que le alimentamos directamente, así que deberíamos ver qué representan esos números. En visión por computadora, por ejemplo, eso significa mirar las imágenes decodificadas de los píxeles que pasas, en voz significa escuchar las muestras de audio decodificadas, y para nuestro ejemplo de PNL aquí significa usar nuestro tokenizer para decodificar las entradas:
tokenizer.decode(trainer.train_dataset[0]["input_ids"])
'[CLS] conceptually cream skimming has two basic dimensions - product and geography. [SEP] product and geography are what make cream skimming work. [SEP]'
Así que eso parece correcto. Deberías hacer esto para todas las claves en las entradas:
trainer.train_dataset[0].keys()
dict_keys(['attention_mask', 'hypothesis', 'idx', 'input_ids', 'label', 'premise'])
Ten en cuenta que las claves que no corresponden a entradas aceptadas por el modelo se descartarán automáticamente, así que aquí solo conservaremos input_ids, attention_mask y label (que se renombrará labels). Para verificar la firma del modelo, puedes imprimir la clase de tu modelo y luego ir a consultar su documentación:
type(trainer.model)
transformers.models.distilbert.modeling_distilbert.DistilBertForSequenceClassification
Así que en nuestro caso, podemos verificar los parámetros aceptados en esta página. El Trainer también registrará las columnas que está descartando.
Hemos comprobado que los IDs de entrada son correctos decodificándolos. Lo siguiente es el attention_mask:
trainer.train_dataset[0]["attention_mask"]
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
Dado que no aplicamos padding en nuestro preprocesamiento, esto parece perfectamente natural. Para asegurarnos de que no haya ningún problema con esa máscara de atención, comprobemos que tiene la misma longitud que nuestros IDs de entrada:
len(trainer.train_dataset[0]["attention_mask"]) == len(
trainer.train_dataset[0]["input_ids"]
)
True
¡Eso es bueno! Por último, comprobemos nuestra etiqueta:
trainer.train_dataset[0]["label"]
1
Al igual que los IDs de entrada, este es un número que no tiene mucho sentido por sí solo. Como vimos antes, el mapa entre enteros y nombres de etiquetas se almacena dentro del atributo names de la feature correspondiente del conjunto de datos:
trainer.train_dataset.features["label"].names
['entailment', 'neutral', 'contradiction']
Así que 1 significa neutral, lo que significa que las dos oraciones que vimos arriba no están en contradicción, y la primera no implica la segunda. ¡Eso parece correcto!
No tenemos IDs de tipo de token aquí, ya que DistilBERT no los espera; si tienes algunos en tu modelo, también debes asegurarte de que coincidan correctamente con la ubicación de la primera y segunda oración en la entrada.
[!TIP] ✏️ ¡Tu turno! Comprueba que todo parece correcto con el segundo elemento del conjunto de datos de entrenamiento.
Solo estamos haciendo la verificación en el conjunto de entrenamiento aquí, pero por supuesto, deberías verificar los conjuntos de validación y prueba de la misma manera.
Ahora que sabemos que nuestros conjuntos de datos se ven bien, es hora de verificar el siguiente paso del pipeline de entrenamiento.
De conjuntos de datos a dataloaders[[from-datasets-to-dataloaders]]
Lo siguiente que puede salir mal en el pipeline de entrenamiento es cuando el Trainer intenta formar lotes a partir del conjunto de entrenamiento o validación. Una vez que estés seguro de que los conjuntos de datos del Trainer son correctos, puedes intentar formar un lote manualmente ejecutando lo siguiente (reemplaza train con eval para el dataloader de validación):
for batch in trainer.get_train_dataloader():
break
Este código crea el dataloader de entrenamiento, luego itera a través de él, deteniéndose en la primera iteración. Si el código se ejecuta sin error, tienes el primer lote de entrenamiento que puedes inspeccionar, y si el código da error, sabes con seguridad que el problema está en el dataloader, como es el caso aquí:
~/git/transformers/src/transformers/data/data_collator.py in torch_default_data_collator(features)
105 batch[k] = torch.stack([f[k] for f in features])
106 else:
--> 107 batch[k] = torch.tensor([f[k] for f in features])
108
109 return batch
ValueError: expected sequence of length 45 at dim 1 (got 76)
Inspeccionar el último frame del traceback debería ser suficiente para darte una pista, pero vamos a profundizar un poco más. La mayoría de los problemas durante la creación de lotes surgen debido a la colación de ejemplos en un solo lote, así que lo primero que debes verificar cuando tengas dudas es qué collate_fn está usando tu DataLoader:
data_collator = trainer.get_train_dataloader().collate_fn
data_collator
<function transformers.data.data_collator.default_data_collator(features: List[InputDataClass], return_tensors='pt') -> Dict[str, Any]>
Así que este es el default_data_collator, pero eso no es lo que queremos en este caso. Queremos rellenar nuestros ejemplos a la oración más larga del lote, lo cual se hace con el collator DataCollatorWithPadding. Y se supone que este collator de datos debe ser usado por defecto por el Trainer, entonces ¿por qué no se usa aquí?
La respuesta es porque no pasamos el tokenizer al Trainer, por lo que no pudo crear el DataCollatorWithPadding que queremos. En la práctica, nunca debes dudar en pasar explícitamente el collator de datos que deseas usar, para asegurarte de evitar este tipo de errores. Adapta nuestro código para hacer exactamente eso:
from datasets import load_dataset
from transformers import (
AutoTokenizer,
AutoModelForSequenceClassification,
DataCollatorWithPadding,
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)
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,
)
trainer.train()
¿La buena noticia? No obtenemos el mismo error que antes, lo cual es definitivamente un progreso. ¿La mala noticia? En su lugar, obtenemos un infame error de CUDA:
RuntimeError: CUDA error: CUBLAS_STATUS_ALLOC_FAILED when calling `cublasCreate(handle)`
Esto es malo porque los errores de CUDA son extremadamente difíciles de depurar en general. Veremos en un minuto cómo resolver esto, pero primero terminemos nuestro análisis de la creación de lotes.
Si estás seguro de que tu collator de datos es el correcto, deberías intentar aplicarlo en un par de muestras de tu conjunto de datos:
data_collator = trainer.get_train_dataloader().collate_fn
batch = data_collator([trainer.train_dataset[i] for i in range(4)])
Este código fallará porque el train_dataset contiene columnas de cadena, que el Trainer suele eliminar. Puedes eliminarlas manualmente, o si quieres replicar exactamente lo que el Trainer está haciendo entre bastidores, puedes llamar al método privado Trainer._remove_unused_columns() que hace eso:
data_collator = trainer.get_train_dataloader().collate_fn
actual_train_set = trainer._remove_unused_columns(trainer.train_dataset)
batch = data_collator([actual_train_set[i] for i in range(4)])
Entonces deberías poder depurar manualmente lo que sucede dentro del collator de datos si el error persiste.
Ahora que hemos depurado el proceso de creación de lotes, ¡es hora de pasar uno por el modelo!
Pasando por el modelo[[going-through-the-model]]
Deberías poder obtener un lote ejecutando el siguiente comando:
for batch in trainer.get_train_dataloader():
break
Si estás ejecutando este código en un notebook, es posible que obtengas un error de CUDA similar al que vimos antes, en cuyo caso debes reiniciar tu notebook y volver a ejecutar el último fragmento sin la línea trainer.train(). Esa es la segunda cosa más molesta de los errores de CUDA: rompen irremediablemente tu kernel. Lo más molesto de ellos es el hecho de que son difíciles de depurar.
¿Por qué es eso? Tiene que ver con la forma en que funcionan las GPU. Son extremadamente eficientes en la ejecución de muchas operaciones en paralelo, pero el inconveniente es que cuando una de esas instrucciones resulta en un error, no lo sabes al instante. Es solo cuando el programa llama a una sincronización de los múltiples procesos en la GPU que se dará cuenta de que algo salió mal, por lo que el error se genera en un lugar que no tiene nada que ver con lo que lo creó. Por ejemplo, si miramos nuestro traceback anterior, el error se generó durante el paso hacia atrás, pero veremos en un minuto que en realidad se deriva de algo en el paso hacia adelante.
Entonces, ¿cómo depuramos esos errores? La respuesta es fácil: no lo hacemos. A menos que tu error de CUDA sea un error de falta de memoria (lo que significa que no hay suficiente memoria en tu GPU), siempre debes volver a la CPU para depurarlo.
Para hacer esto en nuestro caso, solo tenemos que volver a poner el modelo en la CPU y llamarlo en nuestro lote; el lote devuelto por el DataLoader aún no se ha movido a la GPU:
outputs = trainer.model.cpu()(**batch)
~/.pyenv/versions/3.7.9/envs/base/lib/python3.7/site-packages/torch/nn/functional.py in nll_loss(input, target, weight, size_average, ignore_index, reduce, reduction)
2386 )
2387 if dim == 2:
-> 2388 ret = torch._C._nn.nll_loss(input, target, weight, _Reduction.get_enum(reduction), ignore_index)
2389 elif dim == 4:
2390 ret = torch._C._nn.nll_loss2d(input, target, weight, _Reduction.get_enum(reduction), ignore_index)
IndexError: Target 2 is out of bounds.
Así que, la imagen se está aclarando. En lugar de tener un error de CUDA, ahora tenemos un IndexError en el cálculo de la pérdida (así que nada que ver con el paso hacia atrás, como dijimos antes). Más precisamente, podemos ver que es el objetivo 2 el que crea el error, así que este es un muy buen momento para verificar el número de etiquetas de nuestro modelo:
trainer.model.config.num_labels
2
Con dos etiquetas, solo se permiten 0s y 1s como objetivos, pero según el mensaje de error obtuvimos un 2. Obtener un 2 es en realidad normal: si recordamos los nombres de las etiquetas que extrajimos antes, había tres, por lo que tenemos los índices 0, 1 y 2 en nuestro conjunto de datos. El problema es que no le dijimos eso a nuestro modelo, que debería haberse creado con tres etiquetas. ¡Así que arreglemos eso!
from datasets import load_dataset
from transformers import (
AutoTokenizer,
AutoModelForSequenceClassification,
DataCollatorWithPadding,
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, num_labels=3)
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)
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,
)
Todavía no estamos incluyendo la línea trainer.train(), para tomarnos el tiempo de verificar que todo se vea bien. Si solicitamos un lote y lo pasamos a nuestro modelo, ¡ahora funciona sin errores!
for batch in trainer.get_train_dataloader():
break
outputs = trainer.model.cpu()(**batch)
El siguiente paso es volver a la GPU y verificar que todo siga funcionando:
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)
Si sigues obteniendo un error, asegúrate de reiniciar tu notebook y solo ejecutar la última versión del script.
Realizando un paso de optimización[[performing-one-optimization-step]]
Ahora que sabemos que podemos construir lotes que realmente pasan por el modelo, estamos listos para el siguiente paso del pipeline de entrenamiento: calcular los gradientes y realizar un paso de optimización.
La primera parte es solo cuestión de llamar al método backward() en la pérdida:
loss = outputs.loss
loss.backward()
Es bastante raro obtener un error en esta etapa, pero si lo obtienes, asegúrate de volver a la CPU para obtener un mensaje de error útil.
Para realizar el paso de optimización, solo necesitamos crear el optimizer y llamar a su método step():
trainer.create_optimizer()
trainer.optimizer.step()
De nuevo, si estás usando el optimizador predeterminado en el Trainer, no deberías obtener un error en esta etapa, pero si tienes un optimizador personalizado, podría haber algunos problemas que depurar aquí. No olvides volver a la CPU si obtienes un error de CUDA extraño en esta etapa. Hablando de errores de CUDA, antes mencionamos un caso especial. Echemos un vistazo a eso ahora.
Lidiando con errores de falta de memoria de CUDA[[dealing-with-cuda-out-of-memory-errors]]
Cada vez que recibes un mensaje de error que comienza con RuntimeError: CUDA out of memory, esto indica que te has quedado sin memoria de GPU. Esto no está directamente relacionado con tu código, y puede ocurrir con un script que se ejecuta perfectamente bien. Este error significa que intentaste poner demasiadas cosas en la memoria interna de tu GPU, y eso resultó en un error. Al igual que con otros errores de CUDA, deberás reiniciar tu kernel para poder volver a ejecutar tu entrenamiento.
Para resolver este problema, solo necesitas usar menos espacio de GPU, algo que a menudo es más fácil de decir que de hacer. Primero, asegúrate de no tener dos modelos en la GPU al mismo tiempo (a menos que sea necesario para tu problema, por supuesto). Luego, probablemente deberías reducir el tamaño de tu lote, ya que afecta directamente los tamaños de todas las salidas intermedias del modelo y sus gradientes. Si el problema persiste, considera usar una versión más pequeña de tu modelo.
[!TIP] En la siguiente parte del curso, veremos técnicas más avanzadas que pueden ayudarte a reducir tu huella de memoria y permitirte ajustar los modelos más grandes.
Evaluando el modelo[[evaluating-the-model]]
Ahora que hemos resuelto todos los problemas con nuestro código, todo es perfecto y el entrenamiento debería ejecutarse sin problemas, ¿verdad? ¡No tan rápido! Si ejecutas el comando trainer.train(), todo parecerá bien al principio, pero después de un tiempo obtendrás lo siguiente:
# This will take a long time and error out, so you shouldn't run this cell
trainer.train()
TypeError: only size-1 arrays can be converted to Python scalars
Te darás cuenta de que este error aparece durante la fase de evaluación, así que esto es lo último que tendremos que depurar.
Puedes ejecutar el bucle de evaluación del Trainer independientemente del entrenamiento de esta manera:
trainer.evaluate()
TypeError: only size-1 arrays can be converted to Python scalars
[!TIP] 💡 Siempre debes asegurarte de poder ejecutar
trainer.evaluate()antes de lanzartrainer.train(), para evitar desperdiciar muchos recursos computacionales antes de encontrar un error.
Antes de intentar depurar un problema en el bucle de evaluación, primero debes asegurarte de haber echado un vistazo a los datos, de poder formar un lote correctamente y de poder ejecutar tu modelo en él. Hemos completado todos esos pasos, por lo que el siguiente código puede ejecutarse sin errores:
for batch in trainer.get_eval_dataloader():
break
batch = {k: v.to(device) for k, v in batch.items()}
with torch.no_grad():
outputs = trainer.model(**batch)
El error aparece más tarde, al final de la fase de evaluación, y si miramos el traceback vemos esto:
~/git/datasets/src/datasets/metric.py in add_batch(self, predictions, references)
431 """
432 batch = {"predictions": predictions, "references": references}
--> 433 batch = self.info.features.encode_batch(batch)
434 if self.writer is None:
435 self._init_writer()
Esto nos dice que el error se origina en el módulo datasets/metric.py, por lo que es un problema con nuestra función compute_metrics(). Toma una tupla con los logits y las etiquetas como arrays de NumPy, así que intentemos alimentarle eso:
predictions = outputs.logits.cpu().numpy()
labels = batch["labels"].cpu().numpy()
compute_metrics((predictions, labels))
TypeError: only size-1 arrays can be converted to Python scalars
Obtenemos el mismo error, así que el problema definitivamente reside en esa función. Si volvemos a mirar su código, vemos que solo está reenviando el predictions y el labels a metric.compute(). Entonces, ¿hay un problema con ese método? Realmente no. Echemos un vistazo rápido a las formas:
predictions.shape, labels.shape
((8, 3), (8,))
Nuestras predicciones siguen siendo logits, no las predicciones reales, por lo que la métrica devuelve este error (algo oscuro). La solución es bastante fácil; solo tenemos que añadir un argmax en la función compute_metrics():
def compute_metrics(eval_pred):
predictions, labels = eval_pred
predictions = np.argmax(predictions, axis=1)
return metric.compute(predictions=predictions, references=labels)
compute_metrics((predictions, labels))
{'accuracy': 0.625}
¡Ahora nuestro error está solucionado! Este fue el último, así que nuestro script ahora entrenará un modelo correctamente.
Como referencia, aquí está el script completamente corregido:
from datasets import load_dataset
from transformers import (
AutoTokenizer,
AutoModelForSequenceClassification,
DataCollatorWithPadding,
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, num_labels=3)
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
predictions = np.argmax(predictions, axis=1)
return metric.compute(predictions=predictions, references=labels)
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,
)
trainer.train()
En este caso, no hay más problemas, y nuestro script ajustará un modelo que debería dar resultados razonables. Pero, ¿qué podemos hacer cuando el entrenamiento procede sin ningún error, y el modelo entrenado no funciona bien en absoluto? Esa es la parte más difícil del aprendizaje automático, y te mostraremos algunas técnicas que pueden ayudar.
[!TIP] 💡 Si estás utilizando un bucle de entrenamiento manual, los mismos pasos se aplican para depurar tu pipeline de entrenamiento, pero es más fácil separarlos. ¡Asegúrate de no haber olvidado el
model.eval()omodel.train()en los lugares correctos, o elzero_grad()en cada paso, sin embargo!
Depuración de errores silenciosos durante el entrenamiento[[debugging-silent-errors-during-training]]
¿Qué podemos hacer para depurar un entrenamiento que se completa sin errores pero no obtiene buenos resultados? Te daremos algunas pistas aquí, pero ten en cuenta que este tipo de depuración es la parte más difícil del aprendizaje automático, y no hay una respuesta mágica.
Verifica tus datos (¡otra vez!)[[check-your-data-again]]
Tu modelo solo aprenderá algo si realmente es posible aprender algo de tus datos. Si hay un error que corrompe los datos o las etiquetas se atribuyen aleatoriamente, es muy probable que no obtengas ningún entrenamiento de modelo en tu conjunto de datos. Así que siempre comienza por verificar tus entradas y etiquetas decodificadas, y hazte las siguientes preguntas:
- ¿Los datos decodificados son comprensibles?
- ¿Estás de acuerdo con las etiquetas?
- ¿Hay una etiqueta que sea más común que las otras?
- ¿Cuál debería ser la pérdida/métrica si el modelo predijo una respuesta aleatoria/siempre la misma respuesta?
[!WARNING] ⚠️ Si estás realizando un entrenamiento distribuido, imprime muestras de tu conjunto de datos en cada proceso y verifica tres veces que obtienes lo mismo. Un error común es tener alguna fuente de aleatoriedad en la creación de datos que hace que cada proceso tenga una versión diferente del conjunto de datos.
Después de mirar tus datos, revisa algunas de las predicciones del modelo y decodifícalas también. Si el modelo siempre predice lo mismo, podría ser porque tu conjunto de datos está sesgado hacia una categoría (para problemas de clasificación); técnicas como el sobremuestreo de clases raras podrían ayudar.
Si la pérdida/métrica que obtienes en tu modelo inicial es muy diferente de la pérdida/métrica que esperarías para predicciones aleatorias, verifica la forma en que se calcula tu pérdida o métrica, ya que probablemente haya un error allí. Si estás utilizando varias pérdidas que sumas al final, asegúrate de que sean de la misma escala.
Cuando estés seguro de que tus datos son perfectos, puedes ver si el modelo es capaz de entrenar con ellos con una simple prueba.
Sobreajusta tu modelo en un lote[[overfit-your-model-on-one-batch]]
El sobreajuste suele ser algo que intentamos evitar al entrenar, ya que significa que el modelo no está aprendiendo a reconocer las características generales que queremos que reconozca, sino que simplemente está memorizando las muestras de entrenamiento. Sin embargo, intentar entrenar tu modelo en un lote una y otra vez es una buena prueba para verificar si el problema tal como lo has planteado puede ser resuelto por el modelo que intentas entrenar. También te ayudará a ver si tu tasa de aprendizaje inicial es demasiado alta.
Hacer esto una vez que hayas definido tu Trainer es realmente fácil; simplemente toma un lote de datos de entrenamiento, luego ejecuta un pequeño bucle de entrenamiento manual usando solo ese lote durante unos 20 pasos:
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()
[!TIP] 💡 Si tus datos de entrenamiento están desequilibrados, asegúrate de construir un lote de datos de entrenamiento que contenga todas las etiquetas.
El modelo resultante debería tener resultados casi perfectos en el mismo batch. Calculemos la métrica en las predicciones resultantes:
with torch.no_grad():
outputs = trainer.model(**batch)
preds = outputs.logits
labels = batch["labels"]
compute_metrics((preds.cpu().numpy(), labels.cpu().numpy()))
{'accuracy': 1.0}
100% de precisión, ¡ahora este es un buen ejemplo de sobreajuste (lo que significa que si pruebas tu modelo en cualquier otra oración, es muy probable que te dé una respuesta incorrecta)!
Si no logras que tu modelo obtenga resultados perfectos como este, significa que hay algo mal en la forma en que planteaste el problema o en tus datos, por lo que deberías solucionarlo. Solo cuando logres pasar la prueba de sobreajuste podrás estar seguro de que tu modelo realmente puede aprender algo.
[!WARNING] ⚠️ Tendrás que recrear tu modelo y tu
Trainerdespués de esta prueba, ya que el modelo obtenido probablemente no podrá recuperarse y aprender algo útil en tu conjunto de datos completo.
No ajustes nada hasta que tengas una primera línea base[[dont-tune-anything-until-you-have-a-first-baseline]]
El ajuste de hiperparámetros siempre se enfatiza como la parte más difícil del aprendizaje automático, pero es solo el último paso para ayudarte a ganar un poco en la métrica. La mayoría de las veces, los hiperparámetros predeterminados del Trainer funcionarán bien para darte buenos resultados, así que no te lances a una búsqueda de hiperparámetros costosa y que consume mucho tiempo hasta que tengas algo que supere la línea base que tienes en tu conjunto de datos.
Una vez que tengas un modelo lo suficientemente bueno, puedes empezar a ajustar un poco. No intentes lanzar mil ejecuciones con diferentes hiperparámetros, sino compara un par de ejecuciones con diferentes valores para un hiperparámetro para tener una idea de cuál tiene el mayor impacto.
Si estás ajustando el modelo en sí, mantenlo simple y no intentes nada que no puedas justificar razonablemente. Siempre asegúrate de volver a la prueba de sobreajuste para verificar que tu cambio no haya tenido consecuencias no deseadas.
Pide ayuda[[ask-for-help]]
Esperamos que hayas encontrado algún consejo en esta sección que te haya ayudado a resolver tu problema, pero si no es así, recuerda que siempre puedes preguntar a la comunidad en los foros.
Aquí tienes algunos recursos adicionales que pueden ser útiles:
- "Reproducibility as a vehicle for engineering best practices" por Joel Grus
- "Checklist for debugging neural networks" por Cecelia Shao
- "How to unit test machine learning code" por Chase Roberts
- "A Recipe for Training Neural Networks" por Andrej Karpathy
¡Por supuesto, no todos los problemas que encuentras al entrenar redes neuronales son culpa tuya! Si encuentras algo en la biblioteca 🤗 Transformers o 🤗 Datasets que no parece correcto, es posible que hayas encontrado un error. Definitivamente deberías contarnos todo al respecto, y en la siguiente sección te explicaremos exactamente cómo hacerlo.