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

Un premier modèle : Le réseau convolutionnel (NLP)

CBTW
L’Actualité Tech — Blog CBTW
8 min readJan 9, 2018

--

Prédiction du prix d’un article
Caddie — Prédiction du prix d’un article

Le challenge Kaggle Mercari consiste à prédire au mieux le prix des articles d’une plateforme e-commerce en utilisant la description des articles et d’autres informations (catégorie, livraison offerte et état de l’article).

Nous avons traité dans un premier article le nettoyage des données, la présentation du modèle d’embedding et la récupération de l’un de ces modèles pré-entrainés (lien ci-dessous). Avec celui-ci, nous entrons dans le détail de l’implémentation d’un réseau de neurones convolutionnel et dans la conception d’un objet qui gérera l’entraînement du modèle.

Le plus simple, pour démarrer

Il y a deux manières usuelles de traiter le langage avec des réseaux de neurones : avec des convolutions, ou avec des réseaux de neurones récurrents. Le premier est le plus ancien, mais il a deux avantages : il est plus facile à optimiser et les calculs sont plus rapides.

Pour une présentation simple des filtres de convolution et les max pooling, je vous propose de lire l’article suivant de Hackernoon :

La notion de convolution est facilement compréhensible avec des exemples quand il s’agit d’images. Pour du texte, l’interprétation est beaucoup plus complexe voire impossible. C’est le principal défaut du deep learning : c’est une boîte noire. Surtout dans ce cas-là.

Par où commencer ? Dans un premier temps, nous allons utiliser les word vector que nous avons récupérés dans l’article précédent, et les placer dans un objet Tensorflow qui renverra le word vector correspondant à l’entier que nous lui donnons. Ensuite, nous allons connecter les word vector obtenus à une couche de convolution et ajouter d’autres convolutions et des max-poolings. Ensuite, nous connecterons les données en sortie de ces filtres vers des neurones à activation « leaky relu ». Cela peut sembler compliqué, mais en pratique pas tant que cela.

Simple comme bonjour

À chaque itération, nous allons envoyer dans le réseau de neurones une matrice de taille [batch, 150] en entrée et les prix réels sous forme d’un vecteur de dimension [batch], l’objectif du modèle étant de minimiser le plus possible l’écart entre ce qu’il prédit et le prix reel.

L’embedding va renvoyer pour chaque entier le word vector qui lui est associé. Nous utiliserons les représentations vectorielles des mots comme figées, elles ne seront pas une variable que le modèle cherchera à ajuster. Nous n’avons pas un échantillon suffisamment important pour cela.

class TextCNN :
def __init__(self) :
pass

def create_conv_model(self, embedding_model, filter_shape,
nb_filters, keep_prob) :

self.input_x = tf.placeholder(dtype = tf.int32,
shape = [None, 150],
name = "input_x")
self.input_y = tf.placeholder(dtype = tf.float32,
shape = [None],
name = "input_y")

with tf.device("/gpu:0") :
#Create embedding with pretrained model weights
self.embedding =
tf.Variable(np.vstack(embedding_model.embeddings),
dtype = tf.float32,
trainable = False,
name = "embedding"
)
self.embedding_lookup =
tf.nn.embedding_lookup(self.embedding,
self.input_x,
name = "embedding_lookup"
)

Qu’avons-nous fait ? Dans l’ordre, nous avons défini les données que nous allions donner en entrée au modèle, à savoir les données et la variable à prédire. Ce type d’objet Tensorflow est un « placeholder ». Ensuite, nous avons concaténé les vecteurs associés aux différents mots dans une seule grande matrice, que nous avons assignée à notre embedding. Enfin, nous avons défini l’embedding lookup, qui assure la conversion des entiers vers leurs vecteurs respectifs.

L’intérêt de tensorflow est qu’il faut bien comprendre ce que l’on manipule et surtout les dimensions des vecteurs, matrices et tenseurs. Cela oblige à plus de rigueur, et à mieux comprendre ce que l’on fait avec des outils de plus haut niveau comme keras, et donc d’éviter des erreurs conceptuelles sur les tenseurs que l’on utilise. Si l’on regarde dans le détail les objets manipulés, on a :

  • Une matrice de words vector dans l’attribut « embedding », avec une ligne par mot de vocabulaire (soit 400000) et 300 colonnes, la dimension de l’embedding que nous avons choisi.
  • L’embedding lookup va aller chercher le vecteur associé à chaque entier et nous renverra un tenseur de dimension [batch, 150, 300].

