Pytorch
TensorFlow

#
Fine-tuningul la un masked language model

Pentru multe aplicații NLP care implică modele Transformer, puteți lua pur și simplu un model preantrenat de pe Hugging Face Hub și să îl faceți fine-tune direct pe datele voastre pentru sarcina dată. Cu condiția că corpusul utilizat pentru preantrenare să nu fie prea diferit de corpusul utilizat pentru fine-tuning, învățarea prin transfer va produce de obicei rezultate bune.

Cu toate acestea, există câteva cazuri în care veți dori să faceți fine-tune mai întâi modelelor lingvistice pe datele voastre, înainte de a antrena un head specific sarcinii. De exemplu, dacă datasetul vostru conține contracte juridice sau articole științifice, un model Transformer obișnuit, precum BERT, va trata de obicei cuvintele specifice domeniului din corpus ca pe niște tokeni rari, iar performanța rezultată poate fi mai puțin satisfăcătoare. Prin fine-tuningul modelului lingvistic pe baza datelor din domeniu, puteți crește performanța multor sarcini, ceea ce înseamnă că, de obicei, trebuie să efectuați acest pas o singură dată!

Acest proces de fine-tuning a unui model lingvistic preantrenat pe date din domeniu se numește de obicei adaptare la domeniu. Acesta a fost popularizat în 2018 de ULMFiT, care a fost una dintre primele arhitecturi neuronale (bazate pe LSTM-uri) care a făcut ca învățarea prin transfer să funcționeze cu adevărat pentru NLP. Un exemplu de adaptare la domeniu cu ULMFiT este prezentat în imaginea de mai jos; în această secțiune vom face ceva similar, dar cu un Transformer în loc de un LSTM!

Până la sfârșitul acestei secțiuni, veți avea un model de limbaj mascat pe Hub, care poate completa automat propoziții, după cum se poate vedea mai jos:

Hai să începem!

> 🙋 Dacă termenii “masked language modeling” și “pretrained model” nu vă sună familiar, mergeți să verificați Capitolul 1, unde vă explicăm toate aceste concepte de bază, cu videoclipuri!

##
Alegerea unui model preantrenat pentru masked language modeling

Pentru a începe, să alegem un model preantrenat adecvat pentru modelarea limbajului mascat. După cum se vede în următoarea captură de ecran, puteți găsi o listă de candidați prin aplicarea filtrului “Fill-Mask” pe Hugging Face Hub:

Deși modelele din familia BERT și RoBERTa sunt cele mai descărcate, vom utiliza un model numit DistilBERT care poate fi antrenat mult mai rapid, cu o pierdere mică sau nulă a performanței în aval. Acest model a fost antrenat folosind o tehnică specială numită knowledge distillation, în care un “model profesor” mare, precum BERT, este folosit pentru a ghida antrenarea unui “model elev” care are mult mai puțini parametrii. O explicație a detaliilor privind distilarea cunoștințelor ne-ar duce prea departe în această secțiune, dar dacă ești interesat, poți citi totul despre aceasta în Natural Language Processing with Transformers (cunoscut sub numele colocvial de Transformers textbooks).

Să continuăm și să descărcăm modelul DistilBERT folosind clasa `AutoModelForMaskedLM`:

    from transformers import AutoModelForMaskedLM

    model_checkpoint = "distilbert-base-uncased"
    model = AutoModelForMaskedLM.from_pretrained(model_checkpoint)

Putem vedea câți parametri are acest model prin apelarea metodei `num_parameters()`:

    distilbert_num_parameters = model.num_parameters() / 1_000_000
    print(f"'>>> DistilBERT number of parameters: {round(distilbert_num_parameters)}M'")
    print(f"'>>> BERT number of parameters: 110M'")

    '>>> DistilBERT number of parameters: 67M'
    '>>> BERT number of parameters: 110M'

Cu aproximativ 67 de milioane de parametri, DistilBERT este de aproximativ două ori mai mic decât modelul de bază BERT, ceea ce se traduce aproximativ printr-o creștere de două ori a vitezei de antrenare - super! Să vedem acum ce tipuri de tokeni prezice acest model ca fiind cele mai probabile completări ale unui mic sample de text:

    text = "This is a great [MASK]."

Ca oameni, ne putem imagina multe posibilități pentru tokenul `[MASK]`, cum ar fi “day”, “ride” sau “painting”. Pentru modelele preantrenate, predicțiile depind de corpusul pe care modelul a fost antrenat, deoarece acesta învață să detecteze tiparele statistice prezente în date. La fel ca BERT, DistilBERT a fost preantrenat pe dataseturile English Wikipedia și BookCorpus, astfel încât ne așteptăm ca predicțiile pentru `[MASK]` să reflecte aceste domenii. Pentru a prezice masca, avem nevoie de tokenizerul DistilBERT pentru a produce inputurile pentru model, deci hai să-l descărcăm și pe acesta din Hub:

    from transformers import AutoTokenizer

    tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)

