Pytorch
TensorFlow

#
Traducere

Să trecem acum la traducere. Aceasta este o altă sarcină de sequence-to-sequence, ceea ce înseamnă că este o problemă care poate fi formulată ca trecerea de la o secvență la alta. În acest sens, problema este destul de apropiată de sumarizare și ați putea adapta ceea ce vom vedea aici la alte probleme de la sequence-to-sequence, cum ar fi:

- Style transfer: Crearea unui model care traduce texte scrise într-un anumit stil în altul (de exemplu, din formal în casual sau din engleza Shakespeariană în engleza modernă)
- Generative question answering: Crearea unui model care generează răspunsuri la întrebări, fiind dat un context

Dacă aveți un corpus suficient de mare de texte în două (sau mai multe) limbi, puteți antrena un nou model de traducere de la zero, așa cum vom face în secțiunea despre [causal language modeling] (/course/chapter7/6). Cu toate acestea, va fi mai rapid să faceți fine-tune unui model de traducere existent, fie că este vorba de un model multilingv precum mT5 sau mBART, pe care doriți să îi faceți fine-tune pentru o anumită pereche de limbi, sau chiar un model specializat pentru traducerea dintr-o limbă în alta, pe care doriți să îl perfecționați pentru corpusul vostru specific.

În această secțiune, vom pune face fine-tune unui model Marian preantrenat pentru a traduce din engleză în franceză (deoarece mulți dintre angajații Hugging Face vorbesc ambele limbi) pe datasetul KDE4, care este un set de fișiere localizate pentru KDE apps. Modelul pe care îl vom utiliza a fost preantrenat pe un corpus mare de texte în franceză și engleză preluate din datasetul Opus, care conține de fapt datasetul KDE4. Dar chiar dacă modelul preantrenat pe care îl folosim a văzut aceste date în timpul preantrenării sale, vom vedea că putem obține o versiune mai bună a acestuia după fine-tuning.

Odată ce am terminat, vom avea un model capabil să facă predicții ca acesta:

Ca și în secțiunile anterioare, puteți găsi modelul pe care îl vom antrena și încărca în Hub utilizând codul de mai jos și puteți verifica predicțiile acestuia aici.

##
Pregătirea datelor

Pentru a face fine-tune sau a antrena un model de traducere de la zero, vom avea nevoie de un dataset adecvat pentru această sarcină. După cum am menționat anterior, vom utiliza datasetul KDE4 în această secțiune, dar puteți adapta codul pentru a utiliza propriile date destul de ușor, atâta timp cât aveți perechi de propoziții în cele două limbi din și în care doriți să traduceți. Consultați Capitolul 5 dacă aveți nevoie de o reamintire a modului de încărcare a datelor personalizate într-un `Dataset`.

###
Datasetul KDE4

Ca de obicei, descărcăm datasetul nostru folosind funcția `load_dataset()`:

    from datasets import load_dataset

    raw_datasets = load_dataset("kde4", lang1="en", lang2="fr")

Dacă doriți să lucrați cu o pereche de limbi diferită, le puteți specifica prin codurile lor. Un total de 92 de limbi sunt disponibile pentru acest dataset; le puteți vedea pe toate prin extinderea labelurilor de limbă pe dataset cardul acesteia.

Să aruncăm o privire la dataset:

    raw_datasets

    DatasetDict({
        train: Dataset({
            features: ['id', 'translation'],
            num_rows: 210173
        })
    })

Avem 210 173 de perechi de propoziții, dar într-o singură împărțire, deci va trebui să ne creăm propriul set de validare. Așa cum am văzut în Capitolul 5, un `Dataset` are o metodă `train_test_split()` care ne poate ajuta. Vom furniza un seed pentru reproductibilitate:

    split_datasets = raw_datasets["train"].train_test_split(train_size=0.9, seed=20)
    split_datasets

    DatasetDict({
        train: Dataset({
            features: ['id', 'translation'],
            num_rows: 189155
        })
        test: Dataset({
            features: ['id', 'translation'],
            num_rows: 21018
        })
    })

Putem redenumi cheia `"test"` în `"validation"` astfel:

    split_datasets["validation"] = split_datasets.pop("test")

