Pytorch
TensorFlow

#
Răspuns la întrebări

Este timpul să analizăm răspunsul la întrebări! Această sarcină are mai multe variante, dar cea pe care ne vom concentra în această secțiune se numește răspuns extractiv la întrebări. Aceasta presupune formularea de întrebări cu privire la un document și identificarea răspunsurilor ca intervale de text în documentul în sine.

Vom face fine-tuning unui-model BERT pe [datasetul SQuAD] (https://rajpurkar.github.io/SQuAD-explorer/), care constă din întrebări adresate de mulțimea de lucrători pe un set de articole Wikipedia. Acest lucru ne va oferi un model capabil să calculeze predicții precum aceasta:

Aceasta este de fapt o prezentare a modelului care a fost antrenat și încărcat în Hub folosind codul prezentat în această secțiune. Puteți să-l găsiți și să verificați predicțiile aici.

💡 Modelele bazate doar pe encoding, cum ar fi BERT, tind să fie foarte bune la extragerea răspunsurilor la întrebări de tip factoid, cum ar fi “Cine a inventat arhitectura Transformer?”, dar nu se descurcă prea bine atunci când primesc întrebări deschise, cum ar fi “De ce este cerul albastru?” În aceste cazuri mai dificile, modelele encoder-decoder precum T5 și BART sunt utilizate de obicei pentru a sintetiza informațiile într-un mod destul de similar cu rezumarea textului. Dacă sunteți interesat de acest tip de răspuns generativ la întrebări, vă recomandăm să consultați demo-ul nostru bazat pe datasetul ELI5.

##
Pregătirea datelor

Datasetul care este cel mai utilizat ca referință academică pentru răspunderea extractivă la întrebări este SQuAD, deci acesta este cel pe care îl vom utiliza aici. Există, de asemenea, un benchmark mai dificil SQuAD v2, care include întrebări care nu au un răspuns. Atât timp cât propriul dataset conține o coloană pentru contexte, o coloană pentru întrebări și o coloană pentru răspunsuri, ar trebui să puteți adapta pașii de mai jos.

###
Datasetul SQuD

Ca de obicei, putem descărca și stoca în cache datasetul într-un singur pas datorită funcției `load_dataset()`:

    from datasets import load_dataset

    raw_datasets = load_dataset("squad")

Ne putem uita apoi la acest obiect pentru a afla mai multe despre datasetul SQuAD:

    raw_datasets

    DatasetDict({
        train: Dataset({
            features: ['id', 'title', 'context', 'question', 'answers'],
            num_rows: 87599
        })
        validation: Dataset({
            features: ['id', 'title', 'context', 'question', 'answers'],
            num_rows: 10570
        })
    })

Se pare că avem tot ce ne trebuie cu câmpurile `context`, `question` și `answers`, așa că să le afișăm pentru primul element al datasetului nostru de antrenare:

    print("Context: ", raw_datasets["train"][0]["context"])
    print("Question: ", raw_datasets["train"][0]["question"])
    print("Answer: ", raw_datasets["train"][0]["answers"])

    Context: 'Architecturally, the school has a Catholic character. Atop the Main Building\'s gold dome is a golden statue of the Virgin Mary. Immediately in front of the Main Building and facing it, is a copper statue of Christ with arms upraised with the legend "Venite Ad Me Omnes". Next to the Main Building is the Basilica of the Sacred Heart. Immediately behind the basilica is the Grotto, a Marian place of prayer and reflection. It is a replica of the grotto at Lourdes, France where the Virgin Mary reputedly appeared to Saint Bernadette Soubirous in 1858. At the end of the main drive (and in a direct line that connects through 3 statues and the Gold Dome), is a simple, modern stone statue of Mary.'
    Question: 'To whom did the Virgin Mary allegedly appear in 1858 in Lourdes France?'
    Answer: {'text': ['Saint Bernadette Soubirous'], 'answer_start': [515]}

Câmpurile `context` și `question` sunt foarte simplu de utilizat. Câmpul `answers` este un pic mai complicat, deoarece conține un dicționar cu două câmpuri care sunt ambele liste. Acesta este formatul care va fi așteptat de metrica `squad` în timpul evaluării; dacă utilizați propriile date, nu trebuie neapărat să vă faceți griji cu privire la plasarea răspunsurilor în același format. Câmpul `text` este destul de evident, iar câmpul `answer_start` conține indicele caracterului de început al fiecărui răspuns din context.

În timpul antrenamentului, există un singur răspuns posibil. Putem verifica acest lucru folosind metoda `Dataset.filter()`:

    raw_datasets["train"].filter(lambda x: len(x["answers"]["text"]) != 1)

    Dataset({
        features: ['id', 'title', 'context', 'question', 'answers'],
        num_rows: 0
    })

Cu toate acestea, pentru evaluare, există mai multe răspunsuri posibile pentru fiecare sample, care pot fi identice sau diferite:

    print(raw_datasets["validation"][0]["answers"])
    print(raw_datasets["validation"][2]["answers"])

    {'text': ['Denver Broncos', 'Denver Broncos', 'Denver Broncos'], 'answer_start': [177, 177, 177]}
    {'text': ['Santa Clara, California', "Levi's Stadium", "Levi's Stadium in the San Francisco Bay Area at Santa Clara, California."], 'answer_start': [403, 355, 355]}

Nu ne vom aprofunda în scriptul de evaluare, deoarece totul va fi încorporat de o metrică 🤗 Datasets pentru noi, dar versiunea scurtă este că unele dintre întrebări au mai multe răspunsuri posibile, iar acest script va compara un răspuns prezis cu toate răspunsurile acceptabile și va lua cel mai bun scor. Dacă ne uităm la sampleul de la indexul 2, de exemplu:

    print(raw_datasets["validation"][2]["context"])
    print(raw_datasets["validation"][2]["question"])

    'Super Bowl 50 was an American football game to determine the champion of the National Football League (NFL) for the 2015 season. The American Football Conference (AFC) champion Denver Broncos defeated the National Football Conference (NFC) champion Carolina Panthers 24–10 to earn their third Super Bowl title. The game was played on February 7, 2016, at Levi\'s Stadium in the San Francisco Bay Area at Santa Clara, California. As this was the 50th Super Bowl, the league emphasized the "golden anniversary" with various gold-themed initiatives, as well as temporarily suspending the tradition of naming each Super Bowl game with Roman numerals (under which the game would have been known as "Super Bowl L"), so that the logo could prominently feature the Arabic numerals 50.'
    'Where did Super Bowl 50 take place?'

putem vedea că răspunsul poate fi într-adevăr una dintre cele trei posibilități pe care le-am văzut anterior.

###
Procesarea datelor de antrenare

Să începem cu preprocesarea datelor de antrenare. Partea dificilă va fi generarea labelurilor pentru răspunsul la întrebare, care vor fi pozițiile de început și de sfârșit ale tokenilor corespunzătoare răspunsului în context.

Dar să nu ne grăbim. În primul rând, trebuie să convertim textul din datele de intrare în ID-uri pe care modelul să le poată înțelege, utilizând un tokenizer:

    from transformers import AutoTokenizer

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

După cum am menționat anterior, vom face fine-tune unui model BERT, dar puteți utiliza orice alt tip de model, atâta timp cât are implementat un tokenizer rapid. 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

Putem transmite împreună întrebarea și contextul către tokenizerul nostru, iar acesta va introduce în mod corespunzător tokenii speciali pentru a forma o propoziție ca aceasta:

    [CLS] question [SEP] context [SEP]

Hai să verificăm de două ori:

    context = raw_datasets["train"][0]["context"]
    question = raw_datasets["train"][0]["question"]

    inputs = tokenizer(question, context)
    tokenizer.decode(inputs["input_ids"])

    '[CLS] To whom did the Virgin Mary allegedly appear in 1858 in Lourdes France? [SEP] Architecturally, '
    'the school has a Catholic character. Atop the Main Building\'s gold dome is a golden statue of the Virgin '
    'Mary. Immediately in front of the Main Building and facing it, is a copper statue of Christ with arms '
    'upraised with the legend " Venite Ad Me Omnes ". Next to the Main Building is the Basilica of the Sacred '
    'Heart. Immediately behind the basilica is the Grotto, a Marian place of prayer and reflection. It is a '
    'replica of the grotto at Lourdes, France where the Virgin Mary reputedly appeared to Saint Bernadette '
    'Soubirous in 1858. At the end of the main drive ( and in a direct line that connects through 3 statues '
    'and the Gold Dome ), is a simple, modern stone statue of Mary. [SEP]'

Labelurile vor fi apoi indexul tokenilor care încep și termină răspunsul, iar modelul va fi însărcinat să prezică un logit de început și de sfârșit pentru fiecare token din intrare, labelurile teoretice fiind următoarele:

În acest caz, contextul nu este prea lung, dar unele dintre exemplele din dataset au contexte foarte lungi care vor depăși lungimea maximă pe care am stabilit-o (care este de 384 în acest caz). După cum am văzut în Capitolul 6 când am explorat elementele interne ale pipelineului `question-answering`, vom trata contextele lungi prin crearea mai multor caracteristici de antrenare dintr-un sample din datasetul nostru, cu un sliding window între ele.

Pentru a vedea cum funcționează acest lucru folosind exemplul curent, putem limita lungimea la 100 și putem utiliza un sliding window de 50 de tokeni. Vă reamintim că folosim:

- `max_length` pentru a stabili lungimea maximă (aici 100)
- `truncation="only_second"` pentru a trunchia contextul (care este în poziția a doua) atunci când întrebarea cu contextul său este prea lungă
- `stride` pentru a seta numărul de tokeni care se suprapun între două bucăți succesive (aici 50)
- `return_overflowing_tokens=True` pentru ca tokenizerul să știe că dorim tokenii care se suprapun

    inputs = tokenizer(
        question,
        context,
        max_length=100,
        truncation="only_second",
        stride=50,
        return_overflowing_tokens=True,
    )

    for ids in inputs["input_ids"]:
        print(tokenizer.decode(ids))

    '[CLS] To whom did the Virgin Mary allegedly appear in 1858 in Lourdes France? [SEP] Architecturally, the school has a Catholic character. Atop the Main Building\'s gold dome is a golden statue of the Virgin Mary. Immediately in front of the Main Building and facing it, is a copper statue of Christ with arms upraised with the legend " Venite Ad Me Omnes ". Next to the Main Building is the Basilica of the Sacred Heart. Immediately behind the basi [SEP]'
    '[CLS] To whom did the Virgin Mary allegedly appear in 1858 in Lourdes France? [SEP] the Main Building and facing it, is a copper statue of Christ with arms upraised with the legend " Venite Ad Me Omnes ". Next to the Main Building is the Basilica of the Sacred Heart. Immediately behind the basilica is the Grotto, a Marian place of prayer and reflection. It is a replica of the grotto at Lourdes, France where the Virgin [SEP]'
    '[CLS] To whom did the Virgin Mary allegedly appear in 1858 in Lourdes France? [SEP] Next to the Main Building is the Basilica of the Sacred Heart. Immediately behind the basilica is the Grotto, a Marian place of prayer and reflection. It is a replica of the grotto at Lourdes, France where the Virgin Mary reputedly appeared to Saint Bernadette Soubirous in 1858. At the end of the main drive ( and in a direct line that connects through 3 [SEP]'
    '[CLS] To whom did the Virgin Mary allegedly appear in 1858 in Lourdes France? [SEP]. It is a replica of the grotto at Lourdes, France where the Virgin Mary reputedly appeared to Saint Bernadette Soubirous in 1858. At the end of the main drive ( and in a direct line that connects through 3 statues and the Gold Dome ), is a simple, modern stone statue of Mary. [SEP]'

După cum se poate observa, exemplul nostru a fost împărțit în patru inputuri, fiecare dintre acestea conținând întrebarea și o parte din context. Rețineți că răspunsul la întrebare (“Bernadette Soubirous”) apare doar în al treilea și ultimul input, astfel încât, prin tratarea contextelor lungi în acest mod, vom crea câteva exemple de antrenament în care răspunsul nu este inclus în context. Pentru aceste exemple, labelurile vor fi `start_position = end_position = 0` (deci vom prezice tokenul `[CLS]`). Vom seta aceste labeluri și în cazul nefericit în care răspunsul a fost trunchiat, astfel încât avem doar începutul (sau sfârșitul) acestuia. Pentru exemplele în care răspunsul este complet în context, labelurile vor fi indicele tokenului în care începe răspunsul și indicele tokenului în care se termină răspunsul.

Datasetul ne oferă caracterul de început al răspunsului în context, iar prin adăugarea lungimii răspunsului, putem găsi caracterul de sfârșit în context. Pentru a le corela cu indicii tokenilor, va trebui să folosim offset mapping pe care le-am studiat în Capitolul 6. Putem face ca tokenizatorul nostru să le returneze trecând `return_offsets_mapping=True`:

    inputs = tokenizer(
        question,
        context,
        max_length=100,
        truncation="only_second",
        stride=50,
        return_overflowing_tokens=True,
        return_offsets_mapping=True,
    )
    inputs.keys()

    dict_keys(['input_ids', 'token_type_ids', 'attention_mask', 'offset_mapping', 'overflow_to_sample_mapping'])

După cum putem vedea, primim înapoi ID-urile obișnuite de intrare, ID-urile tipului de token și attention maskul, precum și offset mapping necesar și o cheie suplimentară, `overflow_to_sample_mapping`. Valoarea corespunzătoare ne va fi de folos atunci când vom tokeniza mai multe texte în același timp (ceea ce ar trebui să facem pentru a beneficia de faptul că tokenizerul nostru este susținut de Rust). Deoarece un sample poate oferi mai multe caracteristici, aceasta mapează fiecare caracteristică la exemplul din care provine. Deoarece aici am tokenizat un singur exemplu, obținem o listă de `0`:

    inputs["overflow_to_sample_mapping"]

    [0, 0, 0, 0]

Dar dacă vom tokeniza mai multe exemple, acest lucru va deveni mai util:

    inputs = tokenizer(
        raw_datasets["train"][2:6]["question"],
        raw_datasets["train"][2:6]["context"],
        max_length=100,
        truncation="only_second",
        stride=50,
        return_overflowing_tokens=True,
        return_offsets_mapping=True,
    )

    print(f"The 4 examples gave {len(inputs['input_ids'])} features.")
    print(f"Here is where each comes from: {inputs['overflow_to_sample_mapping']}.")

    'The 4 examples gave 19 features.'
    'Here is where each comes from: [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3].'

După cum se poate observa, primele trei exemple (la indicii 2, 3 și 4 din setul de antrenare) au dat fiecare câte patru caracteristici, iar ultimul exemplu (la indicele 5 din setul de antrenare) a dat 7 caracteristici.

Aceste informații vor fi utile pentru a corela fiecare caracteristică obținută cu labeul corespunzător. După cum am menționat anterior, aceste labelurile sunt:

- `(0, 0)` dacă răspunsul nu se află în intervalul corespunzător al contextului
- `(start_position, end_position)` dacă răspunsul se află în intervalul corespunzător al contextului, cu `start_position` fiind indicele tokenului (în ID-urile de intrare) la începutul răspunsului și `end_position` fiind indicele tokenului (în ID-urile de intrare) unde se termină răspunsul

Pentru a determina care dintre acestea este cazul și, dacă este relevant, pozițiile tokenilor, vom găsi mai întâi indicii care încep și termină contextul în ID-urile de intrare. Am putea folosi ID-urile tipului de token pentru a face acest lucru, dar deoarece acestea nu există neapărat pentru toate modelele (DistilBERT nu le solicită, de exemplu), vom folosi în schimb metoda `sequence_ids()` a `BatchEncoding` pe care tokenizerul nostru o returnează.

Odată ce avem indicii tokenilor, ne uităm la offseturile corespunzătoare, care sunt tupeluri de două numere întregi reprezentând intervalul de caractere din contextul original. Astfel, putem detecta dacă bucățica de context din această caracteristică începe după răspuns sau se termină înainte de începerea răspunsului (caz în care eticheta este `(0, 0)`). Dacă nu este cazul, facem o buclă pentru a găsi primul și ultimul token al răspunsului:

    answers = raw_datasets["train"][2:6]["answers"]
    start_positions = []
    end_positions = []

    for i, offset in enumerate(inputs["offset_mapping"]):
        sample_idx = inputs["overflow_to_sample_mapping"][i]
        answer = answers[sample_idx]
        start_char = answer["answer_start"][0]
        end_char = answer["answer_start"][0] + len(answer["text"][0])
        sequence_ids = inputs.sequence_ids(i)

        # Find the start and end of the context
        idx = 0
        while sequence_ids[idx] != 1:
            idx += 1
        context_start = idx
        while sequence_ids[idx] == 1:
            idx += 1
        context_end = idx - 1

        # If the answer is not fully inside the context, label is (0, 0)
        if offset[context_start][0] > start_char or offset[context_end][1] < end_char:
            start_positions.append(0)
            end_positions.append(0)
        else:
            # Otherwise it's the start and end token positions
            idx = context_start
            while idx <= context_end and offset[idx][0] <= start_char:
                idx += 1
            start_positions.append(idx - 1)

            idx = context_end
            while idx >= context_start and offset[idx][1] >= end_char:
                idx -= 1
            end_positions.append(idx + 1)

    start_positions, end_positions

    ([83, 51, 19, 0, 0, 64, 27, 0, 34, 0, 0, 0, 67, 34, 0, 0, 0, 0, 0],
     [85, 53, 21, 0, 0, 70, 33, 0, 40, 0, 0, 0, 68, 35, 0, 0, 0, 0, 0])

Să aruncăm o privire la câteva rezultate pentru a verifica dacă abordarea noastră este corectă. Pentru prima caracteristică găsim `(83, 85)` ca labeluri, așa că comparăm răspunsul teoretic cu intervalul decodat de tokeni de la 83 la 85 (inclusiv):

    idx = 0
    sample_idx = inputs["overflow_to_sample_mapping"][idx]
    answer = answers[sample_idx]["text"][0]

    start = start_positions[idx]
    end = end_positions[idx]
    labeled_answer = tokenizer.decode(inputs["input_ids"][idx][start : end + 1])

    print(f"Theoretical answer: {answer}, labels give: {labeled_answer}")

    'Theoretical answer: the Main Building, labels give: the Main Building'

Deci, asta e o potrivire! Acum să verificăm indexul 4, unde am setat labelurile la `(0, 0)`, ceea ce înseamnă că răspunsul nu se află în chunkul de context al acelei caracteristici:

    idx = 4
    sample_idx = inputs["overflow_to_sample_mapping"][idx]
    answer = answers[sample_idx]["text"][0]

    decoded_example = tokenizer.decode(inputs["input_ids"][idx])
    print(f"Theoretical answer: {answer}, decoded example: {decoded_example}")

    'Theoretical answer: a Marian place of prayer and reflection, decoded example: [CLS] What is the Grotto at Notre Dame? [SEP] Architecturally, the school has a Catholic character. Atop the Main Building\'s gold dome is a golden statue of the Virgin Mary. Immediately in front of the Main Building and facing it, is a copper statue of Christ with arms upraised with the legend " Venite Ad Me Omnes ". Next to the Main Building is the Basilica of the Sacred Heart. Immediately behind the basilica is the Grot [SEP]'

Într-adevăr, nu vedem răspunsul în interiorul contextului.

✏️ E rândul tău! Atunci când se utilizează arhitectura XLNet, paddingul este aplicat la stânga, iar întrebarea și contextul sunt schimbate. Adaptați tot codul pe care tocmai l-am văzut la arhitectura XLNet (și adăugați `padding=True`). Fiți conștienți de faptul că tokenul `[CLS]` ar putea să nu se afle la poziția 0 în cazul aplicării paddingului.

Acum că am văzut pas cu pas cum să preprocesăm datele de antrenare, le putem grupa într-o funcție pe care o vom aplica întregului dataset de antrenare. Vom umple fiecare caracteristică la lungimea maximă pe care am stabilit-o, deoarece majoritatea contextelor vor fi lungi (iar sampleurile corespunzătoare vor fi împărțite în mai multe caracteristici), astfel încât nu există niciun beneficiu real pentru aplicarea paddingului dinamic aici:

    max_length = 384
    stride = 128

    def preprocess_training_examples(examples):
        questions = [q.strip() for q in examples["question"]]
        inputs = tokenizer(
            questions,
            examples["context"],
            max_length=max_length,
            truncation="only_second",
            stride=stride,
            return_overflowing_tokens=True,
            return_offsets_mapping=True,
            padding="max_length",
        )

        offset_mapping = inputs.pop("offset_mapping")
        sample_map = inputs.pop("overflow_to_sample_mapping")
        answers = examples["answers"]
        start_positions = []
        end_positions = []

        for i, offset in enumerate(offset_mapping):
            sample_idx = sample_map[i]
            answer = answers[sample_idx]
            start_char = answer["answer_start"][0]
            end_char = answer["answer_start"][0] + len(answer["text"][0])
            sequence_ids = inputs.sequence_ids(i)

            # Find the start and end of the context
            idx = 0
            while sequence_ids[idx] != 1:
                idx += 1
            context_start = idx
            while sequence_ids[idx] == 1:
                idx += 1
            context_end = idx - 1

            # If the answer is not fully inside the context, label is (0, 0)
            if offset[context_start][0] > start_char or offset[context_end][1] < end_char:
                start_positions.append(0)
                end_positions.append(0)
            else:
                # Otherwise it's the start and end token positions
                idx = context_start
                while idx <= context_end and offset[idx][0] <= start_char:
                    idx += 1
                start_positions.append(idx - 1)

                idx = context_end
                while idx >= context_start and offset[idx][1] >= end_char:
                    idx -= 1
                end_positions.append(idx + 1)

        inputs["start_positions"] = start_positions
        inputs["end_positions"] = end_positions
        return inputs

Rețineți că am definit două constante pentru a determina lungimea maximă utilizată, precum și lungimea al sliding window, și că am adăugat o mică curățare înainte de tokenizare: unele dintre întrebările din datasetul SQuAD au spații suplimentare la început și la sfârșit care nu adaugă nimic (și ocupă spațiu atunci când sunt tokenizate dacă utilizați un model precum RoBERTa), așa că am eliminat aceste spații suplimentare.

Pentru a aplica această funcție întregului set de antrenare, folosim metoda `Dataset.map()` cu flagul `batched=True`. Acesta este necesar aici, deoarece modificăm lungimea datasetului (deoarece un exemplu poate oferi mai multe caracteristici de antrenare):

    train_dataset = raw_datasets["train"].map(
        preprocess_training_examples,
        batched=True,
        remove_columns=raw_datasets["train"].column_names,
    )
    len(raw_datasets["train"]), len(train_dataset)

    (87599, 88729)

După cum putem vedea, preprocesarea a adăugat aproximativ 1.000 de caracteristici. Setul nostru de antrenare este acum gata de utilizare - să trecem la preprocesarea setului de validare!

###
Procesarea datelor de validare

Preprocesarea datelor de validare va fi puțin mai ușoară, deoarece nu trebuie să generăm labeluri (cu excepția cazului în care dorim să calculăm o pierdere de validare, dar acest număr nu ne va ajuta să înțelegem cât de bun este modelul). Adevărata bucurie va fi să interpretăm predicțiile modelului în intervale ale contextului original. Pentru aceasta, va trebui doar să stocăm atât offset mappings, cât și o modalitate de a corela fiecare caracteristică creată cu exemplul original din care provine. Deoarece există o coloană ID în datasetul original, vom utiliza acel ID.

Singurul lucru pe care îl vom adăuga aici este o mică curățare a offset mappings. Acestea vor conține offseturi pentru întrebare și context, dar odată ajunși în etapa de postprocesare nu vom avea nicio modalitate de a ști care parte a ID-urilor de intrare corespunde contextului și care parte este întrebarea (metoda `sequence_ids()` pe care am folosit-o este disponibilă doar pentru ieșirea tokenizerului). Prin urmare, vom seta offseturile corespunzătoare întrebării la `None`:

    def preprocess_validation_examples(examples):
        questions = [q.strip() for q in examples["question"]]
        inputs = tokenizer(
            questions,
            examples["context"],
            max_length=max_length,
            truncation="only_second",
            stride=stride,
            return_overflowing_tokens=True,
            return_offsets_mapping=True,
            padding="max_length",
        )

        sample_map = inputs.pop("overflow_to_sample_mapping")
        example_ids = []

        for i in range(len(inputs["input_ids"])):
            sample_idx = sample_map[i]
            example_ids.append(examples["id"][sample_idx])

            sequence_ids = inputs.sequence_ids(i)
            offset = inputs["offset_mapping"][i]
            inputs["offset_mapping"][i] = [
                o if sequence_ids[k] == 1 else None for k, o in enumerate(offset)
            ]

        inputs["example_id"] = example_ids
        return inputs

Putem aplica această funcție pe întregul dataset de validare, ca și înainte:

    validation_dataset = raw_datasets["validation"].map(
        preprocess_validation_examples,
        batched=True,
        remove_columns=raw_datasets["validation"].column_names,
    )
    len(raw_datasets["validation"]), len(validation_dataset)

    (10570, 10822)

În acest caz, am adăugat doar câteva sute de sampleuri, astfel încât se pare că contextele din datasetul de validare sunt un pic mai scurte.

Acum că am preprocesat toate datele, putem trece la antrenare.

##
Fine-tuningul modelului cu API-ul Trainer

Codul de antrenare pentru acest exemplu va semăna foarte mult cu codul din secțiunile anterioare - cel mai greu lucru va fi să scriem funcția `compute_metrics()`. Deoarece am făcut padding tuturor sampleurilor la lungimea maximă pe care am stabilit-o, nu trebuie definit niciun data collator, astfel încât acest calcul al metricii este singurul lucru de care trebuie să ne facem griji. Partea dificilă va fi să postprocesăm predicțiile modelului în intervale de text în exemplele originale; odată ce am făcut acest lucru, metrica din biblioteca 🤗 Datasets va face cea mai mare parte a muncii pentru noi.

###
Post-procesare

Modelul va produce logits pentru pozițiile de început și de sfârșit ale răspunsului în ID-urile de intrare, așa cum am văzut în timpul explorării `question-answering` pipeline. Etapa de post-procesare va fi similară cu ceea ce am făcut acolo, așa că iată vă reamintim ce acțiuni am luat:

- Am mascat logiturile de început și de sfârșit corespunzătoare tokenilor din afara contextului.
- Am convertit apoi logiturile de început și de sfârșit în probabilități utilizând un softmax.
- Am atribuit un scor fiecărei perechi `(start_token, end_token)` prin calcularea produsului celor două probabilități corespunzătoare.
- Am căutat perechea cu scorul maxim care a dat un răspuns valid (de exemplu, `start_token` mai mic decât `end_token`).

Aici vom schimba ușor acest proces, deoarece nu trebuie să calculăm scorurile reale (doar răspunsul prezis). Aceasta înseamnă că putem sări peste etapa softmax. De asemenea, pentru a merge mai repede, nu vom puncta toate perechile posibile `(start_token, end_token)`, ci doar pe cele care corespund celor mai mari `n_best` logits (cu `n_best=20`). Deoarece vom trece peste softmax, aceste scoruri vor fi scoruri logit și vor fi obținute prin însumarea logiturilor de început și de sfârșit (în loc de produs, datorită reguliilog⁡(a b)=log⁡(a)+log⁡(b)\log(ab) = \log(a) + \log(b)log(ab)=log(a)+log(b)).

Pentru a demonstra toate acestea, vom avea nevoie de un fel de predicții. Deoarece nu ne-am antrenat încă modelul, vom utiliza modelul implicit pentru pipelineuri QA pentru a genera unele predicții pe o mică parte a setului de validare. Putem utiliza aceeași funcție de procesare ca înainte; deoarece se bazează pe constanta globală `tokenizer`, trebuie doar să schimbăm acest obiect cu tokenizerul modelului pe care dorim să îl utilizăm temporar:

    small_eval_set = raw_datasets["validation"].select(range(100))
    trained_checkpoint = "distilbert-base-cased-distilled-squad"

    tokenizer = AutoTokenizer.from_pretrained(trained_checkpoint)
    eval_set = small_eval_set.map(
        preprocess_validation_examples,
        batched=True,
        remove_columns=raw_datasets["validation"].column_names,
    )

Acum că preprocesarea este terminată, schimbăm tokenizerul înapoi la cel pe care l-am ales inițial:

    tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)

