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

460 lines
24 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Redes neuronais recorrentes\n",
"\n",
"No módulo anterior, abordámos representações semânticas ricas de texto. A arquitetura que temos utilizado captura o significado agregado das palavras numa frase, mas não considera a **ordem** das palavras, porque a operação de agregação que segue as embeddings elimina esta informação do texto original. Como estes modelos não conseguem representar 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, utilizaremos uma arquitetura de rede neural chamada **rede neural recorrente**, ou RNN. Ao usar uma RNN, passamos a nossa frase pela rede, um token de cada vez, e a rede produz um **estado**, que depois passamos novamente para a rede juntamente com o próximo token.\n",
"\n",
"![Imagem que mostra um exemplo de geração de rede neural recorrente.](../../../../../lessons/5-NLP/16-RNN/images/rnn.png)\n",
"\n",
"Dada a sequência de entrada de tokens $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 $Y_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",
"> A figura acima mostra uma rede neural recorrente na forma expandida (à esquerda) e numa representação recorrente mais compacta (à direita). É importante perceber que todas as células RNN têm os mesmos **pesos partilháveis**.\n",
"\n",
"Como os vetores de estado $S_0,\\dots,S_n$ são passados pela rede, a RNN é capaz de aprender 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.\n",
"\n",
"Internamente, cada célula RNN contém duas matrizes de pesos: $W_H$ e $W_I$, e um bias $b$. Em cada passo da RNN, dado o input $X_i$ e o estado de entrada $S_i$, o estado de saída é calculado como $S_{i+1} = f(W_H\\times S_i + W_I\\times X_i+b)$, onde $f$ é uma função de ativação (frequentemente $\\tanh$).\n",
"\n",
"> Para problemas como geração de texto (que abordaremos na próxima unidade) ou tradução automática, também queremos obter um valor de saída em cada passo da RNN. Neste caso, existe também outra matriz $W_O$, e a saída é calculada como $Y_i=f(W_O\\times S_i+b_O)$.\n",
"\n",
"Vamos ver como as redes neuronais recorrentes podem ajudar-nos a classificar o nosso conjunto de dados de notícias.\n",
"\n",
"> Para o ambiente sandbox, precisamos de executar a seguinte célula para garantir que a biblioteca necessária está instalada e os dados estão pré-carregados. Se estiver a trabalhar localmente, pode ignorar a célula seguinte.\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"!{sys.executable} -m pip install --quiet tensorflow_datasets==4.4.0\n",
"!cd ~ && wget -q -O - https://mslearntensorflowlp.blob.core.windows.net/data/tfds-ag-news.tgz | tar xz"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"import tensorflow as tf\n",
"from tensorflow import keras\n",
"import tensorflow_datasets as tfds\n",
"import numpy as np\n",
"\n",
"# We are going to be training pretty large models. In order not to face errors, we need\n",
"# to set tensorflow option to grow GPU memory allocation when required\n",
"physical_devices = tf.config.list_physical_devices('GPU') \n",
"if len(physical_devices)>0:\n",
" tf.config.experimental.set_memory_growth(physical_devices[0], True)\n",
"\n",
"ds_train, ds_test = tfds.load('ag_news_subset').values()"
]
},
{
"cell_type": "markdown",
"metadata": {
"nteract": {
"transient": {
"deleting": false
}
}
},
"source": [
"Ao treinar modelos grandes, a alocação de memória da GPU pode tornar-se um problema. Também pode ser necessário experimentar diferentes tamanhos de minibatch, de forma a que os dados caibam na memória da GPU, mas que o treino seja suficientemente rápido. Se estiver a executar este código na sua própria máquina com GPU, pode experimentar ajustar o tamanho do minibatch para acelerar o treino.\n",
"\n",
"> **Nota**: Certas versões dos drivers da NVidia são conhecidas por não libertarem a memória após o treino do modelo. Estamos a executar vários exemplos neste notebook, e isso pode levar ao esgotamento da memória em determinadas configurações, especialmente se estiver a realizar as suas próprias experiências no mesmo notebook. Se encontrar erros estranhos ao iniciar o treino do modelo, pode ser necessário reiniciar o kernel do notebook.\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"collapsed": true,
"jupyter": {
"outputs_hidden": false,
"source_hidden": false
},
"nteract": {
"transient": {
"deleting": false
}
}
},
"outputs": [],
"source": [
"batch_size = 16\n",
"embed_size = 64"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Classificador RNN Simples\n",
"\n",
"No caso de uma RNN simples, cada unidade recorrente é uma rede linear simples, que recebe um vetor de entrada e um vetor de estado, e produz um novo vetor de estado. No Keras, isto pode ser representado pela camada `SimpleRNN`.\n",
"\n",
"Embora possamos passar diretamente tokens codificados em one-hot para a camada RNN, isto não é uma boa ideia devido à sua alta dimensionalidade. Por isso, utilizaremos uma camada de embedding para reduzir a dimensionalidade dos vetores de palavras, seguida por uma camada RNN e, por fim, um classificador `Dense`.\n",
"\n",
"> **Nota**: Em casos onde a dimensionalidade não é tão alta, por exemplo, ao usar tokenização a nível de caracteres, pode fazer sentido passar tokens codificados em one-hot diretamente para a célula RNN.\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Model: \"sequential\"\n",
"_________________________________________________________________\n",
"Layer (type) Output Shape Param # \n",
"=================================================================\n",
"text_vectorization (TextVect (None, None) 0 \n",
"_________________________________________________________________\n",
"embedding (Embedding) (None, None, 64) 1280000 \n",
"_________________________________________________________________\n",
"simple_rnn (SimpleRNN) (None, 16) 1296 \n",
"_________________________________________________________________\n",
"dense (Dense) (None, 4) 68 \n",
"=================================================================\n",
"Total params: 1,281,364\n",
"Trainable params: 1,281,364\n",
"Non-trainable params: 0\n",
"_________________________________________________________________\n"
]
}
],
"source": [
"vocab_size = 20000\n",
"\n",
"vectorizer = keras.layers.experimental.preprocessing.TextVectorization(\n",
" max_tokens=vocab_size,\n",
" input_shape=(1,))\n",
"\n",
"model = keras.models.Sequential([\n",
" vectorizer,\n",
" keras.layers.Embedding(vocab_size, embed_size),\n",
" keras.layers.SimpleRNN(16),\n",
" keras.layers.Dense(4,activation='softmax')\n",
"])\n",
"\n",
"model.summary()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> **Nota:** Aqui usamos uma camada de incorporação não treinada para simplificar, mas para obter melhores resultados podemos usar uma camada de incorporação pré-treinada utilizando Word2Vec, conforme descrito na unidade anterior. Seria um bom exercício para si adaptar este código para funcionar com incorporações pré-treinadas.\n",
"\n",
"Agora vamos treinar a nossa RNN. As RNNs, de forma geral, são bastante difíceis de treinar, porque, uma vez que as células da RNN são desenroladas ao longo do comprimento da sequência, o número resultante de camadas envolvidas na retropropagação é bastante grande. Por isso, precisamos selecionar uma taxa de aprendizagem menor e treinar a rede num conjunto de dados maior para obter bons resultados. Isto pode demorar bastante tempo, por isso é preferível usar uma GPU.\n",
"\n",
"Para acelerar o processo, vamos treinar o modelo RNN apenas nos títulos das notícias, omitindo a descrição. Pode tentar treinar com a descrição e ver se consegue fazer o modelo treinar.\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Training vectorizer\n"
]
}
],
"source": [
"def extract_title(x):\n",
" return x['title']\n",
"\n",
"def tupelize_title(x):\n",
" return (extract_title(x),x['label'])\n",
"\n",
"print('Training vectorizer')\n",
"vectorizer.adapt(ds_train.take(2000).map(extract_title))"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"7500/7500 [==============================] - 82s 11ms/step - loss: 0.6629 - acc: 0.7623 - val_loss: 0.5559 - val_acc: 0.7995\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\n"
]
},
{
"data": {
"text/plain": [
"<tensorflow.python.keras.callbacks.History at 0x7f3e0030d350>"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"model.compile(loss='sparse_categorical_crossentropy',metrics=['acc'], optimizer='adam')\n",
"model.fit(ds_train.map(tupelize_title).batch(batch_size),validation_data=ds_test.map(tupelize_title).batch(batch_size))"
]
},
{
"cell_type": "markdown",
"metadata": {
"nteract": {
"transient": {
"deleting": false
}
}
},
"source": [
"> **Nota** que a precisão é provavelmente menor aqui, porque estamos a treinar apenas com títulos de notícias.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Revisitar sequências de variáveis\n",
"\n",
"Lembre-se de que a camada `TextVectorization` irá automaticamente preencher sequências de comprimento variável em um minibatch com tokens de preenchimento (pad tokens). Acontece que esses tokens também participam do treino, e isso pode complicar a convergência do modelo.\n",
"\n",
"Existem várias abordagens que podemos adotar para minimizar a quantidade de preenchimento. Uma delas é reorganizar o conjunto de dados pelo comprimento das sequências e agrupar todas as sequências por tamanho. Isso pode ser feito utilizando a função `tf.data.experimental.bucket_by_sequence_length` (consulte a [documentação](https://www.tensorflow.org/api_docs/python/tf/data/experimental/bucket_by_sequence_length)).\n",
"\n",
"Outra abordagem é usar **masking**. No Keras, algumas camadas suportam entradas adicionais que indicam quais tokens devem ser considerados durante o treino. Para incorporar masking no nosso modelo, podemos incluir uma camada `Masking` separada ([documentação](https://keras.io/api/layers/core_layers/masking/)) ou especificar o parâmetro `mask_zero=True` na nossa camada `Embedding`.\n",
"\n",
"> **Note**: Este treino levará cerca de 5 minutos para completar uma época em todo o conjunto de dados. Sinta-se à vontade para interromper o treino a qualquer momento se perder a paciência. Outra opção é limitar a quantidade de dados usados para o treino, adicionando a cláusula `.take(...)` após os conjuntos de dados `ds_train` e `ds_test`.\n"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"7500/7500 [==============================] - 371s 49ms/step - loss: 0.5401 - acc: 0.8079 - val_loss: 0.3780 - val_acc: 0.8822\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\n"
]
},
{
"data": {
"text/plain": [
"<tensorflow.python.keras.callbacks.History at 0x7f3dec118850>"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def extract_text(x):\n",
" return x['title']+' '+x['description']\n",
"\n",
"def tupelize(x):\n",
" return (extract_text(x),x['label'])\n",
"\n",
"model = keras.models.Sequential([\n",
" vectorizer,\n",
" keras.layers.Embedding(vocab_size,embed_size,mask_zero=True),\n",
" keras.layers.SimpleRNN(16),\n",
" keras.layers.Dense(4,activation='softmax')\n",
"])\n",
"\n",
"model.compile(loss='sparse_categorical_crossentropy',metrics=['acc'], optimizer='adam')\n",
"model.fit(ds_train.map(tupelize).batch(batch_size),validation_data=ds_test.map(tupelize).batch(batch_size))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Agora que estamos a usar mascaramento, podemos treinar o modelo em todo o conjunto de dados de títulos e descrições.\n",
"\n",
"> **Note**: Reparaste que temos estado a usar um vetorizador treinado nos títulos das notícias, e não no corpo completo do artigo? Potencialmente, isto pode fazer com que alguns dos tokens sejam ignorados, por isso é melhor re-treinar o vetorizador. No entanto, isto pode ter apenas um efeito muito pequeno, por isso vamos continuar a usar o vetorizador pré-treinado anterior para simplificar.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## LSTM: Memória de longo curto prazo\n",
"\n",
"Um dos principais problemas das RNNs é o **desvanecimento de gradientes**. As RNNs podem ser bastante longas e podem ter dificuldade em propagar os gradientes até à primeira camada da rede durante a retropropagação. Quando isso acontece, a rede não consegue aprender relações entre tokens distantes. Uma forma de evitar este problema é introduzir uma **gestão explícita de estados** utilizando **portas**. As duas arquiteturas mais comuns que introduzem portas são a **memória de longo curto prazo** (LSTM) e a **unidade de retransmissão com portas** (GRU). Vamos abordar as LSTMs aqui.\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",
"Uma rede LSTM é organizada de forma semelhante a uma RNN, mas existem dois estados que são passados de camada para camada: o estado real $c$ e o vetor oculto $h$. Em cada unidade, o vetor oculto $h_{t-1}$ é combinado com a entrada $x_t$, e juntos controlam o que acontece ao estado $c_t$ e à saída $h_{t}$ através de **portas**. Cada porta tem uma 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. As LSTMs possuem as seguintes portas (da esquerda para a direita na imagem acima):\n",
"* **porta de esquecimento**, que determina quais componentes do vetor $c_{t-1}$ precisamos esquecer e quais devemos manter.\n",
"* **porta de entrada**, que determina quanta informação do vetor de entrada e do vetor oculto anterior deve ser incorporada no vetor de estado.\n",
"* **porta de saída**, que pega o novo vetor de estado e decide quais dos seus componentes serão usados para produzir o novo vetor oculto $h_t$.\n",
"\n",
"Os componentes do estado $c$ podem ser vistos como indicadores que podem ser ativados ou desativados. Por exemplo, quando encontramos o nome *Alice* na sequência, presumimos que se refere a uma mulher e ativamos o indicador no estado que diz que temos um substantivo feminino na frase. Quando mais tarde encontramos as palavras *e Tom*, ativamos o indicador que diz que temos um substantivo no plural. Assim, ao manipular o estado, podemos acompanhar as propriedades gramaticais da frase.\n",
"\n",
"> **Nota**: Aqui está um excelente recurso para compreender os detalhes internos das LSTMs: [Understanding LSTM Networks](https://colah.github.io/posts/2015-08-Understanding-LSTMs/) de Christopher Olah.\n",
"\n",
"Embora a estrutura interna de uma célula LSTM possa parecer complexa, o Keras esconde esta implementação dentro da camada `LSTM`, por isso a única coisa que precisamos fazer no exemplo acima é substituir a camada recorrente:\n"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"15000/15000 [==============================] - 188s 13ms/step - loss: 0.5692 - acc: 0.7916 - val_loss: 0.3441 - val_acc: 0.8870\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\n"
]
},
{
"data": {
"text/plain": [
"<tensorflow.python.keras.callbacks.History at 0x7f3d6af5c350>"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"model = keras.models.Sequential([\n",
" vectorizer,\n",
" keras.layers.Embedding(vocab_size, embed_size),\n",
" keras.layers.LSTM(8),\n",
" keras.layers.Dense(4,activation='softmax')\n",
"])\n",
"\n",
"model.compile(loss='sparse_categorical_crossentropy',metrics=['acc'], optimizer='adam')\n",
"model.fit(ds_train.map(tupelize).batch(8),validation_data=ds_test.map(tupelize).batch(8))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## RNNs Bidirecionais e Multicamadas\n",
"\n",
"Nos nossos exemplos até agora, as redes recorrentes operam do início de uma sequência até ao fim. Isto parece natural para nós porque segue a mesma direção em que lemos ou ouvimos uma fala. No entanto, para cenários que requerem acesso aleatório à sequência de entrada, faz mais sentido executar o cálculo recorrente em ambas as direções. As RNNs que permitem cálculos em ambas as direções são chamadas de RNNs **bidirecionais**, e podem ser criadas ao envolver a camada recorrente com uma camada especial chamada `Bidirectional`.\n",
"\n",
"> **Note**: A camada `Bidirectional` cria duas cópias da camada dentro dela e define a propriedade `go_backwards` de uma dessas cópias como `True`, fazendo com que ela percorra a sequência na direção oposta.\n",
"\n",
"As redes recorrentes, sejam unidirecionais ou bidirecionais, capturam padrões dentro de uma sequência e armazenam-nos em vetores de estado ou retornam-nos como saída. Tal como nas redes convolucionais, podemos construir outra camada recorrente após a primeira para capturar padrões de nível superior, construídos a partir de padrões de nível inferior extraídos pela primeira camada. Isto leva-nos ao conceito de uma **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 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 Keras torna a construção destas redes uma tarefa simples, porque basta adicionar mais camadas recorrentes ao modelo. Para todas as camadas, exceto a última, precisamos especificar o parâmetro `return_sequences=True`, porque precisamos que a camada retorne todos os estados intermédios, e não apenas o estado final do cálculo recorrente.\n",
"\n",
"Vamos construir uma LSTM bidirecional de duas camadas para o nosso problema de classificação.\n",
"\n",
"> **Note** este código, novamente, demora bastante tempo a ser executado, mas oferece-nos a maior precisão que vimos até agora. Por isso, talvez valha a pena esperar e ver o resultado.\n"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"5044/7500 [===================>..........] - ETA: 2:33 - loss: 0.3709 - acc: 0.8706\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r5045/7500 [===================>..........] - ETA: 2:33 - loss: 0.3709 - acc: 0.8706"
]
}
],
"source": [
"model = keras.models.Sequential([\n",
" vectorizer,\n",
" keras.layers.Embedding(vocab_size, 128, mask_zero=True),\n",
" keras.layers.Bidirectional(keras.layers.LSTM(64,return_sequences=True)),\n",
" keras.layers.Bidirectional(keras.layers.LSTM(64)), \n",
" keras.layers.Dense(4,activation='softmax')\n",
"])\n",
"\n",
"model.compile(loss='sparse_categorical_crossentropy',metrics=['acc'], optimizer='adam')\n",
"model.fit(ds_train.map(tupelize).batch(batch_size),\n",
" validation_data=ds_test.map(tupelize).batch(batch_size))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## RNNs para outras tarefas\n",
"\n",
"Até agora, concentrámo-nos em usar RNNs para classificar sequências de texto. No entanto, elas podem lidar com muitas outras tarefas, como geração de texto e tradução automática — abordaremos 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 ter em conta 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": {
"kernel_info": {
"name": "conda-env-py37_tensorflow-py"
},
"kernelspec": {
"display_name": "py37_tensorflow",
"language": "python",
"name": "conda-env-py37_tensorflow-py"
},
"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.7.9"
},
"nteract": {
"version": "nteract-front-end@1.0.0"
},
"coopTranslator": {
"original_hash": "81351e61f619b432ff51010a4f993194",
"translation_date": "2025-08-31T12:00:24+00:00",
"source_file": "lessons/5-NLP/16-RNN/RNNTF.ipynb",
"language_code": "pt"
}
},
"nbformat": 4,
"nbformat_minor": 4
}