Acum să aruncăm o privire la un element al datasetului:

    split_datasets["train"][1]["translation"]

    {'en': 'Default to expanded threads',
     'fr': 'Par défaut, développer les fils de discussion'}

Obținem un dicționar cu două propoziții în perechea de limbi solicitate. O particularitate a acestui dataset plin de termeni tehnici din domeniul informaticii este că toate sunt traduse integral în franceză. Cu toate acestea, inginerii francezi lasă majoritatea cuvintelor specifice informaticii în engleză atunci când vorbesc. Aici, de exemplu, cuvântul “threads” ar putea foarte bine să apară într-o propoziție în limba franceză, în special într-o conversație tehnică; dar în acest dataset a fost tradus în “fils de discussion”. Modelul preantrenat pe care îl folosim, care a fost preantrenat pe un corpus mai mare de propoziții în franceză și engleză, alege opțiunea mai ușoară de a lăsa cuvântul așa cum este:

    from transformers import pipeline

    model_checkpoint = "Helsinki-NLP/opus-mt-en-fr"
    translator = pipeline("translation", model=model_checkpoint)
    translator("Default to expanded threads")

    [{'translation_text': 'Par défaut pour les threads élargis'}]

Un alt exemplu al acestui comportament poate fi văzut cu cuvântul “plugin”, care nu este oficial un cuvânt francez, dar pe care majoritatea vorbitorilor nativi îl vor înțelege și nu se vor obosi să îl traducă. În datasetul KDE4, acest cuvânt a fost tradus în franceză în termenul puțin mai oficial “module d’extension”:

    split_datasets["train"][172]["translation"]

    {'en': 'Unable to import %1 using the OFX importer plugin. This file is not the correct format.',
     'fr': "Impossible d'importer %1 en utilisant le module d'extension d'importation OFX. Ce fichier n'a pas un format correct."}

Modelul nostru preantrenat, cu toate acestea, rămâne la cuvântul englez compact și familiar:

    translator(
        "Unable to import %1 using the OFX importer plugin. This file is not the correct format."
    )

    [{'translation_text': "Impossible d'importer %1 en utilisant le plugin d'importateur OFX. Ce fichier n'est pas le bon format."}]

Va fi interesant de văzut dacă modelul nostru fine-tuned reține aceste particularități ale datasetului(spoiler alert: va reține).

✏️ Rândul tău! Un alt cuvânt englezesc care este adesea folosit în franceză este “email”. Găsiți primul sample din datasetul de antrenare care utilizează acest cuvânt. Cum este tradus? Cum traduce modelul preantrenat aceeași propoziție în limba engleză?

###
Procesarea datelor

Ar trebui să știți deja cum stă treaba: toate textele trebuie convertite în seturi de ID-uri token, astfel încât modelul să le poată înțelege. Pentru această sarcină, va trebui să tokenizăm atât inputurile, cât și targeturile. Prima noastră sarcină este să creăm obiectul `tokenizer`. După cum am menționat mai devreme, vom utiliza un model Marian preantrenat din engleză în franceză. Dacă încercați acest cod cu o altă pereche de limbi, asigurați-vă că adaptați checkpointul modelului. Organizația Helsinki-NLP oferă mai mult de o mie de modele în mai multe limbi.

    from transformers import AutoTokenizer

    model_checkpoint = "Helsinki-NLP/opus-mt-en-fr"
    tokenizer = AutoTokenizer.from_pretrained(model_checkpoint, return_tensors="pt")

De asemenea, puteți înlocui `model_checkpoint` cu orice alt model preferat din Hub sau cu un folder local în care ați salvat un model preantrenat și un tokenizer.

💡 Dacă utilizați un tokenizer multilingv, cum ar fi mBART, mBART-50 sau M2M100, va trebui să setați codurile de limbă ale inputurilor și targeturilor în tokenizer prin setarea `tokenizer.src_lang` și `tokenizer.tgt_lang` la valorile corecte.

Pregătirea datelor noastre este destul de simplă. Trebuie să rețineți un singur lucru; trebuie să vă asigurați că tokenizerul procesează targeturile în limba de ieșire (aici, franceză). Puteți face acest lucru trecând targeturile la argumentul `text_targets` al metodei `__call__` a tokenizerului.

