Pytorch
TensorFlow

#
Dietro la pipeline

Questa è la prima sezione in cui il contenuto è leggermente diverso a seconda che si utilizzi PyTorch o TensorFlow. Attivate lo switch sopra il titolo per selezionare la tua piattaforma preferita!

Cominciamo con un esempio completo, dando un’occhiata a ciò che è successo dietro le quinte quando abbiamo eseguito il seguente codice nel Capitolo 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!",
        ]
    )

e ottenuto:

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

Come abbiamo visto nel Capitolo 1, questa pipeline raggruppa tre fasi: la pre-elaborazione, il passaggio degli input attraverso il modello e la post-elaborazione:

Esaminiamo rapidamente ciascuno di essi.

##
Preelaborazione con un tokenizer

Come altre reti neurali, i modelli Transformer non possono elaborare direttamente il testo non elaborato, quindi la prima fase della nostra pipeline consiste nel convertire gli input testuali in numeri che il modello possa interpretare. Per fare ciò, utilizziamo un tokenizer, che sarà responsabile di:

- Suddivisione dell’input in parole, sottoparole o simboli (come la punteggiatura) che vengono chiamati token.
- Mappare ogni token in un numero intero
- Aggiunta di ulteriori input che possono essere utili per il modello

Tutta questa preelaborazione deve essere fatta esattamente nello stesso modo in cui è stato preaddestrato il modello, quindi dobbiamo prima scaricare queste informazioni dal Model Hub. Per farlo, si usa la classe `AutoTokenizer` e il suo metodo `from_pretrained()`. Utilizzando il nome del checkpoint del nostro modello, recupererà automaticamente i dati associati al tokenizer del modello e li metterà in cache (in modo che vengano scaricati solo la prima volta che si esegue il codice sottostante).

Poiché il checkpoint predefinito della pipeline `sentiment-analysis` è `distilbert-base-uncased-finetuned-sst-2-english` (si può vedere la sua scheda modello qui), eseguiamo quanto segue:

    from transformers import AutoTokenizer

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

Una volta che abbiamo il tokenizer, possiamo passargli direttamente le nostre frasi e otterremo un dizionario pronto per il nostro modello! L’unica cosa che resta da fare è convertire l’elenco degli ID in ingresso in tensori.

È possibile utilizzare i 🤗 Transformer senza doversi preoccupare di quale framework ML venga utilizzato come backend;potrebbe essere PyTorch o TensorFlow, o Flax per alcuni modelli. Tuttavia, i modelli Transformer accettano solo tensors come input. Se è la prima volta che sentite parlare di tensori, potete pensare a loro come array NumPy. Un array NumPy può essere uno scalare (0D), un vettore (1D), una matrice (2D) o avere più dimensioni. Si tratta effettivamente di un tensore; i tensori di altri framework ML si comportano in modo simile e di solito sono semplici da istanziare come gli array NumPy.

