LLM Course documentation
Fine-tuningul la un masked language model
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_datasetDatasetDict({
    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 nici0, nici1. În același timp, ați putea verifica și dacă labelurile din segmenteletrainșitestsunt într-adevăr0sau1- 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_datasetsDatasetDict({
    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
512Această 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_lengtheste î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 = 128Reț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 resultObservaț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_datasetsDatasetDict({
    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()cutokenizer.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()cutokenizer.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_datasetDatasetDict({
    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 loginin 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.75Un 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.32Grozav - 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.729503505340409Miș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!
Update on GitHub✏️ Î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.