Pentru a vedea cum funcționează acest lucru, să procesăm un sample din fiecare limbă din setul de antrenare:

    en_sentence = split_datasets["train"][1]["translation"]["en"]
    fr_sentence = split_datasets["train"][1]["translation"]["fr"]

    inputs = tokenizer(en_sentence, text_target=fr_sentence)
    inputs

    {'input_ids': [47591, 12, 9842, 19634, 9, 0], 'attention_mask': [1, 1, 1, 1, 1, 1], 'labels': [577, 5891, 2, 3184, 16, 2542, 5, 1710, 0]}

După cum putem vedea, rezultatul conține ID-urile de input asociate cu propoziția în limba engleză, în timp ce ID-urile asociate cu cea în limba franceză sunt stocate în câmpul `labels`. Dacă uitați să indicați că tokenizați labelurile, acestea vor fi tokenizate de către tokenizerul de input, ceea ce în cazul unui model Marian nu va merge deloc bine:

    wrong_targets = tokenizer(fr_sentence)
    print(tokenizer.convert_ids_to_tokens(wrong_targets["input_ids"]))
    print(tokenizer.convert_ids_to_tokens(inputs["labels"]))

    ['▁Par', '▁dé', 'f', 'aut', ',', '▁dé', 've', 'lop', 'per', '▁les', '▁fil', 's', '▁de', '▁discussion', '</s>']
    ['▁Par', '▁défaut', ',', '▁développer', '▁les', '▁fils', '▁de', '▁discussion', '</s>']

După cum se poate observa, utilizarea tokenizerului englez pentru preprocesarea unei propoziții franceze are ca rezultat mult mai mulți tokeni, deoarece tokenizerul nu cunoaște niciun cuvânt francez (cu excepția celor care apar și în limba engleză, precum “discussion”).

Deoarece `inputs` este un dicționar cu cheile noastre obișnuite (ID-uri de input, attention mask etc.), ultimul pas este să definim funcția de preprocesare pe care o vom aplica dataseturilor:

    max_length = 128

    def preprocess_function(examples):
        inputs = [ex["en"] for ex in examples["translation"]]
        targets = [ex["fr"] for ex in examples["translation"]]
        model_inputs = tokenizer(
            inputs, text_target=targets, max_length=max_length, truncation=True
        )
        return model_inputs

Rețineți că am stabilit aceeași lungime maximă pentru inputurile și outputurile noastre. Deoarece textele cu care avem de-a face par destul de scurte, vom folosi 128.

💡 Dacă utilizați un model T5 (mai precis, unul dintre checkpointurile `t5-xxx`), modelul se va aștepta ca inputurile text să aibă un prefix care să indice sarcina în cauză, cum ar fi `translate: din engleză în franceză:`.

⚠️ Nu acordăm atenție attention maskului a targeturilor, deoarece modelul nu se va aștepta la aceasta. În schimb, labelurile corespunzătoare unui padding token trebuie setate la `-100`, astfel încât acestea să fie ignorate în calculul pierderilor. Acest lucru va fi făcut mai târziu de data collatorul nostru, deoarece aplicăm padding dinamic, dar dacă utilizați padding aici, ar trebui să adaptați funcția de preprocesare pentru a seta toate labelurile care corespund simbolului de padding la `-100`.

Acum putem aplica această preprocesare dintr-o singură dată pe toate diviziunile datasetului nostru:

    tokenized_datasets = split_datasets.map(
        preprocess_function,
        batched=True,
        remove_columns=split_datasets["train"].column_names,
    )

Acum că datele au fost preprocesate, suntem pregătiți să facem fine-tune modelul nostru preantrenat!

##
Fine-tuningul modelului cu API-ul Trainer

Codul real care utilizează `Trainer` va fi același ca înainte, cu o singură mică schimbare: folosim aici un `Seq2SeqTrainer`, care este o subclasă a `Trainer` care ne va permite să ne ocupăm în mod corespunzător de evaluare, folosind metoda `generate()` pentru a prezice rezultatele din inputuri. Vom analiza acest aspect mai în detaliu atunci când vom vorbi despre calculul metricelor.

În primul rând, avem nevoie de un model pe care să îl aplicăm fine-tuningul. Vom utiliza API-ul obișnuit `AutoModel`:

    from transformers import AutoModelForSeq2SeqLM

    model = AutoModelForSeq2SeqLM.from_pretrained(model_checkpoint)

