Pytorch
TensorFlow

#
În spatele pipeline-ului

Aceasta este prima secțiune în care conținutul este ușor diferit în funcție de utilizarea PyTorch sau TensorFlow. Schimbați comutatorul din partea de sus a titlului pentru a selecta platforma pe care o preferați!

Să începem cu un exemplu complet, aruncând o privire la ceea ce s-a întâmplat în spate atunci când am executat următorul cod în Capitolul 1:

    from transformers import pipeline

    classifier = pipeline("sentiment-analysis")
    classifier(
        [
            "I've been waiting for a HuggingFace course my whole life.",
            "I hate this so much!",
        ]
    )

și am obținut:

    [{'label': 'POSITIVE', 'score': 0.9598047137260437},
     {'label': 'NEGATIVE', 'score': 0.9994558095932007}]

După cum am văzut în Capitolul 1, acest pipeline grupează trei etape: preprocesarea, trecerea intrărilor prin model și postprocesarea:

Să trecem rapid prin fiecare dintre acestea.

##
Preprocesarea cu un tokenizator

La fel ca alte rețele neuronale, modelele Transformer nu pot procesa direct text brut, astfel încât primul pas al pipeline-ului nostru este de a converti intrările de text în numere pe care modelul le poate înțelege. Pentru a face acest lucru, folosim un tokenizer, care va fi responsabil pentru:

- Împărțirea datelor de intrare în cuvinte, subcuvinte sau simboluri (cum ar fi punctuația) care se numesc tokens
- Maparea fiecărui token într-un număr întreg
- Adăugarea de intrări suplimentare care pot fi utile pentru model

Toată această preprocesare trebuie efectuată exact în același mod ca atunci când modelul a fost preantrenat, așa că mai întâi trebuie să descărcăm aceste informații din Model Hub. Pentru a face acest lucru, folosim clasa `AutoTokenizer` și metoda sa `from_pretrained()`. Folosind numele checkpoint-ului modelului nostru, aceasta va prelua automat datele asociate cu tokenizer-ul modelului și le va stoca în cache (astfel încât acestea să fie descărcate doar prima dată când executați codul de mai jos).

Deoarece punctul de control implicit al pipeline-ului `sentiment-analysis` este `distilbert-base-uncased-finetuned-sst-2-english` (puteți vedea fișa modelului aici), executăm următoarele:

    from transformers import AutoTokenizer

    checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
    tokenizer = AutoTokenizer.from_pretrained(checkpoint)

Odată ce avem tokenizatorul, putem să îi transmitem direct propozițiile noastre și vom primi înapoi un dicționar care este gata să fie introdus în modelul nostru! Singurul lucru rămas de făcut este să convertim lista de ID-uri de intrare în tensori.

Puteți utiliza 🤗 Transformers fără a trebui să vă faceți griji cu privire la cadrul ML utilizat ca backend; ar putea fi PyTorch sau TensorFlow, sau Flax pentru unele modele. Cu toate acestea, modelele Transformer acceptă numai tensori ca intrare. Dacă este prima dată când auziți despre tensori, vă puteți gândi la ei ca la array-uri NumPy. Un array NumPy poate fi un scalar (0D), un vector (1D), o matrice (2D) sau poate avea mai multe dimensiuni. Este de fapt un tensor; tensorii altor cadre ML se comportă similar și sunt de obicei la fel de simplu de instanțiat ca și array-urile NumPy.

Pentru a specifica tipul de tensori pe care dorim să îi primim înapoi (PyTorch, TensorFlow sau NumPy simplu), folosim argumentul `return_tensors`:

    raw_inputs = [
        "I've been waiting for a HuggingFace course my whole life.",
        "I hate this so much!",
    ]
    inputs = tokenizer(raw_inputs, padding=True, truncation=True, return_tensors="pt")
    print(inputs)

