LLM Course documentation

Antrenarea de la zero a unui model de limbaj cauzal

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Antrenarea de la zero a unui model de limbaj cauzal

Ask a Question Open In Colab Open In Studio Lab

Până acum, am folosit în principal modele preantrenate și le-am făcut fine-tuning pentru noi cazuri de utilizare prin reutilizarea weighturilor din preantrenare. După cum am văzut în Capitolul 1, acest lucru este denumit în mod obișnuit învățare prin transfer și este o strategie foarte reușită pentru aplicarea modelelor Transformer la majoritatea cazurilor de utilizare din lumea reală în care datele etichetate sunt puține. În acest capitol, vom adopta o abordare diferită și vom antrena un model complet nou de la zero. Aceasta este o abordare bună dacă aveți multe date și este foarte diferită de datele de preantrenare utilizate pentru modelele disponibile. Cu toate acestea, preantrenarea unui model lingvistic necesită, de asemenea, mult mai multe resurse de calcul decât fine-tuningul unui model existent. Printre exemplele în care poate fi utilă antrenarea unui nou model se numără dataseturile formate din note muzicale, secvențe moleculare precum ADN sau limbaje de programare. Acestea din urmă au câștigat recent teren datorită unor instrumente precum TabNine și Copilot de la GitHub, alimentate de modelul Codex al OpenAI, care pot genera secvențe lungi de cod. Această sarcină de generare a textului este cel mai bine abordată cu modele de limbaj autoregresive sau cauzale, cum ar fi GPT-2.

În această secțiune vom construi o versiune la scară redusă a unui model de generare a codului: ne vom concentra pe completări de o linie în loc de funcții sau clase complete, folosind un subset de cod Python. Atunci când lucrați cu date în Python, sunteți în contact frecvent Python data science stack, formată din bibliotecile matplotlib, seaborn, pandas și scikit-learn. Atunci când se utilizează aceste cadre, este frecvent să fie nevoie să se caute comenzi specifice, astfel încât ar fi bine dacă am putea utiliza un model care să efectueze aceste apeluri pentru noi.

În Capitolul 6 am creat un tokenizer eficient pentru a procesa codul sursă Python, dar avem nevoie de un dataset la scară largă pe care să preantrenăm un model. Aici, vom aplica tokenizerul nostru la un corpus de cod Python derivat din repositoriile GitHub. Vom utiliza apoi API-ul Trainer și 🤗 Accelerate pentru a antrena modelul. Să trecem la treabă!

Aceasta este de fapt o prezentare a modelului care a fost antrenat și încărcat în Hub folosind codul prezentat în această secțiune. Îl puteți găsi aici. Rețineți că, deoarece are loc o anumită randomizare în generarea textului, veți obține probabil un rezultat ușor diferit.

Colectarea datelor

Codul Python este disponibil din abundență în repositorii de cod, cum ar fi GitHub, pe care le putem utiliza pentru a crea un dataset prin scraping pentru fiecare repositoriu Python. Aceasta a fost abordarea adoptată în Transformers textbook pentru a preantrena un model GPT-2. Folosind o descărcare GitHub de aproximativ 180 GB care conține aproximativ 20 de milioane de fișiere Python numită codeparrot, autorii au construit un dataset pe care l-au oferit apoi pe Hugging Face Hub.

Cu toate acestea, antrenarea pe întregul corpus consumă timp și puterea calculatorului, iar noi avem nevoie doar de subsetul datasetului referitor la Python data science stack. Așadar, să începem prin filtrarea datasetului codeparrot pentru toate fișierele care includ oricare dintre bibliotecile din aceast stack. Din cauza dimensiunii datasetului, dorim să evităm descărcarea acestuia; în schimb, vom utiliza funcția de streaming pentru a-l filtra din mers. Pentru a ne ajuta să filtrăm exemplele de cod care utilizează bibliotecile pe care le-am menționat mai devreme, vom utiliza următoarea funcție:

def any_keyword_in_string(string, keywords):
    for keyword in keywords:
        if keyword in string:
            return True
    return False

Să-l testăm pe două exemple:

filters = ["pandas", "sklearn", "matplotlib", "seaborn"]
example_1 = "import numpy as np"
example_2 = "import pandas as pd"

print(
    any_keyword_in_string(example_1, filters), any_keyword_in_string(example_2, filters)
)
False True

