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의 조언을 충실히 따라 주어진 작업에서 모델을 학습하거나 파인튜닝 하는 아름다운 스크립트를 작성했습니다. 하지만 trainer.train() 명령을 실행하면 끔찍한 일이 발생합니다. 에러가 발생합니다 😱! 또는 더 나쁜 것은 모든 것이 정상인 것처럼 보이고 학습이 에러 없이 실행되지만 결과 모델은 엉망입니다. 이 섹션에서는 이러한 종류의 문제를 디버그하기 위해 수행할 수 있는 것들을 보여줍니다.

학습 파이프라인 디버깅

trainer.train()에서 에러가 발생했을 때의 문제는 Trainer가 일반적으로 많은 것을 결합하기 때문에 여러 소스에서 올 수 있다는 것입니다. 데이터 세트를 데이터 로더로 변환하므로 데이터 세트에 문제가 있거나 데이터 세트의 요소를 함께 일괄 처리하려고 할 때 문제가 발생할 수 있습니다. 그런 다음 데이터 배치를 가져와 모델에 공급하므로 문제가 모델 코드에 있을 수 있습니다. 그 다음 기울기를 계산하고 최적화 단계를 수행하므로 문제가 옵티마이저에도 있을 수 있습니다. 모든 것이 학습에 적합하더라도 평가 함수에 문제가 있으면 평가 중에 문제가 발생할 수 있습니다.

trainer.train()에서 발생하는 오류를 디버그하는 가장 좋은 방법은 이 전체 파이프라인을 직접 살펴보고 문제가 발생한 부분을 확인하는 것입니다. 그러면 에러를 해결하기가 매우 쉬운 경우가 많습니다.

이를 시연하기 위해 MNLI 데이터 세트에서 DistilBERT 모델을 파인튜닝(시도하는)하는 다음 스크립트를 사용합니다.:

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

traceback을 보면 데이터 정렬 단계에서 에러가 발생하는 것을 볼 수 있습니다.:

~/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_masklabel(이름이 labels로 변경됨)만 유지합니다. 모델 정보를 확실하게 확인하려면 모델의 클래스를 출력해본 다음 문서를 확인하세요.:

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]

전처리에서 패딩을 적용하지 않았기 때문에 위 값은 완벽하게 자연스러워 보입니다. attention mask에 문제가 없는지 확인하기 위해 input IDs 와 길이가 같은지 확인합니다.:

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

좋습니다! 마지막으로 레이블을 확인해 보겠습니다.:

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

input IDs 와 마찬가지로 이 숫자만으로는 의미가 없습니다. 이전에 보았듯이 정수와 레이블 이름 사이의 맵은 데이터세트의 해당 featurenames 속성 내부에 저장됩니다.:

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

따라서 1중립을 의미합니다. 즉, 위에서 본 두 문장이 모순되지 않고 첫 번째 문장이 두 번째 문장을 의미하지 않습니다. 맞는 것 같습니다!

여기에는 token type IDs 가 없습니다. DistilBERT는 사용하지 않기 때문입니다. token type IDs를 사용하는 모델의 경우 입력에서 첫 번째 및 두 번째 문장이 있는 위치와 올바르게 일치하는지 확인해야 합니다.

✏️ 여러분 차례입니다! 학습 데이터 세트의 두 번째 원소가 정상적인지 확인해보세요.

여기에선 학습 세트에 대해서만 확인하지만, 동일한 방식으로 검증 및 평가 세트를 다시 확인해야 합니다.

데이터 세트가 문제 없으므로 이제 학습 파이프라인의 다음 단계를 확인할 차례입니다.

데이터세트에서 데이터로더까지

학습 파이프라인에서 다음으로 잘못될 수 있는 사항은 Trainer가 학습 또는 검증 세트에서 배치를 형성하려고 할 때입니다. Trainer의 데이터 세트가 정확하다고 확신하면 다음을 실행하여 직접 배치를 형성할 수 있습니다(검증 데이터 로더의 경우 traineval로 대체).:

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)

Traceback의 마지막 프레임을 조사하면 단서를 제공하기 충분할테지만 조금 더 파헤쳐 보겠습니다. 배치 생성 중 대부분의 문제는 예제를 단일 배치로 조합하기 때문에 발생하므로 의심스러운 경우 가장 먼저 확인해야 할 것은 DataLoader가 사용하는 collate_fn입니다.:

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’ collator에 의해 수행되는 배치에서 가장 긴 문장으로 패딩을 하고 싶습니다. 그리고 이 데이터 콜레이터는 기본적으로 Trainer에 의해 사용된다고 하는데 여기에서는 사용하지 않는 이유는 무엇일까요?