Nu vă faceți încă griji cu privire la padding și trunchiere; le vom explica mai târziu. Principalele lucruri de reținut aici sunt că puteți trece o propoziție sau o listă de propoziții, precum și specificarea tipului de tensori pe care doriți să îi primiți înapoi (dacă nu este specificat niciun tip, veți primi o listă de liste ca rezultat).

Iată cum arată rezultatele ca tensori PyTorch:

    {
        'input_ids': tensor([
            [  101,  1045,  1005,  2310,  2042,  3403,  2005,  1037, 17662, 12172, 2607,  2026,  2878,  2166,  1012,   102],
            [  101,  1045,  5223,  2023,  2061,  2172,   999,   102,     0,     0,     0,     0,     0,     0,     0,     0]
        ]),
        'attention_mask': tensor([
            [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
            [1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]
        ])
    }

Rezultatul în sine este un dicționar care conține două chei, `input_ids` și `attention_mask`. `input_ids` conține două rânduri de numere întregi (unul pentru fiecare propoziție) care sunt identificatorii unici ai simbolurilor din fiecare propoziție. Vom explica ce este `attention_mask` mai târziu în acest capitol.

##
Parcurgerea modelului

Putem descărca modelul nostru preantrenat în același mod în care am făcut-o cu tokenizatorul nostru. 🤗 Transformers oferă o clasă `AutoModel` care are și o metodă `from_pretrained()`:

    from transformers import AutoModel

    checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
    model = AutoModel.from_pretrained(checkpoint)

În acest fragment de cod, am descărcat același checkpoint pe care l-am folosit anterior în pipeline-ul nostru (de fapt, ar fi trebuit să fie deja în cache) și am instanțiat un model cu acesta.

Această arhitectură conține doar modulul Transformer de bază: având în vedere anumite intrări, acesta produce ceea ce vom numi stări ascunse, cunoscute și ca caracteristici. Pentru fiecare intrare a modelului, vom extrage un vector multidimensional care reprezintă înțelegerea contextuală a intrării respective de către modelul Transformer.

Dacă acest lucru nu are sens, nu vă faceți griji. Vom explica totul mai târziu.

Deși aceste stări ascunse pot fi utile pe cont propriu, ele sunt de obicei intrări pentru o altă parte a modelului, cunoscută sub numele de head. În Capitolul 1, diferitele sarcini ar fi putut fi efectuate cu aceeași arhitectură, dar fiecăreia dintre aceste sarcini îi va fi asociat un head diferit.

###
Un vector multidimensional?

Vectorul emis de modulul Transformator este de obicei de dimensiuni mari. Acesta are în general trei dimensiuni:

- Dimensiunea batch-ului: Numărul de secvențe prelucrate simultan (2 în exemplul nostru).
- Lungimea secvenței: Lungimea reprezentării numerice a secvenței (16 în exemplul nostru).
- Dimensiunea ascunsă: Dimensiunea vectorială a fiecărei intrări a modelului.

Se spune că este multidimensional din cauza ultimei valori. Dimensiunea ascunsă poate fi foarte mare (768 este comună pentru modelele mai mici, iar în modelele mai mari aceasta poate ajunge la 3072 sau mai mult).

Putem vedea acest lucru dacă introducem în modelul nostru intrările pe care le-am preprocesat:

    outputs = model(**inputs)
    print(outputs.last_hidden_state.shape)

    torch.Size([2, 16, 768])

Rețineți că ieșirile modelelor 🤗 Transformers se comportă ca `namedtuple`s sau dicționare. Puteți accesa elementele prin atribute (așa cum am făcut noi) sau prin cheie (`outputs[„last_hidden_state”]`), sau chiar prin index dacă știți exact unde se află lucrul pe care îl căutați (`outputs[0]`).

###
Modele de head-uri: Înțelegerea numerelor

Head-urile modelelor iau ca intrare vectorul multidimensional al stărilor ascunse și le proiectează pe o altă dimensiune. Acestea sunt de obicei compuse din unul sau câteva straturi liniare:

Rezultatul modelului Transformer este trimis direct la head-ul modelului pentru a fi prelucrat.

În această diagramă, modelul este reprezentat de stratul său de încorporare și de straturile următoare. Stratul de încorporare convertește fiecare ID de intrare din intrarea tokenizată într-un vector care reprezintă tokenul asociat. Straturile ulterioare manipulează acești vectori folosind mecanismul de atenție pentru a produce reprezentarea finală a propozițiilor.

Există multe arhitecturi diferite disponibile în 🤗 Transformers, fiecare fiind concepută în jurul abordării unei sarcini specifice. Iată o listă neexhaustivă:

- `*Model` (extragerea stărilor ascunse)
- `*ForCausalLM`
- `*ForMaskedLM`
- `*ForMultipleChoice`
- `*ForQuestionAnswering`
- `*ForSequenceClassification`
- `*ForTokenClassification`
- și altele 🤗

Pentru exemplul nostru, vom avea nevoie de un model cu un head de clasificare a secvențelor (pentru a putea clasifica propozițiile ca fiind pozitive sau negative). Așadar, nu vom utiliza clasa `AutoModel`, ci `AutoModelForSequenceClassification`:

    from transformers import AutoModelForSequenceClassification

    checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
    model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
    outputs = model(**inputs)

Acum, dacă ne uităm la forma ieșirilor noastre, dimensiunea va fi mult mai mică: head-ul modelului ia ca intrare vectorii multidimensionali pe care i-am văzut înainte și scoate vectori care conțin două valori (una pentru fiecare etichetă):

    print(outputs.logits.shape)

    torch.Size([2, 2])

Deoarece avem doar două propoziții și două etichete, rezultatul pe care îl obținem din modelul nostru este de forma 2 x 2.

##
Postprocesarea rezultatului

Valorile pe care le obținem ca rezultat al modelului nostru nu au neapărat sens în sine. Să aruncăm o privire:

    print(outputs.logits)

    tensor([[-1.5607,  1.6123],
            [ 4.1692, -3.3464]], grad_fn=<AddmmBackward>)

Modelul nostru a prezis `[-1.5607, 1.6123]` pentru prima propoziție și `[ 4.1692, -3.3464]` pentru cea de-a doua. Acestea nu sunt probabilități, ci logits, scorurile brute, nenormalizate, emise de ultimul strat al modelului. Pentru a fi convertite în probabilități, acestea trebuie să treacă printr-un strat SoftMax (toate modelele 🤗 Transformers produc logits, deoarece funcția de pierdere pentru formare va fuziona în general ultima funcție de activare, cum ar fi SoftMax, cu funcția de pierdere reală, cum ar fi entropia încrucișată):

    import torch

    predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
    print(predictions)

    tensor([[4.0195e-02, 9.5980e-01],
            [9.9946e-01, 5.4418e-04]], grad_fn=<SoftmaxBackward>)

Acum putem vedea că modelul a prezis `[0.0402, 0.9598]` pentru prima propoziție și `[0.9995, 0.0005]` pentru cea de-a doua. Acestea sunt scoruri de probabilitate care pot fi recunoscute.

Pentru a obține etichetele corespunzătoare fiecărei poziții, putem inspecta atributul `id2label` din configurația modelului (mai multe despre acest lucru în secțiunea următoare):

    model.config.id2label

    {0: 'NEGATIVE', 1: 'POSITIVE'}

Acum putem concluziona că modelul a prezis următoarele:

- Prima propoziție: NEGATIV: 0.0402, POZITIV: 0.9598
- A doua propoziție: NEGATIVĂ: 0.9995, POZITIVĂ: 0.0005

Am reprodus cu succes cele trei etape ale pipeline-ului: preprocesarea cu tokenizatoare, trecerea intrărilor prin model și postprocesarea! Acum haideți să analizăm în profunzime fiecare dintre aceste etape.

✏️ Încercați! Alegeți două (sau mai multe) texte proprii și treceți-le prin conducta `sentiment-analysis`. Apoi repetați pașii pe care i-ați văzut aici și verificați dacă obțineți aceleași rezultate!

< > Update on GitHub