LLM Course documentation
O instruire completă
O instruire completă
Acum vom vedea cum să obținem aceleași rezultate ca în secțiunea anterioară, dar fără să folosim clasa Trainer
. Din nou, presupunem că ați parcurs deja procesarea datelor din secțiunea 2. Iată un scurt rezumat al tot ceea ce veți avea nevoie:
from datasets import load_dataset
from transformers import AutoTokenizer, DataCollatorWithPadding
raw_datasets = load_dataset("glue", "mrpc")
checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
def tokenize_function(example):
return tokenizer(example["sentence1"], example["sentence2"], truncation=True)
tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
Pregătirea pentru antrenament
Înainte de a scrie efectiv bucla de antrenament, va trebui să definim câteva obiecte. Primele sunt încărcătoarele de date (dataloaders) pe care le vom folosi pentru a itera pe batch-uri. Dar, înainte de a putea defini acele dataloaders, trebuie să aplicăm un pic de postprocesare dataset-urilor noastre tokenized_datasets
, pentru a ne ocupa de câteva lucruri pe care Trainer
le făcea automat pentru noi. Mai exact, trebuie să:
- Eliminăm coloanele care corespund valorilor pe care modelul nu le așteaptă (cum ar fi coloanele
sentence1
șisentence2
). - Redenumim coloana
label
înlabels
(pentru că modelul se așteaptă ca argumentul să se numeascălabels
). - Setăm formatul dataset-urilor astfel încât să returneze tensori PyTorch în loc de liste.
tokenized_datasets
are câte o metodă pentru fiecare dintre acești pași:
tokenized_datasets = tokenized_datasets.remove_columns(["sentence1", "sentence2", "idx"])
tokenized_datasets = tokenized_datasets.rename_column("label", "labels")
tokenized_datasets.set_format("torch")
tokenized_datasets["train"].column_names
Putem apoi să verificăm că rezultatul are doar coloanele pe care modelul le va accepta:
["attention_mask", "input_ids", "labels", "token_type_ids"]
Acum, după ce am terminat acest pas, putem defini foarte ușor dataloader-urile noastre:
from torch.utils.data import DataLoader
train_dataloader = DataLoader(
tokenized_datasets["train"], shuffle=True, batch_size=8, collate_fn=data_collator
)
eval_dataloader = DataLoader(
tokenized_datasets["validation"], batch_size=8, collate_fn=data_collator
)
Pentru a verifica rapid că nu există nicio eroare în procesarea datelor, putem inspecta un batch astfel:
for batch in train_dataloader:
break
{k: v.shape for k, v in batch.items()}
{'attention_mask': torch.Size([8, 65]),
'input_ids': torch.Size([8, 65]),
'labels': torch.Size([8]),
'token_type_ids': torch.Size([8, 65])}
Observați că formele reale ar putea fi ușor diferite pentru voi, pentru că am setat shuffle=True
în dataloader-ul nostru de antrenament și pentru că împachetăm (padding) la lungimea maximă în interiorul batch-ului.
Acum că am terminat complet procesarea datelor (un obiectiv satisfăcător, dar uneori greu de atins pentru orice practician ML), să trecem la model. Îl instanțiem exact ca în secțiunea anterioară:
from transformers import AutoModelForSequenceClassification
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
Pentru a ne asigura că totul va decurge fără probleme în timpul antrenamentului, trecem batch-ul nostru prin model:
outputs = model(**batch)
print(outputs.loss, outputs.logits.shape)
tensor(0.5441, grad_fn=<NllLossBackward>) torch.Size([8, 2])
Toate modelele 🤗 Transformers vor returna pierderea (loss) când labels
sunt furnizate, și, de asemenea, obținem logits (două pentru fiecare intrare în batch, deci un tensor de mărimea 8 x 2).
Suntem aproape gata să scriem bucla de antrenament! Ne mai lipsesc două lucruri: un optimizer și un scheduler pentru rata de învățare. Pentru că încercăm să reproducem ceea ce făcea Trainer
, vom folosi aceleași valori implicite. Optimizer-ul folosit de Trainer
este AdamW
, care este același cu Adam, dar cu o abordare particulară pentru regularizarea weight decay (vedeți lucrarea “Decoupled Weight Decay Regularization” de Ilya Loshchilov și Frank Hutter):
from transformers import AdamW
optimizer = AdamW(model.parameters(), lr=5e-5)
În final, scheduler-ul pentru rata de învățare folosit implicit este doar o descreștere liniară de la valoarea maximă (5e-5) la 0. Pentru a-l defini corect, trebuie să știm numărul de pași de antrenament pe care îi vom face, care este numărul de epoci dorit înmulțit cu numărul de batch-uri de antrenament (care este lungimea dataloader-ului nostru de antrenament). Trainer
folosește trei epoci implicit, așa că vom urma acest exemplu:
from transformers import get_scheduler
num_epochs = 3
num_training_steps = num_epochs * len(train_dataloader)
lr_scheduler = get_scheduler(
"linear",
optimizer=optimizer,
num_warmup_steps=0,
num_training_steps=num_training_steps,
)
print(num_training_steps)
1377
Bucla de antrenament
Încă un lucru: vom dori să folosim GPU-ul dacă avem acces la unul (pe un CPU, antrenamentul poate dura câteva ore în loc de câteva minute). Pentru asta, definim un device
pe care vom pune modelul și batch-urile noastre:
import torch
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
model.to(device)
device
device(type='cuda')
Acum suntem gata de antrenament! Pentru a ne face o idee despre momentul în care se va termina antrenamentul, adăugăm o bară de progres peste numărul de pași de antrenament, folosind biblioteca tqdm
:
from tqdm.auto import tqdm
progress_bar = tqdm(range(num_training_steps))
model.train()
for epoch in range(num_epochs):
for batch in train_dataloader:
batch = {k: v.to(device) for k, v in batch.items()}
outputs = model(**batch)
loss = outputs.loss
loss.backward()
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
progress_bar.update(1)
Observați că partea principală a buclei de antrenament arată foarte asemănător cu cea din introducere. Nu am cerut niciun raport, așa că această buclă de antrenament nu ne va spune nimic despre performanța modelului. Pentru a avea feedback, trebuie să adăugăm o buclă de evaluare.
Bucla de evaluare
Ca și înainte, vom folosi o metrică oferită de biblioteca 🤗 Evaluate. Am văzut deja metoda metric.compute()
, dar metricle pot de fapt să acumuleze batch-uri pentru noi în timp ce parcurgem bucla de predicție, cu metoda add_batch()
. Odată ce am acumulat toate batch-urile, putem obține rezultatul final cu metric.compute()
. Iată cum să implementăm toate acestea într-o buclă de evaluare:
import evaluate
metric = evaluate.load("glue", "mrpc")
model.eval()
for batch in eval_dataloader:
batch = {k: v.to(device) for k, v in batch.items()}
with torch.no_grad():
outputs = model(**batch)
logits = outputs.logits
predictions = torch.argmax(logits, dim=-1)
metric.add_batch(predictions=predictions, references=batch["labels"])
metric.compute()
{'accuracy': 0.8431372549019608, 'f1': 0.8907849829351535}
Din nou, rezultatele voastre vor fi ușor diferite din cauza aleatorietății în inițializarea layer-ului final (model head) și a amestecării datelor, dar ar trebui să fie în aceeași zonă valorică.
✏️ Încercați! Modificați bucla de antrenament anterioară pentru a vă rafina modelul pe dataset-ul SST-2.
Îmbunătățiți circuitul de antrenament cu 🤗 Accelerate
Bucla de antrenament pe care am definit-o anterior funcționează bine pe un singur CPU sau GPU. Dar, folosind biblioteca 🤗 Accelerate, cu doar câteva ajustări putem activa antrenarea distribuită pe mai multe GPU-uri sau TPU-uri. Pornind de la crearea dataloader-urilor de antrenament și validare, iată cum arată bucla noastră manuală de antrenament:
from transformers import AdamW, AutoModelForSequenceClassification, get_scheduler
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
optimizer = AdamW(model.parameters(), lr=3e-5)
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
model.to(device)
num_epochs = 3
num_training_steps = num_epochs * len(train_dataloader)
lr_scheduler = get_scheduler(
"linear",
optimizer=optimizer,
num_warmup_steps=0,
num_training_steps=num_training_steps,
)
progress_bar = tqdm(range(num_training_steps))
model.train()
for epoch in range(num_epochs):
for batch in train_dataloader:
batch = {k: v.to(device) for k, v in batch.items()}
outputs = model(**batch)
loss = outputs.loss
loss.backward()
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
progress_bar.update(1)
Iar aici sunt modificările:
+ from accelerate import Accelerator
from transformers import AdamW, AutoModelForSequenceClassification, get_scheduler
+ accelerator = Accelerator()
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
optimizer = AdamW(model.parameters(), lr=3e-5)
- device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
- model.to(device)
+ train_dataloader, eval_dataloader, model, optimizer = accelerator.prepare(
+ train_dataloader, eval_dataloader, model, optimizer
+ )
num_epochs = 3
num_training_steps = num_epochs * len(train_dataloader)
lr_scheduler = get_scheduler(
"linear",
optimizer=optimizer,
num_warmup_steps=0,
num_training_steps=num_training_steps
)
progress_bar = tqdm(range(num_training_steps))
model.train()
for epoch in range(num_epochs):
for batch in train_dataloader:
- batch = {k: v.to(device) for k, v in batch.items()}
outputs = model(**batch)
loss = outputs.loss
- loss.backward()
+ accelerator.backward(loss)
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
progress_bar.update(1)
Prima linie de adăugat este linia de import. A doua linie instanțiază un obiect Accelerator
care va examina mediul și va inițializa setarea distribuită corespunzătoare. 🤗 Accelerate se ocupă de plasarea pe device pentru voi, așa că puteți elimina liniile care pun modelul pe device (sau, dacă preferați, le puteți schimba să folosească accelerator.device
în loc de device
).
Apoi, partea principală a muncii este făcută în linia care trimite dataloaders, modelul și optimizer-ul la accelerator.prepare()
. Aceasta va împacheta acele obiecte în containerul potrivit pentru a vă asigura că antrenarea distribuită funcționează corespunzător. Restul modificărilor constau în eliminarea liniei care mută batch-ul pe device
(din nou, dacă doriți să o păstrați puteți doar să o schimbați să folosească accelerator.device
) și înlocuirea loss.backward()
cu accelerator.backward(loss)
.
Dacă vreți să copiați și să lipiți pentru a vă juca, iată cum arată bucla completă de antrenament cu 🤗 Accelerate:
from accelerate import Accelerator
from transformers import AdamW, AutoModelForSequenceClassification, get_scheduler
accelerator = Accelerator()
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
optimizer = AdamW(model.parameters(), lr=3e-5)
train_dl, eval_dl, model, optimizer = accelerator.prepare(
train_dataloader, eval_dataloader, model, optimizer
)
num_epochs = 3
num_training_steps = num_epochs * len(train_dl)
lr_scheduler = get_scheduler(
"linear",
optimizer=optimizer,
num_warmup_steps=0,
num_training_steps=num_training_steps,
)
progress_bar = tqdm(range(num_training_steps))
model.train()
for epoch in range(num_epochs):
for batch in train_dl:
outputs = model(**batch)
loss = outputs.loss
accelerator.backward(loss)
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
progress_bar.update(1)
Plasând acest cod într-un fișier train.py
îl face rulabil pe orice tip de configurare distribuită. Pentru a-l încerca în configurarea voastră distribuită, rulați comanda:
accelerate config
care vă va cere să răspundeți la câteva întrebări și vă va crea un fișier de configurare folosit de comanda:
accelerate launch train.py
care va porni antrenarea distribuită.
Dacă vreți să încercați asta într-un Notebook (de exemplu, pentru a-l testa cu TPU-uri pe Colab), doar lipiți codul într-o funcție training_function()
și rulați într-o celulă finală:
from accelerate import notebook_launcher
notebook_launcher(training_function)
Puteți găsi mai multe exemple în repo-ul 🤗 Accelerate.
< > Update on GitHub