🗿

Hugging Faceモデル訓練時のエラー対応tips

2023/11/03に公開

概要

以下のページの纏め。
ファインチューニング時に遭遇する様々な問題に対処する方法が書かれている。
https://huggingface.co/learn/nlp-course/en/chapter8/4

Debugging the training pipeline

以下のコードでトレーニングしようとしたケース。

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'

Check your data

最初にすべきこととして、訓練データの形式を見てみる。

trainer.train_dataset[0]

input_idsが存在しないことが分かる。
またテキスト形式のカラムが存在しているが、モデルが理解できるのは数値だけ。
※Trainerはモデルのシグネチャにマッチしないカラムを自動的に削除する。

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

ここでraw_datasetsではなくtokenized_datasetsを渡すべきだった。

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

修正して再度実行。

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

次にやったほうが良いことは、モデルの入力をデコードして見ること。
モデルに入力されている数値が何を表しているのか調べる必要がある。

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

モデルのシグネチャを見るためにモデルを特定する。

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

そのモデルのページでシグニチャを確認する。
https://huggingface.co/docs/transformers/model_doc/distilbert#transformers.DistilBertForSequenceClassification

ちなみに、Trainerは必要のないカラムを除去したときにログに出しているらしい。

input_idsは確認できたので次の引数である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, 1]

input_idsと長さが合っているか確認。

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

問題なし。

True

次はlabelを確認。
※head_maskなどはoptionalなので飛ばす。
※ドキュメントだとlabelsになっているけど、それはいいのか?

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

labelと文字列の対応を見てみる。

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

1 > neutral
で問題なし。

['entailment', 'neutral', 'contradiction']

訓練データセットには問題なさそうなことがわかった。
本来は検証データセットも同じようにチェックする必要がある。

From datasets to dataloaders

Trainerが訓練データセットや検証データセットをバッチにする際にも問題が起こることがある。
手動でバッチを作成するコードは以下のようになる。
※検証セットの場合はget_eval_dataloader

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)

バッチ生成のときにエラーになる場合はだいたいcollationする際に起きている。
該当の関数は以下で取得できる。

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

今回はバッチ内の一番長い文にパディングしたいので、これはDataCollatorWithPaddingコレーターを使う必要があった。
Trainer生成時に引数として渡すように修正する。

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

一旦いくつかのサンプルでバッチ生成がうまくいくか再度見てみる。
_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)])

問題なさそうであれば、次はモデルにバッチを入力してみる。

Going through the model

バッチを1つ取得。

for batch in trainer.get_train_dataloader():
    break

GPUだとデバッグしづらいのでCPUで動かすことでデバッグし易くする。

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

損失関数の計算時にIndexErrorが発生していることがわかった。

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

labelが何個あるか見てみる。

trainer.model.config.num_labels

labelは['entailment', 'neutral', 'contradiction']の3つだったはずだが、伝わっていなかった。

2

モデル生成時にnum_labelsを指定することで修正。
trainer.train()は外しておく。

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

再度モデルにバッチを渡す。
エラーは出なくなった。

for batch in trainer.get_train_dataloader():
    break

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

モデルを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)

Performing one optimization step

モデルの実行は良さそうなので次は勾配を計算する。

loss = outputs.loss
loss.backward()

重みも更新してみる。
問題なし。

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

このあたりでエラーが出ることはあまりないが、問題が起こるようであればCPUに戻して実行してみる。

Dealing with CUDA out-of-memory errors

RuntimeError: CUDA out of memory
が出てしまった場合はGPUのメモリが足りていない。

できることとしては以下がある。

  • 2つ以上のモデルがGPUに乗っていないか確認する。
  • バッチサイズを減らしてみる。(中間出力とその勾配のサイズに直接影響する)

解決しない場合は小さいサイズのモデルを使うことを検討する。

Evaluating the model

うまくいきそうなので改めて訓練を実行してみる。

# 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

評価だけを実行してみる。
※これは実際の訓練前に通しておくと良い

trainer.evaluate()

同様のエラー。

TypeError: only size-1 arrays can be converted to Python scalars
~/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()

デバッグのために1度optputsを取得する。

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)

評価関数に通してみる。

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.shape, labels.shape

metric.computeに渡すpredictionsはラベルにする必要があるが、logitsのまま渡していた。

((8, 3), (8,))

argmaxでlabelに変換して渡す。

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

Debugging silent errors during training

学習自体は正常に実行されるのだが、良い結果が得られない場合について。

Check your data (again!)

破損したデータや、ランダムに与えられたラベルなどはないか。
デコードした入力とラベルを照らし合わせて確認する。

実際にモデルで予測した結果も確認してみる。
いつも同じ結果になっているときなどはデータセットに偏りがあるのかもしれない。

初期状態の重みのモデルで、損失やメトリックがランダムな予測で算出される値と大きく異なる場合は損失やメトリックの計算方法を再チェックする。

Overfit your model on one batch

通常過学習は良くないことだが、わざと特定のデータで過学習させることで、モデルが正しく学習できているか確認する方法がある。
また、最初の学習率が高すぎないか確認するのにも役立つ。

バッチを1つ作って、それに対してのみ学習を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}

Don't tune anything until you have a first baseline

ハイパーパラメータのチューニングは機械学習で最も難しい部分であるとされているが、それは指標を少しでも向上させるための最後のステップにすぎない。
たいていの場合、Trainerのデフォルトのハイパーパラメータで十分良い結果が得られるので、データセットにあるベースラインを上回るものができるまで、時間とコストのかかるハイパーパラメータ探索に着手する必要はない。

十分に良いモデルができたらハイパーパラメータのチューニングを行うが、
一気にパラメータを変えて1000回の訓練行うのではなく、まずは1つのパラメータに対して2,3このパターンで試して、どのパラメータが効くのか把握するのが大事。

モデルのアーキテクチャそのものを変更する場合は、シンプルに保つこと、合理的でない修正は行わないこと。
変更したら過学習テストによって、今まで通り学習できることを確認すること。

Ask for help

わからないことがあったらフォーラムに聞くこともできる。

以下の資料も参考になる。

https://docs.google.com/presentation/d/1yHLPvPhUs2KGI5ZWo0sU-PKU3GimAk3iTsI38Z-B5Gw/edit#slide=id.p
https://towardsdatascience.com/checklist-for-debugging-neural-networks-d8b2a9434f21
https://medium.com/@keeper6928/how-to-unit-test-machine-learning-code-57cf6fd81765
http://karpathy.github.io/2019/04/25/recipe/

Discussion