Travaux pratiques - Sélection de variables

(correspond à 1 séance de TP)

Références externes utiles :

L’objectif de cette séance de TP est de présenter l’utilisation de quelques méthodes de sélection de variables mises en œuvre dans Scikit-learn, suivant l’approche de filtrage et l’approche wrapper.

La sélection de variables dans Scikit-learn

Scikit-learn v1.3.2 propose plusieurs méthodes de sélection de variables (feature selection) suivant les trois approches mentionnées dans le cours (filtrage, wrapper et intégrée). Nous nous intéressons ici aux approches filtrage et wrapper décrites dans la documentation.

La suppression de variables à faible variance (removing features with low variance) consiste à éliminer les variables dont la variance est inférieure à un seuil (par défaut la valeur du seuil est 0, sont donc éliminées les variables constantes). Comme nous l’avons constaté lors de l’étude de l’ACP, ce n’est pas parce que la variance d’une variable est faible qu’elle est nécessairement inutile (trop peu « explicative » de la variable de sortie).

La sélection univariée (univariate feature selection) cherche à déterminer (à travers des tests statistiques) dans quelle mesure chaque variable d’entrée « explique » la variable de sortie ; les variables les moins explicatives individuellement sont éliminées. Les deux méthodes suivent l’approche de filtrage : la sélection est réalisée en amont de la construction d’un modèle décisionnel.

La sélection séquentielle (sequential feature selection) suit clairement l’approche wrapper : un modèle décisionnel doit être développé sur chaque (sous-)ensemble candidat de variables d’entrée et c’est la performance du modèle qui caractérise le (sous-)ensemble de variable. Deux versions sont proposées, une incrémentale (Forward-SFS) et une décrémentale (Backward-SFS).

L’élimination récursive de variables et la sélection avec SelectFromModel comptent sur un modèle décisionnel appris en utilisant la totalité des variables d’entrée pour avoir des indications sur la pertinence de chaque variable d’entrée ; tous les modèles décisionnels ne fournissent pas de telles informations. Ces méthodes peuvent être considérées comme suivant plutôt l’approche wrapper car (au moins) un modèle décisionnel doit être développé pour permettre la sélection, en revanche la sélection est ensuite réalisée sur la base des coefficients de « pertinence » assignés (par le modèle décisionnel) aux variables individuelles. Le coût de ces deux méthodes est ainsi inférieur au coût des méthodes wrapper génériques, qui doivent faire apprendre un modèle décisionnel pour toute combinaison candidate de variables.

Les données utilisées

