Classification de pages via le Machine Learning sur un univers de concurrence avec Python – II

Cet article fait suite aux 5 articles précédents :

Dans cet article nous allons refaire un essai de Machine Learning sur un univers de concurrence à partir des données que nous avions enrichies dans l’article précédent.

Pour ce faire nous allons re-tester les algorithmes que nous avions utilisés précédement : K-NN (k plus proches voisins), LogisticRegression et LinearSVC ainsi que Ridge, Lasso et ElasticNet. Nous allons aussi tester l’algorithme XGBoost que nous avions utilisé dans une série d’articles précédents sur le Machine Learning avec R.

Algorithmes Ridge, Lasso et ElasticNet

Les modèles Ridge, Lasso et Elasticnet font partie des modèles linéaires « régularisés ».

Pour résumer, il s’agit de modèles dont on peut faire varier des paramètres qui permettent d’éviter la sur-optimisation sur les données d’entrainement.

Algorithme XGBoost

L’algorithme XGBoost (pour eXtrem Gradient Boosting) est un algorithme d’apprentissage machine supervisé. Actuellement très à la mode, il donne souvent de très bons résultats par rapport à d’autres algorithmes.

Plus complexe que les modèles linéaires précédents, il n’est pas très aisée d’expliquer facilement comment cela fonctionne.

Pour faire simple, disons que le « boosting » singe les règles de l’apprentissage : le boosting consiste à faire des essais/erreurs et de sélectionner ce qui marche.

La méthode derrière le boosting est basée sur des arbres de décisions comme par exemple dans le modèle Random Forest.

La différence fondamentale est que Random Forest crée plusieurs arbres en parallèle et ensuite on construit la meilleure solution à partir de tous les arbres créés. Cela se fait selon un « vote majoritaire », ce qui veut dire que tous les arbres ont le même poids dans le calcul. On ne considère pas qu’il y en a de meilleurs que d’autres..

Le boosting se fait de façon séquentielle. A chaque itération l’arbre de décision s’améliore car il tient compte des erreurs précédentes et va les corriger. Du fait de ces calculs à chaque itération, il peut être plus lent que Random Forest.

Le gradient boosting utilise une méthode dite de « descente du gradient » (ou plus forte pente) pour minimiser cette erreur à chaque itération.

Remarque : on parle aussi de notion de coût pour l’erreur et de fonction de coût pour la fonction qui calcule cette erreur.

eXtrem Gradient Boosting est un version sophistiquée du Gradient Boosting et est implémentée dans Python.

XGBoost comporte de nombreux paramètres et donne des résultats très différents selon ceux-ci. C’est pourquoi nous verrons dans un prochain article comment optimiser ces paramètres de façon automatique.

De quoi aurons nous besoin ?

Python Anaconda

Comme précédemment, téléchargez la version de Python Anaconda qui vous convient selon votre ordinateur.

Jeu de données

Téléchargez le jeu de données des Requêtes/Pages/Positions sur Github : https://github.com/Anakeyn/GSCCompetitorsMachineLearning2/raw/master/dfQPPS7.csv

Code Source

Vous pouvez soit copier/coller les morceaux de codes ci-dessous, soit récupérer le code source en entier sur notre Github à l’adresse : https://github.com/Anakeyn/GSCCompetitorsMachineLearning2.

Chargement des bibliothèques utiles

# -*- coding: utf-8 -*-
"""
Created on Mon Jul 1 10:29:18 2019

@author: Pierre
"""
##########################################################################
# GSCCompetitorsML2  - Modifié le 26/11/2019
# Auteur : Pierre Rouarch - Licence GPL 3
# Machine Learning sur un univers de concurrence 2
#Données enrichies via Scraping précédemment. récupérées via le fichier dfQPPS7.csv allégé.
# focus sur la précision du test au lieu du F1-Score global
#####################################################################################

###################################################################
# On démarre ici 
###################################################################
#Chargement des bibliothèques générales utiles
import numpy as np #pour les vecteurs et tableaux notamment
import matplotlib.pyplot as plt  #pour les graphiques
#import scipy as sp  #pour l'analyse statistique
import pandas as pd  #pour les Dataframes ou tableaux de données
#import seaborn as sns #graphiques étendues
#import math #notamment pour sqrt()
import os