그 이유는 우리가 Trainertokenizer를 전달하지 않았기 때문에 우리가 원하는 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_datasetTrainer가 일반적으로 제거하는 문자열 열이 포함되어 있기 때문에 이 코드는 실패합니다. 이 부분을 수동으로 제거하거나 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 작동 방식과 관련이 있습니다. 많은 작업을 병렬로 실행하는 데 매우 효율적이지만, 이러한 명령 중 하나의 에러가 발생했을 때 이를 즉시 알 수 없다는 단점이 있습니다. 프로그램이 GPU에서 여러 프로세스의 동기화를 호출할 때만 문제가 발생했음을 깨닫게 되므로 실제로 에러를 만든 곳과 관련이 없는 위치에서 오류가 발생합니다. 예를 들어, 이전 Traceback을 보면 역방향 패스 중에 오류가 발생했지만 실제로는 순방향 중 어딘가에서 비롯된 에러임을 곧 알 수 있습니다.

그렇다면 이러한 에러를 어떻게 디버깅할까요? 답은 간단합니다. 하지 않습니다. 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를 얻는 것은 실제로 일반적입니다. 이전에 추출한 레이블 이름을 기억해보면 3개가 있었으므로 인덱스는 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)

다음 단계는 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)

여전히 에러가 발생하면 노트북을 다시 시작하고 스크립트의 마지막 버전만 실행해야 합니다.

한번의 최적화 단계 수행

이제 실제로 모델을 통과하는 배치를 빌드할 수 있다는 것을 알았으므로 학습 파이프라인의 다음 단계인 그래디언트 계산 및 최적화 단계를 수행할 준비가 되었습니다.:

첫 번째 부분은 로스에 대해 backward() 메서드를 호출하는 것입니다.:

loss = outputs.loss
loss.backward()

이 단계에서 에러가 발생하는 것은 매우 드물지만 에러가 발생하면 CPU로 돌아가 유용한 에러 메시지를 받으세요.

최적화 단계를 수행하려면 optimizer를 만들고 step() 메서드를 호출하기만 하면 됩니다.:

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

다시 말하지만, Trainer에서 기본 옵티마이저를 사용하는 경우 이 단계에서 오류가 발생하지 않아야 하지만 사용자 지정 옵티마이저가 있는 경우 여기에서 디버깅에 몇 가지 문제가 있을 수 있습니다. 이 단계에서 이상한 CUDA 오류가 발생하면 CPU로 돌아가는 것을 잊지 마세요. CUDA 오류에 대해 말하자면, 앞서 우리는 특별한 경우를 언급했습니다. 그 부분을 지금부터 살펴보겠습니다.

CUDA 메모리 부족 오류 다루기

RuntimeError: CUDA out of memory로 시작하는 에러 메시지는 GPU 메모리가 부족하다는 의미입니다. 이 부분은 코드에 직접 연결되지 않으며 완벽하게 실행되는 스크립트에서 발생할 수 있습니다. 이 에러는 GPU의 내부 메모리에 너무 많은 것을 넣으려고 해서 에러가 발생했음을 의미합니다. 다른 CUDA 오류와 마찬가지로 학습을 다시 실행할 수 있는 위치에서 커널을 다시 시작해야 합니다.

이 문제를 해결하려면 GPU 공간을 적게 사용하면 됩니다. 먼저 GPU에 동시에 두 개의 모델이 있지 않은지 확인합니다(물론 문제 해결에 필요한 경우 제외). 그런 다음 배치 크기를 줄여야 합니다. 이는 모델의 모든 중간 결과값 크기와 기울기에 직접적인 영향을 미치기 때문입니다. 문제가 지속되면 더 작은 모델 버전을 사용하는 것이 좋습니다.

코스의 다음 부분에서는 메모리 사용량을 줄이고 가장 큰 모델을 파인 튜닝할 수 있는 고급 기술을 살펴보겠습니다.

모델 평가하기

이제 코드의 모든 문제를 해결했으므로 모든 것이 완벽하고 학습이 원활하게 실행되어야 합니다. 그렇죠? 그렇게 빠르진 않습니다! trainer.train() 명령을 실행하면 처음에는 모든 것이 좋아 보이지만 잠시 후 다음의 출력을 보게 됩니다.: Now that we’ve solved all the issues with our code, everything is perfect and the training should run smoothly, right? Not so fast! If you run the trainer.train() command, everything will look good at first, but after a while you will get the following:

# 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의 평가 루프를 실행할 수 있습니다.:

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)

에러는 나중에 평가 단계가 끝날 때 발생하며 Traceback을 보면 다음과 같이 표시됩니다.:

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

