Reti ricorrenti 🇬🇧
Note a cura di Antonino Furnari - antonino.furnari@unict.it🔝
Università di Catania, Dipartimento di Matematica e Informatica
Note disponibili qui: http://www.antoninofurnari.github.iohttps://antoninofurnari.github.io/lecture-notes/it/data-science-python/reti-ricorrenti/
In questo laboratorio vedremo come costruire ed allenare un modello di tipo LSTM. I modelli visti finora analizzano ciascun dato in input in maniera totalmente indipendente e pertanto essi sono limitati quando è necessario processare delle sequenze di dati. Si pensi ad esempio a un video: una CNN può essere utilizzata per analizzare ciascun frame del video, tuttavia l’analisi di ciascun frame sarà totalmente indipendente rispetto agli altri frame e pertanto il modello non potrà modellare le interdipendenze temporali tra frame adiacenti (ad esempio due frame adiacenti verosimilmente contengon scene e oggetti simili). Per risolvere questo problema, sono state proposte le reti ricorrenti (Recurrent Neural Networks - RNN).
Una rete ricorrente può essere utilizzata per analizzare una sequenza di dati di lunghezza non fissa. Con riferimento al fatto che i diversi elementi della sequenza sono disponibili in istanti temporali diversi, ciascun passo della computazione è generalmente detto “time-step”. Per permettere questo tipo di analisi, il modello mantiene un vettore di “memoria” che viene passato da un time-step al successivo.
Una rete ricorrente può essere utilizzata per implementare diversi schemi di processing di sequenze, come mostrato nella seguente figura:
(Immagine da http://karpathy.github.io/2015/05/21/rnn-effectiveness/ )
In particolare:
- One-to-one: è il tipo di processing visto finora, in cui i dati non sono cosniderati come parte di una sequenza;
- One-to-many: si tratta di task di “generazione” di sequenze, in cui l’input è un singolo data point, mentre l’output è una sequenze. Ad esempio: generazione di una parola data la sua prima lettera;
- Many-to-one: l’input è una sequenza (ad esempio una stringa di caratteri), l’output è un valore scalare. Ad esempio: classificazione di testo;
- Many-to-many 1: l’input è una sequenza, l’output è una sequenza. Ad esempio: traduzione di testo da una lingua a un’altra;
- Many-to-many 2: Simile allo schema precedente, ma l’output viene generato passo dopo passo senza attendere di aver prima processato l’intera sequenza di input. Ad esempio: Part of Speech tagging, ovvero classificaizione di ciascuna parola del testo per indicare il ruolo all’interno della frase (es. nome, verbo, ecc).
Per una discussione più approfondita, si veda qui: http://karpathy.github.io/2015/05/21/rnn-effectiveness/.
|
|
1 Classificazione di sequenze (many to one)
Inizieremo vedremo un esempio di classificazione di sequenze. Nell’illustrazione vista sopra, ciò corrisponde ad adottare uno schemo many-to-one.
1.1 Vanilla RNN
Il modello più semplice di rete ricorrente (spesso detto “vanilla RNN”) viene definito in maniera ricorsiva utilizzando le seguenti equazioni:
\begin{equation} h_0 = \mathbf{0} \end{equation}
\begin{equation} h_t = rnn(x_t, h_{t-1}) = tanh(W_{ih} \cdot x_t + b_{ih} + W_{hh} \cdot h_{t-1} + b_{hh}) \end{equation}
dove $h_t$ ha dimensione $d_h$, $\mathbf{0}$ indica un vettore di dimensione $d_h$ formato da zero, $x_t$ ha dimensione $d_x$ e i parametri del modello sono i pesi $W_{ih}$ (“i” sta per input e “h” per hidden), $b_{ih}$, $W_{hh}$, $b_{hh}$.
Domanda 1 Che dimensioni hanno le matrici $W_{ih}$, $W_{hh}$ e i vettori $b_{ih}$, $b_{hh}$? |
La computazione espressa dalla formula sopra può essere riassunta dalla seguente figura:
Come possiamo notare l’input della cella RNN è una coppia di valori $h_{t-1}$ (hidden cell) e $x_t$ (input). Possiamo usare una RNN per analizzare una sequenza di input ${x_t}_t$ applicando la cella in maniera ricorsiva come segue:
Da notare che $h_0$ viene inizializzata con degli zeri (caso base della ricorsione). In questo caso l’input è una sequenza di vettori ${x_t}_t$, mentre l’output è una sequenza di vettori ${h_t}_t$.
1.2 RNN per la classificazione di cognomi
Vedremo un semplice esempio di utilizzo di un RNN per classificare sequenze. Riprenderemo l’esempio proposto dai tutorial di PyTorch (https://pytorch.org/tutorials/intermediate/char_rnn_classification_tutorial.html) sulla classificazione di cognomi.
Il problema considerato consiste nell’analizzare un cognome (inteso come una sequenza di caratteri) e classificarne la lingua di appartenenza. Ad esempio “Rossi” dovrebbe essere riconosciuto come un cognome italiano, mentre “Smith” come un cognome inglese. Va notato che per risolvere questo problema, è necessario avere un modello che modelli la sequenza dei caratteri in input e non solo i singoli caratteri in maniera indipendente come visto nei modelli finora analizzati.
Iniziamo scaricando i dati disponibili a questo link: https://download.pytorch.org/tutorial/data.zip. Estraiamo quindi il contenuto dell’archivio nella directory di lavoro. Nella cartella data/names
sono contenuti dei file di testo nel formato [lingua].txt
.
Carichiamo tutti i file, trasformiamo i nomi da unicode a ascii e mettiamo nomi e relative etichette in due liste:
|
|
Controlliamo quanti elementi abbiamo:
|
|
(20074, 20074)
Visualizziamo i primi elementi e le loro etichette:
|
|
(['Khoury', 'Nahas', 'Daher'], ['Arabic', 'Arabic', 'Arabic'])
Controlliamo infine il numero di classi:
|
|
18
Il nostro modello dovrà analizzare le sequenze di caratteri, pertanto abbiamo bisogno di trasfromare ciascun carattere in un tensore. Dato che abbiamo un numero finito di caratteri, un approccio possibile consiste nel rappresentare ciascun carattere come un one-hot-vector. Ad esempio, il carattere “a” può essere rappresentato mediante il vettore [1, 0, 0, ...., 0]
se a è il primo primo carattere dell’alfabeto.
Definiamo il nostro alfabeto considerando tutte le lettere ascii e alcuni segni di punteggiatura:
|
|
57 abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ .,;'
Il nostro alfabeto contiene 57
elementi. Possiamo trovare l’indice di un dato carattere mediante il metodo find
delle stringhe:
|
|
0
Scriviamo dunque un codice che trasformi ciascun nome in una sequenza di one hot vector:
|
|
Proviamo il codice scritto sopra:
|
|
Visualizziamo il one hot vector del carattere ‘a’ (il terzo):
|
|
tensor([1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0.])
Assegniamo inoltre ad ogni classe un id numerico:
|
|
(9, 'Italian')
A questo punto trasfromiamo la lista di nomi in una lista di tensori e la lista di classi in una lista di id:
|
|
Mettiamo insieme quanto visto finora per costruire un oggetto di tipo dataset. Suddivideremo inoltre in training e test set in maniera casuale usando un seed:
|
|
Verifichiamo che tutto funzioni correttamente:
|
|
14051
torch.Size([5, 57]) 10
torch.Size([8, 57]) 12
L’oggetto dataset ci permette di caricare i nomi come sequenze di un numero variabile di vettori di dimensione $57$. Ogni vettore è associato a una etichetta. Controlliamo che lo stesso valga per il dataset di test:
|
|
6023
torch.Size([3, 57]) 11
torch.Size([4, 57]) 14
Costruiamo adesso dei DataLoader. Dato che ogni sequenza del dataset ha una lunghezza diversa, ultizzeremo intanto un batch size uguale a $1$.
|
|
Domanda 2 Perché non è possibile utilizzare un batch size diverso da 1? |
Costruiamo adesso una architettura basata su RNN che ci permetta di classificare ciascuna sequenza. L’input di questo modello sarà un sequenza, mentre l’output è una etichetta. Dato che ci interessa ottenere un solo valore in output, considereremo solamente l’ultimo valore generato dalla RNN $h_t$. Dato che abbiamo $18$ classi, il nostro output dovrebbe contenere $18$ elementi. Per evitare di imporre che la dimensione del layer nascosto sia pari al numero di classi, introduciamo un layer di tipo Linear
per trasformare l’ultimo hidden layer nell’output desiderato. Ciò permetterà inoltre di mappare il range dell’hidden layer ($[-1,1]$) su un range arbitrario. Il modello seguirà la seguente architettura:
Domanda 3 Perché il range dell’hidden layer è $[-1,1]$? In quali casi serve avere un range diverso? |
Definiamo l’architettura facendo uso della implementazione delle RNN fornita da PyTorch:
|
|
Definiamo adesso il modulo di Lightning per effettuare il training:
|
|
Effettuiamo il training. Facciamo training solo per poche epoche in quanto questo modello è poco ottimizzato (vediamo fra poco perché).
|
|
GPU available: True, used: True
TPU available: False, using: 0 TPU cores
IPU available: False, using: 0 IPUs
LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0,1,2,3]
| Name | Type | Params
-----------------------------------------------
0 | model | NameClassifier | 26.3 K
1 | criterion | CrossEntropyLoss | 0
-----------------------------------------------
26.3 K Trainable params
0 Non-trainable params
26.3 K Total params
0.105 Total estimated model params size (MB)
Alla fine del training, possiamo validare i risultati come segue:
|
|
LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0,1,2,3]
--------------------------------------------------------------------------------
DATALOADER:0 VALIDATE RESULTS
{'val/accuracy': 0.034534286707639694}
--------------------------------------------------------------------------------
[{'val/accuracy': 0.034534286707639694}]
1.3 Padding delle sequenze
Finora abbiamo effettuato il training con batch size pari a 1, il che ha limitato molto la velocità della rete. Se proviamo a cambiare il batch size, otteniamo un errore. Ciò è dovuto al fatto che ciascuna sequenza ha una lunghezza diversa e quindi non è possibile concatenare i tensori relative alle singole sequenze in un unico tensore. Ad esempio, due sequenze di shape [8, 57]
e [3, 57]
non possono essere concatenate in un unico tensore di shape [?, 57]
. Per ovviare a questo problema, è possibile formare un batch aggiungendo degli zeri al tensore più piccolo in modo che le due dimensioni coincidano. I due tensori possono essere quindi concatenati in un unico tensore di shape [2, 8, 57]
. Questa operazione è detta “padding” e può essere implementata come segue in PyTorch:
|
|
In particolare abbiamo ridefinito la funzione collate_fn
che viene richiamata dal DataLoader quando è necessario raggruppare una serie di elementi in un batch. Proviamo a visualizzare le shape di qualche batch:
|
|
torch.Size([16, 11, 57])
torch.Size([16, 10, 57])
Domanda 4 I batch generati dal dataloader hanno in genere dimensioni diverse. Questo è un problema durante il training? Perché? |
Effettuiamo il training:
|
|
GPU available: True, used: True
TPU available: False, using: 0 TPU cores
IPU available: False, using: 0 IPUs
LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0,1,2,3]
| Name | Type | Params
-----------------------------------------------
0 | model | NameClassifier | 26.3 K
1 | criterion | CrossEntropyLoss | 0
-----------------------------------------------
26.3 K Trainable params
0 Non-trainable params
26.3 K Total params
0.105 Total estimated model params size (MB)
Alla fine del training, dovremmo ottenere un grafico del genere:
Validiamo anche in questo caso:
|
|
LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0,1,2,3]
--------------------------------------------------------------------------------
DATALOADER:0 VALIDATE RESULTS
{'val/accuracy': 0.18346340954303741}
--------------------------------------------------------------------------------
[{'val/accuracy': 0.18346340954303741}]
Domanda 5 Il padding delle sequenze cambia il modo in cui viene effettuato il training? In fase di test è strettamente necessario effettuare il padding? |
1.4 LSTM
PyTorch mette a disposizione diversi modelli di RNN, tra cui LSTM e GRU. Utilizzare questi modelli è molto semplice. A differenza di una semplice RNN, una LSTM include un input gate, che modula il contributo della lettura dell’input, un forget gate, che regola quanto dello stato che viene dallo scorso timestep vada “dimenticato” e un output gate, che permette di generare l’output della cella. In maniera simile a una RNN classica, ad ogni timestep una LSTM prende in input un valore ed emette un hidden vector. Oltre all’hidden vector, la LSTM produrrà anche un altro vettore detto “cell state”. Le seguenti equazioni descrivono il funzionamento di una LSTM:
$$ f_t = \sigma(W_f[h_{t-1}, x_t]+b_f)\ \text{(forget gate)}$$ $$ i_t = \sigma(W_i[h_{t-1}, x_t]+b_i)\ \text{(input gate)}$$ $$ \tilde{C}_t = \tanh(W_C[h_{t-1}, x_t]+b_C)\ \text{(candate cell state)}$$ $$C_t = f_t C_{t-1} + i_t \tilde{C}_{t}\ \text{(cell state)}$$ $$ o_t = \sigma(W_o[h_{t-1}, x_t]+b_o)\ \text{(output gate)}$$ $$h_t = o_t \tanh(C_t)\ \text{(hidden vector)}$$
Il funzionamento di una LSTM è riassunto dal seguente schema:
(Immagine da https://colah.github.io/posts/2015-08-Understanding-LSTMs/ )
Per una descrizione più approfondita, vedere qui: https://colah.github.io/posts/2015-08-Understanding-LSTMs/
Le LSTM sono implementate dal modulo torch.nn.LSTM
. Vediamo un esempio:
|
|
torch.Size([1, 100, 128])
torch.Size([1, 1, 128])
torch.Size([1, 1, 128])
tensor(True)
Per effettuare la classificazione dei nomi, ci servirà dunque solo h_n
. Riscriviamo il modulo di classificazione usando una LSTM invece di una classica RNN:
|
|
Possiamo dunque allenare il modello come segue:
|
|
GPU available: True, used: True
TPU available: False, using: 0 TPU cores
IPU available: False, using: 0 IPUs
LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0,1,2,3]
| Name | Type | Params
-------------------------------------------------
0 | model | NameClassifierLSTM | 98.1 K
1 | criterion | CrossEntropyLoss | 0
-------------------------------------------------
98.1 K Trainable params
0 Non-trainable params
98.1 K Total params
0.392 Total estimated model params size (MB)
Validiamo anche in questo caso:
|
|
LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0,1,2,3]
--------------------------------------------------------------------------------
DATALOADER:0 VALIDATE RESULTS
{'val/accuracy': 0.8120537996292114}
--------------------------------------------------------------------------------
[{'val/accuracy': 0.8120537996292114}]
Domanda 6 Si confrontino i risultati ottenuti con la LSTM con quelli ottenuti con la RNN. Ci sono delle differenze? |
2 Generazione di nomi (one to many)
Vediamo adesso un esempio di utilizzo di una RNN per un tipo di mapping one-to-many. In particolare, vedremo come allenare una rete ricorrente a generare nomi a partire dalla lettera iniziale. Il mapping one-to-many è dunque il mapping tra la prima lettera in input e la sequenza di lettere in uscita. Per generare caratteri, seguiremo questo schema:
- La RNN prende in input il primo carattere della sequenza all prima iterazione il carattere precedente in tutte le altre iterazioni;
- La RNN può emettere un carattere di terminazione che indica che ha finito di generare caratteri. Quando questo carattere viene emesso, la generazione viene arrestata;
- Rappresenteremo i caratteri predetti mediante un problema di classificazione in cui prediciamo gli score dei singoli caratteri.
La predizione seguirà il seguente approccio (da https://pytorch.org/tutorials/intermediate/char_rnn_generation_tutorial.html):
Dove EOS
sta per “end of sequence” e segnala che la sequenza è terminata. Iniziamo ri-definendo il modulo dataset partendo da quello definito in precedenza. Vogliamo che il dataset restituisca:
- La sequenza di caratteri in input;
- La sequenza di caratteri desiderata in output;
- La classe del nome (nazionalità).
Passeremo la classe come input al modello, quindi invece di restituire degli indici, restituiremo dei one-hot vector come nel caso dei caratteri.
Implementiamo il modulo:
|
|
Vediamo un esempio:
|
|
torch.Size([5, 58])
torch.Size([5])
torch.Size([18])
Vediamo adesso gli indici dei one-hot vectors contenuti nei due tensori:
|
|
(tensor([38, 0, 8, 19, 0]), tensor([ 0, 8, 19, 0, 57]), tensor(10))
Definiamo la funzione collate
con il padding delle sequenze e i loader di train e test:
|
|
Domanda 7 Si notano delle similarità tra i due vettori? Quali? Qual è l’ultimo indice contenuto nel tensore di output? A cosa corrisponde? |
Adesso definiamo una rete neurale basato su LSTM che ci permetta di generare le sequenze. La rete dovrà prendere in input la sequenza di input e la categoria e restituire una sequenza di caratteri di output. Durante il training, imporremo che la sequenza predetta sia uguale a quella di ground truth. Inseriremo inoltre un metodo sample
che ci permetta di generare un nuovo nome a partire da un carattere iniziale e una categoria.
|
|
Costruiamo il modello e testiamolo con una sequenza fittizia di $5$ caratteri, verificando che l’output sa coerente:
|
|
torch.Size([2, 5, 58])
Domanda 8 La shape di output è corretta? Perché? |
Proviamo a generare un nome. Il risultato sarà chiaramente poco indicativo dato che il modello non è ancora stato allenato:
|
|
('Czech', 'RNlllllllllllllllllll')
A questo punto definiamo il modulo di PyTorch lightning per il training:
|
|
|
|
GPU available: True, used: True
TPU available: False, using: 0 TPU cores
IPU available: False, using: 0 IPUs
LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0,1,2,3]
| Name | Type | Params
-----------------------------------------------
0 | model | GeneratorLSTM | 149 K
1 | criterion | CrossEntropyLoss | 0
-----------------------------------------------
149 K Trainable params
0 Non-trainable params
149 K Total params
0.598 Total estimated model params size (MB)
Proviamo adesso a generare dei nomi:
|
|
Chinese : Ung
Irish : Brann
German : Gerner
Portuguese : Esander
Greek : Wakos
Vietnamese : Fung
Portuguese : Ros
Spanish : Inagrandez
Polish : Panek
Italian : Joni
Dutch : Lann
Greek : Ontopoulos
Russian : Handyukov
Greek : Stroumanis
Irish : Xann
Domanda 9 Il modello è in grado di generare solo un nome per iniziale. Come si potrebbe modificare il modello per generare nomi diversi con la stessa iniziale? |
3 Traduzione (many to many)
Vediamo adesso un esempio di trasformazione many-to-many. Nello specifico, considereremo il problema di tradurre una frase da una lingua a un’altra. Costruiremo un modello basato su due RNN: un encoder, che “legge” la frase in input e la trasforma in un vettore a dimensione fissa detto “contesto” e un decoder, che viene inizializzata con il contesto prodotto dall’encoder e produce la frase in output in maniera simile a come la RNN precedentemente vista generava nomi. Se prima ci siamo posti il problema di generare le singole lettere di un nome, adesso vogliamo generare le parole di una frase, per cui considereremo una rappresentazione in cui ciascuna parola viene associata a un one-hot vector. Il modello proposto funziona come segue:
(Immagine da https://pytorch.org/tutorials/intermediate/seq2seq_translation_tutorial.html)
Come possiamo vedere dallo schema mostrato sopra, ciascuna parola viene mappata su un ID univoco, al quale corrisponderà un one-hot vector. Mentre il decoder “legge” le parole in input e produce solo un vettore di contesto alla fine della sequenza, il decoder sfrutta il metodo “autoregressivo” visto precedentemente in cui l’ultima parola generata viene passata in input al passo successivo. Dato che non forniamo una parola di inizio, oltre al carattere speciale EOS
, considereremo un carattere SOS
(Start Of Sequence). Iniziamo scaricando i dati presenti dal sito https://www.manythings.org/anki/:
wget https://www.manythings.org/anki/ita-eng.zip
unzip ita-eng.zip
Abbiamo estratto il file di testo ita.txt
, che contiene coppie di frasi nelle lingue inglese e italiana separate da una tabulazione. Leggiamo il file:
|
|
Visualizziamo il numero di coppie totali e qualche coppia di frasi:
|
|
Coppie: 352040
Ciao! - Hi.
Sono malvagie. - They're evil.
Ha fatto i suoi compiti da solo? - Did you do your homework by yourself?
Giunse un rapporto che l'allunaggio dell'Apollo 11 era riuscito. - A report came in that Apollo 11 succeeded in landing on the moon.
Adesso dobbiamo trasformare le frasi in sequenze di entità riconducibili a degli ID distinti. Per fare ciò possiamo utilizzare un tokenizer. La libreria spacy
ne mette a disposizione diversi. Installiamo la libreria con:
pip install spacy
Poi installiamo il supporto per le lingue italiana e inglese con:
python -m spacy download it_core_news_sm
python -m spacy download en_core_web_sm
Ora installiamo la libreria torchtext
che permette di lavorare con il testo con PyTorch:
pip install torchtext
A questo punto possiamo definire i due tokenizer per le due lingue come segue:
|
|
I tokenizer possono essere usati per suddividere una frase in token:
|
|
['Ciao', 'sono', 'io', '.']
|
|
['Hello', ',', 'is', 'there', 'anybody', 'in', 'there']
Domanda 10 Si confronti il risultato ottenuto sopra con quello di un algoritmo che considera i token come sequenze id caratteri separati da spazi. Il tokenizer fa qualcosa di più sofisticato? |
Definiamo adesso dei token speciali che useremo per il nostro problema di traduzione:
<unk>
: indica un token sconosciuto, ovvero una parola non appartenente al vocabolario che costruiremo a partire dai dati:<pad>
: indica un token usato per fare padding delle sequenze;<bos>
: beginning of sequence (equivalente aSOS
);<eod>
: end of sequence.
Associeremo un id a ciascuno di questi token speciali:
|
|
Costruiamo adesso un iteratore che ci permetta di scorrere tutti i token presenti nei nostri dati:
|
|
Possiamo adesso costruire un vocabolario per ciascuna lingua come segue:
|
|
Impostiamo i token di default a UNK_IDX
. In questo modo, se un token non viene trovato, verrà associato a questo indice:
|
|
Possiamo utilizzare i due oggetti “vocab_transform” per ottenre una lista di indici da una lista di token:
|
|
[4110, 21, 201]
[110, 98, 525, 19, 98, 9]
Da notare che se un token non viene trovato nel vocabolario, ad esso viene associato l’id 0:
|
|
[0, 0, 0, 0, 0]
Mettiamo adesso insieme quanto visto finora per creare un oggetto dataset che restituisca coppie input-output di frasi. L’oggetto dataset restituirà:
- La sequenza degli indici dei token della frase in input all’encoder;
- La sequenza degli indici dei token della frase in input al decoder;
- La sequenza degli indici dei token della frase output del decoder;
Differentemente da quanto visto prima, non lavoreremo esplitamente con one-hot vectors, ma utilizzeremo dei layer di embedding all’interno del modello. Definiamo l’oggetto dataset:
|
|
Definiamo l’oggetto training set usando delle trasformazioni composte che mettono in pipeline il tokenizer e la vocab transform di ciascuna lingua:
|
|
Visualizziamo un esempio di training:
|
|
(tensor([ 13, 26, 826, 8767, 9, 175, 15, 390, 4, 3]),
tensor([ 2, 5, 21, 123, 7721, 19, 763, 11, 352, 4]),
tensor([ 5, 21, 123, 7721, 19, 763, 11, 352, 4, 3]))
Domanda 11 Si analizzino le sequenze ottenute. I caratteri di inizio e fine sono corretti? |
Definiamo una funzione collate che faccia padding con il carattere di padding e dei dataloader:
|
|
Verifichiamo che il loader di training funzioni correttamente:
|
|
tensor([[ 5, 101, 325, ..., 1, 1, 1],
[ 5, 2148, 209, ..., 1, 1, 1],
[ 318, 113, 44, ..., 1, 1, 1],
...,
[ 524, 40, 616, ..., 1, 1, 1],
[ 13, 44, 143, ..., 1, 1, 1],
[ 60, 75, 52, ..., 1, 1, 1]])
Domanda 12 Si analizzino le sequenze. Il padding è stato applicato in maniera corretta? Qual è l’indice di padding? |
Definiremo adesso il nostro modello di rete neurale basato su due LSTM (un encoder e un decoder). Piuttosto che costruire i one-hot vector manualmente, utilizzeremo un layer di “embedding” che assocerà a ciascun id di token un vettore di rappresentazione. Definiamo il modello:
|
|
Instanziamo il modello e visualizziamo la shape dell’output per un possibile input:
|
|
torch.Size([256, 19, 15255])
Proviamo adesso ad effettuare una traduzione. Il risultato sarà una sequenza di token:
|
|
tensor([ 5, 101, 325, 98, 296, 4, 3, 1, 1, 1, 1, 1, 1, 1,
1, 1])
|
|
[3314, 6556, 13507, 5998, 947, 6591, 2331, 472, 5532, 6214]
Possiamo rimappare gli indici dei token nelle parole utilizzando il metodo lookup_tokens
della vocab_transform
:
|
|
Input: Tom vuole comprare un' auto . <eos> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad>
Output: charged floats cart 65 Those infallible lessons turn flinched literally expresses Franklin witnesses needs penny blow details cheeseburger womanizer interrupt admirable trustworthy General opponent carrots hardly Louvre Louvre troubles 'm gregarious puzzled revisions impostor Fishing 1949 coincidence somehow contact Shoot engines Transplants develops slowing prohibited gather chips Testing mangoes incorporated incorporated causing pond blank signaled artists ocarina manna bin causes ambiguity wheelchair daughters happening commanded fourths dimension insomnia sobbing asking ordinary interesting cart Hands stink volcanoes recognising 20th daughters drop bragging distracts distracts breathless straight put put Clark duke cheat android ability mess seltzer abhor awakened everyone Working Delegates interrupt
La traduzione non ha alcun senso perché il modello deve essere ancora allenato. Definiamo il modulo di PyTorch Lightning per effettuare il training:
|
|
Effettuiamo il training:
|
|
GPU available: True, used: True
TPU available: False, using: 0 TPU cores
IPU available: False, using: 0 IPUs
LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0,1,2,3]
| Name | Type | Params
-----------------------------------------------
0 | model | TranslatorLSTM | 8.2 M
1 | criterion | CrossEntropyLoss | 0
-----------------------------------------------
8.2 M Trainable params
0 Non-trainable params
8.2 M Total params
32.749 Total estimated model params size (MB)
Visualizziamo alcuni esempi di traduzione dal training set:
|
|
|
|
Input: Non ho alcuna esitazione a dire la verità . <eos>
Output: I have n't found the courage to stand up to stay .
---------------------------
Input: Tom sta danneggiando la sua reputazione . <eos>
Output: Tom 's wearing his age .
---------------------------
Input: Trovai la banana su una ripida strada di montagna . <eos>
Output: I have the red around the family almost every day .
---------------------------
Input: È piccola ? <eos>
Output: It 's our new life .
---------------------------
Input: Tom è nella squadra di lacrosse . <eos>
Output: It 's on our duty to our neighbor ?
---------------------------
Vediamo adesso qualche esempio dal test set:
|
|
Input: Io sono preparata al peggio . <eos>
Output: I 'm sitting on the same age .
---------------------------
Input: Imbrogliai . <eos>
Output: We 're going to have an alibi the dictionary .
---------------------------
Input: C' è un ragazzino che cammina con il suo cane . <eos>
Output: There 's a cat with my new book .
---------------------------
Input: Ha giocato a baseball ieri ? <eos>
Output: He taught you how to sing so fast ?
---------------------------
Input: Viene coltivato molto tabacco nella Carolina del Nord . <eos>
Output: You look very sick that would make the child as much as the past .
---------------------------
Come possiamo vedere, le traduzioni non sono accurate, ma il modello in genere produce frasi sensate. Ciò dipende in parte dalla semplicità del modello e in parte dal fatto che lo abbiamo allenato per poche epoche.
Esercizi
Esercizio 1 Si ripeta l’esperimento di training della RNN per la classificazione dei nomi utilizzando il modello di rete ricorrente GRU (https://pytorch.org/docs/stable/generated/torch.nn.GRU.html). Si ottimizzino gli iperparametri in modo da ottenere dei buoni risultati. Si confrontino i risultati ottenuti con quelli del modello RNN vanilla e della LSTM. |
Esercizio 2 Si costruisca un modello basato su una LSTM che generi frasi a partire da una parola iniziale. Il modello può essere basato su quello usato per generare nomi, con la differenza che in questo caso verranno generati i token della frase. Si allenino due versioni del modello nelle lingue italiano e inglese. |
Esercizio 3 Si alleni una GRU per la traduzione dall’inglese allo spagnolo. Si faccia riferimento a questo sito per il download dei dati: https://www.manythings.org/anki/. |
Esercizio 4 Si costruisca un modello LSTM capace di comprendere la lingua di una frase (si scelgano 10 lingue diverse). Il modello può essere basato su quello per la classificazione dei nomi, con la principale differenza che in questo caso verranno presi in input i token delle parole invece che i singoli caratteri del nome. Si faccia riferimento al seguente sito per il downoad dei dati: https://www.manythings.org/anki/. |
Esercizio 5 Si modifichi il modello costruito in precedenza per la generazione di frasi da una parola iniziale per funzionare in maniera multi-lingua. In maniera simile al modello per la predizione dei nomi, questo modello dovrà prendere in input l’ID della lingua oltre che la parola iniziale della frase. |
Esercizio 6 Allenare il modello di traduzione dall’italiano all’inglese per più epoche in modo da migliorare i risultati. |
References
- Documentazione di PyTorch. http://pytorch.org/docs/stable/index.html
- Documentazione di PyTorch Lightning. https://www.pytorchlightning.ai/
- Tutorial su predizione nomi in PyTorch. https://pytorch.org/tutorials/intermediate/char_rnn_classification_tutorial
- Tutorial su generazione nomi in PyTorch. https://pytorch.org/tutorials/intermediate/char_rnn_generation_tutorial.html
- Articolo di blog di Andrej Karpathy. http://karpathy.github.io/2015/05/21/rnn-effectiveness/