# Machine Learning
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import LinearSVC
from xgboost import XGBClassifier
from sklearn.linear_model import Ridge
from sklearn.linear_model import Lasso
from sklearn.linear_model import ElasticNet
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

#pour les scores
from sklearn.metrics import f1_score
#from sklearn.metrics import matthews_corrcoef
from sklearn.metrics import accuracy_score
print(os.getcwd())  #verif
#mon répertoire sur ma machine - nécessaire quand on fait tourner le programme 
#par morceaux dans Spyder.
#myPath = "C:/Users/Pierre/MyPath"
#os.chdir(myPath) #modification du path
#print(os.getcwd()) #verif

Détermination des variables et préparation des données

#############################################################
#  Machine Learning sur les données enrichies après scraping
#############################################################

#Lecture des données suite  à scraping ############
dfQPPS8 = pd.read_csv("dfQPPS7.csv")
dfQPPS8.info(verbose=True) # 12194 enregistrements.    
dfQPPS8.reset_index(inplace=True, drop=True) 

#Variables explicatives
X =  dfQPPS8[['isHttps', 'level', 
             'lenWebSite', 'lenTokensWebSite',  'lenTokensQueryInWebSiteFrequency',  'sumTFIDFWebSiteFrequency',            
             'lenPath', 'lenTokensPath',  'lenTokensQueryInPathFrequency' , 'sumTFIDFPathFrequency',  
              'lenTitle', 'lenTokensTitle', 'lenTokensQueryInTitleFrequency', 'sumTFIDFTitleFrequency',
              'lenDescription', 'lenTokensDescription', 'lenTokensQueryInDescriptionFrequency', 'sumTFIDFDescriptionFrequency',
              'lenH1', 'lenTokensH1', 'lenTokensQueryInH1Frequency' ,  'sumTFIDFH1Frequency',        
              'lenH2', 'lenTokensH2',  'lenTokensQueryInH2Frequency' ,  'sumTFIDFH2Frequency',          
              'lenH3', 'lenTokensH3', 'lenTokensQueryInH3Frequency' , 'sumTFIDFH3Frequency',
              'lenH4',  'lenTokensH4','lenTokensQueryInH4Frequency', 'sumTFIDFH4Frequency', 
              'lenH5', 'lenTokensH5', 'lenTokensQueryInH5Frequency', 'sumTFIDFH5Frequency', 
              'lenH6', 'lenTokensH6', 'lenTokensQueryInH6Frequency', 'sumTFIDFH6Frequency', 
              'lenB', 'lenTokensB', 'lenTokensQueryInBFrequency', 'sumTFIDFBFrequency', 
              'lenEM', 'lenTokensEM', 'lenTokensQueryInEMFrequency', 'sumTFIDFEMFrequency', 
              'lenStrong', 'lenTokensStrong', 'lenTokensQueryInStrongFrequency', 'sumTFIDFStrongFrequency', 
              'lenBody', 'lenTokensBody', 'lenTokensQueryInBodyFrequency', 'sumTFIDFBodyFrequency', 
              'elapsedTime', 'nbrInternalLinks', 'nbrExternalLinks' ]]  #variables explicatives

X.info()
y =  dfQPPS8['group']

#on va scaler
scaler = StandardScaler()
scaler.fit(X)


X_Scaled = pd.DataFrame(scaler.transform(X.values), columns=X.columns, index=X.index)
X_Scaled.info()

#on choisit random_state = 42 en hommage à La grande question sur la vie, l'univers et le reste
#dans "Le Guide du voyageur galactique"   par  Douglas Adams. Ceci afin d'avoir le même split
#tout au long de notre étude.
X_train, X_test, y_train, y_test = train_test_split(X_Scaled,y, random_state=42)

Méthode des K plus proches voisins

On va rechercher le « meilleur » K en le faisant varier. le choix se fait au meilleur test score.

######################################################
# MODELE KNN
######################################################

#pour KNN  recherche du nombre de voisins optimal
nMax=20   #nombre max de voisins
myTrainScore =  np.zeros(shape=nMax)
myTestScore = np.zeros(shape=nMax)
myTrainTestScore = np.zeros(shape=nMax)

