Ce poste est un tutoriel pour coder une architecture basique d’un réseau de neurones qui traduit un texte d’une langue à un autre. Il s’adresse à des lecteurs désireux d’apprendre le Machine Learning et qui ne connaissent pas ou peu les cellules récurrentes. Cependant on suppose que le lecteur connaît déjà les principes de base des réseaux de neurones (backpropagation, fonction d’activation, perceptron multicouche) ainsi que l’utilisation de la librairie keras (en particulier l’utilisation des Tensor, Layer et Model).
Après avoir introduit les données d’entraînement, nous présenterons le principe des réseau de neurones récurrents. C’est un type de réseau qui utilise des opérations récursives (appelées Recurent Layer en anglais), et qui est adapté à tout type de données présentant une composante temporelle ou timestep. Ensuite nous présenterons l’architecture du réseau basé sur un encodeur et un décodeur. Et finalement nous verrons comment l’entraîner et faire une inférence.
Préparation des données d’entrainement
Nous allons nous intéresser à la traduction de l’anglais vers le français. Il nous faut donc un jeux de données qui contienne des phrases en anglais avec leurs traductions françaises. On peux obtenir un tel jeu de manière gratuite sur le site suivant http://www.manythings.org/anki/.
Chargement en DataFrame
On téléchargera le fichier fra-eng.zip. C’est un fichier .txt de type tsv (Tabulation-separated values), c’est-à-dire que les colonnes sont séparées par une tabulation. Il y a trois colonnes : la phrase en anglais, la phrase en français et une indication sur l’attribution de la traduction. Cette dernière colonne ne nous servira pas.
Ensuite nous allons importer les données sous forme d’un pandas.DataFrame. Comme il y a des phrases très longues dans les données (plus de 300 caractères!), pour simplifier la présentation on ne prendra que les dix milles premières lignes, ce qui limite les phrases à environ 30 caractères.
1 2 3 4 | import pandas Data = pandas.read_csv("fra.txt", sep = "\t", names = ["EN","FR"], usecols=[0,1], nrows=10000) print(Data.sample(50)) ## Affichage d'un échantillon des données |
3465 Are you tall? Es-tu grande ?
3178 We must run. Il nous faut courir.
3759 I can't sing. Je ne sais pas chanter.
7922 Don't tease me. Ne me taquine pas !
5521 He pinched me! Il m'a pincé !
5729 I fed the dog. J'ai nourri le chien.
1691 Let's talk. Discutons !
8597 I need it ASAP. J'en ai besoin aussi vite que possible.
9075 It didn't help. Cela n'a pas aidé.
4424 Ladies first. Les femmes d'abord.
582 Come here. Venez là.
8885 I'm going bald. Je deviens chauve.
480 Say what? De quoi ?
694 I give up. J'abandonne.
198 Keep it. Gardez-le !
5139 You're upset. Tu es contrariée.
7360 Were you hurt? As-tu été blessé ?
2779 It's my job. C'est mon emploi.
2007 Are you Tom? Êtes-vous Tom ?
4826 Tom's stoned. Tom est stone.
1888 We like it. Nous l'apprécions.
7719 Can you juggle? Savez-vous jongler ?
4793 Tom is there. Tom est là-bas.
8936 I'm not skinny. Je ne suis pas maigrichonne.
9087 It looked real. Ça avait l'air réel.
380 I talked. J’ai parlé.
9433 She came alone. Elle est venue seule.
3419 Are you busy? Es-tu occupée ?
9008 I've messed up. J'ai merdé.
8277 I can't see it. Je ne peux pas la voir.
3435 Are you hurt? T'es-tu blessé ?
7844 Do you miss it? Ça te manque ?
4104 I'm a surfer. Je suis surfeuse.
300 Drive on. Continue à rouler !
4857 We all cried. Nous avons tous pleuré.
2486 I waited up. J'ai veillé.
5476 He got caught. Il s'est fait prendre.
8778 I won't forget. Je n'oublierai pas.
4825 Tom's stoned. Tom est défoncé.
4141 I'm homesick. J'ai le mal du pays.
8237 I am too short. Je suis trop petit.
2638 I'm not sad. Je ne suis pas triste.
8992 I'm very tired. Je suis très fatigué.
6104 I wasn't busy. Je n'étais pas occupé.
5594 Hold the door. Retiens la porte !
3231 What a bore! Quelle emmerdeuse !
5658 I can buy one. Je peux en acheter une.
8219 How's your son? Comment va ton fils ?
1761 Stay still. Ne bouge pas.
5152 A car hit Tom. Tom a été renversé par une voiture.
Encodage des données
Afin de faire passer ces données dans un réseau de neurones, il faut les transformer en des vecteurs de nombres. Pour cela, nous allons considérer chaque caractère comme un jeton (token en anglais) et le coder par un nombre, sachant que les espaces et les ponctuations comptent pour un caractère. De plus on ne fera pas de distinction en une lettre minuscule et sa lettre majuscule.
Heureusement la librairie keras admet la classe keras.
D’abord avant d’utiliser le Tokenizer, il faut créer deux jetons spéciaux l’un indiquant le début d’une phrase et l’autre indiquant la fin (les raisons de la nécessité de ces jetons seront expliquées plus bas). Comme les caractères ‘\t’ et ‘\n’ ne sont jamais utilisés, on prendra le premier comme jeton indiquant le début et l’autre comme indiquant la fin. Ensuite il instancier un Tokenizer avec ses paramètres, puis appeler la méthode .fit sur un corpus de texte, qui est une liste de string, pour créer le dictionnaire.
L’instanciation du Tokenizer se fait avec de nombreux paramètres, mais les plus importants sont les suivants :
- lower (booléen, True par défaut) : si True les lettres majuscules sont considéreés comme des lettres minuscules.
- char_level (booléen, False par défaut) : si True chaque caractère est considéré comme un jeton et si False chaque mot est considéré comme un jeton.
1 2 3 4 5 6 7 | import keras Data['EN'] = Data['EN'].apply( lambda x : '\t{}\n'.format(x) ) ## Ajout des jetons de début et de fin Data['FR'] = Data['FR'].apply( lambda x : '\t{}\n'.format(x) ) ## Ajout des jetons de début et de fin Tokenizer = keras.preprocessing.text.Tokenizer(lower=True, char_level=True) Tokenizer.fit_on_texts(list(Data['EN']) + list(Data['FR'])) |
Une fois le Tokenizer construit, on peut récupérer le dictionnaire via l’attribut .word_index.
1 | print(Tokenizer.word_index) |
'l': 14, 'm': 15, 'd': 16, 'c': 17, "'": 18, 'h': 19, 'p': 20, 'v': 21, 'y': 22, 'j': 23, 'g': 24, 'é': 25,
'w': 26, 'f': 27, '?': 28, 'b': 29, '!': 30, 'k': 31, '-': 32, 'z': 33, 'q': 34, 'x': 35, 'ê': 36, 'à': 37,
'\u202f': 38, 'ç': 39, 'è': 40, ',': 41, '\xa0': 42, 'û': 43, 'â': 44, 'î': 45, 'ô': 46, '’': 47, 'ï': 48,
'ù': 49, '\u2009': 50, 'œ': 51, '0': 52, '9': 53, '3': 54, ':': 55, '1': 56, '«': 57, '»': 58, '5': 59,
'%': 60, '8': 61, '$': 62, '&': 63, '(': 64, ')': 65, 'ë': 66, '7': 67, '6': 68}
Ainsi par exemple l’espace sera codé par 1, la lettre e par 2, la lettre s par 3, etc… À noter que l’on devrait faire un nettoyage des données (data cleaning) pour avoir des données plus consistants, car par exemple on peut voir qu’il y a des caractères parasites : ‘\u202f’ (narrow no-break space), ‘\xa0’ (non-breaking space in Latin1), etc… Mais pour faire simple, nous nous passerons cette étape.
Le Tokenizer admet la méthode texts_to_sequences permettant d’encoder un corpus de textes que nous allons utiliser pour créer les données à faire passer dans le réseau de neuronnes.
1 2 3 | Data['EN_enc'] = Tokenizer.texts_to_sequences(Data['EN']) Data['FR_enc'] = Tokenizer.texts_to_sequences(Data['FR']) print( data[['EN', 'EN_enc', 'FR', 'FR_enc']].sample(10).to_string() ) # Affichage d'un echantillon |
5842 \tLet me see it.\n \tLaissez-moi les voir.\n [5, 14, 2, 7, 1, 15, 2, 1, 3, 2, 2, 1, 4, 7, 10, 6] [5, 14, 8, 4, 3, 3, 2, 33, 32, 15, 9, 4, 1, 14, 2, 3, 1, 21, 9, 4, 13, 10, 6]
9901 \tHe is a student.\n \tC'est un étudiant.\n [5, 19, 2, 1, 4, 3, 1, 8, 1, 3, 7, 12, 16, 2, 11, 7, 10, 6] [5, 17, 18, 2, 3, 7, 1, 12, 11, 1, 25, 7, 12, 16, 4, 8, 11, 7, 10, 6]
268 \tHumor me.\n \tFais-moi rire.\n [5, 19, 12, 15, 9, 13, 1, 15, 2, 10, 6] [5, 27, 8, 4, 3, 32, 15, 9, 4, 1, 13, 4, 13, 2, 10, 6]
4594 \tAre you happy?\n \tEs-tu contente ?\n [5, 8, 13, 2, 1, 22, 9, 12, 1, 19, 8, 20, 20, 22, 28, 6] [5, 2, 3, 32, 7, 12, 1, 17, 9, 11, 7, 2, 11, 7, 2, 1, 28, 6]
1401 \tIt's a fad.\n \tC'est un phénomène de mode.\n [5, 4, 7, 18, 3, 1, 8, 1, 27, 8, 16, 10, 6] [5, 17, 18, 2, 3, 7, 1, 12, 11, 1, 20, 19, 25, 11, 9, 15, 40, 11, 2, 1, 16, 2, 1, 15, 9, 16, 2, 10, 6]
4307 \tWe overslept.\n \tNous n'entendîmes pas le réveil.\n [5, 26, 2, 1, 9, 21, 2, 13, 3, 14, 2, 20, 7, 10, 6] [5, 11, 9, 12, 3, 1, 11, 18, 2, 11, 7, 2, 11, 16, 45, 15, 2, 3, 1, 20, 8, 3, 1, 14, 2, 1, 13, 25, 21, 2, 4, 14, 10, 6]
6639 \tYou're a liar.\n \tVous êtes une menteuse.\n [5, 22, 9, 12, 18, 13, 2, 1, 8, 1, 14, 4, 8, 13, 10, 6] [5, 21, 9, 12, 3, 1, 36, 7, 2, 3, 1, 12, 11, 2, 1, 15, 2, 11, 7, 2, 12, 3, 2, 10, 6]
9070 \tWhat if I fail?\n \tQue se passe-t-il si j'échoue ?\n [5, 26, 19, 8, 7, 1, 4, 27, 1, 4, 1, 27, 8, 4, 14, 28, 6] [5, 34, 12, 2, 1, 3, 2, 1, 20, 8, 3, 3, 2, 32, 7, 32, 4, 14, 1, 3, 4, 1, 23, 18, 25, 17, 19, 9, 12, 2, 38, 28, 6]
7702 \tI said get out.\n \tJe vous ai dit de sortir.\n [5, 4, 1, 3, 8, 4, 16, 1, 24, 2, 7, 1, 9, 12, 7, 10, 6] [5, 23, 2, 1, 21, 9, 12, 3, 1, 8, 4, 1, 16, 4, 7, 1, 16, 2, 1, 3, 9, 13, 7, 4, 13, 10, 6]
9556 \tClose your eyes.\n \tFerme tes yeux.\n [5, 17, 14, 9, 3, 2, 1, 22, 9, 12, 13, 1, 2, 22, 2, 3, 10, 6] [5, 27, 2, 13, 15, 2, 1, 7, 2, 3, 1, 22, 2, 12, 35, 10, 6]
Ainsi les colonnes ‘EN_enc’ et ‘FR_enc’ vont constituer les données à partir duquel le réseau va apprendre. À noter que la méthode texts_to_sequences prend en paramètre un liste de phrases et non pas une phrase. Si l’on souhaite encoder une seule phrase, alors il faudra la mettre dans une liste à un seul élément.
Les réseaux récurrents
Qu’est-ce qu’un réseau récurrent ? C’est un réseau qui utilise des opérations qu’on appelle cellules récursives, qui ont la propriété de s’alimenter elles-mêmes via un vecteur d’état. Elles ont été inventés pour traiter des séries temporelles c’est-à dire des données , , , , de longueur variable qui arrivent dans un ordre séquentiel.
Cellule LSTM
La cellule récursive la plus populaire est le Long Short-Term Memory (abgrégé en LSTM), dont le fonctionnement est le suivant. Une cellule LSTM prend en entrée un vecteur et deux vecteurs et , appelé états de la cellule, et renvoie en sortie un vecteur et deux vecteur et . Les vecteurs , , , et sont tous de même taille, que l’on appelle la dimension de la cellule. On résume le calcul et le fonctionnement d’une cellule LSTM par le schémas suivant, sachant qu’il n’est important de connaître ces détails pour la suite.
Quelques remarques sur cette implémentation. D’abord chaque opération Dense transforme le vecteur de concaténation en un vecteur de taille égale à la dimension de la cellule LSTM. Ensuite les poids de la cellule qui doivent être appris par rétropropagation du gradient sont les poids des quatres opérations Dense qui incluent une matrice de multiplication et une matrice de biais.
Maintenant nous allons voir comment une cellule LSTM s’utilise de manière récursive. Si on lui donne en entrée une suite de vecteurs , , , de longueur , alors la cellule LSTM renvoie une suite de vecteurs , , , de même longueur construite de la manière suivante :
- Il initialise les vecteurs et à zéros. La taille de ces vecteurs est égal à la dimension de la cellule.
- Pour tout instant allant de à , la cellule prend en entrée , , et on définit , et comme ses valeurs de sortie.
Ainsi les vecteurs d’états et sont envoyés par la cellule à elle-même pour être mis-à-jour à chaque instant . Tout ce qu’il faudra retenir d’une cellule LSTM, c’est qu’à partir d’une suite de vecteurs , , , elle fournit une suite de vecteurs , , , ainsi que deux vecteurs et , tous de taille égale à la dimension de la cellule.
L’architecture du modèle
La feuille de route pour entraîner un modèle et faire une prédiction est très particulière. Les modèles prédictifs classiques prennent une valeur en entrée et renvoient une valeur en sortie. C’est par exemple le cas d’un réseau de neuronnes convolutif (aussi appelé CNN pour Convolutionnal Neural Network) qui fait la classification d’image.
Dans le cas de la traduction, on aurait pu s’attendre à avoir une architecture de réseau qui prend en entrée une phrase anglaise sous forme d’un vecteur caractères encodés et renvoie en sortie la traduction française toutjours sous forme de vecteur. Cependant on ne peut malheureusement pas créer une telle architecture, en tout cas pas l’aide des cellules LSTM. Car la raison est qu’une cellule LSTM fonctionne en prennant entrée une suite (, , , ) et en renvoyant une suite (, , , ) qui vérifie les conditions suivantes :
- La suite est de même longeur que la suite .
- Pour tout , la valeur ne dépend que des valeurs pour , c’est-à-dire que des valeurs aux instants passés et non pas aux instants futures.
Or une phrase traduite n’a pas la même longueur que la phrase originale. De plus à cause des différences de règles synthaxiques entre les langues, les mots dans la phrase traduite dépendent de la globalité de la phrase originale.
Le modèle va en fait comporter deux réseaux de neurones. Le premier, qui s’appelle encodeur, se charge de prendre en entrée la phrase anglaise et de renvoyer un vecteur. Ce vecteur est abstrait pour l’utilisateur et représente en quelque sorte « une compression » de la phrase. Et le deuxième réseau appelé décodeur prend en entrée un vecteur dit d’état, de même dimension que la sortie de l’encodeur, ainsi qu’un caractère de la phrase traduite et renvoie en sortie un vecteur d’état ainsi qu’un caractère.
Le fonctionnement de cette architecture est comme suit. D’abords on commence par alimenter l’entrée de l’encodeur. Ensuite le vecteur de sortie de l’encodeur va passer en entrée du décodeur. Enfin le décodeur va fonctionner de manière récursive, c’est-à-dire qu’il va s’alimenter lui-même. Son rôle est de prédire le caractère qui suit le caractère en entrée.
Nous allons dans la suite décrire la phase d’entrainement et la phase de prédiction (ou inférence). Cependant contrairement aux modèles classiques, ces deux phases diffèrent fondamentalement par le fait que les valeurs en entrée et en sortie de sont pas les mêmes.
Entraînement du modèle
À partir de l’encodeur et du décodeur, on fabrique un troisième modèle, le modèle d’entraînement. Étant données une phrase anglaise formée de jetons , , , et sa traduction , , , .
- D’abord on donne la liste , , , en entrée de l’encodeur qui va retourner un vecteur, disons .
- Ensuite on donne en entrée du décodeur le vecteur et récursivement la liste , , , de la même manière qu’une cellule LSTM. Le décodeur retourne alors un vecteur de même longueur, disons , , ,
- Enfin on applique la rétropropagation du gradient en faisant correspondre , , , à la cible qui est , , , .
Par exemple si nous voulons entraîner le modèle sur la phrase « hello » traduite par « salut », et si nous notons et les jetons de début et de fin. alors nous aurons :
- .
- .
- .
- .
Inférence sur modèle
Une fois que l’encodeur et le décodeur ont été entrainés via le modèle d’entraînement, faire une prédiction (ou inférence) consiste à donner en entrée une phrase anglaise et à avoir en sortie sa traduction française. Étant donnée une phrase anglaise formée de jetons (, , , ) pour la faire traduire par le modèle :
- Premièrement on passe (, , , ) en entrée de l’encodeur. En sortie on renvoie un vecteur .
-
Deuxièmement la prédiction (, , , ) est définie récursivement de la manière suivante.
- Initialiser et prend la valeur du jeton .
- Puis faire passer et en entrée du décodeur. Le décodeur renvoie alors une prédiction et un vecteur .
- Enfin incrémenter et recommencer l’étape précédente jusqu’à que soit égal au jeton (ou que soit trop grand pour s’assuser que l’algorithme termine).
Ce schémas d’inférence explique la raison d’avoir les jetons et . En effet le jeton sert de premier entrée au décodeur et le jeton sert à savoir quand il faut arrêter d’alimenter le décodeur.
Implémentation du modèle
Nous allons donner une implémentation simple d’une telle architecture avec la librairie keras.
Codage de l’encodeur, décodeur et modèle d’entrainement
Nous utiliserons l’unité de calcul LSTM implémentée par la classe keras.layers.LSTM dont nous commençons par expliquer le fonctionnement. Comme indiqué dans l’introduction le lecteur est supposé connaître les notions de tenseur, batch, feature et de l’utilisation d’un keras.layers (instanciation et appel).
La classe keras.layers.LSTM s’instencie avec des paramètres dont les principaux que nous allons utilisés sont :
- units (entier) : indique la dimension de la cellule.
- return_sequences (booléen, False par défaut) : True signifie qu’en sortie on récupère toute la suite (, , , ) sur la figure Fig 2, et False signifie qu’on ne récupère que le dernier élément de la suite.
- return_state (booléen, False par défaut) : True signifie qu’en plus à la sortie en retourne les deux états et de la figure Fig. 2.
Une fois qu’une cellule keras.
Si return_sequences = True, alors l’appel crée un nouveau tenseur de format [batch, timesteps, feature] et si return_sequences = False, alors le tenseur crée est de format [batch, feature]. Dans les deux cas la dimension de feature est égale à units, la dimension de la cellule.
Si return_state = True, alors l’appel crée en plus deux tenseurs en plus et chacun de forme [batch, feature], avec feature de dimension égale à units.
Lors de l’appel on peut aussi passer en paramètre initial_state la liste formée des deux états et en entrée, qui sont des tenseurs de format [batch, feature], à passer en entrée de la cellule. Et si ce paramètre n’est pas renseigné, alors par défaut ils sont égaux au tenseur nulle.
Les détails d’implémentation de l’encodeur et du décodeur, ainsi que le modèle d’entrainement sont donnés par les schémas suivants. Ici, Num indique le nombre de jetons différents dans le Tokenizer.
Quelques précisions sur l’implémentation.
- L’encodeur prend entrée Input, un tenseur de format [batch, timestep]. Les valeurs de ce tenseur sont des entiers positifs representant le code des jetons (ce n’est pas l’encodage one-hot que l’on met en entrée). En effet l’encodage one-hot est géré par l’unité de calcul keras.layers.Embedding qui se charge de convertir Input en sa version one-hot puis d’appliquer une transformation linéaire.
- De même l’entrée du décodeur est un tenseur de format [batch, timestep] d’entiers positifs et son encodage one-hot est géré par un keras.layers.Embedding.
- Par contre la sortie Output du décodeur est un tenseur de format [batch, timestep, feature] où feature est de dimension le nombre de jeton du Tokenizer noté Num. La valeur prédite en retour est donc une version one-hot encoded. Ainsi lors de la phase d’inférence il faudra donc convertir la valeur de sortie en utilisant argmax.
- L’unité de calcul Dense Softmax a deux rôles : avoir une dimension égale à Num en sortie, et utiliser l’activation Softmax pour avoir des probabilités (c’est-à-dire des valeurs positives de somme 1).
- Le choix des dimensions de Emdedding (32) et de LSTM (1024) est arbitraire et doivent être calibrées par le lecteur pour des résultats optimales.
Note : La classe keras.layers.LSTM admet à la construction un paramètre activation. Cependant contrairement à ce que l’on peut penser cette activation n’est pas appliquée à la sortie de la cellule. En effet cet attribut vaut par défaut et correspond aux deux activations dur la figure Fig. 1 (l’autre activation est donné par le paramètre recurrent_activation). Ainsi s’il l’on crée un keras.layers.LSTM avec activation = ‘softmax’, le vecteur de sortie n’est pas de somme 1, car c’est le produit d’un vecteur activé avec Softmax par un vecteur activé avec Sigmoid.
Voici l’implémentation de l’encodeur, du décodeur et du modèle d’entrainement.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | import keras num_tokens = len(Tokenizer.word_index) + 1 # +1 car l'indice des jetons commence à 1 embedding_units = 32 lstm_units = 1024 def createEncoder(): inputs = keras.layers.Input(shape=(None,)) embed_layer = keras.layers.Embedding(input_dim=num_tokens, output_dim=embedding_units) embed = embed_layer(inputs) lstm_layer = keras.layers.LSTM(units=lstm_units, return_sequences=False, return_state=True) _, h, c = lstm_layer(embed) return keras.models.Model(inputs, [h, c]) def createDecoder(): inputs = keras.layers.Input(shape=(None,)) embed_layer = keras.layers.Embedding(input_dim=num_tokens, output_dim=embedding_units) embed = embed_layer(inputs) input_h = keras.layers.Input(shape=(lstm_units,)) input_c = keras.layers.Input(shape=(lstm_units,)) lstm_layer = keras.layers.LSTM(units=lstm_units, return_sequences=True, return_state=True) lstm_out, h , c = lstm_layer(embed, initial_state=[input_h, input_c]) dense_layer = keras.layers.Dense(num_tokens, activation='softmax') out = dense_layer(lstm_out) return keras.models.Model([inputs, input_h, input_c], [out, h, c]) def createTrainingModel(Encoder, Decoder): out, h, c = Decoder( (Decoder.input[0], Encoder.output[0], Encoder.output[1]) ) # Note : Decoder.input[0] = inputs, Encoder.output[0] = h, Encoder.output[1] = c model = keras.models.Model([Encoder.input, Decoder.input[0]], out) model.compile(optimizer='adam', loss='sparse_categorical_crossentropy') return model Encoder = createEncoder() Decoder = createDecoder() TrainingModel = createTrainingModel(Encoder, Decoder) |
Concernant la métrique loss=’sparse_categorical_crossentropy’, le mot sparse signifie que lorsqu’on appelle la méthode TrainingModel.fit la cible devra être un tenseur de format [batch, timestep] de coefficient entiers positifs (et non pas sa version one-hot qui est un tenseur de format [batch, timestep, feature]).
Phase d’entrainement
La phase d’entraînement d’un tel modèle est un peu plus délicate qu’un réseau de neuronnes classique. Effectivement dans un modèle classique, il suffit d’appeler la méthode .fit sur les données, qui se charge d’appliquer la méthode du gradient par la méthode mini-batch. Mais cela ne marche que si toutes les entrées des données ont le même format. Or ici ce n’est pas le cas, car la dimension timestep est différente d’une donnée à l’autre. Cependant il y a deux façons pour régler ce problème :
- Ne pas utiliser la méthode mini-batch et appeler la méthode .fit sur chaque entrée des données.
- Compléter les entrées par des jetons pour qu’elles aient toutes la même dimension timestep.
La première est la plus simple, mais elle a comme gros inconvenient d’être déraisonnablement lente. La deuxième méthode est beaucoup plus rapide, mais lors de la phase d’inférence il faut aussi compléter les données avec les jetons , car le modèle n’aura été appris que sur des données d’une taille fixée. C’est cette deuxième qu’il faut privilégier.
Bien qu’il serait facile de d’implémenter la deuxième méthode à la main,
Keras admet une fonction qui permet de faire cette opération, à savoir
keras.
- sequences (liste de séquences) : liste formée des séquence à compléter par value.
- maxlen (entier ou None (par défaut)) : taille maximale. Si None, alors maxlen est la taille de la plus longue séquence apparaissant dans sequences
- dtype (par défaut ‘int32’) : type de données.
- padding (vaut ‘pre’ (par défaut) ou ‘post’) : indique si on complète au début ou à la fin.
- truncating (vaut ‘pre’ (par défaut) ou ‘post’) : indique si on tronque les séquences de longueur supérieure à maxlen au début ou à la fin.
- value (vaut par défaut 0) : valeur par laquelle compléter.
Cette fonction retourne un numpy.array de format [batch, timestep] avec batch de dimension len(sequences) et timestep de dimension maxlen. En conséquence le code pour entraîner TrainingModel se présente alors de la manière suivante.
1 2 3 4 5 6 7 | input_encoder = keras.preprocessing.sequence.pad_sequences(Data["EN_enc"], maxlen=None, dtype='int32', padding='post', value=Tokenizer.word_index['\n']) input_decoder = keras.preprocessing.sequence.pad_sequences(Data["FR_enc"], maxlen=None, dtype='int32', padding='post', value=Tokenizer.word_index['\n']) target = numpy.roll(input_decoder, shift=-1) # Décalage vers la gauche target[:,-1] = Tokenizer.word_index['\n'] # Correction sur le dernier jeton TrainingModel.fit([input_encoder, input_decoder], target, epochs=100, verbose = 2) |
Le lecteur pourra éventuellement faire une séparation avec des données de validation et/ou utiliser des callbacks.
Phase d’inférence
Encore une fois cela ne se passe pas comme pour les modèles classiques où il suffit d’appeler la méthode .predict sur le modèle. En effet la fonction pour faire l’inférence donnée pas la figure Fig. 5 doit être malheureusement codée à la main. La fonction Translate suivante prend entrée une phrase anglaise sous forme de string et renvoie la prédiction par le modèle sous forme de string.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | MAX_LENGTH = input_encoder.shape[1] ## dimension timestep def Translate(phrase): ## Ajout des jetons de début et de fin phrase = '\t' + phrase + '\n' ## Encodage des jetons encoder_input_data = Tokenizer.texts_to_sequences([phrase]) # format [batch, timestep] avec batch=1 ## ajustement de la dimension timestep encoder_input_data = keras.preprocessing.sequence.pad_sequences(encoder_input_data, maxlen=MAX_LENGTH, dtype='int32', padding='post', truncating='post', value=tokenizer_EN.word_index['\n']) ## Récupération de la sortie de l'encodeur h, c = Encoder.predict(encoder_input_data) # format [batch, feature] avec batch=1 et feature=1024 char = Tokenizer.word_index['\t'] ## Jeton <START> result = list() result.append(char) while char != tokenizer_FR.word_index['\n'] and len(result) <1000: ## Tant que char != <END> ou result pas trop grand input_dec = np.array([[char]]) ## input_dec : format [batch, timestep] avec batch=1, timestep=1, out ,h ,c = Decoder.predict([input_dec ,h ,c]) ## out : format [batch, timestep, feature] avec batch=1, timestep=1, feature=num_tokens char = np.argmax(out[0,-1,:]) result.append(char) return ''.join([Tokenizer.index_word[x] for x in result]) ## Conversion des jetons encodés en caractères |
Ainsi se termine ce tutoriel sur comment créer un réseau de neurones pour faire de la traduction. Précisons finalement que le modèle est extrêmement simple car le but était de présenter le principe d’utilisation des réseaux récurrents ainsi que l’architecture avec encodeur et décodeur.
Enfin vous pouvez trouver les codes Python de cet article ici