TP 11 : LSTM et GRU

Dans ce TP, nous allons apprendre à créer et entrainer des réseaux de type LSTM et GRU pour créer des modèles génératifs.

Dans la première partie, nous allons entrainer un réseau LSTM qui permet de générer du texte “similaire” à des données d’entrainement. Nous utiliserons pour cela le premier livre des Misérables.

Dans la deuxième partie, nous allons entrainer un réseau GRU qui permet de générer des prénoms féminins ou masculins qui “sonnent juste”, mais qui n’existent pas nécessairement. Nous utiliserons pour cela le jeu de données de prénoms anglophones trouvé sur http://www.cs.cmu.edu/afs/cs/project/ai-repository/ai/areas/nlp/corpora/names/ (mais vous pouvez essayer avec d’autres listes de prénoms, ces dernières sont faciles à trouver).

Les librairies nécessaires sont les suivantes :

import numpy as np
import pandas as pd
import torch
from torch import nn
from torch.utils.data import Dataset, DataLoader
import spacy
import string
# Permet d'afficher le texte avec une certaine largeur
import textwrap

Enregistrons le dispositif de calcul dans une variable.

device = "cuda" if torch.cuda.is_available() else "cpu"
print(device)
cuda

1 Génération de textes avec LSTM

Dans cette partie, nous allons créer un modèle de génération de textes qui sera entrainé sur le premier livre des Misérables (cf. TP 10). Ce modèle sera constitué de plusieurs couches LSTM successives, et devra être entrainé à prédire le prochain token d’une séquence en fonction des tokens précédents.

On commence par créer une classe héritée de torch.utils.data.Dataset pour pouvoir générer des exemples d’entrainement.

Un entrée/sortie de ce dataset sera constitué de deux séquences, tirées de notre document, de taille seq_len. La séquence de sortie sera décalée d’un token sur la droite par rapport à l’entrée (voir l’exemple plus bas). Ainsi, chaque token de la séquence d’entrée sera associé avec le token suivant.

Nous allons utiliser un modèle de langage de Spacy pour tokeniser notre document.

class SentenceData(Dataset):
    def __init__(self, file_path, nlp, seq_len):

        # Le chemin d'accès au fichier
        self.file_path = file_path
        # La longueur des séquences d'entrée et sortie
        self.seq_len = seq_len

        # On ouvre notre fichier et on crée un object Spacy
        with open(self.file_path, "r") as f:
          text = f.read()
        doc = nlp(text)

        # On découpe notre texte en tokens, sans les espaces
        self.tokens = [token.text for token in doc if not token.is_space]

        # On enregistre le vocabulaire utilisé, et on crée des dictionnaires
        # permettant de transformer chaque token en identifiant numérique,
        # ou l'inverse.
        self.dictionary = list(set(self.tokens))
        self.id2token = {id: token for id, token in enumerate(self.dictionary)}
        self.token2id = {token: id for id, token in enumerate(self.dictionary)}

        # On transforme notre corpus en une séquence de valeurs numériques
        self.token_ids = [self.token2id[w] for w in self.tokens]

    # La taille de notre dataset est le nombre de séquences possibles
    def __len__(self):
        return len(self.token_ids) - self.seq_len

    # Un élément de notre corpus sera la séquence id et la séquence id+1.
    def __getitem__(self, id):
        return (torch.tensor(self.token_ids[id:id+self.seq_len]),
                torch.tensor(self.token_ids[id+1:id+self.seq_len+1]))

On charge le modèle de langage de Spacy.

# !spacy download "en_core_web_sm"
nlp = spacy.load("en_core_web_sm")

On crée une instance de notre classe SentenceData, en utilisant le premier livre de notre corpus et en le divisant en séquences de 10 tokens.

doc_path = "/content/drive/MyDrive/Colab Notebooks/ml_data/TP11/book_01.txt"
example_doc = SentenceData(doc_path, nlp, 10)

Notre document contient 28932 paires de séquences.

len(example_doc)
28932

On peut examiner un exemple en particulier, en traduisant les séquences numériques en mots.

