NLP Course documentation

Отладка обучения

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Отладка обучения

Ask a Question Open In Colab Open In Studio Lab

Вы написали прекрасный сценарий для обучения или дообучения модели на заданной задаче, послушно следуя советам из Главы 7. Но когда вы запускаете команду model.fit(), происходит нечто ужасное: вы получаете ошибку 😱! Или, что еще хуже, все вроде бы хорошо, обучение проходит без ошибок, но результирующая модель получается плохой. В этом разделе мы покажем вам, что можно сделать для отладки подобных проблем.

Отладка обучающего пайплайна

Проблема, когда вы сталкиваетесь с ошибкой в trainer.train(), заключается в том, что она может возникнуть из нескольких источников, поскольку Trainer обычно собирает вместе множество вещей. Он преобразует наборы данных в загрузчики данных, поэтому проблема может заключаться в том, что в вашем наборе данных что-то не так, или в том, что вы пытаетесь объединить элементы наборов данных в батч. Затем он берет батч данных и подает его в модель, так что проблема может быть в коде модели. После этого вычисляются градиенты и выполняется этап оптимизации, так что проблема может быть и в вашем оптимизаторе. И даже если во время обучения все идет хорошо, во время валидации все равно что-то может пойти не так, если проблема в метрике.

Лучший способ отладить ошибку, возникшую в trainer.train(), - это вручную пройти весь пайплайн и посмотреть, где все пошло не так. В этом случае ошибку часто очень легко устранить.

Чтобы продемонстрировать это, мы используем следующий скрипт, который (пытается) точно настроить модель DistilBERT на наборе данных MNLI dataset:

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()

Если вы попытаетесь выполнить его, то столкнетесь с довольно загадочной ошибкой:

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

Проверка данных

Это может быть очевидно, но если ваши данные повреждены, Trainer не сможет сформировать батчи, не говоря уже об обучении вашей модели. Поэтому прежде всего необходимо посмотреть, что находится в вашем обучающем наборе.

Чтобы избежать бесчисленных часов, потраченных на попытки исправить то, что не является источником ошибки, мы рекомендуем использовать trainer.train_dataset для проверок и ничего больше. Так что давайте сделаем это здесь:

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.'}

Вы заметили что-то неладное? В сочетании с сообщением об ошибке input_ids, вы должны понять, что это тексты, а не числа, которые модель может понять. Здесь исходная ошибка вводит в заблуждение, потому что Trainer автоматически удаляет столбцы, которые не соответствуют сигнатуре модели (то есть аргументам, ожидаемым моделью). Это означает, что здесь было удалено все, кроме меток. Таким образом, не было никаких проблем с созданием батчей и отправкой их модели, которая, в свою очередь, жаловалась, что не получила нужных входных данных.

Почему данные не обрабатывались? Мы действительно использовали метод Dataset.map() для наборов данных, чтобы применить токенизатор к каждой выборке. Но если вы внимательно посмотрите на код, то увидите, что мы допустили ошибку при передаче обучающего и валидационного наборов в Trainer. Вместо того чтобы использовать tokenized_datasets, мы использовали raw_datasets 🤦. Так что давайте исправим это!

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=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation_matched"],
    compute_metrics=compute_metrics,
)
trainer.train()

Теперь этот новый код будет выдавать другую ошибку (прогресс!):

'ValueError: expected sequence of length 43 at dim 1 (got 37)'

Посмотрев на трассировку, мы видим, что ошибка происходит на этапе сборки данных:

~/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

Поэтому нам следует перейти к этому. Однако перед этим давайте закончим проверку наших данных, чтобы быть на 100% уверенными в их правильности.

При отладке обучения всегда нужно смотреть на декодированные входы модели. Мы не можем понять смысл чисел, которые подаем ей напрямую, поэтому мы должны посмотреть, что эти числа представляют. В компьютерном зрении, например, это означает просмотр декодированных изображений пройденных пикселей, в речи - прослушивание декодированных образцов звука, а в нашем примере с NLP - использование нашего токенизатора для декодирования входных данных:

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]'

Так что, похоже, все правильно. Вы должны сделать это для всех ключей во входах:

trainer.train_dataset[0].keys()
dict_keys(['attention_mask', 'hypothesis', 'idx', 'input_ids', 'label', 'premise'])

Обратите внимание, что ключи, не соответствующие входам, принимаемым моделью, будут автоматически отброшены, поэтому здесь мы оставим только input_ids, attention_mask и label (которая будет переименована в labels). Чтобы перепроверить сигнатуру модели, вы можете вывести класс вашей модели, а затем проверить ее документацию:

