Traitement Naturel du Langage (NLP) : Génération de Texte avec des Réseaux Récurrent.

Au cours de ce calepin, nous allons voir comment générer des description de produits à l'aide de Réseaux Récurents et notamment grace aux structure LSTM (Long-Short Term Memory).

L'intérêt de cette application est limité. Les descriptions de textes de ce document sont trop pauvres syntaxiquement pour pouvoir juger réellement de la qualité du texte généré. L'intérêt réel de ce calepin est de voir comment les données doivent être mis en forme pour être utilisé dans un réseau recurrent dans un but de génération de texte.

Librairies


In [ ]:
#Importation des librairies utilisées

import pandas as pd
import numpy as np
import pickle
import functools
from tqdm import tqdm

import keras.models as km
import keras.layers as kl

Téléchargement des données

La Catégorie de Niveau 3 COQUE - BUMPER - FACADE TELEPHONE est la catégorie le plus représenté du jeu de données originale Cdiscount avec 2.184.671 déscriptions présentent. Parmis ces descriptions, 1.761.637 sont composés d'exactement 197 caractères.

Nous allons nous servir de ces lignes (ou un sous ensemble de ces lignes, en fonction de la puissance de calcul disponible sur votre machine) pour apprendre un modèle de génération de texte qui permettra de généré automatiquement une nouvelle description de ce type de produit.


In [ ]:
N = 100000
DATA_DIR = ""
X = np.load(DATA_DIR+"data/description_coque.npy")[:N]
print(X.shape)
print(X[:3])

Exercice Vérifiez que toutes les séquences sont bien de tailles 197.


In [ ]:
Nd=197

Mise en forme des données

La génération de texte implique de constuire un réseau Many-To-One :

Ou la prédiction $y_t$ servira d'entrée au réseau au temps $t+1$, i.e : $y_t=x_{t+1}$.

Chaque $x_t$ représente ici un caractère de la déscription encodé en One-Hot encoding. Ainsi une description $x$ composé de $N_d$ caractères sera modélisé par une matrice de taille $(N_v\times N_d)$ $x=[x_1,x_2,...,x_{N_d}]$ ou $x_i \in \mathbb{R}^{N_v}$

Création de la liste des caractères

Afin d'encoder les description sous format 'One-Hot encoding' nous devons dans un premier temps retrouver la taille $Nv$ de notre vocabulaire constitué de tout les caractères présent dans la description.


In [ ]:
chars = list(functools.reduce(lambda x,y : x.union(y), [set(x) for x in X], set()))
print("Vocabulaire : " +  str(chars))

Nous ajoutons à ce vocabulaire deux indicateur permettant de localiser le début et la fin de chaque description


In [ ]:
chars.extend(["start","end"])

In [ ]:
Nv = len(chars)
print("Taille du vocabulaire : %d" %Nv)

Création des dictionnaires

Les dictionnaires char_to_int et int_to_char permettent respectivement d'encoder une description texte et de décoder un encodage `One-Hot``


In [ ]:
int_to_char = {i:c for i,c in enumerate(chars)}
char_to_int = {c:i for i,c in int_to_char.items()}
I_START = char_to_int["start"]
I_END = char_to_int["end"]

Encodage des Descriptions

La fonction suivante, permet d'encoder une matrice $X\in \mathbb{R}^{N \times N_d}$ constitués de N descriptions en une matrice $X_{vec} \in \mathbb{R}^{N \times N_d \times N_v}$ contenant les description encodées.


In [ ]:
def encode_input_output_sequence(x, length_sequence, size_vocab, char_to_int_dic, i_start, i_end):
    n = x.shape[0]
    x_vec = np.zeros((n,length_sequence, size_vocab))
    y_vec = np.zeros((n,length_sequence, size_vocab))
    x_vec[:,0,i_start] = 1
    y_vec[:,-1,i_end] = 1
    for ix,x in tqdm(enumerate(x)):
        for ic,c in enumerate(x):
            c_int = char_to_int_dic[c]
            x_vec[ix,ic+1,c_int]=1
    y_vec[:,:-1,:] = x_vec[:,1:,:] 
    return x_vec, y_vec

In [ ]:
X_vec, Y_vec = encode_input_output_sequence(X[:N], Nd+1, Nv, char_to_int,I_START,I_END)

In [ ]:
X_vec.shape

Exercice Retrouvez la phrase originale de la phrase test affiché ci-dessous à partir de la phrase encodé. Vérifiez que x et y sont bien les mêmes descriptions seulement décalées d'un index


In [ ]:
# %load solution/3_1.py

Apprentissage

Nous allons maintenant définir notre modèle récurrent afin de générer notre modèle de prédiction.

Prenez le temps de bien comprendre toutes les fonctions et arguments utilisés pour construire ce modèle.


In [ ]:
nb_hidden = 32
epochs = 20
batch_size= 128

model = km.Sequential()
model.add(kl.LSTM(nb_hidden, input_shape=(None, Nv), return_sequences=True))
model.add(kl.TimeDistributed(kl.Dense(Nv)))
model.add(kl.Activation('softmax'))
model.summary()

In [ ]:
model.compile(loss="categorical_crossentropy", optimizer="rmsprop")
model.fit(X_vec, Y_vec, epochs=epochs, batch_size=batch_size)
model.save("data/generate_model.h5")

Q Pourquoi est-ce la categorical_crossentropy qui est utilisée comme fonction de perte?

Génération de Texte

La celulle suivante permet de générer une description produit :


In [ ]:
x_pred = np.zeros((1, Nd+1, Nv))
print("step 0")
x_pred[0,0,I_START] =1
x_pred_str = decode_sequence(x_pred[0], int_to_char)
print(x_pred_str)

for i in range(Nd):
    ix = np.argmax(model.predict(x_pred[:,:i+1,:])[0][-1,:])
    x_pred[0,i+1,ix] = 1
x_pred_str=decode_sequence(x_pred[0], int_to_char)
print(x_pred_str)

Q Comment cette génération est-elle produite?

Exercice Effectuez une génération en choissisant la ou les premières lettres qui seront générées.


In [ ]:
#%load solution/3_2.py

Exercice Effectuez une génération en ajoutant de l'aléa. Vous pouvez par exemple faire en sorte que chaque lettre soit séléctionnée selon une loi multinomiale.


In [ ]:
#%load solution/3_3.py

In [ ]: