AI-For-Beginners/translations/it/lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb

411 lines
22 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Reti generative\n",
"\n",
"Le Reti Neurali Ricorrenti (RNN) e le loro varianti con celle gated, come le celle Long Short Term Memory (LSTM) e le Gated Recurrent Units (GRU), hanno fornito un meccanismo per il modellamento del linguaggio, ovvero possono apprendere l'ordine delle parole e fornire previsioni per la parola successiva in una sequenza. Questo ci permette di utilizzare le RNN per **compiti generativi**, come la generazione di testo ordinario, la traduzione automatica e persino la descrizione di immagini.\n",
"\n",
"Nell'architettura RNN che abbiamo discusso nell'unità precedente, ogni unità RNN produceva il prossimo stato nascosto come output. Tuttavia, possiamo anche aggiungere un altro output a ciascuna unità ricorrente, che ci consentirebbe di generare una **sequenza** (che ha la stessa lunghezza della sequenza originale). Inoltre, possiamo utilizzare unità RNN che non accettano un input a ogni passo, ma prendono solo un vettore di stato iniziale e poi producono una sequenza di output.\n",
"\n",
"In questo notebook, ci concentreremo su modelli generativi semplici che ci aiutano a generare testo. Per semplicità, costruiamo una rete **a livello di carattere**, che genera testo lettera per lettera. Durante l'addestramento, dobbiamo prendere un corpus di testo e suddividerlo in sequenze di lettere.\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Loading dataset...\n",
"Building vocab...\n"
]
}
],
"source": [
"import torch\n",
"import torchtext\n",
"import numpy as np\n",
"from torchnlp import *\n",
"train_dataset,test_dataset,classes,vocab = load_dataset()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Costruire un vocabolario di caratteri\n",
"\n",
"Per creare una rete generativa a livello di caratteri, è necessario suddividere il testo in singoli caratteri anziché in parole. Questo può essere fatto definendo un tokenizer diverso:\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Vocabulary size = 82\n",
"Encoding of 'a' is 1\n",
"Character with code 13 is c\n"
]
}
],
"source": [
"def char_tokenizer(words):\n",
" return list(words) #[word for word in words]\n",
"\n",
"counter = collections.Counter()\n",
"for (label, line) in train_dataset:\n",
" counter.update(char_tokenizer(line))\n",
"vocab = torchtext.vocab.vocab(counter)\n",
"\n",
"vocab_size = len(vocab)\n",
"print(f\"Vocabulary size = {vocab_size}\")\n",
"print(f\"Encoding of 'a' is {vocab.get_stoi()['a']}\")\n",
"print(f\"Character with code 13 is {vocab.get_itos()[13]}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Vediamo l'esempio di come possiamo codificare il testo dal nostro dataset:\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"tensor([ 0, 1, 2, 2, 3, 4, 5, 6, 3, 7, 8, 1, 9, 10, 3, 11, 2, 1,\n",
" 12, 3, 7, 1, 13, 14, 3, 15, 16, 5, 17, 3, 5, 18, 8, 3, 7, 2,\n",
" 1, 13, 14, 3, 19, 20, 8, 21, 5, 8, 9, 10, 22, 3, 20, 8, 21, 5,\n",
" 8, 9, 10, 3, 23, 3, 4, 18, 17, 9, 5, 23, 10, 8, 2, 2, 8, 9,\n",
" 10, 24, 3, 0, 1, 2, 2, 3, 4, 5, 9, 8, 8, 5, 25, 10, 3, 26,\n",
" 12, 27, 16, 26, 2, 27, 16, 28, 29, 30, 1, 16, 26, 3, 17, 31, 3, 21,\n",
" 2, 5, 9, 1, 23, 13, 32, 16, 27, 13, 10, 24, 3, 1, 9, 8, 3, 10,\n",
" 8, 8, 27, 16, 28, 3, 28, 9, 8, 8, 16, 3, 1, 28, 1, 27, 16, 6])"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def enc(x):\n",
" return torch.LongTensor(encode(x,voc=vocab,tokenizer=char_tokenizer))\n",
"\n",
"enc(train_dataset[0][1])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Addestrare un RNN generativo\n",
"\n",
"Il modo in cui addestreremo l'RNN per generare testo è il seguente. A ogni passo, prenderemo una sequenza di caratteri di lunghezza `nchars` e chiederemo alla rete di generare il carattere successivo per ogni carattere di input:\n",
"\n",
"![Immagine che mostra un esempio di generazione RNN della parola 'HELLO'.](../../../../../translated_images/it/rnn-generate.56c54afb52f9781d.webp)\n",
"\n",
"A seconda dello scenario specifico, potremmo anche voler includere alcuni caratteri speciali, come *fine-sequenza* `<eos>`. Nel nostro caso, vogliamo semplicemente addestrare la rete per una generazione di testo continua, quindi fisseremo la dimensione di ogni sequenza uguale a `nchars` token. Di conseguenza, ogni esempio di addestramento sarà composto da `nchars` input e `nchars` output (che sono la sequenza di input spostata di un simbolo a sinistra). Il minibatch sarà composto da diverse di queste sequenze.\n",
"\n",
"Il modo in cui genereremo i minibatch sarà prendere ogni testo di notizia di lunghezza `l` e generare tutte le possibili combinazioni input-output da esso (ci saranno `l-nchars` tali combinazioni). Queste formeranno un minibatch, e la dimensione dei minibatch sarà diversa a ogni passo di addestramento.\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(tensor([[ 0, 1, 2, ..., 28, 29, 30],\n",
" [ 1, 2, 2, ..., 29, 30, 1],\n",
" [ 2, 2, 3, ..., 30, 1, 16],\n",
" ...,\n",
" [20, 8, 21, ..., 1, 28, 1],\n",
" [ 8, 21, 5, ..., 28, 1, 27],\n",
" [21, 5, 8, ..., 1, 27, 16]]),\n",
" tensor([[ 1, 2, 2, ..., 29, 30, 1],\n",
" [ 2, 2, 3, ..., 30, 1, 16],\n",
" [ 2, 3, 4, ..., 1, 16, 26],\n",
" ...,\n",
" [ 8, 21, 5, ..., 28, 1, 27],\n",
" [21, 5, 8, ..., 1, 27, 16],\n",
" [ 5, 8, 9, ..., 27, 16, 6]]))"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"nchars = 100\n",
"\n",
"def get_batch(s,nchars=nchars):\n",
" ins = torch.zeros(len(s)-nchars,nchars,dtype=torch.long,device=device)\n",
" outs = torch.zeros(len(s)-nchars,nchars,dtype=torch.long,device=device)\n",
" for i in range(len(s)-nchars):\n",
" ins[i] = enc(s[i:i+nchars])\n",
" outs[i] = enc(s[i+1:i+nchars+1])\n",
" return ins,outs\n",
"\n",
"get_batch(train_dataset[0][1])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Ora definiamo la rete generativa. Può essere basata su qualsiasi cella ricorrente di cui abbiamo discusso nell'unità precedente (semplice, LSTM o GRU). Nel nostro esempio utilizzeremo LSTM.\n",
"\n",
"Poiché la rete prende caratteri come input e la dimensione del vocabolario è piuttosto piccola, non abbiamo bisogno di uno strato di embedding; l'input codificato in one-hot può essere inviato direttamente alla cella LSTM. Tuttavia, poiché passiamo numeri di caratteri come input, dobbiamo codificarli in one-hot prima di passarli all'LSTM. Questo viene fatto chiamando la funzione `one_hot` durante il passaggio `forward`. L'encoder di output sarà uno strato lineare che convertirà lo stato nascosto in un output codificato in one-hot.\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"class LSTMGenerator(torch.nn.Module):\n",
" def __init__(self, vocab_size, hidden_dim):\n",
" super().__init__()\n",
" self.rnn = torch.nn.LSTM(vocab_size,hidden_dim,batch_first=True)\n",
" self.fc = torch.nn.Linear(hidden_dim, vocab_size)\n",
"\n",
" def forward(self, x, s=None):\n",
" x = torch.nn.functional.one_hot(x,vocab_size).to(torch.float32)\n",
" x,s = self.rnn(x,s)\n",
" return self.fc(x),s"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Durante l'addestramento, vogliamo essere in grado di campionare testo generato. Per fare ciò, definiremo la funzione `generate` che produrrà una stringa di output di lunghezza `size`, partendo dalla stringa iniziale `start`.\n",
"\n",
"Il funzionamento è il seguente. Per prima cosa, passeremo l'intera stringa iniziale attraverso la rete, ottenendo lo stato di output `s` e il prossimo carattere previsto `out`. Poiché `out` è codificato in one-hot, utilizziamo `argmax` per ottenere l'indice del carattere `nc` nel vocabolario e usiamo `itos` per determinare il carattere effettivo, aggiungendolo alla lista risultante di caratteri `chars`. Questo processo di generazione di un carattere viene ripetuto `size` volte per generare il numero richiesto di caratteri.\n"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
"def generate(net,size=100,start='today '):\n",
" chars = list(start)\n",
" out, s = net(enc(chars).view(1,-1).to(device))\n",
" for i in range(size):\n",
" nc = torch.argmax(out[0][-1])\n",
" chars.append(vocab.get_itos()[nc])\n",
" out, s = net(nc.view(1,-1),s)\n",
" return ''.join(chars)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Ora iniziamo l'addestramento! Il ciclo di addestramento è quasi identico a quello di tutti i nostri esempi precedenti, ma invece di mostrare l'accuratezza, stampiamo il testo generato campionato ogni 1000 epoche.\n",
"\n",
"Bisogna prestare particolare attenzione al modo in cui calcoliamo la perdita. Dobbiamo calcolare la perdita utilizzando l'output codificato in one-hot `out` e il testo previsto `text_out`, che è la lista degli indici dei caratteri. Fortunatamente, la funzione `cross_entropy` si aspetta come primo argomento l'output non normalizzato della rete e come secondo argomento il numero della classe, che è esattamente ciò che abbiamo. Inoltre, esegue automaticamente la media sulla dimensione del minibatch.\n",
"\n",
"Limitiamo anche l'addestramento a un numero di campioni pari a `samples_to_train`, per evitare di aspettare troppo a lungo. Ti incoraggiamo a sperimentare e provare un addestramento più lungo, possibilmente per diverse epoche (in tal caso, dovresti creare un altro ciclo attorno a questo codice).\n"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Current loss = 4.398899078369141\n",
"today sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr s\n",
"Current loss = 2.161320447921753\n",
"today and to the tor to to the tor to to the tor to to the tor to to the tor to to the tor to to the tor t\n",
"Current loss = 1.6722588539123535\n",
"today and the court to the could to the could to the could to the could to the could to the could to the c\n",
"Current loss = 2.423795223236084\n",
"today and a second to the conternation of the conternation of the conternation of the conternation of the \n",
"Current loss = 1.702607274055481\n",
"today and the company to the company to the company to the company to the company to the company to the co\n",
"Current loss = 1.692358136177063\n",
"today and the company to the company to the company to the company to the company to the company to the co\n",
"Current loss = 1.9722288846969604\n",
"today and the control the control the control the control the control the control the control the control \n",
"Current loss = 1.8705692291259766\n",
"today and the second to the second to the second to the second to the second to the second to the second t\n",
"Current loss = 1.7626899480819702\n",
"today and a security and a security and a security and a security and a security and a security and a secu\n",
"Current loss = 1.5574463605880737\n",
"today and the company and the company and the company and the company and the company and the company and \n",
"Current loss = 1.5620026588439941\n",
"today and the be that the be the be that the be the be that the be the be that the be the be that the be t\n"
]
}
],
"source": [
"net = LSTMGenerator(vocab_size,64).to(device)\n",
"\n",
"samples_to_train = 10000\n",
"optimizer = torch.optim.Adam(net.parameters(),0.01)\n",
"loss_fn = torch.nn.CrossEntropyLoss()\n",
"net.train()\n",
"for i,x in enumerate(train_dataset):\n",
" # x[0] is class label, x[1] is text\n",
" if len(x[1])-nchars<10:\n",
" continue\n",
" samples_to_train-=1\n",
" if not samples_to_train: break\n",
" text_in, text_out = get_batch(x[1])\n",
" optimizer.zero_grad()\n",
" out,s = net(text_in)\n",
" loss = torch.nn.functional.cross_entropy(out.view(-1,vocab_size),text_out.flatten()) #cross_entropy(out,labels)\n",
" loss.backward()\n",
" optimizer.step()\n",
" if i%1000==0:\n",
" print(f\"Current loss = {loss.item()}\")\n",
" print(generate(net))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Questo esempio genera già un testo piuttosto buono, ma può essere ulteriormente migliorato in diversi modi:\n",
"* **Migliore generazione di minibatch**. Il modo in cui abbiamo preparato i dati per l'addestramento è stato generare un minibatch da un singolo campione. Questo non è ideale, perché i minibatch hanno dimensioni diverse e alcuni di essi non possono nemmeno essere generati, poiché il testo è più piccolo di `nchars`. Inoltre, minibatch di piccole dimensioni non sfruttano sufficientemente la GPU. Sarebbe più saggio ottenere un grande blocco di testo da tutti i campioni, poi generare tutte le coppie input-output, mescolarle e generare minibatch di dimensioni uguali.\n",
"* **LSTM multilivello**. Ha senso provare 2 o 3 livelli di celle LSTM. Come abbiamo menzionato nell'unità precedente, ogni livello di LSTM estrae determinati schemi dal testo e, nel caso di un generatore a livello di carattere, possiamo aspettarci che il livello inferiore di LSTM sia responsabile dell'estrazione delle sillabe, mentre i livelli superiori - delle parole e delle combinazioni di parole. Questo può essere implementato semplicemente passando il parametro del numero di livelli al costruttore di LSTM.\n",
"* Potresti anche voler sperimentare con **unità GRU** per vedere quali offrono prestazioni migliori, e con **dimensioni diverse dei livelli nascosti**. Un livello nascosto troppo grande potrebbe portare a overfitting (ad esempio, la rete apprenderà il testo esatto), mentre una dimensione troppo piccola potrebbe non produrre risultati soddisfacenti.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Generazione di testo morbido e temperatura\n",
"\n",
"Nella precedente definizione di `generate`, sceglievamo sempre il carattere con la probabilità più alta come prossimo carattere nel testo generato. Questo portava spesso al fatto che il testo \"ciclasse\" tra le stesse sequenze di caratteri ripetutamente, come in questo esempio:\n",
"```\n",
"today of the second the company and a second the company ...\n",
"```\n",
"\n",
"Tuttavia, se osserviamo la distribuzione di probabilità per il prossimo carattere, potrebbe accadere che la differenza tra alcune delle probabilità più alte non sia così grande, ad esempio un carattere potrebbe avere una probabilità di 0,2, un altro di 0,19, ecc. Per esempio, quando cerchiamo il prossimo carattere nella sequenza '*play*', il carattere successivo potrebbe essere ugualmente uno spazio o **e** (come nella parola *player*).\n",
"\n",
"Questo ci porta alla conclusione che non è sempre \"giusto\" selezionare il carattere con la probabilità più alta, perché scegliere il secondo più probabile potrebbe comunque portarci a un testo significativo. È più saggio **campionare** i caratteri dalla distribuzione di probabilità fornita dall'output della rete.\n",
"\n",
"Questo campionamento può essere effettuato utilizzando la funzione `multinomial`, che implementa la cosiddetta **distribuzione multinomiale**. Una funzione che implementa questa generazione di testo **morbida** è definita di seguito:\n"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"--- Temperature = 0.3\n",
"Today and a company and complete an all the land the restrational the as a security and has provers the pay to and a report and the computer in the stand has filities and working the law the stations for a company and with the company and the final the first company and refight of the state and and workin\n",
"\n",
"--- Temperature = 0.8\n",
"Today he oniis its first to Aus bomblaties the marmation a to manan boogot that pirate assaid a relaid their that goverfin the the Cappets Ecrotional Assonia Cition targets it annight the w scyments Blamity #39;s TVeer Diercheg Reserals fran envyuil that of ster said access what succers of Dour-provelith\n",
"\n",
"--- Temperature = 1.0\n",
"Today holy they a 11 will meda a toket subsuaties, engins for Chanos, they's has stainger past to opening orital his thempting new Nattona was al innerforder advan-than #36;s night year his religuled talitatian what the but with Wednesday to Justment will wemen of Mark CCC Camp as Timed Nae wome a leaders\n",
"\n",
"--- Temperature = 1.3\n",
"Today gpone 2.5 fech atcusion poor cocles toparsdorM.cht Line Pamage put 43 his calt lowed to the book, that has authh-the silia rruch ailing to'ory andhes beutirsimi- Aefffive heading offil an auf eacklets is charged evis, Gunymy oy) Mony has it after-sloythyor loveId out filme, the Natabl -Najuntaxiggs \n",
"\n",
"--- Temperature = 1.8\n",
"Today plary, P.slan chly\\401 mardregationly #39;t 8.1Mide) closes ,filtcon alfly playin roven!\\grea.-QFBEP: Iss onfarchQ/itilia CCf Zivesigntwasta orce.-Peul-aw.uicrin of fuglinfsut aftaningwo, MIEX awayew Aice Woiduar Corvagiugge oppo esig ThusBratourid canthly-RyI.co lagitems\\eexciaishes.conBabntusmor I\n",
"\n"
]
}
],
"source": [
"def generate_soft(net,size=100,start='today ',temperature=1.0):\n",
" chars = list(start)\n",
" out, s = net(enc(chars).view(1,-1).to(device))\n",
" for i in range(size):\n",
" #nc = torch.argmax(out[0][-1])\n",
" out_dist = out[0][-1].div(temperature).exp()\n",
" nc = torch.multinomial(out_dist,1)[0]\n",
" chars.append(vocab.get_itos()[nc])\n",
" out, s = net(nc.view(1,-1),s)\n",
" return ''.join(chars)\n",
" \n",
"for i in [0.3,0.8,1.0,1.3,1.8]:\n",
" print(f\"--- Temperature = {i}\\n{generate_soft(net,size=300,start='Today ',temperature=i)}\\n\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Abbiamo introdotto un altro parametro chiamato **temperatura**, che viene utilizzato per indicare quanto strettamente dovremmo aderire alla probabilità più alta. Se la temperatura è 1.0, facciamo un campionamento multinomiale equo, e quando la temperatura va all'infinito - tutte le probabilità diventano uguali, e selezioniamo casualmente il prossimo carattere. Nell'esempio qui sotto possiamo osservare che il testo diventa privo di senso quando aumentiamo troppo la temperatura, e somiglia a un testo \"ciclato\" generato rigidamente quando si avvicina a 0.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n---\n\n**Disclaimer**: \nQuesto documento è stato tradotto utilizzando il servizio di traduzione AI [Co-op Translator](https://github.com/Azure/co-op-translator). Sebbene ci impegniamo per garantire l'accuratezza, si prega di notare che le traduzioni automatiche possono contenere errori o imprecisioni. Il documento originale nella sua lingua nativa dovrebbe essere considerato la fonte autorevole. Per informazioni critiche, si raccomanda una traduzione professionale effettuata da un esperto umano. Non siamo responsabili per eventuali fraintendimenti o interpretazioni errate derivanti dall'uso di questa traduzione.\n"
]
}
],
"metadata": {
"interpreter": {
"hash": "16af2a8bbb083ea23e5e41c7f5787656b2ce26968575d8763f2c4b17f9cd711f"
},
"kernelspec": {
"display_name": "Python 3.8.12 ('py38')",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.12"
},
"coopTranslator": {
"original_hash": "7673cd150d96c74c6d6011460094efb4",
"translation_date": "2025-08-28T14:06:41+00:00",
"source_file": "lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb",
"language_code": "it"
}
},
"nbformat": 4,
"nbformat_minor": 4
}