type(trainer.model)
transformers.models.distilbert.modeling_distilbert.DistilBertForSequenceClassification

Итак, в нашем случае мы можем проверить принятые параметры на этой странице. “Trainer” также будет регистрировать столбцы, которые он отбрасывает.

Мы проверили правильность входных идентификаторов, декодировав их. Далее находится 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]

Так как мы не применяли в препроцессинге дополнение нулями, это кажется совершенно естественным. Чтобы убедиться в отсутствии проблем с этой маской внимания, давайте проверим, что она имеет ту же длину, что и наши входные идентификаторы:

len(trainer.train_dataset[0]["attention_mask"]) == len(
    trainer.train_dataset[0]["input_ids"]
)
True

Это хорошо! И наконец, давайте проверим нашу метку класса:

trainer.train_dataset[0]["label"]
1

Как и входные идентификаторы, это число, которое само по себе не имеет смысла. Как мы уже видели, соответствие между целыми числами и именами меток хранится в атрибуте names соответствующей функции набора данных:

trainer.train_dataset.features["label"].names
['entailment', 'neutral', 'contradiction']

Итак, 1 означает нейтральный, а значит, два предложения, которые мы видели выше, не противоречат друг другу, и из первого не следует второе. Это кажется правильным!

Здесь у нас нет идентификаторов типов токенов, поскольку DistilBERT их не ожидает; если в вашей модели они есть, вам также следует убедиться, что они правильно соответствуют месту первого и второго предложений во входных данных.

✏️ Ваша очередь! Проверьте, все ли правильно со вторым элементом обучающего набора данных.

В данном случае мы проверяем только обучающий набор, но, конечно, вы должны дважды проверить валидационный и тестовый наборы таким же образом.

Теперь, когда мы знаем, что наши наборы данных выглядят хорошо, пришло время проверить следующий этап пайплайна обучения.

От датасетов к загрузчикам данных

Следующее, что может пойти не так в пайплайне обучения, - это когда Trainer пытается сформировать батчи из обучающего или проверочного набора. Убедившись, что наборы данных Trainer корректны, можно попробовать вручную сформировать батч, выполнив следующие действия (замените train на eval для валидационного загрузчика данных):

for batch in trainer.get_train_dataloader():
    break

Этот код создает загрузчик данных для обучения, затем выполняет итерации по нему, останавливаясь на первой итерации. Если код выполняется без ошибок, у вас есть первый обучающий батч, который можно проверить, а если код выдает ошибку, вы точно знаете, что проблема в загрузчике данных, как в данном случае:

~/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)

Осмотра последнего кадра трассировки должно быть достаточно, чтобы дать вам подсказку, но давайте покопаемся еще немного. Большинство проблем при создании батчей возникает из-за объединения примеров в один батч, поэтому первое, что нужно проверить при возникновении сомнений, это то, какой collate_fn использует ваш 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]>

Этот default_data_collator не то, что нам нужно в данном случае. Мы хотим разбить наши примеры на самые длинные предложения в пакете, что делает DataCollatorWithPadding. И этот класс данных должен использоваться по умолчанию в Trainer, так почему же он не используется здесь?

Ответ заключается в том, что мы не передали tokenizer в Trainer, поэтому он не смог создать нужный нам DataCollatorWithPadding. На практике вам никогда не следует стесняться явно передавать data collator, который вы хотите использовать, чтобы избежать подобных ошибок. Давайте адаптируем наш код, чтобы сделать именно это:

from datasets import load_dataset
import evaluate
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()

Хорошие новости? Мы не получаем ту же ошибку, что и раньше, что, безусловно, является прогрессом. Плохие новости? Вместо нее мы получаем печально известную ошибку CUDA:

RuntimeError: CUDA error: CUBLAS_STATUS_ALLOC_FAILED when calling `cublasCreate(handle)`

Это плохо, потому что ошибки CUDA вообще очень трудно отлаживать. Через минуту мы увидим, как решить эту проблему, но сначала давайте закончим анализ создания батчей.

Если вы уверены, что ваш data collator правильный, попробуйте применить его на паре примеров вашего набора данных:

data_collator = trainer.get_train_dataloader().collate_fn
batch = data_collator([trainer.train_dataset[i] for i in range(4)])

Этот код не сработает, потому что train_dataset содержит строковые колонки, которые Trainer обычно удаляет. Вы можете удалить их вручную, или, если вы хотите в точности повторить то, что Trainer делает за кулисами: нужно вызвать приватный метод Trainer._remove_unused_columns(), который делает это:

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)])

Если ошибка не исчезнет, вы сможете вручную отладить, что происходит внутри data collator.

Теперь, когда мы отладили процесс создания батча, пришло время пропустить его через модель!

Проверьте данные

Вы должны иметь возможность получить батч, выполнив следующую команду:

for batch in trainer.get_train_dataloader():
    break

Если вы выполняете этот код в ноутбуке, вы можете получить ошибку CUDA, похожую на ту, что мы видели ранее, и в этом случае вам нужно перезапустить ноутбук и заново выполнить последний фрагмент без строки trainer.train(). Это вторая самая неприятная вещь в ошибках CUDA: они безвозвратно ломают ваше ядро. Самое неприятное в них то, что их трудно отлаживать.

Почему? Это связано с тем, как работают графические процессоры. Они чрезвычайно эффективны при параллельном выполнении множества операций, но их недостаток в том, что когда одна из этих инструкций приводит к ошибке, вы не сразу об этом узнаете. Только когда программа вызовет синхронизацию нескольких процессов на GPU, она поймет, что что-то пошло не так, поэтому ошибка возникает в том месте, которое не имеет никакого отношения к тому, что ее создало. Например, если мы посмотрим на наш предыдущий трассировочный откат, ошибка была вызвана во время обратного прохода, но через минуту мы увидим, что на самом деле она возникла из-за чего-то в прямом проходе через модель.

Так как же отладить эти ошибки? Ответ прост: никак. Если только ошибка CUDA не является ошибкой вне памяти (что означает, что в вашем GPU недостаточно памяти), вы всегда должны возвращаться к CPU, чтобы отладить ее.

Чтобы сделать это в нашем случае, нам просто нужно вернуть модель на CPU и вызвать ее на нашем пакете - пакет, возвращаемый DataLoader, еще не был перемещен на 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.

Итак, картина проясняется. Вместо ошибки CUDA у нас теперь IndexError при вычислении функции потерь (так что обратный проход, как мы уже говорили, здесь ни при чем). Точнее, мы видим, что ошибка возникает именно в метке класса 2, так что это очень хороший момент для проверки количества меток нашей модели:

trainer.model.config.num_labels
2

При двух метках в качестве значений допускаются только 0 и 1, но, согласно сообщению об ошибке, мы получили 2. Получение 2 на самом деле нормально: если мы помним имена меток, которые мы извлекли ранее, их было три, поэтому в нашем наборе данных есть индексы 0, 1 и 2. Проблема в том, что мы не сообщили об этом нашей модели, которая должна была быть создана с тремя метками. Так что давайте это исправим!

from datasets import load_dataset
import evaluate
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,
)

Мы пока не включаем строку trainer.train(), чтобы потратить время на проверку того, что все выглядит хорошо. Если мы запросим батч и передадим ее нашей модели, то теперь она работает без ошибок!

for batch in trainer.get_train_dataloader():
    break

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

Следующим шагом будет возвращение к графическому процессору и проверка того, что все по-прежнему работает:

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)

Если вы все еще получаете ошибку, убедитесь, что перезагрузили ноутбук и выполнили только последнюю версию скрипта.

Выполнение одного шага оптиимзации

Теперь, когда мы знаем, что можем создавать батчи, которые действительно проходят через модель без ошибок, мы готовы к следующему шагу пайплайна обучения: вычислению градиентов и выполнению шага оптимизации.

Первая часть заключается в вызове метода backward() для функции потерь:

loss = outputs.loss
loss.backward()

Ошибки на этом этапе возникают довольно редко, но если они все же возникнут, обязательно вернитесь к процессору, чтобы получить полезное сообщение об ошибке.

Чтобы выполнить шаг оптимизации, нам нужно просто создать optimizer и вызвать его метод step():

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

Опять же, если вы используете оптимизатор по умолчанию в Trainer, вы не должны получить ошибку на этом этапе, но если вы используете собственный оптимизатор, здесь могут возникнуть некоторые проблемы для отладки. Не забудьте вернуться к процессору, если на этом этапе вы получите странную ошибку CUDA. Говоря об ошибках CUDA, ранее мы упоминали особый случай. Давайте посмотрим на него сейчас.

Как справиться с ошибками нехватки памяти

Если вы получаете сообщение об ошибке, начинающееся с RuntimeError: CUDA out of memory, это означает, что вам не хватает памяти GPU. Это не связано напрямую с вашим кодом, и может произойти со скриптом, который работает совершенно нормально. Эта ошибка означает, что вы пытались поместить слишком много данных во внутреннюю память вашего GPU, и это привело к ошибке. Как и в случае с другими ошибками CUDA, вам придется перезапустить ядро, чтобы снова запустить обучение.