Rețineți că de data aceasta folosim un model care a fost antrenat pe o sarcină de traducere și care poate fi utilizat deja, astfel încât nu există niciun avertisment cu privire la weighturile lipsă sau la cele nou inițializate.

###
Data collation

Vom avea nevoie de un data collator care să se ocupe de paddingul pentru batching-ul dinamic. Nu putem folosi un `DataCollatorWithPadding` ca în Capitolul 3 în acest caz, pentru că acesta doar face padding inputurilor (input Is, attention mask și token type IDs). Labelurile noastre ar trebui, de asemenea, să fie padded la lungimea maximă întâlnită în labeluri. Și, așa cum am menționat anterior, valoarea de padding utilizată pentru a face padding labelurilor ar trebui să fie `-100` și nu padding tokenul tokenizerului, pentru a ne asigura că aceste valori de padding sunt ignorate în calculul pierderilor.

Toate acestea sunt realizate de un `DataCollatorForSeq2Seq`. Ca și `DataCollatorWithPadding`, acesta preia `tokenizer` utilizat pentru preprocesarea inputurilor, dar preia și `modelul`. Acest lucru se datorează faptului că acest data collator va fi, de asemenea, responsabil de pregătirea ID-urilor de input ale decoderului, care sunt versiuni schimbate ale labelurilor cu un token special la început. Deoarece această schimbare se face ușor diferit pentru diferite arhitecturi, `DataCollatorForSeq2Seq` trebuie să cunoască obiectul `model`:

    from transformers import DataCollatorForSeq2Seq

    data_collator = DataCollatorForSeq2Seq(tokenizer, model=model)

Pentru a testa acest lucru pe câteva sampleuri, îl apelăm doar pe o listă de sampleuri din setul nostru de antrenare tokenizat:

    batch = data_collator([tokenized_datasets["train"][i] for i in range(1, 3)])
    batch.keys()

    dict_keys(['attention_mask', 'input_ids', 'labels', 'decoder_input_ids'])

Putem verifica dacă labelurile noastre au fost padded la lungimea maximă a batchului, folosind `-100`:

    batch["labels"]

    tensor([[  577,  5891,     2,  3184,    16,  2542,     5,  1710,     0,  -100,
              -100,  -100,  -100,  -100,  -100,  -100],
            [ 1211,     3,    49,  9409,  1211,     3, 29140,   817,  3124,   817,
               550,  7032,  5821,  7907, 12649,     0]])

De asemenea, putem arunca o privire la ID-urile de input ale decoderului, pentru a vedea că acestea sunt versiuni schimbate ale labelurilor:

    batch["decoder_input_ids"]

    tensor([[59513,   577,  5891,     2,  3184,    16,  2542,     5,  1710,     0,
             59513, 59513, 59513, 59513, 59513, 59513],
            [59513,  1211,     3,    49,  9409,  1211,     3, 29140,   817,  3124,
               817,   550,  7032,  5821,  7907, 12649]])

Iată labelurile pentru primul și al doilea element din datasetul nostru:

    for i in range(1, 3):
        print(tokenized_datasets["train"][i]["labels"])

    [577, 5891, 2, 3184, 16, 2542, 5, 1710, 0]
    [1211, 3, 49, 9409, 1211, 3, 29140, 817, 3124, 817, 550, 7032, 5821, 7907, 12649, 0]

Vom transmite acest `data_collator` către `Seq2SeqTrainer`. În continuare, să aruncăm o privire la metrice.

###
Metrice

Caracteristica pe care `Seq2SeqTrainer` o adaugă superclasei sale `Trainer` este capacitatea de a utiliza metoda `generate()` în timpul evaluării sau predicției. În timpul antrenării, modelul va utiliza `decoder_input_ids` cu un attention mask care asigură că nu utilizează tokenii de după tokenul pe care încearcă să îl prezică, pentru a accelera antrenarea. În timpul inferenței, nu le vom putea utiliza deoarece nu vom avea labeluri, deci este o idee bună să ne evaluăm modelul cu aceeași configurație.