Le filtre de convolution prend en entrée un tenseur de 4 dimensions. Nous allons donc en rajouter une sur le dernier axe puis le connecter sur ce filtre.

#Embedding expansion for compatibility with conv layer
self.expanded_embedding = tf.expand_dims(self.embedding_lookup,
axis = -1,
name = "expanded_embedding"
)
#convolution layer
self.conv1 = tf.layers.conv2d(inputs = self.expanded_embedding,
padding = "same",
filters = nb_filters,
kernel_size = filter_shape,
name = "conv1",
activation = tf.nn.leaky_relu
)
#first max pooling layer
self.max_pool1 = tf.layers.max_pooling2d(self.conv1,
pool_size = [1, 2],
strides = [1, 2],
name = "max_pool1"
)

Les dimensions des tenseurs se compliquent un petit peu. Ici, la couche de convolution renvoie un tenseur de dimension [batch, 150, 300, nombre de filtres]. Puis ce tenseur arrive dans la couche de max_pooling, qui va retourner un tenseur de dimension [batch, 50, 100, nombre de filtres].

Comme pour le traitement d’image, il faut « empiler » les convolutions pour extraire le plus d’information possible. Donc nous allons en rajouter deux couches supplémentaires.

Encore des convolutions et des max pooling

Parce que plus c’est mieux. Ou trop.

On rajoute des convolutions et on transforme le tenseur en matrice :

#second convolution layer
self.conv2 = tf.layers.conv2d(inputs = self.max_pool1,
padding = "same",
filters = nb_filters,
kernel_size = filter_shape,
name = "conv_layer2",
activation = tf.nn.leaky_relu
)
#second max pooling layer
self.max_pool2 = tf.layers.max_pooling2d(self.conv2,
pool_size = [1, 2],
strides = [1, 2],
name = "max_pool_layer2"
)
#third conv layer
self.conv3 = tf.layers.conv2d(inputs = self.max_pool2,
padding = "same",
filters = nb_filters * 2,
kernel_size = filter_shape,
name = "conv_layer3",
activation = tf.nn.leaky_relu
)
#pool flatting to enable dense connexion
self.flat_pool = tf.reshape(self.conv3, [-1, 50*50*nb_filters*2])

Le retour de notre tenseur en une matrice nous permet de le connecter vers des couches d’activations linéaires rectifiées. Nous régularisons les activations avec des dropout :

self.W2 = tf.Variable(tf.random_normal(stddev = 0.1,
shape = [2500, 500]),
dtype = tf.float32,
name = "W2"
)
self.b2 = tf.Variable(tf.random_normal(stddev = 0.1,
shape = [500]),
dtype = tf.float32,
name = "b2"
)
self.relu_activation2 =
tf.nn.leaky_relu(tf.matmul(self.relu_activation1_regularized,
self.W2) + self.b2,
name = "relu_activation2"
)
self.relu_activation2_regularized =
tf.nn.dropout(self.relu_activation2,
keep_prob = keep_prob,
name = "relu2_regularized"
)

Notre sortie de première couche de dropout a donc une dimension de [batch, 2500] et la sortie de notre deuxième couche une taille de [batch, 500].

Par ici la sortie

Nous allons maintenant passer par une activation linéaire pour obtenir un vecteur de dimension [batch] qui correspondra à la prédiction du modèle. Nous allons ensuite calculer la moyenne du carré des écarts : c’est cette valeur que nous allons chercher à minimiser

self.W3 = tf.Variable(tf.random_normal(stddev = 0.1,
shape = [500, 1]),
name = "output_weights"
)
self.b3 = tf.Variable(tf.random_normal(stddev = 0.1,
shape = [1]),
name = "output_biases"
)
self.output = tf.matmul(self.relu_activation2_regularized,
self.W3) + self.b3
#loss definition
self.loss = tf.reduce_mean(tf.square(self.input_y - self.output),
name = "loss"
)

Minimiser l’erreur

Choisir un algorithme d’optimisation pour un réseau de neurones profond est une étape complexe, et il est souvent nécessaire d’en tester plusieurs avec différents paramètres. Compte tenu de la structure du modèle que nous avons implémenté, nous avons pris le parti d’utiliser le même algorithme d’optimisation que celui utilisé sur les réseaux convolutionnels pour le traitement d’image avec des paramètres similaires. La vitesse de convergence est bonne.

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

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.AdamOptimizer(
evolutive_lr,
epsilon = 1.0,
name = "optimizer"
)

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

