Modelli di codificatori-decodificatori basati su Transformer

Modelli basati su Transformer

!pip install transformers==4.2.1
!pip install sentencepiece==0.1.95

Il modello codificatore-decodificatore basato su trasformatori è stato introdotto da Vaswani et al. nel famoso articolo “Attention is all you need” ed è oggi l’architettura codificatore-decodificatore di fatto nello sviluppo del linguaggio naturale (NLP).

Recentemente, ci sono state molte ricerche su diversi obiettivi di pre-training per i modelli codificatore-decodificatore basati su trasformatori, ad esempio T5, Bart, Pegasus, ProphetNet, Marge, ecc…, ma l’architettura del modello è rimasta in gran parte la stessa.

Lo scopo del post sul blog è quello di fornire una spiegazione dettagliata di come i modelli di architettura codificatore-decodificatore basati su trasformatori risolvono i problemi di sequenza-a-sequenza. Ci concentreremo sul modello matematico definito dall’architettura e su come il modello può essere utilizzato nell’inferenza. Lungo il percorso, forniremo alcuni concetti di base sui modelli di sequenza-a-sequenza in NLP e analizzeremo l’architettura codificatore-decodificatore basata su trasformatori nelle sue parti di codificatore e decodificatore. Forniremo molte illustrazioni e stabiliremo il collegamento tra la teoria dei modelli codificatore-decodificatore basati su trasformatori e il loro utilizzo pratico in 🤗Transformers per l’inferenza. Si tenga presente che questo post sul blog non spiega come tali modelli possano essere addestrati: questo sarà argomento di un futuro post sul blog.

I modelli codificatore-decodificatore basati su trasformatori sono il risultato di anni di ricerca sull’apprendimento delle rappresentazioni e sulle architetture dei modelli. Questo notebook fornisce un breve riassunto della storia dei modelli codificatore-decodificatore neurali. Per maggiori dettagli, si consiglia al lettore di leggere questo fantastico post sul blog di Sebastion Ruder. Inoltre, si raccomanda una comprensione di base dell’architettura di auto-attenzione. Il seguente post sul blog di Jay Alammar serve come un buon ripasso del modello Transformer originale qui .

Al momento della stesura di questo notebook, 🤗Transformers comprende i modelli codificatore-decodificatore T5, Bart, MarianMT e Pegasus, che sono riassunti nella documentazione sotto riassunti dei modelli .

Il notebook è diviso in quattro parti:

  • Background – Viene fornita una breve storia dei modelli codificatore-decodificatore neurali con un focus sui modelli basati su RNN.
  • Codificatore-Decodificatore – Viene presentato il modello codificatore-decodificatore basato su trasformatori e viene spiegato come il modello viene utilizzato per l’inferenza.
  • Codificatore – Viene spiegata in dettaglio la parte del codificatore del modello.
  • Decodificatore – Viene spiegata in dettaglio la parte del decodificatore del modello.

Ogni parte si basa sulla parte precedente, ma può anche essere letta singolarmente.

Background

I compiti di generazione di linguaggio naturale (NLG), un sottocampo di NLP, sono meglio espressi come problemi di sequenza-a-sequenza. Tali compiti possono essere definiti come la ricerca di un modello che mappa una sequenza di parole di input in una sequenza di parole di destinazione. Alcuni esempi classici sono la sintesi e la traduzione . Nel seguito, si assume che ogni parola sia codificata in una rappresentazione vettoriale. n n n le parole di input possono quindi essere rappresentate come una sequenza di vettori di input n n n :

X 1 : n = { x 1 , … , x n } . \mathbf{X}_{1:n} = \{\mathbf{x}_1, \ldots, \mathbf{x}_n\}. X 1 : n ​ = { x 1 ​ , … , x n ​ } .

Di conseguenza, i problemi di sequenza-a-sequenza possono essere risolti trovando una mappatura f f f da una sequenza di n n n vettori di input X 1 : n \mathbf{X}_{1:n} X 1 : n ​ a una sequenza di m m m vettori di destinazione Y 1 : m \mathbf{Y}_{1:m} Y 1 : m ​ , dove il numero di vettori di destinazione m m m è sconosciuto a priori e dipende dalla sequenza di input:

f : X 1 : n → Y 1 : m . f: \mathbf{X}_{1:n} \to \mathbf{Y}_{1:m}. f
X 1 : n ​ → Y 1 : m ​ .

Sutskever et al. (2014) hanno osservato che le reti neurali profonde (DNN), “*nonostante la loro flessibilità e potenza, possono solo definire una mappatura i cui input e obiettivi possono essere sensatamente codificati con vettori di dimensionalità fissa.*” 1 {}^1 1

Utilizzare un modello DNN 2 {}^2 2 per risolvere problemi di sequenza-a-sequenza significherebbe che il numero di vettori target m m m deve essere noto a priori e deve essere indipendente dall’input X 1 : n \mathbf{X}_{1:n} X 1 : n ​ . Questo è subottimale perché, per compiti di NLG, il numero di parole target dipende di solito dall’input X 1 : n \mathbf{X}_{1:n} X 1 : n ​ e non solo dalla lunghezza dell’input n n n . Ad esempio, un articolo di 1000 parole può essere riassunto sia in 200 parole che in 100 parole a seconda del suo contenuto.

Nel 2014, Cho et al. e Sutskever et al. hanno proposto di utilizzare un modello di encoder-decoder basato esclusivamente su reti neurali ricorrenti (RNN) per compiti di sequenza-a-sequenza. A differenza delle DNNS, le RNN sono in grado di modellare una mappatura verso un numero variabile di vettori target. Approfondiamo un po’ il funzionamento dei modelli di encoder-decoder basati su RNN.

Nell’infereza, la RNN di encoder codifica una sequenza di input X 1 : n \mathbf{X}_{1:n} X 1 : n ​ aggiornando successivamente il suo stato nascosto 3 {}^3 3 . Dopo aver elaborato l’ultimo vettore di input x n \mathbf{x}_n x n ​ , lo stato nascosto dell’encoder definisce la codifica dell’input c \mathbf{c} c . Così, l’encoder definisce la mappatura:

f θ e n c : X 1 : n → c . f_{\theta_{enc}}: \mathbf{X}_{1:n} \to \mathbf{c}. f θ e n c ​ ​ : X 1 : n ​ → c .

Successivamente, lo stato nascosto del decoder viene inizializzato con la codifica dell’input e durante l’infereza, la RNN del decoder viene utilizzata per generare in modo auto-regressivo la sequenza target. Spieghiamo.

Matematicamente, il decoder definisce la distribuzione di probabilità di una sequenza target Y 1 : m \mathbf{Y}_{1:m} Y 1 : m ​ dato lo stato nascosto c \mathbf{c} c :

p θ d e c ( Y 1 : m ∣ c ) . p_{\theta_{dec}}(\mathbf{Y}_{1:m} |\mathbf{c}). p θ d e c ​ ​ ( Y 1 : m ​ ∣ c ) .

Applicando la regola di Bayes, la distribuzione può essere scomposta in distribuzioni condizionali di singoli vettori target come segue:

p θ d e c ( Y 1 : m ∣ c ) = ∏ i = 1 m p θ dec ( y i ∣ Y 0 : i − 1 , c ) . p_{\theta_{dec}}(\mathbf{Y}_{1:m} |\mathbf{c}) = \prod_{i=1}^{m} p_{\theta_{\text{dec}}}(\mathbf{y}_i | \mathbf{Y}_{0: i-1}, \mathbf{c}). p θ d e c ​ ​ ( Y 1 : m ​ ∣ c ) = i = 1 ∏ m ​ p θ dec ​ ​ ( y i ​ ∣ Y 0 : i − 1 ​ , c ) .

Quindi, se l’architettura può modellare la distribuzione condizionale del prossimo vettore target, dato tutti i vettori target precedenti:

p θ dec ( y i ∣ Y 0 : i − 1 , c ) , ∀ i ∈ { 1 , … , m } , p_{\theta_{\text{dec}}}(\mathbf{y}_i | \mathbf{Y}_{0: i-1}, \mathbf{c}), \forall i \in \{1, \ldots, m\}, p θ dec ​ ​ ( y i ​ ∣ Y 0 : i − 1 ​ , c ) , ∀ i ∈ { 1 , … , m } ,

allora può modellare la distribuzione di qualsiasi sequenza di vettori target dato lo stato nascosto c \mathbf{c} c semplicemente moltiplicando tutte le probabilità condizionali.

Quindi come l’architettura del decoder basato su RNN modella p θ dec ( y i ∣ Y 0
i − 1 , c ) p_{\theta_{\text{dec}}}(\mathbf{y}_i | \mathbf{Y}_{0: i-1}, \mathbf{c}) p θ dec ​ ​ ( y i ​ ∣ Y 0 : i − 1 ​ , c ) ?

In termini computazionali, il modello mappa sequenzialmente lo stato nascosto interno precedente c i − 1 \mathbf{c}_{i-1} c i − 1 ​ e il vettore obiettivo precedente y i − 1 \mathbf{y}_{i-1} y i − 1 ​ allo stato nascosto interno corrente c i \mathbf{c}_i c i ​ e al vettore logit l i \mathbf{l}_i l i ​ (mostrato in rosso scuro di seguito):

f θ dec ( y i − 1 , c i − 1 ) → l i , c i . f_{\theta_{\text{dec}}}(\mathbf{y}_{i-1}, \mathbf{c}_{i-1}) \to \mathbf{l}_i, \mathbf{c}_i. f θ dec ​ ​ ( y i − 1 ​ , c i − 1 ​ ) → l i ​ , c i ​ . c 0 \mathbf{c}_0 c 0 ​ è quindi definito come c \mathbf{c} c essendo lo stato nascosto di output dell’encoder basato su RNN. Successivamente, l’operazione softmax viene utilizzata per trasformare il vettore logit l i \mathbf{l}_i l i ​ in una distribuzione di probabilità condizionale del prossimo vettore obiettivo:

p ( y i ∣ l i ) = Softmax ( l i ) , con l i = f θ dec ( y i − 1 , c prev ) . p(\mathbf{y}_i | \mathbf{l}_i) = \textbf{Softmax}(\mathbf{l}_i), \text{ con } \mathbf{l}_i = f_{\theta_{\text{dec}}}(\mathbf{y}_{i-1}, \mathbf{c}_{\text{prev}}). p ( y i ​ ∣ l i ​ ) = Softmax ( l i ​ ) , con l i ​ = f θ dec ​ ​ ( y i − 1 ​ , c prev ​ ) .

Per maggiori dettagli sul vettore logit e sulla distribuzione di probabilità risultante, si prega di consultare la nota a piè di pagina 4 {}^4 4 . Dall’equazione precedente, possiamo vedere che la distribuzione del vettore obiettivo corrente y i \mathbf{y}_i y i ​ è direttamente condizionata al vettore obiettivo precedente y i − 1 \mathbf{y}_{i-1} y i − 1 ​ e allo stato nascosto precedente c i − 1 \mathbf{c}_{i-1} c i − 1 ​ . Poiché lo stato nascosto precedente c i − 1 \mathbf{c}_{i-1} c i − 1 ​ dipende da tutti i vettori obiettivo precedenti y 0 , … , y i − 2 \mathbf{y}_0, \ldots, \mathbf{y}_{i-2} y 0 ​ , … , y i − 2 ​ , si può affermare che il decoder basato su RNN modella implicitamente ( ad esempio in modo indiretto ) la distribuzione condizionale p θ dec ( y i ∣ Y 0 : i − 1 , c ) p_{\theta_{\text{dec}}}(\mathbf{y}_i | \mathbf{Y}_{0: i-1}, \mathbf{c}) p θ dec ​ ​ ( y i ​ ∣ Y 0 : i − 1 ​ , c ) .

Lo spazio delle possibili sequenze di vettori obiettivo Y 1 : m \mathbf{Y}_{1:m} Y 1 : m ​ è eccessivamente grande, quindi durante l’inférence, bisogna fare affidamento su metodi di decodifica 5 {}^5 5 che campionano efficientemente sequenze di vettori obiettivo ad alta probabilità da p θ d e c ( Y 1 : m ∣ c ) p_{\theta_{dec}}(\mathbf{Y}_{1:m} |\mathbf{c}) p θ d e c ​ ​ ( Y 1 : m ​ ∣ c ) .