Per specificare il tipo di tensori che vogliamo ottenere (PyTorch, TensorFlow o NumPy), usiamo l’argomento `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)

Non preoccupatevi ancora di padding e truncation; li spiegheremo più avanti.Le cose principali da ricordare sono che si può passare una frase o un elenco di frasi, oltre a specificare il tipo di tensori che si desidera ottenere (se non viene passato alcun tipo, si otterrà una lista di liste come risultato).

Ecco come appaiono i risultati come 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, 0, 0, 0, 0, 0, 0, 0, 0, 0]
        ])
    }

L’output stesso è un dizionario contenente due chiavi, `input_ids` e `attention_mask`. `input_ids` contiene due righe di interi (uno per ogni frase) che sono gli identificatori unici dei token in ogni frase. Spiegheremo cosa sia la `attention_mask` più avanti in questo capitolo.

##
Passare attraverso il modello

Possiamo scaricare il nostro modello preaddestrato nello stesso modo in cui abbiamo fatto con il nostro tokenizer. 🤗 Transformers fornisce una classe `AutoModel` che ha anche un metodo `from_pretrained()`:

    from transformers import AutoModel

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

In questo frammento di codice, abbiamo scaricato lo stesso checkpoint usato in precedenza nella nostra pipeline (in realtà dovrebbe essere già nella cache) e abbiamo istanziato un modello con esso.

Questa architettura contiene solo il modulo Transformer di base: dati alcuni input, produce quelli che chiameremo hidden states, noti anche come features. Per ogni input del modello, recupereremo un vettore ad alta dimensionalità che rappresenta la comprensione contestuale di quell’input da parte del modello Transformer.

Se per te tutto questo non ha senso, non preoccuparti. Ti spiegheremo tutto più avanti.

Anche se questi stati nascosti possono essere utili da soli, di solito sono input di un’altra parte del modello, nota come head. Nel Capitolo 1, i diversi compiti potrebbero essere eseguiti con la stessa architettura, ma a ciascuno di essi sarà associata una head diversa.

###
Un vettore ad alta dimensionalità?

Il vettore emesso dal modulo Transformer è solitamente di grandi dimensioni. In genere ha tre dimensioni:

- Dimensione del batch: Il numero di sequenze elaborate alla volta (2 nel nostro esempio).
- Lunghezza della sequenza: La lunghezza della rappresentazione numerica della sequenza (16 nel nostro esempio).
- Dimensione nascosta: La dimensione del vettore di ciascun ingresso del modello.

Si dice che è “ad alta dimensionalità” a causa dell’ultimo valore. La dimensione nascosta può essere molto grande (768 è comune per i modelli più piccoli, mentre nei modelli più grandi può arrivare a 3072 o più).

Lo possiamo vedere se alimentiamo il nostro modello con gli input che abbiamo preelaborato:

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

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

Si noti che gli output dei modelli 🤗 Transformers si comportano come `namedtuple` o dizionari. Si può accedere agli elementi per attributi (come abbiamo fatto noi) sia per chiave (`outputs["last_hidden_state"]`), sia per indice se si sa esattamente dove si trova ciò che si sta cercando (`outputs[0]`).

###
Model heads: Dare un senso ai numeri

Le model head prendono in input il vettore ad alta dimensione degli stati nascosti e lo proiettano su una dimensione diversa. Di solito sono composte da uno o pochi strati lineari:

Gli output del modello Transformer vengono inviati direttamente alla model head per essere elaborati.

In questo diagramma, il modello è rappresentato dallo strato embeddings e dagli strati successivi. Il livello embeddings converte ogni ID dell’input tokenizzato in un vettore che rappresenta il token associato. I livelli successivi manipolano questi vettori utilizzando il meccanismo di attenzione per produrre la rappresentazione finale delle frasi.

Esistono diverse architetture disponibili nei 🤗 Transformers, ognuna delle quali è stata progettata per affrontare un compito specifico. Ecco un elenco non esaustivo:

- `*Model` (retrieve the hidden states)
- `*ForCausalLM`
- `*ForMaskedLM`
- `*ForMultipleChoice`
- `*ForQuestionAnswering`
- `*ForSequenceClassification`
- `*ForTokenClassification`
- e altre 🤗

Per il nostro esempio, avremo bisogno di un modello con una classificazion head della sequenza (per poter classificare le frasi come positive o negative). Quindi, non useremo la classe `AutoModel`, ma `AutoModelForSequenceClassification`:

    from transformers import AutoModelForSequenceClassification

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

Ora, se osserviamo la forma dei nostri output, la dimensionalità sarà molto più bassa: la model head prende in input i vettori ad alta dimensionalità che abbiamo visto prima e produce vettori contenenti due valori (uno per etichetta):

    print(outputs.logits.shape)

    torch.Size([2, 2])

Dato che abbiamo solo due frasi e due etichette, il risultato che otteniamo dal nostro modello è di forma 2 x 2.

##
Postprocessing the output

I valori che otteniamo come output dal nostro modello non hanno necessariamente senso da soli. Diamo un’occhiata:

    print(outputs.logits)

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

Il nostro modello ha previsto `[-1.5607, 1.6123]` per la prima frase e `[ 4.1692, -3.3464]` per la seconda. Non si tratta di probabilità ma di logit, i punteggi non normalizzati emessi dall’ultimo livello del modello. Per poterli convertire in probabilità, devono passare attraverso un layer SoftMax (tutti i modelli 🤗 Transformers producono i logits, poiché la funzione di perdita per l’addestramento generalmente fonde l’ultima funzione di attivazione, come SoftMax, con la funzione di perdita effettiva, come la cross entropy):

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

Ora possiamo vedere che il modello ha previsto `[0,0402, 0,9598]` per la prima frase e `[0,9995, 0,0005]` per la seconda. Si tratta di punteggi di probabilità riconoscibili.

Per ottenere le etichette corrispondenti a ogni posizione, si può ispezionare l’attributo `id2label` della configurazione del modello (si veda la prossima sezione):

    model.config.id2label

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

Ora possiamo concludere che il modello ha previsto quanto segue:

- Prima frase: NEGATIVE: 0.0402, POSITIVE: 0.9598
- Seconda frase: NEGATIVE: 0.9995, POSITIVE: 0.0005

Abbiamo riprodotto con successo le tre fasi della pipeline: preelaborazione con i tokenizer, passaggio degli input attraverso il modello e postelaborazione! Ora prendiamoci un po’ di tempo per approfondire ognuna di queste fasi.

✏️ **Provaci anche tu!** Scegli due (o più) testi di tua proprietà e lanciali all'interno della pipeline `sentiment-analysis`. Successivamente, replica i passi che hai visto qui e verifica di aver ottenuto gli stessi risultati!