Putem folosi acest lucru pentru a crea o funcție care va transmite în flux datasetul și va filtra elementele dorite:

from collections import defaultdict
from tqdm import tqdm
from datasets import Dataset


def filter_streaming_dataset(dataset, filters):
    filtered_dict = defaultdict(list)
    total = 0
    for sample in tqdm(iter(dataset)):
        total += 1
        if any_keyword_in_string(sample["content"], filters):
            for k, v in sample.items():
                filtered_dict[k].append(v)
    print(f"{len(filtered_dict['content'])/total:.2%} of data after filtering.")
    return Dataset.from_dict(filtered_dict)

Apoi, putem aplica pur și simplu această funcție pe datasetului din flux:

# This cell will take a very long time to execute, so you should skip it and go to
# the next one!
from datasets import load_dataset

split = "train"  # "valid"
filters = ["pandas", "sklearn", "matplotlib", "seaborn"]

data = load_dataset(f"transformersbook/codeparrot-{split}", split=split, streaming=True)
filtered_data = filter_streaming_dataset(data, filters)
3.26% of data after filtering.

Acest lucru ne lasă cu aproximativ 3% din setul de date original, care este încă destul de mare - datasetul rezultat este de 6 GB și constă din 600.000 de scripturi Python!

Filtrarea datasetului complet poate dura 2-3 ore, în funcție de calculator și de bandwidth. Dacă nu doriți să parcurgeți singur acest proces îndelungat, vă punem la dispoziție datasetul filtrat pe Hub pentru a-l descărca:

from datasets import load_dataset, DatasetDict

ds_train = load_dataset("huggingface-course/codeparrot-ds-train", split="train")
ds_valid = load_dataset("huggingface-course/codeparrot-ds-valid", split="validation")

raw_datasets = DatasetDict(
    {
        "train": ds_train,  # .shuffle().select(range(50000)),
        "valid": ds_valid,  # .shuffle().select(range(500))
    }
)

raw_datasets
DatasetDict({
    train: Dataset({
        features: ['repo_name', 'path', 'copies', 'size', 'content', 'license'],
        num_rows: 606720
    })
    valid: Dataset({
        features: ['repo_name', 'path', 'copies', 'size', 'content', 'license'],
        num_rows: 3322
    })
})

Preantrenarea modelului de limbaj va dura ceva timp. Vă sugerăm să rulați mai întâi bucla de antrenare pe un sample de date prin decomentarea celor două linii parțiale de mai sus și să vă asigurați că antrenarea se finalizează cu succes și că modelele sunt stocate. Nimic nu este mai frustrant decât o rulare de antrenare care eșuează la ultimul pas pentru că ați uitat să creați un folder sau pentru că există o greșeală de tipar la sfârșitul buclei de antrenare!

Să ne uităm la un exemplu din dataset. Vom arăta doar primele 200 de caractere din fiecare câmp:

for key in raw_datasets["train"][0]:
    print(f"{key.upper()}: {raw_datasets['train'][0][key][:200]}")
'REPO_NAME: kmike/scikit-learn'
'PATH: sklearn/utils/__init__.py'
'COPIES: 3'
'SIZE: 10094'
'''CONTENT: """
The :mod:`sklearn.utils` module includes various utilites.
"""

from collections import Sequence

import numpy as np
from scipy.sparse import issparse
import warnings

from .murmurhash import murm
LICENSE: bsd-3-clause'''

Putem vedea căci câmpul content conține codul pe care dorim ca modelul nostru să se antreneze. Acum că avem un dataset, trebuie să pregătim textele astfel încât acestea să fie într-un format adecvat pentru preantrenare.

Pregătirea datasetului

Primul pas va fi tokenizarea datelor, astfel încât să le putem utiliza pentru antrenare. Deoarece obiectivul nostru este de a autocompleta în principal apeluri scurte de funcții, putem păstra dimensiunea contextului relativ mică. Acest lucru are avantajul că putem antrena modelul mult mai rapid și că necesită semnificativ mai puțină memorie. Dacă este important pentru aplicația voastră să aveți mai mult context (de exemplu, dacă doriți ca modelul să scrie teste unitare pe baza unui fișier cu definiția funcției), asigurați-vă că măriți acest număr, dar rețineți, de asemenea, că acest lucru vine cu utilizare mai mare de memorie GPU. Pentru moment, să fixăm dimensiunea contextului la 128 de tokeni, spre deosebire de 1 024 sau 2 048 utilizate în GPT-2 sau respectiv GPT-3.