동일한 에러가 발생하므로 문제는 분명히 해당 기능에 있습니다. 코드를 다시 보면 predictionslabelsmetric.compute()로 전달하고 있음을 알 수 있습니다. 그럼 그 방법에 문제가 있는 걸까요? 설마… 형태을 간단히 살펴보겠습니다.:

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

우리의 예측은 실제 예측값이 아니라 여전히 로짓입니다. 이것이 메트릭이 이 (다소 모호한) 에러를 반환하는 이유입니다. 수정은 매우 쉽습니다. compute_metrics() 함수에 argmax를 추가하기만 하면 됩니다.:

import numpy as np


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


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()를 잊지 않았는지 확인하세요!

학습 중 조용한 에러 디버깅

에러 없이 완료되지만 좋은 결과를 얻지 못하는 학습을 디버그하려면 어떻게 해야 할까요? 여기에서 몇 가지 지침을 제공하겠지만 이러한 종류의 디버깅은 기계 학습에서 가장 어려운 부분이며 마법 같은 답은 없다는 점을 기억하세요.

데이터 확인(다시!)

모델은 데이터에서 실제로 뭔가 학습할 수 있는 경우에만 학습합니다. 데이터를 손상시키는 버그가 있거나 레이블이 무작위로 지정된 경우 데이터 세트에 대한 학습을 제대로 진행하지 못할 가능성이 매우 높습니다. 따라서 항상 디코딩된 입력과 레이블을 다시 확인하는 것으로 시작하고 다음의 질문을 스스로에게 물어보세요.:

  • 디코딩된 데이터가 이해할만한지?
  • 레이블에 납득할 수 있는지?
  • 다른 레이블보다 정답에 가까운 레이블이 있는지?
  • 모델이 무작위 답, 언제나 같은 답을 예측할 경우 어떤 손실 함수와 매트릭을 정해야할지?

분산 학습을 수행하는 경우 각 프로세스에서 데이터 세트의 샘플을 출력하고 동일한 결과를 얻었는지 세 번 확인하세요. 한 가지 일반적인 버그는 데이터 생성 시 각 프로세스가 서로 다른 버전의 데이터 세트를 갖도록 만드는 임의성의 원인이 있다는 것입니다.

데이터를 살펴본 후 모델의 몇 가지 예측을 살펴보고 디코딩합니다. 모델이 항상 동일한 것을 예측하는 경우 데이터 세트가 하나의 범주(분류 문제의 경우)로 편향되어 있기 때문일 수 있습니다. 희귀 클래스를 오버샘플링하는 것과 같은 기술이 도움이 될 수 있습니다.

초기 모델에서 얻은 손실값/메트릭값이 무작위 예측에 대해 예상한 손실값/메트릭값과 매우 다른 경우 버그가 있을 수 있으므로 손실값 또는 메트릭값이 계산되는 방식을 다시 확인하세요. 마지막에 추가적인 여러 손실함수를 사용하는 경우 동일한 크기인지 확인하세요.

데이터가 완벽하다고 확신하는 경우, 모델이 학습을 진행할 수 있는지 한번 간단하게 테스트 해보세요.

한번의 배치에 모델 과적합 해보기

과적합은 일반적으로 훈련할 때 피하려고 합니다. 과적합은 모델이 우리가 원하는 일반적인 기능을 인식하는 방법을 배우는 것이 아니라 훈련 샘플을 암기하는 것임을 의미하기 때문입니다. 그러나 하나의 배치에서 반복해서 모델을 훈련시키려는 시도는 훈련하려는 모델에서 설정한 문제를 해결할 수 있는지 확인하는 좋은 테스트입니다. 또한 초기 학습률이 너무 높은지 확인하는 데 도움이 됩니다.

Trainer를 정의한 후에는 이 작업을 수행하는 것이 정말 쉽습니다. 학습 데이터 배치를 사용하여 20단계 정도로 작은 수동 학습 루프를 실행해보세요.:

Doing this once you have defined your Trainer is really easy; just grab a batch of training data, then run a small manual training loop only using that batch for something like 20 steps:

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

💡 학습 데이터가 불균형한 경우 모든 레이블을 포함하는 학습 데이터 배치를 빌드해야 합니다.

결과 모델은 동일한 batch에서 완벽에 가까운 결과를 가져야 합니다. 결과 예측에 대한 메트릭을 계산해 보겠습니다.:

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 라이브러리에서 이상한 것을 본다면 버그가 발생했을 수 있습니다. 버그를 봤을 경우 우리에게 상세하게 말해야 하며, 다음 섹션에서 그 방법을 정확히 설명할 것입니다.