Dato un tale metodo di decodifica, durante l’inférence, il prossimo vettore di input y i \mathbf{y}_i y i ​ può quindi essere campionato da p θ dec ( y i ∣ Y 0 : i − 1 , c ) p_{\theta_{\text{dec}}}(\mathbf{y}_i | \mathbf{Y}_{0: i-1}, \mathbf{c}) p θ dec ​ ​ ( y i ​ ∣ Y 0 : i − 1 ​ , c ) e viene consequentemente aggiunto alla sequenza di input in modo che il decoder RNN modelli poi p θ dec ( y i + 1 ∣ Y 0 : i , c ) p_{\theta_{\text{dec}}}(\mathbf{y}_{i+1} | \mathbf{Y}_{0: i}, \mathbf{c}) p θ dec ​ ​ ( y i + 1 ​ ∣ Y 0 : i ​ , c ) per campionare il prossimo vettore di input y i + 1 \mathbf{y}_{i+1} y i + 1 ​ e così via in modo auto-regressivo.

Una caratteristica importante dei modelli di encoder-decoder basati su RNN è la definizione di vettori speciali, come il vettore EOS \text{EOS} EOS e BOS \text{BOS} BOS. Il vettore EOS \text{EOS} EOS rappresenta spesso il vettore di input finale x n \mathbf{x}_n x n ​ per “segnalare” all’encoder che la sequenza di input è terminata e definisce anche la fine della sequenza di destinazione. Non appena il vettore EOS \text{EOS} EOS viene selezionato da un vettore di logit, la generazione è completa. Il vettore BOS \text{BOS} BOS rappresenta il vettore di input y 0 \mathbf{y}_0 y 0 ​ fornito all’RNN del decoder al primo passaggio di decodifica. Per generare il primo logit l 1 \mathbf{l}_1 l 1 ​, è necessario un input e poiché nessun input è stato generato al primo passaggio, viene fornito un vettore di input BOS \text{BOS} BOS speciale all’RNN del decoder. Ok – abbastanza complicato! Illustreremo e passeremo attraverso un esempio.

L’encoder RNN srotolato è colorato in verde e il decoder RNN srotolato è colorato in rosso.

La frase in inglese “I want to buy a car”, rappresentata da x 1 = I \mathbf{x}_1 = \text{I} x 1 ​ = I , x 2 = want \mathbf{x}_2 = \text{want} x 2 ​ = want , x 3 = to \mathbf{x}_3 = \text{to} x 3 ​ = to , x 4 = buy \mathbf{x}_4 = \text{buy} x 4 ​ = buy , x 5 = a \mathbf{x}_5 = \text{a} x 5 ​ = a , x 6 = car \mathbf{x}_6 = \text{car} x 6 ​ = car e x 7 = EOS \mathbf{x}_7 = \text{EOS} x 7 ​ = EOS viene tradotta in tedesco: “Ich will ein Auto kaufen” e definita come y 0 = BOS \mathbf{y}_0 = \text{BOS} y 0 ​ = BOS , y 1 = Ich \mathbf{y}_1 = \text{Ich} y 1 ​ = Ich , y 2 = will \mathbf{y}_2 = \text{will} y 2 ​ = will , y 3 = ein \mathbf{y}_3 = \text{ein} y 3 ​ = ein , y 4 = Auto , y 5 = kaufen \mathbf{y}_4 = \text{Auto}, \mathbf{y}_5 = \text{kaufen} y 4 ​ = Auto , y 5 ​ = kaufen e y 6 = EOS \mathbf{y}_6=\text{EOS} y 6 ​ = EOS . Per iniziare, il vettore di input x 1 = I \mathbf{x}_1 = \text{I} x 1 ​ = I viene elaborato dall’RNN dell’encoder e aggiorna il suo stato nascosto. Si noti che poiché siamo interessati solo allo stato nascosto finale dell’encoder c \mathbf{c} c , possiamo trascurare il vettore di destinazione dell’encoder RNN. L’encoder RNN quindi elabora il resto della frase di input want \text{want} want , to \text{to} to , buy \text{buy} buy , a \text{a} a , car \text{car} car , EOS \text{EOS} EOS allo stesso modo, aggiornando il suo stato nascosto ad ogni passaggio fino a quando viene raggiunto il vettore x 7 = E O S \mathbf{x}_7={EOS} x 7 ​ = E O S 6 {}^6 6 . Nell’illustrazione sopra, la freccia orizzontale che collega l’RNN dell’encoder srotolato rappresenta gli aggiornamenti sequenziali dello stato nascosto. Lo stato nascosto finale dell’RNN dell’encoder, rappresentato da c \mathbf{c} c , definisce completamente la codifica della sequenza di input e viene utilizzato come stato nascosto iniziale dell’RNN del decoder. Questo può essere visto come la condizionatura dell’RNN del decoder sull’input codificato.

Per generare il primo vettore di destinazione, viene fornito al decoder il vettore BOS \text{BOS} BOS, illustrato come y 0 \mathbf{y}_0 y 0 ​ nel design sopra. Il vettore di destinazione dell’RNN viene quindi mappato ulteriormente nel vettore di logit l 1 \mathbf{l}_1 l 1 ​ mediante il livello di feed-forward LM Head per definire la distribuzione condizionale del primo vettore di destinazione come spiegato sopra:

p θ d e c ( y ∣ BOS , c ) . p_{\theta_{dec}}(\mathbf{y} | \text{BOS}, \mathbf{c}). p θ d e c ​ ​ ( y ∣ BOS , c ) .

La parola Ich \text{Ich} Ich viene campionata (come mostrato dalla freccia grigia che collega l 1 \mathbf{l}_1 l 1 ​ e y 1 \mathbf{y}_1 y 1 ​ ) e di conseguenza il secondo vettore di destinazione può essere campionato:

will ∼ p θ d e c ( y ∣ BOS , Ich , c ) . \text{will} \sim p_{\theta_{dec}}(\mathbf{y} | \text{BOS}, \text{Ich}, \mathbf{c}). will ∼ p θ d e c ​ ​ ( y ∣ BOS , Ich , c ) .

E così via fino a quando, al passo i = 6 i=6 i = 6 , il vettore EOS \text{EOS} EOS viene campionato da l 6 \mathbf{l}_6 l 6 ​ e la decodifica è completata. La sequenza di destinazione risultante è Y 1 : 6 = { y 1 , … , y 6 } \mathbf{Y}_{1:6} = \{\mathbf{y}_1, \ldots, \mathbf{y}_6\} Y 1 : 6 ​ = { y 1 ​ , … , y 6 ​ } , che corrisponde a “Ich will ein Auto kaufen” nel nostro esempio sopra.

Per riassumere, un modello encoder-decoder basato su RNN, rappresentato da f θ enc f_{\theta_{\text{enc}}} f θ enc ​ ​ e p θ dec p_{\theta_{\text{dec}}} p θ dec ​ ​, definisce la distribuzione p ( Y 1 : m ∣ X 1 : n ) p(\mathbf{Y}_{1:m} | \mathbf{X}_{1:n}) p ( Y 1 : m ​ ∣ X 1 : n ​ ) mediante la fattorizzazione:

p θ enc , θ dec ( Y 1 : m ∣ X 1 : n ) = ∏ i = 1 m p θ enc , θ dec ( y i ∣ Y 0 : i − 1 , X 1 : n ) = ∏ i = 1 m p θ dec ( y i ∣ Y 0 : i − 1 , c ) , con c = f θ e n c ( X ) . p_{\theta_{\text{enc}}, \theta_{\text{dec}}}(\mathbf{Y}_{1:m} | \mathbf{X}_{1:n}) = \prod_{i=1}^{m} p_{\theta_{\text{enc}}, \theta_{\text{dec}}}(\mathbf{y}_i | \mathbf{Y}_{0: i-1}, \mathbf{X}_{1:n}) = \prod_{i=1}^{m} p_{\theta_{\text{dec}}}(\mathbf{y}_i | \mathbf{Y}_{0: i-1}, \mathbf{c}), \text{ con } \mathbf{c}=f_{\theta_{enc}}(X). p θ enc ​ , θ dec ​ ​ ( Y 1 : m ​ ∣ X 1 : n ​ ) = i = 1 ∏ m ​ p θ enc ​ , θ dec ​ ​ ( y i ​ ∣ Y 0 : i − 1 ​ , X 1 : n ​ ) = i = 1 ∏ m ​ p θ dec ​ ​ ( y i ​ ∣ Y 0 : i − 1 ​ , c ) , con c = f θ e n c ​ ​ ( X ) .

Durante l’infereza, metodi di decodifica efficienti possono generare in modo auto-regressivo la sequenza di destinazione Y 1 : m \mathbf{Y}_{1:m} Y 1 : m ​ .

Il modello encoder-decoder basato su RNN ha colpito la comunità NLG. Nel 2016, Google ha annunciato di sostituire completamente il suo servizio di traduzione altamente ingegnerizzato con un singolo modello encoder-decoder basato su RNN (vedi qui ).

Tuttavia, i modelli encoder-decoder basati su RNN presentano due problematiche. In primo luogo, le RNN soffrono del problema del gradiente che svanisce, rendendo molto difficile catturare dipendenze a lungo raggio, cf. Hochreiter et al. (2001) . In secondo luogo, l’architettura ricorrente intrinseca delle RNN impedisce una parallelizzazione efficiente durante la codifica, cf. Vaswani et al. (2017) .


1 {}^1 1 La citazione originale del paper è “Nonostante la loro flessibilità e potenza, le DNN possono essere applicate solo a problemi la cui input e target possono essere codificati in modo sensato con vettori di dimensionalità fissa”, che qui è leggermente adattata.

2 {}^2 2 Lo stesso vale essenzialmente per le reti neurali convoluzionali (CNN). Mentre una sequenza di input di lunghezza variabile può essere alimentata in una CNN, la dimensionalità del target sarà sempre dipendente dalla dimensionalità dell’input o fissata a un valore specifico.

3 {}^3 3 Al primo passo, lo stato nascosto viene inizializzato come un vettore di zeri e alimentato all’RNN insieme al primo vettore di input x 1 \mathbf{x}_1 x 1 ​ .

4 {}^4 4 Una rete neurale può definire una distribuzione di probabilità su tutte le parole, cioè p ( y ∣ c , Y 0 : i − 1 ) p(\mathbf{y} | \mathbf{c}, \mathbf{Y}_{0: i-1}) p ( y ∣ c , Y 0 : i − 1 ​ ) come segue. Inizialmente, la rete definisce una mappatura dagli input c , Y 0 : i − 1 \mathbf{c}, \mathbf{Y}_{0: i-1} c , Y 0 : i − 1 ​ a una rappresentazione vettoriale incorporata y ′ \mathbf{y’} y ′ , che corrisponde al vettore target dell’RNN. La rappresentazione vettoriale incorporata y ′ \mathbf{y’} y ′ viene quindi passata al livello “language model head”, il che significa che viene moltiplicata per la matrice di embedding delle parole , cioè Y vocab \mathbf{Y}^{\text{vocab}} Y vocab , in modo che venga calcolato uno score tra y ′ \mathbf{y’} y ′ e ogni vettore codificato y ∈ Y vocab \mathbf{y} \in \mathbf{Y}^{\text{vocab}} y ∈ Y vocab . Il vettore risultante viene chiamato vettore di logit l = Y vocab y ′ \mathbf{l} = \mathbf{Y}^{\text{vocab}} \mathbf{y’} l = Y vocab y ′ e può essere mappato in una distribuzione di probabilità su tutte le parole applicando un’operazione softmax: p ( y ∣ c ) = Softmax ( Y vocab y ′ ) = Softmax ( l ) p(\mathbf{y} | \mathbf{c}) = \text{Softmax}(\mathbf{Y}^{\text{vocab}} \mathbf{y’}) = \text{Softmax}(\mathbf{l}) p ( y ∣ c ) = Softmax ( Y vocab y ′ ) = Softmax ( l ) .