Majoritatea documentelor conțin mult mai mult de 128 de cuvinte, astfel încât trunchierea simplă a inputurilor la lungimea maximă ar elimina o mare parte din datasetul nostru. În schimb, vom utiliza opțiunea return_overflowing_tokens pentru a tokeniza întreagul input și a o împărți în mai multe bucăți, așa cum am făcut în Capitolul 6. De asemenea, vom utiliza opțiunea return_length pentru a returna automat lungimea fiecărui fragment creat. Adesea, ultimul fragment va fi mai mic decât dimensiunea contextului, iar noi vom scăpa de aceste bucăți pentru a evita problemele de padding; nu avem nevoie de ele, deoarece oricum avem o mulțime de date.

Fragmentarea unui text mare în mai multe bucăți.

Să vedem exact cum funcționează acest lucru analizând primele două exemple:

from transformers import AutoTokenizer

context_length = 128
tokenizer = AutoTokenizer.from_pretrained("huggingface-course/code-search-net-tokenizer")

outputs = tokenizer(
    raw_datasets["train"][:2]["content"],
    truncation=True,
    max_length=context_length,
    return_overflowing_tokens=True,
    return_length=True,
)

print(f"Input IDs length: {len(outputs['input_ids'])}")
print(f"Input chunk lengths: {(outputs['length'])}")
print(f"Chunk mapping: {outputs['overflow_to_sample_mapping']}")
Input IDs length: 34
Input chunk lengths: [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 117, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 41]
Chunk mapping: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

Putem vedea că obținem 34 de segmente în total din aceste două exemple. Uitându-ne la lungimea segmentelor, putem vedea că segmentele de la sfârșitul ambelor documente au mai puțin de 128 de token-uri (117 și, respectiv, 41). Acestea reprezintă doar o mică parte din totalul segmentelor pe care le avem, așa că le putem șterge în siguranță. Cu ajutorul câmpului overflow_to_sample_mapping, putem, de asemenea, să reconstituim care segmente au aparținut căror probe de intrare.

Cu această operațiune folosim o caracteristică utilă a funcției Dataset.map() din 🤗 Datasets, și anume că nu necesită one-to-one maps; așa cum am văzut în secțiunea 3, putem crea batch-uri cu mai multe sau mai puține elemente decât batch-ul de intrare. Acest lucru este util atunci când efectuăm operațiuni precum augmentarea sau filtrarea datelor care modifică numărul de elemente. În cazul nostru, atunci când tokenizăm fiecare element în segmente de dimensiunea contextului specificat, creăm multe probe din fiecare document. Trebuie doar să ne asigurăm că ștergem coloanele existente, deoarece acestea au o dimensiune conflictuală. Dacă am dori să le păstrăm, am putea să le repetăm în mod corespunzător și să le returnăm în cadrul apelului Dataset.map():

def tokenize(element):
    outputs = tokenizer(
        element["content"],
        truncation=True,
        max_length=context_length,
        return_overflowing_tokens=True,
        return_length=True,
    )
    input_batch = []
    for length, input_ids in zip(outputs["length"], outputs["input_ids"]):
        if length == context_length:
            input_batch.append(input_ids)
    return {"input_ids": input_batch}


tokenized_datasets = raw_datasets.map(
    tokenize, batched=True, remove_columns=raw_datasets["train"].column_names
)
tokenized_datasets
DatasetDict({
    train: Dataset({
        features: ['input_ids'],
        num_rows: 16702061
    })
    valid: Dataset({
        features: ['input_ids'],
        num_rows: 93164
    })
})

Avem acum 16,7 milioane de exemple cu 128 de tokenii fiecare, ceea ce corespunde unui total de aproximativ 2,1 miliarde de tokeni. Ca referință, modelele GPT-3 și Codex ale OpenAI sunt antrenate pe 300 și, respectiv, 100 de miliarde de tokeni, unde modelele Codex sunt inițializate din checkpointurile GPT-3. Scopul nostru în această secțiune nu este de a concura cu aceste modele, care pot genera texte lungi și coerente, ci de a crea o versiune la scară redusă care să ofere o funcție rapidă de autocompletare pentru data scientists.