Apoi eliminăm coloanele din `eval_set` care nu sunt așteptate de model, construim un batch cu întregul set de validare și îl trecem prin model. Dacă este disponibil un GPU, îl folosim pentru a merge mai repede:

    import torch
    from transformers import AutoModelForQuestionAnswering

    eval_set_for_model = eval_set.remove_columns(["example_id", "offset_mapping"])
    eval_set_for_model.set_format("torch")

    device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
    batch = {k: eval_set_for_model[k].to(device) for k in eval_set_for_model.column_names}
    trained_model = AutoModelForQuestionAnswering.from_pretrained(trained_checkpoint).to(
        device
    )

    with torch.no_grad():
        outputs = trained_model(**batch)

Deoarece `Trainer` ne va oferi predicții sub formă de matrici NumPy, preluăm logiturile de început și de sfârșit și le convertim în acest format:

    start_logits = outputs.start_logits.cpu().numpy()
    end_logits = outputs.end_logits.cpu().numpy()

Acum, trebuie să găsim răspunsul prezis pentru fiecare exemplu din `small_eval_set`. Este posibil ca un exemplu să fi fost împărțit în mai multe caracteristici în `eval_set`, astfel încât primul pas constă în maparea fiecărui exemplu din `small_eval_set` la caracteristicile corespunzătoare din `eval_set`:

    import collections

    example_to_features = collections.defaultdict(list)
    for idx, feature in enumerate(eval_set):
        example_to_features[feature["example_id"]].append(idx)