5 {}^5 5 La decodifica con beam-search è un esempio di tale metodo di decodifica. Metodi di decodifica diversi escono dal campo di questo notebook. Si consiglia al lettore di fare riferimento a questo notebook interattivo sui metodi di decodifica.

6 {}^6 6 Sutskever et al. (2014) inverte l’ordine dell’input in modo che nell’esempio sopra i vettori di input corrispondano a x 1 = car \mathbf{x}_1 = \text{car} x 1 ​ = car , x 2 = a \mathbf{x}_2 = \text{a} x 2 ​ = a , x 3 = buy \mathbf{x}_3 = \text{buy} x 3 ​ = buy , x 4 = to \mathbf{x}_4 = \text{to} x 4 ​ = to , x 5 = want \mathbf{x}_5 = \text{want} x 5 ​ = want , x 6 = I \mathbf{x}_6 = \text{I} x 6 ​ = I e x 7 = EOS \mathbf{x}_7 = \text{EOS} x 7 ​ = EOS . La motivazione è quella di permettere una connessione più breve tra le coppie di parole corrispondenti come ad esempio x 6 = I \mathbf{x}_6 = \text{I} x 6 ​ = I e y 1 = Ich \mathbf{y}_1 = \text{Ich} y 1 ​ = Ich . Il gruppo di ricerca sottolinea che l’inversione della sequenza di input è stata una delle ragioni chiave per le migliori prestazioni del loro modello nella traduzione automatica.

Encoder-Decoder

Nel 2017, Vaswani et al. hanno introdotto il Transformer e con esso hanno dato vita ai modelli encoder-decoder basati su Transformer.

Analogamente ai modelli di codificatori-decodificatori basati su RNN, i modelli di codificatori-decodificatori basati su trasformatori consistono in un codificatore e un decodificatore, entrambi composti da una serie di blocchi di attenzione residua. L’innovazione chiave dei modelli di codificatori-decodificatori basati su trasformatori è che tali blocchi di attenzione residua possono processare una sequenza di input X 1 : n \mathbf{X}_{1:n} X 1 : n ​ di lunghezza variabile n n n senza mostrare una struttura ricorrente. Non fare affidamento su una struttura ricorrente consente ai codificatori-decodificatori basati su trasformatori di essere altamente parallelizzabili, rendendo il modello di ordini di grandezza più efficiente dal punto di vista computazionale rispetto ai modelli di codificatori-decodificatori basati su RNN su hardware moderno.

Come promemoria, per risolvere un problema di sequenza in sequenza, è necessario trovare una mappatura di una sequenza di input X 1 : n \mathbf{X}_{1:n} X 1 : n ​ a una sequenza di output Y 1 : m \mathbf{Y}_{1:m} Y 1 : m ​ di lunghezza variabile m m m . Vediamo come i modelli di codificatori-decodificatori basati su trasformatori vengono utilizzati per trovare tale mappatura.

Similmente ai modelli di codificatori-decodificatori basati su RNN, i modelli di codificatori-decodificatori basati su trasformatori definiscono una distribuzione condizionale di vettori target Y 1 : n \mathbf{Y}_{1:n} Y 1 : n ​ dati una sequenza di input X 1 : n \mathbf{X}_{1:n} X 1 : n ​ :

p θ enc , θ dec ( Y 1 : m ∣ X 1 : n ) . p_{\theta_{\text{enc}}, \theta_{\text{dec}}}(\mathbf{Y}_{1:m} | \mathbf{X}_{1:n}). p θ enc ​ , θ dec ​ ​ ( Y 1 : m ​ ∣ X 1 : n ​ ) .

La parte codificatore basata su trasformatori codifica la sequenza di input X 1 : n \mathbf{X}_{1:n} X 1 : n ​ in una sequenza di stati nascosti X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n ​ , definendo così la mappatura:

f θ enc : X 1 : n → X ‾ 1 : n . f_{\theta_{\text{enc}}}: \mathbf{X}_{1:n} \to \mathbf{\overline{X}}_{1:n}. f θ enc ​ ​ : X 1 : n ​ → X 1 : n ​ .

La parte decodificatore basata su trasformatori modella quindi la distribuzione di probabilità condizionata della sequenza di vettori target Y 1 : n \mathbf{Y}_{1:n} Y 1 : n ​ dati la sequenza di stati nascosti codificati X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n ​ :

p θ d e c ( Y 1 : n ∣ X ‾ 1 : n ) . p_{\theta_{\text{dec}}}(\mathbf{Y}_{1:n} | \mathbf{\overline{X}}_{1:n}). p θ d e c ​ ​ ( Y 1 : n ​ ∣ X 1 : n ​ ) .

Utilizzando la regola di Bayes, questa distribuzione può essere fattorizzata come il prodotto della distribuzione di probabilità condizionata del vettore target y i \mathbf{y}_i y i ​ dati gli stati nascosti codificati X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n ​ e tutti i vettori target precedenti Y 0 : i − 1 \mathbf{Y}_{0:i-1} Y 0 : i − 1 ​ :

p θ d e c ( Y 1 : n ∣ X ‾ 1 : n ) = ∏ i = 1 n p θ dec ( y i ∣ Y 0 : i − 1 , X ‾ 1 : n ) . p_{\theta_{\text{dec}}}(\mathbf{Y}_{1:n} | \mathbf{\overline{X}}_{1:n}) = \prod_{i=1}^{n} p_{\theta_{\text{dec}}}(\mathbf{y}_i | \mathbf{Y}_{0: i-1}, \mathbf{\overline{X}}_{1:n}). p θ d e c ​ ​ ( Y 1 : n ​ ∣ X 1 : n ​ ) = i = 1 ∏ n ​ p θ dec ​ ​ ( y i ​ ∣ Y 0 : i − 1 ​ , X 1 : n ​ ) .

Il decoder basato sul transformer mappa qui la sequenza di stati nascosti codificati X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n ​ e tutti i vettori target precedenti Y 0 : i − 1 \mathbf{Y}_{0:i-1} Y 0 : i − 1 ​ al vettore di logit l i \mathbf{l}_i l i ​ . Il vettore di logit l i \mathbf{l}_i l i ​ viene quindi processato dall’operazione softmax per definire la distribuzione condizionale p θ dec ( y i ∣ Y 0 : i − 1 , X ‾ 1 : n ) p_{\theta_{\text{dec}}}(\mathbf{y}_i | \mathbf{Y}_{0: i-1}, \mathbf{\overline{X}}_{1:n}) p θ dec ​ ​ ( y i ​ ∣ Y 0 : i − 1 ​ , X 1 : n ​ ) , proprio come avviene per i decoder basati su RNN. Tuttavia, a differenza dei decoder basati su RNN, la distribuzione del vettore target y i \mathbf{y}_i y i ​ è esplicitamente (o direttamente) condizionata a tutti i vettori target precedenti y 0 , … , y i − 1 \mathbf{y}_0, \ldots, \mathbf{y}_{i-1} y 0 ​ , … , y i − 1 ​ come vedremo in seguito in modo più dettagliato. Il vettore target 0 y 0 \mathbf{y}_0 y 0 ​ è rappresentato qui da un vettore speciale “inizio frase” BOS \text{BOS} BOS .

Avendo definito la distribuzione condizionale p θ dec ( y i ∣ Y 0 : i − 1 , X ‾ 1 : n ) p_{\theta_{\text{dec}}}(\mathbf{y}_i | \mathbf{Y}_{0: i-1}, \mathbf{\overline{X}}_{1:n}) p θ dec ​ ​ ( y i ​ ∣ Y 0 : i − 1 ​ , X 1 : n ​ ) , possiamo ora generare in modo auto-regressivo l’output e definire così una mappatura di una sequenza di input X 1 : n \mathbf{X}_{1:n} X 1 : n ​ a una sequenza di output Y 1 : m \mathbf{Y}_{1:m} Y 1 : m ​ durante l’elaborazione.

Visualizziamo il processo completo di generazione auto-regressiva dei modelli codificatore-decodificatore basati sul transformer.

Il codificatore basato sul transformer è colorato in verde e il decodificatore basato sul transformer è colorato in rosso. Come nella sezione precedente, mostriamo come la frase in inglese “I want to buy a car”, rappresentata da x 1 = I \mathbf{x}_1 = \text{I} x 1 ​ = I , x 2 = want \mathbf{x}_2 = \text{want} x 2 ​ = want , x 3 = to \mathbf{x}_3 = \text{to} x 3 ​ = to , x 4 = buy \mathbf{x}_4 = \text{buy} x 4 ​ = buy , x 5 = a \mathbf{x}_5 = \text{a} x 5 ​ = a , x 6 = car \mathbf{x}_6 = \text{car} x 6 ​ = car e x 7 = EOS \mathbf{x}_7 = \text{EOS} x 7 ​ = EOS viene tradotta in tedesco: “Ich will ein Auto kaufen” definita come y 0 = BOS \mathbf{y}_0 = \text{BOS} y 0 ​ = BOS , y 1 = Ich \mathbf{y}_1 = \text{Ich} y 1 ​ = Ich , y 2 = will \mathbf{y}_2 = \text{will} y 2 ​ = will , y 3 = ein \mathbf{y}_3 = \text{ein} y 3 ​ = ein , y 4 = Auto , y 5 = kaufen \mathbf{y}_4 = \text{Auto}, \mathbf{y}_5 = \text{kaufen} y 4 ​ = Auto , y 5 ​ = kaufen e y 6 = EOS \mathbf{y}_6=\text{EOS} y 6 ​ = EOS .

Per iniziare, l’encoder elabora l’intera sequenza di input X 1 : 7 \mathbf{X}_{1:7} X 1 : 7 ​ = “Voglio comprare una macchina” (rappresentata dai vettori verde chiaro) in una sequenza codificata contestualizzata X ‾ 1 : 7 \mathbf{\overline{X}}_{1:7} X 1 : 7 ​ . Ad esempio, x ‾ 4 \mathbf{\overline{x}}_4 x 4 ​ definisce una codifica che dipende non solo dall’input x 4 \mathbf{x}_4 x 4 ​ = “comprare”, ma anche da tutte le altre parole “Voglio”, “a”, “macchina” e “EOS”, ovvero il contesto.

Successivamente, la codifica dell’input X ‾ 1 : 7 \mathbf{\overline{X}}_{1:7} X 1 : 7 ​ insieme al vettore BOS, ovvero y 0 \mathbf{y}_0 y 0 ​ , viene alimentata al decoder. Il decoder elabora gli input X ‾ 1 : 7 \mathbf{\overline{X}}_{1:7} X 1 : 7 ​ e y 0 \mathbf{y}_0 y 0 ​ per ottenere il primo logit l 1 \mathbf{l}_1 l 1 ​ (mostrato in rosso scuro) per definire la distribuzione condizionale del primo vettore target y 1 \mathbf{y}_1 y 1 ​ :

p θ e n c , d e c ( y ∣ y 0 , X 1 : 7 ) = p θ e n c , d e c ( y ∣ BOS , Voglio comprare una macchina EOS ) = p θ d e c ( y ∣ BOS , X ‾ 1 : 7 ) . p_{\theta_{enc, dec}}(\mathbf{y} | \mathbf{y}_0, \mathbf{X}_{1:7}) = p_{\theta_{enc, dec}}(\mathbf{y} | \text{BOS}, \text{Voglio comprare una macchina EOS}) = p_{\theta_{dec}}(\mathbf{y} | \text{BOS}, \mathbf{\overline{X}}_{1:7}). p θ e n c , d e c ​ ​ ( y ∣ y 0 ​ , X 1 : 7 ​ ) = p θ e n c , d e c ​ ​ ( y ∣ BOS , Voglio comprare una macchina EOS ) = p θ d e c ​ ​ ( y ∣ BOS , X 1 : 7 ​ ) .

Successivamente, il primo vettore target y 1 \mathbf{y}_1 y 1 ​ = Ich \text{Ich} Ich viene campionato dalla distribuzione (rappresentata dalle frecce grigie) e può ora essere nuovamente alimentato al decoder. Il decoder elabora sia y 0 \mathbf{y}_0 y 0 ​ = “BOS” che y 1 \mathbf{y}_1 y 1 ​ = “Ich” per definire la distribuzione condizionale del secondo vettore target y 2 \mathbf{y}_2 y 2 ​ :