Acum că avem datasetul gata, hai să configurăm modelul!

✏️ Încercați! Eliminarea tuturor bucăților care sunt mai mici decât dimensiunea contextului nu a fost o problemă majoră aici, deoarece folosim ferestre de context mici. Pe măsură ce creșteți dimensiunea contextului (sau dacă aveți un corpus de documente scurte), fracțiunea de segmente care sunt aruncate va crește și ea. O modalitate mai eficientă de a pregăti datele este de a uni toate sampleurile tokenizate într-un batch cu un token eos_token_id între ele, iar apoi de a efectua chunkingul pe secvențele concatenate. Ca exercițiu, modificați funcția tokenize() pentru a utiliza această abordare. Rețineți că veți dori să setați truncation=False și să eliminați celelalte argumente din tokenizer pentru a obține secvența completă de token IDs.

Inițializarea unui nou model

Primul nostru pas este să inițializăm un model GPT-2. Vom utiliza aceeași configurație pentru modelul nostru ca și pentru modelul GPT-2 mic, deci încărcăm configurația preantrenată, ne asigurăm că dimensiunea tokenizerlui corespunde cu dimensiunea vocabularului modelului și transmitem ID-urile tokenilor bos și eos (începutul și sfârșitul secvenței):

from transformers import AutoTokenizer, GPT2LMHeadModel, AutoConfig

config = AutoConfig.from_pretrained(
    "gpt2",
    vocab_size=len(tokenizer),
    n_ctx=context_length,
    bos_token_id=tokenizer.bos_token_id,
    eos_token_id=tokenizer.eos_token_id,
)

Cu această configurație, putem încărca un nou model. Rețineți că aceasta este prima dată când nu folosim funcția from_pretrained(), deoarece inițializăm noi înșine un model:

model = GPT2LMHeadModel(config)
model_size = sum(t.numel() for t in model.parameters())
print(f"GPT-2 size: {model_size/1000**2:.1f}M parameters")
GPT-2 size: 124.2M parameters

Modelul nostru are 124 milioane de parametri pe care va trebui să le facem tune. Înainte de a începe antrenarea, trebuie să configurăm un data collator care se va ocupa de crearea batch-urilor. Putem utiliza colatorul DataCollatorForLanguageModeling, care este conceput special pentru modelarea limbajului (după cum sugerează subtil numele). Pe lângă stacking și paddingul batchurilor, acesta se ocupă și de crearea labelurilor modelului lingvistic - în modelarea cauzală a limbajului, inputurile servesc și ca labels (doar că sunt decalate cu un element), iar acest data collator le creează din mers în timpul antrenării, astfel încât să nu fie nevoie să duplicăm input_ids.

Rețineți că DataCollatorForLanguageModeling acceptă atât masked language masking (MLM), cât și causal language modeling(CLM). În mod implicit, acesta pregătește datele pentru MLM, dar putem trece la CLM prin setarea argumentului mlm=False:

from transformers import DataCollatorForLanguageModeling

tokenizer.pad_token = tokenizer.eos_token
data_collator = DataCollatorForLanguageModeling(tokenizer, mlm=False)

Să aruncăm o privire la un exemplu:

out = data_collator([tokenized_datasets["train"][i] for i in range(5)])
for key in out:
    print(f"{key} shape: {out[key].shape}")
input_ids shape: torch.Size([5, 128])
attention_mask shape: torch.Size([5, 128])
labels shape: torch.Size([5, 128])

Putem vedea că exemplele sunt stacked și că toți tensorii au aceeași formă.

⚠️ Schimbarea inputurilor și a labelurilor pentru a le alinia are loc în interiorul modelului, astfel încât data collatorului doar copiază inputurile pentru a crea labeluri.

Acum avem totul pregătit pentru a ne antrena modelul - până la urmă nu a fost atât de greu! Înainte de a începe antrenamentul, trebuie să ne conectăm la Hugging Face. Dacă lucrați într-un notebook, puteți face acest lucru cu următoarea funcție de utilitate:

from huggingface_hub import notebook_login

notebook_login()

Aceasta va afișa un widget în care puteți introduce datele voastre de autentificare Hugging Face.

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

huggingface-cli login

