Prédire le prix d’un article de e-commerce — le Challenge Kaggle Mercari 3/3

Photo by Ashim D’Silva on Unsplash

Suite et fin : l’implémentation d’un réseau de neurones récurrent (NLP)

Dans le précédent article, nous avons implémenté un réseau de neurones convolutionnel. Cette architecture a fait ses preuves et a été utilisée pendant longtemps, mais elle est aujourd’hui plutôt en perte de vitesse par rapport aux réseaux de neurones récurrents pour les problématiques NLP.

Le réseau de neurones récurrent, et plus spécifiquement les réseaux LSTM (pour Long Short Term Memory), sont extraordinairement bien décrits et expliqués dans cet article.

L’utilisation des réseaux de neurones récurrents dans le NLP est basée sur une idée simple : une phrase est une séquence de mots dont l’ordre influence le sens. En cela, les réseaux de neurones récurrents devraient nous aider, puisque la séquentialité des mots sera mieux comprise par notre modèle. Les RNN décrits dans l’article précédent posent deux problèmes : premièrement d’optimisation, avec un phénomène d’explosion du gradient bien décrit dans les cours de Stanford du premier article, et enfin de compréhension des séquences car ils ont du mal à intégrer des effets de long terme. C’est pour ces deux raisons que les LSTM sont préférés dans le cadre du traitement du langage (mais aussi ailleurs, notamment en analyse des séries temporelles). Les GRU (Gate Recurrent Unit) montrent aussi des résultats intéressants et sont plus rapides à calculer.

Pour créer des réseaux LSTM avec tensorflow, la marche à suivre consiste d’abord à définir les cellules. Nous allons écrire une fonction qui crée une cellule régularisée :

def create_regularized_lstm_cell(nb_hidden, keep_prob) :
return tf.contrib.rnn.DropoutWrapper(
tf.contrib.rnn.BasicLSTMCell(nb_hidden),
output_keep_prob = keep_prop,
state_keep_prob = 1.0
)

Nous avons une cellule dont le dropout s’applique à son output vers la couche suivante de neurones et à la cellule LSTM suivante. Nous déclarons une nouvelle classe qui ressemble beaucoup à celle de l’article précédent, mais en modifiant les méthodes create_conv_model et set_optimizer.

Le modèle

Nous utilisons le même word embedding que lors du dernier article, aucun changement à ce niveau là !

self.embedding = 
tf.Variable(np.vstack(embedding_model.embeddings),
dtype = tf.float32,
trainable = False,
name = "sf_embedding"
)

self.embedding_lookup = tf.nn.embedding_lookup(
self.embedding,
self.input_x,
name = "embedding_lookup"
)

C’est ici que les choses changent. L’idée est d’envoyer à chaque cellule d’une couche de LSTM un word vector, donc il faut transformer notre tenseur de dimension [batch, 150, 300] en une liste de 150 matrices de dimension [batch, 300]. Nous avons donc 150 cellules LSTM par couche.

self.unstacked_embedding = tf.unstack(self.embedding_lookup,
num = 150,
axis = 1,
name = "unstacked_embeddings"
)

Maintenant, nous allons créer les cellules. Pour l’instant, on ne spécifie que le nombre de couches de LSTM et le nombre de cellules parallèles que nous voulons par couches.

self.lstm_cells = [create_regularized_lstm_cell(nb_hidden_lstm, keep_prob) for _ in range(nb_layers)]

Puis nous stackons les cellules :

self.multi_lstm_layer = 
tf.contrib.rnn.MultiRNNCell(self.lstm_cells)

Attention, dans les précédentes versions de tensorflow, il était possible de coder :

self.lstm_cells = create_regularized_lstm_cell(nb_hidden_lstm,
keep_prob)
self.multi_lstm_layer =
tf.contrib.rnn.MultiRNNCell([self.lstm_cells]*nb_layers)

Mais dans la dernière version cette manière de construire le réseau récurrent ne fonctionne plus.

Maintenant, nous connectons les LSTM que nous avons créés et nous les connectons avec les word vectors. Nous récupérons les output et les states (valeurs transmises par chaque cellule LSTM à la suivante)

self.lstm_outputs, self.lstm_states = 
tf.contrib.rnn.static_rnn(self.multi_lstm_layer,
self.unstacked_embedding,
dtype = tf.float32
)

Et voilà ! Maintenant que nos couches de LSTM ont été connectées à nos word vectors, nos allons récupérer le dernier input de la dernière couche de LSTM et le connecter à notre couche de sortie.

self.W = tf.Variable(tf.random_normal(stddev = 0.1,
shape = (nb_hidden_lstm, 1)),
name = "W",
dtype = tf.float32
)
self.b = tf.Variable(tf.random_normal(stddev = 0.1, shape = [1]),
name = "b",
dtype = tf.float32
)
self.output = tf.matmul(self.lstm_outputs[-1], self.W) + self.b

Comme dans notre dernier article, nous définissons la moyenne des carrés des écarts que nous allons chercher à minimiser :

#defining loss
self.loss = tf.reduce_mean(tf.square(self.input_y - self.output),
name = "loss")

À nouveau, nous définissons notre algorithme d’optimisation. En utilisant des réseaux de neurones récurrents, il est très fortement recommandé d’utiliser l’algorithme RMSProp, adapté à ce type d’architectures.

def set_optimizer(self, learning_rate = 0.0001,
decay_rate = 0.9999) :

self.global_step = tf.Variable(0, trainable = False)

evolutive_lr = tf.train.exponential_decay(
learning_rate = learning_rate,
global_step = self.global_step,
decay_rate = decay_rate,
decay_steps = 1
)

self.optimizer = tf.train.RMSPropOptimizer(
evolutive_lr, name = "optimizer")

self.train_op = self.optimizer.minimize(
self.loss, global_step = self.global_step)

Le paramètre de réduction du learning rate à chaque nouveau batch est plus important que dans notre article précédent parce que nous utilisons un batch plus important (ici 250, précédemment 20). Nous le faisons principalement pour des raisons de performances, le temps de calcul avec des LSTM est trop long avec des batch de petite taille. Nous réutilisons exactement la même méthode de early stopping que celle utilisée dans notre article précédent. Le code est à nouveau disponible sur notre repository Git.

En comparant les performances des modèles, on remarque qu’elles sont très proches, avec un léger avantage pour le réseau convolutionnel. Au delà de la performance, le RNN a un gros désavantage : le temps de calcul. La GTX 1070 que nous avons utilisé a été utilisée à 100% de ses capacités pendant près de 15 heures ! Beaucoup d’autres architectures pourraient être testées avec les RNN, notamment les LSTM bidirectionnels, mais vu le temps de calcul il faut soit avoir beaucoup de GPU, soit se restreindre à quelques modèles.

Conclusion

Ce challenge montre que le NLP permet de répondre à des problématiques très complexes avec efficacité. Il nous apprend également que les architectures récentes basées sur des LSTM ne sont pas forcément le meilleur choix en toute circonstance. Finalement, la puissance de calcul nécéssaire reste raisonnable : avec un PC dimensionné spécifiquement pour le deep learning d’une valeur d’environ 5000€, il est possible de tester suffisamment d’architectures et d’hyperparamètres pour aboutir à un modèle performant.

L’auteur

Jean-Baptiste
Data Scientist & deep learning fanatic


Prédire le prix d’un article de e-commerce — le Challenge Kaggle Mercari 3/3 was originally published in The Official Linkvalue Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.

Source: Deep Learning on Medium