LLM Course documentation

Clasificarea tokenilor

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Clasificarea tokenilor

Ask a Question Open In Colab Open In Studio Lab

Prima aplicație pe care o vom explora este clasificarea tokenilor. Această sarcină generică cuprinde orice problemă care poate fi formulată ca “atribuirea unui label fiecărui token dintr-o propoziție”, cum ar fi:

  • Named entity recognition(NER): Găsirea entităților (cum ar fi persoane, locații sau organizații) într-o propoziție. Acest lucru poate fi formulat ca atribuirea unui label fiecărui token, având o clasă pentru fiecare entitate și o clasă pentru “nicio entitate”.
  • Part-of-speech tagging (POS): Marchează fiecare cuvânt dintr-o propoziție ca corespunzând unei anumite părți de vorbire (cum ar fi substantiv, verb, adjectiv etc.).
  • Chunking: Găsirea tokenilor care aparțin aceleiași entități. Această sarcină (care poate fi combinată cu POS sau NER) poate fi formulată ca atribuirea unui label (de obicei B-) tuturor tokenilor care se află la începutul unui chunk, a unui alt label (de obicei I-) la tokeni care se află în interiorul unui chunk și a unui al treilea label (de obicei O) la tokeni care nu aparțin niciunui chunk.

Desigur, există multe alte tipuri de probleme de clasificare a tokenilor; acestea sunt doar câteva exemple reprezentative. În această secțiune, vom pune la punct un model (BERT) pe o sarcină NER, care va fi apoi capabil să calculeze predicții precum aceasta:

One-hot encoding labels pentru răspunderea la întrebări.

Puteți găsi modelul pe care îl vom antrena și încărca în Hub și puteți verifica de două ori predicțiile sale aici.

Pregătirea datelor

În primul rând, avem nevoie de un dataset adecvat pentru clasificarea simbolurilor. În această secțiune vom utiliza [datasetul CoNLL-2003] (https://huggingface.co/datasets/conll2003), care conține știri de la Reuters.

💡 Atât timp cât datasetul vostru constă în texte împărțite în cuvinte cu labelurile corespunzătoare, veți putea adapta procedurile de preprocesare a datelor descrise aici la propriul dataset. Consultați Capitolul 5 dacă aveți nevoie de o recapitulare a modului de încărcare a propriilor date personalizate într-un Dataset.

Datasetul CoNLL-2003

Pentru a încărca datasetul CoNLL-2003, folosim metoda load_dataset() din biblioteca 🤗 Datasets:

from datasets import load_dataset

raw_datasets = load_dataset("conll2003")

Acest lucru va descărca și va stoca în cache datasetul, așa cum am văzut în Capitolul 3 pentru setul de date GLUE MRPC. Inspectarea acestui obiect ne arată coloanele prezente și împărțirea între seturile de antrenare, validare și testare:

raw_datasets
DatasetDict({
    train: Dataset({
        features: ['chunk_tags', 'id', 'ner_tags', 'pos_tags', 'tokens'],
        num_rows: 14041
    })
    validation: Dataset({
        features: ['chunk_tags', 'id', 'ner_tags', 'pos_tags', 'tokens'],
        num_rows: 3250
    })
    test: Dataset({
        features: ['chunk_tags', 'id', 'ner_tags', 'pos_tags', 'tokens'],
        num_rows: 3453
    })
})

În special, putem vedea că datasetul conține labeluri pentru cele trei sarcini pe care le-am menționat mai devreme: NER, POS și Chunking. O mare diferență față de alte datseturi este că textele de intrare nu sunt prezentate ca propoziții sau documente, ci ca liste de cuvinte (ultima coloană se numește tokens, dar conține cuvinte în sensul că acestea sunt inputuri pre-tokenizate care mai trebuie să treacă prin tokenizer pentru tokenizarea subcuvintelor).

Să aruncăm o privire la primul element al setului de formare:

raw_datasets["train"][0]["tokens"]
['EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'lamb', '.']

Deoarece dorim să efectuăm named entity recognition, ne vom uita la etichetele NER:

raw_datasets["train"][0]["ner_tags"]
[3, 0, 7, 0, 0, 0, 7, 0, 0]

Acestea sunt labelurile ca numere întregi pregătite pentru antrenare, dar nu sunt neapărat utile atunci când dorim să inspectăm datele. La fel ca în cazul clasificării textului, putem accesa corespondența dintre aceste numere întregi și numele labelurilor consultând atributul features al datasetului nostru:

ner_feature = raw_datasets["train"].features["ner_tags"]
ner_feature
Sequence(feature=ClassLabel(num_classes=9, names=['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC'], names_file=None, id=None), length=-1, id=None)

Deci, această coloană conține elemente care sunt secvențe a ClassLabel. Tipul elementelor din secvență se află în atributul feature al acestei ner_feature, iar noi putem accesa lista de nume consultând atributul names al acelei feature:

label_names = ner_feature.feature.names
label_names
['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC']

Am văzut deja aceste labeluri atunci când am cercetat pipelineul token-classification în Capitolul 6, dar pentru o reîmprospătare rapidă:

  • O înseamnă că cuvântul nu corespunde niciunei entități.
  • B-PER/I-PER înseamnă că cuvântul corespunde începutului/este în interiorul unei entități de tip persoană.
  • B-ORG/I-ORG înseamnă că cuvântul corespunde începutului/este în interiorul unei entități de tip organizaționale.
  • B-LOC/I-LOC înseamnă că cuvântul corespunde începutului/este în interiorul unei entități de tip locație.
  • B-MISC/I-MISC înseamnă că cuvântul corespunde începutului/este în interiorul unei entități de tip *miscellaneous.

Acum, decodificarea labelurilor pe care le-am văzut mai devreme ne dă următorul rezultat:

words = raw_datasets["train"][0]["tokens"]
labels = raw_datasets["train"][0]["ner_tags"]
line1 = ""
line2 = ""
for word, label in zip(words, labels):
    full_label = label_names[label]
    max_length = max(len(word), len(full_label))
    line1 += word + " " * (max_length - len(word) + 1)
    line2 += full_label + " " * (max_length - len(full_label) + 1)

print(line1)
print(line2)
'EU    rejects German call to boycott British lamb .'
'B-ORG O       B-MISC O    O  O       B-MISC  O    O'

Și pentru un exemplu de amestecare a labelurilor B- și I-, iată ce ne oferă același cod pentru elementul din setul de antrenare de la indexul 4:

'Germany \'s representative to the European Union \'s veterinary committee Werner Zwingmann said on Wednesday consumers should buy sheepmeat from countries other than Britain until the scientific advice was clearer .'
'B-LOC   O  O              O  O   B-ORG    I-ORG O  O          O         B-PER  I-PER     O    O  O         O         O      O   O         O    O         O     O    B-LOC   O     O   O          O      O   O       O'

După cum putem vedea, entităților care cuprind două cuvinte, precum “Uniunea Europeană” și “Werner Zwingmann”, li se atribuie un label B- pentru primul cuvânt și un label I- pentru al doilea.

✏️ E rândul tău! Afișați aceleași două propoziții cu labelurile POS sau chunking.

Procesarea datelor

Ca de obicei, textele noastre trebuie să fie convertite în ID-uri token înainte ca modelul să le poată înțelege. După cum am văzut în Capitolul 6, o mare diferență în cazul sarcinilor de clasificare a tokenilor este că avem inputuri pre-tokenizate. Din fericire, API-ul tokenizerului poate face față acestei situații destul de ușor; trebuie doar să avertizăm tokenizerul cu un indicator special.

Pentru început, hai să creăm obiectul tokenizer. După cum am mai spus, vom utiliza un model BERT preantrenat, deci vom începe prin a descărca și a stoca în cache tokenizerul asociat:

from transformers import AutoTokenizer

model_checkpoint = "bert-base-cased"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)

Puteți înlocui model_checkpoint cu orice alt model preferat din Hub sau cu un folder local în care ați salvat un model preantreant și un tokenizer. Singura constrângere este că tokenizerul trebuie să fie susținut de biblioteca 🤗 Tokenizers, pentru ca să existe o versiune “rapidă” disponibilă. Puteți vedea toate arhitecturile care vin cu o versiune rapidă în [acest tabel mare] (https://huggingface.co/transformers/#supported-frameworks), iar pentru a verifica dacă obiectul tokenizer pe care îl utilizați este într-adevăr susținut de 🤗 Tokenizers, vă puteți uita la atributul său is_fast:

tokenizer.is_fast
True

Pentru a tokeniza un input pre-tokenizat, putem utiliza tokenizer ca de obicei și să adăugăm is_split_into_words=True:

inputs = tokenizer(raw_datasets["train"][0]["tokens"], is_split_into_words=True)
inputs.tokens()
['[CLS]', 'EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'la', '##mb', '.', '[SEP]']

După cum putem vedea, tokenizerul a adăugat tokenii speciali utilizați de model ([CLS] la început și [SEP] la sfârșit) și a lăsat majoritatea cuvintelor neatinse. Cu toate acestea, cuvântul lamb a fost tokenizat în două subcuvinte, la și ##mb. Acest lucru introduce o neconcordanță între inputurile noastre și labeluri: lista de labeluri are doar 9 elemente, în timp ce inputurile noastre au acum 12 tokeni. Să luăm în considerare tokenii speciali este ușor (știm că sunt la început și la sfârșit), dar trebuie să ne asigurăm că aliniem toate labelurile cu cuvintele corespunzătoare.

Din fericire, pentru că folosim un tokenizer rapid, avem acces la superputerile 🤗 Tokenizers, ceea ce înseamnă că putem mapa cu ușurință fiecare token la cuvântul corespunzător (după cum se vede în Capitolul 6):

inputs.word_ids()
[None, 0, 1, 2, 3, 4, 5, 6, 7, 7, 8, None]

Cu puțină muncă, putem apoi extinde lista de labeluri pentru a se potrivi cu tokenii. Prima regulă pe care o vom aplica este că tokenii special primesc un label -100. Acest lucru se datorează faptului că în mod implicit -100 este un indice care este ignorat în funcția de pierdere pe care o vom utiliza (cross-entropy). Apoi, fiecare token primește același label ca și tokenul care a inițiat cuvântul în care se află, deoarece fac parte din aceeași entitate. Pentru tokenii din interiorul unui cuvânt, dar care nu se află la început, înlocuim B- cu I- (deoarece tokenul nu începe entitatea):

def align_labels_with_tokens(labels, word_ids):
    new_labels = []
    current_word = None
    for word_id in word_ids:
        if word_id != current_word:
            # Start of a new word!
            current_word = word_id
            label = -100 if word_id is None else labels[word_id]
            new_labels.append(label)
        elif word_id is None:
            # Special token
            new_labels.append(-100)
        else:
            # Same word as previous token
            label = labels[word_id]
            # If the label is B-XXX we change it to I-XXX
            if label % 2 == 1:
                label += 1
            new_labels.append(label)

    return new_labels

Hai să îl încercăm cu prima noastră propoziție:

labels = raw_datasets["train"][0]["ner_tags"]
word_ids = inputs.word_ids()
print(labels)
print(align_labels_with_tokens(labels, word_ids))
[3, 0, 7, 0, 0, 0, 7, 0, 0]
[-100, 3, 0, 7, 0, 0, 0, 7, 0, 0, 0, -100]

După cum putem vedea, funcția noastră a adăugat -100 pentru cei doi tokeni speciali de la început și de la sfârșit, și un nou 0 pentru cuvântul nostru care a fost împărțit în doi tokeni.

✏️ Rândul tău! Unii cercetători preferă să atribuie un singur label pe cuvânt și să atribuie -100 celorlalți subtokeni dintr-un cuvânt dat. Aceasta are loc pentru a evita ca cuvintele lungi care se împart în mai mulți subtokeni să contribuie puternic la pierdere. Modificați funcția anterioară pentru a alinia labelurile cu ID-urile de input urmând această regulă.

Pentru a preprocesa întregul nostru dataset, trebuie să tokenizăm toate inputurile și să aplicăm align_labels_with_tokens() pe toate labelurile. Pentru a profita de viteza tokenizerului nostru rapid, este mai bine să tokenizăm multe texte în același timp, așa că vom scrie o funcție care procesează o listă de exemple și vom folosi metoda Dataset.map() cu opțiunea batched=True. Singurul lucru diferit față de exemplul nostru anterior este că funcția word_ids() trebuie să obțină indexul exemplului din care dorim ID-urile cuvintelor atunci când inputurile către tokenizer sunt liste de texte (sau, în cazul nostru, liste de liste de cuvinte), așa că adăugăm și acest lucru:

def tokenize_and_align_labels(examples):
    tokenized_inputs = tokenizer(
        examples["tokens"], truncation=True, is_split_into_words=True
    )
    all_labels = examples["ner_tags"]
    new_labels = []
    for i, labels in enumerate(all_labels):
        word_ids = tokenized_inputs.word_ids(i)
        new_labels.append(align_labels_with_tokens(labels, word_ids))

    tokenized_inputs["labels"] = new_labels
    return tokenized_inputs

Rețineți că nu am completat încă inputurile; vom face acest lucru mai târziu, atunci când vom crea batchurile cu un data collator.

Acum putem aplica toate aceste preprocesări dintr-o dată celorlalte fracțiuni ale datasetului nostru:

tokenized_datasets = raw_datasets.map(
    tokenize_and_align_labels,
    batched=True,
    remove_columns=raw_datasets["train"].column_names,
)

Am făcut partea cea mai grea! Acum, că datele au fost preprocesate, antrenarea efectivă va semăna mult cu ceea ce am făcut în Capitolul 3.

Fine-tuningul modelului cu Trainer API

Codul real care utilizează Trainer va fi același ca înainte; singurele modificări sunt modul în care datele sunt collated într-un batch și metric computation function.

Data collation

Nu putem folosi doar un DataCollatorWithPadding ca în Capitolul 3 deoarece acesta doar adaugă padding inputurilor(input IDs, attention mask, and token type IDs). Aici labelurile noastre ar trebui să fie padded exact în același mod ca și inputurile, astfel încât să rămână de aceeași dimensiune, folosind -100 ca valoare, astfel încât predicțiile corespunzătoare să fie ignorate în calculul pierderilor.

Toate acestea sunt realizate de un DataCollatorForTokenClassification. La fel ca DataCollatorWithPadding, acesta ia tokenizer-ul folosit pentru preprocesarea inputurilor:

from transformers import DataCollatorForTokenClassification

data_collator = DataCollatorForTokenClassification(tokenizer=tokenizer)

Pentru a testa acest lucru pe câteva sampleuri, îl putem apela doar pe o listă de exemple din setul nostru de antrenat tokenizat:

batch = data_collator([tokenized_datasets["train"][i] for i in range(2)])
batch["labels"]
tensor([[-100,    3,    0,    7,    0,    0,    0,    7,    0,    0,    0, -100],
        [-100,    1,    2, -100, -100, -100, -100, -100, -100, -100, -100, -100]])

Hai să comparăm acest lucru cu labelurile pentru primul și al doilea element din datasetul nostru:

for i in range(2):
    print(tokenized_datasets["train"][i]["labels"])
[-100, 3, 0, 7, 0, 0, 0, 7, 0, 0, 0, -100]
[-100, 1, 2, -100]

După cum se poate observa, al doilea set de labeluri a fost padded la lungimea primului folosind -100.

Metrici

Pentru ca Trainer să calculeze o metrică în fiecare epocă, va trebui să definim o funcție compute_metrics() care primește matricele de predicții și labelurile și returnează un dicționar cu numele și valorile metricilor.

Frameworkul tradițional utilizat pentru a evalua predicția clasificării a tokenilor este seqeval. Pentru a utiliza această metrică, trebuie mai întâi să instalăm biblioteca seqeval:

!pip install seqeval

Apoi îl putem încărca prin intermediul funcției evaluate.load() așa cum am făcut în Capitolul 3:

import evaluate

metric = evaluate.load("seqeval")

Această metrică nu se comportă ca precizia standard: de fapt, va lua listele de labeluri ca șiruri de caractere, nu ca numere întregi, deci va trebui să decodificăm complet predicțiile și labelurile înainte de a le trece în metrice. Să vedem cum funcționează. În primul rând, vom obține labelurile pentru primul nostru exemplu de antrenare:

labels = raw_datasets["train"][0]["ner_tags"]
labels = [label_names[i] for i in labels]
labels
['B-ORG', 'O', 'B-MISC', 'O', 'O', 'O', 'B-MISC', 'O', 'O']

Putem apoi crea predicții false pentru acestea prin simpla schimbare a valorii la indexul 2:

predictions = labels.copy()
predictions[2] = "O"
metric.compute(predictions=[predictions], references=[labels])

Rețineți că metrica ia o listă de predicții (nu doar una) și o listă de labels. Iată rezultatul:

{'MISC': {'precision': 1.0, 'recall': 0.5, 'f1': 0.67, 'number': 2},
 'ORG': {'precision': 1.0, 'recall': 1.0, 'f1': 1.0, 'number': 1},
 'overall_precision': 1.0,
 'overall_recall': 0.67,
 'overall_f1': 0.8,
 'overall_accuracy': 0.89}

Acesta trimite înapoi o mulțime de informații! Noi obținem precizia, recallul și scorul F1 pentru fiecare entitate în parte, precum și scorul general. Pentru calculul nostru metric, vom păstra doar scorul global, dar nu ezitați să modificați funcția compute_metrics() pentru a returna toate metricile pe care doriți să le raportați.

Această funcție compute_metrics() ia mai întâi argmaxul logiturilor pentru a le converti în predicții (ca de obicei, logiturile și probabilitățile sunt în aceeași ordine, deci nu trebuie să aplicăm softmaxul). Apoi trebuie să convertim atât labelurile, cât și predicțiile din numere întregi în șiruri de caractere. Eliminăm toate valorile în care labelul este -100, apoi transmitem rezultatele metodei metric.compute():

import numpy as np


def compute_metrics(eval_preds):
    logits, labels = eval_preds
    predictions = np.argmax(logits, axis=-1)

    # Remove ignored index (special tokens) and convert to labels
    true_labels = [[label_names[l] for l in label if l != -100] for label in labels]
    true_predictions = [
        [label_names[p] for (p, l) in zip(prediction, label) if l != -100]
        for prediction, label in zip(predictions, labels)
    ]
    all_metrics = metric.compute(predictions=true_predictions, references=true_labels)
    return {
        "precision": all_metrics["overall_precision"],
        "recall": all_metrics["overall_recall"],
        "f1": all_metrics["overall_f1"],
        "accuracy": all_metrics["overall_accuracy"],
    }

Acum că acest lucru este făcut, suntem aproape gata să definim Trainer-ul nostru. Avem nevoie doar de un model pentru a face fine-tune!

Definirea modelului

Deoarece lucrăm la o problemă de clasificare a tokenilor, vom utiliza clasa AutoModelForTokenClassification. Principalul lucru de care trebuie să ne amintim atunci când definim acest model este să transmitem informații privind numărul de labeluri pe care le avem. Cel mai simplu mod de a face acest lucru este să transmiteți acest număr cu argumentul num_labels, dar dacă dorim un inferencea widget frumos care să funcționeze ca cel pe care l-am văzut la începutul acestei secțiuni, este mai bine să setați în schimb corespondențele corecte ale labelurilor.

Acestea ar trebui să fie stabilite de două dicționare, id2label și label2id, care conțin corespondențele de la ID la labeluri și viceversa:

id2label = {i: label for i, label in enumerate(label_names)}
label2id = {v: k for k, v in id2label.items()}

Acum putem doar să le transmitem metodei AutoModelForTokenClassification.from_pretrained(), iar acestea vor fi setate în configurația modelului și apoi salvate și încărcate corespunzător în Hub:

from transformers import AutoModelForTokenClassification

model = AutoModelForTokenClassification.from_pretrained(
    model_checkpoint,
    id2label=id2label,
    label2id=label2id,
)

La fel ca atunci când am definit AutoModelForSequenceClassification în Capitolul 3, crearea modelului emite un avertisment că unele weights nu au fost utilizate (cele din headul de antrenare) și alte weights sunt inițializate aleatoriu (cele din headul de clasificare a tokenilor noi) și că acest model ar trebui să fie format. Vom face acest lucru într-un minut, dar mai întâi să verificăm de două ori că modelul nostru are numărul corect de labeluri:

model.config.num_labels
9

⚠️ Dacă aveți un model cu un număr greșit de labeluri, veți primi o eroare obscură atunci când apelați metoda Trainer.train() mai târziu (ceva de genul “CUDA error: device-side assert triggered”). Aceasta este cauza numărul unu a erorilor raportate de utilizatori pentru astfel de erori, așa că asigurați-vă că faceți această verificare pentru a confirma că aveți numărul de labeluri așteptat.

Fine-tuningul modelului

Acum suntem gata să ne antrenăm modelul! Trebuie doar să facem ultimele două lucruri înainte de a defini modelul nostru Trainer: să ne conectăm la Hugging Face și să definim argumentele de antrenare. Dacă lucrați într-un notebook, există o funcție convenabilă pentru a vă ajuta cu acest lucru:

from huggingface_hub import notebook_login

notebook_login()

Aceasta va afișa un widget în care puteți introduce datele tale de autentificare Hugging Face.

Dacă nu lucrați într-un notebook, tastați următoarea linie în terminal:

huggingface-cli login

Odată făcut acest lucru, putem definiTrainingArguments:

from transformers import TrainingArguments

args = TrainingArguments(
    "bert-finetuned-ner",
    evaluation_strategy="epoch",
    save_strategy="epoch",
    learning_rate=2e-5,
    num_train_epochs=3,
    weight_decay=0.01,
    push_to_hub=True,
)

Ați mai văzut cele mai multe dintre acestea: stabilim niște hiperparametri (cum ar fi learning rate, numărul de epoci pentru care să ne antrenăm și weights decay) și specificăm push_to_hub=True pentru a indica faptul că dorim să salvăm modelul și să îl evaluăm la sfârșitul fiecărei epoci și că dorim să încărcăm rezultatele noastre în Model Hub. Rețineți că puteți specifica numele repositoriului către care doriți să faceți push cu argumentul hub_model_id (în special, va trebui să utilizați acest argument pentru a face push către o organizație). De exemplu, atunci când am trimis modelul către organizația huggingface-course, am adăugat hub_model_id="huggingface-course/bert-finetuned-ner" la TrainingArguments. În mod implicit, repositoriul utilizat va fi în namespaceul tău și denumit după output directory-ul pe care l-ați setat, deci în cazul nostru va fi "sgugger/bert-finetuned-ner".

💡 Dacă output directory-ul pe care îl utilizați există deja, acesta trebuie să fie o clonă locală a repositoriul către care doriți să faceți push. Dacă nu este așa, veți primi o eroare la definirea Trainer și va trebui să setați un nume nou.

În final, transmitem totul către Trainer și lansăm antrenarea:

from transformers import Trainer

trainer = Trainer(
    model=model,
    args=args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
    data_collator=data_collator,
    compute_metrics=compute_metrics,
    tokenizer=tokenizer,
)
trainer.train()

Rețineți că, în timpul antrenării, de fiecare dată când modelul este salvat (aici, la fiecare epocă), acesta este încărcat pe Hub în fundal. În acest fel, veți putea să reluați antrenareape o altă mașină, dacă este necesar.

Odată ce antrenamentul este complet, folosim metoda push_to_hub() pentru a ne asigura că încărcăm cea mai recentă versiune a modelului:

trainer.push_to_hub(commit_message="Training complete")

Această comandă returnează URL-ul comitului pe care tocmai l-ai făcut, dacă doriți să o inspectați:

'https://huggingface.co/sgugger/bert-finetuned-ner/commit/26ab21e5b1568f9afeccdaed2d8715f571d786ed'

De asemenea, Trainer redactează un model card cu toate rezultatele evaluării și o încarcă. În acest stadiu, puteți utiliza inference widgetul de pe Model Hub pentru a testa modelul și a-l partaja cu prietenii. Ați făcut fine-tune cu succes un model pentru o sarcină de clasificare a tokenilor - felicitări!

Dacă doriți să vă scufundați puțin mai profund în bucla de antrenare, vă vom arăta acum cum să faceți același lucru utilizând 🤗 Accelerate.

Un training loop personalizat

Să aruncăm acum o privire la training loopul complet, astfel încât să puteți personaliza cu ușurință părțile de care aveți nevoie. Va semăna foarte mult cu ceea ce am făcut în Capitolul 3, cu câteva modificări pentru evaluare.

Pregătiți totul pentru antrenare

Mai întâi trebuie să construim DataLoaders din dataseturile noastre. Vom reutiliza data_collator ca un collate_fn și vom amesteca setul de antrenare, dar nu și setul de validare:

from torch.utils.data import DataLoader

train_dataloader = DataLoader(
    tokenized_datasets["train"],
    shuffle=True,
    collate_fn=data_collator,
    batch_size=8,
)
eval_dataloader = DataLoader(
    tokenized_datasets["validation"], collate_fn=data_collator, batch_size=8
)

În continuare, reinstanțiem modelul, pentru a ne asigura că nu continuăm fine-tuningul de dinainte, ci pornim din nou de la modelul preantrenat BERT:

model = AutoModelForTokenClassification.from_pretrained(
    model_checkpoint,
    id2label=id2label,
    label2id=label2id,
)

Atunci vom avea nevoie de un optimizator. Vom folosi clasicul AdamW, care este ca Adam, dar cu o corecție în modul în care se aplică weight decay-ul:

from torch.optim import AdamW

optimizer = AdamW(model.parameters(), lr=2e-5)

Odată ce avem toate aceste obiecte, le putem trimite la metoda accelerator.prepare():

from accelerate import Accelerator

accelerator = Accelerator()
model, optimizer, train_dataloader, eval_dataloader = accelerator.prepare(
    model, optimizer, train_dataloader, eval_dataloader
)

🚨 Dacă vă antrenați pe un TPU, va trebui să mutați tot codul începând de la celula de mai sus într-o funcție de antrenament dedicată. Consultați Capitolul 3 pentru mai multe detalii.

Acum că am trimis train_dataloader la accelerator.prepare(), putem utiliza lungimea acestuia pentru a calcula numărul de pași de antrenare. Rețineți că ar trebui să facem întotdeauna acest lucru după ce pregătim dataloaderul, deoarece această metodă îi va modifica lungimea. Utilizăm un classic liner schedule de la rata de învățare la 0:

from transformers import get_scheduler

num_train_epochs = 3
num_update_steps_per_epoch = len(train_dataloader)
num_training_steps = num_train_epochs * num_update_steps_per_epoch

lr_scheduler = get_scheduler(
    "linear",
    optimizer=optimizer,
    num_warmup_steps=0,
    num_training_steps=num_training_steps,
)

În cele din urmă, pentru a trimite modelul nostru către Hub, va trebui să creăm un obiect Repository într-un folder de lucru. În primul rând, conectați-vă la Hugging Face, dacă nu sunteți deja conectat. Vom determina numele repositoriului pornind de la ID-ul modelului pe care dorim să îl dăm modelului nostru (nu ezitați să înlocuiți repo_name cu propriul nume; trebuie doar să conțină numele vostru de utilizator, ceea ce face funcția get_full_repo_name()):

from huggingface_hub import Repository, get_full_repo_name

model_name = "bert-finetuned-ner-accelerate"
repo_name = get_full_repo_name(model_name)
repo_name
'sgugger/bert-finetuned-ner-accelerate'

Apoi putem clona acel repositoriu într-un folder local. Dacă există deja, acest folder local ar trebui să fie o clonă existentă a repositoriului cu care lucrăm:

output_dir = "bert-finetuned-ner-accelerate"
repo = Repository(output_dir, clone_from=repo_name)

Acum putem încărca orice salvăm în output_dir prin apelarea metodei repo.push_to_hub(). Acest lucru ne va ajuta să încărcăm modelele intermediare la sfârșitul fiecărei epoci.

Loopul de antrenare

Acum suntem pregătiți să scriem bucla de antrenare completă. Pentru a simplifica partea sa de evaluare, definim funcția postprocess() care preia predicțiile și labelurile și le convertește în liste de șiruri de caractere, așa cum se așteaptă obiectul nostru metric:

def postprocess(predictions, labels):
    predictions = predictions.detach().cpu().clone().numpy()
    labels = labels.detach().cpu().clone().numpy()

    # Remove ignored index (special tokens) and convert to labels
    true_labels = [[label_names[l] for l in label if l != -100] for label in labels]
    true_predictions = [
        [label_names[p] for (p, l) in zip(prediction, label) if l != -100]
        for prediction, label in zip(predictions, labels)
    ]
    return true_labels, true_predictions

Apoi putem scrie bucla de antrenare. După definirea unei bare de progres pentru a urmări modul în care decurge antrenarea, bucla are trei părți:

  • Antrenarea în sine, care este iterația clasică peste train_dataloader, trecerea înainte prin model, apoi trecerea înapoi și pasul optimizatorului.
  • Evaluarea, în care există o noutate după obținerea outputurilor modelului nostru pe un batch: din moment ce două procese pot ar fi putut face padding inputurilor și labelurile la forme diferite, trebuie să folosim accelerator.pad_across_processes() pentru a face predicțiile și labelurile să aibă aceeași formă înainte de a apela metoda gather(). Dacă nu facem acest lucru, evaluarea va da eroare sau se va bloca pentru totdeauna. Apoi trimitem rezultatele la metric.add_batch() și apelăm metric.compute() odată ce bucla de evaluare s-a încheiat.
  • Salvarea și încărcarea, unde mai întâi salvăm modelul și tokenizerul, apoi apelăm repo.push_to_hub(). Observați că folosim argumentul blocking=False pentru a spune bibliotecii 🤗 Hub să efectueze push-ul într-un proces asincron. În acest fel, antrenamentul continuă normal, iar această instrucțiune (lungă) este executată în fundal.

Iată codul complet pentru bucla de antrenare:

from tqdm.auto import tqdm
import torch

progress_bar = tqdm(range(num_training_steps))

for epoch in range(num_train_epochs):
    # Training
    model.train()
    for batch in train_dataloader:
        outputs = model(**batch)
        loss = outputs.loss
        accelerator.backward(loss)

        optimizer.step()
        lr_scheduler.step()
        optimizer.zero_grad()
        progress_bar.update(1)

    # Evaluation
    model.eval()
    for batch in eval_dataloader:
        with torch.no_grad():
            outputs = model(**batch)

        predictions = outputs.logits.argmax(dim=-1)
        labels = batch["labels"]

        # Necessary to pad predictions and labels for being gathered
        predictions = accelerator.pad_across_processes(predictions, dim=1, pad_index=-100)
        labels = accelerator.pad_across_processes(labels, dim=1, pad_index=-100)

        predictions_gathered = accelerator.gather(predictions)
        labels_gathered = accelerator.gather(labels)

        true_predictions, true_labels = postprocess(predictions_gathered, labels_gathered)
        metric.add_batch(predictions=true_predictions, references=true_labels)

    results = metric.compute()
    print(
        f"epoch {epoch}:",
        {
            key: results[f"overall_{key}"]
            for key in ["precision", "recall", "f1", "accuracy"]
        },
    )

    # Save and upload
    accelerator.wait_for_everyone()
    unwrapped_model = accelerator.unwrap_model(model)
    unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save)
    if accelerator.is_main_process:
        tokenizer.save_pretrained(output_dir)
        repo.push_to_hub(
            commit_message=f"Training in progress epoch {epoch}", blocking=False
        )

În cazul în care este prima dată când vedeți un model salvat cu 🤗 Accelerate, să ne oprim puțin pentru a inspecta cele trei linii de cod care îl însoțesc:

accelerator.wait_for_everyone()
unwrapped_model = accelerator.unwrap_model(model)
unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save)

Prima linie se explică de la sine: aceasta spune tuturor proceselor să aștepte până când toată lumea se află în etapa respectivă înainte de a continua. Acest lucru are rolul de a ne asigura că avem același model în fiecare proces înainte de a salva. Apoi luăm unwrapped_model, care este modelul de bază pe care l-am definit. Metoda accelerator.prepare() modifică modelul pentru a funcționa în antrenarea distribuită, deci nu va mai avea metoda save_pretrained(); metoda accelerator.unwrap_model() anulează acest pas. În cele din urmă, apelăm metoda save_pretrained(), dar îi spunem să folosească metoda accelerator.save() în loc de torch.save().

Odată făcut acest lucru, ar trebui să aveți un model care produce rezultate destul de asemănătoare cu cel antrenat cu Trainer. Puteți verifica modelul pe care l-am antrenat folosind acest cod la [huggingface-course/bert-finetuned-ner-accelerate] (https://huggingface.co/huggingface-course/bert-finetuned-ner-accelerate). Și dacă doriți să testați orice modificări ale buclei de antrenare, le puteți implementa direct prin editarea codului prezentat mai sus!

Utilizarea model fine-tuned

V-am arătat deja cum puteți utiliza modelul pe care l-am ajustat pe Model Hub cu inference widget. Pentru a-l utiliza la nivel local într-un pipeline, trebuie doar să specificați identificatorul de model corespunzător:

from transformers import pipeline

# Replace this with your own checkpoint
model_checkpoint = "huggingface-course/bert-finetuned-ner"
token_classifier = pipeline(
    "token-classification", model=model_checkpoint, aggregation_strategy="simple"
)
token_classifier("My name is Sylvain and I work at Hugging Face in Brooklyn.")
[{'entity_group': 'PER', 'score': 0.9988506, 'word': 'Sylvain', 'start': 11, 'end': 18},
 {'entity_group': 'ORG', 'score': 0.9647625, 'word': 'Hugging Face', 'start': 33, 'end': 45},
 {'entity_group': 'LOC', 'score': 0.9986118, 'word': 'Brooklyn', 'start': 49, 'end': 57}]

Grozav! Modelul nostru funcționează la fel de bine ca cel implicit pentru aceast pipeline!

Update on GitHub