Cu un tokenizer și un model, putem acum să transmitem exemplul nostru de text modelului, să extragem logiturile și să tipărim primii 5 candidați:

    import torch

    inputs = tokenizer(text, return_tensors="pt")
    token_logits = model(**inputs).logits
    # Find the location of [MASK] and extract its logits
    mask_token_index = torch.where(inputs["input_ids"] == tokenizer.mask_token_id)[1]
    mask_token_logits = token_logits[0, mask_token_index, :]
    # Pick the [MASK] candidates with the highest logits
    top_5_tokens = torch.topk(mask_token_logits, 5, dim=1).indices[0].tolist()

    for token in top_5_tokens:
        print(f"'>>> {text.replace(tokenizer.mask_token, tokenizer.decode([token]))}'")

    '>>> This is a great deal.'
    '>>> This is a great success.'
    '>>> This is a great adventure.'
    '>>> This is a great idea.'
    '>>> This is a great feat.'

Putem vedea din rezultate că predicțiile modelului se referă la termeni din viața de zi cu zi, ceea ce poate că nu este surprinzător având în vedere fundamentul Wikipedia în limba engleză. Să vedem cum putem schimba acest domeniu în ceva puțin mai nișat - recenzii de filme foarte polarizate!

##
Datasetul

Pentru a prezenta adaptarea la domeniu, vom utiliza faimosul [Large Movie Review Dataset] (https://huggingface.co/datasets/imdb) (sau IMDb pe scurt), care este un corpus de recenzii de filme care este adesea utilizat pentru a evalua modelele de analiză a sentimentelor. Prin fine-tuningul aplicat asupra DistilBERT pe acest corpus, ne așteptăm ca modelul de limbaj să își adapteze vocabularul de la datele factuale din Wikipedia pe care a fost antrenat în prealabil la elementele mai subiective ale recenziilor de film. Putem obține datele din Hugging Face Hub cu funcția `load_dataset()` din 🤗 Datasets:

    from datasets import load_dataset

    imdb_dataset = load_dataset("imdb")
    imdb_dataset

    DatasetDict({
        train: Dataset({
            features: ['text', 'label'],
            num_rows: 25000
        })
        test: Dataset({
            features: ['text', 'label'],
            num_rows: 25000
        })
        unsupervised: Dataset({
            features: ['text', 'label'],
            num_rows: 50000
        })
    })

Putem vedea că segmentele `train` și `test` conțin fiecare 25.000 de recenzii, în timp ce există un segment fără label numită `unsupervised` care conține 50.000 de recenzii. Să aruncăm o privire la câteva sampleuri pentru a ne face o idee despre tipul de text cu care avem de-a face. Așa cum am făcut în capitolele anterioare ale cursului, vom combina funcțiile `Dataset.shuffle()` și `Dataset.select()` pentru a crea un sample aleatoriu:

    sample = imdb_dataset["train"].shuffle(seed=42).select(range(3))

    for row in sample:
        print(f"\n'>>> Review: {row['text']}'")
        print(f"'>>> Label: {row['label']}'")

    '>>> Review: This is your typical Priyadarshan movie--a bunch of loony characters out on some silly mission. His signature climax has the entire cast of the film coming together and fighting each other in some crazy moshpit over hidden money. Whether it is a winning lottery ticket in Malamaal Weekly, black money in Hera Pheri, "kodokoo" in Phir Hera Pheri, etc., etc., the director is becoming ridiculously predictable. Don\'t get me wrong; as clichéd and preposterous his movies may be, I usually end up enjoying the comedy. However, in most his previous movies there has actually been some good humor, (Hungama and Hera Pheri being noteworthy ones). Now, the hilarity of his films is fading as he is using the same formula over and over again.<br /><br />Songs are good. Tanushree Datta looks awesome. Rajpal Yadav is irritating, and Tusshar is not a whole lot better. Kunal Khemu is OK, and Sharman Joshi is the best.'
    '>>> Label: 0'

    '>>> Review: Okay, the story makes no sense, the characters lack any dimensionally, the best dialogue is ad-libs about the low quality of movie, the cinematography is dismal, and only editing saves a bit of the muddle, but Sam" Peckinpah directed the film. Somehow, his direction is not enough. For those who appreciate Peckinpah and his great work, this movie is a disappointment. Even a great cast cannot redeem the time the viewer wastes with this minimal effort.<br /><br />The proper response to the movie is the contempt that the director San Peckinpah, James Caan, Robert Duvall, Burt Young, Bo Hopkins, Arthur Hill, and even Gig Young bring to their work. Watch the great Peckinpah films. Skip this mess.'
    '>>> Label: 0'

    '>>> Review: I saw this movie at the theaters when I was about 6 or 7 years old. I loved it then, and have recently come to own a VHS version. <br /><br />My 4 and 6 year old children love this movie and have been asking again and again to watch it. <br /><br />I have enjoyed watching it again too. Though I have to admit it is not as good on a little TV.<br /><br />I do not have older children so I do not know what they would think of it. <br /><br />The songs are very cute. My daughter keeps singing them over and over.<br /><br />Hope this helps.'
    '>>> Label: 1'

Da, acestea sunt cu siguranță recenzii de film și, dacă sunteți suficient de bătrâni, ați putea chiar înțelege comentariul din ultima recenzie despre deținerea unei versiuni VHS 😜! Deși nu vom avea nevoie de labeluri pentru modelarea limbajului, putem vedea deja că un `0` denotă o recenzie negativă, în timp ce un `1` corespunde uneia pozitive.

> ✏️ Încearcă! Creați un sample aleatoriu din segmentul `unsupervised` și verificați că labelurile nu sunt nici `0`, nici `1`. În același timp, ați putea verifica și dacă labelurile din segmentele `train` și `test` sunt într-adevăr `0` sau `1` - aceasta este o verificare utilă pe care orice practicant NLP ar trebui să o efectueze la începutul unui nou proiect!

Acum că am aruncat o privire rapidă asupra datelor, să ne apucăm să le pregătim pentru modelarea limbajului mascat. După cum vom vedea, există câteva etape suplimentare pe care trebuie să le parcurgem în comparație cu sarcinile de clasificare a secvențelor pe care le-am văzut în Capitolul 3. Să începem!

##
Preprocesarea datelor

Atât pentru auto-regressive cât și pentru masked language modeling, un pas comun de preprocesare este concatenarea tuturor exemplelor și apoi împărțirea întregului corpus în bucăți de dimensiuni egale. Acest lucru este destul de diferit de abordarea noastră obișnuită, în care pur și simplu tokenizăm exemplele individuale. De ce să concatenăm totul împreună? Motivul este că exemplele individuale ar putea fi trunchiate dacă sunt prea lungi, ceea ce ar duce la pierderea de informații care ar putea fi utile pentru sarcina de modelare a limbajului!

Deci, pentru a începe, vom tokeniza mai întâi corpusul nostru ca de obicei, dar fără a seta opțiunea `truncation=True` în tokenizerul nostru. De asemenea, vom prelua ID-urile cuvintelor dacă acestea sunt disponibile (ceea ce va fi cazul dacă folosim un tokenizer rapid, așa cum este descris în Capitolul 6), deoarece vom avea nevoie de ele mai târziu pentru a face mascarea întregului cuvânt. Vom include acest lucru într-o funcție simplă și, în același timp, vom elimina coloanele `text` și `label`, deoarece nu mai avem nevoie de ele:

    def tokenize_function(examples):
        result = tokenizer(examples["text"])
        if tokenizer.is_fast:
            result["word_ids"] = [result.word_ids(i) for i in range(len(result["input_ids"]))]
        return result

    # Utilizați batched=True pentru a activa multithreadingul!
    tokenized_datasets = imdb_dataset.map(
        tokenize_function, batched=True, remove_columns=["text", "label"]
    )
    tokenized_datasets

    DatasetDict({
        train: Dataset({
            features: ['attention_mask', 'input_ids', 'word_ids'],
            num_rows: 25000
        })
        test: Dataset({
            features: ['attention_mask', 'input_ids', 'word_ids'],
            num_rows: 25000
        })
        unsupervised: Dataset({
            features: ['attention_mask', 'input_ids', 'word_ids'],
            num_rows: 50000
        })
    })

Deoarece DistilBERT este un model de tip BERT, putem vedea că textele codate constau din `input_ids` și `attention_mask` pe care le-am văzut în alte capitole, precum și din `word_ids` pe care le-am adăugat.

Acum că am tokenizat recenziile de filme, următorul pas este să le grupăm pe toate și să împărțim rezultatul în chunkuri. Dar cât de mari ar trebui să fie aceste chunkuri? Acest lucru va fi determinat în cele din urmă de cantitatea de memorie GPU pe care o aveți disponibilă, dar un bun punct de plecare este să vedeți care este dimensiunea maximă a contextului modelului. Aceasta poate fi dedusă prin inspectarea atributului `model_max_length` al tokenizerului:

    tokenizer.model_max_length

    512

Această valoare este derivată din fișierul tokenizer_config.json asociat cu un checkpoint; în acest caz putem vedea că dimensiunea contextului este de 512 tokeni, la fel ca în cazul BERT.

> ✏️ Încearcă! Unele modele Transformer, precum BigBird și Longformer, au o lungime de context mult mai mare decât BERT și alte modele Transformer mai vechi. Inițializați tokenizerul pentru unul dintre aceste checkpointuri și verificați dacă `model_max_length` este în concordanță cu ceea ce este menționat pe model card.

Prin urmare, pentru a derula experimentele pe GPU-uri precum cele de pe Google Colab, vom alege ceva mai mic care să încapă în memorie:

    chunk_size = 128

> Rețineți că utilizarea unei dimensiuni mici a chunkurilor poate fi dăunător în scenariile din lumea reală, astfel încât ar trebui să utilizați o dimensiune care corespunde cazului de utilizare la care veți aplica modelul.

Acum vine partea distractivă. Pentru a arăta cum funcționează concatenarea, să luăm câteva recenzii din setul nostru de antrenare tokenizat și să imprimăm numărul de tokeni per recenzie:

    # Slicingul produce o listă de liste pentru fiecare caracteristică
    tokenized_samples = tokenized_datasets["train"][:3]

    for idx, sample in enumerate(tokenized_samples["input_ids"]):
        print(f"'>>> Review {idx} length: {len(sample)}'")

    '>>> Review 0 length: 200'
    '>>> Review 1 length: 559'
    '>>> Review 2 length: 192'

Putem apoi concatena toate aceste exemple cu un dictionary comprehension, după cum urmează:

    concatenated_examples = {
        k: sum(tokenized_samples[k], []) for k in tokenized_samples.keys()
    }
    total_length = len(concatenated_examples["input_ids"])
    print(f"'>>> Concatenated reviews length: {total_length}'")

    '>>> Concatenated reviews length: 951'

Minunat, lungimea totală se verifică - așa că acum să împărțim recenziile concatenate în chunkuri de dimensiunea dată de `chunk_size`. Pentru a face acest lucru, iterăm peste caracteristicile din `concatenated_examples` și folosim un list comprehension pentru a crea slice-uri ale fiecărei caracteristici. Rezultatul este un dicționar de chunkuri pentru fiecare caracteristică:

    chunks = {
        k: [t[i : i + chunk_size] for i in range(0, total_length, chunk_size)]
        for k, t in concatenated_examples.items()
    }

    for chunk in chunks["input_ids"]:
        print(f"'>>> Chunk length: {len(chunk)}'")

    '>>> Chunk length: 128'
    '>>> Chunk length: 128'
    '>>> Chunk length: 128'
    '>>> Chunk length: 128'
    '>>> Chunk length: 128'
    '>>> Chunk length: 128'
    '>>> Chunk length: 128'
    '>>> Chunk length: 55'

După cum puteți vedea în acest exemplu, ultimul fragment va fi în general mai mic decât dimensiunea maximă a fragmentului. Există două strategii principale pentru a face față acestei situații:

- Aruncați ultimul chunk dacă este mai mic decât `chunk_size`.
- Faceți padding ultimului chunk până când lungimea sa este egală cu `chunk_size`.

Vom adopta prima abordare aici, așa că hai să încorporăm toată logica de mai sus într-o singură funcție pe care o putem aplica dataseturilor tokenizate:

    def group_texts(examples):
        # Concatenarea tuturor textelor
        concatenated_examples = {k: sum(examples[k], []) for k in examples.keys()}
        # Calcularea lungimii textelor concatenate
        total_length = len(concatenated_examples[list(examples.keys())[0]])
        # Renunțăm la ultimul chunk dacă este mai mic decât chunk_size
        total_length = (total_length // chunk_size) * chunk_size
        # Împărțiți pe bucăți de max_len
        result = {
            k: [t[i : i + chunk_size] for i in range(0, total_length, chunk_size)]
            for k, t in concatenated_examples.items()
        }
        # Creați o nouă coloană de labeluri
        result["labels"] = result["input_ids"].copy()
        return result

Observați că în ultimul pas al `group_texts()` creăm o nouă coloană `labels` care este o copie a coloanei `input_ids`. După cum vom vedea în curând, acest lucru se datorează faptului că în modelarea limbajului mascat obiectivul este de a prezice tokeni mascați aleatoriu în input batch, iar prin crearea unei coloane `labels` furnizăm adevărul de bază din care modelul nostru de limbaj poate să învețe.

Să aplicăm acum funcția `group_texts()` dataseturilor tokenizate folosind funcția noastră de încredere `Dataset.map()`:

    lm_datasets = tokenized_datasets.map(group_texts, batched=True)
    lm_datasets

    DatasetDict({
        train: Dataset({
            features: ['attention_mask', 'input_ids', 'labels', 'word_ids'],
            num_rows: 61289
        })
        test: Dataset({
            features: ['attention_mask', 'input_ids', 'labels', 'word_ids'],
            num_rows: 59905
        })
        unsupervised: Dataset({
            features: ['attention_mask', 'input_ids', 'labels', 'word_ids'],
            num_rows: 122963
        })
    })

Puteți vedea că gruparea și apoi fragmentarea textelor a produs mult mai multe exemple decât cele 25.000 inițiale pentru spliturile `train` și `test`. Acest lucru se datorează faptului că acum avem exemple care implică contigous tokens care se întind pe mai multe exemple din corpusul original. Puteți vedea acest lucru în mod explicit căutând tokenii speciali `[SEP]` și `[CLS]` într-unul dintre chunkuri:

    tokenizer.decode(lm_datasets["train"][1]["input_ids"])

    ".... at.......... high. a classic line : inspector : i'm here to sack one of your teachers. student : welcome to bromwell high. i expect that many adults of my age think that bromwell high is far fetched. what a pity that it isn't! [SEP] [CLS] homelessness ( or houselessness as george carlin stated ) has been an issue for years but never a plan to help those on the street that were once considered human who did everything from going to school, work, or vote for the matter. most people think of the homeless"

În acest exemplu puteți vedea două recenzii de film care se suprapun, una despre un film de liceu și cealaltă despre persoanele fără adăpost. Să verificăm, de asemenea, cum arată labelurile pentru modelarea limbajului mascat:

    tokenizer.decode(lm_datasets["train"][1]["labels"])

    ".... at.......... high. a classic line : inspector : i'm here to sack one of your teachers. student : welcome to bromwell high. i expect that many adults of my age think that bromwell high is far fetched. what a pity that it isn't! [SEP] [CLS] homelessness ( or houselessness as george carlin stated ) has been an issue for years but never a plan to help those on the street that were once considered human who did everything from going to school, work, or vote for the matter. most people think of the homeless"

Așa cum era de așteptat de la funcția noastră `group_texts()` de mai sus, acest lucru pare identic cu `input_ids` decodificat - dar atunci cum poate modelul nostru să învețe ceva? Ne lipsește un pas cheie: inserarea tokenilor `[MASK]` în poziții aleatorii în inputuri! Să vedem cum putem face acest lucru din mers, în timpul fine-tuningului, folosind un data collator special.

##
Fine-tuningul asupra DistilBERT cu API-ul Trainer

Fine-tuningul unui model lingvistic mascat este aproape identic cu fine-tuningul a unui model de clasificare a secvențelor, așa cum am făcut în Capitolul 3. Singura diferență este că avem nevoie de un data collator care poate masca aleatoriu o parte dintre tokeni din fiecare batch de texte. Din fericire, 🤗 Transformers vine pregătit cu un `DataCollatorForLanguageModeling` dedicat tocmai pentru această sarcină. Trebuie doar să îi transmitem tokenizerul și un argument `mlm_probability` care specifică ce fracțiune din tokeni trebuie mascată. Vom alege 15%, care este cantitatea utilizată pentru BERT și o alegere comună în literatura de specialitate:

    from transformers import DataCollatorForLanguageModeling

    data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm_probability=0.15)

Pentru a vedea cum funcționează mascarea aleatorie, să introducem câteva exemple în data collator. Deoarece se așteaptă la o listă de “dict”-uri, în care fiecare “dict” reprezintă un singur chunk de text continuu, mai întâi iterăm peste dataset înainte de a trimite batchul către data collator. Eliminăm cheia `"word_ids"` pentru acest data collator, deoarece acesta nu o așteaptă:

    samples = [lm_datasets["train"][i] for i in range(2)]
    for sample in samples:
        _ = sample.pop("word_ids")

    for chunk in data_collator(samples)["input_ids"]:
        print(f"\n'>>> {tokenizer.decode(chunk)}'")

    '>>> [CLS] bromwell [MASK] is a cartoon comedy. it ran at the same [MASK] as some other [MASK] about school life, [MASK] as " teachers ". [MASK] [MASK] [MASK] in the teaching [MASK] lead [MASK] to believe that bromwell high\'[MASK] satire is much closer to reality than is " teachers ". the scramble [MASK] [MASK] financially, the [MASK]ful students whogn [MASK] right through [MASK] pathetic teachers\'pomp, the pettiness of the whole situation, distinction remind me of the schools i knew and their students. when i saw [MASK] episode in [MASK] a student repeatedly tried to burn down the school, [MASK] immediately recalled. [MASK]...'

    '>>> .... at.. [MASK]... [MASK]... high. a classic line plucked inspector : i\'[MASK] here to [MASK] one of your [MASK]. student : welcome to bromwell [MASK]. i expect that many adults of my age think that [MASK]mwell [MASK] is [MASK] fetched. what a pity that it isn\'t! [SEP] [CLS] [MASK]ness ( or [MASK]lessness as george 宇in stated )公 been an issue for years but never [MASK] plan to help those on the street that were once considered human [MASK] did everything from going to school, [MASK], [MASK] vote for the matter. most people think [MASK] the homeless'

Frumos, a funcționat! Putem vedea că tokenul `[MASK]` a fost inserat aleatoriu în diferite locuri din textul nostru. Acestea vor fi tokenii pe care modelul nostru va trebui să le prezică în timpul antrenamentului - iar frumusețea data collatorului este că va introduce aleatoriu tokenul `[MASK]` cu fiecare batch!

> ✏️ Încercați! Rulați fragmentul de cod de mai sus de mai multe ori pentru a vedea cum se întâmplă mascarea aleatorie în fața ochilor voștri! De asemenea, înlocuiți metoda `tokenizer.decode()` cu `tokenizer.convert_ids_to_tokens()` pentru a vedea că uneori un singur token dintr-un cuvânt dat este mascat, și nu celelalte.

Un efect secundar al mascării aleatorii este faptul că metricile noastre de evaluare nu vor fi deterministe atunci când folosim `Trainer`, deoarece folosim același data collator pentru seturile de antrenare și testare. Vom vedea mai târziu, când ne vom uita la aplicarea fine-tuningului cu 🤗 Accelerate, cum putem folosi flexibilitatea unei bucle de evaluare personalizate pentru a îngheța caracterul aleatoriu.

La antrenarea modelelor pentru modelarea limbajului mascat, o tehnică care poate fi utilizată este mascarea cuvintelor întregi împreună, nu doar a tokenilor individuali. Această abordare se numește whole word masking. Dacă dorim să utilizăm mascarea întregului cuvânt, va trebui să construim noi înșine un data collator. Un data collator este doar o funcție care preia o listă de sampleuri și le convertește într-un batch, așa că hai să facem asta acum! Vom utiliza ID-urile cuvintelor calculate mai devreme pentru a realiza o hartă între indicii cuvintelor și tokenii corespunzători, apoi vom decide aleatoriu ce cuvinte să mascăm și vom aplica masca respectivă asupra inputurilor. Rețineți că labelurile sunt toate `-100`, cu excepția celor care corespund cuvintelor mascate.

    import collections
    import numpy as np

    from transformers import default_data_collator

    wwm_probability = 0.2

    def whole_word_masking_data_collator(features):
        for feature in features:
            word_ids = feature.pop("word_ids")

            # Create a map between words and corresponding token indices
            mapping = collections.defaultdict(list)
            current_word_index = -1
            current_word = None
            for idx, word_id in enumerate(word_ids):
                if word_id is not None:
                    if word_id != current_word:
                        current_word = word_id
                        current_word_index += 1
                    mapping[current_word_index].append(idx)

            # Randomly mask words
            mask = np.random.binomial(1, wwm_probability, (len(mapping),))
            input_ids = feature["input_ids"]
            labels = feature["labels"]
            new_labels = [-100] * len(labels)
            for word_id in np.where(mask)[0]:
                word_id = word_id.item()
                for idx in mapping[word_id]:
                    new_labels[idx] = labels[idx]
                    input_ids[idx] = tokenizer.mask_token_id
            feature["labels"] = new_labels

        return default_data_collator(features)

În continuare, îl putem încerca pe aceleași sampleuri ca înainte:

    samples = [lm_datasets["train"][i] for i in range(2)]
    batch = whole_word_masking_data_collator(samples)

    for chunk in batch["input_ids"]:
        print(f"\n'>>> {tokenizer.decode(chunk)}'")

    '>>> [CLS] bromwell high is a cartoon comedy [MASK] it ran at the same time as some other programs about school life, such as " teachers ". my 35 years in the teaching profession lead me to believe that bromwell high\'s satire is much closer to reality than is " teachers ". the scramble to survive financially, the insightful students who can see right through their pathetic teachers\'pomp, the pettiness of the whole situation, all remind me of the schools i knew and their students. when i saw the episode in which a student repeatedly tried to burn down the school, i immediately recalled.....'

    '>>> .... [MASK] [MASK] [MASK] [MASK]....... high. a classic line : inspector : i\'m here to sack one of your teachers. student : welcome to bromwell high. i expect that many adults of my age think that bromwell high is far fetched. what a pity that it isn\'t! [SEP] [CLS] homelessness ( or houselessness as george carlin stated ) has been an issue for years but never a plan to help those on the street that were once considered human who did everything from going to school, work, or vote for the matter. most people think of the homeless'

> ✏️ Încercați! Rulați fragmentul de cod de mai sus de mai multe ori pentru a vedea cum se întâmplă mascarea aleatorie în fața ochilor voștri! De asemenea, înlocuiți metoda `tokenizer.decode()` cu `tokenizer.convert_ids_to_tokens()` pentru a vedea că tokenii dintr-un cuvânt dat sunt întotdeauna mascați împreună.

Acum, că avem două data collators, restul pașilor fine-tuning sunt standard. Pregătirea poate dura ceva timp pe Google Colab dacă nu sunteți suficient de norocos să obțineți un GPU P100 mitic 😭, așa că vom reduce mai întâi dimensiunea setului de antrenare la câteva mii de exemple. Nu vă faceți griji, vom obține în continuare un model lingvistic destul de decent! O modalitate rapidă de a reduce sampleurile unui dataset în 🤗 Datasets este prin intermediul funcției `Dataset.train_test_split()` pe care am văzut-o în Capitolul 5:

    train_size = 10_000
    test_size = int(0.1 * train_size)

    downsampled_dataset = lm_datasets["train"].train_test_split(
        train_size=train_size, test_size=test_size, seed=42
    )
    downsampled_dataset

    DatasetDict({
        train: Dataset({
            features: ['attention_mask', 'input_ids', 'labels', 'word_ids'],
            num_rows: 10000
        })
        test: Dataset({
            features: ['attention_mask', 'input_ids', 'labels', 'word_ids'],
            num_rows: 1000
        })
    })

Acest lucru a creat în mod automat noi splituri de `train` și `test`, cu dimensiunea setului de antrenare setată la 10.000 de exemple și a setului de validare la 10% din aceasta - nu ezitați să măriți această valoare dacă aveți un GPU puternic! Următorul lucru pe care trebuie să îl facem este să ne conectăm la Hugging Face Hub. Dacă executați acest cod într-un notebook, puteți face acest lucru cu următoarea funcție utilitară:

    from huggingface_hub import notebook_login

    notebook_login()

care va afișa un widget în care vă puteți introduce credențialele. Alternativ, puteți rula:

    huggingface-cli login

in your favorite terminal and log in there.

Odată ce suntem conectați, putem specifica argumentele pentru `Trainer`:

    from transformers import TrainingArguments

    batch_size = 64
    # Show the training loss with every epoch
    logging_steps = len(downsampled_dataset["train"]) // batch_size
    model_name = model_checkpoint.split("/")[-1]

    training_args = TrainingArguments(
        output_dir=f"{model_name}-finetuned-imdb",
        overwrite_output_dir=True,
        evaluation_strategy="epoch",
        learning_rate=2e-5,
        weight_decay=0.01,
        per_device_train_batch_size=batch_size,
        per_device_eval_batch_size=batch_size,
        push_to_hub=True,
        fp16=True,
        logging_steps=logging_steps,
    )

Aici am modificat câteva dintre opțiunile implicite, inclusiv `logging_steps` pentru a ne asigura că urmărim pierderea de antrenare cu fiecare epocă. De asemenea, am folosit `fp16=True` pentru a activa antrenarea cu precizie mixtă, ceea ce ne oferă un alt impuls vitezei. În mod implicit, `Trainer` va elimina toate coloanele care nu fac parte din metoda `forward()` a modelului. Aceasta înseamnă că, dacă utilizați whole word masking collator, va trebui să setați și `remove_unused_columns=False` pentru a vă asigura că nu pierdem coloana `word_ids` în timpul antrenamentului.

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 făcut push modelului către organizația `huggingface-course`, am adăugat `hub_model_id="huggingface-course/distilbert-finetuned-imdb"` la `TrainingArguments`. Î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 `"lewtun/distilbert-finetuned-imdb"`.

Acum avem toate ingredientele pentru inițializarea `Trainer`. Aici folosim doar `data_collator` standard, dar puteți încerca whole word masking collator și să comparați rezultatele ca un exercițiu:

    from transformers import Trainer

    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=downsampled_dataset["train"],
        eval_dataset=downsampled_dataset["test"],
        data_collator=data_collator,
        tokenizer=tokenizer,
    )

Acum suntem gata să rulăm `trainer.train()` - dar înainte de a face acest lucru, să analizăm pe scurt perplexitatea, care este o metrică comună de evaluare a performanței modelelor de limbaj.

###
Perplexity pentru language models

Spre deosebire de alte sarcini, cum ar fi clasificarea textului sau răspunderea la întrebări, unde ni se oferă un corpus labeled pe care să antrenăm, cu modelarea limbajului nu avem labeluri explicite. Așadar, cum determinăm ce face un model lingvistic bun? La fel ca în cazul funcției de autocorectare din telefon, un model lingvistic bun este unul care atribuie probabilități ridicate propozițiilor corecte din punct de vedere gramatical și probabilități scăzute propozițiilor fără sens. Pentru a vă face o idee mai bună despre cum arată acest lucru, puteți găsi online seturi întregi de “autocorrect fails”, în care modelul din telefonul unei persoane a produs niște completări destul de amuzante (și adesea nepotrivite)!

Presupunând că setul nostru de testare constă în cea mai mare parte din propoziții corecte din punct de vedere gramatical, atunci o modalitate de a măsura calitatea modelului nostru lingvistic este de a calcula probabilitățile pe care le atribuie următorului cuvânt în toate propozițiile din setul de testare. Probabilitatea ridicată indică faptul că modelul nu este “surprins” sau “perplex” de exemplele nevăzute și sugerează că a învățat tiparele gramaticale de bază ale limbii. Există diverse definiții matematice ale perplexității, dar cea pe care o vom utiliza o definește ca the exponential of the cross-entropy loss. Astfel, putem calcula perplexitatea modelului nostru preantrenat utilizând funcția `Trainer.evaluate()` pentru a calcula pierderea de cross-entropy pe setul de testare și apoi luând exponențiala rezultatului:

    import math

    eval_results = trainer.evaluate()
    print(f">>> Perplexity: {math.exp(eval_results['eval_loss']):.2f}")

    >>> Perplexity: 21.75

Un scor de perplexitate mai mic înseamnă un model lingvistic mai bun, iar aici putem vedea că modelul nostru inițial are o valoare oarecum mare. Să vedem dacă o putem reduce prin fine-tuning! Pentru a face acest lucru, vom rula mai întâi bucla de antrenare:

    trainer.train()

și apoi calculați perplexitatea rezultată pe setul de testare ca înainte:

    eval_results = trainer.evaluate()
    print(f">>> Perplexity: {math.exp(eval_results['eval_loss']):.2f}")

    >>> Perplexity: 11.32

Grozav - aceasta este o reducere destul de mare a perplexității, ceea ce ne spune că modelul a învățat ceva despre domeniul recenziilor de filme!

Odată ce antrenarea este finalizată, putem trimite cardul modelului cu informațiile de antrenare către Hub (checkpointurile sunt salvate în timpul antrenare):

    trainer.push_to_hub()

> ✏️ Rândul tău! Rulați antrenamentul de mai sus după schimbarea data collatorului cu whole word masking collator. Obțineți rezultate mai bune?

În cazul nostru de utilizare, nu a fost nevoie să facem nimic special cu bucla de antrenare, dar în unele cazuri s-ar putea să fie nevoie să implementați o logică personalizată. Pentru aceste aplicații, puteți utiliza 🤗 Accelerate — să aruncăm o privire!

##
Fine-tuningul DistilBERT cu 🤗 Accelerate

Așa cum am văzut cu `Trainer`, fine-tuningul unui model de limbaj mascat este foarte asemănător cu exemplul de clasificare a textului din Capitolul 3. De fapt, singura subtilitate este utilizarea unui data collator special, pe care l-am abordat mai devreme în această secțiune!

Cu toate acestea, am văzut că `DataCollatorForLanguageModeling` aplică, de asemenea, o mascare aleatorie cu fiecare evaluare, astfel încât vom vedea unele fluctuații în scorurile noastre de perplexitate cu fiecare rulare de antrenament. O modalitate de a elimina această sursă de dezordine este de a aplica mascarea o singură dată pe întregul set de teste și apoi de a utiliza data collatorul implicit din 🤗 Transformers pentru a colecta batch-urile în timpul evaluării. Pentru a vedea cum funcționează acest lucru, să implementăm o funcție simplă care aplică mascarea pe un batch, similară cu prima noastră întâlnire cu `DataCollatorForLanguageModeling`:

    def insert_random_mask(batch):
        features = [dict(zip(batch, t)) for t in zip(*batch.values())]
        masked_inputs = data_collator(features)
        # Create a new "masked" column for each column in the dataset
        return {"masked_" + k: v.numpy() for k, v in masked_inputs.items()}