sample_id = 0
input, output = example_doc[sample_id]
print(" ".join([example_doc.id2token[id.item()] for id in input]))
print(" ".join([example_doc.id2token[id.item()] for id in output]))
In 1815 , M. Charles - François - Bienvenu Myriel
1815 , M. Charles - François - Bienvenu Myriel was

On crée un objet torch.utils.data.Dataloader pour parcourir notre base de données, avec une taille de batch donnée.

batch_size = 128
my_dataloader = DataLoader(example_doc, batch_size=batch_size)

Il est temps de créer notre modèle. Ce dernier prendra notre séquence d’entrée et sera consititué de :

  • Une couche d’embedding, qui transforme nos identifiants numériques en vecteurs one-hot de taille n_vocab, puis en vecteur de taille embedding_dim.
  • num_layers couches de LSTM, avec des vecteurs d’état caché et d’état de cellule de taille hidden_size. On peut ajouter un dropout aux couches, afin que ces dernières n’apprennent pas le texte entièrement par coeur.
  • Une couche entièrement connectée, avec n_vocab sorties.

La sortie de ce réseau donnera les log-odds pour les mots suivants, qui peuvent entre converties en probabilités.

Notez que la longueur de la séquence n’a pas besoin d’être précisée ici. Pytorch se chargera de prendre tous les éléments reçus, de “déplier” le réseau en fonction de leur nombre, et de calculer les sorties en transmettant les états entre les cellules.