for n in range(1,nMax) :
    print("n_neighbors:"+str(n))
    knn = KNeighborsClassifier(n_neighbors=n) 
    knn.fit(X_train, y_train) 
    myTrainScore[n]=knn.score(X_train,y_train)
    print("Training set score: {:.3f}".format(knn.score(X_train,y_train))) #
    myTestScore[n]=knn.score(X_test,y_test)
    print("Test set score: {:.4f}".format(knn.score(X_test,y_test))) #

    
#Graphique train score vs test score 
sns.set()  #paramètres esthétiques ressemble à ggplot par défaut.
fig, ax = plt.subplots()  #un seul plot
sns.lineplot(x=np.arange(1,nMax), y=myTrainScore[1:nMax])
sns.lineplot(x=np.arange(1,nMax), y=myTestScore[1:nMax], color='red')

fig.suptitle("Les scores diminuent avec le nombre de voisins.", fontsize=14, fontweight='bold')
ax.set(xlabel='n neighbors', ylabel='Train (bleu) / Test (rouge) / F1 (jaune)',
       title="")
fig.text(.2,-.06,"Classification Knn - Univers de Concurrence - Position  dans 2 groupes \n vs variables construites + variables pages en fonction des n voisins", 
         fontsize=9)
#plt.show()
fig.savefig("QPPS6-KNN-Classifier-2goups.png", bbox_inches="tight", dpi=600)

#on choist le meilleur test score
#à vérifier toutefois en regardant la courbe.
indices = np.where(myTestScore == np.amax(myTestScore))
n_neighbor =  indices[0][0]
n_neighbor
knn = KNeighborsClassifier(n_neighbors=n_neighbor) 
knn.fit(X_train, y_train) 
print("N neighbor="+str(n_neighbor))
print("Training set score: {:.3f}".format(knn.score(X_train,y_train))) #
print("Test set score: {:.4f}".format(knn.score(X_test,y_test)))

#Test Score retenu pour knn :  0.7553 avec 2 voisins 
#légèrement meilleur que précédemment  avec moins de variables : 0,7368

Recherche du k voisin

Le meilleur Test score est pour 2 voisins

N neighbor=2
Training set score: 0.876
Test set score: 0.7553

Régression Logistique

Voyons ce que l’on obtient avec la régression logistique en faisant varier le paramètre de régularisation C.

###############################################################################
#Classification linéaire 1 :   Régression Logistique
#on faire varier C : inverse of regularization strength; must be a positive float. 
#Like in support vector machines, smaller values specify stronger regularization.
myC=1
print("Regression Logistique myC="+str(myC))
logreg = LogisticRegression(C=myC, solver='lbfgs', max_iter=1000).fit(X_train,y_train)
print("Training set score: {:.3f}".format(logreg.score(X_train,y_train)))  
print("Test set score: {:.3f}".format(logreg.score(X_test,y_test))) 
#le test  score 0.700 est moins bon que pour knn 0.7553  

myC=100
print("Regression Logistique myC="+str(myC))
logreg100 = LogisticRegression(C=myC, solver='lbfgs', max_iter=1000).fit(X_train,y_train)
print("Training set score: {:.3f}".format(logreg100.score(X_train,y_train)))  
print("Test set score: {:.3f}".format(logreg100.score(X_test,y_test)))  
#le test  score 0.700 est moins bon que pour knn 0.7553  .

myC=0.01
print("Regression Logistique myC="+str(myC))
logreg001 = LogisticRegression(C=myC, solver='lbfgs',max_iter=1000).fit(X_train,y_train)
print("Training set score: {:.3f}".format(logreg001.score(X_train,y_train)))  
print("Test set score: {:.3f}".format(logreg001.score(X_test,y_test)))  
#le test  score 0.699  est moins bon que pour knn 0.7553  .et que pour C=1 ou C=100


myC=1000
print("Regression Logistique myC="+str(myC))
logreg1000 = LogisticRegression(C=myC, solver='lbfgs', max_iter=1000).fit(X_train,y_train)
print("Training set score: {:.3f}".format(logreg1000.score(X_train,y_train)))  
print("Test set score: {:.3f}".format(logreg1000.score(X_test,y_test)))  
#pareil le test  score 0.700 est moins bon que pour knn 0.7553  .


Les résultats sont moins bon que pour KNN. Notre meilleur test score est obtenu avec C = 1, 100 ou 100 :

Regression Logistique myC=1
Training set score: 0.697
Test set score: 0.700

Machine à vecteurs de support linéaire