Tot ce a mai rămas de făcut este să configurăm argumentele de antrenare și să pornim Trainer-ul. Vom utiliza un cosine learning rate schedule cu un warmup și o dimensiune efectivă a batch-ului de 256 (per_device_train_batch_size * gradient_accumulation_steps). Acumularea gradientului este utilizată atunci când un singur batch nu încape în memorie și construiește treptat gradientul prin mai multe treceri înainte/înapoi. Vom vedea acest lucru în acțiune atunci când vom crea bucla de antrenare cu 🤗 Accelerate.

from transformers import Trainer, TrainingArguments

args = TrainingArguments(
    output_dir="codeparrot-ds",
    per_device_train_batch_size=32,
    per_device_eval_batch_size=32,
    evaluation_strategy="steps",
    eval_steps=5_000,
    logging_steps=5_000,
    gradient_accumulation_steps=8,
    num_train_epochs=1,
    weight_decay=0.1,
    warmup_steps=1_000,
    lr_scheduler_type="cosine",
    learning_rate=5e-4,
    save_steps=5_000,
    fp16=True,
    push_to_hub=True,
)

trainer = Trainer(
    model=model,
    tokenizer=tokenizer,
    args=args,
    data_collator=data_collator,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["valid"],
)

Acum putem doar să pornim Trainer-ul și să așteptăm ca antrenamentul să se termine. În funcție de executarea antrenării pe întregul set de antrenarea sau pe un subset al acestuia, va dura 20 minute, sau respectiv 2 ore, așa că luați câteva cafeluțe și o carte bună de citit!

trainer.train()

După finalizarea antrenării, putem trimite modelul și tokenizerul către Hub:

trainer.push_to_hub()

✏️ Try it out! Ne-a luat doar aproximativ 30 de linii de cod în plus față de TrainingArguments pentru a ajunge de la texte brute la antrenarea GPT-2. Încercați antrenarea cu propriul dataset și vedeți dacă puteți obține rezultate bune!

💡 Dacă aveți acces la un calculator cu mai multe GPU-uri, încercați să rulați codul acolo. Trainer gestionează automat mai multe calculatoare, iar acest lucru poate accelera foarte mult antrenamentul.

Generarea codului cu un pipeline

Acum este momentul adevărului: să vedem cât de bine funcționează de fapt modelul antrenat! Putem vedea în loguri că pierderea a scăzut în mod constant, dar pentru a testa modelul, hai să vedem cât de bine funcționează la câteva încerări. Pentru a face acest lucru, vom încorpora modelul într-un pipeline de generare a textului și îl vom pune pe un GPU pentru generații rapide, dacă există unul disponibil:

import torch
from transformers import pipeline

device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
pipe = pipeline(
    "text-generation", model="huggingface-course/codeparrot-ds", device=device
)

Să începem cu sarcina simplă de a crea un scatter plot:

txt = """\
# create some data
x = np.random.randn(100)
y = np.random.randn(100)

# crearea unui scatter plot cu x, y
"""
print(pipe(txt, num_return_sequences=1)[0]["generated_text"])
# crearea unor date
x = np.random.randn(100)
y = np.random.randn(100)

# crearea scatter plot cu x, y
plt.scatter(x, y)

# crearea scatter

Rezultatul pare corect. Funcționează și pentru o operație pandas? Să vedem dacă putem crea un DataFrame din două array-uri:

txt = """\
# create some data
x = np.random.randn(100)
y = np.random.randn(100)

# creați dataframeul din x și y
"""
print(pipe(txt, num_return_sequences=1)[0]["generated_text"])
# create some data
x = np.random.randn(100)
y = np.random.randn(100)

# crează un dataframe din x și y
df = pd.DataFrame({'x': x, 'y': y})
df.insert(0,'x', x)
for

Excelent, acesta este răspunsul corect - deși apoi introduce din nou coloana x. Deoarece numărul de tokeni generate este limitat, următoarea buclă for este întreruptă. Să vedem dacă putem face ceva un pic mai complex și dacă modelul ne ajută să folosim operația groupby:

txt = """\
# dataframe with profession, income and name
df = pd.DataFrame({'profession': x, 'income':y, 'name': z})

# calculați venitul mediu pe profesie
"""
print(pipe(txt, num_return_sequences=1)[0]["generated_text"])
# dataframe with profession, income and name
df = pd.DataFrame({'profession': x, 'income':y, 'name': z})

# calculeză venitul mediu pe profesie
profession = df.groupby(['profession']).mean()

# calculează

Nu este rău; acesta este modul corect de a face acest lucru. În cele din urmă, să vedem dacă îl putem folosi și pentru scikit-learn și să configurăm un model Random Forest:

txt = """
# import random forest regressor from scikit-learn
from sklearn.ensemble import RandomForestRegressor

# ajustați modelul Random Forest cu 300 de estimatori pe X, y:
"""
print(pipe(txt, num_return_sequences=1)[0]["generated_text"])
# import random forest regressor from scikit-learn
from sklearn.ensemble import RandomForestRegressor

# ajustați modelul Random Forest cu 300 de estimatori pe X, y:
rf = RandomForestRegressor(n_estimators=300, random_state=random_state, max_depth=3)
rf.fit(X, y)
rf

Privind aceste câteva exemple, se pare că modelul a învățat o parte din sintaxa Python data science stack(desigur, ar trebui să o evaluăm mai bine înainte de a implementa modelul în lumea reală). Cu toate acestea, uneori este nevoie de o mai mare personalizare a antrenării modelului pentru a obține performanța necesară pentru un anumit caz de utilizare. De exemplu, dacă am dori să actualizăm dinamic dimensiunea batch-ului sau să avem o buclă de antrenare condiționată care trece peste exemplele proaste din mers? O opțiune ar fi să facem subclass la Trainer și să adăugăm modificările necesare, dar uneori este mai simplu să scriem bucla de antrenare de la zero. Aici intervine 🤗 Accelerate.

Antrenarea cu 🤗 Accelerate

Am văzut cum să antrenăm un model cu Trainer, care poate permite o anumită personalizare. Cu toate acestea, uneori dorim control deplin asupra buclei de antrenare sau dorim să facem unele schimbări exotice. În acest caz, 🤗 Accelerate este o alegere excelentă, iar în această secțiune vom parcurge pașii de utilizare a acestuia pentru a ne antrena modelul. Pentru a face lucrurile mai interesante, vom adăuga și un twist buclei de antrenare.

Deoarece suntem interesați în principal de o autocompletare sensibilă pentru bibliotecile din domeniul data science, este logic să acordăm mai multă importanță exemplelor de antrenare care utilizează mai mult aceste biblioteci. Putem identifica cu ușurință aceste exemple prin utilizarea unor cuvinte-cheie precum plt, pd, sk, fit și predict, care sunt cele mai frecvente nume de import pentru matplotlib.pyplot, pandas și sklearn, precum și modelul fit/predict al acestora din urmă. Dacă acestea sunt reprezentate fiecare ca un singur simbol, putem verifica cu ușurință dacă apar în secvența de input. Tokenii pot avea un prefix de spațiu, deci vom verifica și aceste versiuni în vocabularul tokenizerului. Pentru a verifica dacă funcționează, vom adăuga un token de test care ar trebui să fie împărțit în mai mulți tokeni:

keytoken_ids = []
for keyword in [
    "plt",
    "pd",
    "sk",
    "fit",
    "predict",
    " plt",
    " pd",
    " sk",
    " fit",
    " predict",
    "testtest",
]:
    ids = tokenizer([keyword]).input_ids[0]
    if len(ids) == 1:
        keytoken_ids.append(ids[0])
    else:
        print(f"Keyword has not single token: {keyword}")
'Keyword has not single token: testtest'

Grozav, se pare că funcționează bine! Acum putem scrie o funcție de pierdere personalizată care ia ca secvența de input, logurile și tokenii cheie pe care tocmai le-am selectat. În primul rând, trebuie să aliniem logurile și inputurile: secvența de intrare deplasată cu o unitate la dreapta formează labeluri, deoarece următorul tokenul este labelul pentru tokenul curent. Putem realiza acest lucru începând cu labelurile de la al doilea token al secvenței de intrare, deoarece modelul nu face o predicție pentru primul token în orice caz. Apoi tăiem ultimul logit, deoarece nu avem un label pentru tokenul care urmează secvenței complete de intrare. Astfel, putem calcula pierderea per sample și putem număra aparițiile tuturor cuvintelor-cheie în fiecare sample. În cele din urmă, calculăm media weighturilor pe fiecare sample folosind aparițiile ca weighturi. Deoarece nu dorim să eliminăm toate sampleurile care nu au cuvinte-cheie, adăugăm 1 la weighturi:

from torch.nn import CrossEntropyLoss
import torch


def keytoken_weighted_loss(inputs, logits, keytoken_ids, alpha=1.0):
    # Shift so that tokens < n predict n
    shift_labels = inputs[..., 1:].contiguous()
    shift_logits = logits[..., :-1, :].contiguous()
    # Calculate per-token loss
    loss_fct = CrossEntropyLoss(reduce=False)
    loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1))
    # Resize and average loss per sample
    loss_per_sample = loss.view(shift_logits.size(0), shift_logits.size(1)).mean(axis=1)
    # Calculate and scale weighting
    weights = torch.stack([(inputs == kt).float() for kt in keytoken_ids]).sum(
        axis=[0, 2]
    )
    weights = alpha * (1.0 + weights)
    # Calculate weighted average
    weighted_loss = (loss_per_sample * weights).mean()
    return weighted_loss

Înainte de a începe antrenamentul cu această nouă funcție de pierdere minunată, trebuie să pregătim câteva lucruri:

  • Avem nevoie de dataloaders pentru a încărca datele în batch-uri.
  • Trebuie să configurăm parametrii de scădere a weighturilor.
  • Din când în când, dorim să evaluăm, astfel încât este logic să includem codul de evaluare într-o funcție.

Să începem cu dataloaders. Trebuie doar să setăm formatul datasetului la "torch", iar apoi îl putem trece la un DataLoader PyTorch cu dimensiunea corespunzătoare a batch-lui:

from torch.utils.data.dataloader import DataLoader

tokenized_datasets.set_format("torch")
train_dataloader = DataLoader(tokenized_datasets["train"], batch_size=32, shuffle=True)
eval_dataloader = DataLoader(tokenized_datasets["valid"], batch_size=32)

În continuare, grupăm parametrii astfel încât optimizatorul să știe care dintre aceștia vor primi o scădere suplimentară a weighturilor. De obicei, toți termenii weighturilor bias și LayerNorm sunt scutiți de acest lucru; iată cum putem face acest lucru:

weight_decay = 0.1


def get_grouped_params(model, no_decay=["bias", "LayerNorm.weight"]):
    params_with_wd, params_without_wd = [], []
    for n, p in model.named_parameters():
        if any(nd in n for nd in no_decay):
            params_without_wd.append(p)
        else:
            params_with_wd.append(p)
    return [
        {"params": params_with_wd, "weight_decay": weight_decay},
        {"params": params_without_wd, "weight_decay": 0.0},
    ]

Deoarece dorim să evaluăm modelul în mod regulat pe setul de validare în timpul antrenării, trebuie să scriem o funcție și pentru acest lucru. Aceasta rulează pur și simplu prin dataloaderul de evaluare și adună toate pierderile în cadrul proceselor:

def evaluate():
    model.eval()
    losses = []
    for step, batch in enumerate(eval_dataloader):
        with torch.no_grad():
            outputs = model(batch["input_ids"], labels=batch["input_ids"])

        losses.append(accelerator.gather(outputs.loss))
    loss = torch.mean(torch.cat(losses))
    try:
        perplexity = torch.exp(loss)
    except OverflowError:
        perplexity = float("inf")
    return loss.item(), perplexity.item()

Cu funcția evaluate() putem raporta pierderile și perplexitatea la intervale regulate. În continuare, redefinim modelul nostru pentru a ne asigura că antrenăm din nou de la zero:

model = GPT2LMHeadModel(config)

Apoi putem defini optimizatorul nostru, folosind funcția de mai devreme pentru a împărți parametrii pentru scăderea weighturilor:

from torch.optim import AdamW

optimizer = AdamW(get_grouped_params(model), lr=5e-4)

Acum să pregătim modelul, optimizatorul și dataloaderurile, astfel încât să putem începe antrenamentul:

from accelerate import Accelerator

accelerator = Accelerator(fp16=True)

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

🚨 Dacă antrenați pe un TPU, va trebui să mutați tot codul începând cu celula de mai sus într-o funcție de antrenare dedicată. Consultați Capitolul 3 pentru mai multe detalii.