Remarquez également que nous précisons avec batch_first=True, lors de la création des couches LSTM, que notre batch se situe sur la première dimension de nos tenseurs. Ainsi, Pytorch comprend que la séquence se situe sur la deuxième dimension des tenseurs (voir https://pytorch.org/docs/stable/generated/torch.nn.LSTM.html).

class SeqGen(nn.Module):
    def __init__(self, n_vocab, embedding_dim, hidden_size, num_layers):
        super(SeqGen, self).__init__()

        # On sauve les paramètres dans des attributs
        self.n_vocab = n_vocab
        self.embedding_dim = embedding_dim
        self.hidden_size = hidden_size
        self.num_layers = num_layers

        # La couche d'embedding
        self.embedding = nn.Embedding(
            num_embeddings=self.n_vocab,
            embedding_dim=self.embedding_dim,
        )
        # Les couches LSTM
        self.lstm = nn.LSTM(
            input_size=self.embedding_dim,
            hidden_size=self.hidden_size,
            num_layers=self.num_layers,
            dropout=0.2,
            batch_first=True
        )
        # La couche entièrement connectée
        self.lin_layer = nn.Linear(self.hidden_size, self.n_vocab)

    # La fonction foward peut prendre, en plus des entrées,
    # l'état caché et l'état de la cellule utilisé au début de la séquence.
    # Par défaut, ces états sont posés comme None.
    def forward(self, x, prev_state=None):
        embed = self.embedding(x)
        output, state = self.lstm(embed, prev_state)
        logodds = self.lin_layer(output)
        return logodds, state

On crée une instance de notre modèle, avec nombre de mots correspondant à notre vocabulaire, embedding de 128 et 3 couches de LSTM avec états de taille 128.

seq_gen = SeqGen(len(example_doc.dictionary), 128, 128, 3)

Nous allons utiliser l’entropie croisée et l’optimisateur Adam pour entrainer notre modèle.

loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(seq_gen.parameters(), lr=0.001)

On entraine maintenant le modèle. Notez qu’aucun état caché est donné à notre modèle avant chaque batch, ce qui veut dire que les états initiaux seront nuls avant chaque séquence. L’entrainement est relativement simple, car la surparamétrisation n’est pas très importante pour notre modèle.

# Le nombre d'epochs
n_epochs = 30

# On met le modèle sur le dispositif de calcul
seq_gen.to(device)
# On le met en mode entrainement
seq_gen.train()
# On boucle sur les epochs
for epoch in range(n_epochs):

  print(f"Epoch {epoch+1}", end=": ")

  # Pour calculer la perte moyenne
  sum_loss = 0
  # On boucle sur notre dataloader
  for input, output in my_dataloader:

    # Les entrées et sorties sont mises sur le dispositif de calcul
    input = input.to(device)
    output = output.to(device)

    # On met à zéro les gradients
    optimizer.zero_grad()

    # On fait les prédictions
    pred, _ = seq_gen(input)
    # On calcule la perte, en transposant nos résultats
    loss = loss_fn(pred.transpose(1, 2), output)

    # On fait une itération de descente du gradient
    loss.backward()
    optimizer.step()

    # On ajoute la perte
    sum_loss += loss.item()

  print(f"mean loss = {sum_loss / len(my_dataloader):.4f}")
Epoch 1: mean loss = 6.7487
Epoch 2: mean loss = 6.2715
Epoch 3: mean loss = 6.1804
Epoch 4: mean loss = 6.0350
Epoch 5: mean loss = 5.8529
Epoch 6: mean loss = 5.6564
Epoch 7: mean loss = 5.4718
Epoch 8: mean loss = 5.3008
Epoch 9: mean loss = 5.1350
Epoch 10: mean loss = 4.9768
Epoch 11: mean loss = 4.8290
Epoch 12: mean loss = 4.6939
Epoch 13: mean loss = 4.5696
Epoch 14: mean loss = 4.4485
Epoch 15: mean loss = 4.3354
Epoch 16: mean loss = 4.2326
Epoch 17: mean loss = 4.1389
Epoch 18: mean loss = 4.0545
Epoch 19: mean loss = 3.9665
Epoch 20: mean loss = 3.8858
Epoch 21: mean loss = 3.8075
Epoch 22: mean loss = 3.7356
Epoch 23: mean loss = 3.6717
Epoch 24: mean loss = 3.6004
Epoch 25: mean loss = 3.5305
Epoch 26: mean loss = 3.4642
Epoch 27: mean loss = 3.3958
Epoch 28: mean loss = 3.3308
Epoch 29: mean loss = 3.2676
Epoch 30: mean loss = 3.2095

On va maintenant donner à notre modèle un début de séquence et voir comment il génère une suite. Le processus se passe en deux étapes :

  • On passe une première fois la séquence initiale dans notre modèle, afin de générer le prochain token et pour récupérer l’état caché résultant.
  • On va ensuite faire une boucle pour les tokens restants, en passant dans le réseau le dernier token et états obtenus à l’étape précédente.

Pour générer chaque nouveau token, on transforme les log-odds en probabilités, puis on effectue un tirage aléatoire selon ces dernières.

Le séquence finale est affichée formattée.

# Séquence initiale
input_sentence = "When the man"
# Nombre de tokens désiré
n_generated_tokens = 600

# --- Etape 1

# On coupe notre séquence intiale pour créer notre séquence de sortie
output_tokens = input_sentence.split()
# On met le modèle en mode évaluation
seq_gen.eval()
# Notre séquence d'entrée est transformée en identifiants numériques
input = torch.tensor([example_doc.token2id[token]
                      for token in output_tokens]).to(device)
# On passe nos entrées dans notre modèle, en ne donnant aucun état.
pred, hidden = seq_gen(input)
# On garde les logodds du dernier mot. Notez que nous utilisons
# detach().cpu() pour ne pas garder le graph de calcul et pour
# basculer le tenseur sur le cpu.
new_token_logodds = pred.detach().cpu()[-1]
# Les probabilités sont calculées avec un softmax
probs = torch.nn.functional.softmax(new_token_logodds, dim=0).numpy()
# On tire aléatoirement le token suivant, avec les probabilités précédentes
token_index = np.random.choice(len(new_token_logodds), p=probs)
# On l'ajoute à notre sortie
output_tokens.append(example_doc.id2token[token_index])

# --- Etape 2

# On boucle sur le nombre restant de tokens demandés
for i in range(n_generated_tokens - 1):
  # Notre dernier token est transformé en entrée
  input = torch.tensor([example_doc.token2id[output_tokens[-1]]]).to(device)
  # On passe notre entrée, avec les états précédants, dans notre modèle
  pred, hidden = seq_gen(input, hidden)
  # On détache, met sur le cpu et applatit la sortie
  new_token_logodds = pred.detach().cpu().flatten()
  # Les probabilités sont calculées avec un softmax
  probs = torch.nn.functional.softmax(new_token_logodds, dim=0).numpy()
  # On tire aléatoirement, avec les probabilités calculées, le token suivant
  token_index = np.random.choice(len(new_token_logodds), p=probs)
  # On l'ajoute à notre sortie
  output_tokens.append(example_doc.id2token[token_index])

# On affiche notre sortie formatée
print(textwrap.fill(" ".join(output_tokens), 79))
When the man is , the midst of no ignorant about other time , that had still be
neither by his gallican . These variety which are a peasants for a carriage ,
which is not only a magnificent revere with only lucky , and and ten -
philosophy of those three man . Win which are single HOUSEHOLD with that
harshness , which is forced to be a little man , he than he respected no hall
was avoided . ” This girls , as in rusticity with nonsense , without Brignolles
desirable , create from unity into indulgent during carriage , so , with its
dangerous detested , very widely existed , he gave through in his old mitres .
Each of the drawing - room of his man ? ” “ That is interrogated from the Duke
are of these - peasants ; he can feel bear somewhat working - kill men , and
becomes be expressing in walks Bienvenu ; there is nothing for its atheist at
the outset appeared for sixty audaciously two two hours of our conversation
which pleased that is its house ; probably strange of escape of pale ,
everywhere we have none slowly , which is peculiar to matter , that he was
yonder , still by what near that he angel settled on the despot , but the road
if that Madame from not become a heart with his sister , them , speaking for
two power , which slipped is neither in receipt of recognized,—that at great
three wrote . This jurisdiction to anguish forth it , authorized that vaguely
had attempts come at back , which may be amavit_,—because a sacred days for I
time into pursued , so so also how to ’ call it delivery of going , from a
demand , is up at him to wish , deceiving invented groups in an acre , beneath
rather years is not a sacred men held alone upon which is at turn , Monsieur ,
below , justice , at success , we come it takes , behold , than that from the
hatred of France ; softened strange alone who was shoe - lys , and performs the
olden agony , in short , above the midst of that gardens was a shadow of deeds
, for its pastoral non accompanied presented for your heart for his regimentals
, and far was long into his day . Gregory die , were made by his incurable
which were understood , which was plump for its life , above the transformation
the fate of His Dauphiné ; let it give to one persons ; as they often rising
since but thirty - cart ; he knew nothing with liberty , even in chaplaincies ,
as we most so - lines , was contemplation and Pascal , far from lucky , and to
arouse those , without seeking to condense he gazed for him to be a worthy
dreamed of bitterness , which Lucretius , oh for his things , and seeing an
degree a cap hour of D — — _ I will be elsewhere . We analyzes on tolerable at
the verge of Mousqueton misery , which seemed deciphering to mark ; his broad
after grave reaction , the precipices of idioms . His little arm , then he had
planted than — with his whole society which one story as on the stories which
had stand formed but when also the first away which entered into being wounded
in the intelligent waited , has there was its rascal ;

2 Génération de prénoms avec GRU

Maintenant, nous allons utiliser les GRU pour créer des modèles de génération de prénoms, en utilisant des données de prénoms féminins et masculins. Le principe reste assez similaire à la partie précédente, mais nous adopterons une programmation de style plus fonctionnel.

On commence par créer une classe héritée de torch.utils.data.Dataset qui va donner des exemples constitués d’un nom comme entrée et du même nom décalé d’une lettre sur la droite comme sortie. Comme caractère de fin de séquence et de padding, nous allons utiliser le caractère "\n" déjà présent à la fin de chaque nom dans le fichier (sauf du dernier). Nous effectuons un padding sur tous les prénoms afin que tous fassent la taille du plus long prénom (pour entrainer les modèles avec des batchs).

class NameData(Dataset):
    def __init__(self, file_path):
        # On enregistre le chemin du fichier
        self.file_path = file_path
        # On ouvre le fichier et on lit les lignes
        with open(file_path, "r") as f:
          self.lines = f.readlines()
        # On ajoute notre caractère de padding à la dernière ligne
        self.lines[-1] += "\n"
        # La longueur maximale est la longueur de la plus grande ligne
        self.max_len = max([len(line) for line in self.lines])
        # Les lignes sont complétées avec "\n" pour avoir la même taille
        self.lines = [line + "\n"*(self.max_len - len(line))
                      for line in self.lines]
        # On met les lignes en minuscules
        self.lines_low = [line.lower() for line in self.lines]
        # On sauve le nombre de lettres différentes
        self.letters = list(set("".join(self.lines_low)))
        # On crée notre liste de nom, avec chaque lettre
        # comme élément d'une liste
        self.names = [list(line) for line in self.lines_low]

        # Ces fonctions permettent de transformer les lettres en identifiants
        # numériques, ou vice-versa.
        self.id2letter = {id: letter for id, letter in enumerate(self.letters)}
        self.letter2id = {letter: id for id, letter in enumerate(self.letters)}

        # On sauve l'identifiant du caractère de padding
        self.endl_id = self.letter2id["\n"]

        # On transforme nos listes de lettres en listes d'ids
        self.names_ids = [[self.letter2id[l] for l in name]
                          for name in self.names]

    # La longueur du dataset
    def __len__(self):
        return len(self.names_ids)

    # Un item est une entrée/sortie avec, respectivement,
    # prénom sans la dernière lettre, prénom sans la première lettre.
    def __getitem__(self, id):
        return (torch.tensor(self.names_ids[id][:-1]),
                torch.tensor(self.names_ids[id][1:]))

On crée maintenant nos instances contenant nos jeux de données.

female_path = "/content/drive/MyDrive/Colab Notebooks/ml_data/TP11/female.txt"
male_path = "/content/drive/MyDrive/Colab Notebooks/ml_data/TP11/male.txt"
female_data = NameData(female_path)
male_data = NameData(male_path)

Regardons un exemple.

input, output = female_data[0]
print([female_data.id2letter[id.item()] for id in input])
print([female_data.id2letter[id.item()] for id in output])
['a', 'b', 'a', 'g', 'a', 'e', 'l', '\n', '\n', '\n', '\n', '\n', '\n', '\n', '\n']
['b', 'a', 'g', 'a', 'e', 'l', '\n', '\n', '\n', '\n', '\n', '\n', '\n', '\n', '\n']

Nous créons maintenant la classe de notre modèle. Ce dernier passe directement les vecteurs one-hot dans les couches GRU, car le nombre de caractères existants n’est pas très élevé (la dimensionnalité des vecteurs one-hot est déjà basse). Le modèle est constitué de :

  • num_layer couches GRU avec états cachés de taille hidden_size.
  • Une couche entièrement connectée qui envoie les états cachés finaux sur la taille du vocabulaire.

Le résultat du modèle sont les log-odds des prochains caractères.

class NameGen(nn.Module):
    def __init__(self, n_vocab, hidden_size, num_layers):
        super(NameGen, self).__init__()

        # On enregistre les paramètres du modèle
        self.n_vocab = n_vocab
        self.hidden_size = hidden_size
        self.num_layers = num_layers

        # Les couches GRU
        self.gru = nn.GRU(
            input_size=self.n_vocab,
            hidden_size=self.hidden_size,
            num_layers=self.num_layers,
            batch_first=True
        )
        # La couche entièrement connectée
        self.lin_layer = nn.Linear(self.hidden_size, self.n_vocab)

    def forward(self, x, prev_state=None):
        # Cette fonction transforme les entiers en vecteurs one-hot
        one_hot = nn.functional.one_hot(x, num_classes=self.n_vocab)
        # On passe dans les couches GRU
        output, state = self.gru(one_hot.to(torch.float), prev_state)
        # Les log-odds résultantes
        logodds = self.lin_layer(output)
        return logodds, state

Nous instancions un modèle et lui passons un exemple, pour voir si tout fonctionne.

female_name_gen = NameGen(len(female_data.letters), 20, 2)
female_name_gen(input)[0].shape
torch.Size([15, 30])

Dans l’idée de prendre une approche fonctionnelle, nous définissons ici la fonction qui permet d’entrainer un modèle.

def train_model(model, data, batch_size, n_epochs, device):

  # On définit la fonction de perte et l'optimisateur
  loss_fn = nn.CrossEntropyLoss()
  optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

  # On crée le dataloader
  dataloader = DataLoader(data, batch_size, shuffle=True)

  # On bascule le modèle en mode entrainement
  model.train()
  # On le met sur le dispositif de calcul
  model.to(device)
  # La boucle d'entrainement
  for epoch in range(n_epochs):

    print(f"Epoch {epoch+1}", end=": ")

    # Pour calculer la perte moyenne
    sum_loss = 0
    # La boucle sur les batchs
    for input, output in dataloader:

      # On met l'entrée et la sortie sur le dispositif de calcul
      input = input.to(device)
      output = output.to(device)

      # On met les gradients à zéro, on fait des prédictions et on
      # calcule la perte
      optimizer.zero_grad()
      pred, _ = model(input)
      loss = loss_fn(pred.transpose(1, 2), output)

      # On effectue la descente du gradient et on cumule les pertes
      loss.backward()
      optimizer.step()
      sum_loss += loss.item()

    print(f"mean Loss = {sum_loss / len(dataloader):.4f}")

On utilise maintenant la fonction créée pour entrainer un modèle avec les prénoms féminins.

female_name_gen = NameGen(len(female_data.letters), 30, 3)
train_model(female_name_gen, female_data, 16, 20, device)
Epoch 1: mean Loss = 1.3447
Epoch 2: mean Loss = 0.9837
Epoch 3: mean Loss = 0.9192
Epoch 4: mean Loss = 0.8817
Epoch 5: mean Loss = 0.8639
Epoch 6: mean Loss = 0.8501
Epoch 7: mean Loss = 0.8355
Epoch 8: mean Loss = 0.8218
Epoch 9: mean Loss = 0.8073
Epoch 10: mean Loss = 0.7915
Epoch 11: mean Loss = 0.7783
Epoch 12: mean Loss = 0.7679
Epoch 13: mean Loss = 0.7596
Epoch 14: mean Loss = 0.7520
Epoch 15: mean Loss = 0.7449
Epoch 16: mean Loss = 0.7381
Epoch 17: mean Loss = 0.7322
Epoch 18: mean Loss = 0.7269
Epoch 19: mean Loss = 0.7216
Epoch 20: mean Loss = 0.7163

Idem pour les prénoms masculins.

male_name_gen = NameGen(len(male_data.letters), 30, 3)
train_model(male_name_gen, male_data, 16, 20, device)
Epoch 1: mean Loss = 1.5680
Epoch 2: mean Loss = 1.1217
Epoch 3: mean Loss = 1.0467
Epoch 4: mean Loss = 1.0271
Epoch 5: mean Loss = 1.0135
Epoch 6: mean Loss = 0.9924
Epoch 7: mean Loss = 0.9527
Epoch 8: mean Loss = 0.9259
Epoch 9: mean Loss = 0.9137
Epoch 10: mean Loss = 0.9059
Epoch 11: mean Loss = 0.8993
Epoch 12: mean Loss = 0.8922
Epoch 13: mean Loss = 0.8839
Epoch 14: mean Loss = 0.8756
Epoch 15: mean Loss = 0.8684
Epoch 16: mean Loss = 0.8619
Epoch 17: mean Loss = 0.8563
Epoch 18: mean Loss = 0.8508
Epoch 19: mean Loss = 0.8452
Epoch 20: mean Loss = 0.8400

Créons maintenant une fonction qui permet de générer un prénom en fonction des premiers caractères. La procédure est très similaire à la génération de tokens, mais on va arrêter la génération dès que notre modèle prédit le caractère “”, signifiant la fin de séquence.

def generate_name(init_letters, model, data, device):

  # --- Etape 1

  output_letters = list(init_letters)
  model.eval()
  input = torch.tensor([data.letter2id[letter]
                        for letter in output_letters]).to(device)
  pred, hidden = model(input)
  new_letter_logodds = pred.detach().cpu()[-1]
  probs = torch.nn.functional.softmax(new_letter_logodds, dim=0).numpy()
  next_letter_id = np.random.choice(len(new_letter_logodds), p=probs)
  output_letters.append(data.id2letter[next_letter_id])

  # --- Etape 2

  # On boucle tant que l'on a pas généré "\n"
  while not output_letters[-1] == "\n":
    input = torch.tensor([data.letter2id[output_letters[-1]]]).to(device)
    pred, hidden = model(input, hidden)
    new_letter_logodds = pred.detach().cpu().flatten()
    probs = torch.nn.functional.softmax(new_letter_logodds, dim=0).numpy()
    next_letter_id = np.random.choice(len(new_letter_logodds), p=probs)
    output_letters.append(data.id2letter[next_letter_id])

  return "".join(output_letters)[:-1]

Testons la fonction de génération avec un début de prénom.

generate_name("lun", female_name_gen, female_data, device)
'luna'

Créons une liste conséquente de prénoms féminins, pour toutes les lettres de l’alphabet.

init_letters = string.ascii_lowercase
n_gen = 10
female_names = []
for init_letter in init_letters:
  letter_female_names = []
  for _ in range(n_gen):
    female_name = generate_name(init_letter, female_name_gen,
                                female_data, device)
    letter_female_names.append(female_name)

  female_names.extend(letter_female_names)
  print(f"{init_letter}: {', '.join(letter_female_names)}")
a: aulyll, argesta, aumel, anderitte, alicra, andel, alli, argyel, aletra, aurelle
b: bianthita, brab, billen, braelie, brilly, baidia, brimtra, bmristine, bimtrina, berjianne
c: chella, celiy, cirthele, cavissin, cieh, cathrie, carde, carhelle, cas, cath
d: dvarri, demin, dirky, dandi, dess, deflynn, darley, dellie, dwalcathanl, daletia
e: ellbie, eusty, elbarina, elorie, elica, elena, etincisa, edette, elma, elobetta
f: frenney, faverinie, fyaara, flagie, franmy, fekdia, frandar, fror, frynnie, frandsa
g: gome, gony, goridaul, gertia, gickie, gay, gea, gorelei, granni, gunjeentins
h: hilys, hally, henetta, halene, hivfrindte, hollica, hemhil, hosfits, helprina, hillane
i: ilballata, idellka, iella, ida, imecella, inna, igma, ilisra, ipy, iborta
j: jisanph, jonkti, jerista, jore, jopa, julissanne, junor, jeann, jioonna, jeanne
k: kast, karlece, ketty, kaulle, keltabe, kardy, kathine, kelli, kags, katbel
l: lili, lotrie, laile, lillinne, layby, lorah, lorea, loranna, leacive, lerin
m: mlaygea, mangqie, merni, madcie, meran, marmerta, meadonn, margul, marree, myela
n: nidy, nagis, neria, nyxy, ninssia, nandu, nic, nanes, ninny, nandie
o: olmeria, opleo, obby, osatinta, onamire, onanetta, oilveh, ondya, olanre, odelin
p: pathren, pellette, palleen, porth, porigabi, pardote, pellyns, phicy, pasi, pil
q: quelajia, quabine, quinnah, quannex, queena, querhy, quannel, qlishella, qomeilrine, quenelle
r: robalva, rosie, rlosella, reedeia, ribritta, rozann, rilli, renolend, rovena, ropav
s: shya, shir, sun, shacy, shin, shean, sharlene, shati, systin, snella
t: tori, tade-lea, tubpida, trofi, tissay, tesfiida, tongia, terice, thily, terri
u: unna, uuthorha, uxlianda, ully, umi, uggarlyn, ulisal, uminetta, udofalee, uobel
v: velissa, vilena, vortyn, vagkardina, vagrie, va, varinde, violia, vyndean, vibin
w: wonnana, wynna, wilisa, wel, winnye, wan, wille, walllie, wen, willabebaria
x: xuenita, xilisa, xixela, xonbelle, xanettie, xiy, xaina, xic, xalorine, xoneth
y: yliesa, yorina, yeen, ytabetta, yletta, ylethe, yienda, yphine, ylol, yleenda
z: zatta, zzesta, zoron, zanci, zetse, ziby, zeora, zel, zare, zera

Idem pour les prénoms masculins.

init_letters = string.ascii_lowercase
n_gen = 10
male_names = []
for init_letter in init_letters:
  letter_male_names = []
  for _ in range(n_gen):
    male_name = generate_name(init_letter, male_name_gen,
                              male_data, device)
    letter_male_names.append(male_name)

  male_names.extend(letter_male_names)
  print(f"{init_letter}: {', '.join(letter_male_names)}")
a: amgrin, avlob, awe, andord, albrik, anled, ake, anrinke, amreys, anfourch
b: barrerd, birroch, branol, baiyron, berd, bim, bergie, bevold, byy, berces
c: conl, chamofe, chupbaldon, cqamue, chreldie, crarnih, cbeitt, cirryy, cerese, carckar
d: dorer, dinterg, dipby, doran, distandy, darmreen, docen, dave, dumrel, daril
e: enmaet, eaxulm, efter, eflilno, erdand, elury, ekaal, erakly, evelf, etten
f: furfie, fremlon, furech, firlal, fyy, fungian, frestar, famt, farmarlie, frin
g: gawue, garstan, goanfee, gametten, gugdeeb, golwrinl, grenom, gaale, gormy, garrinn
h: hasthin, hich, heeras, hard, hyy, ho, hilmy, hybon, horrie, hurtaldie
i: irbertan, ironho, ilstico, igole, ijhy, iounlis, ilisp, iduci, iscorthorian, itun
j: joyras, jeynew, javerosor, jovof, jheuamunn, jance, jerripson, jrandy, jiabe, jaal
k: kepkartz, kafvorcop, kacher, kaospy, kas, kan, komasten, khinog, karpos, kavian
l: long, likd, lov, labward, lulocil, lent, loydanwon, libersun, lerriv, less
m: merak, marmand, merbel, marly, mumola, muanfan, moblyy, michon, minno, marwishen
n: noxrien, naokie, nemny, niefian, netaldus, nennerlo, nrett, nones, neun, neymie
o: ogbarriih, orn, o, olly, obal, orily, oefsos, onborgo, orcansrik, oshady
p: peerfuph, peyik, pachin, penk, paoby, padberton, pandar, pamweem, pronckeucl, peryer
q: qaynie, qaalnlers, qakilly, qalgon, qabiys, qeiiqonite, qandiif, qareby, quigkin, qagiit
r: redman, rerdene, revastasey, ralen, redras, rub, ragih, rars, rtiodarte, relligi
s: shadtarard, sermon, sbobry, seras, silmante, sorriy, shefce, shurdy, smichy, sogdie
t: tramrer, taodie, tregas, tarbie, tocties, theonler, tlepees, taxsor, toyne, tinberel
u: unnie, ulnelulce, udlaston, umis, unnant, urens, ulosom, ulletdeo, ulicmar, ubkdalce
v: vaydoun, vidy, vin, vamius, veowar, veanris, vrastom, vadtico, vicpy, vom
w: wanren, wodein, wintichas, wemwennon, wens, wabeny, wods, wav, wrinpe, wonnich
x: xille, xadmiol, xifdan, xovo, xalfy, xuchor, xamar, xipie, xammie, xater
y: yranlily, yals, yodia, ylnill, yanny, yit, ynewen, ygeran, yeris, yict
z: zerabney, zuc-xogoron, zom, zaviy, zork, zobdaule, zanimy, zenig, zikres, zege

On peut maintenant regarder si notre générateur a créé des prénoms qui se trouvaient dans nos listes. Notre modèle est intéressant s’il crée quelques prénoms existants, mais pas en majorité. S’il ne crée que des prénoms existants, c’est qu’il a appris notre liste par coeur et est beaucoup trop entrainé.

female_found_names = [name for name in female_names
                      if name + "\n"*(female_data.max_len - len(name))
                      in female_data.lines_low]
print(textwrap.fill(", ".join(female_found_names), 79))
alli, elena, gay, hally, ida, inna, jeanne, ketty, kelli, rosie, shir,
sharlene, tori, terri
male_found_names = [name for name in male_names
                    if name + "\n"*(male_data.max_len - len(name))
                    in male_data.lines_low]
print(textwrap.fill(", ".join(male_found_names), 79))
dave, vin