Pour examiner quelque sméthodes de sélection de variables mises en œ dans Scikit-learn nous nous servirons des données Textures (5500 observations qui sont des pixels extraits d’images de textures et décrits par 40 variables qui sont des descripteurs visuels locaux ; chaque observation appartient à une des 11 classes de textures. Pour cela, sous linux vous pouvez entrer (ou alors vous pouvez télécharger les données en suivant ce lien) :

!wget -nc http://cedric.cnam.fr/~crucianm/src/texture.dat

Pour illustrer le propos, après la lecture des données, nous ajoutons quatre variables d’entrée qui représentent du bruit suivant une loi normale (pour les deux premières) et une loi uniforme (pour les deux dernières) et nous limitons les observations à 110 (au lieu de 5500), choisies aléatoirement.

import numpy as np
textures = np.loadtxt('texture.dat')
print(textures.shape)
from sklearn.utils import shuffle
# génération 2 variables uniformes et 2 variables normales
np.random.seed(1)
n_samples = 150
varUniformes = np.random.uniform(0, 1, size=(n_samples, 2))
varNormales = np.random.randn(n_samples, 2)
# introduction des nouvelles variables dans le tableau de données, devant les autres
donnees = np.hstack((varUniformes, varNormales, shuffle(textures, random_state=1, n_samples=n_samples)))
print(donnees.shape)

Rappelons que la dernière colonne du tableau textures contient les étiquettes de classe.

Pour référence, appliquons l’étape décisionnelle de l’analyse discriminante (comme modèle décisionnel) sur les données initiales et ensuite sur les données auxquelles les nouvelles variables aléatoires ont été ajoutées :

from sklearn.model_selection import train_test_split
train, test = train_test_split(donnees, test_size = 0.5, random_state=1)
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
# sur données initiales
lda = LinearDiscriminantAnalysis()
lda.fit(train[:,4:44],train[:,44])
lda.score(test[:,4:44],test[:,44])
#0.7272727272727273
# sur données avec variables ajoutées
lda.fit(train[:,:44],train[:,44])
lda.score(test[:,:44],test[:,44])
#0.6909090909090909

Il est facile de constater que la présence des quatre nouvelles variables, de valeurs aléatoires, a un impact négatif sur la capacité de généralisation du modèle décisionnel construit.

Application de la sélection par suppression de variables à faible variance

La première méthode de sélection est la plus simple, suppression de variables à faible variance. Pour définir le seuil, appliquons d’abord la méthode avec un seuil par défaut qui est égale à 0 (seules seront éliminées les variables constantes) :

from sklearn.feature_selection import VarianceThreshold
selVarianceThreshold = VarianceThreshold()
Xtrain_removed0variance = selVarianceThreshold.fit_transform(train[:,:44])
# affichage des variances des différentes variables
selVarianceThreshold.variances_
# affichage des variances triées en ordre croissant pour définir le seuil
np.sort(selVarianceThreshold.variances_)
# avec un seuil choisi pour éliminer la moitié des variables
selVarianceThreshold2 = VarianceThreshold(threshold=0.04384)
Xtrain_removedHalf = selVarianceThreshold2.fit_transform(train[:,:44])
Xtrain_removedHalf.shape
#(55, 22)
# quelles variables ont été conservées ?
print(selVarianceThreshold2.get_support())

Question :

Les quatre variables à valeurs aléatoires ont-elles été éliminées ?

Correction :

Les quatre variables sont les quatre premières dans la liste et les valeurs True dans la réponse de selVarianceThreshold2.get_support() montrent que ces variables n’ont pas été éliminées (sont donc sélectionnées).

Examinons l’effet sur les performances du modèle décisionnel développé :

lda = LinearDiscriminantAnalysis()
lda.fit(Xtrain_removedHalf,train[:,44])
lda.score(selVarianceThreshold2.transform(test[:,:44]),test[:,44])
#0.8363636363636363

Les variables aléatoires introduites au départ ne sont pas éliminées par cette méthode car leurs variances sont parmi les plus élevées (0.08331242, 0.07685248, 0.6509644, 1.03908011). Malgré cela, on observe une augmentation des performances de généralisation (y compris par rapport au cas sans variables aléatoires) qui est probablement due simplement à une réduction de la complexité du modèle (le nombre de variables d’entrée est une des composantes de la complexité d’un modèle).

Application de la sélection univariée

La deuxième méthode de sélection employée ici est la sélection univariée sur la base de information mutuelle pour la classification. Cette méthode élimine les variables pour lesquelles les valeurs de l’information mutuelle avec la variable de sortie sont les plus faibles (c’est à dire, qui « expliquent » le moins bien la variable de sortie). Nous avons l’intention de garder la moitié des variables et utilisons donc la fonction SelectKBest (d’autres sont disponibles, voir la documentation).

from sklearn.feature_selection import SelectKBest, mutual_info_classif
selKBestMutualInfo = SelectKBest(score_func=mutual_info_classif, k=22)
Xtrain_removedHalf = selKBestMutualInfo.fit_transform(train[:,:44], train[:,44])
Xtrain_removedHalf.shape
#(55, 22)
# quelles variables ont été conservées (celles avec True) ?
print(selKBestMutualInfo.get_support())

Les quatre premières variables, dont les valeurs étaient aléatoires, ont bien été éliminées (non sélectionnées) par cette méthode.

Question :

Examinez l’effet sur les performances du modèle décisionnel développé. Comparez aux résultats obtenus par la méthode précédente.

Correction :

lda = LinearDiscriminantAnalysis()
lda.fit(Xtrain_removedHalf,train[:,44])
lda.score(selKBestMutualInfo.transform(test[:,:44]),test[:,44])
#0.8

Les performances de généralisation sont améliorées par rapport à l’utilisation de toutes les variables initiales (sans variables aléatoires). Le fait que la performance soit un peu moins bonne que pour la méthode précédente (sélection par suppression de variables à faible variance) est probablement une conséquence du choix de l’échantillon et du faible nombre d’observations.

Application de la sélection séquentielle

La troisième méthode choisie est la méthode de sélection séquentielle (Sequential Feature Selection) qui fait partie de l’approche wrapper. Le modèle décisionnel employé est l’analyse discriminante linéaire.

from sklearn.feature_selection import SequentialFeatureSelector
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
lda = LinearDiscriminantAnalysis()
selSFS = SequentialFeatureSelector(lda, cv=3, n_features_to_select=22)
Xtrain_removedHalf = selSFS.fit_transform(train[:,:44], train[:,44])
Xtrain_removedHalf.shape
#(55, 22)
# quelles variables ont été conservées (celles avec True) ?
print(selSFS.get_support())

Question :

Quelle version a été employée, la version incrémentale ou la version décrémentale ? Les variables dont les valeurs sont aléatoires ont-elles été toutes éliminées (non sélectionnées) par cette approche ?

Correction :

L’argument direction de SequentialFeatureSelector(...) n’a pas été précisé, sa valeur par défaut est 'forward' d’après la documentation, c’est donc la version incrémentale qui a été employée.

Question :

Examinez l’effet sur les performances du modèle décisionnel développé. Comparez aux résultats obtenus par la méthode précédente.

Correction :

lda = LinearDiscriminantAnalysis()
lda.fit(Xtrain_removedHalf,train[:,44])
lda.score(selSFS.transform(test[:,:44]),test[:,44])
#0.9454545454545454

Question :

Appliquez l’autre variante de l’algorithme et comparez les résultats.

Correction :

selSFSb = SequentialFeatureSelector(lda, direction='backward', cv=3, n_features_to_select=22)
Xtrain_removedHalf = selSFSb.fit_transform(train[:,:44], train[:,44])
Xtrain_removedHalf.shape
#(55, 22)
# quelles variables ont été conservées (celles avec True) ?
print(selSFSb.get_support())

L’effet sur les performances du modèle décisionnel développé :

lda = LinearDiscriminantAnalysis()
lda.fit(Xtrain_removedHalf,train[:,44])
lda.score(selSFSb.transform(test[:,:44]),test[:,44])
#0.9272727272727272