După cum am văzut în Capitolul 1, decoderul realizează inferența prin prezicerea tokenilor unul câte unul - lucru care este implementat behind the scenes în 🤗 Transformers prin metoda `generate()`. `Seq2SeqTrainer` ne va permite să folosim această metodă pentru evaluare dacă setăm `predict_with_generate=True`.

Metricele tradiționale utilizate pentru traducere sunt scorul BLEU, introdus în un articol din 2002 de Kishore Papineni et al. Scorul BLEU evaluează cât de apropiate sunt traducerile de labelurile lor. Acesta nu măsoară inteligibilitatea sau corectitudinea gramaticală a rezultatelor generate de model, ci utilizează reguli statistice pentru a se asigura că toate cuvintele din rezultatele generate apar și în targets. În plus, există reguli care penalizează repetițiile acelorași cuvinte dacă acestea nu sunt repetate și în targets(pentru a evita ca modelul să producă propoziții de tipul `"the the the the the the"`) și să producă propoziții care sunt mai scurte decât cele din targets(pentru a evita ca modelul să producă propoziții de tipul `"the"`).

Un punct slab al BLEU este că se așteaptă ca textul să fie deja tokenizat, ceea ce face dificilă compararea scorurilor între modele care utilizează tokenizere diferite. În schimb, cea mai frecvent utilizată măsură pentru evaluarea comparativă a modelelor de traducere este SacreBLEU, care abordează acest punct slab (și altele) prin standardizarea etapei de tokenizare. Pentru a utiliza această metrică, trebuie mai întâi să instalăm biblioteca SacreBLEU:

    !pip install sacrebleu

Îl putem încărca apoi prin `evaluate.load()` așa cum am făcut în Capitolul 3:

    import evaluate

    metric = evaluate.load("sacrebleu")

Această metrică va lua texte ca inputuri și targeturi. Este conceput pentru a lua mai multe obiective acceptabile, deoarece există adesea mai multe traduceri acceptabile ale aceleiași propoziții - datasetul pe care îl folosim oferă doar una, dar nu este neobișnuit în NLP să găsim dataseturi care oferă mai multe propoziții ca labeluri. Deci, predicțiile ar trebui să fie o listă de propoziții, dar referințele ar trebui să fie o listă de liste de propoziții.

Hai să încercăm acest exemplu:

    predictions = [
        "This plugin lets you translate web pages between several languages automatically."
    ]
    references = [
        [
            "This plugin allows you to automatically translate web pages between several languages."
        ]
    ]
    metric.compute(predictions=predictions, references=references)

    {'score': 46.750469682990165,
     'counts': [11, 6, 4, 3],
     'totals': [12, 11, 10, 9],
     'precisions': [91.67, 54.54, 40.0, 33.33],
     'bp': 0.9200444146293233,
     'sys_len': 12,
     'ref_len': 13}

Se obține un scor BLEU de 46,75, ceea ce este destul de bine - ca referință, modelul original Transformer din lucrarea “Attention Is All You Need” a obținut un scor BLEU de 41,8 la o sarcină similară de traducere între engleză și franceză! (Pentru mai multe informații despre parametrii individuali, precum `counts` și `bp`, consultați repositoriul SacreBLEU.) Pe de altă parte, dacă încercăm cu cele două tipuri de predicții proaste (multe repetări sau prea scurte) care rezultă adesea din modelele de traducere, vom obține scoruri BLEU destul de proaste:

    predictions = ["This This This This"]
    references = [
        [
            "This plugin allows you to automatically translate web pages between several languages."
        ]
    ]
    metric.compute(predictions=predictions, references=references)

    {'score': 1.683602693167689,
     'counts': [1, 0, 0, 0],
     'totals': [4, 3, 2, 1],
     'precisions': [25.0, 16.67, 12.5, 12.5],
     'bp': 0.10539922456186433,
     'sys_len': 4,
     'ref_len': 13}

    predictions = ["This plugin"]
    references = [
        [
            "This plugin allows you to automatically translate web pages between several languages."
        ]
    ]
    metric.compute(predictions=predictions, references=references)

    {'score': 0.0,
     'counts': [2, 1, 0, 0],
     'totals': [2, 1, 0, 0],
     'precisions': [100.0, 100.0, 0.0, 0.0],
     'bp': 0.004086771438464067,
     'sys_len': 2,
     'ref_len': 13}

Scorul poate varia de la 0 la 100, iar mai mare înseamnă un scor mai bun.

Pentru a trece de la rezultatele modelului la textele pe care metricele le pot utiliza, vom utiliza metoda `tokenizer.batch_decode()`. Trebuie doar să curățăm toate `-100` din labeluri(tokenizerul va face automat același lucru pentru padding token):

    import numpy as np

    def compute_metrics(eval_preds):
        preds, labels = eval_preds
        # In case the model returns more than the prediction logits
        if isinstance(preds, tuple):
            preds = preds[0]

        decoded_preds = tokenizer.batch_decode(preds, skip_special_tokens=True)

        # Replace -100s in the labels as we can't decode them
        labels = np.where(labels != -100, labels, tokenizer.pad_token_id)
        decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)

        # Some simple post-processing
        decoded_preds = [pred.strip() for pred in decoded_preds]
        decoded_labels = [[label.strip()] for label in decoded_labels]

        result = metric.compute(predictions=decoded_preds, references=decoded_labels)
        return {"bleu": result["score"]}

Acum că am făcut acest lucru, suntem gata să facem fine-tune modelului!

###
Fine-tuningul modelului

Primul pas pe care trebuie să îl faceți este să vă conectați la Hugging Face, astfel încât să vă puteți încărca rezultatele în Model Hub. Există o funcție convenabilă care vă ajuta cu acest lucru într-un notebook:

    from huggingface_hub import notebook_login

    notebook_login()

Aceasta va afișa un widget în care puteți introduce datele voastre 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 `Seq2SeqTrainingArguments`. Ca și pentru `Trainer`, folosim o subclasă a `TrainingArguments` care conține câteva câmpuri suplimentare:

    from transformers import Seq2SeqTrainingArguments

    args = Seq2SeqTrainingArguments(
        f"marian-finetuned-kde4-en-to-fr",
        evaluation_strategy="no",
        save_strategy="epoch",
        learning_rate=2e-5,
        per_device_train_batch_size=32,
        per_device_eval_batch_size=64,
        weight_decay=0.01,
        save_total_limit=3,
        num_train_epochs=3,
        predict_with_generate=True,
        fp16=True,
        push_to_hub=True,
    )

În afară de hiperparametrii obișnuiți (cum ar fi rata de învățare, numărul de epoci, dimensiunea batch-ului și o anumită scădere a weighturilor), aici sunt câteva schimbări în comparație cu ceea ce am văzut în secțiunile anterioare:

- Nu setăm nicio evaluare periodică, deoarece evaluarea durează; ne vom evalua modelul doar o dată înainte și după antrenare.
- Setăm `fp16=True`, care accelerează antrenarea pe GPU-urile moderne.
- Setăm `predict_with_generate=True`, așa cum am discutat mai sus.
- Utilizăm `push_to_hub=True` pentru a încărca modelul în Hub la sfârșitul fiecărei epoci.

Rețineți că puteți specifica numele complet al 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/marian-finetuned-kde4-en-to-fr"` la `Seq2SeqTrainingArguments`. În mod implicit, repositoriul utilizat va fi în namespaceul vostru și denumit după output directory-ul pe care l-ați stabilit, deci în cazul nostru va fi `"sgugger/marian-finetuned-kde4-en-to-fr"` (care este modelul la care am făcut legătura la începutul acestei secțiuni).

💡 Dacă output directory-ul pe care îl utilizați există deja, acesta trebuie să fie o clonă locală a repositoriului către care doriți să faceți push. În caz contrar, veți primi o eroare atunci când vă definiți `Seq2SeqTrainer` și va trebui să stabiliți un nume nou.

În final, transmitem totul către `Seq2SeqTrainer`:

    from transformers import Seq2SeqTrainer

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

Înainte de antrenare, ne vom uita mai întâi la scorul obținut de modelul nostru, pentru a verifica dacă nu înrăutățim lucrurile prin fine-tuning. Această comandă va dura ceva timp, așa că puteți lua o cafea sau două în timp ce se execută:

    trainer.evaluate(max_length=max_length)

    {'eval_loss': 1.6964408159255981,
     'eval_bleu': 39.26865061007616,
     'eval_runtime': 965.8884,
     'eval_samples_per_second': 21.76,
     'eval_steps_per_second': 0.341}

Un scor BLEU de 39 nu este prea rău, ceea ce reflectă faptul că modelul nostru este deja bun la traducerea propozițiilor din engleză în franceză.

Urmează antrenarea, care va necesita, de asemenea, puțin timp:

    trainer.train()

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

Odată ce formarea este terminată, evaluăm din nou modelul nostru - sperăm că vom vedea o îmbunătățire a scorului BLEU!

    trainer.evaluate(max_length=max_length)

    {'eval_loss': 0.8558505773544312,
     'eval_bleu': 52.94161337775576,
     'eval_runtime': 714.2576,
     'eval_samples_per_second': 29.426,
     'eval_steps_per_second': 0.461,
     'epoch': 3.0}

Aceasta este o îmbunătățire de aproape 14 puncte, ceea ce este minunat.

În final, folosim metoda `push_to_hub()` pentru a ne asigura că încărcăm cea mai recentă versiune a modelului. De asemenea, `Trainer` redactează un model card cu toate rezultatele evaluării și o încarcă. Acest model card conține metadate care ajută Model Hub să aleagă widgetul pentru demonstrația de inferență. De obicei, nu este nevoie să se menționeze nimic, deoarece poate deduce widgetul potrivit din clasa modelului, dar în acest caz, aceeași clasă de model poate fi utilizată pentru toate tipurile de probleme de tip sequence-to-sequence, așa că specificăm că este un model de traducere:

    trainer.push_to_hub(tags="translation", commit_message="Training complete")

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

    'https://huggingface.co/sgugger/marian-finetuned-kde4-en-to-fr/commit/3601d621e3baae2bc63d3311452535f8f58f6ef3'

În această etapă, puteți utiliza widget-ul de inferență de pe Model Hub pentru a vă testa modelul și pentru a-l partaja cu prietenii. Ați făcut fine-tune cu succes un model pentru o sarcină de traducere - felicitări!

Dacă doriți să pătrundeți puțin mai adânc în bucla de antrenare, vă vom arăta acum să faceți același lucru folosind 🤗 Accelerate.

##
O buclă de antrenare personalizată

Să aruncăm acum o privire la bucla de antrenare completă, astfel încât să puteți personaliza cu ușurință părțile de care aveți nevoie. Va arăta foarte asemănător cu ceea ce am făcut în secțiunea 2 și capitolul 3.

###
Pregătirea tuturor lucrulilor pentru antrenare

Ați văzut toate acestea de câteva ori până acum, așa că vom trece prin cod destul de repede. Mai întâi vom construi `DataLoader`s din dataseturile noastre, după ce vom seta dataseturile în formatul `"torch"` astfel încât să obținem tensori PyTorch:

    from torch.utils.data import DataLoader

    tokenized_datasets.set_format("torch")
    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, reinițializăm modelul, pentru a ne asigura că nu continuăm fine-tuningul de dinainte, ci pornim din nou de la modelul preantrenat:

    model = AutoModelForSeq2SeqLM.from_pretrained(model_checkpoint)

Pe urmă vom avea nevoie de un optimizator:

    from transformers import AdamW

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

Odată ce avem toate aceste obiecte, le putem trimite la metoda `accelerator.prepare()`. Amintiți-vă că, dacă doriți să vă antrenați pe un TPU într-un notebook Colab, va trebui să mutați tot acest cod într-o funcție de antrenare, care nu ar trebui să execute nicio celulă care inițializează un `Accelerator`.

    from accelerate import Accelerator

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

Acum că am trimis `train_dataloader` la `accelerator.prepare()`, putem utiliza lungimea acestuia pentru a calcula numărul de pași de antrenare. Amintiți-vă că trebuie să facem acest lucru întotdeauna după pregătirea data loaderului, deoarece metoda respectivă va modifica lungimea `DataLoader`. Utilizăm un program liniar clasic 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 Hub, dacă nu sunteți deja conectat. Vom determina numele repositoriului pornind de la ID-ul modelului pe care dorim să îl atribuim modelului nostru (nu ezitați să înlocuiți `repo_name` cu propria alegere; acesta 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 = "marian-finetuned-kde4-en-to-fr-accelerate"
    repo_name = get_full_repo_name(model_name)
    repo_name

    'sgugger/marian-finetuned-kde4-en-to-fr-accelerate'

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

    output_dir = "marian-finetuned-kde4-en-to-fr-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.

###
Bucla de antrenare

Acum suntem pregătiți să scriem bucla de antrenare completă. Pentru a simplifica partea de evaluare, definim această funcție `postprocess()` care preia predicțiile și labelurile și le convertește în liste de stringuri pe care obiectul nostru `metric` le va aștepta:

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

        decoded_preds = tokenizer.batch_decode(predictions, skip_special_tokens=True)

        # Replace -100 in the labels as we can't decode them.
        labels = np.where(labels != -100, labels, tokenizer.pad_token_id)
        decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)

        # Some simple post-processing
        decoded_preds = [pred.strip() for pred in decoded_preds]
        decoded_labels = [[label.strip()] for label in decoded_labels]
        return decoded_preds, decoded_labels

Bucla de antrenare seamănă foarte mult cu cele din secțiunea 2 și capitolul 3, cu câteva diferențe în partea de evaluare - așa că hai să ne concentrăm pe asta!

Primul lucru pe care îl putem remarca este că folosim metoda `generate()` pentru a calcula predicțiile, dar aceasta este o metodă a modelul nostru de bază, nu pe modelul wrapped 🤗 Accelerate creat în metoda `prepare()`. De aceea, mai întâi facem unwrap modelului, apoi apelăm această metodă.

Al doilea lucru este că, la fel ca în cazul token classification, este posibil ca două procese să fi făcut padding inputurilor și labelurilor cu forme diferite, așa că folosim `accelerator.pad_across_processes()` pentru a face predicțiile și labelurile de aceeași formă înainte de a apela metoda `gather()`. Dacă nu facem acest lucru, evaluarea va da o eroare sau se va bloca pentru totdeauna.

    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 tqdm(eval_dataloader):
            with torch.no_grad():
                generated_tokens = accelerator.unwrap_model(model).generate(
                    batch["input_ids"],
                    attention_mask=batch["attention_mask"],
                    max_length=128,
                )
            labels = batch["labels"]

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

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

            decoded_preds, decoded_labels = postprocess(predictions_gathered, labels_gathered)
            metric.add_batch(predictions=decoded_preds, references=decoded_labels)

        results = metric.compute()
        print(f"epoch {epoch}, BLEU score: {results['score']:.2f}")

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

    epoch 0, BLEU score: 53.47
    epoch 1, BLEU score: 54.24
    epoch 2, BLEU score: 54.44

Odată făcut acest lucru, ar trebui să aveți un model care are rezultate destul de asemănătoare cu cel antrenat cu `Seq2SeqTrainer`. Îl puteți verifica pe cel pe care l-am antrenat folosind acest cod la [huggingface-course/marian-finetuned-kde4-en-to-fr-accelerate] (https://huggingface.co/huggingface-course/marian-finetuned-kde4-en-to-fr-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 modelului fine-tuned

V-am arătat deja cum puteți utiliza modelul pe căruia i-am aplicat fine-tune pe Model Hub cu widgetul de inferență. Pentru a-l utiliza la nivel local într-un `pipeline`, trebuie doar să specificăm identificatorul de model corespunzător:

    from transformers import pipeline

    # Replace this with your own checkpoint
    model_checkpoint = "huggingface-course/marian-finetuned-kde4-en-to-fr"
    translator = pipeline("translation", model=model_checkpoint)
    translator("Default to expanded threads")

    [{'translation_text': 'Par défaut, développer les fils de discussion'}]

Așa cum era de așteptat, modelul nostru preantrenat și-a adaptat cunoștințele la corpusul pe care i-am făcut fine-tune și, în loc să lase cuvântul englezesc “threads”, acum îl traduce în versiunea oficială franceză. Același lucru este valabil și pentru “plugin”:

    translator(
        "Unable to import %1 using the OFX importer plugin. This file is not the correct format."
    )

    [{'translation_text': "Impossible d'importer %1 en utilisant le module externe d'importation OFX. Ce fichier n'est pas le bon format."}]

Un alt exemplu excelent de adaptare a domeniului!

✏️ E rândul tău! Care este rezultatul modelului pentru sampleul cuvântul “email” pe care l-ai identificat mai devreme?

< > Update on GitHub