LLM 課程文件

除錯訓練管道

Hugging Face's logo
加入 Hugging Face 社群

並獲得增強的文件體驗

開始使用

除錯訓練管道

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` 時犯了一個錯誤。我們在這裡使用了 `raw_datasets` 而不是 `tokenized_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` 也會記錄它正在丟棄的列。

我們已透過解碼輸入 ID 檢查了它們的正確性。接下來是 `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]

由於我們沒有在預處理中應用填充,這看起來非常自然。為了確保注意掩碼沒有問題,我們來檢查它的長度是否與我們的輸入 ID 相同

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

那很好!最後,我們來檢查一下我們的標籤

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

就像輸入 ID 一樣,這是一個本身沒有實際意義的數字。正如我們之前看到的,整數和標籤名稱之間的對映儲存在資料集相應*特徵*的 `names` 屬性中

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

所以 `1` 意味著 `中立`,這意味著我們上面看到的兩句話沒有矛盾,第一句話不暗示第二句話。這看起來是正確的!

這裡沒有 token type IDs,因為 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)

檢查回溯的最後一幀應該足以給您一個線索,但讓我們再深入一點。批處理建立期間的大多數問題都源於將示例整理成一個批次,因此在不確定時首先要檢查的是您的 `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` 預設使用,那麼為什麼這裡沒有使用它呢?

答案是因為我們沒有將 `tokenizer` 傳遞給 `Trainer`,所以它無法建立我們想要的 `DataCollatorWithPadding`。實際上,您應該毫不猶豫地明確傳遞您想要使用的資料整理器,以確保您避免此類錯誤。讓我們修改我們的程式碼來做到這一點

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

如果錯誤仍然存在,您應該能夠手動除錯資料整理器內部發生的情況。

既然我們已經除錯了批處理建立過程,是時候將一個批處理透過模型了!

透過模型

您應該能夠透過執行以下命令來獲取一批次

for batch in trainer.get_train_dataloader():
    break

如果您在筆記本中執行此程式碼,您可能會遇到類似於我們之前看到的 CUDA 錯誤,在這種情況下,您需要重新啟動筆記本並重新執行最後的程式碼片段,但不要包含 `trainer.train()` 行。這是 CUDA 錯誤第二煩人的地方:它們會不可挽回地破壞您的核心。最煩人的是它們難以除錯。

為什麼會這樣?這與 GPU 的工作方式有關。它們在並行執行大量操作方面效率極高,但缺點是當其中一個指令導致錯誤時,您不會立即知道。只有當程式呼叫 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)

下一步是回到 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()` 命令,一開始一切看起來都很好,但過了一會兒您會得到以下結果

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

錯誤發生在稍後,在評估階段結束時,如果我們查看回溯,我們會看到這個

~/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()` 函式的問題。它接受一個包含 logits 和標籤的元組作為 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,))

我們的預測仍然是 logits,而不是實際預測,這就是為什麼度量返回這個(有點模糊的)錯誤。修復方法很簡單;我們只需要在 `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 步

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 庫中遇到看起來不對勁的地方,您可能遇到了錯誤。您絕對應該告訴我們所有相關資訊,在下一節中我們將確切解釋如何做到這一點。

< > 在 GitHub 上更新

© . This site is unofficial and not affiliated with Hugging Face, Inc.