AI-For-Beginners/translations/fr/lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb

414 lines
23 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Réseaux génératifs\n",
"\n",
"Les réseaux neuronaux récurrents (RNN) et leurs variantes à cellules à portes, comme les cellules de mémoire à long court terme (LSTM) et les unités récurrentes à portes (GRU), ont fourni un mécanisme pour la modélisation du langage, c'est-à-dire qu'ils peuvent apprendre l'ordre des mots et fournir des prédictions pour le mot suivant dans une séquence. Cela nous permet d'utiliser les RNN pour des **tâches génératives**, telles que la génération de texte ordinaire, la traduction automatique et même la génération de légendes pour des images.\n",
"\n",
"Dans l'architecture RNN que nous avons abordée dans l'unité précédente, chaque unité RNN produisait le prochain état caché comme sortie. Cependant, nous pouvons également ajouter une autre sortie à chaque unité récurrente, ce qui nous permettrait de produire une **séquence** (de même longueur que la séquence originale). De plus, nous pouvons utiliser des unités RNN qui n'acceptent pas d'entrée à chaque étape, mais qui prennent simplement un vecteur d'état initial, puis produisent une séquence de sorties.\n",
"\n",
"Dans ce notebook, nous allons nous concentrer sur des modèles génératifs simples qui nous aident à générer du texte. Pour simplifier, construisons un **réseau au niveau des caractères**, qui génère du texte lettre par lettre. Pendant l'entraînement, nous devons prendre un corpus de texte et le diviser en séquences de lettres.\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",
"import numpy as np\n",
"from torchnlp import *\n",
"train_dataset,test_dataset,classes,vocab = load_dataset()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Construire un vocabulaire de caractères\n",
"\n",
"Pour créer un réseau génératif au niveau des caractères, il est nécessaire de diviser le texte en caractères individuels plutôt qu'en mots. Cela peut être réalisé en définissant un tokenizer différent :\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Vocabulary size = 82\n",
"Encoding of 'a' is 1\n",
"Character with code 13 is c\n"
]
}
],
"source": [
"def char_tokenizer(words):\n",
" return list(words) #[word for word in words]\n",
"\n",
"counter = collections.Counter()\n",
"for (label, line) in train_dataset:\n",
" counter.update(char_tokenizer(line))\n",
"vocab = torchtext.vocab.vocab(counter)\n",
"\n",
"vocab_size = len(vocab)\n",
"print(f\"Vocabulary size = {vocab_size}\")\n",
"print(f\"Encoding of 'a' is {vocab.get_stoi()['a']}\")\n",
"print(f\"Character with code 13 is {vocab.get_itos()[13]}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Voyons l'exemple de la façon dont nous pouvons encoder le texte de notre ensemble de données :\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"tensor([ 0, 1, 2, 2, 3, 4, 5, 6, 3, 7, 8, 1, 9, 10, 3, 11, 2, 1,\n",
" 12, 3, 7, 1, 13, 14, 3, 15, 16, 5, 17, 3, 5, 18, 8, 3, 7, 2,\n",
" 1, 13, 14, 3, 19, 20, 8, 21, 5, 8, 9, 10, 22, 3, 20, 8, 21, 5,\n",
" 8, 9, 10, 3, 23, 3, 4, 18, 17, 9, 5, 23, 10, 8, 2, 2, 8, 9,\n",
" 10, 24, 3, 0, 1, 2, 2, 3, 4, 5, 9, 8, 8, 5, 25, 10, 3, 26,\n",
" 12, 27, 16, 26, 2, 27, 16, 28, 29, 30, 1, 16, 26, 3, 17, 31, 3, 21,\n",
" 2, 5, 9, 1, 23, 13, 32, 16, 27, 13, 10, 24, 3, 1, 9, 8, 3, 10,\n",
" 8, 8, 27, 16, 28, 3, 28, 9, 8, 8, 16, 3, 1, 28, 1, 27, 16, 6])"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def enc(x):\n",
" return torch.LongTensor(encode(x,voc=vocab,tokenizer=char_tokenizer))\n",
"\n",
"enc(train_dataset[0][1])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Entraîner un RNN génératif\n",
"\n",
"La manière dont nous allons entraîner un RNN à générer du texte est la suivante. À chaque étape, nous prendrons une séquence de caractères de longueur `nchars` et demanderons au réseau de générer le caractère de sortie suivant pour chaque caractère d'entrée :\n",
"\n",
"![Image montrant un exemple de génération RNN du mot 'HELLO'.](../../../../../lessons/5-NLP/17-GenerativeNetworks/images/rnn-generate.png)\n",
"\n",
"Selon le scénario spécifique, nous pourrions également vouloir inclure certains caractères spéciaux, tels que *fin de séquence* `<eos>`. Dans notre cas, nous souhaitons simplement entraîner le réseau pour une génération de texte infinie. Par conséquent, nous fixerons la taille de chaque séquence à `nchars` tokens. Ainsi, chaque exemple d'entraînement sera composé de `nchars` entrées et de `nchars` sorties (qui correspondent à la séquence d'entrée décalée d'un symbole vers la gauche). Un minibatch sera constitué de plusieurs de ces séquences.\n",
"\n",
"La manière dont nous générerons les minibatches consiste à prendre chaque texte d'actualité de longueur `l` et à en extraire toutes les combinaisons possibles entrée-sortie (il y aura `l-nchars` combinaisons de ce type). Ces combinaisons formeront un minibatch, et la taille des minibatches variera à chaque étape d'entraînement.\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(tensor([[ 0, 1, 2, ..., 28, 29, 30],\n",
" [ 1, 2, 2, ..., 29, 30, 1],\n",
" [ 2, 2, 3, ..., 30, 1, 16],\n",
" ...,\n",
" [20, 8, 21, ..., 1, 28, 1],\n",
" [ 8, 21, 5, ..., 28, 1, 27],\n",
" [21, 5, 8, ..., 1, 27, 16]]),\n",
" tensor([[ 1, 2, 2, ..., 29, 30, 1],\n",
" [ 2, 2, 3, ..., 30, 1, 16],\n",
" [ 2, 3, 4, ..., 1, 16, 26],\n",
" ...,\n",
" [ 8, 21, 5, ..., 28, 1, 27],\n",
" [21, 5, 8, ..., 1, 27, 16],\n",
" [ 5, 8, 9, ..., 27, 16, 6]]))"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"nchars = 100\n",
"\n",
"def get_batch(s,nchars=nchars):\n",
" ins = torch.zeros(len(s)-nchars,nchars,dtype=torch.long,device=device)\n",
" outs = torch.zeros(len(s)-nchars,nchars,dtype=torch.long,device=device)\n",
" for i in range(len(s)-nchars):\n",
" ins[i] = enc(s[i:i+nchars])\n",
" outs[i] = enc(s[i+1:i+nchars+1])\n",
" return ins,outs\n",
"\n",
"get_batch(train_dataset[0][1])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Définissons maintenant le réseau générateur. Il peut être basé sur n'importe quelle cellule récurrente que nous avons abordée dans l'unité précédente (simple, LSTM ou GRU). Dans notre exemple, nous utiliserons un LSTM.\n",
"\n",
"Étant donné que le réseau prend des caractères en entrée et que la taille du vocabulaire est assez petite, nous n'avons pas besoin de couche d'embedding ; une entrée encodée en one-hot peut être directement transmise à la cellule LSTM. Cependant, comme nous passons des numéros de caractères en entrée, nous devons les encoder en one-hot avant de les transmettre au LSTM. Cela se fait en appelant la fonction `one_hot` pendant le passage `forward`. L'encodeur de sortie sera une couche linéaire qui convertira l'état caché en une sortie encodée en one-hot.\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"class LSTMGenerator(torch.nn.Module):\n",
" def __init__(self, vocab_size, hidden_dim):\n",
" super().__init__()\n",
" self.rnn = torch.nn.LSTM(vocab_size,hidden_dim,batch_first=True)\n",
" self.fc = torch.nn.Linear(hidden_dim, vocab_size)\n",
"\n",
" def forward(self, x, s=None):\n",
" x = torch.nn.functional.one_hot(x,vocab_size).to(torch.float32)\n",
" x,s = self.rnn(x,s)\n",
" return self.fc(x),s"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Pendant l'entraînement, nous voulons pouvoir échantillonner du texte généré. Pour cela, nous allons définir une fonction `generate` qui produira une chaîne de caractères de longueur `size`, en commençant par la chaîne initiale `start`.\n",
"\n",
"Voici comment cela fonctionne. Tout d'abord, nous passons la chaîne de départ complète à travers le réseau, et nous obtenons l'état de sortie `s` ainsi que le prochain caractère prédit `out`. Comme `out` est encodé en one-hot, nous utilisons `argmax` pour obtenir l'indice du caractère `nc` dans le vocabulaire, puis nous utilisons `itos` pour déterminer le caractère réel et l'ajouter à la liste résultante de caractères `chars`. Ce processus de génération d'un caractère est répété `size` fois pour générer le nombre requis de caractères.\n"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
"def generate(net,size=100,start='today '):\n",
" chars = list(start)\n",
" out, s = net(enc(chars).view(1,-1).to(device))\n",
" for i in range(size):\n",
" nc = torch.argmax(out[0][-1])\n",
" chars.append(vocab.get_itos()[nc])\n",
" out, s = net(nc.view(1,-1),s)\n",
" return ''.join(chars)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Passons à l'entraînement ! La boucle d'entraînement est presque identique à celle de tous nos exemples précédents, mais au lieu d'afficher la précision, nous affichons un texte généré échantillonné tous les 1000 epochs.\n",
"\n",
"Une attention particulière doit être portée à la manière dont nous calculons la perte. Nous devons calculer la perte en utilisant une sortie encodée en one-hot `out` et le texte attendu `text_out`, qui est la liste des indices de caractères. Heureusement, la fonction `cross_entropy` attend en premier argument la sortie non normalisée du réseau, et en second le numéro de classe, ce qui correspond exactement à ce que nous avons. Elle effectue également une moyenne automatique sur la taille du minibatch.\n",
"\n",
"Nous limitons également l'entraînement à `samples_to_train` échantillons, afin de ne pas attendre trop longtemps. Nous vous encourageons à expérimenter et à essayer un entraînement plus long, éventuellement sur plusieurs epochs (dans ce cas, vous devrez créer une autre boucle autour de ce code).\n"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Current loss = 4.398899078369141\n",
"today sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr s\n",
"Current loss = 2.161320447921753\n",
"today and to the tor to to the tor to to the tor to to the tor to to the tor to to the tor to to the tor t\n",
"Current loss = 1.6722588539123535\n",
"today and the court to the could to the could to the could to the could to the could to the could to the c\n",
"Current loss = 2.423795223236084\n",
"today and a second to the conternation of the conternation of the conternation of the conternation of the \n",
"Current loss = 1.702607274055481\n",
"today and the company to the company to the company to the company to the company to the company to the co\n",
"Current loss = 1.692358136177063\n",
"today and the company to the company to the company to the company to the company to the company to the co\n",
"Current loss = 1.9722288846969604\n",
"today and the control the control the control the control the control the control the control the control \n",
"Current loss = 1.8705692291259766\n",
"today and the second to the second to the second to the second to the second to the second to the second t\n",
"Current loss = 1.7626899480819702\n",
"today and a security and a security and a security and a security and a security and a security and a secu\n",
"Current loss = 1.5574463605880737\n",
"today and the company and the company and the company and the company and the company and the company and \n",
"Current loss = 1.5620026588439941\n",
"today and the be that the be the be that the be the be that the be the be that the be the be that the be t\n"
]
}
],
"source": [
"net = LSTMGenerator(vocab_size,64).to(device)\n",
"\n",
"samples_to_train = 10000\n",
"optimizer = torch.optim.Adam(net.parameters(),0.01)\n",
"loss_fn = torch.nn.CrossEntropyLoss()\n",
"net.train()\n",
"for i,x in enumerate(train_dataset):\n",
" # x[0] is class label, x[1] is text\n",
" if len(x[1])-nchars<10:\n",
" continue\n",
" samples_to_train-=1\n",
" if not samples_to_train: break\n",
" text_in, text_out = get_batch(x[1])\n",
" optimizer.zero_grad()\n",
" out,s = net(text_in)\n",
" loss = torch.nn.functional.cross_entropy(out.view(-1,vocab_size),text_out.flatten()) #cross_entropy(out,labels)\n",
" loss.backward()\n",
" optimizer.step()\n",
" if i%1000==0:\n",
" print(f\"Current loss = {loss.item()}\")\n",
" print(generate(net))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Cet exemple génère déjà un texte de bonne qualité, mais il peut être encore amélioré de plusieurs façons :\n",
"\n",
"* **Meilleure génération de minibatchs**. La manière dont nous avons préparé les données pour l'entraînement consistait à générer un minibatch à partir d'un seul échantillon. Ce n'est pas idéal, car les minibatchs ont tous des tailles différentes, et certains ne peuvent même pas être générés, car le texte est plus petit que `nchars`. De plus, les petits minibatchs n'exploitent pas suffisamment le GPU. Il serait plus judicieux de prendre un grand bloc de texte à partir de tous les échantillons, de générer ensuite toutes les paires entrée-sortie, de les mélanger, puis de créer des minibatchs de taille égale.\n",
"\n",
"* **LSTM multicouche**. Il est pertinent d'essayer 2 ou 3 couches de cellules LSTM. Comme nous l'avons mentionné dans l'unité précédente, chaque couche de LSTM extrait certains motifs du texte, et dans le cas d'un générateur au niveau des caractères, on peut s'attendre à ce que les couches inférieures du LSTM soient responsables de l'extraction des syllabes, tandis que les couches supérieures s'occupent des mots et des combinaisons de mots. Cela peut être simplement mis en œuvre en passant un paramètre pour le nombre de couches au constructeur LSTM.\n",
"\n",
"* Vous pouvez également expérimenter avec des **unités GRU** pour voir lesquelles donnent de meilleurs résultats, ainsi qu'avec **différentes tailles de couches cachées**. Une couche cachée trop grande peut entraîner un surapprentissage (par exemple, le réseau apprendra le texte exact), tandis qu'une taille trop petite pourrait ne pas produire de bons résultats.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Génération de texte souple et température\n",
"\n",
"Dans la définition précédente de `generate`, nous choisissions toujours le caractère avec la probabilité la plus élevée comme prochain caractère dans le texte généré. Cela avait pour conséquence que le texte \"tournait\" souvent en boucle entre les mêmes séquences de caractères, comme dans cet exemple :\n",
"```\n",
"today of the second the company and a second the company ...\n",
"```\n",
"\n",
"Cependant, si nous examinons la distribution de probabilité pour le prochain caractère, il se peut que la différence entre quelques-unes des probabilités les plus élevées ne soit pas énorme, par exemple un caractère peut avoir une probabilité de 0,2, un autre de 0,19, etc. Par exemple, lorsqu'on cherche le prochain caractère dans la séquence '*play*', le caractère suivant pourrait tout aussi bien être un espace ou **e** (comme dans le mot *player*).\n",
"\n",
"Cela nous amène à la conclusion qu'il n'est pas toujours \"juste\" de sélectionner le caractère avec la probabilité la plus élevée, car choisir le deuxième plus probable pourrait également conduire à un texte cohérent. Il est plus judicieux de **prélever un échantillon** des caractères à partir de la distribution de probabilité donnée par la sortie du réseau.\n",
"\n",
"Ce prélèvement peut être effectué à l'aide de la fonction `multinomial`, qui met en œuvre ce qu'on appelle la **distribution multinomiale**. Une fonction qui implémente cette génération de texte **souple** est définie ci-dessous :\n"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"--- Temperature = 0.3\n",
"Today and a company and complete an all the land the restrational the as a security and has provers the pay to and a report and the computer in the stand has filities and working the law the stations for a company and with the company and the final the first company and refight of the state and and workin\n",
"\n",
"--- Temperature = 0.8\n",
"Today he oniis its first to Aus bomblaties the marmation a to manan boogot that pirate assaid a relaid their that goverfin the the Cappets Ecrotional Assonia Cition targets it annight the w scyments Blamity #39;s TVeer Diercheg Reserals fran envyuil that of ster said access what succers of Dour-provelith\n",
"\n",
"--- Temperature = 1.0\n",
"Today holy they a 11 will meda a toket subsuaties, engins for Chanos, they's has stainger past to opening orital his thempting new Nattona was al innerforder advan-than #36;s night year his religuled talitatian what the but with Wednesday to Justment will wemen of Mark CCC Camp as Timed Nae wome a leaders\n",
"\n",
"--- Temperature = 1.3\n",
"Today gpone 2.5 fech atcusion poor cocles toparsdorM.cht Line Pamage put 43 his calt lowed to the book, that has authh-the silia rruch ailing to'ory andhes beutirsimi- Aefffive heading offil an auf eacklets is charged evis, Gunymy oy) Mony has it after-sloythyor loveId out filme, the Natabl -Najuntaxiggs \n",
"\n",
"--- Temperature = 1.8\n",
"Today plary, P.slan chly\\401 mardregationly #39;t 8.1Mide) closes ,filtcon alfly playin roven!\\grea.-QFBEP: Iss onfarchQ/itilia CCf Zivesigntwasta orce.-Peul-aw.uicrin of fuglinfsut aftaningwo, MIEX awayew Aice Woiduar Corvagiugge oppo esig ThusBratourid canthly-RyI.co lagitems\\eexciaishes.conBabntusmor I\n",
"\n"
]
}
],
"source": [
"def generate_soft(net,size=100,start='today ',temperature=1.0):\n",
" chars = list(start)\n",
" out, s = net(enc(chars).view(1,-1).to(device))\n",
" for i in range(size):\n",
" #nc = torch.argmax(out[0][-1])\n",
" out_dist = out[0][-1].div(temperature).exp()\n",
" nc = torch.multinomial(out_dist,1)[0]\n",
" chars.append(vocab.get_itos()[nc])\n",
" out, s = net(nc.view(1,-1),s)\n",
" return ''.join(chars)\n",
" \n",
"for i in [0.3,0.8,1.0,1.3,1.8]:\n",
" print(f\"--- Temperature = {i}\\n{generate_soft(net,size=300,start='Today ',temperature=i)}\\n\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Nous avons introduit un paramètre supplémentaire appelé **température**, qui est utilisé pour indiquer à quel point nous devons nous en tenir à la probabilité la plus élevée. Si la température est de 1,0, nous effectuons un échantillonnage multinomial équitable, et lorsque la température tend vers l'infini - toutes les probabilités deviennent égales, et nous sélectionnons aléatoirement le prochain caractère. Dans l'exemple ci-dessous, nous pouvons observer que le texte devient dénué de sens lorsque nous augmentons trop la température, et qu'il ressemble à un texte \"cyclé\" généré de manière rigide lorsqu'il se rapproche de 0.\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": "7673cd150d96c74c6d6011460094efb4",
"translation_date": "2025-08-31T15:13:04+00:00",
"source_file": "lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb",
"language_code": "fr"
}
},
"nbformat": 4,
"nbformat_minor": 4
}