Pytorch
TensorFlow

#
Clasificarea tokenilor

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:

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 defini`TrainingArguments`:

    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 `DataLoader`s 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