Чтобы решить эту проблему, нужно просто использовать меньше памяти на GPU - что зачастую легче сказать, чем сделать. Во-первых, убедитесь, что у вас нет двух моделей на GPU одновременно (если, конечно, это не требуется для решения вашей задачи). Затем, вероятно, следует уменьшить размер батча, поскольку он напрямую влияет на размеры всех промежуточных выходов модели и их градиентов. Если проблема сохраняется, подумайте о том, чтобы использовать меньшую версию модели.

В следующей части курса мы рассмотрим более продвинутые техники, которые помогут вам уменьшить объем занимаемой памяти и позволят точно настроить самые большие модели.

Валидация модели

Теперь, когда мы решили все проблемы с нашим кодом, все идеально, и обучение должно пройти гладко, верно? Не так быстро! Если вы запустите команду trainer.train(), сначала все будет выглядеть хорошо, но через некоторое время вы получите следующее:

# Это займет много времени и приведет к ошибке, поэтому не стоит запускать эту ячейку
trainer.train()
TypeError: only size-1 arrays can be converted to Python scalars

Вы поймете, что эта ошибка появляется во время фазы валидации, так что это последнее, что нам нужно будет отладить.

Вы можете запустить цикл оценки Trainer независимо от обучения следующим образом:

trainer.evaluate()
TypeError: only size-1 arrays can be converted to Python scalars

💡 Перед запуском trainer.train() всегда следует убедиться, что вы можете запустить trainer.evaluate(), чтобы не тратить много вычислительных ресурсов до того, как столкнетесь с ошибкой.

Прежде чем пытаться отладить проблему в цикле валидации, нужно сначала убедиться, что вы посмотрели на данные, смогли правильно сформировать батч и запустить на нем свою модель. Мы выполнили все эти шаги, поэтому следующий код может быть выполнен без ошибок:

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)

Ошибка возникает позже, в конце фазы валидации, и если мы посмотрим на трассировку, то увидим следующее:

~/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()

Это говорит нам о том, что ошибка возникает в модуле datasets/metric.py - так что это проблема с нашей функцией compute_metrics(). Она принимает кортеж с логитами и метками в виде массивов NumPy, так что давайте попробуем скормить ей это:

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

Мы получаем ту же ошибку, так что проблема определенно кроется в этой функции. Если мы посмотрим на ее код, то увидим, что она просто передает predictions и labels в metric.compute(). Так есть ли проблема в этом методе? На самом деле нет. Давайте посмотрим на размерности:

predictions.shape, labels.shape
((8, 3), (8,))

Наши предсказания все еще являются логитами, а не реальными предсказаниями, поэтому метрика возвращает эту (несколько непонятную) ошибку. Исправить это довольно просто: нужно просто добавить argmax в функцию compute_metrics():

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)


compute_metrics((predictions, labels))
{'accuracy': 0.625}

Теперь наша ошибка исправлена! Она была последней, поэтому теперь наш скрипт будет правильно обучать модель.

Для справки, вот полностью исправленный скрипт:

import numpy as np
from datasets import load_dataset
import evaluate
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()

В этом случае проблем больше нет, и наш скрипт обучит модель, которая должна дать приемлемые результаты. Но что делать, если обучение проходит без ошибок, а обученная модель совсем не работает? Это самая сложная часть машинного обучения, и мы покажем вам несколько приемов, которые могут помочь.

💡 Если вы используете ручной цикл обучения, для отладки пайплайна обучения применимы те же шаги, но их проще разделить. Убедитесь, что вы не забыли model.eval() или model.train() в нужных местах, или zero_grad() на каждом шаге!

Отладка скрытых ошибок во время обучения

Что можно сделать, чтобы отладить обучение, которое завершается без ошибок, но не дает хороших результатов? Мы дадим вам несколько советов, но имейте в виду, что такая отладка - самая сложная часть машинного обучения, и волшебного ответа на этот вопрос не существует.

Проверьте свои данные (еще раз!)

Ваша модель научится чему-то только в том случае, если из ваших данных действительно можно чему-то научиться. Если в данных есть ошибка, которая портит их, или метки приписаны случайным образом, то, скорее всего, вы не сможете обучить модель на своем наборе данных. Поэтому всегда начинайте с перепроверки декодированных входных данных и меток и задавайте себе следующие вопросы:

  • Понятны ли декодированные данные?
  • Правильные ли метки?
  • Есть ли одна метка, которая встречается чаще других?
  • Каким должно быть значение функции потерь/метрики если модель предсказала случайный ответ/всегда один и тот же ответ?

