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

414 lines
21 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Redes Generativas\n",
"\n",
"As Redes Neuronais Recorrentes (RNNs) e suas variantes com células controladas, como as Células de Memória de Longo Prazo (LSTMs) e as Unidades Recorrentes Controladas (GRUs), proporcionaram um mecanismo para modelagem de linguagem, ou seja, elas podem aprender a ordem das palavras e fornecer previsões para a próxima palavra em uma sequência. Isso permite que utilizemos RNNs para **tarefas generativas**, como geração de texto comum, tradução automática e até mesmo legendagem de imagens.\n",
"\n",
"Na arquitetura de RNN que discutimos na unidade anterior, cada unidade RNN produzia o próximo estado oculto como saída. No entanto, também podemos adicionar outra saída a cada unidade recorrente, o que nos permitiria gerar uma **sequência** (que tem o mesmo comprimento da sequência original). Além disso, podemos usar unidades RNN que não aceitam uma entrada em cada passo, mas apenas recebem um vetor de estado inicial e, em seguida, produzem uma sequência de saídas.\n",
"\n",
"Neste notebook, vamos focar em modelos generativos simples que nos ajudam a gerar texto. Para simplificar, vamos construir uma **rede ao nível de caracteres**, que gera texto letra por letra. Durante o treino, precisamos pegar um corpus de texto e dividi-lo em sequências de letras.\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": [
"## Construir um vocabulário de caracteres\n",
"\n",
"Para criar uma rede generativa a nível de caracteres, é necessário dividir o texto em caracteres individuais em vez de palavras. Isto pode ser feito ao definir um tokenizador diferente:\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": [
"Vamos ver o exemplo de como podemos codificar o texto do nosso conjunto de dados:\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": [
"## Treinar uma RNN generativa\n",
"\n",
"A forma como iremos treinar a RNN para gerar texto é a seguinte. A cada passo, iremos pegar uma sequência de caracteres com comprimento `nchars` e pedir à rede que gere o próximo caractere de saída para cada caractere de entrada:\n",
"\n",
"![Imagem mostrando um exemplo de geração de RNN com a palavra 'HELLO'.](../../../../../lessons/5-NLP/17-GenerativeNetworks/images/rnn-generate.png)\n",
"\n",
"Dependendo do cenário específico, também podemos querer incluir alguns caracteres especiais, como *fim de sequência* `<eos>`. No nosso caso, queremos apenas treinar a rede para geração contínua de texto, portanto, iremos fixar o tamanho de cada sequência para ser igual a `nchars` tokens. Consequentemente, cada exemplo de treino consistirá em `nchars` entradas e `nchars` saídas (que são a sequência de entrada deslocada um símbolo para a esquerda). O minibatch consistirá em várias dessas sequências.\n",
"\n",
"A forma como iremos gerar os minibatches será pegar cada texto de notícias com comprimento `l` e gerar todas as combinações possíveis de entrada-saída a partir dele (haverá `l-nchars` dessas combinações). Elas formarão um minibatch, e o tamanho dos minibatches será diferente em cada passo de treino.\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": [
"Agora vamos definir a rede geradora. Pode ser baseada em qualquer célula recorrente que discutimos na unidade anterior (simples, LSTM ou GRU). No nosso exemplo, utilizaremos LSTM.\n",
"\n",
"Como a rede recebe caracteres como entrada e o tamanho do vocabulário é relativamente pequeno, não precisamos de uma camada de embedding; a entrada codificada em one-hot pode ir diretamente para a célula LSTM. No entanto, como passamos números de caracteres como entrada, é necessário codificá-los em one-hot antes de enviá-los para o LSTM. Isto é feito ao chamar a função `one_hot` durante a passagem `forward`. O codificador de saída será uma camada linear que converterá o estado oculto em uma saída codificada em 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 o treino, queremos ser capazes de amostrar texto gerado. Para isso, iremos definir a função `generate`, que produzirá uma string de saída com comprimento `size`, começando pela string inicial `start`.\n",
"\n",
"O funcionamento é o seguinte. Primeiro, passamos a string inicial completa pela rede e obtemos o estado de saída `s` e o próximo carácter previsto `out`. Como `out` está codificado em one-hot, utilizamos `argmax` para obter o índice do carácter `nc` no vocabulário e usamos `itos` para identificar o carácter real e adicioná-lo à lista resultante de caracteres `chars`. Este processo de gerar um carácter é repetido `size` vezes para gerar o número necessário de caracteres.\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": [
"Agora vamos começar o treino! O ciclo de treino é quase o mesmo que em todos os nossos exemplos anteriores, mas, em vez de precisão, imprimimos texto gerado amostrado a cada 1000 épocas.\n",
"\n",
"É necessário prestar atenção especial à forma como calculamos a perda. Precisamos calcular a perda com base na saída codificada em one-hot `out` e no texto esperado `text_out`, que é a lista de índices de caracteres. Felizmente, a função `cross_entropy` espera como primeiro argumento a saída não normalizada da rede e, como segundo argumento, o número da classe, que é exatamente o que temos. Além disso, realiza automaticamente a média em relação ao tamanho do minibatch.\n",
"\n",
"Também limitamos o treino por amostras definidas em `samples_to_train`, para não esperar demasiado tempo. Incentivamos-te a experimentar e tentar treinos mais longos, possivelmente por várias épocas (nesse caso, seria necessário criar outro ciclo em torno deste código).\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": [
"Este exemplo já gera um texto bastante bom, mas pode ser melhorado de várias formas:\n",
"\n",
"* **Melhor geração de minibatches**. A forma como preparamos os dados para o treino foi gerar um minibatch a partir de uma única amostra. Isto não é ideal, porque os minibatches têm tamanhos diferentes e alguns deles nem sequer podem ser gerados, porque o texto é menor do que `nchars`. Além disso, minibatches pequenos não utilizam suficientemente a GPU. Seria mais sensato obter um grande bloco de texto de todas as amostras, depois gerar todos os pares de entrada-saída, embaralhá-los e criar minibatches de tamanho igual.\n",
"\n",
"* **LSTM multicamada**. Faz sentido experimentar 2 ou 3 camadas de células LSTM. Como mencionámos na unidade anterior, cada camada de LSTM extrai certos padrões do texto e, no caso de um gerador a nível de caracteres, podemos esperar que os níveis inferiores de LSTM sejam responsáveis por extrair sílabas, e os níveis superiores por palavras e combinações de palavras. Isto pode ser implementado facilmente passando o parâmetro número-de-camadas ao construtor de LSTM.\n",
"\n",
"* Também pode querer experimentar com **unidades GRU** e ver quais apresentam melhor desempenho, bem como com **diferentes tamanhos de camadas ocultas**. Uma camada oculta demasiado grande pode resultar em overfitting (por exemplo, a rede aprenderá o texto exato), enquanto um tamanho menor pode não produzir bons resultados.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Geração de texto suave e temperatura\n",
"\n",
"Na definição anterior de `generate`, estávamos sempre a escolher o carácter com a maior probabilidade como o próximo carácter no texto gerado. Isto resultava no facto de o texto frequentemente \"ciclar\" entre as mesmas sequências de caracteres repetidamente, como neste exemplo: \n",
"```\n",
"today of the second the company and a second the company ...\n",
"```\n",
"\n",
"No entanto, se analisarmos a distribuição de probabilidades para o próximo carácter, pode acontecer que a diferença entre algumas das probabilidades mais altas não seja muito grande, por exemplo, um carácter pode ter uma probabilidade de 0,2, outro de 0,19, etc. Por exemplo, ao procurar o próximo carácter na sequência '*play*', o próximo carácter pode ser igualmente um espaço ou **e** (como na palavra *player*).\n",
"\n",
"Isto leva-nos à conclusão de que nem sempre é \"justo\" selecionar o carácter com maior probabilidade, porque escolher o segundo mais provável ainda pode levar-nos a um texto significativo. É mais sensato **amostrar** caracteres a partir da distribuição de probabilidades fornecida pela saída da rede.\n",
"\n",
"Esta amostragem pode ser feita utilizando a função `multinomial`, que implementa a chamada **distribuição multinomial**. Uma função que implementa esta geração de texto **suave** está definida abaixo:\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": [
"Introduzimos mais um parâmetro chamado **temperatura**, que é usado para indicar o quão rigorosamente devemos aderir à maior probabilidade. Se a temperatura for 1.0, fazemos uma amostragem multinomial justa, e quando a temperatura vai para infinito - todas as probabilidades tornam-se iguais, e selecionamos aleatoriamente o próximo caractere. No exemplo abaixo, podemos observar que o texto torna-se sem sentido quando aumentamos demasiado a temperatura, e assemelha-se a um texto \"ciclado\" gerado rigidamente quando se aproxima de 0.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n---\n\n**Aviso Legal**: \nEste documento foi traduzido utilizando o serviço de tradução por IA [Co-op Translator](https://github.com/Azure/co-op-translator). Embora nos esforcemos para garantir a precisão, é importante notar que traduções automáticas podem conter erros ou imprecisões. O documento original na sua língua nativa deve ser considerado a fonte autoritária. Para informações críticas, recomenda-se a tradução profissional realizada por humanos. Não nos responsabilizamos por quaisquer mal-entendidos ou interpretações incorretas decorrentes da utilização desta tradução.\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-31T11:56:23+00:00",
"source_file": "lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb",
"language_code": "pt"
}
},
"nbformat": 4,
"nbformat_minor": 4
}