Hugging Faceモデル訓練時のエラー対応tips
概要
以下のページの纏め。
ファインチューニング時に遭遇する様々な問題に対処する方法が書かれている。
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
そのモデルのページでシグニチャを確認する。
ちなみに、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
わからないことがあったらフォーラムに聞くこともできる。
以下の資料も参考になる。
Discussion