p θ d e c ( y ∣ BOS Ich , X ‾ 1 : 7 ) . p_{\theta_{dec}}(\mathbf{y} | \text{BOS Ich}, \mathbf{\overline{X}}_{1:7}). p θ d e c ​ ​ ( y ∣ BOS Ich , X 1 : 7 ​ ) .

Possiamo campionare nuovamente e produrre il vettore target y 2 \mathbf{y}_2 y 2 ​ = “will”. Continuiamo in modo auto-regressivo fino a quando al passaggio 6 viene campionato il vettore EOS dalla distribuzione condizionale:

EOS ∼ p θ d e c ( y ∣ BOS Ich will ein Auto kaufen , X ‾ 1 : 7 ) . \text{EOS} \sim p_{\theta_{dec}}(\mathbf{y} | \text{BOS Ich will ein Auto kaufen}, \mathbf{\overline{X}}_{1:7}). EOS ∼ p θ d e c ​ ​ ( y ∣ BOS Ich will ein Auto kaufen , X 1 : 7 ​ ) .

E così via in modo auto-regressivo.

È importante capire che l’encoder viene utilizzato solo nel primo passaggio in avanti per mappare X 1 : n \mathbf{X}_{1:n} X 1 : n ​ a X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n ​ . Dal secondo passaggio in avanti, il decoder può utilizzare direttamente l’encoding calcolato in precedenza X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n ​ . Per chiarezza, illustreremo il primo e il secondo passaggio in avanti per il nostro esempio sopra.

Come si può vedere, solo nel passaggio i = 1 i=1 i = 1 dobbiamo codificare “Voglio comprare una macchina EOS” in X ‾ 1 : 7 \mathbf{\overline{X}}_{1:7} X 1 : 7 ​ . Nel passaggio i = 2 i=2 i = 2 , gli encoding contestualizzati di “Voglio comprare una macchina EOS” vengono semplicemente riutilizzati dal decoder.

In 🤗Transformers, questa generazione auto-regressiva avviene internamente quando si chiama il metodo .generate(). Utilizzeremo uno dei nostri modelli di traduzione per vedere questo in azione.

from transformers import MarianMTModel, MarianTokenizer

tokenizer = MarianTokenizer.from_pretrained("Helsinki-NLP/opus-mt-en-de")
model = MarianMTModel.from_pretrained("Helsinki-NLP/opus-mt-en-de")

# crea gli id dei vettori di input codificati
input_ids = tokenizer("Voglio comprare una macchina", return_tensors="pt").input_ids

# traduzione dell'esempio
output_ids = model.generate(input_ids)[0]

# decodifica e stampa
print(tokenizer.decode(output_ids))

Output:

    <pad> Ich will ein Auto kaufen

Chiamando .generate() si effettuano molte operazioni internamente. Prima, passa gli input_ids all’encoder. Seconda, passa un token predefinito, che è il simbolo <pad> \text{<pad>} <pad> nel caso di MarianMTModel insieme agli input_ids codificati al decoder. Terza, applica il meccanismo di decodifica beam search per campionare in modo auto-regressivo la prossima parola di output dell’ultimo output del decoder 1 {}^1 1 . Per maggiori dettagli su come funziona la decodifica beam search, si consiglia di leggere questo post sul blog.

Nell’Appendice abbiamo incluso un frammento di codice che mostra come implementare un semplice metodo di generazione “da zero”. Per comprendere appieno come funziona la generazione auto-regressiva internamente, è altamente consigliato leggere l’Appendice.

Per riassumere:

  • L’encoder basato su transformer definisce una mappatura dalla sequenza di input X 1 : n \mathbf{X}_{1:n} X 1 : n ​ a una sequenza di encoding contestualizzati X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n ​ .
  • Il decoder basato su transformer definisce la distribuzione condizionale p θ dec ( y i ∣ Y 0 : i − 1 , X ‾ 1 : n ) p_{\theta_{\text{dec}}}(\mathbf{y}_i | \mathbf{Y}_{0: i-1}, \mathbf{\overline{X}}_{1:n}) p θ dec ​ ​ ( y i ​ ∣ Y 0 : i − 1 ​ , X 1 : n ​ ) .
  • Dato un appropriato meccanismo di decodifica, la sequenza di output Y 1 : m \mathbf{Y}_{1:m} Y 1 : m ​ può essere campionata in modo auto-regressivo da p θ dec ( y i ∣ Y 0 : i − 1 , X ‾ 1 : n ) , ∀ i ∈ { 1 , … , m } p_{\theta_{\text{dec}}}(\mathbf{y}_i | \mathbf{Y}_{0: i-1}, \mathbf{\overline{X}}_{1:n}), \forall i \in \{1, \ldots, m\} p θ dec ​ ​ ( y i ​ ∣ Y 0 : i − 1 ​ , X 1 : n ​ ) , ∀ i ∈ { 1 , … , m } .

Ottimo, ora che abbiamo ottenuto una panoramica generale di come funzionano i modelli di tipo codificatore-decodificatore basati su trasformatori, possiamo approfondire sia la parte del codificatore che del decodificatore del modello. Più precisamente, vedremo esattamente come il codificatore utilizza il livello di auto-attenzione per produrre una sequenza di codifiche vettoriali dipendenti dal contesto e come i livelli di auto-attenzione permettano una parallelizzazione efficiente. Successivamente, spiegheremo in dettaglio come funziona il livello di auto-attenzione nel modello del decodificatore e come il decodificatore sia condizionato dall’output del codificatore con livelli di attenzione incrociata per definire la distribuzione condizionale p θ dec ( y i ∣ Y 0 : i − 1 , X ‾ 1 : n ) p_{\theta_{\text{dec}}}(\mathbf{y}_i | \mathbf{Y}_{0: i-1}, \mathbf{\overline{X}}_{1:n}) p θ dec ​ ​ ( y i ​ ∣ Y 0 : i − 1 ​ , X 1 : n ​ ) . Lungo il percorso diventerà evidente come i modelli di tipo codificatore-decodificatore basati su trasformatori risolvano il problema delle dipendenze a lungo raggio dei modelli di tipo codificatore-decodificatore basati su RNN.


1 {}^1 1 Nel caso di "Helsinki-NLP/opus-mt-en-de", i parametri di decodifica possono essere accessibili qui, dove possiamo vedere che il modello applica la ricerca beam con num_beams=6.

Codificatore

Come accennato nella sezione precedente, il codificatore basato su trasformatori mappa la sequenza di input in una sequenza di codifiche contestualizzate:

f θ enc : X 1 : n → X ‾ 1 : n . f_{\theta_{\text{enc}}}: \mathbf{X}_{1:n} \to \mathbf{\overline{X}}_{1:n}. f θ enc ​ ​ : X 1 : n ​ → X 1 : n ​ .

Analizzando più da vicino l’architettura, il codificatore basato su trasformatori è una pila di blocchi di codifica residuale. Ogni blocco di codifica è composto da un livello di auto-attenzione bi-direzionale, seguito da due livelli di feed-forward. Per semplicità, trascuriamo i livelli di normalizzazione in questo notebook. Inoltre, non discuteremo ulteriormente il ruolo dei due livelli di feed-forward, ma li considereremo semplicemente come una mappatura finale da vettore a vettore richiesta in ogni blocco di codifica 1 {}^1 1 . Il livello di auto-attenzione bi-direzionale mette ogni vettore di input x ′ j , ∀ j ∈ { 1 , … , n } \mathbf{x’}_j, \forall j \in \{1, \ldots, n\} x ′ j ​ , ∀ j ∈ { 1 , … , n } in relazione con tutti i vettori di input x ′ 1 , … , x ′ n \mathbf{x’}_1, \ldots, \mathbf{x’}_n x ′ 1 ​ , … , x ′ n ​ e, facendo ciò, trasforma il vettore di input x ′ j \mathbf{x’}_j x ′ j ​ in una rappresentazione contestuale più “raffinata” di sé stesso, definita come x ′ ′ j \mathbf{x”}_j x ′ ′ j ​ . In questo modo, il primo blocco di codifica trasforma ogni vettore di input della sequenza di input X 1 : n \mathbf{X}_{1:n} X 1 : n ​ (mostrato in verde chiaro di seguito) da una rappresentazione vettoriale indipendente dal contesto a una rappresentazione vettoriale dipendente dal contesto, e i blocchi di codifica successivi affinano ulteriormente questa rappresentazione contestuale fino a quando l’ultimo blocco di codifica produce la codifica contestuale finale X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n ​ (mostrato in verde scuro di seguito).

Visualizziamo come il codificatore elabora la sequenza di input “Voglio comprare una macchina EOS” in una sequenza di codifiche contestualizzate. Come per i codificatori basati su RNN, anche i codificatori basati su trasformatori aggiungono un vettore di input speciale “fine-sequenza” alla sequenza di input per indicare al modello che la sequenza di vettori di input è terminata 2 {}^2 2 .

Il nostro esemplare di codificatore basato su trasformatori è composto da tre blocchi di codifica, mentre il secondo blocco di codifica è mostrato in maggior dettaglio nella casella rossa a destra per i primi tre vettori di input x 1 , x 2 e x 3 \mathbf{x}_1, \mathbf{x}_2 e \mathbf{x}_3 x 1 ​ , x 2 ​ e x 3 ​ . Il meccanismo di auto-attenzione bi-direzionale è illustrato dal grafo completamente connesso nella parte inferiore della casella rossa e i due livelli di feed-forward sono mostrati nella parte superiore della casella rossa. Come detto prima, ci concentreremo solo sul meccanismo di auto-attenzione bi-direzionale.

Come si può vedere, ogni vettore di output dello strato di auto-attenzione x ′ ′ i , ∀ i ∈ { 1 , … , 7 } \mathbf{x”}_i, \forall i \in \{1, \ldots, 7\} x ′ ′ i ​ , ∀ i ∈ { 1 , … , 7 } dipende direttamente da tutti i vettori di input x ′ 1 , … , x ′ 7 \mathbf{x’}_1, \ldots, \mathbf{x’}_7 x ′ 1 ​ , … , x ′ 7 ​ . Ciò significa, ad esempio, che la rappresentazione vettoriale di input della parola “want”, cioè x ′ 2 \mathbf{x’}_2 x ′ 2 ​ , viene messa in relazione diretta con la parola “buy”, cioè x ′ 4 \mathbf{x’}_4 x ′ 4 ​ , ma anche con la parola “I”, cioè x ′ 1 \mathbf{x’}_1 x ′ 1 ​ . La rappresentazione vettoriale di output di “want”, cioè x ′ ′ 2 \mathbf{x”}_2 x ′ ′ 2 ​ , rappresenta quindi una rappresentazione contestuale più raffinata per la parola “want”.

Approfondiamo ora come funziona l’auto-attenzione bidirezionale. Ogni vettore di input x ′ i \mathbf{x’}_i x ′ i ​ di una sequenza di input X ′ 1 : n \mathbf{X’}_{1:n} X ′ 1 : n ​ di un blocco codificatore viene proiettato su un vettore chiave k i \mathbf{k}_i k i ​ , un vettore valore v i \mathbf{v}_i v i ​ e un vettore query q i \mathbf{q}_i q i ​ (mostrati rispettivamente in arancione, blu e viola) attraverso tre matrici di peso addestrabili W q , W v , W k \mathbf{W}_q, \mathbf{W}_v, \mathbf{W}_k W q ​ , W v ​ , W k ​ :

q i = W q x ′ i , \mathbf{q}_i = \mathbf{W}_q \mathbf{x’}_i, q i ​ = W q ​ x ′ i ​ , v i = W v x ′ i , \mathbf{v}_i = \mathbf{W}_v \mathbf{x’}_i, v i ​ = W v ​ x ′ i ​ , k i = W k x ′ i , \mathbf{k}_i = \mathbf{W}_k \mathbf{x’}_i, k i ​ = W k ​ x ′ i ​ , ∀ i ∈ { 1 , … n } . \forall i \in \{1, \ldots n \}. ∀ i ∈ { 1 , … n } .