############################################################
#Classification linéaire 2 :  machine à vecteurs de support linéaire (linear SVC).
LinSVC = LinearSVC(max_iter=100000).fit(X_train,y_train)
print("Training set score: {:.3f}".format(LinSVC.score(X_train,y_train)))  
print("Test set score: {:.3f}".format(LinSVC.score(X_test,y_test))) 
y_pred=LinSVC .predict(X_Scaled) 
print("F1-Score : {:.4f}".format(f1_score(y, y_pred, average ='weighted')))

Ce modèle ne fait pas mieux. En plus il faut un nombre d’itérations importantes pour que le modèle converge. Ici même à 100.000 itérations on a une erreur.

Training set score: 0.693
Test set score: 0.685

Régularisation Ridge

On fera varier le paramètre alpha : 1, 10 et 0,1.

############################################################
#Ridge 
#régression ridge avec la valeur par défaut du paramètre de contrôle 
#alpha=1, 
ridge = Ridge().fit(X_train,y_train)
print("Training set score: {:.2f}".format(ridge.score(X_train,y_train)))
print("Test set score: {:.2f}".format(ridge.score(X_test,y_test)))
#valeurs étranges 
#Training set score: 0.08
#Test set score: 0.05

#Autres valeurs pour Ridge 
ridge10 = Ridge(alpha=10).fit(X_train,y_train)
print("Training set score: {:.2f}".format(ridge10.score(X_train,y_train)))
print("Test set score: {:.2f}".format(ridge10.score(X_test,y_test)))
#valeurs étranges 
#Training set score: 0.08
#Test set score: 0.06

ridge01 = Ridge(alpha=0.1).fit(X_train,y_train)
print("Training set score: {:.2f}".format(ridge01.score(X_train,y_train)))
print("Test set score: {:.2f}".format(ridge01.score(X_test,y_test)))
#valeurs étranges 
#Training set score: 0.08
#Test set score: 0.05

Le modèle Ridge produit des valeurs aberrantes.

Régularisation Lasso

on fait varier le paramètre alpha : 1, 0,01, 10, 0,0001

##############################################
#Lasso
#Valeurs par défaut alpha=1
lasso = Lasso().fit(X_train,y_train)
print("Training set score: {:.2f}".format(lasso.score(X_train,y_train)))
print("Test set score: {:.2f}".format(lasso.score(X_test,y_test)))
print("Number of features used: {}".format(np.sum(lasso.coef_ != 0)))
#Valeurs étranges
#Training set score: 0.00
#Test set score: -0.00
#Number of features used: 0

#Lasso autres valeurs
lasso001 = Lasso(alpha=0.01,max_iter=100000).fit(X_train,y_train)
print("Training set score: {:.2f}".format(lasso001.score(X_train,y_train)))
print("Test set score: {:.2f}".format(lasso001.score(X_test,y_test)))
print("Number of features used: {}".format(np.sum(lasso001.coef_ != 0)))
#Valeurs étranges
#Training set score: 0.07
#Test set score: 0.07
#Number of features used: 18

lasso10 = Lasso(alpha=10,max_iter=100000).fit(X_train,y_train)
print("Training set score: {:.2f}".format(lasso10.score(X_train,y_train)))
print("Test set score: {:.2f}".format(lasso10.score(X_test,y_test)))
print("Number of features used: {}".format(np.sum(lasso10.coef_ != 0)))
#Valeurs étranges
#Training set score: 0.00
#Test set score: -0.00
#Number of features used: 0

lasso00001 = Lasso(alpha=0.0001,max_iter=100000).fit(X_train,y_train)
print("Training set score: {:.2f}".format(lasso00001.score(X_train,y_train)))
print("Test set score: {:.2f}".format(lasso00001.score(X_test,y_test)))
print("Number of features used: {}".format(np.sum(lasso00001.coef_ != 0)))
#Valeurs étranges
#Training set score: 0.08
#Test set score: 0.06
#Number of features used: 57

Lasso donne aussi des valeurs étranges.

Régularisation ElasticNet

On va faire varier les paramètre alpha et l1_ratio

##############################################
#régression elastic-net
#avec les valeurs par défaut des paramètres de contrôle.

