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).
Le principal objectif est de comparer les performances: temps de calcul, qualité des résultats, des principales technologies; ici Python avec la librairie Scikit-Learn. Il s'agit d'un problème de fouille de texte qui enchaîne nécessairement plusieurs étapes et le choix de la meilleure stratégie est fonction de l'étape:
L'objectif est ici de comparer les performances des méthodes et technologies en fonction de la taille de la base d'apprentissage. La stratégie de sous ou sur échantillonnage des catégories qui permet d'améliorer la prévision n'a pas été mise en oeuvre.
In [ ]:
#Importation des librairies utilisées
import unicodedata
import time
import pandas as pd
import numpy as np
import random
import nltk
import collections
import itertools
import csv
import warnings
from sklearn.cross_validation import train_test_split
Définition du répertoir de travail, des noms des différents fichiers utilisés et des variables globales.
Dans un premier temps, il vous faut télécharger les fichiers Categorie_reduit.csv
et lucene_stopwords.txt
disponible dans le corpus de données de wikistat.
Une fois téléchargées, placez ces données dans le repertoire de travail de votre choix et préciser la direction de ce repertoir dans la variable DATA_DIR
In [ ]:
# Répertoire de travail
DATA_DIR = ""
# Nom des fichiers
training_reduit_path = DATA_DIR + "data/cdiscount_train.csv.zip"
# Variable Globale
HEADER_TEST = ['Description','Libelle','Marque']
HEADER_TRAIN =['Categorie1','Categorie2','Categorie3','Description','Libelle','Marque']
In [ ]:
## Si nécessaire (première exécution) chargement de nltk, librairie pour la suppression
## des mots d'arrêt et la racinisation
# nltk.download()
Fonction permettant de lire le fichier d'apprentissage et de créer deux DataFrame Pandas, un pour l'apprentissage, l'autre pour la validation. La première méthode créée un DataFrame en lisant entièrement le fichier. Puis elle scinde le DataFrame en deux grâce à la fonction dédiée de sklearn.
In [ ]:
def split_dataset(input_path, nb_line, tauxValid,columns):
time_start = time.time()
data_all = pd.read_csv(input_path,sep=",",names=columns,nrows=nb_line)
data_all = data_all.fillna("")
data_train, data_valid = train_test_split(data_all, test_size = tauxValid)
time_end = time.time()
print("Split Takes %d s" %(time_end-time_start))
return data_train, data_valid
nb_line=20000 # part totale extraite du fichier initial ici déjà réduit
tauxValid=0.10 # part totale extraite du fichier initial ici déjà réduit
data_train, data_valid = split_dataset(training_reduit_path, nb_line, tauxValid, HEADER_TRAIN)
# Cette ligne permet de visualiser les 5 premières lignes de la DataFrame
data_train.head(5)
Afin de limiter la dimension de l'espace des variables ou features, tout en conservant les informations essentielles, il est nécessaire de nettoyer les données en appliquant plusieurs étapes:
STEMMER.stem
de la librairie nltk. La racinisation transforme un mot en son radical ou sa racine. Par exemple, les mots: cheval, chevaux, chevalier, chevalerie, chevaucher sont tous remplacés par "cheva".
In [ ]:
# Librairies
from bs4 import BeautifulSoup #Nettoyage d'HTML
import re # Regex
import nltk # Nettoyage des données
## listes de mots à supprimer dans la description des produits
## Depuis NLTK
nltk_stopwords = nltk.corpus.stopwords.words('french')
## Depuis Un fichier externe.
lucene_stopwords =open(DATA_DIR+"data/lucene_stopwords.txt","r").read().split(",") #En local
## Union des deux fichiers de stopwords
stopwords = list(set(nltk_stopwords).union(set(lucene_stopwords)))
## Fonction de setmming de stemming permettant la racinisation
stemmer=nltk.stem.SnowballStemmer('french')
Fonction qui prend en intrée un texte et retourne le texte nettoyé en appliquant successivement les étapes suivantes: Nettoyage des données HTML, conversion en texte minuscule, encodage uniforme, suppression des caractéres non alpha numérique (ponctuations), suppression des stopwords, racinisation de chaque mot individuellement.
In [ ]:
# Fonction clean générale
def clean_txt(txt):
### remove html stuff
txt = BeautifulSoup(txt,"html.parser",from_encoding='utf-8').get_text()
### lower case
txt = txt.lower()
### special escaping character '...'
txt = txt.replace(u'\u2026','.')
txt = txt.replace(u'\u00a0',' ')
### remove accent btw
txt = unicodedata.normalize('NFD', txt).encode('ascii', 'ignore').decode("utf-8")
###txt = unidecode(txt)
### remove non alphanumeric char
txt = re.sub('[^a-z_]', ' ', txt)
### remove french stop words
tokens = [w for w in txt.split() if (len(w)>2) and (w not in stopwords)]
### french stemming
tokens = [stemmer.stem(token) for token in tokens]
### tokens = stemmer.stemWords(tokens)
return ' '.join(tokens)
def clean_marque(txt):
txt = re.sub('[^a-zA-Z0-9]', '_', txt).lower()
return txt
In [ ]:
# fonction de nettoyage du fichier(stemming et liste de mots à supprimer)
def clean_df(input_data, column_names= ['Description', 'Libelle', 'Marque']):
#Test if columns entry match columns names of input data
column_names_diff= set(column_names).difference(set(input_data.columns))
if column_names_diff:
warnings.warn("Column(s) '"+", ".join(list(column_names_diff)) +"' do(es) not match columns of input data", Warning)
nb_line = input_data.shape[0]
print("Start Clean %d lines" %nb_line)
# Cleaning start for each columns
time_start = time.time()
clean_list=[]
for column_name in column_names:
column = input_data[column_name].values
if column_name == "Marque":
array_clean = np.array(list(map(clean_marque,column)))
else:
array_clean = np.array(list(map(clean_txt,column)))
clean_list.append(array_clean)
time_end = time.time()
print("Cleaning time: %d secondes"%(time_end-time_start))
#Convert list to DataFrame
array_clean = np.array(clean_list).T
data_clean = pd.DataFrame(array_clean, columns = column_names)
return data_clean
In [ ]:
# Take approximately 2 minutes fors 100.000 rows
data_valid_clean = clean_df(data_valid)
data_train_clean = clean_df(data_train)
Affiche les 5 premières lignes de la DataFrame d'apprentissage après nettoyage.
In [ ]:
data_train_clean.head(5)
La vectorisation, c'est-à-dire la construction des caractéristiques à partir de la liste des mots se fait en 2 étapes:
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
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.
In [ ]:
## Création d’une matrice indiquant
## les fréquences des mots contenus dans chaque description
## de nombreux paramètres seraient à tester
from sklearn.feature_extraction.text import TfidfVectorizer, TfidfTransformer
from sklearn.feature_extraction import FeatureHasher
def vectorizer_train(df, columns=['Description', 'Libelle', 'Marque'], nb_hash=None, stop_words=None):
# Hashage
if nb_hash is None:
data_hash = map(lambda x : " ".join(x), df[columns].values)
feathash = None
# TFIDF
vec = TfidfVectorizer(
min_df = 1,
stop_words = stop_words,
smooth_idf=True,
norm='l2',
sublinear_tf=True,
use_idf=True,
ngram_range=(1,2)) #bi-grams
tfidf = vec.fit_transform(data_hash)
else:
df_text = map(lambda x : collections.Counter(" ".join(x).split(" ")), df[columns].values)
feathash = FeatureHasher(nb_hash)
data_hash = feathash.fit_transform(map(collections.Counter,df_text))
vec = TfidfTransformer(use_idf=True,
smooth_idf=True, sublinear_tf=False)
tfidf = vec.fit_transform(data_hash)
return vec, feathash, tfidf
def apply_vectorizer(df, vec, columns =['Description', 'Libelle', 'Marque'], feathash = None ):
#Hashage
if feathash is None:
data_hash = map(lambda x : " ".join(x), df[columns].values)
else:
df_text = map(lambda x : collections.Counter(" ".join(x).split(" ")), df[columns].values)
data_hash = feathash.transform(df_text)
# TFIDF
tfidf=vec.transform(data_hash)
return tfidf
In [ ]:
vec, feathash, X = vectorizer_train(data_train_clean, nb_hash=60)
Y = data_train['Categorie1'].values
Xv = apply_vectorizer(data_valid_clean, vec, feathash=feathash)
Yv=data_valid['Categorie1'].values
In [ ]:
# Regression Logistique
## estimation
from sklearn.linear_model import LogisticRegression
cla = LogisticRegression(C=100)
cla.fit(X,Y)
score=cla.score(X,Y)
print('# training score:',score)
In [ ]:
## erreur en validation
scoreValidation=cla.score(Xv,Yv)
print('# validation score:',scoreValidation)
In [ ]:
#Méthode CART
from sklearn import tree
clf = tree.DecisionTreeClassifier()
time_start = time.time()
clf = clf.fit(X, Y)
time_end = time.time()
print("CART Takes %d s" %(time_end-time_start) )
score=clf.score(X,Y)
print('# training score :',score)
In [ ]:
scoreValidation=clf.score(Xv,Yv)
print('# validation score :',scoreValidation)
In [ ]:
# Random forest
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_estimators=100,n_jobs=-1,max_features=24)
time_start = time.time()
rf = rf.fit(X, Y)
time_end = time.time()
print("RF Takes %d s" %(time_end-time_start) )
score=rf.score(X,Y)
print('# training score :',score)
In [ ]:
scoreValidation=rf.score(Xv,Yv)
print('# validation score :',scoreValidation)