Cu acest lucru în mână, ne putem apuca de treabă trecând prin toate exemplele și, pentru fiecare exemplu, prin toate caracteristicile asociate. Așa cum am spus mai devreme, ne vom uita la scorurile logit pentru `n_cele mai bune` logits de început și de sfârșit, excluzând pozițiile care dau:

- Un răspuns care nu ar fi în interiorul contextului
- Un răspuns cu lungime negativă
- Un răspuns care este prea lung (limităm posibilitățile la `max_answer_length=30`)

Odată ce avem toate răspunsurile posibile scored pentru un exemplu, îl alegem pe cel cu cel mai bun scor logit:

    import numpy as np

    n_best = 20
    max_answer_length = 30
    predicted_answers = []

    for example in small_eval_set:
        example_id = example["id"]
        context = example["context"]
        answers = []

        for feature_index in example_to_features[example_id]:
            start_logit = start_logits[feature_index]
            end_logit = end_logits[feature_index]
            offsets = eval_set["offset_mapping"][feature_index]

            start_indexes = np.argsort(start_logit)[-1 : -n_best - 1 : -1].tolist()
            end_indexes = np.argsort(end_logit)[-1 : -n_best - 1 : -1].tolist()
            for start_index in start_indexes:
                for end_index in end_indexes:
                    # Skip answers that are not fully in the context
                    if offsets[start_index] is None or offsets[end_index] is None:
                        continue
                    # Skip answers with a length that is either < 0 or > max_answer_length.
                    if (
                        end_index < start_index
                        or end_index - start_index + 1 > max_answer_length
                    ):
                        continue

                    answers.append(
                        {
                            "text": context[offsets[start_index][0] : offsets[end_index][1]],
                            "logit_score": start_logit[start_index] + end_logit[end_index],
                        }
                    )

        best_answer = max(answers, key=lambda x: x["logit_score"])
        predicted_answers.append({"id": example_id, "prediction_text": best_answer["text"]})

Formatul final al răspunsurilor prezise este cel care va fi așteptat de metrica pe care o vom utiliza. Ca de obicei, o putem încărca cu ajutorul bibliotecii 🤗 Evaluate:

    import evaluate

    metric = evaluate.load("squad")

Această metrică așteaptă răspunsurile prezise în formatul pe care l-am văzut mai sus (o listă de dicționare cu o cheie pentru ID-ul exemplului și o cheie pentru textul prezis) și răspunsurile teoretice în formatul de mai jos (o listă de dicționare cu o cheie pentru ID-ul exemplului și o cheie pentru răspunsurile posibile):

    theoretical_answers = [
        {"id": ex["id"], "answers": ex["answers"]} for ex in small_eval_set
    ]

Acum putem verifica dacă obținem rezultate rezonabile analizând primul element din ambele liste:

    print(predicted_answers[0])
    print(theoretical_answers[0])

    {'id': '56be4db0acb8001400a502ec', 'prediction_text': 'Denver Broncos'}
    {'id': '56be4db0acb8001400a502ec', 'answers': {'text': ['Denver Broncos', 'Denver Broncos', 'Denver Broncos'], 'answer_start': [177, 177, 177]}}

Nu-i deloc rău! Acum putem arunca o privire la scorul pe care ni-l oferă metrica:

    metric.compute(predictions=predicted_answers, references=theoretical_answers)

    {'exact_match': 83.0, 'f1': 88.25}