baseElasticNet = ElasticNet().fit(X_train,y_train)
print("Training set score: {:.2f}".format(baseElasticNet.score(X_train,y_train)))
print("Test set score: {:.2f}".format(baseElasticNet.score(X_test,y_test)))
print("Number of features used: {}".format(np.sum(baseElasticNet.coef_ != 0)))
#Valeurs étranges
#Training set score: 0.00
#Test set score: -0.00
#Number of features used: 0


#ElasticNet avec plusieurs valeurs pour alpha et l1_ratio
import itertools  #pour itérer sur 2 variables.
a=[0.0001, 0.001, 0.01, 0.1, 0.2, 0.3]
l=[0.0001, 0.01, 0.1, 0.5, 1]  



myENTestScore = []
for myAlpha, myL1 in itertools.product(a,l) :
    myElasticNet =   ElasticNet(alpha=myAlpha, l1_ratio=myL1 ).fit(X_train,y_train)
    print("Alpha: {:.6f}".format(myAlpha)) 
    print("L1 Ratio: {:.6f}".format(myL1)) 
    print("Training set score: {:.2f}".format(myElasticNet.score(X_train,y_train)))
    print("Test set score: {:.2f}".format(myElasticNet.score(X_test,y_test)))
    print("Number of features used: {}".format(np.sum(myElasticNet.coef_ != 0)))
    myENTestScore.append(myElasticNet.score(X_test,y_test))

#le max des Test Score pour ElasticNet
max( myENTestScore)
#le meilleur test  score   0.07332222123115184  valeur aberrantes

Le modèle ElasticNet donne aussi des valeurs aberrantes.

Graphique Importance des variables.

On va faire le graphique pour la régression logistique avec C=1 (standard) qui a eu le meilleur test Score des modèles linéaires :

#######################################################################
# Affichage de l'importance des variables on prend logreg qui 
# est le "meilleur"
#######################################################################
signed_feature_importance = logreg.coef_[0] #pour afficher le sens 
feature_importance = abs(logreg.coef_[0])  #pous classer par importance
#feature_importance = 100.0 * (feature_importance / feature_importance.max())
sorted_idx = np.argsort(feature_importance)
pos = np.arange(sorted_idx.shape[0]) + .5

sns.set()  #paramètres esthétiques ressemble à ggplot par défaut.
fig, ax = plt.subplots()  #un seul plot
fig.set_figheight(15)
ax.barh(pos, signed_feature_importance[sorted_idx], align='center')
ax.set_yticks(pos)
ax.set_yticklabels(np.array(X.columns)[sorted_idx], fontsize=8)
#fig.suptitle("aaa \n bbb ", fontsize=10)
ax.set(xlabel='Importance Relative des variables\nRégression Logistique C=1 - Univers de concurrence - Importance des variables',
       title="La taille du Body en caractères et en nombre de mots sont les 2 facteurs importants \n toutefois dans un sens différent !")
fig.savefig("QPPS6-logreg1000-Importance-Variables-2goups.png", bbox_inches="tight", dpi=600)
##############################################################

Le fait que la longueur en caractères et en nombre de mots du corps influencent de façon contraire le modèle nous montre ici simplement que la longueur des mots est importante.

Il serait judicieux à l’avenir d’enlever les « stopwords » i.e. les petits mots non significatifs : le, la, les, des, au, mes … de nos variables.

Modèle XGBoost

Calculons XGBoost avec les paramètres par défaut.

#########################################################################
# XGBOOST  
##########################################################################
#xgboost avec parametres standards par défaut

myXGBoost =   XGBClassifier().fit(X_train,y_train)
print("Training set score: {:.3f}".format(myXGBoost.score(X_train,y_train))) 
print("Test set score: {:.3f}".format(myXGBoost.score(X_test,y_test))) 



#pour info : parametres par défaut    
myXGBoost.get_xgb_params()

##########################################################################
# MERCI pour votre attention !
##########################################################################
#on reste dans l'IDE
#if __name__ == '__main__':
#  main()

Les scores donnent les valeurs suivantes :

Training set score: 0.755
Test set score: 0.734

Si le Test Score pour XGBoost est moins bon que celui de KNN (0,734 vs 0.7553) , il est bien meilleur que le meilleur score pour la régression logistique (0,700). Ceci est encourageant.

Dans l’article suivant, nous verrons que nous pourrons améliorer grandement ce score en optimisant les hyper-paramètres d’XGBoost au moyen d’un algorithme génétique.

A bientôt,

Pierre

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.