Nous utilisons une réduction de la vitesse d’apprentissage à chaque étape. Attention, le paramètre epsilon de l’algorithme Adam doit avoir la valeur 1.0, sans quoi l’optimisation diverge quand elle commence à s’approcher de la solution.

Entraîner le modèle

C’est ici qu’il faut un GPU

Nous allons entraîner le modèle en utilisant une procédure d’early stopping. Toutes les 50 000 observations, nous allons examiner la performance du modèle sur l’ensemble de l’échantillon de test. Si la performance est la meilleure, nous sauvegardons les paramètres du réseau de neurones, et dans le cas contraire nous incrémentons un compteur. Si ce compteur dépasse une certaine valeur, l’optimisation s’arrête.

def fit_with_early_stopping(self, x, y, x_test, y_test, nb_epoch,
early_stopping = 10, batch_size = 20,
step = 50000, init = True) :

nb_obs_train = len(x)
nb_obs_test = len(x_test)
self.test_errors_log = []
early_stopping_count = 0
self.train_error_log = []
saver = tf.train.Saver()
#we use _step because is step is not a multiple of batch size
#(i % step == 0) will always be false
_step = step % batch_size

with tf.Session() as sess :
if init :
_init = tf.global_variables_initializer()
sess.run(_init)
for epoch in range(nb_epoch) :
batch_errors = []
avg_cost = 0.0
i = 0
test_loss_improvement = 0
# fitting on train sample
while (i + batch_size <= nb_obs_train - 1) :
_, c = sess.run([self.train_op, self.loss],
feed_dict = {self.input_x :
x[i : i + batch_size, :],
self.input_y :
y[i : i + batch_size]})
#saving training loss
batch_errors.append(c)
i += batch_size
if (i % step == _step) :
j = 0
test_errors = []
#testing performance on test sample
while (j + batch_size <= nb_obs_test - 1) :
test_error = sess.run([self.loss],
feed_dict =
{self.input_x :
x_test[j : j + batch_size, :],
self.input_y :
y_test[j : j + batch_size]})
test_errors.append(test_error)
j += batch_size
test_error_mean = np.mean(test_errors)
self.test_errors_log.append(test_error_mean)
batch_errors_mean = np.mean(batch_errors)
self.train_error_log.append(
batch_errors_mean)
print("train error : "+str(batch_errors_mean)
+ " test error : "+ str(test_error_mean))
batch_errors = []
#saving model and reset early stopping count
#if loss is the best on test set
if test_error_mean==min(self.test_errors_log):
saver.save(sess,
"/Users/jeanbaptiste/TextCNN")
early_stopping_count = 0
else :
early_stopping_count += 1
#stop if early stopping limit has been reached
if early_stopping_count == early_stopping :
print("early stopping reached +
str(min(self.test_errors_log)))
return None

Conclusion

Ce modèle est une bonne base de travail pour commencer. Il y a toujours moyen d’améliorer un modèle de deep learning, notamment en essayant de rajouter des couches de convolution, en testant d’autres valeurs pour le dropout, en jouant sur la taille des deux activations linéaires rectifiées en fin de parcours, etc. Cependant, il apporte déjà de bonnes performances et permet de se placer correctement dans le leaderboard kaggle. Son autre avantage est qu’il n’est pas trop long à optimiser, comptez quelques heures sur une carte graphique “grand public” récente, comme une GTX 1070. Le notebook complet est disponible sur ce github.

La semaine prochaine nous verrons comment implémenter un modèle de deep learning récurrent basé sur des LSTM, une architecture plus en phase avec ce qui est développé récemment.

Nous publions régulièrement des articles sur des sujets de développement produit web et mobile, data et analytics, sécurité, cloud, hyperautomatisation et digital workplace.
Suivez-nous pour être notifié des prochains articles et réaliser votre veille professionnelle.

Retrouvez aussi nos publications et notre actualité via notre newsletter, ainsi que nos différents réseaux sociaux : LinkedIn, Twitter, Youtube, Twitch et Instagram

Vous souhaitez en savoir plus ? Consultez notre site web et nos offres d’emploi.

L’auteur

Jean-Baptiste
Data Scientist & deep learning fanatic

--

--

CBTW
L’Actualité Tech — Blog CBTW

Nos experts partagent leur vision et leur veille en développement web et mobile, data et analytics, sécurité, cloud, hyperautomation et digital workplace.