Din nou, acest lucru este destul de bun, având în vedere că, în conformitate cu [documentul acestuia] (https://arxiv.org/abs/1910.01108v2), DistilBERT fine-tuned pe SQuAD obține 79,1 și 86,9 pentru aceste scoruri pe întregul dataset.

Acum să punem tot ce am făcut într-o funcție `compute_metrics()` pe care o vom folosi în `Trainer`. În mod normal, această funcție `compute_metrics()` primește doar un tuple `eval_preds` cu logiți și labels. Aici vom avea nevoie de ceva mai mult, deoarece trebuie să căutăm în dataset de caracteristici pentru offset și în datasetul de exemple pentru contextele originale, astfel încât nu vom putea utiliza această funcție pentru a obține rezultate de evaluare regulate în timpul antrenării. O vom utiliza doar la sfârșitul antrenamentului pentru a verifica rezultatele.

Funcția `compute_metrics()` grupează aceiași pași ca înainte; adăugăm doar o mică verificare în cazul în care nu obținem niciun răspuns valid (caz în care prezicem un șir gol).

    from tqdm.auto import tqdm

    def compute_metrics(start_logits, end_logits, features, examples):
        example_to_features = collections.defaultdict(list)
        for idx, feature in enumerate(features):
            example_to_features[feature["example_id"]].append(idx)

        predicted_answers = []
        for example in tqdm(examples):
            example_id = example["id"]
            context = example["context"]
            answers = []

            # Loop through all features associated with that example
            for feature_index in example_to_features[example_id]:
                start_logit = start_logits[feature_index]
                end_logit = end_logits[feature_index]
                offsets = features[feature_index]["offset_mapping"]

                start_indexes = np.argsort(start_logit)[-1 : -n_best - 1 : -1].tolist()
                end_indexes = np.argsort(end_logit)[-1 : -n_best - 1 : -1].tolist()
                for start_index in start_indexes:
                    for end_index in end_indexes:
                        # Skip answers that are not fully in the context
                        if offsets[start_index] is None or offsets[end_index] is None:
                            continue
                        # Skip answers with a length that is either < 0 or > max_answer_length
                        if (
                            end_index < start_index
                            or end_index - start_index + 1 > max_answer_length
                        ):
                            continue

                        answer = {
                            "text": context[offsets[start_index][0] : offsets[end_index][1]],
                            "logit_score": start_logit[start_index] + end_logit[end_index],
                        }
                        answers.append(answer)

            # Select the answer with the best score
            if len(answers) > 0:
                best_answer = max(answers, key=lambda x: x["logit_score"])
                predicted_answers.append(
                    {"id": example_id, "prediction_text": best_answer["text"]}
                )
            else:
                predicted_answers.append({"id": example_id, "prediction_text": ""})

        theoretical_answers = [{"id": ex["id"], "answers": ex["answers"]} for ex in examples]
        return metric.compute(predictions=predicted_answers, references=theoretical_answers)

Putem verifica dacă funcționează pe baza predicțiilor noastre:

    compute_metrics(start_logits, end_logits, eval_set, small_eval_set)

    {'exact_match': 83.0, 'f1': 88.25}

Arată bine! Acum să folosim acest lucru pentru a face fine-tune modelului nostru.

###
Fine-tuningul modelului

Acum suntem gata să antrenăm modelul. Să-l creăm mai întâi, folosind clasa `AutoModelForQuestionAnswering` ca și înainte:

    model = AutoModelForQuestionAnswering.from_pretrained(model_checkpoint)

Ca de obicei, primim un avertisment că unele weighturi nu sunt utilizate (cele din headul de preantrenare), iar altele sunt inițializate aleatoriu (cele pentru headul pentru răspuns la întrebări). Ar trebui să fiți obișnuiți cu acest lucru până acum, dar înseamnă că acest model nu este încă pregătit pentru a fi utilizat și trebuie să fie fine-tuned - bine că suntem pe cale să facem asta!

Pentru a putea trimite modelul nostru către Hub, va trebui să ne conectăm la Hugging Face. Dacă executați acest cod într-un notebook, puteți face acest lucru cu următoarea funcție utilitară, care afișează un widget în care puteți introduce datele voastre de autentificare:

    from huggingface_hub import notebook_login

    notebook_login()

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

    huggingface-cli login

Odată făcut acest lucru, ne putem defini `TrainingArguments`. Așa cum am spus atunci când am definit funcția noastră pentru a calcula metrica, nu vom putea avea o buclă de evaluare obișnuită din cauza parametrilor funcției `compute_metrics()`. Am putea scrie propria noastră subclasă a `Trainer` pentru a face acest lucru (o abordare pe care o puteți găsi în scriptul exemplu pentru răspunderea la întrebări), dar este un pic prea lung pentru această secțiune. În schimb, aici vom evalua modelul doar la sfârșitul antrenării și vă vom arăta cum să efectuați o evaluare obișnuită în secțiunea “O buclă de antrenare personalizată” de mai jos.

Acesta este într-adevăr locul în care API-ul `Trainer` își arată limitele și biblioteca 🤗 Accelerate strălucește: personalizarea clasei pentru un caz de utilizare specific poate fi greu de implementat, dar modificarea unei bucle de antrenare complet expuse este ușoară.

Să aruncăm o privire la `TrainingArguments`:

    from transformers import TrainingArguments

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

Am mai văzut cele mai multe dintre acestea: stabilim niște hiperparametrii (cum ar fi learning rate, numărul de epoci pentru antrenament și o anumită scădere a weighturilor) și indicăm că dorim să salvăm modelul la sfârșitul fiecărei epoci, să sărim peste evaluare și să încărcăm rezultatele noastre în Model Hub. De asemenea, activăm antrenarea cu precizie mixtă cu `fp16=True`, deoarece aceasta poate accelera foarte mult antrenarea pe un GPU recent.

În mod implicit, repositoriul utilizat va fi în namespaceul vostru și numit după foldrul de ieșire pe care l-ați stabilit, deci în cazul nostru va fi în `"sgugger/bert-finetuned-squad"`. Putem trece peste acest lucru prin trecerea unui `hub_model_id`; de exemplu, pentru a încărca modelul în organizația `huggingface_course`, am folosit `hub_model_id="huggingface_course/bert-finetuned-squad"` (care este modelul la care am făcut legătura la începutul acestei secțiuni).

💡 Dacă folderul de ieșire pe care îl utilizați există, acesta trebuie să fie o clonă locală a repositoriul în care doriți să faceți push (deci setați un nume nou dacă primiți o eroare la definirea `Trainer`).

În cele din urmă, trecem totul în clasa `Trainer` și lansăm antrenarea:

    from transformers import Trainer

    trainer = Trainer(
        model=model,
        args=args,
        train_dataset=train_dataset,
        eval_dataset=validation_dataset,
        tokenizer=tokenizer,
    )
    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 pe fundal. În acest fel, veți putea să reluați antrenarea pe un alt device, dacă este necesar. Întreaga pregătire durează ceva timp (puțin peste o oră pe un Titan RTX), așa că puteți să vă luați o cafea sau să recitiți unele dintre părțile cursului care vi s-au părut mai dificile în timp ce se desfășoară. De asemenea, rețineți că, de îndată ce se termină prima epocă, veți vedea câteva weighturu încărcate în Hub și puteți începe să vă jucați cu modelul vostru pe pagina acestuia.

Odată ce antrenamentul este complet, putem în cele din urmă să evaluăm modelul (și să ne rugăm să nu fi petrecut tot timpul de calcul degeaba). Metoda `predict()` a `Trainer` va returna un tuple în care primele elemente vor fi predicțiile modelului (aici o pereche cu logiturile de început și de sfârșit). Trimitem acest rezultat funcției noastre `compute_metrics()`:

    predictions, _, _ = trainer.predict(validation_dataset)
    start_logits, end_logits = predictions
    compute_metrics(start_logits, end_logits, validation_dataset, raw_datasets["validation"])

    {'exact_match': 81.18259224219489, 'f1': 88.67381321905516}

Super! Ca o comparație, scorurile de bază raportate în articolul BERT pentru acest model sunt 80,8 și 88,5, deci suntem exact unde ar trebui să fim.

În final, 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")

Aceasta returnează URL-ul commit-ului pe care tocmai l-a făcut, dacă doriți să îl inspectați:

    'https://huggingface.co/sgugger/bert-finetuned-squad/commit/9dcee1fbc25946a6ed4bb32efb1bd71d5fa90b68'

De asemenea, `Trainer` redactează un model card cu toate rezultatele evaluării și o încarcă.

În această etapă, puteți utiliza widgetul de inferență de pe Model Hub pentru a testa modelul și pentru a-l oferi prietenilor, familia și animalele de companie preferate. Ați făcut fine-tune cu succes unui model pentru o sarcină de răspundere a unei întrebari - felicitări!

✏️ E rândul tău! Încearcă un alt model de arhitectură pentru a vedea dacă are performanțe mai bune la această sarcină!

Dacă doriți să pătrundeți puțin mai adânc în bucla de antrenare, vă vom arăta acum cum 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. Aceasta va semăna foarte mult cu bucla de antrenare din Capitolul 3, cu excepția buclei de evaluare. Vom putea evalua modelul în mod regulat, deoarece nu mai suntem constrânși de clasa `Trainer`.

###
Pregătirea pentru antrenament

Mai întâi trebuie să construim `DataLoader`s din dataseturile noastre. Am setat formatul acestor dataseturi la `"torch"` și am eliminat coloanele din setul de validare care nu sunt utilizate de model. Apoi, putem utiliza `default_data_collator` furnizat de Transformers ca un `collate_fn` și să amestecăm setul de antrenare, dar nu și setul de validare:

    from torch.utils.data import DataLoader
    from transformers import default_data_collator

    train_dataset.set_format("torch")
    validation_set = validation_dataset.remove_columns(["example_id", "offset_mapping"])
    validation_set.set_format("torch")

    train_dataloader = DataLoader(
        train_dataset,
        shuffle=True,
        collate_fn=default_data_collator,
        batch_size=8,
    )
    eval_dataloader = DataLoader(
        validation_set, collate_fn=default_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 BERT:

    model = AutoModelForQuestionAnswering.from_pretrained(model_checkpoint)

Atunci vom avea nevoie de un optimizator. Ca de obicei, folosim clasicul `AdamW`, care este ca Adam, dar cu o corecție în modul în care se aplică scăderea weighturilor:

    from torch.optim import AdamW

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

Odată ce avem toate aceste obiecte, le putem trimite metodei `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 antrenament, care nu ar trebui să execute nicio celulă care inițializează un `Accelerator`. Putem forța antrenarea cu precizie mixtă trecând `fp16=True` la `Accelerator` (sau, dacă executați codul ca un script, asigurați-vă că completați `config` în 🤗 Accelerate în mod corespunzător).

    from accelerate import Accelerator

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

După cum ar trebui să știți din secțiunile anterioare, putem utiliza lungimea `train_dataloader` pentru a calcula numărul de pași de antrenare numai după ce a trecut prin metoda `accelerator.prepare()`. Utilizăm același program liniar ca în secțiunile anterioare:

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

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 repositoriul 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 = "bert-finetuned-squad-accelerate"
    repo_name = get_full_repo_name(model_name)
    repo_name

    'sgugger/bert-finetuned-squad-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 = "bert-finetuned-squad-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ă. După definirea unei bare de progres pentru a urmări modul în care decurge antrenamentul, bucla are trei părți:

- Pregătirea în sine, care este iterația clasică peste `train_dataloader`, trecerea înainte prin model, apoi trecerea înapoi și pasul optimizatorului.
- Evaluarea, în care adunăm toate valorile pentru `start_logits` și `end_logits` înainte de a le converti în matrici NumPy. Odată ce bucla de evaluare este terminată, concatenăm toate rezultatele. Rețineți că trebuie să truncăm deoarece `Accelerator` ar fi putut adăuga câteva exemple la sfârșit pentru a ne asigura că avem același număr de exemple în fiecare proces.
- Salvarea și încărcarea, unde mai întâi salvăm modelul și tokenizatorul, apoi apelăm `repo.push_to_hub()`. Ca și înainte, 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ă pe 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 step, batch in enumerate(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()
        start_logits = []
        end_logits = []
        accelerator.print("Evaluation!")
        for batch in tqdm(eval_dataloader):
            with torch.no_grad():
                outputs = model(**batch)

            start_logits.append(accelerator.gather(outputs.start_logits).cpu().numpy())
            end_logits.append(accelerator.gather(outputs.end_logits).cpu().numpy())

        start_logits = np.concatenate(start_logits)
        end_logits = np.concatenate(end_logits)
        start_logits = start_logits[: len(validation_dataset)]
        end_logits = end_logits[: len(validation_dataset)]

        metrics = compute_metrics(
            start_logits, end_logits, validation_dataset, raw_datasets["validation"]
        )
        print(f"epoch {epoch}:", metrics)

        # 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 salvare. 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 pe [huggingface-course/bert-finetuned-squad-accelerate] (https://huggingface.co/huggingface-course/bert-finetuned-squad-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 căruia i-am făcut fine-tune pe Model Hub cu widgetul de inferență. Pentru a-l utiliza local într-un `pipeline`, trebuie doar să specificați identificatorul modelului:

    from transformers import pipeline

    # Înlocuiți acest checkpoint cu propriul checkpoint
    model_checkpoint = "huggingface-course/bert-finetuned-squad"
    question_answerer = pipeline("question-answering", model=model_checkpoint)

    context = """
    🤗 Transformers is backed by the three most popular deep learning libraries — Jax, PyTorch and TensorFlow — with a seamless integration
    between them. It's straightforward to train your models with one before loading them for inference with the other.
    """
    question = "Which deep learning libraries back 🤗 Transformers?"
    question_answerer(question=question, context=context)

    {'score': 0.9979003071784973,
     'start': 78,
     'end': 105,
     'answer': 'Jax, PyTorch and TensorFlow'}

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

< > Update on GitHub