{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Réseaux de neurones récurrents\n", "\n", "Dans le module précédent, nous avons utilisé des représentations sémantiques riches de texte, associées à un simple classificateur linéaire au-dessus des embeddings. Cette architecture permet de capturer le sens global des mots dans une phrase, mais elle ne prend pas en compte l'**ordre** des mots, car l'opération d'agrégation appliquée aux embeddings supprime cette information issue du texte original. Étant donné que ces modèles ne peuvent pas modéliser l'ordre des mots, ils ne sont pas capables de résoudre des tâches plus complexes ou ambiguës, comme la génération de texte ou la réponse à des questions.\n", "\n", "Pour capturer le sens d'une séquence de texte, nous devons utiliser une autre architecture de réseau de neurones, appelée **réseau de neurones récurrent**, ou RNN. Dans un RNN, nous faisons passer notre phrase à travers le réseau, un symbole à la fois, et le réseau produit un certain **état**, que nous transmettons ensuite au réseau avec le symbole suivant.\n", "\n", "Étant donné la séquence d'entrée de tokens $X_0,\\dots,X_n$, le RNN crée une séquence de blocs de réseau de neurones et entraîne cette séquence de bout en bout à l'aide de la rétropropagation. Chaque bloc de réseau prend une paire $(X_i,S_i)$ en entrée et produit $S_{i+1}$ en sortie. L'état final $S_n$ ou la sortie $X_n$ est ensuite transmis à un classificateur linéaire pour produire le résultat. Tous les blocs de réseau partagent les mêmes poids et sont entraînés de bout en bout en une seule passe de rétropropagation.\n", "\n", "Grâce aux vecteurs d'état $S_0,\\dots,S_n$ qui sont transmis à travers le réseau, celui-ci est capable d'apprendre les dépendances séquentielles entre les mots. Par exemple, lorsque le mot *pas* apparaît quelque part dans la séquence, le réseau peut apprendre à inverser certains éléments du vecteur d'état, ce qui entraîne une négation.\n", "\n", "> Étant donné que les poids de tous les blocs RNN sur l'image sont partagés, la même image peut être représentée par un seul bloc (à droite) avec une boucle de rétroaction récurrente, qui renvoie l'état de sortie du réseau à l'entrée.\n", "\n", "Voyons comment les réseaux de neurones récurrents peuvent nous aider à classifier notre ensemble de données de nouvelles.\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": [ "## Classificateur RNN simple\n", "\n", "Dans le cas d'un RNN simple, chaque unité récurrente est un réseau linéaire simple, qui prend un vecteur d'entrée concaténé et un vecteur d'état, et produit un nouveau vecteur d'état. PyTorch représente cette unité avec la classe `RNNCell`, et un réseau de telles cellules - comme une couche `RNN`.\n", "\n", "Pour définir un classificateur RNN, nous appliquerons d'abord une couche d'embedding pour réduire la dimensionnalité du vocabulaire d'entrée, puis ajouterons une couche RNN par-dessus :\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": [ "> **Note:** Nous utilisons ici une couche d'embedding non entraînée pour simplifier, mais pour obtenir de meilleurs résultats, nous pouvons utiliser une couche d'embedding pré-entraînée avec des embeddings Word2Vec ou GloVe, comme décrit dans l'unité précédente. Pour mieux comprendre, vous pourriez adapter ce code pour qu'il fonctionne avec des embeddings pré-entraînés.\n", "\n", "Dans notre cas, nous utiliserons un chargeur de données avec padding, de sorte que chaque lot contiendra un certain nombre de séquences remplies pour avoir la même longueur. La couche RNN prendra la séquence de tenseurs d'embedding et produira deux sorties : \n", "* $x$ est une séquence des sorties des cellules RNN à chaque étape \n", "* $h$ est l'état caché final pour le dernier élément de la séquence \n", "\n", "Nous appliquons ensuite un classificateur linéaire entièrement connecté pour obtenir le nombre de classes.\n", "\n", "> **Note:** Les RNN sont assez difficiles à entraîner, car une fois que les cellules RNN sont déroulées sur la longueur de la séquence, le nombre de couches impliquées dans la rétropropagation devient assez important. Par conséquent, nous devons sélectionner un faible taux d'apprentissage et entraîner le réseau sur un ensemble de données plus large pour obtenir de bons résultats. Cela peut prendre beaucoup de temps, donc l'utilisation d'un GPU est préférable.\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": [ "## Mémoire à Long et Court Terme (LSTM)\n", "\n", "L'un des principaux problèmes des RNN classiques est le problème des **gradients qui disparaissent**. Étant donné que les RNN sont entraînés de bout en bout en une seule passe de rétropropagation, il est difficile de propager l'erreur jusqu'aux premières couches du réseau, ce qui empêche le réseau d'apprendre les relations entre des tokens éloignés. Une des façons de contourner ce problème est d'introduire une **gestion explicite de l'état** en utilisant ce qu'on appelle des **portes**. Les deux architectures les plus connues de ce type sont : **Mémoire à Long et Court Terme** (LSTM) et **Unité de Relais Gâtée** (GRU).\n", "\n", "![Image montrant un exemple de cellule de mémoire à long et court terme](../../../../../lessons/5-NLP/16-RNN/images/long-short-term-memory-cell.svg)\n", "\n", "Le réseau LSTM est organisé de manière similaire au RNN, mais il y a deux états qui sont transmis d'une couche à l'autre : l'état actuel $c$, et le vecteur caché $h$. À chaque unité, le vecteur caché $h_i$ est concaténé avec l'entrée $x_i$, et ils contrôlent ce qui arrive à l'état $c$ via des **portes**. Chaque porte est un réseau neuronal avec une activation sigmoïde (sortie dans la plage $[0,1]$), qui peut être considérée comme un masque binaire lorsqu'elle est multipliée par le vecteur d'état. Les portes suivantes existent (de gauche à droite sur l'image ci-dessus) :\n", "* **Porte d'oubli** : prend le vecteur caché et détermine quelles composantes du vecteur $c$ doivent être oubliées et lesquelles doivent être conservées.\n", "* **Porte d'entrée** : prend certaines informations de l'entrée et du vecteur caché, et les insère dans l'état.\n", "* **Porte de sortie** : transforme l'état via une couche linéaire avec activation $\\tanh$, puis sélectionne certaines de ses composantes en utilisant le vecteur caché $h_i$ pour produire le nouvel état $c_{i+1}$.\n", "\n", "Les composantes de l'état $c$ peuvent être considérées comme des indicateurs qui peuvent être activés ou désactivés. Par exemple, lorsque nous rencontrons un nom comme *Alice* dans une séquence, nous pouvons supposer qu'il fait référence à un personnage féminin et activer l'indicateur dans l'état indiquant que nous avons un nom féminin dans la phrase. Lorsque nous rencontrons ensuite des expressions comme *et Tom*, nous activons l'indicateur indiquant que nous avons un nom au pluriel. Ainsi, en manipulant l'état, nous pouvons théoriquement suivre les propriétés grammaticales des parties de la phrase.\n", "\n", "> **Note** : Une excellente ressource pour comprendre les détails des LSTM est cet article [Understanding LSTM Networks](https://colah.github.io/posts/2015-08-Understanding-LSTMs/) de Christopher Olah.\n", "\n", "Bien que la structure interne d'une cellule LSTM puisse sembler complexe, PyTorch cache cette implémentation dans la classe `LSTMCell` et fournit l'objet `LSTM` pour représenter toute la couche LSTM. Ainsi, l'implémentation d'un classificateur LSTM sera assez similaire à celle du RNN simple que nous avons vu précédemment :\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": [ "## Séquences compactées\n", "\n", "Dans notre exemple, nous avons dû compléter toutes les séquences du minibatch avec des vecteurs de zéros. Bien que cela entraîne un certain gaspillage de mémoire, avec les RNN, il est encore plus problématique que des cellules RNN supplémentaires soient créées pour les éléments d'entrée complétés, qui participent à l'entraînement mais ne contiennent aucune information d'entrée importante. Il serait bien mieux d'entraîner le RNN uniquement sur la taille réelle des séquences.\n", "\n", "Pour cela, un format spécial de stockage des séquences complétées est introduit dans PyTorch. Supposons que nous ayons un minibatch complété qui ressemble à ceci : \n", "```\n", "[[1,2,3,4,5],\n", " [6,7,8,0,0],\n", " [9,0,0,0,0]]\n", "``` \n", "Ici, 0 représente les valeurs complétées, et le vecteur des longueurs réelles des séquences d'entrée est `[5,3,1]`.\n", "\n", "Pour entraîner efficacement un RNN avec des séquences complétées, nous souhaitons commencer l'entraînement du premier groupe de cellules RNN avec un grand minibatch (`[1,6,9]`), mais ensuite arrêter le traitement de la troisième séquence et continuer l'entraînement avec des minibatches réduits (`[2,7]`, `[3,8]`), et ainsi de suite. Ainsi, une séquence compactée est représentée comme un seul vecteur - dans notre cas `[1,6,9,2,7,3,8,4,5]`, et un vecteur de longueurs (`[5,3,1]`), à partir duquel nous pouvons facilement reconstruire le minibatch complété d'origine.\n", "\n", "Pour produire une séquence compactée, nous pouvons utiliser la fonction `torch.nn.utils.rnn.pack_padded_sequence`. Toutes les couches récurrentes, y compris RNN, LSTM et GRU, prennent en charge les séquences compactées en tant qu'entrée et produisent une sortie compactée, qui peut être décodée à l'aide de `torch.nn.utils.rnn.pad_packed_sequence`.\n", "\n", "Pour pouvoir produire une séquence compactée, nous devons transmettre le vecteur des longueurs au réseau, et donc nous avons besoin d'une fonction différente pour préparer les 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": [ "Le réseau réel serait très similaire à `LSTMClassifier` ci-dessus, mais le passage `forward` recevra à la fois le mini-lot avec remplissage et le vecteur des longueurs de séquence. Après avoir calculé l'embedding, nous calculons la séquence empaquetée, la passons à la couche LSTM, puis dépaquetons le résultat.\n", "\n", "> **Note** : En réalité, nous n'utilisons pas le résultat dépaqueté `x`, car nous utilisons la sortie des couches cachées dans les calculs suivants. Ainsi, nous pouvons supprimer complètement le dépaquetage de ce code. La raison pour laquelle nous le plaçons ici est de vous permettre de modifier ce code facilement, au cas où vous auriez besoin d'utiliser la sortie du réseau dans des calculs ultérieurs.\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": [ "> **Remarque :** Vous avez peut-être remarqué le paramètre `use_pack_sequence` que nous passons à la fonction d'entraînement. Actuellement, la fonction `pack_padded_sequence` nécessite que le tenseur de séquence de longueur soit sur le périphérique CPU, et donc la fonction d'entraînement doit éviter de déplacer les données de séquence de longueur vers le GPU lors de l'entraînement. Vous pouvez consulter l'implémentation de la fonction `train_emb` dans le fichier [`torchnlp.py`](../../../../../lessons/5-NLP/16-RNN/torchnlp.py).\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## RNN bidirectionnels et multicouches\n", "\n", "Dans nos exemples, tous les réseaux récurrents fonctionnaient dans une seule direction, de l'origine d'une séquence jusqu'à sa fin. Cela semble naturel, car cela ressemble à la manière dont nous lisons et écoutons un discours. Cependant, dans de nombreux cas pratiques où nous avons un accès aléatoire à la séquence d'entrée, il peut être pertinent d'exécuter le calcul récurrent dans les deux directions. Ces réseaux sont appelés **RNN bidirectionnels**, et ils peuvent être créés en passant le paramètre `bidirectional=True` au constructeur RNN/LSTM/GRU.\n", "\n", "Lorsqu'on travaille avec un réseau bidirectionnel, il nous faut deux vecteurs d'état caché, un pour chaque direction. PyTorch encode ces vecteurs en un seul vecteur de taille double, ce qui est assez pratique, car on passe généralement l'état caché résultant à une couche linéaire entièrement connectée, et il suffit de prendre en compte cette augmentation de taille lors de la création de la couche.\n", "\n", "Un réseau récurrent, qu'il soit unidirectionnel ou bidirectionnel, capture certains motifs au sein d'une séquence et peut les stocker dans un vecteur d'état ou les transmettre en sortie. Comme pour les réseaux convolutionnels, on peut construire une autre couche récurrente au-dessus de la première pour capturer des motifs de niveau supérieur, construits à partir des motifs de bas niveau extraits par la première couche. Cela nous amène à la notion de **RNN multicouche**, qui consiste en deux ou plusieurs réseaux récurrents, où la sortie de la couche précédente est transmise à la couche suivante comme entrée.\n", "\n", "![Image montrant un RNN multicouche avec mémoire à court et long terme](../../../../../lessons/5-NLP/16-RNN/images/multi-layer-lstm.jpg)\n", "\n", "*Image tirée de [cet excellent article](https://towardsdatascience.com/from-a-lstm-cell-to-a-multilayer-lstm-network-with-pytorch-2899eb5696f3) par Fernando López*\n", "\n", "PyTorch simplifie la construction de tels réseaux, car il suffit de passer le paramètre `num_layers` au constructeur RNN/LSTM/GRU pour créer automatiquement plusieurs couches de récurrence. Cela signifie également que la taille du vecteur caché/d'état augmente proportionnellement, et il faut en tenir compte lors de la gestion de la sortie des couches récurrentes.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## RNNs pour d'autres tâches\n", "\n", "Dans cette unité, nous avons vu que les RNNs peuvent être utilisés pour la classification de séquences, mais en réalité, ils peuvent gérer bien d'autres tâches, comme la génération de texte, la traduction automatique, et bien plus encore. Nous aborderons ces tâches dans la prochaine unité.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n---\n\n**Avertissement** : \nCe document a été traduit à l'aide du service de traduction automatique [Co-op Translator](https://github.com/Azure/co-op-translator). Bien que nous nous efforcions d'assurer l'exactitude, veuillez noter que les traductions automatisées peuvent contenir des erreurs ou des inexactitudes. Le document original dans sa langue d'origine doit être considéré comme la source faisant autorité. Pour des informations critiques, il est recommandé de recourir à une traduction professionnelle réalisée par un humain. Nous déclinons toute responsabilité en cas de malentendus ou d'interprétations erronées résultant de l'utilisation de cette traduction.\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-31T15:23:20+00:00", "source_file": "lessons/5-NLP/16-RNN/RNNPyTorch.ipynb", "language_code": "fr" } }, "nbformat": 4, "nbformat_minor": 2 }