LLM Course documentation
Debugging-ul pipeline-ului de antrenament
Debugging-ul pipeline-ului de antrenament
Ați scris un script frumos pentru a antrena sau ajusta fin un model pe o sarcină dată, urmând cu atenție sfaturile din Capitolul 7. Dar când lansați comanda trainer.train(), se întâmplă ceva oribil: primiți o eroare 😱! Sau mai rău, totul pare să fie în regulă și antrenamentul rulează fără eroare, dar modelul rezultat este prost. În această secțiune, vă vom arăta ce puteți face pentru a depana acest tip de probleme.
Debugging-ul pipeline-ului de antrenament
Problema când întâlniți o eroare în trainer.train() este că ar putea veni din mai multe surse, deoarece Trainer de obicei pune împreună multe lucruri. Convertește seturile de date în dataloader-e, așa că problema ar putea fi ceva greșit în setul vostru de date, sau o problemă când încearcă să grupeze elementele seturilor de date împreună. Apoi ia un batch de date și îl alimentează la model, așa că problema ar putea fi în codul modelului. După aceea, calculează gradienții și efectuează pasul de optimizare, așa că problema ar putea fi și în optimizatorul vostru. Și chiar dacă totul merge bine pentru antrenament, ceva ar putea merge prost în timpul evaluării dacă există o problemă cu metrica voastră.
Cea mai bună modalitate de a face debugging la o eroare care apare în trainer.train() este să parcurgeți manual întregul pipeline pentru a vedea unde au mers lucrurile prost. Eroarea este apoi adesea foarte ușor de rezolvat.
Pentru a demonstra aceasta, vom folosi următorul script care (încearcă să) ajusteze fin un model DistilBERT pe setul de date MNLI:
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()Dacă încercați să îl executați, veți fi întâmpinați cu o eroare destul de criptică:
'ValueError: You have to specify either input_ids or inputs_embeds'Verificați datele voastre
Aceasta este de la sine înțeles, dar dacă datele voastre sunt corupte, Trainer nu va putea forma batch-uri, cu atât mai puțin să vă antreneze modelul. Deci primul lucru, trebuie să aruncați o privire asupra a ceea ce este în setul vostru de antrenament.
Pentru a evita nenumărate ore petrecute încercând să reparați ceva care nu este sursa bug-ului, vă recomandăm să folosiți trainer.train_dataset pentru verificările voastre și nimic altceva. Să facem asta aici:
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.'}Observați ceva greșit? Aceasta, în conjuncție cu mesajul de eroare despre input_ids lipsă, ar trebui să vă facă să realizați că acelea sunt texte, nu numere pe care modelul le poate înțelege. Aici, eroarea originală este foarte înșelătoare deoarece Trainer elimină automat coloanele care nu se potrivesc cu semnătura modelului (adică argumentele așteptate de model). Aceasta înseamnă că aici, totul în afară de etichete a fost eliminat. Nu a existat astfel nicio problemă cu crearea batch-urilor și apoi trimiterea lor la model, care la rândul său s-a plâns că nu a primit intrarea potrivită.
De ce nu au fost procesate datele? Am folosit metoda Dataset.map() pe seturi de date pentru a aplica tokenizer-ul pe fiecare eșantion. Dar dacă vă uitați cu atenție la cod, veți vedea că am făcut o greșeală când am trecut seturile de antrenament și evaluare la Trainer. În loc să folosim tokenized_datasets aici, am folosit raw_datasets 🤦. Să reparăm asta!
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()Acest cod nou va da acum o eroare diferită (progres!):
'ValueError: expected sequence of length 43 at dim 1 (got 37)'Uitându-ne la traceback, putem vedea că eroarea se întâmplă în pasul de colectare a datelor:
~/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 batchDeci, ar trebui să ne îndreptăm către asta. Înainte să facem asta, totuși, să terminăm de inspectat datele noastre, doar pentru a fi 100% siguri că sunt corecte.
Un lucru pe care ar trebui să îl faceți întotdeauna când depanați o sesiune de antrenament este să aruncați o privire asupra intrărilor decodate ale modelului vostru. Nu putem înțelege numerele pe care le alimentăm direct, așa că ar trebui să ne uităm la ceea ce reprezintă acele numere. În computer vision, de exemplu, aceasta înseamnă să ne uităm la imaginile decodate ale pixelilor pe care îi treceți, în vorbire înseamnă să ascultați eșantioanele audio decodate, și pentru exemplul nostru NLP aici înseamnă să folosim tokenizer-ul nostru pentru a decoda intrările:
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]'Deci pare corect. Ar trebui să faceți aceasta pentru toate cheile din intrări:
trainer.train_dataset[0].keys()dict_keys(['attention_mask', 'hypothesis', 'idx', 'input_ids', 'label', 'premise'])Rețineți că cheile care nu corespund intrărilor acceptate de model vor fi eliminate automat, așa că aici vom păstra doar input_ids, attention_mask, și label (care va fi redenumit labels). Pentru a verifica din nou semnătura modelului, puteți afișa clasa modelului vostru, apoi să verificați documentația sa:
type(trainer.model)transformers.models.distilbert.modeling_distilbert.DistilBertForSequenceClassification
Deci în cazul nostru, putem verifica parametrii acceptați pe această pagină. Trainer va înregistra de asemenea coloanele pe care le elimină.
Am verificat că ID-urile de intrare sunt corecte prin decodarea lor. Următorul este 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]Deoarece nu am aplicat padding în preprocesarea noastră, aceasta pare perfect naturală. Pentru a fi siguri că nu există nicio problemă cu acea mască de atenție, să verificăm că are aceeași lungime ca ID-urile noastre de intrare:
len(trainer.train_dataset[0]["attention_mask"]) == len(
trainer.train_dataset[0]["input_ids"]
)TrueAsta e bun! În sfârșit, să verificăm eticheta noastră:
trainer.train_dataset[0]["label"]1Ca ID-urile de intrare, acesta este un număr care nu are cu adevărat sens de unul singur. Așa cum am văzut înainte, maparea între întregi și numele etichetelor este stocată în atributul names al feature-ului corespunzător al setului de date:
trainer.train_dataset.features["label"].names['entailment', 'neutral', 'contradiction']Deci 1 înseamnă neutral, ceea ce înseamnă că cele două propoziții pe care le-am văzut mai sus nu sunt în contradicție, și prima nu implică a doua. Pare corect!
Nu avem ID-uri de tip token aici, deoarece DistilBERT nu le așteaptă; dacă aveți unele în modelul vostru, ar trebui să vă asigurați de asemenea că se potrivesc corespunzător unde sunt prima și a doua propoziție în intrare.
✏️ Rândul vostru! Verificați că totul pare corect cu al doilea element al setului de date de antrenament.
Facem verificarea doar pe setul de antrenament aici, dar ar trebui desigur să verificați din nou seturile de validare și test în același mod.
Acum că știm că seturile noastre de date arată bine, este timpul să verificăm următorul pas al pipeline-ului de antrenament.
De la seturi de date la dataloader-e
Următorul lucru care poate merge prost în pipeline-ul de antrenament este când Trainer încearcă să formeze batch-uri din setul de antrenament sau validare. Odată ce sunteți siguri că seturile de date ale Trainer sunt corecte, puteți încerca să formați manual un batch executând următoarele (înlocuiți train cu eval pentru dataloader-ul de validare):
for batch in trainer.get_train_dataloader():
breakAcest cod creează dataloader-ul de antrenament, apoi iterează prin el, oprindu-se la prima iterație. Dacă codul se execută fără eroare, aveți primul batch de antrenament pe care îl puteți inspecta, și dacă codul dă eroare, știți sigur că problema este în dataloader, așa cum este cazul aici:
~/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)Inspectarea ultimului frame al traceback-ului ar trebui să fie suficientă pentru a vă da un indiciu, dar să facem puțină săpătură. Majoritatea problemelor în timpul creării batch-ului apar din cauza colectării exemplelor într-un singur batch, așa că primul lucru de verificat când aveți îndoieli este ce collate_fn folosește DataLoader-ul vostru:
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]>Deci acesta este default_data_collator, dar nu este ceea ce vrem în acest caz. Vrem să facem padding la exemplele noastre la cea mai lungă propoziție din batch, ceea ce se face de colectorul DataCollatorWithPadding. Și acest colector de date ar trebui să fie folosit în mod implicit de Trainer, deci de ce nu este folosit aici?
Răspunsul este pentru că nu am trecut tokenizer la Trainer, așa că nu a putut crea DataCollatorWithPadding pe care îl vrem. În practică, nu ar trebui să ezitați niciodată să treceți explicit colectorul de date pe care doriți să îl folosiți, pentru a vă asigura că evitați acest tip de erori. Să adaptăm codul nostru pentru a face exact asta:
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()Vestea bună? Nu primim aceeași eroare ca înainte, ceea ce este cu siguranță progres. Vestea proastă? Primim în schimb o eroare CUDA infamă:
RuntimeError: CUDA error: CUBLAS_STATUS_ALLOC_FAILED when calling `cublasCreate(handle)`
Aceasta este rea deoarece erorile CUDA sunt extrem de greu de depanat în general. Vom vedea într-un minut cum să rezolvăm aceasta, dar mai întâi să terminăm analiza noastră a creării batch-ului.
Dacă sunteți siguri că colectorul vostru de date este cel potrivit, ar trebui să încercați să îl aplicați pe câteva eșantioane din setul vostru de date:
data_collator = trainer.get_train_dataloader().collate_fn
batch = data_collator([trainer.train_dataset[i] for i in range(4)])Acest cod va eșua deoarece train_dataset conține coloane string, pe care Trainer le elimină de obicei. Le puteți elimina manual, sau dacă doriți să replicați exact ceea ce face Trainer în culise, puteți apela metoda privată Trainer._remove_unused_columns() care face asta:
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)])Ar trebui apoi să puteți depana manual ce se întâmplă în interiorul colectorului de date dacă eroarea persistă.
Acum că am depanat procesul de creare a batch-ului, este timpul să trecem unul prin model!
Trecerea prin model
Ar trebui să puteți obține un batch executând următoarea comandă:
for batch in trainer.get_train_dataloader():
breakDacă rulați acest cod într-un notebook, s-ar putea să primiți o eroare CUDA similară cu cea pe care am văzut-o mai devreme, caz în care trebuie să reporniți notebook-ul și să reexecutați ultimul fragment fără linia trainer.train(). Acesta este al doilea lucru cel mai enervant despre erorile CUDA: ele strică iremediabil kernel-ul vostru. Cel mai enervant lucru despre ele este faptul că sunt greu de depanat.
De ce este așa? Are de-a face cu modul în care funcționează GPU-urile. Ele sunt extrem de eficiente la executarea multor operații în paralel, dar dezavantajul este că atunci când una dintre acele instrucțiuni rezultă într-o eroare, nu știți instantaneu. Doar când programul apelează o sincronizare a multiplelor procese de pe GPU va realiza că ceva a mers prost, așa că eroarea este de fapt ridicată într-un loc care nu are nimic de-a face cu ceea ce a creat-o. De exemplu, dacă ne uităm la traceback-ul nostru anterior, eroarea a fost ridicată în timpul backward pass-ului, dar vom vedea într-un minut că de fapt provine din ceva din forward pass.
Deci cum depanăm aceste erori? Răspunsul este simplu: nu o facem. Dacă eroarea voastră CUDA nu este o eroare out-of-memory (ceea ce înseamnă că nu există suficientă memorie în GPU-ul vostru), ar trebui să vă întoarceți întotdeauna la CPU pentru a o depana.
Pentru a face aceasta în cazul nostru, trebuie doar să punem modelul înapoi pe CPU și să îl apelăm pe batch-ul nostru — batch-ul returnat de DataLoader nu a fost încă mutat pe 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.Deci, imaginea devine mai clară. În loc să avem o eroare CUDA, acum avem un IndexError în calculul loss-ului (deci nimic de-a face cu backward pass-ul, așa cum am spus mai devreme). Mai precis, putem vedea că este target 2 care creează eroarea, așa că acesta este un moment foarte bun pentru a verifica numărul de etichete al modelului nostru:
trainer.model.config.num_labels
2Cu două etichete, doar 0 și 1 sunt permise ca target-uri, dar conform mesajului de eroare am primit un 2. Primirea unui 2 este de fapt normală: dacă ne amintim numele etichetelor pe care le-am extras mai devreme, erau trei, deci avem indicii 0, 1 și 2 în setul nostru de date. Problema este că nu am spus asta modelului nostru, care ar fi trebuit să fie creat cu trei etichete. Să reparăm asta!
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()Nu includem încă linia trainer.train(), pentru a ne lua timpul să verificăm că totul arată bine. Dacă cerem un batch și îl trecem la modelul nostru, acum funcționează fără eroare!
for batch in trainer.get_train_dataloader():
break
outputs = trainer.model.cpu()(**batch)Următorul pas este apoi să ne întoarcem la GPU și să verificăm că totul încă funcționează:
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)Dacă încă primiți o eroare, asigurați-vă că reporniți notebook-ul și executați doar ultima versiune a scriptului.
Efectuarea unui pas de optimizare
Acum că știm că putem construi batch-uri care trec efectiv prin model, suntem gata pentru următorul pas al pipeline-ului de antrenament: calcularea gradienților și efectuarea unui pas de optimizare.
Prima parte este doar o chestiune de a apela metoda backward() pe loss:
loss = outputs.loss loss.backward()
Este destul de rar să primiți o eroare în această etapă, dar dacă o faceți, asigurați-vă să vă întoarceți la CPU pentru a obține un mesaj de eroare util.
Pentru a efectua pasul de optimizare, trebuie doar să creăm optimizer și să apelăm metoda sa step():
trainer.create_optimizer() trainer.optimizer.step()
Din nou, dacă folosiți optimizatorul implicit în Trainer, nu ar trebui să primiți o eroare în această etapă, dar dacă aveți un optimizator personalizat, ar putea fi câteva probleme de depanat aici. Nu uitați să vă întoarceți la CPU dacă primiți o eroare CUDA ciudată în această etapă. Vorbind despre erorile CUDA, mai devreme am menționat un caz special. Să aruncăm o privire asupra acestuia acum.
Gestionarea erorilor CUDA out-of-memory
Ori de câte ori primiți un mesaj de eroare care începe cu RuntimeError: CUDA out of memory, aceasta indică faptul că nu aveți memorie GPU. Aceasta nu este legată direct de codul vostru și se poate întâmpla cu un script care rulează perfect. Această eroare înseamnă că ați încercat să puneți prea multe lucruri în memoria internă a GPU-ului vostru, și aceasta a rezultat într-o eroare. Ca și cu alte erori CUDA, va trebui să reporniți kernel-ul pentru a fi într-un punct unde puteți rula din nou antrenamentul.
Pentru a rezolva această problemă, trebuie doar să folosiți mai puțin spațiu GPU — ceva care este adesea mai ușor de spus decât de făcut. În primul rând, asigurați-vă că nu aveți două modele pe GPU în același timp (dacă nu este necesar pentru problema voastră, desigur). Apoi, probabil ar trebui să reduceți dimensiunea batch-ului, deoarece aceasta afectează direct dimensiunile tuturor ieșirilor intermediare ale modelului și gradienții lor. Dacă problema persistă, considerați folosirea unei versiuni mai mici a modelului vostru.
În următoarea parte a cursului, vom examina tehnici mai avansate care vă pot ajuta să reduceți amprenta de memorie și să vă permită să ajustați fin cele mai mari modele.
Evaluarea modelului
Acum că am rezolvat toate problemele cu codul nostru, totul este perfect și antrenamentul ar trebui să ruleze fără probleme, nu? Nu atât de repede! Dacă rulați comanda trainer.train(), totul va arăta bine la început, dar după un timp veți primi următoarele:
# Aceasta va dura mult timp și va da eroare, așa că nu ar trebui să rulați această celulă
trainer.train()TypeError: only size-1 arrays can be converted to Python scalarsVeți realiza că această eroare apare în timpul fazei de evaluare, deci acesta este ultimul lucru pe care va trebui să îl depanăm.
Puteți rula bucla de evaluare a Trainer independent de antrenament astfel:
trainer.evaluate()
TypeError: only size-1 arrays can be converted to Python scalars💡 Ar trebui să vă asigurați întotdeauna că puteți rula
trainer.evaluate()înainte de a lansatrainer.train(), pentru a evita risipa multor resurse de calcul înainte de a întâlni o eroare.
Înainte de a încerca să depanați o problemă în bucla de evaluare, ar trebui să vă asigurați mai întâi că v-ați uitat la date, puteți forma un batch corespunzător și puteți rula modelul pe el. Am completat toți acești pași, așa că următorul cod poate fi executat fără eroare:
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)Eroarea vine mai târziu, la sfârșitul fazei de evaluare, și dacă ne uităm la traceback vedem aceasta:
~/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()Aceasta ne spune că eroarea provine din modulul datasets/metric.py — deci aceasta este o problemă cu funcția noastră compute_metrics(). Aceasta ia un tuplu cu logit-urile și etichetele ca array-uri NumPy, așa că să încercăm să îi alimentăm asta:
predictions = outputs.logits.cpu().numpy()
labels = batch["labels"].cpu().numpy()
compute_metrics((predictions, labels))TypeError: only size-1 arrays can be converted to Python scalarsPrimim aceeași eroare, deci problema se află cu siguranță în acea funcție. Dacă ne uităm înapoi la codul ei, vedem că doar transmite predictions și labels la metric.compute(). Deci există o problemă cu acea metodă? Nu chiar. Să aruncăm o privire rapidă asupra formelor:
predictions.shape, labels.shape
((8, 3), (8,))Predicțiile noastre sunt încă logit-uri, nu predicțiile reale, de aceea metrica returnează această eroare (oarecum obscură). Reparația este destul de ușoară; trebuie doar să adăugăm un argmax în funcția 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}Acum eroarea noastră este reparată! Aceasta a fost ultima, așa că scriptul nostru va antrena acum un model corespunzător.
Pentru referință, iată scriptul complet reparat:
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()În acest caz, nu mai sunt probleme, și scriptul nostru va ajusta fin un model care ar trebui să dea rezultate rezonabile. Dar ce putem face când antrenamentul continuă fără nicio eroare, și modelul antrenat nu performează deloc bine? Aceasta este partea cea mai dificilă a machine learning-ului, și vă vom arăta câteva tehnici care pot ajuta.
💡 Dacă folosiți o buclă de antrenament manuală, aceiași pași se aplică pentru a vă depana pipeline-ul de antrenament, dar este mai ușor să îi separați. Asigurați-vă că nu ați uitat
model.eval()saumodel.train()la locurile potrivite, sauzero_grad()la fiecare pas, totuși!
Depanarea erorilor silențioase în timpul antrenamentului
Ce putem face pentru a depana un antrenament care se completează fără eroare dar nu obține rezultate bune? Vă vom da câteva indicii aici, dar fiți conștienți că acest tip de depanare este partea cea mai grea a machine learning-ului, și nu există un răspuns magic.
Verificați datele voastre (din nou!)
Modelul vostru va învăța ceva doar dacă este de fapt posibil să învețe ceva din datele voastre. Dacă există o eroare care corupe datele sau etichetele sunt atribuite aleatoriu, este foarte probabil că nu veți obține niciun antrenament de model pe setul vostru de date. Deci începeți întotdeauna prin a verifica din nou intrările și etichetele voastre decodate, și întrebați-vă următoarele întrebări:
- Sunt datele decodate înțelegibile?
- Sunteți de acord cu etichetele?
- Există o etichetă care este mai comună decât altele?
- Care ar trebui să fie loss-ul/metrica dacă modelul ar prezice un răspuns aleatoriu/întotdeauna același răspuns?
⚠️ Dacă faceți antrenament distribuit, afișați eșantioane din setul vostru de date în fiecare proces și verificați de trei ori că obțineți același lucru. O eroare comună este să aveți o sursă de aleatoriu în crearea datelor care face ca fiecare proces să aibă o versiune diferită a setului de date.
După ce vă uitați la datele voastre, treceți prin câteva dintre predicțiile modelului și decodați-le și pe ele. Dacă modelul prezice întotdeauna același lucru, ar putea fi pentru că setul vostru de date este părtinitor către o categorie (pentru problemele de clasificare); tehnici precum supraesantionarea claselor rare ar putea ajuta.
Dacă loss-ul/metrica pe care o obțineți pe modelul vostru inițial este foarte diferită de loss-ul/metrica pe care ați aștepta-o pentru predicții aleatorii, verificați din nou modul în care loss-ul sau metrica voastră este calculată, deoarece probabil există o eroare acolo. Dacă folosiți mai multe loss-uri pe care le adăugați la sfârșit, asigurați-vă că sunt de aceeași scară.
Când sunteți siguri că datele voastre sunt perfecte, puteți vedea dacă modelul este capabil să se antreneze pe ele cu un test simplu.
Supraajustați modelul vostru pe un batch
Supraajustarea este de obicei ceva pe care încercăm să îl evităm când antrenăm, deoarece înseamnă că modelul nu învață să recunoască caracteristicile generale pe care vrem să le recunoască, ci în schimb doar memorează eșantioanele de antrenament. Cu toate acestea, încercarea de a vă antrena modelul pe un batch din nou și din nou este un test bun pentru a verifica dacă problema așa cum ați formulat-o poate fi rezolvată de modelul pe care încercați să îl antrenați. De asemenea, vă va ajuta să vedeți dacă rata voastră de învățare inițială este prea mare.
Făcând aceasta odată ce ați definit Trainer este foarte ușor; doar luați un batch de date de antrenament, apoi rulați o buclă mică de antrenament manual folosind doar acel batch pentru ceva ca 20 de pași:
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()💡 Dacă datele voastre de antrenament sunt dezechilibrate, asigurați-vă să construiți un batch de date de antrenament care conține toate etichetele.
Modelul rezultat ar trebui să aibă rezultate aproape perfecte pe același batch. Să calculăm metrica pe predicțiile rezultate:
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% acuratețe, acum acesta este un exemplu frumos de supraajustare (ceea ce înseamnă că dacă încercați modelul vostru pe orice altă propoziție, foarte probabil vă va da un răspuns greșit)!
Dacă nu reușiți să faceți modelul vostru să obțină rezultate perfecte ca aceasta, înseamnă că există ceva greșit cu modul în care ați formulat problema sau datele voastre, așa că ar trebui să reparați asta. Doar când reușiți să treceți testul de supraajustare puteți fi siguri că modelul vostru poate învăța de fapt ceva.
⚠️ Va trebui să vă recreați modelul și
Trainerdupă acest test, deoarece modelul obținut probabil nu va putea să se recupereze și să învețe ceva util pe setul vostru complet de date.
Nu ajustați nimic până nu aveți o primă linie de bază
Ajustarea hiperparametrilor este întotdeauna subliniată ca fiind partea cea mai grea a machine learning-ului, dar este doar ultimul pas pentru a vă ajuta să câștigați puțin la metrică. Majoritatea timpului, hiperparametrii impliciți ai Trainer vor funcționa bine pentru a vă da rezultate bune, așa că nu vă lansați într-o căutare de hiperparametri consumatoare de timp și costisitoare până nu aveți ceva care bate linia de bază pe care o aveți pe setul vostru de date.
Odată ce aveți un model suficient de bun, puteți începe să ajustați puțin. Nu încercați să lansați o mie de rulări cu hiperparametri diferiți, ci comparați câteva rulări cu valori diferite pentru un hiperparametru pentru a avea o idee despre care are cel mai mare impact.
Dacă ajustați modelul în sine, păstrați-l simplu și nu încercați nimic pe care nu îl puteți justifica în mod rezonabil. Asigurați-vă întotdeauna că vă întoarceți la testul de supraajustare pentru a verifica că schimbarea voastră nu a avut consecințe neintenționate.
Cereți ajutor
Sperăm că veți fi găsit câteva sfaturi în această secțiune care v-au ajutat să vă rezolvați problema, dar dacă nu este cazul, amintiți-vă că puteți întotdeauna să cereți comunității pe forumuri.
Iată câteva resurse suplimentare care se pot dovedi utile:
- “Reproducibility as a vehicle for engineering best practices” de Joel Grus
- “Checklist for debugging neural networks” de Cecelia Shao
- “How to unit test machine learning code” de Chase Roberts
- “A Recipe for Training Neural Networks” de Andrej Karpathy
Desigur, nu fiecare problemă pe care o întâlniți când antrenați rețele neuronale este vina voastră! Dacă întâlniți ceva în biblioteca 🤗 Transformers sau 🤗 Datasets care nu pare corect, s-ar putea să fi întâlnit o eroare. Ar trebui cu siguranță să ne spuneți totul despre aceasta, și în secțiunea următoare vă vom explica exact cum să faceți asta.
Update on GitHub