Il s'agit d'une version simplifiée du concours proposé par Cdiscount et paru sur le site datascience.net. Les données d'apprentissage sont accessibles sur demande auprès de Cdiscount mais les solutions de l'échantillon test du concours ne sont pas et ne seront pas rendues publiques. Un échantillon test est donc construit pour l'usage de ce tutoriel. L'objectif est de prévoir la catégorie d'un produit à partir de son descriptif (text mining). Seule la catégorie principale (1er niveau, 47 classes) est prédite au lieu des trois niveaux demandés dans le concours. L'objectif est plutôt de comparer les performances des méthodes et technologies en fonction de la taille de la base d'apprentissage ainsi que d'illustrer sur un exemple complexe le prétraitement de données textuelles.
Le jeux de données complet (15M produits) permet un test en vrai grandeur du passage à l'échelle volume des phases de préparation (munging), vectorisation (hashage, TF-IDF) et d'apprentissage en fonction de la technologie utilisée.
La synthèse des résultats obtenus est développée par Besse et al. 2016 (section 5).
Les données textuelles ne peuvent pas être utilisés directement dans les différents algorithmes d'apprentissage statistiques. Nous allons voir dans ce tutoriel plusieurs techniques permettant de traduire les données textuelles sous formes de vecteur numérique :
Fonction de vectorisation présente dans la librairie scikit-learn
:
One-Hot-Encoder
Tf-Idf
Hashing
Word Embedding présente dans la librairie gensim
:
Word2Vec
In [ ]:
#Importation des librairies utilisées
import time
import pandas as pd
import numpy as np
import collections
import itertools
import os
import warnings
warnings.filterwarnings('ignore')
from sklearn.cross_validation import train_test_split
In [ ]:
data_valid_clean_stem = pd.read_csv("data/cdiscount_valid_clean_stem.csv").fillna("")
data_train_clean_stem = pd.read_csv("data/cdiscount_train_clean_stem.csv").fillna("")
On créé un dossier dans lequel nous allons sauvegarder les DataFrame constitués des features que l'on va construire dans ce notebook
In [ ]:
DATA_OUTPUT_DIR = "data/features"
if not(os.path.isdir("data/features")):
os.mkdir("data/features")
Dans un premier temps, en guise d'exemple et pour réduire le temps de calcul, on ne considère que la colonne Description de nos DataFrame
générés dans le calepin précédent.
In [ ]:
train_array = data_train_clean_stem["Description"].values
valid_array = data_valid_clean_stem["Description"].values
In [ ]:
train_array[0]
In [ ]:
from sklearn.feature_extraction.text import CountVectorizer
extr_cv = CountVectorizer(binary=False)
data_train_OHE = extr_cv.fit_transform(train_array)
Q A quoi sert l'argument binary de la classe?
Q Sous quel format sont stockées les vecteurs OHE? Pourquoi ce format est-il choisi?
La fonction get_feature_names
permet d'avoir accès à la liste des mots présents dans l'ensemble des lignes de l'array converti.
In [ ]:
vocabulary = extr_cv.get_feature_names()
N_vocabulary = len(vocabulary)
print("Nombre de mots : %d" %N_vocabulary )
Exercice Pour la première ligne de votre dataset train. retrouvez l'ensemble des mots constituant cette ligne à partir de l'objet data_train_OHE
et de vocabulary
ainsi que le nombre d'occurence de chacun de ces mots dans la ligne.
In [ ]:
# %load solution/2_1.py
La même transformation est appliqué sur l'échantillon de validation.
In [ ]:
data_valid_OHE = extr_cv.transform(valid_array)
data_valid_OHE
Q Que se passe-til pour les mots présents dans le dataset de validation mais qui ne sont pas présent dans le dataset d'apprentissage?
Q Pourquoi on ne 're-fit' pas la Classe CountVectorizer
l'échantillon de validation.
TF-IDF. Le TF-IDF permet de faire ressortir l'importance relative de chaque mot $m$ (ou couples de mots consécutifs) dans un texte-produit ou un descriptif $d$, par rapport à la liste entière des produits. La fonction $TF(m,d)$ compte le nombre d'occurences du mot $m$ dans le descriptif $d$. La fonction $IDF(m)$ mesure l'importance du terme dans l'ensemble des documents ou descriptifs en donnant plus de poids aux termes les moins fréquents car considérés comme les plus discriminants (motivation analogue à celle de la métrique du chi2 en anamlyse des correspondance). $IDF(m,l)=\log\frac{D}{f(m)}$ où $D$ est le nombre de documents, la taille de l'échantillon d'apprentissage, et $f(m)$ le nombre de documents ou descriptifs contenant le mot $m$. La nouvelle variable ou features est $V_m(l)=TF(m,l)\times IDF(m,l)$.
Comme pour les transformations des variables quantitatives (centrage, réduction), la même transformation c'est-à-dire les mêmes pondérations, est calculée sur l'achantillon d'apprentissage et appliquée à celui de test.
On utiliser la fonction TfidfVectorizer
qui permet de parser également le texte
Dans un premier temps, on fixe le paramètre norm
= False afin de rendre les résultats plus explicite et analyser les sorties
In [ ]:
from sklearn.feature_extraction.text import TfidfVectorizer
vec = TfidfVectorizer(ngram_range=(1,1), norm = False)
data_train_TFIDF = vec.fit_transform(train_array)
Q A quoi sert l'argument ngram_range?
Constatez que data_train_TFIDF
est stocké sous le même format que data_train_OHE
et que la taile du vocabulaire est la même.
In [ ]:
vocabulary = vec.get_feature_names()
N_vocabulary = len(vocabulary)
N_vocabulary
Exercice Pour la première ligne de votre dataset train. retrouvez l'ensemble des mots constituant cette ligne à partir de l'objet data_train_TFIDF
et de vocabulary
ainsi que la valeur de l'idf, du tf et du poids tfidf de chacun de ces mots dans la ligne
In [ ]:
# %load solution/2_2.py
Q Commentez les valeurs de l'idf pour chacun des mots.
Q Comment evolue les poids en changeant les paramètre smooth idf et sublinear_tf de la méthode TfidfVectorizer
?
Exercice Changez l'argument ngram_range de la méthode TfidfVectorizer
et re-affichez les résultats.
On applique maintenant le vectorizer
sur le jeu de données de validation
In [ ]:
data_valid_TFIDF = vec.transform(valid_array)
data_valid_TFIDF
ATENTION Si le tf est recalculé pour chaque ligne, le même idf est utilisé
In [ ]:
# %load solution/2_2bis.py
Le Hashage. Il permet de réduire l'espace des variables (taille du dictionnaire) en un nombre limité et fixé a priori n_hash
de caractéristiques. Il repose sur la définition d'une fonction de hashage, $h$ qui à un indice $j$ défini dans l'espace des entiers naturels, renvoie un indice $i=h(j)$ dans dans l'espace réduit (1 à n_hash) des caractéristiques. Ainsi le poids de l'indice $i$, du nouvel espace, est l'association de tous les poids d'indice $j$ tels que $i=h(j)$ de l'espace originale. Ici, les poids sont associés d'après la méthode décrite par Weinberger et al. (2009).
N.B. $h$ n'est pas généré aléatoirement. Ainsi pour un même fichier d'apprentissage (ou de test) et pour un même entier n_hash, le résultat de la fonction de hashage est identique
Au contraire des classe CountVectorizer
et TfidfVectorizer
, la classe FeatureHasher
prend en entré un dictionnaire d'occurence des mots.
In [ ]:
train_dict_array = list(map(lambda x : collections.Counter(x.split(" ")), train_array))
train_dict_array[0]
In [ ]:
from sklearn.feature_extraction import FeatureHasher
nb_hash = 300
feathash = FeatureHasher(nb_hash)
data_train_hash = feathash.fit_transform(train_dict_array)
Constatez que data_train_hash
est stocké sous le même format que data_train_OHE
ou data_train_TFIDF
.
Q Que dire cependant de sa dimension?
La cellule suivante permet d'afficher le poids de chacun des indices dans le nouvel espace.
In [ ]:
ir = 0
rw = data_train_hash.getrow(ir)
print("Liste des tokens racinisé de la première ligne : " + train_array[0])
pd.DataFrame([(v, k) for k,v in zip(rw.data,rw.indices)], columns=["indices","weight"])
Q Que pouvez-vous dire des poids?
La taille de la matrice a donc été très réduit par rapport au vectorizer, TFIDF ou One-Hot-Enconding. Cependant, il n'y a pas de fonction inverse transform ce qui peut rendre le résultat moin compréhensible.
Il est possible de combiner le FeatureHasher
avec un autre vectorizer comme le TFIDF.
C'est cette fois la classe TFIDFTransformer
qui est utilisé. Celle ci ne ne considère pas des string en entré mais l'array en sortie de l'étape de Hashage. Les mots sont les nb_hash
indices sélectionnés et le tf pour chaque individu sont les poids calculé par la fonction de hasage.
In [ ]:
from sklearn.feature_extraction.text import TfidfTransformer
vec = TfidfTransformer(norm = False)
data_train_HTfidf = vec.fit_transform(data_train_hash)
data_train_HTfidf
In [ ]:
ir = 0
rw = data_train_HTfidf.getrow(ir)
print(train_array[ir])
pd.DataFrame([(ind, vec.idf_[ind], w/vec.idf_[ind], w) for w,ind in zip(rw.data, rw.indices)], columns=["indices","idf","tf","weight"])
Afin de comparer l'effet de ces différentes vectorisation sur l'apprentissage, nous allons sauvegarder ces dernières sur les machines.
De nombreux paramètres sont à régler ce qui entraine donc un un très grand nombre de combinaison.
ici nous allons créer 4 jeu de données avec CountVectorizer
et TFIDF
chacun avec et sans hashage de taille 300.
Il est evidemment possible de tester d'autre combinaison. Libre à vous de les tester ;)
In [ ]:
def vectorizer_train(df, columns=['Description'], nb_hash=None, nb_gram = 1, vectorizer = "tfidf" , binary = False):
data_array = [" ".join(line) for line in df[columns].values]
# Hashage
if nb_hash is None:
feathash = None
if vectorizer == "tfidf":
vec = TfidfVectorizer(ngram_range=(1,nb_gram))
data_vec = vec.fit_transform(data_array)
else:
vec = CountVectorizer(binary=binary)
data_vec = vec.fit_transform(data_array)
else:
data_dic_array = [collections.Counter(line.split(" ")) for line in data_array]
feathash = FeatureHasher(nb_hash)
data_hash = feathash.fit_transform(data_dic_array)
if vectorizer=="tfidf":
vec = TfidfTransformer()
data_vec = vec.fit_transform(data_hash)
else:
vec = None
data_vec = data_hash
return vec, feathash, data_vec
def apply_vectorizer(df, vec, feathash, columns =['Description', 'Libelle', 'Marque']):
data_array = [" ".join(line) for line in df[columns].values]
#Hashage
if feathash is None:
data_hash = data_array
else:
data_dic_array = [collections.Counter(line.split(" ")) for line in data_array]
data_hash = feathash.transform(data_dic_array)
if vec is None:
data_vec = data_hash
else:
data_vec = vec.transform(data_hash)
return data_vec
In [ ]:
parameters = [[None, "count"],
[300, "count"],
[None, "tfidf"],
[300, "tfidf"]]
from scipy import sparse
for nb_hash, vectorizer in parameters:
ts = time.time()
vec, feathash, data_train_vec = vectorizer_train(data_train_clean_stem, nb_hash=nb_hash, vectorizer = vectorizer)
data_valid_vec = apply_vectorizer(data_valid_clean_stem, vec, feathash)
te = time.time()
print("nb_hash : " + str(nb_hash) + ", vectorizer : " + str(vectorizer))
print("Runing time for vectorization : %.1f seconds" %(te-ts))
print("Train shape : " + str(data_train_vec.shape))
print("Valid shape : " + str(data_valid_vec.shape))
sparse.save_npz(DATA_OUTPUT_DIR +"/vec_train_nb_hash_" + str(nb_hash) + "_vectorizer_" + str(vectorizer), data_train_vec)
sparse.save_npz(DATA_OUTPUT_DIR +"/vec_valid_nb_hash_" + str(nb_hash) + "_vectorizer_" + str(vectorizer), data_valid_vec)
Dans cette partie, des modèles Word2Vec
Seront créés à l'aide de la librairie gensim.
In [ ]:
import gensim
In [ ]:
train_array_token = [line.split(" ") for line in train_array]
valid_array_token = [line.split(" ") for line in valid_array]
train_array_token[0]
Cette fonction contient un grand nombre d' arguments. Le but de ce TP n'est pas d'optimiser les paramètres de ce modèle mais de les comprendre. Nous allons donc fixer quelques arguments par défault :
Q A quoi servent les arguments hs et negative? Quels influences ces arguments ont sur le modèle avec les valeurs définies ici?
In [ ]:
Features_dimension = 300
hs = 0
negative = 10
Nous allons créer deux modèles :
In [ ]:
sg = 1
print("Start learning skip-gram Word2Vec")
ts = time.time()
model_sg = gensim.models.Word2Vec(train_array_token, sg=sg, hs=hs, negative=negative, min_count=1, size=Features_dimension)
te = time.time()
t_learning = te-ts
print("Learning time : %.2f Word2Vec" %t_learning)
sg = 0
print("Start learning CBOW Word2Vec")
ts = time.time()
model_cbow = gensim.models.Word2Vec(train_array_token, sg=sg, hs=hs, negative=negative, min_count=1, size=Features_dimension)
te = time.time()
t_learning = te-ts
print("Learning time : %.2f Word2Vec" %t_learning)
Q Que dire du temps d'apprentissage de ces deux modèles? D'ou vient cette différence?
Comme pour les réseaux de convolution, des modèles pré-entrainés de Word2Vec éxistent également.
Le plus célèbre et le plus utilisé étant GoogleNewsVectors
appris sur plus de 100 milliard de mots à partir des articles de GoogleNews. Cependant ce modèle est en anglais, et n'est donc )as utile ici.
On utilisera des modèle appris dans le projet suivant https://github.com/Kyubyong/wordvectors appris sur 1Giga d'articles de wikipedia en mode *Skip-Gram
Vous pouvez télécharger ce modèle en suivant ce lien. Dezipez-le puis téléchargez le modèle en indiquant la direction du fichier "fr/bin"
In [ ]:
model_online_dir = "data/fr/fr.bin"
#model_online_dir = "ACOMPLETER/fr.bin"
model_online = gensim.models.Word2Vec.load(model_online_dir)
Nous allons maintenant comparer quelques propriétés de chacun des trois modèles à notre disposition (CBOW, Skip-Gram et le modèle online)
Les modèles que nous avons appris l'ont été sur les mots racinisé. Ainsi, nous allons avoir besoin de la racine des mots pour tester les différentes propriétés du modèle.
In [ ]:
import nltk
stemmer=nltk.stem.SnowballStemmer('french')
La fonction most_similar
de gensim permet de retrouver les mots les plus proches à un ou une combinaison de mots données en argument.
Q A l'aide de la documentation répondez aux questions suivantes. Quelle est la mesure de similarité utilisée? Dans quel espace est-elle utilisé? Comment fonctionne la fonction lorsque plusieurs mots sont passés en paramètres?
In [ ]:
# %load solution/2_3.py
Q Comparez la qualité de prévision des modèles que nous avons entrainés sur le jeu de données 'Cdiscount' avec celui appris online. Que pouvez-vous en dire?
Q Comparez les prévisions des deux modèles que nous avons entrainés. Que pouvez-vous en dire?
Exercice Affichez maintenant les sorties de la fonction 'most_similar' pour le mot femme.
Exercice Affichez les sorties de la fonction 'most_similar' pour des mots propre au jeu de données (ex. xbox, pantalon,..)
In [ ]:
# %load solution/2_4.py
Exercice Testez d'autres combinaisons si vous le souhaitez.
La fonction predict_output_word
de gensim permet de retrouver les mots prédit par le modèle à partir d'un mot ou d'une combinaison de mots données en argument.
Exercice Affichez la prédiction des trois modèles pour des mots/combinaisons de mots communs (homme, femme) ou plus propre au jeu de données étudiés. (coque-de-téléphone)...
In [ ]:
# %load solution/2_5.py
Nous allons maintenant créer des matrices de features à partir des modèles de Word2Vec générés précédemment dans un but de prédiction.
La modèles créés permettent d'obtenir pour chaque mot x
, sa représentation dans l'espace d'embeddings de la manière suivante :
x_feature = model_word2vec[x]
Dans notre problématique, les individus que nous cherchons à classer sont des descriptions représentées par une liste de token à l'issue du nettoyage du calepin précédent. Il exsite donc plusieurs façon de représenter ces individus à l'aide de modèle Word2Vec.
C'est la seconde solution qui sera utilisée ici.
Les fonctions détaillées ci-dessous permettent de :
get_features_mean
: retourne le vecteur moyen dans l'espace d'embedding, des projections des mots/tokens composant linesget_matrix_features_means
: applique la fonction get_features_mean
sur tous les éléments de la matrice X.
In [ ]:
def get_features_mean(lines, model, f_size):
features = [model[x] for x in lines if x in model]
if features == []:
fm =np.ones(f_size)
else :
fm = np.mean(features,axis=0)
return fm
def get_matrix_features_means(X, model, f_size):
X_embedded_ = list(map(lambda x : get_features_mean(x, model, f_size), X))
X_embedded = np.vstack(X_embedded_)
return X_embedded
In [ ]:
ts = time.time()
X_embedded_train_cbow = get_matrix_features_means(train_array_token, model_cbow, Features_dimension)
te = time.time()
t_build = te-ts
#np.save(embedded_train_dir, X_embedded_train)
print("Time conversion : %d seconds"%t_build)
print("Shape Matrix : (%d,%d)"%X_embedded_train_cbow.shape)
np.save(DATA_OUTPUT_DIR +"/embedded_train_cbow", X_embedded_train_cbow)
ts = time.time()
X_embedded_valid_cbow = get_matrix_features_means(valid_array_token, model_cbow, Features_dimension)
te = time.time()
t_build = te-ts
#np.save(embedded_train_dir, X_embedded_train)
print("Time conversion : %d seconds"%t_build)
print("Shape Matrix : (%d,%d)"%X_embedded_valid_cbow.shape)
np.save(DATA_OUTPUT_DIR +"/embedded_valid_cbow", X_embedded_valid_cbow)
In [ ]:
ts = time.time()
X_embedded_train_sg = get_matrix_features_means(train_array_token, model_sg, Features_dimension)
te = time.time()
t_build = te-ts
#np.save(embedded_train_dir, X_embedded_train)
print("Time conversion : %d seconds"%t_build)
print("Shape Matrix : (%d,%d)"%X_embedded_train_sg.shape)
np.save(DATA_OUTPUT_DIR +"/embedded_train_sg", X_embedded_train_sg)
ts = time.time()
X_embedded_valid_sg = get_matrix_features_means(valid_array_token, model_sg, Features_dimension)
te = time.time()
t_build = te-ts
#np.save(embedded_train_dir, X_embedded_train)
print("Time conversion : %d seconds"%t_build)
print("Shape Matrix : (%d,%d)"%X_embedded_valid_sg.shape)
np.save(DATA_OUTPUT_DIR +"/embedded_valid_sg", X_embedded_valid_sg)
In [ ]:
data_valid_clean = pd.read_csv("data/cdiscount_valid_clean.csv").fillna("")
data_train_clean = pd.read_csv("data/cdiscount_train_clean.csv").fillna("")
train_array_token_nostem = [line.split(" ") for line in data_train_clean["Description"].values]
valid_array_token_nostem = [line.split(" ") for line in data_valid_clean["Description"].values]
In [ ]:
ts = time.time()
X_embedded_train_online = get_matrix_features_means(train_array_token_nostem, model_online, Features_dimension)
te = time.time()
t_build = te-ts
#np.save(embedded_train_dir, X_embedded_train)
print("Time conversion : %d seconds"%t_build)
print("Shape Matrix : (%d,%d)"%X_embedded_train_online.shape)
np.save(DATA_OUTPUT_DIR +"/embedded_train_online", X_embedded_train_online)
ts = time.time()
X_embedded_valid_online = get_matrix_features_means(valid_array_token_nostem, model_online, Features_dimension)
te = time.time()
t_build = te-ts
#np.save(embedded_train_dir, X_embedded_train)
print("Time conversion : %d seconds"%t_build)
print("Shape Matrix : (%d,%d)"%X_embedded_valid_online.shape)
np.save(DATA_OUTPUT_DIR +"/embedded_valid_online", X_embedded_valid_online)
In [ ]: