AI-For-Beginners/translations/pt/lessons/5-NLP/16-RNN/RNNPyTorch.ipynb

479 lines
24 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Redes neuronais recorrentes\n",
"\n",
"No módulo anterior, utilizámos representações semânticas ricas de texto e um classificador linear simples sobre as embeddings. O que esta arquitetura faz é capturar o significado agregado das palavras numa frase, mas não considera a **ordem** das palavras, porque a operação de agregação sobre as embeddings removeu esta informação do texto original. Como estes modelos não conseguem modelar a ordem das palavras, não conseguem resolver tarefas mais complexas ou ambíguas, como geração de texto ou resposta a perguntas.\n",
"\n",
"Para capturar o significado de uma sequência de texto, precisamos de usar outra arquitetura de rede neural, chamada **rede neural recorrente**, ou RNN. Numa RNN, passamos a nossa frase pela rede, um símbolo de cada vez, e a rede produz um **estado**, que depois passamos novamente para a rede juntamente com o próximo símbolo.\n",
"\n",
"Dada a sequência de tokens de entrada $X_0,\\dots,X_n$, a RNN cria uma sequência de blocos de rede neural e treina esta sequência de ponta a ponta utilizando retropropagação. Cada bloco de rede recebe um par $(X_i,S_i)$ como entrada e produz $S_{i+1}$ como resultado. O estado final $S_n$ ou a saída $X_n$ é enviado para um classificador linear para produzir o resultado. Todos os blocos da rede partilham os mesmos pesos e são treinados de ponta a ponta utilizando uma única passagem de retropropagação.\n",
"\n",
"Como os vetores de estado $S_0,\\dots,S_n$ são passados pela rede, esta consegue aprender as dependências sequenciais entre palavras. Por exemplo, quando a palavra *não* aparece em algum lugar da sequência, a rede pode aprender a negar certos elementos dentro do vetor de estado, resultando em negação.\n",
"\n",
"> Como os pesos de todos os blocos da RNN na imagem são partilhados, a mesma imagem pode ser representada como um único bloco (à direita) com um loop de feedback recorrente, que passa o estado de saída da rede de volta para a entrada.\n",
"\n",
"Vamos ver como as redes neuronais recorrentes podem ajudar-nos a classificar o nosso conjunto de dados de notícias.\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",
"from torchnlp import *\n",
"train_dataset, test_dataset, classes, vocab = load_dataset()\n",
"vocab_size = len(vocab)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Classificador RNN Simples\n",
"\n",
"No caso de um RNN simples, cada unidade recorrente é uma rede linear simples, que recebe um vetor de entrada concatenado e um vetor de estado, e produz um novo vetor de estado. O PyTorch representa esta unidade com a classe `RNNCell`, e uma rede composta por estas células - como a camada `RNN`.\n",
"\n",
"Para definir um classificador RNN, primeiro aplicaremos uma camada de embedding para reduzir a dimensionalidade do vocabulário de entrada, e depois utilizaremos uma camada RNN sobre esta:\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"class RNNClassifier(torch.nn.Module):\n",
" def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):\n",
" super().__init__()\n",
" self.hidden_dim = hidden_dim\n",
" self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n",
" self.rnn = torch.nn.RNN(embed_dim,hidden_dim,batch_first=True)\n",
" self.fc = torch.nn.Linear(hidden_dim, num_class)\n",
"\n",
" def forward(self, x):\n",
" batch_size = x.size(0)\n",
" x = self.embedding(x)\n",
" x,h = self.rnn(x)\n",
" return self.fc(x.mean(dim=1))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> **Nota:** Aqui utilizamos uma camada de embedding não treinada para simplificar, mas para obter resultados ainda melhores, podemos usar uma camada de embedding pré-treinada com embeddings do Word2Vec ou GloVe, conforme descrito na unidade anterior. Para uma melhor compreensão, pode ser útil adaptar este código para funcionar com embeddings pré-treinados.\n",
"\n",
"No nosso caso, utilizaremos um carregador de dados com preenchimento (padded data loader), de forma que cada lote terá um número de sequências preenchidas com o mesmo comprimento. A camada RNN receberá a sequência de tensores de embedding e produzirá dois outputs:\n",
"* $x$ é uma sequência de outputs das células RNN em cada passo\n",
"* $h$ é o estado oculto final para o último elemento da sequência\n",
"\n",
"De seguida, aplicamos um classificador linear totalmente conectado para obter o número de classes.\n",
"\n",
"> **Nota:** As RNNs são bastante difíceis de treinar, porque, uma vez que as células RNN são desenroladas ao longo do comprimento da sequência, o número resultante de camadas envolvidas na retropropagação é bastante elevado. Por isso, é necessário selecionar uma taxa de aprendizagem pequena e treinar a rede num conjunto de dados maior para obter bons resultados. Este processo pode demorar bastante tempo, pelo que é preferível utilizar uma GPU.\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"3200: acc=0.3090625\n",
"6400: acc=0.38921875\n",
"9600: acc=0.4590625\n",
"12800: acc=0.511953125\n",
"16000: acc=0.5506875\n",
"19200: acc=0.57921875\n",
"22400: acc=0.6070089285714285\n",
"25600: acc=0.6304296875\n",
"28800: acc=0.6484027777777778\n",
"32000: acc=0.66509375\n",
"35200: acc=0.6790056818181818\n",
"38400: acc=0.6929166666666666\n",
"41600: acc=0.7035817307692308\n",
"44800: acc=0.7137276785714286\n",
"48000: acc=0.72225\n",
"51200: acc=0.73001953125\n",
"54400: acc=0.7372794117647059\n",
"57600: acc=0.7436631944444444\n",
"60800: acc=0.7503947368421052\n",
"64000: acc=0.75634375\n",
"67200: acc=0.7615773809523809\n",
"70400: acc=0.7662642045454545\n",
"73600: acc=0.7708423913043478\n",
"76800: acc=0.7751822916666666\n",
"80000: acc=0.7790625\n",
"83200: acc=0.7825\n",
"86400: acc=0.7858564814814815\n",
"89600: acc=0.7890513392857142\n",
"92800: acc=0.7920474137931034\n",
"96000: acc=0.7952708333333334\n",
"99200: acc=0.7982258064516129\n",
"102400: acc=0.80099609375\n",
"105600: acc=0.8037594696969697\n",
"108800: acc=0.8060569852941176\n"
]
}
],
"source": [
"train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=padify, shuffle=True)\n",
"net = RNNClassifier(vocab_size,64,32,len(classes)).to(device)\n",
"train_epoch(net,train_loader, lr=0.001)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Long Short Term Memory (LSTM)\n",
"\n",
"Um dos principais problemas das RNNs clássicas é o chamado problema de **gradientes que desaparecem**. Como as RNNs são treinadas de ponta a ponta em uma única passagem de retropropagação, têm dificuldade em propagar o erro para as primeiras camadas da rede, e, por isso, a rede não consegue aprender relações entre tokens distantes. Uma das formas de evitar este problema é introduzir **gestão explícita de estado** utilizando os chamados **gates**. Existem duas arquiteturas mais conhecidas deste tipo: **Long Short Term Memory** (LSTM) e **Gated Relay Unit** (GRU).\n",
"\n",
"![Imagem mostrando um exemplo de célula de memória de longo curto prazo](../../../../../lessons/5-NLP/16-RNN/images/long-short-term-memory-cell.svg)\n",
"\n",
"A rede LSTM é organizada de forma semelhante à RNN, mas existem dois estados que são passados de camada para camada: o estado atual $c$ e o vetor oculto $h$. Em cada unidade, o vetor oculto $h_i$ é concatenado com a entrada $x_i$, e eles controlam o que acontece ao estado $c$ através dos **gates**. Cada gate é uma rede neural com ativação sigmoide (saída no intervalo $[0,1]$), que pode ser vista como uma máscara bit a bit quando multiplicada pelo vetor de estado. Existem os seguintes gates (da esquerda para a direita na imagem acima):\n",
"* **forget gate** utiliza o vetor oculto e determina quais componentes do vetor $c$ precisamos esquecer e quais devemos manter.\n",
"* **input gate** extrai algumas informações da entrada e do vetor oculto e insere-as no estado.\n",
"* **output gate** transforma o estado através de uma camada linear com ativação $\\tanh$, e depois seleciona alguns dos seus componentes usando o vetor oculto $h_i$ para produzir o novo estado $c_{i+1}$.\n",
"\n",
"Os componentes do estado $c$ podem ser vistos como algumas bandeiras que podem ser ativadas ou desativadas. Por exemplo, quando encontramos o nome *Alice* numa sequência, podemos assumir que se refere a uma personagem feminina e ativar a bandeira no estado indicando que temos um substantivo feminino na frase. Quando mais tarde encontramos a expressão *e Tom*, ativamos a bandeira indicando que temos um substantivo no plural. Assim, manipulando o estado, podemos supostamente acompanhar as propriedades gramaticais das partes da frase.\n",
"\n",
"> **Note**: Um excelente recurso para entender os detalhes internos do LSTM é este ótimo artigo [Understanding LSTM Networks](https://colah.github.io/posts/2015-08-Understanding-LSTMs/) de Christopher Olah.\n",
"\n",
"Embora a estrutura interna da célula LSTM possa parecer complexa, o PyTorch oculta esta implementação dentro da classe `LSTMCell` e fornece o objeto `LSTM` para representar toda a camada LSTM. Assim, a implementação de um classificador LSTM será bastante semelhante à RNN simples que vimos acima:\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"class LSTMClassifier(torch.nn.Module):\n",
" def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):\n",
" super().__init__()\n",
" self.hidden_dim = hidden_dim\n",
" self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n",
" self.embedding.weight.data = torch.randn_like(self.embedding.weight.data)-0.5\n",
" self.rnn = torch.nn.LSTM(embed_dim,hidden_dim,batch_first=True)\n",
" self.fc = torch.nn.Linear(hidden_dim, num_class)\n",
"\n",
" def forward(self, x):\n",
" batch_size = x.size(0)\n",
" x = self.embedding(x)\n",
" x,(h,c) = self.rnn(x)\n",
" return self.fc(h[-1])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": []
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"3200: acc=0.259375\n",
"6400: acc=0.25859375\n",
"9600: acc=0.26177083333333334\n",
"12800: acc=0.2784375\n",
"16000: acc=0.313\n",
"19200: acc=0.3528645833333333\n",
"22400: acc=0.3965625\n",
"25600: acc=0.4385546875\n",
"28800: acc=0.4752777777777778\n",
"32000: acc=0.505375\n",
"35200: acc=0.5326704545454546\n",
"38400: acc=0.5557552083333334\n",
"41600: acc=0.5760817307692307\n",
"44800: acc=0.5954910714285714\n",
"48000: acc=0.6118333333333333\n",
"51200: acc=0.62681640625\n",
"54400: acc=0.6404779411764706\n",
"57600: acc=0.6520138888888889\n",
"60800: acc=0.662828947368421\n",
"64000: acc=0.673546875\n",
"67200: acc=0.6831547619047619\n",
"70400: acc=0.6917897727272727\n",
"73600: acc=0.6997146739130434\n",
"76800: acc=0.707109375\n",
"80000: acc=0.714075\n",
"83200: acc=0.7209134615384616\n",
"86400: acc=0.727037037037037\n",
"89600: acc=0.7326674107142858\n",
"92800: acc=0.7379633620689655\n",
"96000: acc=0.7433645833333333\n",
"99200: acc=0.7479032258064516\n",
"102400: acc=0.752119140625\n",
"105600: acc=0.7562405303030303\n",
"108800: acc=0.76015625\n",
"112000: acc=0.7641339285714286\n",
"115200: acc=0.7677777777777778\n",
"118400: acc=0.7711233108108108\n"
]
},
{
"data": {
"text/plain": [
"(0.03487814127604167, 0.7728)"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"net = LSTMClassifier(vocab_size,64,32,len(classes)).to(device)\n",
"train_epoch(net,train_loader, lr=0.001)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Sequências compactadas\n",
"\n",
"No nosso exemplo, tivemos de preencher todas as sequências no minibatch com vetores de zeros. Embora isso resulte em algum desperdício de memória, com RNNs é ainda mais crítico o facto de células adicionais da RNN serem criadas para os itens de entrada preenchidos, que participam no treino, mas não transportam nenhuma informação de entrada relevante. Seria muito melhor treinar a RNN apenas com o tamanho real da sequência.\n",
"\n",
"Para isso, um formato especial de armazenamento de sequências preenchidas foi introduzido no PyTorch. Suponha que temos um minibatch preenchido de entrada que se parece com isto:\n",
"```\n",
"[[1,2,3,4,5],\n",
" [6,7,8,0,0],\n",
" [9,0,0,0,0]]\n",
"```\n",
"Aqui, 0 representa os valores preenchidos, e o vetor de comprimentos reais das sequências de entrada é `[5,3,1]`.\n",
"\n",
"Para treinar eficazmente a RNN com sequências preenchidas, queremos começar o treino do primeiro grupo de células da RNN com um minibatch grande (`[1,6,9]`), mas depois terminar o processamento da terceira sequência e continuar o treino com minibatches mais curtos (`[2,7]`, `[3,8]`), e assim por diante. Assim, a sequência compactada é representada como um único vetor - no nosso caso `[1,6,9,2,7,3,8,4,5]`, e um vetor de comprimentos (`[5,3,1]`), a partir do qual podemos facilmente reconstruir o minibatch preenchido original.\n",
"\n",
"Para produzir uma sequência compactada, podemos usar a função `torch.nn.utils.rnn.pack_padded_sequence`. Todas as camadas recorrentes, incluindo RNN, LSTM e GRU, suportam sequências compactadas como entrada e produzem uma saída compactada, que pode ser decodificada usando `torch.nn.utils.rnn.pad_packed_sequence`.\n",
"\n",
"Para conseguir produzir uma sequência compactada, precisamos passar o vetor de comprimentos para a rede, e, assim, precisamos de uma função diferente para preparar os minibatches:\n"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"def pad_length(b):\n",
" # build vectorized sequence\n",
" v = [encode(x[1]) for x in b]\n",
" # compute max length of a sequence in this minibatch and length sequence itself\n",
" len_seq = list(map(len,v))\n",
" l = max(len_seq)\n",
" return ( # tuple of three tensors - labels, padded features, length sequence\n",
" torch.LongTensor([t[0]-1 for t in b]),\n",
" torch.stack([torch.nn.functional.pad(torch.tensor(t),(0,l-len(t)),mode='constant',value=0) for t in v]),\n",
" torch.tensor(len_seq)\n",
" )\n",
"\n",
"train_loader_len = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=pad_length, shuffle=True)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"A rede real seria muito semelhante ao `LSTMClassifier` acima, mas a passagem `forward` receberá tanto o minibatch preenchido como o vetor de comprimentos das sequências. Após calcular a incorporação (embedding), calculamos a sequência compactada, passamo-la para a camada LSTM e, em seguida, descompactamos o resultado.\n",
"\n",
"> **Nota**: Na verdade, não utilizamos o resultado descompactado `x`, porque usamos a saída das camadas ocultas nos cálculos seguintes. Assim, podemos remover a descompactação completamente deste código. A razão pela qual a colocamos aqui é para que possa modificar este código facilmente, caso precise usar a saída da rede em cálculos posteriores.\n"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
"class LSTMPackClassifier(torch.nn.Module):\n",
" def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):\n",
" super().__init__()\n",
" self.hidden_dim = hidden_dim\n",
" self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n",
" self.embedding.weight.data = torch.randn_like(self.embedding.weight.data)-0.5\n",
" self.rnn = torch.nn.LSTM(embed_dim,hidden_dim,batch_first=True)\n",
" self.fc = torch.nn.Linear(hidden_dim, num_class)\n",
"\n",
" def forward(self, x, lengths):\n",
" batch_size = x.size(0)\n",
" x = self.embedding(x)\n",
" pad_x = torch.nn.utils.rnn.pack_padded_sequence(x,lengths,batch_first=True,enforce_sorted=False)\n",
" pad_x,(h,c) = self.rnn(pad_x)\n",
" x, _ = torch.nn.utils.rnn.pad_packed_sequence(pad_x,batch_first=True)\n",
" return self.fc(h[-1])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": []
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"3200: acc=0.285625\n",
"6400: acc=0.33359375\n",
"9600: acc=0.3876041666666667\n",
"12800: acc=0.44078125\n",
"16000: acc=0.4825\n",
"19200: acc=0.5235416666666667\n",
"22400: acc=0.5559821428571429\n",
"25600: acc=0.58609375\n",
"28800: acc=0.6116666666666667\n",
"32000: acc=0.63340625\n",
"35200: acc=0.6525284090909091\n",
"38400: acc=0.668515625\n",
"41600: acc=0.6822596153846154\n",
"44800: acc=0.6948214285714286\n",
"48000: acc=0.7052708333333333\n",
"51200: acc=0.71521484375\n",
"54400: acc=0.7239889705882353\n",
"57600: acc=0.7315277777777778\n",
"60800: acc=0.7388486842105263\n",
"64000: acc=0.74571875\n",
"67200: acc=0.7518303571428572\n",
"70400: acc=0.7576988636363636\n",
"73600: acc=0.7628940217391305\n",
"76800: acc=0.7681510416666667\n",
"80000: acc=0.7728125\n",
"83200: acc=0.7772235576923077\n",
"86400: acc=0.7815393518518519\n",
"89600: acc=0.7857700892857142\n",
"92800: acc=0.7895043103448276\n",
"96000: acc=0.7930520833333333\n",
"99200: acc=0.7959072580645161\n",
"102400: acc=0.798994140625\n",
"105600: acc=0.802064393939394\n",
"108800: acc=0.8051378676470589\n",
"112000: acc=0.8077857142857143\n",
"115200: acc=0.8104600694444445\n",
"118400: acc=0.8128293918918919\n"
]
},
{
"data": {
"text/plain": [
"(0.029785829671223958, 0.8138166666666666)"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"net = LSTMPackClassifier(vocab_size,64,32,len(classes)).to(device)\n",
"train_epoch_emb(net,train_loader_len, lr=0.001,use_pack_sequence=True)\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> **Nota:** Pode ter reparado no parâmetro `use_pack_sequence` que passamos para a função de treino. Atualmente, a função `pack_padded_sequence` requer que o tensor de sequência de comprimentos esteja no dispositivo CPU, e, portanto, a função de treino precisa evitar mover os dados da sequência de comprimentos para a GPU durante o treino. Pode consultar a implementação da função `train_emb` no ficheiro [`torchnlp.py`](../../../../../lessons/5-NLP/16-RNN/torchnlp.py).\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## RNNs bidirecionais e multicamadas\n",
"\n",
"Nos nossos exemplos, todas as redes recorrentes operaram numa única direção, do início de uma sequência até ao fim. Isto parece natural, pois assemelha-se à forma como lemos e ouvimos fala. No entanto, como em muitos casos práticos temos acesso aleatório à sequência de entrada, pode fazer sentido executar o cálculo recorrente em ambas as direções. Estas redes são chamadas de **RNNs bidirecionais**, e podem ser criadas passando o parâmetro `bidirectional=True` ao construtor de RNN/LSTM/GRU.\n",
"\n",
"Ao trabalhar com uma rede bidirecional, precisaremos de dois vetores de estado oculto, um para cada direção. O PyTorch codifica esses vetores como um único vetor com o dobro do tamanho, o que é bastante conveniente, pois normalmente passaríamos o estado oculto resultante para uma camada linear totalmente conectada, e apenas precisaríamos levar este aumento de tamanho em consideração ao criar a camada.\n",
"\n",
"Uma rede recorrente, seja unidirecional ou bidirecional, captura certos padrões dentro de uma sequência e pode armazená-los no vetor de estado ou passá-los para a saída. Assim como nas redes convolucionais, podemos construir outra camada recorrente sobre a primeira para capturar padrões de nível superior, construídos a partir dos padrões de baixo nível extraídos pela primeira camada. Isto leva-nos ao conceito de **RNN multicamada**, que consiste em duas ou mais redes recorrentes, onde a saída da camada anterior é passada como entrada para a próxima camada.\n",
"\n",
"![Imagem mostrando uma RNN de memória de longo e curto prazo multicamada](../../../../../lessons/5-NLP/16-RNN/images/multi-layer-lstm.jpg)\n",
"\n",
"*Imagem retirada [deste excelente artigo](https://towardsdatascience.com/from-a-lstm-cell-to-a-multilayer-lstm-network-with-pytorch-2899eb5696f3) de Fernando López*\n",
"\n",
"O PyTorch torna a construção de tais redes uma tarefa simples, pois basta passar o parâmetro `num_layers` ao construtor de RNN/LSTM/GRU para criar automaticamente várias camadas de recorrência. Isto também significa que o tamanho do vetor de estado/oculto aumentará proporcionalmente, e será necessário levar isso em consideração ao lidar com a saída das camadas recorrentes.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## RNNs para outras tarefas\n",
"\n",
"Nesta unidade, vimos que as RNNs podem ser usadas para classificação de sequências, mas, na verdade, elas podem lidar com muitas outras tarefas, como geração de texto, tradução automática e muito mais. Vamos abordar essas tarefas na próxima unidade.\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": "522ee52ae3d5ae933e283286254e9a55",
"translation_date": "2025-08-31T12:01:06+00:00",
"source_file": "lessons/5-NLP/16-RNN/RNNPyTorch.ipynb",
"language_code": "pt"
}
},
"nbformat": 4,
"nbformat_minor": 2
}