În continuare, vom aplica această funcție setului nostru de testare și vom elimina coloanele nemascate pentru a le putea înlocui cu cele mascate. Puteți utiliza whole word masking prin înlocuirea `data_collator` de mai sus cu cel corespunzător, caz în care trebuie să eliminați prima linie de aici:

    downsampled_dataset = downsampled_dataset.remove_columns(["word_ids"])
    eval_dataset = downsampled_dataset["test"].map(
        insert_random_mask,
        batched=True,
        remove_columns=downsampled_dataset["test"].column_names,
    )
    eval_dataset = eval_dataset.rename_columns(
        {
            "masked_input_ids": "input_ids",
            "masked_attention_mask": "attention_mask",
            "masked_labels": "labels",
        }
    )

Putem configura apoi dataloaderele ca de obicei, dar vom folosi `default_data_collator` de la 🤗 Transformers pentru setul de evaluare:

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

    batch_size = 64
    train_dataloader = DataLoader(
        downsampled_dataset["train"],
        shuffle=True,
        batch_size=batch_size,
        collate_fn=data_collator,
    )
    eval_dataloader = DataLoader(
        eval_dataset, batch_size=batch_size, collate_fn=default_data_collator
    )

De aici, vom urma pașii standard cu 🤗 Accelerate. În primul rând, se încarcă o versiune nouă a modelului antrenat:

    model = AutoModelForMaskedLM.from_pretrained(model_checkpoint)

