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

479 lines
25 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Redes neuronales recurrentes\n",
"\n",
"En el módulo anterior, hemos estado utilizando representaciones semánticas ricas del texto y un clasificador lineal simple sobre las incrustaciones. Lo que hace esta arquitectura es capturar el significado agregado de las palabras en una oración, pero no tiene en cuenta el **orden** de las palabras, ya que la operación de agregación sobre las incrustaciones elimina esta información del texto original. Debido a que estos modelos no pueden modelar el orden de las palabras, no pueden resolver tareas más complejas o ambiguas como la generación de texto o la respuesta a preguntas.\n",
"\n",
"Para capturar el significado de una secuencia de texto, necesitamos usar otra arquitectura de red neuronal, llamada **red neuronal recurrente**, o RNN. En una RNN, pasamos nuestra oración a través de la red un símbolo a la vez, y la red produce un **estado**, que luego pasamos nuevamente a la red junto con el siguiente símbolo.\n",
"\n",
"Dada la secuencia de tokens de entrada $X_0,\\dots,X_n$, la RNN crea una secuencia de bloques de red neuronal y entrena esta secuencia de extremo a extremo utilizando retropropagación. Cada bloque de red toma un par $(X_i,S_i)$ como entrada y produce $S_{i+1}$ como resultado. El estado final $S_n$ o la salida $X_n$ se pasa a un clasificador lineal para producir el resultado. Todos los bloques de red comparten los mismos pesos y se entrenan de extremo a extremo utilizando una sola pasada de retropropagación.\n",
"\n",
"Debido a que los vectores de estado $S_0,\\dots,S_n$ se pasan a través de la red, esta es capaz de aprender las dependencias secuenciales entre palabras. Por ejemplo, cuando la palabra *no* aparece en algún lugar de la secuencia, puede aprender a negar ciertos elementos dentro del vector de estado, lo que resulta en una negación.\n",
"\n",
"> Dado que los pesos de todos los bloques de RNN en la imagen son compartidos, la misma imagen puede representarse como un solo bloque (a la derecha) con un bucle de retroalimentación recurrente, que pasa el estado de salida de la red nuevamente a la entrada.\n",
"\n",
"Veamos cómo las redes neuronales recurrentes pueden ayudarnos a clasificar nuestro conjunto de datos de noticias.\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": [
"## Clasificador RNN simple\n",
"\n",
"En el caso de una RNN simple, cada unidad recurrente es una red lineal sencilla que toma un vector de entrada concatenado y un vector de estado, y produce un nuevo vector de estado. PyTorch representa esta unidad con la clase `RNNCell`, y una red de dichas celdas como una capa `RNN`.\n",
"\n",
"Para definir un clasificador RNN, primero aplicaremos una capa de incrustación para reducir la dimensionalidad del vocabulario de entrada, y luego añadiremos una capa RNN encima:\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:** Aquí usamos una capa de embedding no entrenada por simplicidad, pero para obtener resultados aún mejores podemos usar una capa de embedding preentrenada con embeddings de Word2Vec o GloVe, como se describió en la unidad anterior. Para una mejor comprensión, podrías adaptar este código para trabajar con embeddings preentrenados.\n",
"\n",
"En nuestro caso, utilizaremos un cargador de datos con padding, de modo que cada lote tendrá un número de secuencias rellenadas con la misma longitud. La capa RNN tomará la secuencia de tensores de embedding y producirá dos salidas: \n",
"* $x$ es una secuencia de salidas de las celdas RNN en cada paso \n",
"* $h$ es el estado oculto final para el último elemento de la secuencia \n",
"\n",
"Luego aplicamos un clasificador lineal completamente conectado para obtener el número de clases.\n",
"\n",
"> **Nota:** Las RNN son bastante difíciles de entrenar, porque una vez que las celdas RNN se despliegan a lo largo de la longitud de la secuencia, el número resultante de capas involucradas en la retropropagación es bastante grande. Por lo tanto, necesitamos seleccionar una tasa de aprendizaje pequeña y entrenar la red en un conjunto de datos más grande para obtener buenos resultados. Esto puede tomar bastante tiempo, por lo que se recomienda usar 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": [
"## Memoria a Largo y Corto Plazo (LSTM)\n",
"\n",
"Uno de los principales problemas de las RNN clásicas es el llamado problema de los **gradientes que se desvanecen**. Debido a que las RNN se entrenan de extremo a extremo en una sola pasada de retropropagación, tienen dificultades para propagar el error a las primeras capas de la red, y por lo tanto, la red no puede aprender relaciones entre tokens distantes. Una de las formas de evitar este problema es introducir una **gestión explícita del estado** mediante el uso de los llamados **puertas**. Hay dos arquitecturas más conocidas de este tipo: **Memoria a Largo y Corto Plazo** (LSTM) y **Unidad de Relevo Controlada** (GRU).\n",
"\n",
"![Imagen que muestra un ejemplo de una celda de memoria a largo y corto plazo](../../../../../lessons/5-NLP/16-RNN/images/long-short-term-memory-cell.svg)\n",
"\n",
"La red LSTM está organizada de una manera similar a las RNN, pero hay dos estados que se pasan de capa en capa: el estado actual $c$ y el vector oculto $h$. En cada unidad, el vector oculto $h_i$ se concatena con la entrada $x_i$, y juntos controlan lo que sucede con el estado $c$ a través de las **puertas**. Cada puerta es una red neuronal con activación sigmoide (salida en el rango $[0,1]$), que puede interpretarse como una máscara bit a bit cuando se multiplica por el vector de estado. Las puertas son las siguientes (de izquierda a derecha en la imagen anterior):\n",
"* **Puerta de olvido**: toma el vector oculto y determina qué componentes del vector $c$ necesitamos olvidar y cuáles pasar.\n",
"* **Puerta de entrada**: toma información de la entrada y del vector oculto, e inserta esa información en el estado.\n",
"* **Puerta de salida**: transforma el estado mediante una capa lineal con activación $\\tanh$, y luego selecciona algunos de sus componentes usando el vector oculto $h_i$ para producir el nuevo estado $c_{i+1}$.\n",
"\n",
"Los componentes del estado $c$ pueden interpretarse como banderas que se pueden activar o desactivar. Por ejemplo, cuando encontramos un nombre como *Alice* en la secuencia, podríamos asumir que se refiere a un personaje femenino y activar la bandera en el estado que indica que hay un sustantivo femenino en la oración. Más adelante, al encontrar frases como *and Tom*, activaríamos la bandera que indica que hay un sustantivo en plural. Así, manipulando el estado, supuestamente podemos hacer un seguimiento de las propiedades gramaticales de las partes de la oración.\n",
"\n",
"> **Nota**: Un excelente recurso para entender los detalles internos de las LSTM es este gran artículo [Understanding LSTM Networks](https://colah.github.io/posts/2015-08-Understanding-LSTMs/) de Christopher Olah.\n",
"\n",
"Aunque la estructura interna de una celda LSTM puede parecer compleja, PyTorch oculta esta implementación dentro de la clase `LSTMCell` y proporciona el objeto `LSTM` para representar toda la capa LSTM. Por lo tanto, la implementación de un clasificador LSTM será bastante similar a la RNN simple que vimos anteriormente:\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": [
"## Secuencias empaquetadas\n",
"\n",
"En nuestro ejemplo, tuvimos que rellenar todas las secuencias en el minibatch con vectores de ceros. Aunque esto genera cierto desperdicio de memoria, con las RNN es más crítico que se creen celdas adicionales para los elementos de entrada rellenados, las cuales participan en el entrenamiento pero no contienen información importante. Sería mucho mejor entrenar la RNN únicamente con el tamaño real de la secuencia.\n",
"\n",
"Para lograr esto, se introduce un formato especial de almacenamiento de secuencias rellenadas en PyTorch. Supongamos que tenemos un minibatch rellenado que se ve así:\n",
"```\n",
"[[1,2,3,4,5],\n",
" [6,7,8,0,0],\n",
" [9,0,0,0,0]]\n",
"```\n",
"Aquí, 0 representa los valores rellenados, y el vector de longitud real de las secuencias de entrada es `[5,3,1]`.\n",
"\n",
"Para entrenar eficazmente una RNN con secuencias rellenadas, queremos comenzar el entrenamiento del primer grupo de celdas de la RNN con un minibatch grande (`[1,6,9]`), pero luego terminar el procesamiento de la tercera secuencia y continuar el entrenamiento con minibatches más pequeños (`[2,7]`, `[3,8]`), y así sucesivamente. Por lo tanto, una secuencia empaquetada se representa como un solo vector - en nuestro caso `[1,6,9,2,7,3,8,4,5]`, y un vector de longitud (`[5,3,1]`), a partir del cual podemos reconstruir fácilmente el minibatch rellenado original.\n",
"\n",
"Para generar una secuencia empaquetada, podemos usar la función `torch.nn.utils.rnn.pack_padded_sequence`. Todas las capas recurrentes, incluidas RNN, LSTM y GRU, admiten secuencias empaquetadas como entrada y producen una salida empaquetada, que puede ser decodificada usando `torch.nn.utils.rnn.pad_packed_sequence`.\n",
"\n",
"Para poder generar una secuencia empaquetada, necesitamos pasar el vector de longitud a la red, y por lo tanto necesitamos una función diferente para preparar los 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": [
"La red real sería muy similar a `LSTMClassifier` mencionado anteriormente, pero el paso `forward` recibirá tanto el minibatch con padding como el vector de longitudes de las secuencias. Después de calcular la incrustación, calculamos la secuencia empaquetada, la pasamos a la capa LSTM y luego desempaquetamos el resultado.\n",
"\n",
"> **Nota**: En realidad no usamos el resultado desempaquetado `x`, porque utilizamos la salida de las capas ocultas en los cálculos posteriores. Por lo tanto, podemos eliminar el desempaquetado por completo de este código. La razón por la que lo colocamos aquí es para que puedas modificar este código fácilmente, en caso de que necesites usar la salida de la red en cálculos adicionales.\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:** Es posible que hayas notado el parámetro `use_pack_sequence` que pasamos a la función de entrenamiento. Actualmente, la función `pack_padded_sequence` requiere que el tensor de la secuencia de longitud esté en el dispositivo CPU, y por lo tanto, la función de entrenamiento necesita evitar mover los datos de la secuencia de longitud a la GPU durante el entrenamiento. Puedes revisar la implementación de la función `train_emb` en el archivo [`torchnlp.py`](../../../../../lessons/5-NLP/16-RNN/torchnlp.py).\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## RNNs bidireccionales y multicapa\n",
"\n",
"En nuestros ejemplos, todas las redes recurrentes operaban en una sola dirección, desde el inicio de una secuencia hasta el final. Esto parece natural, ya que se asemeja a la forma en que leemos y escuchamos el habla. Sin embargo, dado que en muchos casos prácticos tenemos acceso aleatorio a la secuencia de entrada, podría tener sentido realizar cálculos recurrentes en ambas direcciones. Estas redes se llaman **RNNs bidireccionales**, y se pueden crear pasando el parámetro `bidirectional=True` al constructor de RNN/LSTM/GRU.\n",
"\n",
"Al trabajar con una red bidireccional, necesitaríamos dos vectores de estado oculto, uno para cada dirección. PyTorch codifica esos vectores como un solo vector de tamaño doble, lo cual es bastante conveniente, porque normalmente pasarías el estado oculto resultante a una capa lineal completamente conectada, y solo tendrías que tener en cuenta este aumento de tamaño al crear la capa.\n",
"\n",
"Una red recurrente, ya sea unidireccional o bidireccional, captura ciertos patrones dentro de una secuencia y puede almacenarlos en el vector de estado o pasarlos a la salida. Al igual que con las redes convolucionales, podemos construir otra capa recurrente encima de la primera para capturar patrones de nivel superior, construidos a partir de los patrones de bajo nivel extraídos por la primera capa. Esto nos lleva al concepto de **RNN multicapa**, que consiste en dos o más redes recurrentes, donde la salida de la capa anterior se pasa a la siguiente capa como entrada.\n",
"\n",
"![Imagen que muestra una RNN multicapa de memoria a largo y corto plazo](../../../../../lessons/5-NLP/16-RNN/images/multi-layer-lstm.jpg)\n",
"\n",
"*Imagen tomada de [este maravilloso artículo](https://towardsdatascience.com/from-a-lstm-cell-to-a-multilayer-lstm-network-with-pytorch-2899eb5696f3) por Fernando López*\n",
"\n",
"PyTorch facilita la construcción de este tipo de redes, ya que solo necesitas pasar el parámetro `num_layers` al constructor de RNN/LSTM/GRU para construir automáticamente varias capas de recurrencia. Esto también significa que el tamaño del vector de estado oculto aumentará proporcionalmente, y deberás tener esto en cuenta al manejar la salida de las capas recurrentes.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## RNNs para otras tareas\n",
"\n",
"En esta unidad, hemos visto que las RNNs pueden usarse para la clasificación de secuencias, pero de hecho, pueden manejar muchas más tareas, como la generación de texto, la traducción automática y más. Consideraremos esas tareas en la próxima unidad.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n---\n\n**Descargo de responsabilidad**: \nEste documento ha sido traducido utilizando el servicio de traducción automática [Co-op Translator](https://github.com/Azure/co-op-translator). Si bien nos esforzamos por garantizar la precisión, tenga en cuenta que las traducciones automatizadas pueden contener errores o imprecisiones. El documento original en su idioma nativo debe considerarse la fuente autorizada. Para información crítica, se recomienda una traducción profesional realizada por humanos. No nos hacemos responsables de malentendidos o interpretaciones erróneas que puedan surgir del uso de esta traducción.\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-31T17:08:40+00:00",
"source_file": "lessons/5-NLP/16-RNN/RNNPyTorch.ipynb",
"language_code": "es"
}
},
"nbformat": 4,
"nbformat_minor": 2
}