Sfruttare i checkpoint dei modelli di lingua preaddestrati per i modelli codificatore-decodificatore
Utilizzare i checkpoint dei modelli di lingua preaddestrati per i modelli encoder-decoder.
I modelli basati su encoder-decoder trasformatore sono stati proposti da Vaswani et al. (2017) e di recente hanno suscitato un interesse crescente, ad esempio Lewis et al. (2019), Raffel et al. (2019), Zhang et al. (2020), Zaheer et al. (2020), Yan et al. (2020).
Similmente a BERT e GPT2, i modelli di encoder-decoder pre-addestrati su larga scala hanno dimostrato di migliorare significativamente le prestazioni su una varietà di compiti sequenza-su-sequenza, Lewis et al. (2019), Raffel et al. (2019). Tuttavia, a causa del costo computazionale enorme associato al pre-addestramento dei modelli encoder-decoder, lo sviluppo di tali modelli è principalmente limitato a grandi aziende e istituti.
In Leveraging Pre-trained Checkpoints for Sequence Generation Tasks (2020), Sascha Rothe, Shashi Narayan e Aliaksei Severyn inizializzano il modello encoder-decoder con checkpoint pre-addestrati dell’encoder e/o del decoder (ad esempio BERT, GPT2) per evitare il costoso pre-addestramento. Gli autori dimostrano che tali modelli encoder-decoder avviati in questo modo producono risultati competitivi rispetto a modelli encoder-decoder pre-addestrati su larga scala, come T5 e Pegasus, per più compiti sequenza-su-sequenza a una frazione del costo di addestramento.
- Come abbiamo accelerato l’elaborazione di inferenza del transformer di 100 volte per i clienti di 🤗 API
- Ottieni risultati migliori e addestra più velocemente con ZeRO tramite DeepSpeed e FairScale
- Modelli TensorFlow più veloci in Hugging Face Transformers
In questo notebook, spiegheremo in dettaglio come i modelli encoder-decoder possono essere avviati in modo caldo, daremo consigli pratici basati su Rothe et al. (2020) e infine esamineremo un esempio di codice completo che mostra come avviare in modo caldo i modelli encoder-decoder con 🤗Transformers.
Questo notebook è diviso in 4 parti:
- Introduzione – Breve riassunto dei modelli di linguaggio pre-addestrati in NLP e la necessità di avviare in modo caldo i modelli encoder-decoder.
- Avviare in modo caldo i modelli encoder-decoder (Teoria) – Spiegazione illustrativa su come vengono avviati in modo caldo i modelli encoder-decoder.
- Avviare in modo caldo i modelli encoder-decoder (Analisi) – Riassunto di Leveraging Pre-trained Checkpoints for Sequence Generation Tasks
-
- Quali combinazioni di modelli sono efficaci per avviare in modo caldo i modelli encoder-decoder; in che modo differiscono da compito a compito?
-
- Avviare in modo caldo i modelli encoder-decoder con 🤗Transformers (Pratica) – Esempio di codice completo che mostra in dettaglio come utilizzare il framework
EncoderDecoderModel
per avviare in modo caldo i modelli encoder-decoder basati su trasformatori.
È altamente consigliato (probabilmente anche necessario) leggere questo post sul blog sui modelli di encoder-decoder basati su trasformatori.
Iniziamo dando qualche contesto sull’avvio in modo caldo dei modelli encoder-decoder.
Introduzione
Recentemente, i modelli di linguaggio pre-addestrati 1 {}^1 1 hanno rivoluzionato il campo dell’elaborazione del linguaggio naturale (NLP).
I primi modelli di linguaggio pre-addestrati erano basati su reti neurali ricorrenti (RNN) come proposto da Dai et al. (2015). Dai et al. hanno dimostrato che il pre-addestramento di un modello basato su RNN su dati non etichettati e successivamente il raffinamento 2 {}^2 2 su un compito specifico produce risultati migliori rispetto all’addestramento diretto di un modello inizializzato casualmente su tale compito. Tuttavia, è stato solo nel 2018 che i modelli di linguaggio pre-addestrati sono stati ampiamente accettati in NLP. ELMO di Peters et al. e ULMFit di Howard et al. sono stati i primi modelli di linguaggio pre-addestrati a migliorare significativamente lo stato dell’arte su una serie di compiti di comprensione del linguaggio naturale (NLU). Solo un paio di mesi dopo, OpenAI e Google hanno pubblicato modelli di linguaggio pre-addestrati basati su trasformatori, chiamati rispettivamente GPT di Radford et al. e BERT di Devlin et al. L’efficienza migliorata dei modelli di linguaggio basati su trasformatori rispetto alle RNN ha permesso a GPT2 e BERT di essere pre-addestrati su grandi quantità di dati di testo non etichettato. Una volta pre-addestrati, BERT e GPT hanno dimostrato di richiedere pochissimo raffinamento per superare i risultati di stato dell’arte su più di una dozzina di compiti NLU 3 {}^3 3 .
La capacità dei modelli di linguaggio pre-addestrati di trasferire efficacemente conoscenze agnostiche dal compito a conoscenze specifiche del compito si è rivelata un grande catalizzatore per l’NLU. Mentre in passato gli ingegneri e i ricercatori dovevano addestrare un modello di linguaggio da zero, ora checkpoint pre-addestrati di grandi modelli di linguaggio possono essere raffinati a una frazione del costo e del tempo. Questo può risparmiare milioni nell’industria e consente prototipazione più veloce e migliori benchmark nella ricerca.
I modelli di linguaggio pre-addestrati hanno stabilito un nuovo livello di prestazioni nelle attività di NLU e sempre più ricerche si basano sull’utilizzo di tali modelli per migliorare i sistemi di NLU. Tuttavia, i modelli BERT e GPT autonomi sono stati meno efficaci per le attività di sequenza-su-sequenza, ad esempio riassunto di testo, traduzione automatica, riformulazione di frasi, ecc.
Le attività di sequenza-su-sequenza sono definite come una mappatura da 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 di output m m m sconosciuta a priori. Pertanto, un modello di sequenza-su-sequenza dovrebbe definire la distribuzione di probabilità condizionale della sequenza di output Y 1 : m \mathbf{Y}_{1:m} Y 1 : m condizionata alla sequenza di input X 1 : n \mathbf{X}_{1:n} X 1 : n :
p θ model ( Y 1 : m ∣ X 1 : n ) . p_{\theta_{\text{model}}}(\mathbf{Y}_{1:m} | \mathbf{X}_{1:n}). p θ model ( Y 1 : m ∣ X 1 : n ) .
Senza perdita di generalità, una sequenza di parole di input di n n n parole viene qui rappresentata dal vettore di sequenza 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 e una sequenza di output di m m m parole come Y 1 : m = y 1 , … , y m \mathbf{Y}_{1:m} = \mathbf{y}_1, \ldots, \mathbf{y}_m Y 1 : m = y 1 , … , y m .
Vediamo come BERT e GPT2 possono essere adatti per modellare le attività di sequenza-su-sequenza.
BERT
BERT è un modello solo encoder, che mappa una sequenza di input X 1 : n \mathbf{X}_{1:n} X 1 : n in una sequenza codificata contestualizzata X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n :
f θ BERT : X 1 : n → X ‾ 1 : n . f_{\theta_{\text{BERT}}}: \mathbf{X}_{1:n} \to \mathbf{\overline{X}}_{1:n}. f θ BERT : X 1 : n → X 1 : n .
La sequenza codificata contestualizzata X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n di BERT può quindi essere ulteriormente elaborata da uno strato di classificazione per le attività di classificazione NLU, come analisi del sentiment , inferenza del linguaggio naturale, ecc. Per farlo, lo strato di classificazione, ovvero tipicamente uno strato di pooling seguito da uno strato feed-forward, viene aggiunto come ultimo strato sopra BERT per mappare la sequenza codificata contestualizzata X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n a una classe c c c:
f θ p,c : X ‾ 1 : n → c . f_{\theta{\text{p,c}}}: \mathbf{\overline{X}}_{1:n} \to c. f θ p,c : X 1 : n → c .
È stato dimostrato che l’aggiunta di uno strato di pooling e classificazione, definito come θ p,c \theta_{\text{p,c}} θ p,c , sopra un modello BERT pre-addestrato θ BERT \theta_{\text{BERT}} θ BERT e successivamente il raffinamento del modello completo { θ p,c , θ BERT } \{\theta_{\text{p,c}}, \theta_{\text{BERT}}\} { θ p,c , θ BERT } possono produrre prestazioni all’avanguardia in una varietà di attività di NLU, cf. BERT di Devlin et al. .
Vediamo BERT in modo visuale.
Il modello BERT è mostrato in grigio. Il modello impila più blocchi BERT, ognuno dei quali è composto da livelli di auto-attenzione bidirezionale (mostrati nella parte inferiore del riquadro rosso) e due livelli di feed-forward (mostrati nella parte superiore del riquadro rosso).
Ogni blocco BERT utilizza l’auto-attenzione bidirezionale per elaborare una sequenza di input x ′ 1 , … , x ′ n \mathbf{x’}_1, \ldots, \mathbf{x’}_n x ′ 1 , … , x ′ n (mostrata in grigio chiaro) in una sequenza di output contestualizzata x ′ ′ 1 , … , x ′ ′ n \mathbf{x”}_1, \ldots, \mathbf{x”}_n x ′ ′ 1 , … , x ′ ′ n (mostrata in grigio leggermente più scuro) 4 {}^4 4 . La sequenza di output contestualizzata dell’ultimo blocco BERT, ovvero X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n , può quindi essere mappata su una singola classe di output c c c aggiungendo un livello di classificazione specifico per il compito (mostrato in arancione) come spiegato sopra.
I modelli solo encoder possono solo mappare una sequenza di input in una sequenza di output di lunghezza di output a priori nota. In conclusione, la dimensione di output non dipende dalla sequenza di input, il che rende svantaggioso e impraticabile utilizzare modelli solo encoder per compiti di sequenza-a-sequenza.
Come per tutti i modelli solo encoder, l’architettura di BERT corrisponde esattamente all’architettura della parte encoder dei modelli encoder-decoder basati su trasformatori, come mostrato nella sezione “Encoder” nel notebook Encoder-Decoder .
GPT2
GPT2 è un modello solo decoder, che utilizza l’auto-attenzione uni-direzionale (cioè “causale”) per definire una mappatura da una sequenza di input Y 0 : m − 1 \mathbf{Y}_{0: m – 1} Y 0 : m − 1 1 {}^1 1 a una sequenza di vettori di logit “prossima parola” L 1 : m \mathbf{L}_{1:m} L 1 : m :
f θ GPT2 : Y 0 : m − 1 → L 1 : m . f_{\theta_{\text{GPT2}}}: \mathbf{Y}_{0: m – 1} \to \mathbf{L}_{1:m}. f θ GPT2 : Y 0 : m − 1 → L 1 : m .
Elaborando i vettori di logit L 1 : m \mathbf{L}_{1:m} L 1 : m con l’operazione softmax, il modello può definire la distribuzione di probabilità della sequenza di parole Y 1 : m \mathbf{Y}_{1:m} Y 1 : m . Per essere precisi, la distribuzione di probabilità della sequenza di parole Y 1 : m \mathbf{Y}_{1:m} Y 1 : m può essere scomposta in m − 1 m-1 m − 1 distribuzioni condizionali di “prossima parola”:
p θ GPT2 ( Y 1 : m ) = ∏ i = 1 m p θ GPT2 ( y i ∣ Y 0 : i − 1 ) . p_{\theta_{\text{GPT2}}}(\mathbf{Y}_{1:m}) = \prod_{i=1}^{m} p_{\theta_{\text{GPT2}}}(\mathbf{y}_i | \mathbf{Y}_{0:i-1}). p θ GPT2 ( Y 1 : m ) = i = 1 ∏ m p θ GPT2 ( y i ∣ Y 0 : i − 1 ) . p θ GPT2 ( y i ∣ Y 0 : i − 1 ) p_{\theta_{\text{GPT2}}}(\mathbf{y}_i | \mathbf{Y}_{0:i-1}) p θ GPT2 ( y i ∣ Y 0 : i − 1 ) presenta la distribuzione di probabilità della prossima parola y i \mathbf{y}_i y i data tutte le parole precedenti y 0 , … , y i − 1 \mathbf{y}_0, \ldots, \mathbf{y}_{i-1} y 0 , … , y i − 1 3 {}^3 3 ed è definita come l’operazione softmax applicata al vettore di logit l i \mathbf{l}_i l i . In sintesi, le seguenti equazioni sono vere.
p θ gpt2 ( y i ∣ Y 0 : i − 1 ) = Softmax ( l i ) = Softmax ( f θ GPT2 ( Y 0 : i − 1 ) ) . p_{\theta_{\text{gpt2}}}(\mathbf{y}_i | \mathbf{Y}_{0:i-1}) = \textbf{Softmax}(\mathbf{l}_i) = \textbf{Softmax}(f_{\theta_{\text{GPT2}}}(\mathbf{Y}_{0: i – 1})). p θ gpt2 ( y i ∣ Y 0 : i − 1 ) = Softmax ( l i ) = Softmax ( f θ GPT2 ( Y 0 : i − 1 ) ) .
Per maggiori dettagli, fare riferimento alla sezione decodificatore dell’articolo del blog encoder-decoder.
Adesso vediamo anche una visualizzazione di GPT2.
Analogo a BERT, GPT2 è composto da una pila di blocchi GPT2. A differenza del blocco BERT, il blocco GPT2 utilizza l’auto-attenzione unidirezionale per elaborare alcuni vettori di input y ′ 0 , … , y ′ m − 1 (mostrati in azzurro chiaro in basso a destra) in una sequenza di vettori di output y ′ ′ 0 , … , y ′ ′ m − 1 (mostrati in azzurro scuro in alto a destra). Oltre alla pila di blocchi GPT2, il modello ha anche uno strato lineare, chiamato LM Head, che mappa i vettori di output dell’ultimo blocco GPT2 ai vettori di logit l 1 , … , l m. Come già accennato, un vettore di logit l i può quindi essere utilizzato per campionare un nuovo vettore di input y i.
GPT2 è principalmente utilizzato per la generazione di testi in domini aperti. Innanzitutto, un prompt di input Y 0 : i − 1 viene alimentato al modello per ottenere la distribuzione condizionale p θ gpt2 ( y ∣ Y 0 : i − 1 ). Quindi la parola successiva y i viene campionata dalla distribuzione (rappresentata dalle frecce grigie nel grafico sopra) e successivamente aggiunta all’input. In modo auto-regressivo, la parola successiva y i + 1 può quindi essere campionata da p θ gpt2 ( y ∣ Y 0 : i ) e così via.
Pertanto, GPT2 è particolarmente adatto per la generazione di linguaggio, ma meno per la generazione condizionale. Impostando il prompt di input Y 0 : i − 1 uguale alla sequenza di input X 1 : n, GPT2 può essere utilizzato molto bene per la generazione condizionale. Tuttavia, l’architettura del modello ha un inconveniente fondamentale rispetto all’architettura encoder-decoder come spiegato in Raffel et al. (2019) a pagina 17. In breve, l’auto-attenzione unidirezionale limita inutilmente la rappresentazione del modello dell’input della sequenza X 1 : n, poiché x i non può dipendere da x i + 1, ∀ i ∈ { 1 , … , n }.
Codificatore-Decodificatore
Perché i modelli solo codificatore richiedono di conoscere a priori la lunghezza dell’output, sembrano inadatti per i compiti di sequenza-a-sequenza. I modelli solo decodificatore possono funzionare bene per i compiti di sequenza-a-sequenza, ma hanno anche certe limitazioni architettoniche come spiegato sopra.
L’approccio attuale predominante per affrontare i compiti di sequenza-a-sequenza sono i modelli codificatore-decodificatore basati su trasformatori – spesso anche chiamati modelli seq2seq a trasformatore. I modelli codificatore-decodificatore sono stati introdotti in Vaswani et al. (2017) e da allora si è dimostrato che si comportano meglio nei compiti di sequenza-a-sequenza rispetto ai modelli di linguaggio autonomi (ossia i modelli solo decodificatore), ad esempio Raffel et al. (2020). In sostanza, un modello codificatore-decodificatore è la combinazione di un codificatore autonomo, come BERT, e un modello decodificatore autonomo, come GPT2. Per ulteriori dettagli sull’architettura esatta dei modelli codificatore-decodificatore basati su trasformatori, si prega di fare riferimento a questo post sul blog.
Ora sappiamo che i checkpoint liberamente disponibili di grandi modelli autonomi pre-addestrati di codificatori e decodificatori, come BERT e GPT, possono migliorare le prestazioni e ridurre i costi di addestramento per molti compiti di NLU. Sappiamo anche che i modelli codificatore-decodificatore sono essenzialmente la combinazione di modelli autonomi di codificatori e decodificatori. Questo naturalmente solleva la questione di come si possano sfruttare i checkpoint dei modelli autonomi per i modelli codificatore-decodificatore e quali combinazioni di modelli siano più performanti per determinati compiti di sequenza-a-sequenza.
Nel 2020, Sascha Rothe, Shashi Narayan e Aliaksei Severyn hanno indagato proprio su questa questione nel loro articolo Sfruttare i checkpoint pre-addestrati per i compiti di generazione di sequenze. L’articolo offre un’ottima analisi delle diverse combinazioni di modelli codificatore-decodificatore e delle tecniche di fine-tuning, che studieremo in maggior dettaglio in seguito.
Comporre un modello codificatore-decodificatore utilizzando checkpoint di modelli autonomi pre-addestrati viene definito come “warm-starting” del modello codificatore-decodificatore. Le sezioni seguenti mostrano come funziona in teoria il “warm-starting” di un modello codificatore-decodificatore, come si può mettere in pratica la teoria con 🤗Transformers e forniscono anche consigli pratici per migliori prestazioni.
1 {}^1 1 Un modello di linguaggio pre-addestrato viene definito come una rete neurale:
- che è stato addestrato su dati di testo non etichettati, ossia in modo task-agnostic e non supervisionato, e
- che elabora una sequenza di parole di input in un embedding dipendente dal contesto. Ad esempio, il modello di sacco di parole continuo e il modello skip-gram di Mikolov et al. (2013) non vengono considerati modelli di linguaggio pre-addestrati perché gli embedding sono agnostici del contesto.
2 {}^2 2 Il fine-tuning viene definito come l’addestramento specifico del compito di un modello che è stato inizializzato con i pesi di un modello di linguaggio pre-addestrato.
3 {}^3 3 Il vettore di input y 0 \mathbf{y}_0 y 0 corrisponde qui al vettore di embedding BOS \text{BOS} BOS richiesto per prevedere la prima parola di output y 1 \mathbf{y}_1 y 1 .
4 {}^4 4 Per non appesantire le equazioni e le illustrazioni, escludiamo i livelli di normalizzazione.
5 {}^5 5 Per ulteriori dettagli su perché si usa l’auto-attenzione unidirezionale per i modelli “solo decodificatore”, come GPT2, e su come funziona esattamente il campionamento, si prega di fare riferimento alla sezione decodificatore del post sul blog del codificatore-decodificatore.
Warm-starting dei modelli codificatore-decodificatore (Teoria)
Dopo aver letto l’introduzione, siamo ora familiari con i modelli solo codificatore e solo decodificatore. Abbiamo notato che l’architettura del modello codificatore-decodificatore è essenzialmente una composizione di un modello di codificatore autonomo e un modello di decodificatore autonomo, il che ci ha portato alla domanda su come si possano avviare i modelli codificatore-decodificatore dai checkpoint dei modelli autonomi.
Ci sono molte possibilità per avviare un modello codificatore-decodificatore. Si può:
- inizializzare sia la parte del codificatore che del decodificatore da un checkpoint di un modello solo codificatore, ad esempio BERT,
- inizializzare la parte del codificatore da un checkpoint di un modello solo codificatore, ad esempio BERT, e la parte del decodificatore da un checkpoint di un modello solo decodificatore, ad esempio GPT2,
- inizializzare solo la parte del codificatore con un checkpoint di un modello solo codificatore, o
- inizializzare solo la parte del decodificatore con un checkpoint di un modello solo decodificatore.
Nel seguente, ci concentreremo sulle possibilità 1 e 2. Le possibilità 3 e 4 sono banali dopo aver compreso le prime due.
Riepilogo del modello Encoder-Decoder
Prima di tutto, facciamo un rapido riepilogo dell’architettura encoder-decoder.
L’encoder (mostrato in verde) è una serie di blocchi di encoder. Ogni blocco di encoder è composto da un layer di self-attention bidirezionale e due layer di feed-forward 1 {}^1 1 . Il decoder (mostrato in arancione) è una serie di blocchi di decoder, seguito da un dense layer chiamato LM Head. Ogni blocco di decoder è composto da un layer di self-attention unidirezionale, un layer di cross-attention e due layer di feed-forward.
L’encoder mappa la sequenza di input X 1 : n \mathbf{X}_{1:n} X 1 : n in una sequenza codificata contestualizzata X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n nello stesso modo in cui fa BERT. Il decoder mappa quindi la sequenza codificata contestualizzata X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n e una sequenza target Y 0 : m − 1 \mathbf{Y}_{0:m-1} Y 0 : m − 1 in vettori di logit L 1 : m \mathbf{L}_{1:m} L 1 : m . Analogamente a GPT2, i logit vengono quindi utilizzati per definire la distribuzione della sequenza target Y 1 : m \mathbf{Y}_{1:m} Y 1 : m condizionata alla sequenza di input X 1 : n \mathbf{X}_{1:n} X 1 : n mediante un’operazione softmax.
Per esprimerlo in termini matematici, prima di tutto, la distribuzione condizionale viene fattorizzata in m − 1 m – 1 m − 1 distribuzioni condizionali della prossima parola y i \mathbf{y}_i y i utilizzando il teorema di Bayes.
p θ enc, dec ( Y 1 : m ∣ X 1 : n ) = p θ dec ( Y 1 : m ∣ X ‾ 1 : n ) = ∏ i = 1 m p θ dec ( y i ∣ Y 0 : i − 1 , X ‾ 1 : n ) , con X ‾ 1 : n = f θ enc ( X 1 : n ) . p_{\theta_{\text{enc, dec}}}(\mathbf{Y}_{1:m} | \mathbf{X}_{1:n}) = p_{\theta_{\text{dec}}}(\mathbf{Y}_{1:m} | \mathbf{\overline{X}}_{1:n}) = \prod_{i=1}^m p_{\theta_{\text{dec}}}(\mathbf{y}_i | \mathbf{Y}_{0: i -1}, \mathbf{\overline{X}}_{1:n}), \text{ con } \mathbf{\overline{X}}_{1:n} = f_{\theta_{\text{enc}}}(\mathbf{X}_{1:n}). p θ enc, dec ( Y 1 : m ∣ X 1 : n ) = p θ dec ( Y 1 : m ∣ X 1 : n ) = i = 1 ∏ m p θ dec ( y i ∣ Y 0 : i − 1 , X 1 : n ) , con X 1 : n = f θ enc ( X 1 : n ) .
Ogni distribuzione condizionale “prossima parola” viene quindi definita dal softmax del vettore di logit come segue.
p θ dec ( y i ∣ Y 0 : i − 1 , X ‾ 1 : n ) = Softmax ( l i ) . p_{\theta_{\text{dec}}}(\mathbf{y}_i | \mathbf{Y}_{0: i -1}, \mathbf{\overline{X}}_{1:n}) = \textbf{Softmax}(\mathbf{l}_i). p θ dec ( y i ∣ Y 0 : i − 1 , X 1 : n ) = Softmax ( l i ) .
Per ulteriori dettagli, consultare il notebook Encoder-Decoder.
Warm-staring Encoder-Decoder con BERT
Ora illustreremo come un modello BERT pre-addestrato può essere utilizzato per avviare l’encoder-decoder in modo rapido. I parametri di peso pre-addestrati di BERT vengono utilizzati per inizializzare sia i parametri di peso dell’encoder che quelli del decoder. Per fare ciò, l’architettura di BERT viene confrontata con l’architettura dell’encoder e tutti i livelli dell’encoder che esistono anche in BERT verranno inizializzati con i parametri di peso pre-addestrati dei rispettivi livelli. Tutti i livelli dell’encoder che non esistono in BERT avranno semplicemente i loro parametri di peso inizializzati casualmente.
Visualizziamo.
Possiamo vedere che l’architettura dell’encoder corrisponde 1 a 1 all’architettura di BERT. I parametri di peso del livello di auto-attenzione bidirezionale e dei due livelli di feed-forward di tutti i blocchi dell’encoder vengono inizializzati con i parametri di peso dei rispettivi blocchi di BERT. Questo è illustrato in modo esemplificativo per il secondo blocco dell’encoder (caselle rosse in basso) i cui parametri di peso θ enc self-attn , 2 \theta_{\text{enc}}^{\text{self-attn}, 2} θ enc self-attn , 2 e θ enc feed-forward , 2 \theta_{\text{enc}}^{\text{feed-forward}, 2} θ enc feed-forward , 2 vengono impostati sui parametri di peso di BERT θ BERT feed-forward , 2 \theta_{\text{BERT}}^{\text{feed-forward}, 2} θ BERT feed-forward , 2 e θ BERT self-attn , 2 \theta_{\text{BERT}}^{\text{self-attn}, 2} θ BERT self-attn , 2 , rispettivamente, durante l’inizializzazione.
Prima del fine-tuning, l’encoder si comporta quindi esattamente come un modello BERT pre-addestrato. Supponendo che la sequenza di input x 1 , … , x n \mathbf{x}_1, \ldots, \mathbf{x}_n x 1 , … , x n (mostrata in verde) passata all’encoder sia uguale alla sequenza di input x 1 BERT , … , x n BERT \mathbf{x}_1^{\text{BERT}}, \ldots, \mathbf{x}_n^{\text{BERT}} x 1 BERT , … , x n BERT (mostrata in grigio) passata a BERT, ciò significa che anche le rispettive sequenze di vettori di output x ‾ 1 , … , x ‾ n \mathbf{\overline{x}}_1, \ldots, \mathbf{\overline{x}}_n x 1 , … , x n (mostrate in verde scuro) e x ‾ 1 BERT , … , x ‾ n BERT \mathbf{\overline{x}}_1^{\text{BERT}}, \ldots, \mathbf{\overline{x}}_n^{\text{BERT}} x 1 BERT , … , x n BERT (mostrate in grigio scuro) devono essere uguali.
Successivamente, illustreremo come viene avviato in modo rapido il decoder.
L’architettura del decoder è diversa dall’architettura di BERT in tre modi.
-
Innanzitutto, il decoder deve essere condizionato sulla sequenza codificata contestualizzata X ‾ 1 : n \mathbf{\overline{X}}_{1:n} X 1 : n tramite livelli di cross-attenzione. Di conseguenza, vengono aggiunti livelli di cross-attenzione inizializzati casualmente tra il livello di auto-attenzione e i due livelli di feed-forward in ogni blocco di BERT. Questo viene rappresentato in modo esemplificativo per il secondo blocco da + θ dec cross-attention, 2 +\theta_{\text{dec}}^{\text{cross-attention, 2}} + θ dec cross-attention, 2 e illustrato dal grafo completamente connesso appena aggiunto in rosso nella casella rossa inferiore a destra. Questo cambia necessariamente il comportamento di ogni blocco BERT modificato in modo che un vettore di input, ad esempio y ′ 0 \mathbf{y’}_0 y ′ 0 ora produca un vettore di output casuale y ′ ′ 0 \mathbf{y”}_0 y ′ ′ 0 (evidenziato dal bordo rosso intorno al vettore di output y ′ ′ 0 \mathbf{y”}_0 y ′ ′ 0 ).
-
In secondo luogo, i livelli di auto-attenzione bidirezionale di BERT devono essere trasformati in livelli di auto-attenzione unidirezionale per conformarsi alla generazione auto-regressiva. Poiché sia il livello di auto-attenzione bidirezionale che quello unidirezionale si basano sugli stessi pesi di proiezione di chiave, query e valore, i pesi del livello di auto-attenzione del decoder possono essere inizializzati con i pesi del livello di auto-attenzione di BERT. Ad esempio, i parametri di peso delle chiavi, delle query e dei valori del livello di auto-attenzione unidirezionale del decoder vengono inizializzati con quelli del livello di auto-attenzione bidirezionale di BERT θ BERT self-attn , 2 = { W BERT , k self-attn , 2 , W BERT , v self-attn , 2 , W BERT , q self-attn , 2 } → θ dec self-attn , 2 = { W dec , k self-attn , 2 , W dec , v self-attn , 2 , W dec , q self-attn , 2 } . \theta_{\text{BERT}}^{\text{self-attn}, 2} = \{\mathbf{W}_{\text{BERT}, k}^{\text{self-attn}, 2}, \mathbf{W}_{\text{BERT}, v}^{\text{self-attn}, 2}, \mathbf{W}_{\text{BERT}, q}^{\text{self-attn}, 2} \} \to \theta_{\text{dec}}^{\text{self-attn}, 2} = \{\mathbf{W}_{\text{dec}, k}^{\text{self-attn}, 2}, \mathbf{W}_{\text{dec}, v}^{\text{self-attn}, 2}, \mathbf{W}_{\text{dec}, q}^{\text{self-attn}, 2} \}. θ BERT self-attn , 2 = { W BERT , k self-attn , 2 , W BERT , v self-attn , 2 , W BERT , q self-attn , 2 } → θ dec self-attn , 2 = { W dec , k self-attn , 2 , W dec , v self-attn , 2 , W dec , q self-attn , 2 } . Tuttavia, nell’attenzione unidirezionale ogni token si riferisce solo a tutti i token precedenti, quindi i livelli di auto-attenzione del decoder restituiscono vettori di output diversi rispetto ai livelli di auto-attenzione di BERT anche se condividono gli stessi pesi. Confronta ad esempio il grafo connesso causalmente del decoder nella casella a destra con il grafo completamente connesso di BERT nella casella a sinistra.
-
In terzo luogo, il decoder restituisce una sequenza di vettori di logit L 1 : m \mathbf{L}_{1:m} L 1 : m al fine di definire la distribuzione di probabilità condizionale p θ dec ( Y 1 : n ∣ X ‾ ) p_{\theta_{\text{dec}}}(\mathbf{Y}_{1:n} | \mathbf{\overline{X}})
Per concludere, quando si avvia il decodificatore da un modello BERT pre-addestrato, solo i pesi del livello di attenzione incrociata vengono inizializzati in modo casuale. Tutti gli altri pesi, compresi quelli del livello di auto-attenzione e dell’LM Head, vengono inizializzati con i parametri di peso pre-addestrati di BERT.
Dopo aver avviato il modello codificatore-decodificatore, i pesi vengono quindi raffinati in un compito downstream di sequenza in sequenza, come ad esempio la sintesi.
Avvio del codificatore-decodificatore con BERT e GPT2
Invece di avviare sia il codificatore che il decodificatore con un checkpoint di BERT, è possibile sfruttare il checkpoint di BERT per il codificatore e un checkpoint di GPT2 per il decodificatore. A prima vista, sembra che un checkpoint di solo decodificatore GPT2 sia più adatto per avviare il decodificatore perché è già stato addestrato sulla modellazione del linguaggio causale e utilizza livelli di auto-attenzione unidirezionali.
Vediamo come un checkpoint di GPT2 può essere utilizzato per avviare il decodificatore.
Possiamo vedere che il decodificatore è più simile a GPT2 che a BERT. I parametri di peso dell’LM Head del decodificatore possono essere inizializzati direttamente con i parametri di peso dell’LM Head di GPT2, ad esempio θ GPT2 lm-head → θ dec lm-head.
Inoltre, i blocchi del decodificatore e di GPT2 utilizzano entrambi l’auto-attenzione unidirezionale in modo che i vettori di output del livello di auto-attenzione del decodificatore siano equivalenti ai vettori di output di GPT2 assumendo che i vettori di input siano gli stessi, ad esempio y ′ 0 GPT2 = y ′ 0.
A differenza del decodificatore inizializzato da BERT, il decodificatore inizializzato da GPT2 mantiene il grafo connesso causale del livello di auto-attenzione, come si può vedere nelle caselle rosse in basso.
Tuttavia, il decodificatore inizializzato da GPT2 deve anche condizionare il decodificatore su X ‾ 1 : n. Analogamente al decodificatore inizializzato da BERT, i pesi inizializzati in modo casuale per il livello di attenzione incrociata vengono quindi aggiunti a ogni blocco del decodificatore. Questo è illustrato ad esempio per il secondo blocco del codificatore con + θ dec cross-attention, 2.
Anche se GPT2 assomiglia alla parte del decodificatore di un modello codificatore-decodificatore più di BERT, un decodificatore inizializzato da GPT2 produrrà anche vettori di logit casuali L 1 : m senza raffinamento a causa dei livelli di attenzione incrociata inizializzati in modo casuale in ogni blocco del decodificatore. Sarebbe interessante indagare se un decodificatore inizializzato da GPT2 produce risultati migliori o può essere raffinato in modo più efficiente.
Condivisione dei pesi del codificatore-decodificatore
In Raffel et al. (2020), gli autori mostrano che un modello codificatore-decodificatore inizializzato in modo casuale che condivide i pesi del codificatore con il decodificatore, e quindi riduce la memoria necessaria della metà, si comporta solo leggermente peggio rispetto alla sua versione “non condivisa”. La condivisione dei pesi del codificatore con il decodificatore significa che tutti i livelli del decodificatore che si trovano nella stessa posizione del codificatore condividono gli stessi parametri di peso, cioè lo stesso nodo nel grafo di calcolo della rete. Ad esempio, le matrici di proiezione query, chiave e valore del livello di auto-attenzione nel terzo blocco del codificatore, definite come W Enc, k self-attn, 3, W Enc, v self-attn, 3, W Enc, q self-attn, 3, sono identiche alle rispettive matrici di proiezione query, chiave e valore del livello di auto-attenzione nel terzo blocco del decodificatore 2.
W k self-attn, 3 = W enc, k self-attn, 3 ≡ W dec, k self-attn, 3,
\mathbf{W}^{\text{self-attn}, 3}_{k} = \mathbf{W}^{\text{self-attn}, 3}_{\text{enc}, k} \equiv \mathbf{W}^{\text{self-attn}, 3}_{\text{dec}, k},
W q self-attn, 3 = W enc, q self-attn, 3 ≡ W dec, q self-attn, 3,
\mathbf{W}^{\text{self-attn}, 3}_{q} = \mathbf{W}^{\text{self-attn}, 3}_{\text{enc}, q} \equiv \mathbf{W}^{\text{self-attn}, 3}_{\text{dec}, q},
W v self-attn, 3 = W enc, v self-attn, 3 ≡ W dec, v self-attn, 3,
\mathbf{W}^{\text{self-attn}, 3}_{v} = \mathbf{W}^{\text{self-attn}, 3}_{\text{enc}, v} \equiv \mathbf{W}^{\text{self-attn}, 3}_{\text{dec}, v},Come risultato, i pesi di proiezione chiave W k self-attn, 3, W v self-attn, 3, W q self-attn, 3 vengono aggiornati due volte per ogni passaggio di retropropagazione: una volta quando il gradiente viene retropropagato attraverso il terzo blocco decodificatore e una volta quando il gradiente viene retropropagato attraverso il terzo blocco codificatore.
Allo stesso modo, possiamo avviare in modo rapido un modello codificatore-decodificatore condividendo i pesi del codificatore con il decodificatore. La possibilità di condividere i pesi tra il codificatore e il decodificatore richiede che l’architettura del decodificatore (escludendo i pesi di cross-attenzione) sia identica all’architettura del codificatore. Pertanto, la condivisione dei pesi del codificatore-decodificatore è rilevante solo se il modello codificatore-decodificatore viene avviato in modo rapido da un singolo checkpoint pre-allenato solo per il codificatore.
Fantastico! Questa era la teoria sull’avvio iniziale dei modelli codificatore-decodificatore. Ora diamo un’occhiata ai risultati.
1 {}^1 1 Senza perdita di generalità, escludiamo i livelli di normalizzazione per non appesantire le equazioni e le illustrazioni. 2 {}^2 2 Per maggiori dettagli su come funzionano i livelli di auto-attenzione, si prega di fare riferimento a questa sezione del post del blog sul modello codificatore-decodificatore basato su trasformatori per la parte del codificatore (e questa sezione per la parte del decodificatore).
Avvio iniziale dei modelli codificatore-decodificatore (Analisi)
In questa sezione, riassumeremo le scoperte sull’avvio iniziale dei modelli codificatore-decodificatore come presentato in “Leveraging Pre-trained Checkpoints for Sequence Generation Tasks” di Sascha Rothe, Shashi Narayan e Aliaksei Severyn. Gli autori hanno confrontato le prestazioni dei modelli codificatore-decodificatore avviati inizialmente con modelli codificatore-decodificatore inizializzati casualmente su diverse attività di generazione di sequenze, in particolare riassunto, traduzione, divisione delle frasi e fusione delle frasi.
Per essere più precisi, sono stati utilizzati checkpoint pre-allenati pubblicamente disponibili di BERT, RoBERTa e GPT2 in diverse varianti per avviare in modo rapido un modello codificatore-decodificatore. Ad esempio, un codificatore inizializzato con BERT è stato accoppiato con un decodificatore inizializzato con BERT per ottenere un modello BERT2BERT o un codificatore inizializzato con RoBERTa è stato accoppiato con un decodificatore inizializzato con GPT2 per ottenere un modello RoBERTa2GPT2. Inoltre, è stato investigato l’effetto della condivisione dei pesi del codificatore e del decodificatore (come spiegato nella sezione precedente) per RoBERTa, cioè RoBERTaShare, e per BERT, cioè BERTShare. Modelli codificatore-decodificatore inizializzati casualmente o parzialmente casualmente sono stati utilizzati come punto di riferimento, come ad esempio un modello codificatore-decodificatore completamente inizializzato casualmente, chiamato Rnd2Rnd, o un decodificatore inizializzato con BERT accoppiato con un codificatore inizializzato casualmente, definito come Rnd2BERT.
La seguente tabella mostra un elenco completo di tutte le varianti di modelli investigati, compreso il numero di pesi inizializzati casualmente, cioè “random”, e il numero di pesi inizializzati dai rispettivi checkpoint pre-addestrati, cioè “leveraged”. Tutti i modelli si basano su un’architettura a 12 strati con embedding di dimensione nascosta di 768-dim, corrispondente ai checkpoint
bert-base-cased
,bert-base-uncased
,roberta-base
egpt2
nel model hub di 🤗Transformers.Il modello Rnd2Rnd, che si basa sull’architettura BERT2BERT, contiene 221M di parametri di peso – tutti inizializzati casualmente. Gli altri due baselines “BERT-based” Rnd2BERT e BERT2Rnd hanno circa la metà dei loro pesi, cioè 112M di parametri, inizializzati casualmente. Gli altri 109M di parametri di peso sono leveraged dal checkpoint pre-addestrato
bert-base-uncased
per la parte encoder o decoder rispettivamente. I modelli BERT2BERT, BERT2GPT2 e RoBERTa2GPT2 hanno tutti i loro parametri di peso dell’encoder leveraged (dabert-base-uncased
eroberta-base
rispettivamente) e la maggior parte dei pesi dei parametri del decoder anche (dagpt2
ebert-base-uncased
rispettivamente). 26M di parametri di peso del decoder, corrispondenti ai 12 strati di cross-attention, sono inizializzati casualmente. RoBERTa2GPT2 e BERT2GPT2 vengono confrontati con il baseline Rnd2GPT2. Inoltre, va notato che le varianti di modello condiviso BERTShare e RoBERTaShare hanno significativamente meno parametri perché tutti i parametri di peso dell’encoder sono condivisi con i rispettivi parametri di peso del decoder.Esperimenti
I modelli sopra citati sono stati allenati e valutati su quattro compiti di sequenza a sequenza di complessità crescente: fusione a livello di frase, suddivisione a livello di frase, traduzione e riassunto astratto. La seguente tabella mostra quali dataset sono stati utilizzati per ciascun compito.
A seconda del compito, è stato utilizzato un regime di allenamento leggermente diverso. Ad esempio, in base alla dimensione del dataset e al compito specifico, il numero di passi di allenamento varia da 200K a 500K, la dimensione del batch è impostata su 128 o 256, la lunghezza di input varia da 128 a 512 e la lunghezza di output varia da 32 a 128. Tuttavia, è importante sottolineare che all’interno di ogni compito, tutti i modelli sono stati allenati e valutati utilizzando gli stessi iperparametri per garantire un confronto equo. Per ulteriori informazioni sulle impostazioni iperparametriche specifiche per il compito, si consiglia al lettore di consultare la sezione Esperimenti nel paper.
Ora forniremo una panoramica riassunta dei risultati per ciascun compito.
Fusione e suddivisione di frasi (DiscoFuse, WikiSplit)
Fusione di frasi è il compito di combinare più frasi in una singola frase coerente. Ad esempio, le due frasi:
Come bloccante di corsa, Zeitler si muove relativamente bene. Zeitler troppo spesso ha difficoltà nel punto di contatto nello spazio.
dovrebbero essere collegate da una parola di collegamento adeguata, come:
Come bloccante di corsa, Zeitler si muove relativamente bene. Tuttavia, egli troppo spesso ha difficoltà nel punto di contatto nello spazio.
Come si può vedere, la parola di collegamento “tuttavia” fornisce una transizione coerente dalla prima frase alla seconda. Un modello in grado di generare una tale parola di collegamento ha probabilmente imparato a inferire che le due frasi sopra si contrastano tra loro.
Il compito inverso è chiamato Suddivisione di frasi e consiste nel dividere una singola frase complessa in più frasi più semplici che mantengono insieme lo stesso significato. La suddivisione delle frasi è considerata un compito importante nella semplificazione del testo, cf. a Botha et al. (2018).
Come esempio, la frase:
Street Rod è il primo di una serie di due giochi pubblicati per PC e Commodore 64 nel 1989
può essere semplificata in
Street Rod è il primo di una serie di due giochi . È stato pubblicato per PC e Commodore 64 nel 1989
Si può vedere che la lunga frase cerca di trasmettere due importanti informazioni. Una è che il gioco era il primo di due giochi pubblicati per PC, e la seconda è l’anno in cui è stato pubblicato. La suddivisione delle frasi, quindi, richiede al modello di capire quale parte della frase dovrebbe essere divisa in due frasi, rendendo il compito più difficile rispetto alla fusione delle frasi.
Una metrica comune per valutare le prestazioni dei modelli nei compiti di fusione e divisione delle frasi è SARI (Wu et al. (2016)), che si basa ampiamente sul punteggio F1 dell’etichetta e dell’output del modello.
Vediamo come i modelli si comportano nella fusione e divisione delle frasi.
Le prime due colonne mostrano le prestazioni dei modelli di codificatore-decodificatore sui dati di valutazione di DiscoFuse. La prima colonna indica i risultati dei modelli di codificatore-decodificatore addestrati su tutti (100%) i dati di addestramento, mentre la seconda colonna mostra i risultati dei modelli addestrati solo sul 10% dei dati di addestramento. Osserviamo che i modelli avviati con “warm-start” hanno prestazioni significativamente migliori rispetto ai modelli di base inizializzati casualmente Rnd2Rnd, Rnd2Bert e Rnd2GPT2. Un modello RoBERTa2GPT2 avviato con “warm-start” addestrato solo sul 10% dei dati di addestramento è paragonabile a un modello Rnd2Rnd addestrato sul 100% dei dati di addestramento. Interessante notare che il modello di base Bert2Rnd ha prestazioni pari a un modello Bert2Bert completamente avviato con “warm-start”, il che indica che avviare con “warm-start” la parte di codificatore è più efficace rispetto all’avviare con “warm-start” la parte di decodificatore. I migliori risultati sono ottenuti da RoBERTa2GPT2, seguito da RobertaShare. La condivisione dei parametri di peso del codificatore e del decodificatore sembra aumentare leggermente le prestazioni del modello.
Nel compito più difficile di divisione delle frasi, emerge un andamento simile. I modelli di codificatore-decodificatore avviati con “warm-start” superano significativamente i modelli di codificatore-decodificatore il cui codificatore è inizializzato casualmente e i modelli di codificatore-decodificatore con parametri di peso condivisi producono risultati migliori rispetto a quelli con parametri di peso disaccoppiati. Nella divisione delle frasi, i modelli BertShare offrono le migliori prestazioni, seguiti da RobertaShare.
Oltre alle varianti di modelli a 12 livelli, gli autori hanno anche addestrato e valutato un modello RobertaShare a 24 livelli (large) che supera significativamente tutti i modelli a 12 livelli.
Traduzione automatica (WMT14)
Successivamente, gli autori hanno valutato modelli di codificatore-decodificatore avviati con “warm-start” sul dataset WMT14 En → \to → De e De → \to → En, che è probabilmente il benchmark più comune nella traduzione automatica (MT). In questo notebook, presentiamo i risultati sul dataset di valutazione newstest2014. Poiché il benchmark richiede al modello di comprendere sia un vocabolario inglese che tedesco, i modelli di codificatore-decodificatore inizializzati con BERT sono stati avviati con “warm-start” dal checkpoint pre-addestrato multilingue
bert-base-multilingual-cased
. Poiché non esiste un checkpoint multilingue di RoBERTa disponibile pubblicamente, i modelli di codificatore-decodificatore inizializzati con RoBERTa sono stati esclusi per la traduzione automatica. I modelli inizializzati con GPT2 sono stati inizializzati dal checkpoint pre-addestratogpt2
come nell’esperimento precedente. I risultati della traduzione vengono riportati utilizzando la metrica di punteggio BLUE-4 1 {}^1 1.Di nuovo, osserviamo un significativo miglioramento delle prestazioni avviando con “warm-start” la parte di codificatore, con BERT2Rnd e BERT2BERT che offrono i migliori risultati sia per le task En → \to → De che De → \to → En. I modelli inizializzati con GPT2 hanno prestazioni significativamente inferiori persino rispetto al modello di base Rnd2Rnd su En → \to → De. Tenendo conto che il checkpoint
gpt2
è stato addestrato solo su testo in inglese, non è sorprendente che i modelli BERT2GPT2 e Rnd2GPT2 abbiano difficoltà a generare traduzioni in tedesco. Questa ipotesi è supportata dai risultati competitivi (ad es. 31.4 vs 32.7) di BERT2GPT2 nella task De → \to → En per cui il vocabolario di GPT2 si adatta al formato di output in inglese. Contrariamente ai risultati ottenuti nella fusione e divisione delle frasi, la condivisione dei parametri di peso del codificatore e del decodificatore non produce un aumento delle prestazioni nella traduzione automatica. Le possibili ragioni per questo, come indicato dagli autori, includono:- la capacità del modello di codificatore-decodificatore è un fattore importante nella traduzione automatica, e
- il codificatore e il decodificatore devono affrontare una grammatica e un vocabolario diversi.
Poiché il checkpoint
bert-base-multilingual-cased
è stato addestrato su più di 100 lingue, il suo vocabolario è probabilmente eccessivamente ampio per la traduzione automatica En → \to → De e De → \to → En. Pertanto, gli autori hanno pre-addestrato un checkpoint di BERT solo per il codificatore su un sottoinsieme degli articoli di Wikipedia in inglese e tedesco e successivamente lo hanno utilizzato per avviare con “warm-start” un modello di codificatore-decodificatore BERT2Rnd e BERTShare. Grazie al miglioramento del vocabolario, si osserva un altro significativo incremento delle prestazioni, con BERT2Rnd (large, custom) che supera significativamente tutti gli altri modelli.Sommario (CNN/Dailymail, BBC XSum, Gigaword)
Infine, i modelli encoder-decoder sono stati valutati sulla sequenza di task sequenza-sequenza più impegnativa – la sintesi. Gli autori hanno scelto tre set di dati di sintesi con diverse caratteristiche per la valutazione: Gigaword (generazione di titoli), BBC XSum (sintesi estrema) e CNN/Dailymail (sintesi astratta).
Il set di dati Gigaword contiene sintesi astratte a livello di frase, che richiedono al modello di apprendere la comprensione a livello di frase, l’astrazione e, infine, la parafrasatura. Un tipico campione di dati in Gigaword, come
“*il presidente venezuelano Hugo Chávez ha detto giovedì di aver ordinato un’indagine su un presunto complotto golpista che coinvolgerebbe ufficiali militari attivi e in pensione.*”,
avrebbe un titolo corrispondente come etichetta, ad esempio:
“Chávez ordina l’indagine su un presunto complotto golpista”.
Il set di dati BBC XSum è composto da input di testo simili a articoli molto più lunghi, con le etichette che sono per lo più sintesi di una singola frase. Questo set di dati richiede al modello non solo di apprendere l’inferenza a livello di documento, ma anche un alto livello di parafrasatura astratta. Alcuni esempi di dati dei set di dati BBC XSUM sono mostrati qui.
Per il set di dati CNN/Dailymail, i documenti, che hanno una lunghezza simile a quelli del set di dati BBC XSum, devono essere sintetizzati in punti salienti della storia in forma di elenco. Le etichette quindi sono spesso composte da più frasi. Oltre alla comprensione a livello di documento, il set di dati CNN/Dailymail richiede che i modelli siano bravi a copiare le informazioni più salienti. Alcuni esempi possono essere visualizzati qui.
I modelli sono valutati utilizzando la metrica Rouge, mentre i punteggi Rouge-2 sono mostrati di seguito.
Ok, diamo un’occhiata ai risultati.
Osserviamo ancora una volta che l’avvio a caldo della parte dell’encoder dà un miglioramento significativo rispetto ai modelli con encoder inizializzati casualmente, il che è particolarmente evidente per i compiti di astrazione a livello di documento, cioè CNN/Dailymail e BBC XSum. Ciò dimostra che i compiti che richiedono un alto livello di astrazione beneficiano maggiormente di una parte dell’encoder pre-addestrata rispetto a quelli che richiedono solo astrazione a livello di frase. Ad eccezione di Gigaword, i modelli encoder-decoder basati su GPT2 sembrano non essere adatti per la sintesi.
Inoltre, i modelli condivisi encoder-decoder sono i modelli che ottengono le migliori prestazioni per la sintesi. RoBERTaShare e BERTShare sono i modelli che ottengono le migliori prestazioni su tutti i set di dati, mentre il margine è particolarmente significativo sul set di dati BBC XSum, su cui RoBERTaShare (large) supera BERT2BERT e BERT2Rnd di circa 3 punti Rouge-2 e Rnd2Rnd di oltre 8 punti Rouge-2. Come indicato dagli autori, “questo è probabilmente perché le frasi di sintesi della BBC seguono una distribuzione simile a quella delle frasi nel documento, mentre questo non è necessariamente il caso dei titoli di Gigaword e dei punti salienti di CNN/DailyMail”. Intuitivamente, ciò significa che, in BBC XSum, le frasi di input elaborate dall’encoder sono molto simili nella struttura alla sintesi di una singola frase elaborata dal decoder, cioè stessa lunghezza, scelta simile di parole, sintassi simile.
Conclusione
Ok, traiamo una conclusione e cerchiamo di ottenere alcuni consigli pratici.
-
Abbiamo osservato su tutti i compiti che l’avvio a caldo della parte dell’encoder dà un notevole impulso alle prestazioni rispetto ai modelli encoder-decoder con un encoder inizializzato casualmente. D’altra parte, sembra che l’avvio a caldo del decoder sia meno importante, con BERT2BERT che è allo stesso livello di BERT2Rnd nella maggior parte dei compiti. Una ragione intuitiva potrebbe essere che poiché una parte dell’encoder inizializzata da BERT o RoBERTa non ha nessuno dei suoi parametri di peso inizializzati casualmente, l’encoder può sfruttare appieno le conoscenze acquisite dai checkpoint pre-addestrati di BERT o RoBERTa, rispettivamente. Al contrario, il decoder avviato a caldo ha sempre alcune delle sue parti di parametri di peso inizializzate casualmente, il che potrebbe rendere molto più difficile sfruttare in modo efficace le conoscenze acquisite dal checkpoint utilizzato per inizializzare il decoder.
-
Inoltre, abbiamo notato che spesso è vantaggioso condividere i pesi dell’encoder e del decoder, soprattutto se la distribuzione di destinazione è simile alla distribuzione di input (ad esempio, BBC XSum). Tuttavia, per i set di dati il cui distribuzione dei dati di destinazione differisce in modo più significativo dalla distribuzione dei dati di input e per i quali si sa che la capacità del modello è importante, ad esempio WMT14, la condivisione dei pesi dell’encoder-decoder sembra essere svantaggiosa.
-
Infine, abbiamo visto che è molto importante che il vocabolario dei checkpoint pre-addestrati “stand-alone” corrisponda al vocabolario richiesto per risolvere il compito di sequenza-sequenza. Ad esempio, un encoder-decoder BERT2GPT2 avviato a caldo avrà prestazioni scadenti in En → De MT perché GPT2 è stato pre-addestrato in inglese mentre la lingua di destinazione è il tedesco. La scarsa performance complessiva di BERT2GPT2, Rnd2GPT2 e RoBERTa2GPT2 rispetto a BERT2BERT, BERTShared e RoBERTaShared suggerisce che è più efficace avere un vocabolario condiviso. Inoltre, mostra che l’inizializzazione della parte del decoder con un checkpoint GPT2 pre-addestrato non è più efficace dell’inizializzazione con un checkpoint BERT pre-addestrato, nonostante GPT2 sia più simile al decoder nella sua architettura.
Per ciascuno dei compiti sopra elencati, i modelli più performanti sono stati portati su 🤗Transformers e possono essere acceduti qui:
- RoBERTaShared (large) – Wikisplit : google/roberta2roberta_L-24_wikisplit .
- RoBERTaShared (large) – Discofuse : google/roberta2roberta_L-24_discofuse .
- BERT2BERT (large) – WMT en → \to → de : google/bert2bert_L-24_wmt_en_de .
- BERT2BERT (large) – WMT de → \to → en : google/bert2bert_L-24_wmt_de_en .
- RoBERTaShared (large) – CNN/Dailymail : google/roberta2roberta_L-24_cnn_daily_mail .
- RoBERTaShared (large) – BBC XSum : google/roberta2roberta_L-24_bbc .
- RoBERTaShared (large) – Gigaword : google/roberta2roberta_L-24_gigaword .
1 {}^1 1 Per ottenere i punteggi BLEU-4, è stato utilizzato uno script dall’implementazione ufficiale di Tensorflow Transformer https://github.com/tensorflow/models/tree master/official/nlp/transformer. Si noti che, a differenza dello script tensor2tensor/utils/
get_ende_bleu.sh
utilizzato da Vaswani et al. (2017), questo script non divide i composti nominali, ma le virgolette utf-8 sono state normalizzate in virgolette ascii dopo aver notato che l’insieme di addestramento preelaborato contiene solo virgolette ascii.2 {}^2 2 La capacità del modello è una definizione informale di quanto il modello sia bravo a modellare pattern complessi. A volte viene definita anche come la capacità di un modello di apprendere sempre più dati . La capacità del modello è ampiamente misurata dal numero di parametri addestrabili – più parametri, maggiore è la capacità del modello.
Abbiamo spiegato la teoria dei modelli encoder-decoder di avvio rapido, analizzato i risultati empirici su più set di dati e ottenuto conclusioni pratiche. Ora esamineremo un esempio di codice completo che illustra come un modello BERT2BERT può essere avviato rapidamente e di conseguenza aggiustato sul compito di sintesi delle notizie di CNN/Dailymail. Useremo le librerie 🤗datasets e 🤗Transformers.
Inoltre, l’elenco seguente fornisce una versione condensata di questo e altri notebook sull’avvio rapido di altre combinazioni di modelli encoder-decoder.
- per BERT2BERT su CNN/Dailymail (una versione condensata di questo notebook), clicca qui .
- per RoBERTaShare su BBC XSum , clicca qui .
- per BERT2Rnd su WMT14 En → \to → De , clicca qui .
- per RoBERTa2GPT2 su DiscoFuse , clicca qui .
Nota : Questo notebook utilizza solo alcuni esempi di dati di addestramento, di convalida e di test a scopo dimostrativo. Per aggiustare un modello encoder-decoder sui dati di addestramento completi, l’utente dovrebbe modificare di conseguenza i parametri di addestramento e di preelaborazione dei dati come evidenziato dai commenti.
Preelaborazione dei dati
In questa sezione, mostriamo come i dati possono essere preelaborati per l’addestramento. Inoltre, cerchiamo di fornire al lettore una panoramica del processo di decisione su come preelaborare i dati.
Saranno necessarie le librerie datasets e
I dati di input sembrano consistere in brevi articoli di notizie. Interessante, le etichette sembrano essere riassunti simili a elenchi puntati. A questo punto, probabilmente conviene dare un’occhiata ad un paio di altri esempi per avere una migliore comprensione dei dati.
Bisogna anche notare che il testo è sensibile alle maiuscole e minuscole. Questo significa che dobbiamo fare attenzione se vogliamo utilizzare modelli non sensibili alle maiuscole e minuscole. Poiché CNN/Dailymail è un set di dati di riassunto, il modello verrà valutato utilizzando la metrica ROUGE. Controllando la descrizione di ROUGE in 🤗datasets, cf. qui, possiamo vedere che la metrica è insensibile alle maiuscole e minuscole, il che significa che le lettere maiuscole saranno normalizzate in lettere minuscole durante la valutazione. Pertanto, possiamo utilizzare in modo sicuro checkpoint senza maiuscole come
bert-base-uncased
.Geniale! Ora, cerchiamo di capire la lunghezza dei dati di input e delle etichette.
Poiché i modelli calcolano la lunghezza in termini di token, utilizzeremo il tokenizer
bert-base-uncased
per calcolare la lunghezza dell’articolo e del riassunto.Prima di tutto, carichiamo il tokenizer.
from transformers import BertTokenizerFast tokenizer = BertTokenizerFast.from_pretrained("bert-base-uncased")
In seguito, utilizziamo
.map()
per calcolare la lunghezza dell’articolo e del suo riassunto. Poiché sappiamo che la lunghezza massima chebert-base-uncased
può elaborare è di 512, siamo anche interessati alla percentuale di campioni di input che superano la lunghezza massima. Allo stesso modo, calcoliamo la percentuale di riassunti che superano rispettivamente 64 e 128.Possiamo definire la funzione
.map()
come segue.# mappa la lunghezza dell'articolo e del riassunto in un dizionario e se il campione è più lungo di 512 token def map_to_length(x): x["article_len"] = len(tokenizer(x["article"]).input_ids) x["article_longer_512"] = int(x["article_len"] > 512) x["summary_len"] = len(tokenizer(x["highlights"]).input_ids) x["summary_longer_64"] = int(x["summary_len"] > 64) x["summary_longer_128"] = int(x["summary_len"] > 128) return x
Dovrebbe essere sufficiente guardare ai primi 10000 campioni. Possiamo velocizzare la mappatura utilizzando processi multipli con
num_proc=4
.sample_size = 10000 data_stats = train_data.select(range(sample_size)).map(map_to_length, num_proc=4)
Dopo aver calcolato la lunghezza per i primi 10000 campioni, dovremmo ora fare la media. A tal fine, possiamo utilizzare la funzione
.map()
conbatched=True
ebatch_size=-1
per avere accesso a tutti i 10000 campioni all’interno della funzione.map()
.def compute_and_print_stats(x): if len(x["article_len"]) == sample_size: print( "Media dell'articolo: {}, %-Articoli > 512:{}, Media del riassunto:{}, %-Riassunti > 64:{}, %-Riassunti > 128:{}".format( sum(x["article_len"]) / sample_size, sum(x["article_longer_512"]) / sample_size, sum(x["summary_len"]) / sample_size, sum(x["summary_longer_64"]) / sample_size, sum(x["summary_longer_128"]) / sample_size, ) ) output = data_stats.map( compute_and_print_stats, batched=True, batch_size=-1, ) OUTPUT: ------- Media dell'articolo: 847.6216, %-Articoli > 512:0.7355, Media del riassunto:57.7742, %-Riassunti > 64:0.3185, %-Riassunti > 128:0.0
Possiamo vedere che in media un articolo contiene 848 token con circa 3/4 degli articoli che superano i
max_length
di 512 del modello. Il riassunto ha in media una lunghezza di 57 token. Più del 30% dei nostri riassunti su 10000 campioni superano i 64 token, ma nessuno supera i 128 token.bert-base-cased
è limitato a 512 token, il che significa che dovremmo tagliare informazioni possibilmente importanti dall’articolo. Poiché la maggior parte delle informazioni importanti si trova spesso all’inizio degli articoli e perché vogliamo essere efficienti dal punto di vista computazionale, decidiamo di restare subert-base-cased
con unamax_length
di 512 in questo notebook. Questa scelta non è ottimale ma ha dimostrato di dare buoni risultati su CNN/Dailymail. In alternativa, si potrebbe utilizzare modelli di sequenze a lungo raggio, come Longformer, da utilizzare come codificatore.Riguardo alla lunghezza del sommario, possiamo vedere che una lunghezza di 128 include già tutte le etichette del sommario. 128 rientra facilmente nei limiti di
bert-base-cased
, quindi decidiamo di limitare la generazione a 128.Di nuovo, useremo la funzione
.map()
– questa volta per trasformare ogni batch di addestramento in un batch di input del modello."article"
e"highlights"
vengono tokenizzati e preparati come"input_ids"
dell’Encoder e"decoder_input_ids"
del Decoder rispettivamente."labels"
viene automaticamente spostato a sinistra per l’addestramento di modelli di linguaggio.Infine, è molto importante ricordare di ignorare la perdita delle etichette di riempimento. In 🤗Transformers è possibile farlo impostando l’etichetta a -100. Grande, scriviamo quindi la nostra funzione di mappatura.
encoder_max_length=512 decoder_max_length=128 def process_data_to_model_inputs(batch): # tokenizza gli input e le etichette inputs = tokenizer(batch["article"], padding="max_length", truncation=True, max_length=encoder_max_length) outputs = tokenizer(batch["highlights"], padding="max_length", truncation=True, max_length=decoder_max_length) batch["input_ids"] = inputs.input_ids batch["attention_mask"] = inputs.attention_mask batch["labels"] = outputs.input_ids.copy() # poiché BERT sposta automaticamente le etichette, le etichette corrispondono esattamente a `decoder_input_ids`. # Dobbiamo assicurarci che il token PAD venga ignorato batch["labels"] = [[-100 if token == tokenizer.pad_token_id else token for token in labels] for labels in batch["labels"]] return batch
In questo notebook, addestriamo e valutiamo il modello solo su alcuni esempi di addestramento per dimostrazione e impostiamo la
batch_size
a 4 per evitare problemi di memoria esaurita.La riga seguente riduce i dati di addestramento solo ai primi
32
esempi. La cella può essere commentata o non eseguita per un addestramento completo. Sono stati ottenuti buoni risultati con unabatch_size
di 16.train_data = train_data.select(range(32))
Ottimo, prepariamo i dati di addestramento.
# batch_size = 16 batch_size=4 train_data = train_data.map( process_data_to_model_inputs, batched=True, batch_size=batch_size, remove_columns=["article", "highlights", "id"] )
Dando un’occhiata all’insieme di dati di addestramento elaborato, possiamo vedere che i nomi delle colonne
article
,highlights
eid
sono stati sostituiti dagli argomenti previsti dalEncoderDecoderModel
.train_data OUTPUT: ------- Dataset(features: {'attention_mask': Sequence(feature=Value(dtype='int64', id=None), length=-1, id=None), 'decoder_attention_mask': Sequence(feature=Value(dtype='int64', id=None), length=-1, id=None), 'decoder_input_ids': Sequence(feature=Value(dtype='int64', id=None), length=-1, id=None), 'input_ids': Sequence(feature=Value(dtype='int64', id=None), length=-1, id=None), 'labels': Sequence(feature=Value(dtype='int64', id=None), length=-1, id=None)}, num_rows: 32)
Fino ad ora, i dati sono stati manipolati utilizzando il formato
List
di Python. Convertiamo i dati in tensori PyTorch per poterli addestrare sulla GPU.train_data.set_format( type="torch", columns=["input_ids", "attention_mask", "labels"], )
Fantastico, la fase di elaborazione dei dati di addestramento è terminata. In modo analogo, possiamo fare lo stesso per i dati di convalida.
Prima, carichiamo il 10% dell’insieme di dati di convalida:
val_data = datasets.load_dataset("cnn_dailymail", "3.0.0", split="validation[:10%]")
A scopo dimostrativo, i dati di convalida vengono quindi ridotti a solo 8 campioni,
val_data = val_data.select(range(8))
viene applicata la funzione di mappatura,
val_data = val_data.map( process_data_to_model_inputs, batched=True, batch_size=batch_size, remove_columns=["article", "highlights", "id"] )
e, infine, i dati di convalida vengono convertiti anche in tensori PyTorch.
val_data.set_format( type="torch", columns=["input_ids", "attention_mask", "labels"], )
Grande! Ora possiamo passare al warm-start del
EncoderDecoderModel
.Warm-start dell’Encoder-Decoder Model
In questa sezione viene spiegato come un modello Encoder-Decoder può essere avviato in modo caldo utilizzando il checkpoint
bert-base-cased
.Iniziamo importando il
EncoderDecoderModel
. Per informazioni più dettagliate sulla classeEncoderDecoderModel
, si consiglia al lettore di consultare la documentazione.from transformers import EncoderDecoderModel
A differenza delle altre classi di modelli in 🤗Transformers, la classe
EncoderDecoderModel
ha due metodi per caricare pesi preaddestrati, ovvero:-
il metodo “standard”
.from_pretrained(...)
deriva dal metodo generalePretrainedModel.from_pretrained(...)
e corrisponde quindi esattamente all’uno delle altre classi di modelli. La funzione si aspetta un singolo identificatore del modello, ad esempio.from_pretrained("google/bert2bert_L-24_wmt_de_en")
e caricherà un singolo file di checkpoint.pt
nella classeEncoderDecoderModel
. -
un metodo speciale
.from_encoder_decoder_pretrained(...)
, che può essere utilizzato per avviare in modo caldo un modello encoder-decoder da due identificatori di modelli – uno per l’encoder e uno per il decoder. Il primo identificatore del modello viene utilizzato per caricare l’encoder, tramiteAutoModel.from_pretrained(...)
(vedere la documentazione qui) e il secondo identificatore del modello viene utilizzato per caricare il decoder tramiteAutoModelForCausalLM
(vedere la documentazione qui).
Bene, avviamo in modo caldo il nostro modello BERT2BERT. Come accennato in precedenza, avvieremo in modo caldo sia l’encoder che il decoder con il checkpoint
"bert-base-cased"
.bert2bert = EncoderDecoderModel.from_encoder_decoder_pretrained("bert-base-uncased", "bert-base-uncased") OUTPUT: ------- """Alcuni pesi del checkpoint del modello in bert-base-uncased non sono stati utilizzati durante l'inizializzazione di BertLMHeadModel: ['cls.seq_relationship.weight', 'cls.seq_relationship.bias'] - Questo è previsto se si sta inizializzando BertLMHeadModel dal checkpoint di un modello addestrato su un'altra attività o con un'altra architettura (ad es. inizializzando un modello BertForSequenceClassification da un modello BertForPretraining). - Questo NON è previsto se si sta inizializzando BertLMHeadModel dal checkpoint di un modello che ci si aspetta sia esattamente identico (inizializzando un modello BertForSequenceClassification da un modello BertForSequenceClassification). Alcuni pesi di BertLMHeadModel non sono stati inizializzati dal checkpoint del modello in bert-base-uncased e sono stati appena inizializzati: ['bert.encoder.layer.0.crossattention.self.query.weight', 'bert.encoder.layer.0.crossattention.self.query.bias', 'bert.encoder.layer.0.crossattention.self.key.weight', 'bert.encoder.layer.0.crossattention.self.key.bias', 'bert.encoder.layer.0.crossattention.self.value.weight', 'bert.encoder.layer.0.crossattention.self.value.bias', 'bert.encoder.layer.0.crossattention.output.dense.weight', 'bert.encoder.layer.0.crossattention.output.dense.bias', 'bert.encoder.layer.0.crossattention.output.LayerNorm.weight', 'bert.encoder.layer.0.crossattention.output.LayerNorm.bias', 'bert.encoder.layer.1.crossattention.self.query.weight', 'bert.encoder.layer.1.crossattention.self.query.bias', 'bert.encoder.layer.1.crossattention.self.key.weight', 'bert.encoder.layer.1.crossattention.self.key.bias', 'bert.encoder.layer.1.crossattention.self.value.weight', 'bert.encoder.layer.1.crossattention.self.value.bias', 'bert.encoder.layer.1.crossattention.output.dense.weight', 'bert.encoder.layer.1.crossattention.output.dense.bias', 'bert.encoder.layer.1.crossattention.output.LayerNorm.weight', 'bert.encoder.layer.1.crossattention.output.LayerNorm.bias', 'bert.encoder.layer.2.crossattention.self.query.weight', 'bert.encoder.layer.2.crossattention.self.query.bias', 'bert.encoder.layer.2.crossattention.self.key.weight', 'bert.encoder.layer.2.crossattention.self.key.bias', 'bert.encoder.layer.2.crossattention.self.value.weight', 'bert.encoder.layer.2.crossattention.self.value.bias', 'bert.encoder.layer.2.crossattention.output.dense.weight', 'bert.encoder.layer.2.crossattention.output.dense.bias', 'bert.encoder.layer.2.crossattention.output.LayerNorm.weight', 'bert.encoder.layer.2.crossattention.output.LayerNorm.bias', 'bert.encoder.layer.3.crossattention.self.query.weight', 'bert.encoder.layer.3.crossattention.self.query.bias', 'bert.encoder.layer.3.crossattention.self.key.weight', 'bert.encoder.layer.3.crossattention.self.key.bias', 'bert.encoder.layer.3.crossattention.self.value.weight', 'bert.encoder.layer.3.crossattention.self.value.bias', 'bert.encoder.layer.3.crossattention.output.dense.weight', 'bert.encoder.layer.3.crossattention.output.dense.bias', 'bert.encoder.layer.3.crossattention.output.LayerNorm.weight', 'bert.encoder.layer.3.crossattention.output.LayerNorm.bias', 'bert.encoder.layer.4.crossattention.self.query.weight', 'bert.encoder.layer.4.crossattention.self.query.bias', 'bert.encoder.layer.4.crossattention.self.key.weight', 'bert.encoder.layer.4.crossattention.self.key.bias', 'bert.encoder.layer.4.crossattention.self.value.weight', 'bert.encoder.layer.4.crossattention.self.value.bias', 'bert.encoder.layer.4.crossattention.output.dense.weight', 'bert.encoder.layer.4.crossattention.output.dense.bias', 'bert.encoder.layer.4.crossattention.output.LayerNorm.weight', 'bert.encoder.layer.4.crossattention.output.LayerNorm.bias', 'bert.encoder.layer.5.crossattention.self.query.weight', 'bert.encoder.layer.5.crossattention.self.query.bias', 'bert.encoder.layer.5.crossattention.self.key.weight', 'bert.encoder.layer.5.crossattention.self.key.bias', 'bert.encoder.layer.5.crossattention.self.value.weight', 'bert.encoder.layer.5.crossattention.self.value.bias', 'bert.encoder.layer.5.crossattention.output.dense.weight', 'bert.encoder.layer.5.crossattention.output.dense.bias', 'bert.encoder.layer.5.crossattention.output.LayerNorm.weight', 'bert.encoder.layer.5.crossattention.output.LayerNorm.bias', 'bert.encoder.layer.6.crossattention.self.query.weight', 'bert.encoder.layer.6.crossattention.self.query.bias', 'bert.encoder.layer.6.crossattention.self.key.weight', 'bert.encoder.layer.6.crossattention.self.key.bias', 'bert.encoder.layer.6.crossattention.self.value.weight', 'bert.encoder.layer.6.crossattention.self.value.bias', 'bert.encoder.layer.6.crossattention.output.dense.weight', 'bert.encoder.layer.6.crossattention.output.dense.bias', 'bert.encoder.layer.6.crossattention.output.LayerNorm.weight', 'bert.encoder.layer.6.crossattention.output.LayerNorm.bias', 'bert.encoder.layer.7.crossattention.self.query.weight', 'bert.encoder.layer.7.crossattention.self.query.bias', 'bert.encoder.layer.7.crossattention.self.key.weight', 'bert.encoder.layer.7.crossattention.self.key.bias', 'bert.encoder.layer.7.crossattention.self.value.weight', 'bert.encoder.layer.7.crossattention.self.value.bias', 'bert.encoder.layer.7.crossattention.output.dense.weight', 'bert.encoder.layer.7.crossattention.output.dense.bias', 'bert.encoder.layer.7.crossattention.output.LayerNorm.weight', 'bert.encoder.layer.7.crossattention.output.LayerNorm.bias', '
Per una volta, dovremmo dare un'occhiata attenta all'avviso qui. Possiamo vedere che due pesi corrispondenti a un livello
"cls"
non sono stati utilizzati. Questo non dovrebbe essere un problema perché non abbiamo bisogno del livello CLS di BERT per compiti di sequenza. Inoltre, notiamo che molti pesi sono "nuovamente" o inizializzati casualmente. Dando un'occhiata più da vicino, questi pesi corrispondono tutti al livello di cross-attenzione, che è esattamente quello che ci aspetteremmo dopo aver letto la teoria sopra.Diamo un'occhiata più da vicino al modello.
bert2bert OUTPUT: ------- EncoderDecoderModel( (encoder): BertModel( (embeddings): BertEmbeddings( (word_embeddings): Embedding(30522, 768, padding_idx=0) (position_embeddings): Embedding(512, 768) (token_type_embeddings): Embedding(2, 768) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) (encoder): BertEncoder( (layer): ModuleList( (0): BertLayer( (attention): BertAttention( (self): BertSelfAttention( (query): Linear(in_features=768, out_features=768, bias=True) (key): Linear(in_features=768, out_features=768, bias=True) (value): Linear(in_features=768, out_features=768, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) (output): BertSelfOutput( (dense): Linear(in_features=768, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) ) (intermediate): BertIntermediate( (dense): Linear(in_features=768, out_features=3072, bias=True) ) (output): BertOutput( (dense): Linear(in_features=3072, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) ), ... , (11): BertLayer( (attention): BertAttention( (self): BertSelfAttention( (query): Linear(in_features=768, out_features=768, bias=True) (key): Linear(in_features=768, out_features=768, bias=True) (value): Linear(in_features=768, out_features=768, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) (output): BertSelfOutput( (dense): Linear(in_features=768, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) ) (intermediate): BertIntermediate( (dense): Linear(in_features=768, out_features=3072, bias=True) ) (output): BertOutput( (dense): Linear(in_features=3072, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) ) ) ) (pooler): BertPooler( (dense): Linear(in_features=768, out_features=768, bias=True) (activation): Tanh() ) ) (decoder): BertLMHeadModel( (bert): BertModel( (embeddings): BertEmbeddings( (word_embeddings): Embedding(30522, 768, padding_idx=0) (position_embeddings): Embedding(512, 768) (token_type_embeddings): Embedding(2, 768) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) (encoder): BertEncoder( (layer): ModuleList( (0): BertLayer( (attention): BertAttention( (self): BertSelfAttention( (query): Linear(in_features=768, out_features=768, bias=True) (key): Linear(in_features=768, out_features=768, bias=True) (value): Linear(in_features=768, out_features=768, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) (output): BertSelfOutput( (dense): Linear(in_features=768, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) ) (crossattention): BertAttention( (self): BertSelfAttention( (query): Linear(in_features=768, out_features=768, bias=True) (key): Linear(in_features=768, out_features=768, bias=True) (value): Linear(in_features=768, out_features=768, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) (output): BertSelfOutput( (dense): Linear(in_features=768, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) ) (intermediate): BertIntermediate( (dense): Linear(in_features=768, out_features=3072, bias=True) ) (output): BertOutput( (dense): Linear(in_features=3072, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) ), ..., (11): BertLayer( (attention): BertAttention( (self): BertSelfAttention( (query): Linear(in_features=768, out_features=768, bias=True) (key): Linear(in_features=768, out_features=768, bias=True) (value): Linear(in_features=768, out_features=768, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) (output): BertSelfOutput( (dense): Linear(in_features=768, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) ) (crossattention): BertAttention( (self): BertSelfAttention( (query): Linear(in_features=768, out_features=768, bias=True) (key): Linear(in_features=768, out_features=768, bias=True) (value): Linear(in_features=768, out_features=768, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) (output): BertSelfOutput( (dense): Linear(in_features=768, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) ) (intermediate): BertIntermediate( (dense): Linear(in_features=768, out_features=3072, bias=True) ) (output): BertOutput( (dense): Linear(in_features=3072, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) ) ) ) ) (cls): BertOnlyMLMHead( (predictions): BertLMPredictionHead( (transform): BertPredictionHeadTransform( (dense): Linear(in_features=768, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) ) (decoder): Linear(in_features=768, out_features=30522, bias=True) ) ) ) )
Vediamo che
bert2bert.encoder
è un'istanza diBertModel
e chebert2bert.decoder
è una diBertLMHeadModel
. Tuttavia, entrambe le istanze sono ora combinate in un singolotorch.nn.Module
e possono quindi essere salvate come un singolo file di checkpoint.pt
.Proviamolo usando il metodo standard
.save_pretrained(...)
.bert2bert.save_pretrained("bert2bert")
Allo stesso modo, il modello può essere ricaricato usando il metodo standard
.from_pretrained(...)
.bert2bert = EncoderDecoderModel.from_pretrained("bert2bert")
Fantastico. Salviamo anche la configurazione.
bert2bert.config OUTPUT: ------- EncoderDecoderConfig { "_name_or_path": "bert2bert", "architectures": [ "EncoderDecoderModel" ], "decoder": { "_name_or_path": "bert-base-uncased", "add_cross_attention": true, "architectures": [ "BertForMaskedLM" ], "attention_probs_dropout_prob": 0.1, "bad_words_ids": null, "bos_token_id": null, "chunk_size_feed_forward": 0, "decoder_start_token_id": null, "do_sample": false, "early_stopping": false, "eos_token_id": null, "finetuning_task": null, "gradient_checkpointing": false, "hidden_act": "gelu", "hidden_dropout_prob": 0.1, "hidden_size": 768, "id2label": { "0": "LABEL_0", "1": "LABEL_1" }, "initializer_range": 0.02, "intermediate_size": 3072, "is_decoder": true, "is_encoder_decoder": false, "label2id": { "LABEL_0": 0, "LABEL_1": 1 }, "layer_norm_eps": 1e-12, "length_penalty": 1.0, "max_length": 20, "max_position_embeddings": 512, "min_length": 0, "model_type": "bert", "no_repeat_ngram_size": 0, "num_attention_heads": 12, "num_beams": 1, "num_hidden_layers": 12, "num_return_sequences": 1, "output_attentions": false, "output_hidden_states": false, "pad_token_id": 0, "prefix": null, "pruned_heads": {}, "repetition_penalty": 1.0, "return_dict": false, "sep_token_id": null, "task_specific_params": null, "temperature": 1.0, "tie_encoder_decoder": false, "tie_word_embeddings": true, "tokenizer_class": null, "top_k": 50, "top_p": 1.0, "torchscript": false, "type_vocab_size": 2, "use_bfloat16": false, "use_cache": true, "vocab_size": 30522, "xla_device": null }, "encoder": { "_name_or_path": "bert-base-uncased", "add_cross_attention": false, "architectures": [ "BertForMaskedLM" ], "attention_probs_dropout_prob": 0.1, "bad_words_ids": null, "bos_token_id": null, "chunk_size_feed_forward": 0, "decoder_start_token_id": null, "do_sample": false, "early_stopping": false, "eos_token_id": null, "finetuning_task": null, "gradient_checkpointing": false, "hidden_act": "gelu", "hidden_dropout_prob": 0.1, "hidden_size": 768, "id2label": { "0": "LABEL_0", "1": "LABEL_1" }, "initializer_range": 0.02, "intermediate_size": 3072, "is_decoder": false, "is_encoder_decoder": false, "label2id": { "LABEL_0": 0, "LABEL_1": 1 }, "layer_norm_eps": 1e-12, "length_penalty": 1.0, "max_length": 20, "max_position_embeddings": 512, "min_length": 0, "model_type": "bert", "no_repeat_ngram_size": 0, "num_attention_heads": 12, "num_beams": 1, "num_hidden_layers": 12, "num_return_sequences": 1, "output_attentions": false, "output_hidden_states": false, "pad_token_id": 0, "prefix": null, "pruned_heads": {}, "repetition_penalty": 1.0, "return_dict": false, "sep_token_id": null, "task_specific_params": null, "temperature": 1.0, "tie_encoder_decoder": false, "tie_word_embeddings": true, "tokenizer_class": null, "top_k": 50, "top_p": 1.0, "torchscript": false, "type_vocab_size": 2, "use_bfloat16": false, "use_cache": true, "vocab_size": 30522, "xla_device": null }, "is_encoder_decoder": true, "model_type": "encoder_decoder" }
La configurazione è similemente composta da una configurazione di codificatore e una configurazione di decodificatore, entrambe delle quali sono istanze di
BertConfig
nel nostro caso. Tuttavia, la configurazione generale è di tipoEncoderDecoderConfig
e quindi viene salvata come singolo file.json
.In conclusione, si deve ricordare che una volta istanziato un oggetto
EncoderDecoderModel
, fornisce la stessa funzionalità di qualsiasi altro modello Encoder-Decoder in 🤗Transformers, ad esempio BART, T5, ProphetNet, ... L'unica differenza è che unEncoderDecoderModel
fornisce la funzione aggiuntivafrom_encoder_decoder_pretrained(...)
che consente alla classe del modello di partire da zero utilizzando checkpoint di codificatore e decodificatore.Una nota a margine, se si desidera creare un modello di codificatore-decodificatore condiviso, il parametro
tie_encoder_decoder=True
può essere passato come segue:shared_bert2bert = EncoderDecoderModel.from_encoder_decoder_pretrained("bert-base-cased", "bert-base-cased", tie_encoder_decoder=True)
Per fare un confronto, possiamo vedere che il modello condiviso ha molti meno parametri come ci si aspetta.
print(f"\n\nNum Params. Condivisi: {shared_bert2bert.num_parameters()}, Non Condivisi: {bert2bert.num_parameters()}") OUTPUT: ------- Num Params. Condivisi: 137298244, Non Condivisi: 247363386
In questo notebook, tuttavia, addestreremo un modello Bert2Bert non condiviso, quindi continuiamo con
bert2bert
e non conshared_bert2bert
.# liberare memoria del shared_bert2bert
Abbiamo avviato un modello
bert2bert
, ma non abbiamo ancora definito tutti i parametri rilevanti utilizzati per la decodifica della ricerca di fascio.Iniziamo impostando i token speciali.
bert-base-cased
non ha undecoder_start_token_id
o uneos_token_id
, quindi useremo rispettivamente il suocls_token_id
esep_token_id
. Inoltre, dovremmo definire unpad_token_id
nella configurazione e assicurarci che la correttavocab_size
sia impostata.bert2bert.config.decoder_start_token_id = tokenizer.cls_token_id bert2bert.config.eos_token_id = tokenizer.sep_token_id bert2bert.config.pad_token_id = tokenizer.pad_token_id bert2bert.config.vocab_size = bert2bert.config.encoder.vocab_size
Successivamente, definiamo tutti i parametri relativi alla decodifica della ricerca di fascio. Poiché
bart-large-cnn
produce buoni risultati su CNN/Dailymail, copieremo i suoi parametri di decodifica della ricerca di fascio.Per ulteriori dettagli su cosa fa ciascun parametro, si prega di leggere questo post sul blog o la documentazione.
bert2bert.config.max_length = 142 bert2bert.config.min_length = 56 bert2bert.config.no_repeat_ngram_size = 3 bert2bert.config.early_stopping = True bert2bert.config.length_penalty = 2.0 bert2bert.config.num_beams = 4
Ora iniziamo ad addestrare il modello BERT2BERT precedentemente avviato.
Addestramento fine-tuning di modelli di codificatore-decodificatore avviati precedentemente
In questa sezione, mostreremo come si può utilizzare il
Seq2SeqTrainer
per eseguire il fine-tuning di un modello di codificatore-decodificatore avviato precedentemente.Iniziamo importando il
Seq2SeqTrainer
e i suoi argomenti di addestramentoSeq2SeqTrainingArguments
.from transformers import Seq2SeqTrainer, Seq2SeqTrainingArguments
Inoltre, abbiamo bisogno di un paio di pacchetti Python per far funzionare il
Seq2SeqTrainer
.!pip install git-python==1.0.3 !pip install rouge_score !pip install sacrebleu
Il
Seq2SeqTrainer
estende il Trainer di 🤗Transformer per i modelli di codificatore-decodificatore. In breve, consente di utilizzare la funzionegenerate(...)
durante la valutazione, che è necessaria per validare le prestazioni dei modelli di codificatore-decodificatore su gran parte dei compiti sequenza-su-sequenza, come la sintesi.Per ulteriori informazioni sul
Trainer
, si consiglia di leggere questo breve tutorial.Iniziamo configurando il
Seq2SeqTrainingArguments
.L'argomento
predict_with_generate
dovrebbe essere impostato suTrue
, in modo che ilSeq2SeqTrainer
esegua la funzionegenerate(...)
sui dati di validazione e passi l'output generato comepredictions
alla funzionecompute_metric(...)
che definiremo successivamente. Gli argomenti aggiuntivi derivano daTrainingArguments
e possono essere letti qui. Per un run di addestramento completo, è necessario modificare quegli argomenti secondo necessità. Di seguito sono commentati i valori predefiniti buoni.Per ulteriori informazioni sul
Seq2SeqTrainer
, si consiglia di dare un'occhiata al codice.training_args = Seq2SeqTrainingArguments( predict_with_generate=True, evaluation_strategy="steps", per_device_train_batch_size=batch_size, per_device_eval_batch_size=batch_size, fp16=True, output_dir="./", logging_steps=2, save_steps=10, eval_steps=4, # logging_steps=1000, # save_steps=500, # eval_steps=7500, # warmup_steps=2000, # save_total_limit=3, )
Inoltre, è necessario definire una funzione per calcolare correttamente il punteggio ROUGE durante la validazione. Poiché abbiamo attivato
predict_with_generate
, la funzionecompute_metrics(...)
si aspettapredictions
ottenute utilizzando la funzionegenerate(...)
. Come la maggior parte dei compiti di riassunto, CNN/Dailymail viene di solito valutato utilizzando il punteggio ROUGE.Iniziamo caricando la metrica ROUGE utilizzando la libreria 🤗datasets.
rouge = datasets.load_metric("rouge")
Successivamente, definiremo la funzione
compute_metrics(...)
. La metricarouge
calcola il punteggio da due elenchi di stringhe. Quindi decodifichiamo sia lepredictions
che lelabels
- assicurandoci che-100
sia correttamente sostituito dapad_token_id
e rimuoviamo tutti i caratteri speciali impostandoskip_special_tokens=True
.def compute_metrics(pred): labels_ids = pred.label_ids pred_ids = pred.predictions pred_str = tokenizer.batch_decode(pred_ids, skip_special_tokens=True) labels_ids[labels_ids == -100] = tokenizer.pad_token_id label_str = tokenizer.batch_decode(labels_ids, skip_special_tokens=True) rouge_output = rouge.compute(predictions=pred_str, references=label_str, rouge_types=["rouge2"])["rouge2"].mid return { "rouge2_precision": round(rouge_output.precision, 4), "rouge2_recall": round(rouge_output.recall, 4), "rouge2_fmeasure": round(rouge_output.fmeasure, 4), }
Fantastico, ora possiamo passare tutti gli argomenti al
Seq2SeqTrainer
e iniziare il fine-tuning. Eseguire la cella seguente richiederà circa 10 minuti ☕.Il fine-tuning di BERT2BERT sui dati di addestramento completi di CNN/Dailymail richiede ca. 8 ore su una singola GPU TITAN RTX.
# istanzia il trainer trainer = Seq2SeqTrainer( model=bert2bert, tokenizer=tokenizer, args=training_args, compute_metrics=compute_metrics, train_dataset=train_data, eval_dataset=val_data, ) trainer.train()
Fantastico, ora dovremmo essere completamente attrezzati per eseguire il fine-tuning di un modello codificatore-decodificatore con warm-start. Per verificare il risultato del nostro fine-tuning, diamo un'occhiata ai checkpoint salvati.
!ls OUTPUT: ------- bert2bert checkpoint-20 runs seq2seq_trainer.py checkpoint-10 __pycache__ sample_data seq2seq_training_args.py
Infine, possiamo caricare il checkpoint come al solito tramite il metodo
EncoderDecoderModel.from_pretrained(...)
.dummy_bert2bert = EncoderDecoderModel.from_pretrained("./checkpoint-20")
Valutazione
In una fase finale, potremmo voler valutare il modello BERT2BERT sui dati di test.
Per iniziare, anziché caricare il modello fittizio, carichiamo un modello BERT2BERT che è stato addestrato ulteriormente sull'intero set di dati di addestramento. Carichiamo anche il suo tokenizzatore, che è solo una copia del tokenizzatore di
bert-base-cased
.from transformers import BertTokenizer bert2bert = EncoderDecoderModel.from_pretrained("patrickvonplaten/bert2bert_cnn_daily_mail").to("cuda") tokenizer = BertTokenizer.from_pretrained("patrickvonplaten/bert2bert_cnn_daily_mail")
Successivamente, carichiamo solo il 2% dei dati di test di CNN/Dailymail. Per una valutazione completa, ovviamente si dovrebbe utilizzare il 100% dei dati.
test_data = datasets.load_dataset("cnn_dailymail", "3.0.0", split="test[:2%]")
Ora, possiamo nuovamente sfruttare la pratica funzione
map()
di 🤗dataset per generare un riassunto per ogni campione di test.Per ogni campione di dati:
- prima, tokenizziamo l'
"articolo"
, - seconda, generiamo gli id dei token di output, e
- terza, decodifichiamo gli id dei token di output per ottenere il nostro riassunto previsto.
def generate_summary(batch): # tagliamo alla lunghezza massima di BERT, 512 inputs = tokenizer(batch["article"], padding="max_length", truncation=True, max_length=512, return_tensors="pt") input_ids = inputs.input_ids.to("cuda") attention_mask = inputs.attention_mask.to("cuda") outputs = bert2bert.generate(input_ids, attention_mask=attention_mask) output_str = tokenizer.batch_decode(outputs, skip_special_tokens=True) batch["pred_summary"] = output_str return batch
Eseguiamo la funzione map per ottenere il dizionario dei risultati che ha il riassunto previsto del modello memorizzato per ogni campione. L'esecuzione della cella seguente potrebbe richiedere circa 10 minuti ☕.
batch_size = 16 # cambia a 64 per una valutazione completa results = test_data.map(generate_summary, batched=True, batch_size=batch_size, remove_columns=["article"])
Infine, calcoliamo il punteggio ROUGE.
rouge.compute(predictions=results["pred_summary"], references=results["highlights"], rouge_types=["rouge2"])["rouge2"].mid OUTPUT: ------- Score(precision=0.10389454113300968, recall=0.1564771201053348, fmeasure=0.12175271663717585)
Ecco fatto. Abbiamo mostrato come avviare un modello BERT2BERT e affinarlo/valutarlo sul dataset di CNN/Dailymail.
Il modello BERT2BERT completamente addestrato è caricato nell'🤗hub dei modelli con il nome patrickvonplaten/bert2bert_cnn_daily_mail.
Il modello ottiene un punteggio ROUGE-2 di 18.22 sui dati di valutazione completa, che è persino leggermente migliore di quanto riportato nel paper.
Per alcuni esempi di sintesi, si consiglia al lettore di utilizzare l'API di inferenza online del modello, qui.
Un grande ringraziamento a Sascha Rothe, Shashi Narayan e Aliaksei Severyn di Google Research, e a Victor Sanh, Sylvain Gugger e Thomas Wolf di 🤗Hugging Face per la revisione e il prezioso feedback.