Apoi trebuie să specificăm optimizatorul; vom folosi standardul `AdamW`:

    from torch.optim import AdamW

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

Cu aceste obiecte, acum putem pregăti totul pentru antrenare cu obiectul `Accelerator`:

    from accelerate import Accelerator

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

Acum că modelul, optimizatorul și dataloaderul sunt configurate, putem specifica learning rate schedulerul cum urmează:

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

Mai este un singur lucru de făcut înainte de antrenare: creați un repositoriu de modele pe Hugging Face Hub! Putem utiliza biblioteca 🤗 Hub pentru a genera mai întâi numele complet al repositoriul nostru:

    from huggingface_hub import get_full_repo_name

    model_name = "distilbert-base-uncased-finetuned-imdb-accelerate"
    repo_name = get_full_repo_name(model_name)
    repo_name

    'lewtun/distilbert-base-uncased-finetuned-imdb-accelerate'

apoi creați și clonați repositoriul folosind clasa `Repository` din 🤗 Hub:

    from huggingface_hub import Repository

    output_dir = model_name
    repo = Repository(output_dir, clone_from=repo_name)

Odată ce acest lucru este făcut, trebuie doar să scriem ciclul complet de antrenare și evaluare:

    from tqdm.auto import tqdm
    import torch
    import math

    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()
        losses = []
        for step, batch in enumerate(eval_dataloader):
            with torch.no_grad():
                outputs = model(**batch)

            loss = outputs.loss
            losses.append(accelerator.gather(loss.repeat(batch_size)))

        losses = torch.cat(losses)
        losses = losses[: len(eval_dataset)]
        try:
            perplexity = math.exp(torch.mean(losses))
        except OverflowError:
            perplexity = float("inf")

        print(f">>> Epoch {epoch}: Perplexity: {perplexity}")

        # 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: Perplexity: 11.397545307900472
    >>> Epoch 1: Perplexity: 10.904909330983092
    >>> Epoch 2: Perplexity: 10.729503505340409

Mișto, am reușit să evaluăm perplexitatea cu fiecare epocă și să ne asigurăm că mai multe runde de antrenament sunt reproductibile!

##
Utilizarea modelului fine-tuned

Puteți interacționa cu modelul vostru fine-tuned utilizând widgetul său de pe Hub, fie local cu `pipeline` din 🤗 Transformers. Să folosim aceasta din urmă pentru a descărca modelul nostru folosind pipelineuul `fill-mask`:

    from transformers import pipeline

    mask_filler = pipeline(
        "fill-mask", model="huggingface-course/distilbert-base-uncased-finetuned-imdb"
    )

Putem apoi să alimentăm pipelineul cu exemplul nostru de text “This is a hrea [MASK]” și să vedem care sunt primele 5 predicții:

    preds = mask_filler(text)

    for pred in preds:
        print(f">>> {pred['sequence']}")

    '>>> this is a great movie.'
    '>>> this is a great film.'
    '>>> this is a great story.'
    '>>> this is a great movies.'
    '>>> this is a great character.'

Frumos - modelul nostru și-a adaptat în mod clar weighturile pentru a prezice cuvintele care sunt asociate mai puternic cu filmele!

Acest lucru încheie primul nostru experiment de antrenare a unui model lingvistic. În secțiunea 6 veți învăța cum să antrenați de la zero un model auto-regressive precum GPT-2; mergeți acolo dacă doriți să vedeți cum vă puteți preantrena propriul model Transformer!

> ✏️ Încercați! Pentru a cuantifica beneficiile adaptării domeniului, faceți fine-tune unui clasificator pe labelurile IMDb atât pentru checkpointurile DistilBERT preantrenate, cât și pentru cele fine-tuned. Dacă aveți nevoie de o recapitulare a clasificării textului, consultați Capitolul 3.

Update on GitHub