479 lines
24 KiB
Plaintext
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",
|
|
"\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",
|
|
"\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
|
|
} |