Nota che le stesse matrici di peso vengono applicate a ciascun vettore di input x i , ∀ i ∈ { i , … , n } \mathbf{x}_i, \forall i \in \{i, \ldots, n\} x i ​ , ∀ i ∈ { i , … , n } . Dopo aver proiettato ciascun vettore di input x i \mathbf{x}_i x i ​ in un vettore query, chiave e valore, ciascun vettore di query q j , ∀ j ∈ { 1 , … , n } \mathbf{q}_j, \forall j \in \{1, \ldots, n\} q j ​ , ∀ j ∈ { 1 , … , n } viene confrontato con tutti i vettori chiave k 1 , … , k n \mathbf{k}_1, \ldots, \mathbf{k}_n k 1 ​ , … , k n ​ . Più simile è uno dei vettori chiave k 1 , … k n \mathbf{k}_1, \ldots \mathbf{k}_n k 1 ​ , … k n ​ a un vettore di query q j \mathbf{q}_j q j ​ , più importante è il vettore valore corrispondente v j \mathbf{v}_j v j ​ per il vettore di output x ′ ′ j \mathbf{x”}_j x ′ ′ j ​ . Più precisamente, un vettore di output x ′ ′ j \mathbf{x”}_j x ′ ′ j ​ è definito come la somma pesata di tutti i vettori valore v 1 , … , v n \mathbf{v}_1, \ldots, \mathbf{v}_n v 1 ​ , … , v n ​ più il vettore di input x ′ j \mathbf{x’}_j x ′ j ​ . In questo modo, i pesi sono proporzionali alla similarità coseno tra q j \mathbf{q}_j q j ​ e i rispettivi vettori chiave k 1 , … , k n \mathbf{k}_1, \ldots, \mathbf{k}_n k 1 ​ , … , k n ​ , che è espressa matematicamente da Softmax ( K 1 : n ⊺ q j ) \textbf{Softmax}(\mathbf{K}_{1:n}^\intercal \mathbf{q}_j) Softmax ( K 1 : n ⊺ ​ q j ​ ) come illustrato nell’equazione sottostante. Per una descrizione completa dello strato di auto-attenzione, si consiglia al lettore di dare un’occhiata a questo post del blog o all’articolo originale .

Va bene, questo suona piuttosto complicato. Illustreremo il livello di auto-attenzione bidirezionale per uno dei vettori di query del nostro esempio sopra. Per semplicità, si suppone che il nostro decodificatore basato su transformer utilizzi solo una singola testa di attenzione config.num_heads = 1 e che non si applichi alcuna normalizzazione.

Sulla sinistra, viene mostrato nuovamente il secondo blocco dell’encoder precedentemente illustrato e sulla destra viene fornita una visualizzazione dettagliata del meccanismo di auto-attenzione bidirezionale per il secondo vettore di input x ′ 2 \mathbf{x’}_2 x ′ 2 ​ che corrisponde alla parola di input “want”. Inizialmente, tutti i vettori di input x ′ 1 , … , x ′ 7 \mathbf{x’}_1, \ldots, \mathbf{x’}_7 x ′ 1 ​ , … , x ′ 7 ​ vengono proiettati sui rispettivi vettori di query q 1 , … , q 7 \mathbf{q}_1, \ldots, \mathbf{q}_7 q 1 ​ , … , q 7 ​ (solo i primi tre vettori di query sono mostrati in viola sopra), i vettori di valore v 1 , … , v 7 \mathbf{v}_1, \ldots, \mathbf{v}_7 v 1 ​ , … , v 7 ​ (mostrati in blu) e i vettori di chiave k 1 , … , k 7 \mathbf{k}_1, \ldots, \mathbf{k}_7 k 1 ​ , … , k 7 ​ (mostrati in arancione). Il vettore di query q 2 \mathbf{q}_2 q 2 ​ viene quindi moltiplicato per la trasposta di tutti i vettori di chiave, cioè K 1 : 7 ⊺ \mathbf{K}_{1:7}^{\intercal} K 1 : 7 ⊺ ​, seguito dall’operazione di softmax per ottenere i pesi di auto-attenzione . I pesi di auto-attenzione vengono infine moltiplicati per i rispettivi vettori di valore e il vettore di input x ′ 2 \mathbf{x’}_2 x ′ 2 ​ viene aggiunto per produrre la rappresentazione “raffinata” della parola “want”, cioè x ′ ′ 2 \mathbf{x”}_2 x ′ ′ 2 ​ (mostrato in verde scuro a destra). L’intera equazione è illustrata nella parte superiore della casella a destra. La moltiplicazione tra K 1 : 7 ⊺ \mathbf{K}_{1:7}^{\intercal} K 1 : 7 ⊺ ​ e q 2 \mathbf{q}_2 q 2 ​ permette quindi di confrontare la rappresentazione vettoriale di “want” con tutte le altre rappresentazioni vettoriali di input “I”, “to”, “buy”, “a”, “car”, “EOS” in modo che i pesi di auto-attenzione riflettano l’importanza di ciascuna delle altre rappresentazioni vettoriali di input x ′ j , con j ≠ 2 \mathbf{x’}_j \text{, con } j \ne 2 x ′ j ​ , con j  = 2 per la rappresentazione raffinata x ′ ′ 2 \mathbf{x”}_2 x ′ ′ 2 ​ della parola “want”.