Acum că am trimis train_dataloader la accelerator.prepare(), putem utiliza lungimea acestuia pentru a calcula numărul de pași de antrenare. Rețineți că ar trebui să facem acest lucru întotdeauna după ce pregătim dataloaderurile, deoarece această metodă îi va modifica lungimea. Utilizăm un program liniar clasic de la rata de învățare la 0:

from transformers import get_scheduler

num_train_epochs = 1
num_update_steps_per_epoch = len(train_dataloader)
num_training_steps = num_train_epochs * num_update_steps_per_epoch

lr_scheduler = get_scheduler(
    name="linear",
    optimizer=optimizer,
    num_warmup_steps=1_000,
    num_training_steps=num_training_steps,
)

În cele din urmă, pentru a trimite modelul nostru către Hub, va trebui să creăm un obiect Repository într-un folder de lucru. În primul rând, conectați-vă la Hugging Face Hub, dacă nu sunteți deja conectat. Vom determina numele repositoriului pornind de la ID-ul modelului pe care dorim să îl atribuim modelului nostru (nu ezitați să înlocuiți repo_name cu propria alegere; acesta trebuie doar să conțină numele vostru de utilizator, ceea ce face funcția get_full_repo_name()):

from huggingface_hub import Repository, get_full_repo_name

model_name = "codeparrot-ds-accelerate"
repo_name = get_full_repo_name(model_name)
repo_name
'sgugger/codeparrot-ds-accelerate'

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

output_dir = "codeparrot-ds-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.

Înainte de antrenament, să efectuăm un test rapid pentru a vedea dacă funcția de evaluare funcționează corect:

evaluate()
(10.934126853942871, 56057.14453125)

Acestea sunt valori foarte ridicate pentru pierdere și perplexitate, dar acest lucru nu este surprinzător, deoarece nu am antrenat încă modelul. Astfel, avem totul pregătit pentru a scrie partea principală a scriptului de antrenare: bucla de antrenare. În bucla de antrenare, iterăm peste dataloader și transmitem batch-urile către model. Cu logurile, putem apoi evalua funcția noastră de pierdere personalizată. Redimensionăm pierderea în funcție de numărul de etape de acumulare a gradientului pentru a nu crea pierderi mai mari atunci când agregăm mai multe etape. Înainte de a optimiza, comprimăm, de asemenea, gradienții pentru o mai bună convergență. În cele din urmă, la fiecare câțiva pași, evaluăm modelul pe setul de evaluare cu noua noastră funcție evaluate():

from tqdm.notebook import tqdm

gradient_accumulation_steps = 8
eval_steps = 5_000

model.train()
completed_steps = 0
for epoch in range(num_train_epochs):
    for step, batch in tqdm(
        enumerate(train_dataloader, start=1), total=num_training_steps
    ):
        logits = model(batch["input_ids"]).logits
        loss = keytoken_weighted_loss(batch["input_ids"], logits, keytoken_ids)
        if step % 100 == 0:
            accelerator.print(
                {
                    "samples": step * samples_per_step,
                    "steps": completed_steps,
                    "loss/train": loss.item() * gradient_accumulation_steps,
                }
            )
        loss = loss / gradient_accumulation_steps
        accelerator.backward(loss)
        if step % gradient_accumulation_steps == 0:
            accelerator.clip_grad_norm_(model.parameters(), 1.0)
            optimizer.step()
            lr_scheduler.step()
            optimizer.zero_grad()
            completed_steps += 1
        if (step % (eval_steps * gradient_accumulation_steps)) == 0:
            eval_loss, perplexity = evaluate()
            accelerator.print({"loss/eval": eval_loss, "perplexity": perplexity})
            model.train()
            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 step {step}", blocking=False
                )

Și asta e tot - acum aveți propria buclă de antrenare personalizată pentru modele de limbaj cauzal, cum ar fi GPT-2, pe care o puteți personaliza în continuare în funcție de nevoile voastre.

✏️ încercați! Fie vă creați propria funcție de pierdere personalizată, adaptată la cazul vostru de utilizare, fie adăugați un alt pas personalizat în bucla de antrenare.

✏️ încercați! Atunci când efectuați experimente de antrenare de lungă durată, este o idee bună să înregistrați parametrii importanți utilizând instrumente precum TensorBoard sau Weights & Biases. Adăugați o logare adecvată la bucla de antrenare, astfel încât să puteți verifica întotdeauna cum decurge antrenarea.

< > Update on GitHub