⚠️ Если вы проводите распределенное обучение, распечатайте образцы набора данных в каждом процессе и трижды проверьте, что вы получаете одно и то же. Одна из распространенных ошибок - наличие некоторого источника случайности при создании данных, из-за которого каждый процесс имеет свою версию набора данных.

Просмотрев данные, проанализируйте несколько предсказаний модели и декодируйте их. Если модель постоянно предсказывает одно и то же, это может быть связано с тем, что ваш набор данных смещен в сторону одной категории (для проблем классификации); здесь могут помочь такие методы, как oversampling редких классов.

Если значения функции потерь/метрики, которые вы получаете на начальной модели, сильно отличаются от значений функции потерь/метрик, которые можно было бы ожидать для случайных предсказаний, перепроверьте способ вычисления потерь или метрик, так как, возможно, в них есть ошибка. Если вы используете несколько функций потерь, убедитесь, что они имеют одинаковый масштаб.

Когда вы убедитесь, что ваши данные идеальны, вы можете проверить, способна ли модель обучаться на них, с помощью одного простого теста.

Переобучение модели на одном батче

Обычно мы стараемся избегать переобучения при тренировке модели, поскольку это означает, что модель не учится распознавать общие характеристики, а просто запоминает обучающие выборки. Однако попытка обучить модель на одной выборке снова и снова - это хороший тест, позволяющий проверить, может ли проблема в том виде, в котором вы ее сформулировали, быть решена с помощью модели, которую вы пытаетесь обучить. Это также поможет вам понять, не слишком ли высока ваша начальная скорость обучения.

Сделать это после того, как вы определили свой Trainer, очень просто: просто возьмите батч обучающих данных, а затем запустите небольшой цикл ручного обучения, используя только этот батч в течение примерно 20 шагов:

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()

💡 Если ваши обучающие данные несбалансированы, обязательно создайте батч обучающих данных, содержащий все метки.

Результирующая модель должна иметь близкие к идеальным результаты на одном и том же батче. Вычислим метрику по полученным предсказаниям:

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 %, вот это хороший пример переобучения (это значит, что если вы попробуете использовать модель на любом другом предложении, она, скорее всего, даст вам неправильный ответ)!

Если вам не удается добиться от модели таких идеальных результатов, значит, что-то не так с постановкой задачи или данными, и вам следует это исправить. Только когда вам удастся пройти тест на переобучение, вы сможете быть уверены, что ваша модель действительно способна чему-то научиться.

⚠️ Вам придется пересоздать модель и Trainer после этого теста на переобучение, поскольку полученная модель, вероятно, не сможет восстановиться и научиться чему-то полезному на полном наборе данных.

Не обучайте ничего, пока не получите первый бейзлайн.

Настройка гиперпараметров всегда считается самой сложной частью машинного обучения, но это всего лишь последний шаг, который поможет вам немного улучшить метрику. В большинстве случаев гиперпараметры по умолчанию Trainer будут работать нормально и давать вам хорошие результаты, поэтому не приступайте к трудоемкому и дорогостоящему поиску гиперпараметров, пока у вас не будет чего-то, что превосходит базовый уровень, который у вас есть в вашем наборе данных.

Как только у вас будет достаточно хорошая модель, вы можете начать ее немного оптимизировать. Не пытайтесь запустить тысячу раз с разными гиперпараметрами, но сравните пару запусков с разными значениями одного гиперпараметра, чтобы получить представление о том, какой из них оказывает наибольшее влияние.

Если вы настраиваете саму модель, будьте проще и не пробуйте то, что не можете обосновать. Всегда возвращайтесь к тесту на перебор, чтобы проверить, не привело ли ваше изменение к каким-либо непредвиденным последствиям.

Попросите о помощи

Надеемся, вы нашли в этом разделе советы, которые помогли вам решить вашу проблему, но если это не так, помните, что вы всегда можете спросить у сообщества на форумах.

Вот некоторые дополнительные ресурсы, которые могут оказаться полезными:

Конечно, не все проблемы, с которыми вы сталкиваетесь при обучении нейросетей, возникают по вашей вине! Если в библиотеке 🤗 Transformers или 🤗 Datasets вы столкнулись с чем-то, что кажется вам неправильным, возможно, вы обнаружили ошибку. Вам обязательно нужно рассказать нам об этом, и в следующем разделе мы объясним, как именно это сделать.