Per capire ulteriormente le implicazioni del livello di auto-attenzione bidirezionale, supponiamo che la seguente frase venga elaborata: “La casa è bella e ben posizionata nel centro della città dove è facilmente accessibile con i mezzi pubblici”. La parola “it” si riferisce a “house”, che è a 12 “posizioni di distanza”. Nei codificatori basati su transformer, il livello di auto-attenzione bidirezionale esegue un’unica operazione matematica per mettere il vettore di input di “house” in relazione con il vettore di input di “it” (confronto con la prima illustrazione di questa sezione). Al contrario, in un codificatore basato su RNN, una parola che si trova a 12 “posizioni di distanza” richiederebbe almeno 12 operazioni matematiche, il che significa che in un codificatore basato su RNN è necessario un numero lineare di operazioni matematiche. Ciò rende molto più difficile per un codificatore basato su RNN modellare rappresentazioni contestuali a lungo raggio. Inoltre, diventa chiaro che un codificatore basato su transformer è molto meno incline a perdere informazioni importanti rispetto a un modello codificatore-decodificatore basato su RNN perché la lunghezza della sequenza di codifica viene mantenuta uguale, cioè len ( X 1 : n ) = len ( X ‾ 1 : n ) = n \textbf{len}(\mathbf{X}_{1:n}) = \textbf{len}(\mathbf{\overline{X}}_{1:n}) = n len ( X 1 : n ​ ) = len ( X 1 : n ​ ) = n , mentre un RNN comprime la lunghezza da ∗ len ( ( X 1 : n ) = n *\textbf{len}((\mathbf{X}_{1:n}) = n ∗ len ( ( X 1 : n ​ ) = n a solo len ( c ) = 1 \textbf{len}(\mathbf{c}) = 1 len ( c ) = 1 , il che rende molto difficile per gli RNN codificare efficacemente le dipendenze a lungo raggio tra le parole di input.

Oltre a rendere le dipendenze a lungo raggio più facilmente apprendibili, possiamo vedere che l’architettura Transformer è in grado di elaborare il testo in modo parallelo. Matematicamente, questo può essere facilmente dimostrato scrivendo la formula di auto-attenzione come prodotto delle matrici di query, chiave e valore:

X ′ ′ 1 : n = V 1 : n Softmax ( Q 1 : n ⊺ K 1 : n ) + X ′ 1 : n . \mathbf{X”}_{1:n} = \mathbf{V}_{1:n} \text{Softmax}(\mathbf{Q}_{1:n}^\intercal \mathbf{K}_{1:n}) + \mathbf{X’}_{1:n}. X ′ ′ 1 : n ​ = V 1 : n ​ Softmax ( Q 1 : n ⊺ ​ K 1 : n ​ ) + X ′ 1 : n ​ .

L’output X ′ ′ 1 : n = x ′ ′ 1 , … , x ′ ′ n \mathbf{X”}_{1:n} = \mathbf{x”}_1, \ldots, \mathbf{x”}_n X ′ ′ 1 : n ​ = x ′ ′ 1 ​ , … , x ′ ′ n ​ viene calcolato tramite una serie di moltiplicazioni di matrici e un’operazione softmax, che può essere parallelizzata efficacemente. Si noti che, in un modello di codificatore basato su RNN, il calcolo dello stato nascosto c \mathbf{c} c deve essere eseguito in modo sequenziale: calcola lo stato nascosto del primo vettore di input x 1 \mathbf{x}_1 x 1 ​, quindi calcola lo stato nascosto del secondo vettore di input che dipende dallo stato nascosto del primo vettore nascosto, ecc. La natura sequenziale delle RNN impedisce una parallelizzazione efficace e le rende molto più inefficienti rispetto ai modelli di codificatore basati su trasformatori sull’hardware GPU moderno.

Ottimo, ora dovremmo avere una migliore comprensione di a) come i modelli di codificatore basati su trasformatori modellano efficacemente rappresentazioni contestuali a lungo raggio e b) come elaborano efficientemente lunghe sequenze di vettori di input.

Ora, codifichiamo un breve esempio della parte di codificatore dei nostri modelli di codificatore-decodificatore MarianMT per verificare che la teoria spiegata si applichi nella pratica.


1 {}^1 1 Una spiegazione dettagliata del ruolo che svolgono i livelli di feed-forward nei modelli basati su trasformatori è fuori dallo scopo di questo notebook. Si sostiene in Yun et al., (2017) che i livelli di feed-forward sono cruciali per mappare ogni vettore contestuale x ′ i \mathbf{x’}_i x ′ i ​ individualmente nello spazio di output desiderato, cosa che lo strato di auto-attenzione non riesce a fare da solo. Va notato qui che ogni token di output x ′ \mathbf{x’} x ′ è elaborato dallo stesso livello di feed-forward. Per ulteriori dettagli, si consiglia al lettore di leggere l’articolo.

2 {}^2 2 Tuttavia, il vettore di input EOS non deve essere aggiunto alla sequenza di input, ma si è dimostrato che migliora le prestazioni in molti casi. A differenza del vettore di target BOS \text{BOS} BOS iniziale, richiesto dal decodificatore basato su trasformatori come vettore di input di partenza per prevedere un primo vettore di target.

from transformers import MarianMTModel, MarianTokenizer
import torch

tokenizer = MarianTokenizer.from_pretrained("Helsinki-NLP/opus-mt-en-de")
model = MarianMTModel.from_pretrained("Helsinki-NLP/opus-mt-en-de")

embeddings = model.get_input_embeddings()

# create ids of encoded input vectors
input_ids = tokenizer("Voglio comprare una macchina", return_tensors="pt").input_ids

# pass input_ids to encoder
encoder_hidden_states = model.base_model.encoder(input_ids, return_dict=True).last_hidden_state

# change the input slightly and pass to encoder
input_ids_perturbed = tokenizer("Voglio comprare una casa", return_tensors="pt").input_ids
encoder_hidden_states_perturbed = model.base_model.encoder(input_ids_perturbed, return_dict=True).last_hidden_state

# compare shape and encoding of first vector
print(f"Lunghezza dei vettori di input {embeddings(input_ids).shape[1]}. Lunghezza degli stati nascosti dell'encoder {encoder_hidden_states.shape[1]}")

# compare values of word embedding of "Voglio" for input_ids and perturbed input_ids
print("L'encoding per `Voglio` è uguale alla sua versione perturbata?: ", torch.allclose(encoder_hidden_states[0, 0], encoder_hidden_states_perturbed[0, 0], atol=1e-3))

Output:

    Lunghezza dei word embeddings di input 7. Lunghezza di encoder_hidden_states 7
    L'encoding per `I` è uguale alla sua versione perturbata?:  Falso

Confrontiamo la lunghezza dei word embeddings di input, cioè embeddings(input_ids) corrispondente a X 1 : n \mathbf{X}_{1:n} X 1 : n ​, con la lunghezza di encoder_hidden_states, corrispondente a X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n ​ . Inoltre, abbiamo inoltrato la sequenza di parole “Voglio comprare una macchina” e una versione leggermente perturbata “Voglio comprare una casa” attraverso l’encoder per verificare se la codifica del primo output, corrispondente a “I”, differisce quando solo l’ultima parola viene cambiata nella sequenza di input.

Come previsto, la lunghezza di output dei word embeddings di input e delle codifiche di output dell’encoder, cioè len ( X 1 : n ) \textbf{len}(\mathbf{X}_{1:n}) len ( X 1 : n ​ ) e len ( X ‾ 1 : n ) \textbf{len}(\mathbf{\overline{X}}_{1:n}) len ( X 1 : n ​ ) , è uguale. In secondo luogo, si può notare che i valori del vettore di output codificato di x ‾ 1 = “I” \mathbf{\overline{x}}_1 = \text{“I”} x 1 ​ = “I” sono diversi quando l’ultima parola viene cambiata da “macchina” a “casa”. Ciò però non dovrebbe essere una sorpresa per chi ha compreso l’attenzione bidirezionale.

Un’osservazione a parte, i modelli di autoencoding, come BERT, hanno la stessa identica architettura dei modelli di encoder basati su trasformatori. I modelli di autoencoding sfruttano questa architettura per un enorme pre-training auto-supervisionato su dati di testo di dominio aperto, in modo tale da poter mappare qualsiasi sequenza di parole in una rappresentazione bidirezionale profonda. In Devlin et al. (2018), gli autori mostrano che un modello BERT pre-addestrato con un singolo strato di classificazione specifico per il compito può ottenere risultati SOTA su undici compiti di NLP. Tutti i modelli di autoencoding di 🤗Transformers possono essere trovati qui.

Decoder

Come menzionato nella sezione Encoder-Decoder, il decoder basato su trasformatori definisce la distribuzione di probabilità condizionale di una sequenza target data la sequenza di codifiche contestualizzate:

p θ d e c ( Y 1 : m ∣ X ‾ 1 : n ) , p_{\theta_{dec}}(\mathbf{Y}_{1: m} | \mathbf{\overline{X}}_{1:n}), p θ d e c ​ ​ ( Y 1 : m ​ ∣ X 1 : n ​ ) ,

che, secondo il teorema di Bayes, può essere decomposta in un prodotto di distribuzioni condizionali del successivo vettore target dato la sequenza di codifiche contestualizzate e tutti i vettori target precedenti:

p θ d e c ( Y 1 : m ∣ X ‾ 1 : n ) = ∏ i = 1 m p θ d e c ( y i ∣ Y 0 : i − 1 , X ‾ 1 : n ) . p_{\theta_{dec}}(\mathbf{Y}_{1:m} | \mathbf{\overline{X}}_{1:n}) = \prod_{i=1}^{m} p_{\theta_{dec}}(\mathbf{y}_i | \mathbf{Y}_{0: i-1}, \mathbf{\overline{X}}_{1:n}). p θ d e c ​ ​ ( Y 1 : m ​ ∣ X 1 : n ​ ) = i = 1 ∏ m ​ p θ d e c ​ ​ ( y i ​ ∣ Y 0 : i − 1 ​ , X 1 : n ​ ) .

Cominciamo a capire come il decoder basato su trasformatori definisce una distribuzione di probabilità. Il decoder basato su trasformatori è una sequenza di blocchi decoder seguiti da uno strato denso, la “testa LM”. La sequenza di blocchi decoder mappa la sequenza di codifiche contestualizzate X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n ​ e una sequenza di vettori target preceduta dal vettore BOS \text{BOS} BOS e tagliata all’ultimo vettore target, cioè Y 0 : i − 1 \mathbf{Y}_{0:i-1} Y 0 : i − 1 ​ , in una sequenza codificata di vettori target Y ‾ 0 : i − 1 \mathbf{\overline{Y}}_{0: i-1} Y 0 : i − 1 ​ . Successivamente, la “testa LM” mappa la sequenza codificata di vettori target Y ‾ 0 : i − 1 \mathbf{\overline{Y}}_{0: i-1} Y 0 : i − 1 ​ in una sequenza di vettori di logit L 1 : n = l 1 , … , l n \mathbf{L}_{1:n} = \mathbf{l}_1, \ldots, \mathbf{l}_n L 1 : n ​ = l 1 ​ , … , l n ​ , mentre la dimensionalità di ciascun vettore di logit l i \mathbf{l}_i l i ​ corrisponde alla dimensione del vocabolario. In questo modo, per ogni i ∈ { 1 , … , n } i \in \{1, \ldots, n\} i ∈ { 1 , … , n } è possibile ottenere una distribuzione di probabilità sull’intero vocabolario applicando un’operazione softmax su l i \mathbf{l}_i l i ​ . Queste distribuzioni definiscono la distribuzione condizionale:

p θ d e c ( y i ∣ Y 0 : i − 1 , X ‾ 1 : n ) , ∀ i ∈ { 1 , … , n } , p_{\theta_{dec}}(\mathbf{y}_i | \mathbf{Y}_{0: i-1}, \mathbf{\overline{X}}_{1:n}), \forall i \in \{1, \ldots, n\}, p θ d e c ​ ​ ( y i ​ ∣ Y 0 : i − 1 ​ , X 1 : n ​ ) , ∀ i ∈ { 1 , … , n } ,

rispettivamente. Il “LM head” è spesso legato alla trasposta della matrice di embedding delle parole, cioè W emb ⊺ = [ y 1 , … , y vocab ] ⊺ \mathbf{W}_{\text{emb}}^{\intercal} = \left[\mathbf{y}^1, \ldots, \mathbf{y}^{\text{vocab}}\right]^{\intercal} W emb ⊺ ​ = [ y 1 , … , y vocab ] ⊺ 1 {}^1 1 . In modo intuitivo, ciò significa che per tutti i i ∈ { 0 , … , n − 1 } i \in \{0, \ldots, n – 1\} i ∈ { 0 , … , n − 1 } il livello “LM Head” confronta il vettore di output codificato y ‾ i \mathbf{\overline{y}}_i y ​ i ​ con tutti gli embedding delle parole nel vocabolario y 1 , … , y vocab \mathbf{y}^1, \ldots, \mathbf{y}^{\text{vocab}} y 1 , … , y vocab in modo che il vettore di logit l i + 1 \mathbf{l}_{i+1} l i + 1 ​ rappresenti i punteggi di similarità tra il vettore di output codificato e ogni embedding delle parole. L’operazione softmax trasforma semplicemente i punteggi di similarità in una distribuzione di probabilità. Per ogni i ∈ { 1 , … , n } i \in \{1, \ldots, n\} i ∈ { 1 , … , n } , valgono le seguenti equazioni:

p θ d e c ( y ∣ X ‾ 1 : n , Y 0 : i − 1 ) p_{\theta_{dec}}(\mathbf{y} | \mathbf{\overline{X}}_{1:n}, \mathbf{Y}_{0:i-1}) p θ d e c ​ ​ ( y ∣ X 1 : n ​ , Y 0 : i − 1 ​ ) = Softmax ( f θ dec ( X ‾ 1 : n , Y 0 : i − 1 ) ) = \text{Softmax}(f_{\theta_{\text{dec}}}(\mathbf{\overline{X}}_{1:n}, \mathbf{Y}_{0:i-1})) = Softmax ( f θ dec ​ ​ ( X 1 : n ​ , Y 0 : i − 1 ​ ) ) = Softmax ( W emb ⊺ y ‾ i − 1 ) = \text{Softmax}(\mathbf{W}_{\text{emb}}^{\intercal} \mathbf{\overline{y}}_{i-1}) = Softmax ( W emb ⊺ ​ y ​ i − 1 ​ ) = Softmax ( l i ) . = \text{Softmax}(\mathbf{l}_i). = Softmax ( l i ​ ) .

Mettere tutto insieme, per modellare la distribuzione condizionale di una sequenza di vettori target Y 1 : m \mathbf{Y}_{1: m} Y 1 : m ​ , i vettori target Y 1 : m − 1 \mathbf{Y}_{1:m-1} Y 1 : m − 1 ​ preceduti dal vettore speciale BOS \text{BOS} BOS , cioè y 0 \mathbf{y}_0 y 0 ​ , vengono prima mappati insieme alla sequenza di codifica contestualizzata X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n ​ alla sequenza di logit vettoriali L 1 : m \mathbf{L}_{1:m} L 1 : m ​ . Di conseguenza, ogni vettore logit target l i \mathbf{l}_i l i ​ viene trasformato in una distribuzione di probabilità condizionale del vettore target y i \mathbf{y}_i y i ​ utilizzando l’operazione softmax. Infine, le probabilità condizionali di tutti i vettori target y 1 , … , y m \mathbf{y}_1, \ldots, \mathbf{y}_m y 1 ​ , … , y m ​ vengono moltiplicate insieme per ottenere la probabilità condizionale della sequenza completa di vettori target:

p θ d e c (Y 1 : m ∣ X ‾ 1 : n) = ∏ i = 1 m p θ d e c (y i ∣ Y 0 : i − 1, X ‾ 1 : n).

A differenza degli encoder basati su trasformatore, nei decoder basati su trasformatore, il vettore di output codificato y ‾ i dovrebbe essere una buona rappresentazione del vettore target successivo y i + 1 e non del vettore di input stesso. Inoltre, il vettore di output codificato y ‾ i dovrebbe essere condizionato da tutta la sequenza di codifica contestualizzata X ‾ 1 : n. Per soddisfare questi requisiti, ogni blocco del decoder è composto da uno strato di auto-attenzione “unidirezionale”, seguito da uno strato di “cross-attenzione” e da due strati di feed-forward 2^2. Lo strato di auto-attenzione unidirezionale mette in relazione ciascuno dei suoi vettori di input y ′ j solo con tutti i vettori di input precedenti y ′ i, con i ≤ j per tutti j ∈ {1, …, n} per modellare la distribuzione di probabilità dei successivi vettori target. Lo strato di cross-attenzione mette in relazione ciascuno dei suoi vettori di input y ′ ′ j con tutti i vettori di codifica contestualizzati X ‾ 1 : n per condizionare la distribuzione di probabilità dei successivi vettori target anche sull’input dell’encoder stesso.

Ora, visualizziamo il decoder basato su trasformatore per il nostro esempio di traduzione dall’inglese al tedesco.

Possiamo vedere che il decoder mappa l’input Y 0 : 5 “BOS”, “Ich”, “will”, “ein”, “Auto”, “kaufen” (mostrato in rosso chiaro) insieme alla sequenza contestualizzata di “I”, “want”, “to”, “buy”, “a”, “car”, “EOS”, cioè X ‾ 1 : 7 (mostrato in verde scuro) ai vettori di logit L 1 : 6 (mostrati in rosso scuro).

Applicando un’operazione softmax su ciascun l 1, l 2, …, l 5, possiamo quindi definire le distribuzioni di probabilità condizionata:

p θ d e c (y ∣ BOS, X ‾ 1 : 7), p θ d e c (y ∣ BOS Ich, X ‾ 1 : 7), p θ d e c (y ∣ BOS Ich will ein Auto kaufen, X ‾ 1 : 7).

La probabilità condizionale complessiva di:

p θ d e c ( Ich will ein Auto kaufen EOS ∣ X ‾ 1 : n ) p_{\theta_{dec}}(\text{Ich will ein Auto kaufen EOS} | \mathbf{\overline{X}}_{1:n}) p θ d e c ​ ​ ( Ich will ein Auto kaufen EOS ∣ X 1 : n ​ )

può quindi essere calcolata come il prodotto seguente:

p θ d e c ( Ich ∣ BOS , X ‾ 1 : 7 ) × … × p θ d e c ( EOS ∣ BOS Ich will ein Auto kaufen , X ‾ 1 : 7 ) . p_{\theta_{dec}}(\text{Ich} | \text{BOS}, \mathbf{\overline{X}}_{1:7}) \times \ldots \times p_{\theta_{dec}}(\text{EOS} | \text{BOS Ich will ein Auto kaufen}, \mathbf{\overline{X}}_{1:7}). p θ d e c ​ ​ ( Ich ∣ BOS , X 1 : 7 ​ ) × … × p θ d e c ​ ​ ( EOS ∣ BOS Ich will ein Auto kaufen , X 1 : 7 ​ ) .

La casella rossa a destra mostra un blocco del decoder per i primi tre vettori target y 0 , y 1 , y 2 \mathbf{y}_0, \mathbf{y}_1, \mathbf{y}_2 y 0 ​ , y 1 ​ , y 2 ​ . Nella parte inferiore, viene illustrato il meccanismo di auto-attenzione unidirezionale e nella parte centrale viene illustrato il meccanismo di attenzione incrociata. Concentriamoci prima sull’auto-attenzione unidirezionale.

Come nell’auto-attenzione bidirezionale, nell’auto-attenzione unidirezionale, i vettori di query q 0 , … , q m − 1 \mathbf{q}_0, \ldots, \mathbf{q}_{m-1} q 0 ​ , … , q m − 1 ​ (mostrati in viola sotto), i vettori chiave k 0 , … , k m − 1 \mathbf{k}_0, \ldots, \mathbf{k}_{m-1} k 0 ​ , … , k m − 1 ​ (mostrati in arancione sotto) e i vettori valore v 0 , … , v m − 1 \mathbf{v}_0, \ldots, \mathbf{v}_{m-1} v 0 ​ , … , v m − 1 ​ (mostrati in blu sotto) vengono proiettati dai rispettivi vettori di input y ′ 0 , … , y ′ m − 1 \mathbf{y’}_0, \ldots, \mathbf{y’}_{m-1} y ′ 0 ​ , … , y ′ m − 1 ​ (mostrati in rosso chiaro sotto). Tuttavia, nell’auto-attenzione unidirezionale, ogni vettore di query q i \mathbf{q}_i q i ​ viene confrontato solo con il suo rispettivo vettore chiave e con tutti quelli precedenti, ovvero k 0 , … , k i \mathbf{k}_0, \ldots, \mathbf{k}_i k 0 ​ , … , k i ​ per ottenere i rispettivi pesi di attenzione . Questo impedisce a un vettore di output y ′ ′ j \mathbf{y”}_j y ′ ′ j ​ (mostrato in rosso scuro sotto) di includere informazioni sul vettore di input successivo y i , con i > j \mathbf{y}_i, \text{ con } i > j y i ​ , con i > j per tutti i j ∈ { 0 , … , m − 1 } j \in \{0, \ldots, m – 1 \} j ∈ { 0 , … , m − 1 } . Come nel caso dell’auto-attenzione bidirezionale, i pesi di attenzione vengono quindi moltiplicati per i rispettivi vettori valore e sommati insieme.

Possiamo riassumere l’auto-attenzione unidirezionale come segue:

y ′ ′ i = V 0 : i Softmax ( K 0 : i ⊺ q i ) + y ′ i . \mathbf{y”}_i = \mathbf{V}_{0: i} \textbf{Softmax}(\mathbf{K}_{0: i}^\intercal \mathbf{q}_i) + \mathbf{y’}_i. y ′ ′ i ​ = V 0 : i ​ Softmax ( K 0 : i ⊺ ​ q i ​ ) + y ′ i ​ .

Nota che l’intervallo di indicizzazione dei vettori chiave e valore è 0 : i 0:i 0 : i invece di 0 : m − 1 0: m-1 0 : m − 1 che sarebbe l’intervallo dei vettori chiave nell’auto-attenzione bidirezionale.

Illustreremo l’auto-attenzione unidirezionale per il vettore di input y ′ 1 \mathbf{y’}_1 y ′ 1 ​ per il nostro esempio sopra.

Come si può vedere y ′ ′ 1 \mathbf{y”}_1 y ′ ′ 1 ​ dipende solo da y ′ 0 \mathbf{y’}_0 y ′ 0 ​ e y ′ 1 \mathbf{y’}_1 y ′ 1 ​ . Pertanto, mettiamo la rappresentazione vettoriale della parola “Ich”, cioè y ′ 1 \mathbf{y’}_1 y ′ 1 ​, solo in relazione a se stessa e al vettore target “BOS”, cioè y ′ 0 \mathbf{y’}_0 y ′ 0 ​ , ma non con la rappresentazione vettoriale della parola “will”, cioè y ′ 2 \mathbf{y’}_2 y ′ 2 ​ .

Allora perché è importante che utilizziamo l’auto-attenzione unidirezionale nel decoder invece dell’auto-attenzione bidirezionale? Come indicato in precedenza, un decoder basato su trasformatore definisce una mappatura da una sequenza di vettori di input Y 0 : m − 1 \mathbf{Y}_{0: m-1} Y 0 : m − 1 ​ ai logit corrispondenti ai successivi vettori di input del decoder, ovvero L 1 : m \mathbf{L}_{1:m} L 1 : m ​ . Nel nostro esempio, ciò significa, ad esempio, che il vettore di input y 1 \mathbf{y}_1 y 1 ​ = “Ich” viene mappato al vettore di logit l 2 \mathbf{l}_2 l 2 ​ , che viene quindi utilizzato per predire il vettore di input y 2 \mathbf{y}_2 y 2 ​ . Pertanto, se y ′ 1 \mathbf{y’}_1 y ′ 1 ​ avesse accesso ai seguenti vettori di input Y ′ 2 : 5 \mathbf{Y’}_{2:5} Y ′ 2 : 5 ​ , il decoder copierebbe semplicemente la rappresentazione vettoriale di “will”, cioè y ′ 2 \mathbf{y’}_2 y ′ 2 ​ , per essere il suo output y ′ ′ 1 \mathbf{y”}_1 y ′ ′ 1 ​ . Ciò sarebbe inoltrato all’ultimo strato in modo che il vettore di output codificato y ‾ 1 \mathbf{\overline{y}}_1 y ​ 1 ​ corrisponderebbe essenzialmente alla rappresentazione vettoriale y 2 \mathbf{y}_2 y 2 ​ .

Questo è ovviamente svantaggioso poiché il decoder basato su trasformatore non imparerà mai a predire la parola successiva data tutte le parole precedenti, ma copierà semplicemente il vettore target y i \mathbf{y}_i y i ​ attraverso la rete in y ‾ i − 1 \mathbf{\overline{y}}_{i-1} y ​ i − 1 ​ per tutti i ∈ { 1 , … , m } i \in \{1, \ldots, m \} i ∈ { 1 , … , m } . Per definire una distribuzione condizionale del vettore target successivo, la distribuzione non può essere condizionata dal vettore target stesso. Non ha molto senso prevedere y i \mathbf{y}_i y i ​ da p ( y ∣ Y 0 : i , X ‾ ) p(\mathbf{y} | \mathbf{Y}_{0:i}, \mathbf{\overline{X}}) p ( y ∣ Y 0 : i ​ , X ) perché la distribuzione è condizionata dal vettore target che si suppone di modellare. L’architettura di auto-attenzione unidirezionale, quindi, ci permette di definire una distribuzione di probabilità causale, che è necessaria per modellare efficacemente una distribuzione condizionale del vettore target successivo.

Benissimo! Ora possiamo passare allo strato che collega l’encoder e il decoder – il meccanismo di attenzione incrociata!

Lo strato di attenzione incrociata prende in input due sequenze di vettori: le uscite dello strato di auto-attenzione unidirezionale, cioè Y ′ ′ 0 : m − 1 \mathbf{Y”}_{0: m-1} Y ′ ′ 0 : m − 1 ​ e i vettori di codifica contestualizzati X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n ​ . Come nello strato di auto-attenzione, i vettori di query q 0 , … , q m − 1 \mathbf{q}_0, \ldots, \mathbf{q}_{m-1} q 0 ​ , … , q m − 1 ​ sono proiezioni dei vettori di output dello strato precedente, cioè Y ′ ′ 0 : m − 1 \mathbf{Y”}_{0: m-1} Y ′ ′ 0 : m − 1 ​ . Tuttavia, i vettori chiave e valore k 0 , … , k m − 1 \mathbf{k}_0, \ldots, \mathbf{k}_{m-1} k 0 ​ , … , k m − 1 ​ e v 0 , … , v m − 1 \mathbf{v}_0, \ldots, \mathbf{v}_{m-1} v 0 ​ , … , v m − 1 ​ sono proiezioni dei vettori di codifica contestualizzati X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n ​ . Dopo aver definito i vettori di query, chiave e valore, un vettore di query q i \mathbf{q}_i q i ​ viene quindi confrontato con tutti i vettori chiave e il punteggio corrispondente viene utilizzato per pesare i rispettivi vettori valore, proprio come avviene per l’auto-attenzione bidirezionale per fornire il vettore di output y ′ ′ ′ i \mathbf{y”’}_i y ′ ′ ′ i ​ per tutti i ∈ 0 , … , m − 1 i \in \{0, \ldots, m-1\} i ∈ 0 , … , m − 1 . L’attenzione incrociata può essere riassunta come segue:

y ′ ′ ′ i = V 1 : n Softmax ( K 1 : n ⊺ q i ) + y ′ ′ i. \mathbf{y”’}_i = \mathbf{V}_{1:n} \textbf{Softmax}(\mathbf{K}_{1: n}^\intercal \mathbf{q}_i) + \mathbf{y”}_i. y ′ ′ ′ i ​ = V 1 : n ​ Softmax ( K 1 : n ⊺ ​ q i ​ ) + y ′ ′ i ​ .

Si noti che l’intervallo di indice dei vettori chiave e valore è 1 : n 1:n 1 : n corrispondente al numero di vettori di codifica contestualizzati.

Visualizziamo il meccanismo di cross-attenzione per il vettore di input y ′ ′ 1 \mathbf{y”}_1 y ′ ′ 1 ​ per il nostro esempio precedente.

Possiamo vedere che il vettore di query q 1 \mathbf{q}_1 q 1 ​ (mostrato in viola) deriva da y ′ ′ 1 \mathbf{y”}_1 y ′ ′ 1 ​ (mostrato in rosso) e quindi si basa su una rappresentazione vettoriale della parola “Ich”. Il vettore di query q 1 \mathbf{q}_1 q 1 ​ viene quindi confrontato con i vettori chiave k 1 , … , k 7 \mathbf{k}_1, \ldots, \mathbf{k}_7 k 1 ​ , … , k 7 ​ (mostrati in giallo) corrispondenti alla rappresentazione di codifica contestualizzata di tutti i vettori di input dell’encoder X 1 : n \mathbf{X}_{1:n} X 1 : n ​ = “Voglio comprare una macchina EOS”. Ciò mette in relazione la rappresentazione vettoriale di “Ich” con tutti i vettori di input dell’encoder. Infine, i pesi di attenzione vengono moltiplicati per i vettori valore v 1 , … , v 7 \mathbf{v}_1, \ldots, \mathbf{v}_7 v 1 ​ , … , v 7 ​ (mostrati in turchese) per ottenere oltre al vettore di input y ′ ′ 1 \mathbf{y”}_1 y ′ ′ 1 ​ il vettore di output y ′ ′ ′ 1 \mathbf{y”’}_1 y ′ ′ ′ 1 ​ (mostrato in rosso scuro).

Quindi intuitivamente, cosa succede esattamente qui? Ogni vettore di output y ′ ′ ′ i \mathbf{y”’}_i y ′ ′ ′ i ​ è una somma pesata di tutte le proiezioni valore degli input dell’encoder v 1 , … , v 7 \mathbf{v}_{1}, \ldots, \mathbf{v}_7 v 1 ​ , … , v 7 ​ oltre al vettore di input stesso y ′ ′ i \mathbf{y”}_i y ′ ′ i ​ (c.f. formula illustrata sopra). Il meccanismo chiave da capire è il seguente: a seconda di quanto simile è una proiezione di query del vettore di input del decoder q i \mathbf{q}_i q i ​ a una proiezione chiave del vettore di input dell’encoder k j \mathbf{k}_j k j ​ , più importante è la proiezione valore del vettore di input dell’encoder v j \mathbf{v}_j v j ​ . In termini più generici, ciò significa che più una rappresentazione dell’input del decoder è “relata” a una rappresentazione dell’input dell’encoder, più l’input influenza la rappresentazione dell’output del decoder.

Fantastico! Ora possiamo vedere come questa architettura condizioni in modo ottimale ogni vettore di output y ′ ′ ′ i \mathbf{y”’}_i y ′ ′ ′ i ​ sull’interazione tra i vettori di input dell’encoder X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n ​ e il vettore di input y ′ ′ i \mathbf{y”}_i y ′ ′ i ​ . Un’altra osservazione importante in questo punto è che l’architettura è completamente indipendente dal numero n n n di vettori di codifica contestualizzati X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n ​ su cui il vettore di output y ′ ′ ′ i \mathbf{y”’}_i y ′ ′ ′ i ​ è condizionato. Tutte le matrici di proiezione W k cross \mathbf{W}^{\text{cross}}_{k} W k cross ​ e W v cross \mathbf{W}^{\text{cross}}_{v} W v cross ​ per ricavare i vettori chiave k 1 , … , k n \mathbf{k}_1, \ldots, \mathbf{k}_n k 1 ​ , … , k n ​ e i vettori valore v 1 , … , v n \mathbf{v}_1, \ldots, \mathbf{v}_n v 1 ​ , … , v n ​ rispettivamente sono condivisi in tutte le posizioni 1 , … , n 1, \ldots, n 1 , … , n e tutti i vettori valore v 1 , … , v n \mathbf{v}_1, \ldots, \mathbf{v}_n v 1 ​ , … , v n ​ sono sommati insieme per ottenere un singolo vettore pesato medio. Ora diventa ovvio anche perché il decoder basato su Transformer non soffre del problema della dipendenza a lungo raggio, a differenza del decoder basato su RNN. Poiché ogni vettore di logit del decoder dipende direttamente da ogni singolo vettore di output codificato, il numero di operazioni matematiche per confrontare il primo vettore di output codificato e l’ultimo vettore di logit del decoder ammonta essenzialmente a uno solo.

Per concludere, il livello di auto-attenzione unidirezionale è responsabile di condizionare ogni vettore di output su tutti i vettori di input precedenti del decoder e l’attivazione incrociata è responsabile di condizionare ulteriormente ogni vettore di output su tutti i vettori di input codificati.

Per verificare la nostra comprensione teorica, continuiamo l’esempio di codice dalla sezione del codificatore sopra.


1 {}^1 1 La matrice di embedding delle parole W emb \mathbf{W}_{\text{emb}} W emb ​ fornisce ad ogni parola di input una rappresentazione vettoriale unica indipendente dal contesto. Questa matrice viene spesso fissata come “LM Head” layer. Tuttavia, il “LM Head” layer può anche consistere in un mapping di peso completamente indipendente “vettore codificato-a-logit”.

2 {}^2 2 Ancora, una spiegazione dettagliata del ruolo dei livelli feed-forward nei modelli basati sui transformer esula dagli obiettivi di questo notebook. Secondo Yun et. al, (2017) i livelli feed-forward sono cruciali per mappare ogni vettore di contesto x ′ i \mathbf{x’}_i x ′ i ​ individualmente nello spazio di output desiderato, cosa che il livello di auto-attenzione non riesce a fare da solo. Va notato che ogni token di output x ′ \mathbf{x’} x ′ viene elaborato dallo stesso livello feed-forward. Per ulteriori dettagli, si consiglia al lettore di leggere l’articolo.

from transformers import MarianMTModel, MarianTokenizer
import torch

tokenizer = MarianTokenizer.from_pretrained("Helsinki-NLP/opus-mt-en-de")
model = MarianMTModel.from_pretrained("Helsinki-NLP/opus-mt-en-de")
embeddings = model.get_input_embeddings()

# create token ids for encoder input
input_ids = tokenizer("Voglio comprare una macchina", return_tensors="pt").input_ids

# pass input token ids to encoder
encoder_output_vectors = model.base_model.encoder(input_ids, return_dict=True).last_hidden_state

# create token ids for decoder input
decoder_input_ids = tokenizer("<pad> Ich will ein", return_tensors="pt", add_special_tokens=False).input_ids

# pass decoder input ids and encoded input vectors to decoder
decoder_output_vectors = model.base_model.decoder(decoder_input_ids, encoder_hidden_states=encoder_output_vectors).last_hidden_state

# derive embeddings by multiplying decoder outputs with embedding weights
lm_logits = torch.nn.functional.linear(decoder_output_vectors, embeddings.weight, bias=model.final_logits_bias)

# change the decoder input slightly
decoder_input_ids_perturbed = tokenizer("<pad> Ich will das", return_tensors="pt", add_special_tokens=False).input_ids
decoder_output_vectors_perturbed = model.base_model.decoder(decoder_input_ids_perturbed, encoder_hidden_states=encoder_output_vectors).last_hidden_state
lm_logits_perturbed = torch.nn.functional.linear(decoder_output_vectors_perturbed, embeddings.weight, bias=model.final_logits_bias)

# compare shape and encoding of first vector
print(f"Shape of decoder input vectors {embeddings(decoder_input_ids).shape}. Shape of decoder logits {lm_logits.shape}")

# compare values of word embedding of "I" for input_ids and perturbed input_ids
print("Is encoding for `Ich` equal to its perturbed version?: ", torch.allclose(lm_logits[0, 0], lm_logits_perturbed[0, 0], atol=1e-3))

Output:

    Shape of decoder input vectors torch.Size([1, 5, 512]). Shape of decoder logits torch.Size([1, 5, 58101])
    Is encoding for `Ich` equal to its perturbed version?:  True

Confrontiamo la forma dell’output degli embedding delle parole di input del decoder, ossia embeddings(decoder_input_ids) (corrisponde a Y 0 : 4 \mathbf{Y}_{0: 4} Y 0 : 4 ​ , dove <pad> corrisponde a BOS e “Ich will das” è tokenizzato in 4 token) con la dimensionalità dei lm_logits (corrisponde a L 1 : 5 \mathbf{L}_{1:5} L 1 : 5 ​ ). Inoltre, abbiamo passato la sequenza di parole ” <pad> Ich will ein” e una versione leggermente alterata ” <pad> Ich will das” insieme ai encoder_output_vectors attraverso il decoder per verificare se il secondo lm_logit, corrispondente a “Ich”, differisce quando viene cambiata solo l’ultima parola nella sequenza di input (“ein” -> “das”).

Come previsto, le forme di output degli embedding delle parole di input del decoder e di lm_logits, cioè la dimensionalità di Y 0 : 4 \mathbf{Y}_{0: 4} Y 0 : 4 ​ e L 1 : 5 \mathbf{L}_{1:5} L 1 : 5 ​, sono diverse nell’ultima dimensione. Mentre la lunghezza della sequenza è la stessa (=5), la dimensionalità di un embedding di parola di input del decoder corrisponde a model.config.hidden_size, mentre la dimensionalità di un lm_logit corrisponde alla dimensione del vocabolario model.config.vocab_size, come spiegato in precedenza. In secondo luogo, si può notare che i valori del vettore di output codificato di l 1 = “Ich” \mathbf{l}_1 = \text{“Ich”} l 1 ​ = “Ich” sono gli stessi quando l’ultima parola viene cambiata da “ein” a “das”. Tuttavia, questo non dovrebbe sorprendere se si è compreso l’auto-attenzione unidirezionale.

Per quanto riguarda una nota finale, i modelli auto-regressivi, come GPT2, hanno la stessa architettura dei modelli decoder basati su trasformatori se si rimuove il layer di cross-attenzione perché i modelli auto-regressivi autonomi non sono condizionati da alcun output dell’encoder. Quindi i modelli auto-regressivi sono essenzialmente gli stessi dei modelli di auto-codifica, ma sostituiscono l’attenzione bidirezionale con l’attenzione unidirezionale. Questi modelli possono anche essere pre-addestrati su grandi quantità di dati di testo open-domain per mostrare prestazioni impressionanti nelle attività di generazione del linguaggio naturale (NLG). In Radford et al. (2019), gli autori mostrano che un modello GPT2 pre-addestrato può ottenere risultati SOTA o vicini a SOTA su una varietà di compiti NLG senza troppo fine-tuning. Tutti i modelli auto-regressivi di 🤗Transformers possono essere trovati qui.

Ok, questo è tutto! Ora dovresti aver acquisito una buona comprensione dei modelli encoder-decoder basati su trasformatori e di come usarli con la libreria 🤗Transformers.

Grazie mille a Victor Sanh, Sasha Rush, Sam Shleifer, Oliver Åstrand, ‪Ted Moskovitz e Kristian Kyvik per i preziosi feedback.

Appendice

Come menzionato in precedenza, il seguente frammento di codice mostra come si può programmare un semplice metodo di generazione per i modelli encoder-decoder basati su trasformatori. Qui, implementiamo un semplice metodo di decodifica greedy utilizzando torch.argmax per estrarre il vettore target.

from transformers import MarianMTModel, MarianTokenizer
import torch

tokenizer = MarianTokenizer.from_pretrained("Helsinki-NLP/opus-mt-en-de")
model = MarianMTModel.from_pretrained("Helsinki-NLP/opus-mt-en-de")

# crea gli id dei vettori di input codificati
input_ids = tokenizer("Voglio comprare una macchina", return_tensors="pt").input_ids

# crea il token BOS
decoder_input_ids = tokenizer("<pad>", add_special_tokens=False, return_tensors="pt").input_ids

assert decoder_input_ids[0, 0].item() == model.config.decoder_start_token_id, "`decoder_input_ids` dovrebbe corrispondere a `model.config.decoder_start_token_id`"

# STEP 1

# passa input_ids all'encoder e al decoder e passa il token BOS al decoder per ottenere il primo logit
outputs = model(input_ids, decoder_input_ids=decoder_input_ids, return_dict=True)

# ottieni la sequenza codificata
encoded_sequence = (outputs.encoder_last_hidden_state,)
# ottieni i logit
lm_logits = outputs.logits

# campiona l'ultimo token con la probabilità più alta
next_decoder_input_ids = torch.argmax(lm_logits[:, -1:], axis=-1)

# concatena
decoder_input_ids = torch.cat([decoder_input_ids, next_decoder_input_ids], axis=-1)

# STEP 2

# riutilizza gli input codificati e passa BOS + "Ich" al decoder per il secondo logit
lm_logits = model(None, encoder_outputs=encoded_sequence, decoder_input_ids=decoder_input_ids, return_dict=True).logits

# campiona nuovamente l'ultimo token con la probabilità più alta
next_decoder_input_ids = torch.argmax(lm_logits[:, -1:], axis=-1)

# concatena nuovamente
decoder_input_ids = torch.cat([decoder_input_ids, next_decoder_input_ids], axis=-1)

# STEP 3
lm_logits = model(None, encoder_outputs=encoded_sequence, decoder_input_ids=decoder_input_ids, return_dict=True).logits
next_decoder_input_ids = torch.argmax(lm_logits[:, -1:], axis=-1)
decoder_input_ids = torch.cat([decoder_input_ids, next_decoder_input_ids], axis=-1)

# vediamo cosa abbiamo generato finora!
print(f"Così generato finora: {tokenizer.decode(decoder_input_ids[0], skip_special_tokens=True)}")

# Questo può essere scritto anche in un ciclo.

Output:

    Generato finora: Ich will ein

In questo esempio di codice, mostriamo esattamente ciò che è stato descritto in precedenza. Passiamo un input “Voglio comprare una macchina” insieme al token BOS \text{BOS} BOS al modello encoder-decoder e campioniamo dal primo logit l 1 \mathbf{l}_1 l 1 ​ (cioè la prima riga di lm_logits). La nostra strategia di campionamento è semplice: scegliamo avidamente il prossimo vettore di input del decoder che ha la probabilità più alta. In modo auto-regressivo, quindi passiamo il vettore di input del decoder campionato insieme agli input precedenti al modello encoder-decoder e campioniamo di nuovo. Ripetiamo questo processo una terza volta. Come risultato, il modello ha generato le parole “Ich will ein”. Il risultato è perfetto – questa è l’inizio della traduzione corretta dell’input.

Nella pratica, vengono utilizzati metodi di decodifica più complicati per campionare i lm_logits. La maggior parte dei quali è coperta in questo post del blog.