L'exploration de données, ou data mining, englobe généralement plusieurs étapes, telles que l'acquisition des données, l'analyse exploratoire, l'ingénierie des caractéristiques, l'entraînement du modèle et son évaluation. La bibliothèque Scikit-learn (sklearn) en Python se révèle être un outil exceptionnel pour rationaliser l'ingénierie des caractéristiques et l'entraînement des modèles, offrant une interface élégante et cohérente.
Dans sklearn, les classes dédiées à la transformation des caractéristiques et à l'entraînement des modèles partagent une philosophie de conception commune, centrée autour des méthodes fit et transform (ou predict pour les modèles). La méthode fit_transform, souvent présente, est une optimisation qui combine l'appel à fit puis à transform.
La méthode transform est dédiée à la modification des caractéristiques. Ces transformations peuvent être classées selon l'information qu'elles utilisent :
- Transformations sans information : Elles ne nécessitent aucune donnée supplémentaire pour opérer, comme l'application d'une fonction logarithmique ou exponentielle.
- Transformations avec information : Elles s'appuient sur des statistiques ou des propriétés dérivées des données.
- Non supervisées : Elles utilisent uniquement les statistiques des caractéristiques (moyenne, écart-type, bornes, etc.), comme la standardisation (centrage-réduction) ou la réduction de dimensionnalité via ACP (Analyse en Composantes Principales).
- Supervisées : Elles exploitent à la fois les caractéristiques et la variable cible. Des exemples incluent la sélection de caractéristiques basée sur un modèle ou la réduction de dimensionnalité via ADL (Analyse Discriminante Linéaire).
Le tableau suivant récapitule quelques transformateurs courants dans sklearn et indique si leur méthode fit est activement utilisée pour extraire des inforamtions :
| Module | Classe | Paramètres fit |
Catégorie | fit utile ? |
Description |
|---|---|---|---|---|---|
sklearn.preprocessing |
StandardScaler |
Caractéristiques | Non supervisée | Oui | Standardisation des caractéristiques |
sklearn.preprocessing |
MinMaxScaler |
Caractéristiques | Non supervisée | Oui | Mise à l'échelle dans un intervalle |
sklearn.preprocessing |
Normalizer |
Caractéristiques | Sans information | Non | Normalisation (norme unitaire) |
sklearn.preprocessing |
Binarizer |
Caractéristiques | Sans information | Non | Binarisation des valeurs numériques |
sklearn.preprocessing |
OneHotEncoder |
Caractéristiques | Non supervisée | Oui | Encodage One-Hot des caractéristiques catégorielles |
sklearn.impute |
SimpleImputer |
Caractéristiques | Non supervisée | Oui | Gestion des valeurs manquantes |
sklearn.preprocessing |
PolynomialFeatures |
Caractéristiques | Sans information | Non | Génération de caractéristiques polynomiales |
sklearn.preprocessing |
FunctionTransformer |
Caractéristiques | Sans information | Non | Application d'une fonction personnalisée |
sklearn.feature_selection |
VarianceThreshold |
Caractéristiques | Non supervisée | Oui | Sélection par variance |
sklearn.feature_selection |
SelectKBest |
Caract./Caract.+Cible | Non supervisée/Supervisée | Oui | Sélection des K meilleures caractéristiques |
sklearn.feature_selection |
RFE |
Caract.+Cible | Supervisée | Oui | Élimination récursive de caractéristiques |
sklearn.feature_selection |
SelectFromModel |
Caract.+Cible | Supervisée | Oui | Sélection basée sur l'importance des caractéristiques d'un modèle |
sklearn.decomposition |
PCA |
Caractéristiques | Non supervisée | Oui | Réduction de dimensionnalité par ACP |
sklearn.discriminant_analysis |
LinearDiscriminantAnalysis |
Caract.+Cible | Supervisée | Oui | Réduction de dimensionnalité par ADL |
Il est clair que la méthode fit n'est réellement utile que pour les transformations qui nécessitent d'apprendre des propriétés des données. Son rôle est alors d'extraire des informations pertinentes (comme la moyenne et l'écart-type pour la standardisation) ou d'ajuster des paramètres (comme les poids pour un modèle de sélection de caractéristiques). Cette similitude conceptuelle entre le fit d'un transformateur et le fit d'un modèle d'apprentissage est au cœur de la cohérence de sklearn.
Cette uniformité de l'API permet de combiner ces étapes de manière structurée. Les opérations peuvent être enchaînées séquentiellement (un pipeline où la sortie d'une étape est l'entrée de la suivante) ou en parallèle (où plusieurs opérations reçoivent la même entrée et leurs sorties sont ensuite concaténées). Le module pipeline de sklearn est conçu pour faciliter ces compositions.
Préparation des Données d'Exemple
Pour illustrer ces concepts, nous allons utiliser le célèbre jeu de données Iris, en y ajoutant quelques modifications pour simuler des scénarios d'ingénierie de caractéristiques plus complexes : des valeurs manquantes et une caractéristique catégorielle supplémentaire.
import numpy as np
from sklearn.datasets import load_iris
# Charger le dataset Iris
donnees_iris = load_iris()
X_original = donnees_iris.data
y_original = donnees_iris.target
# Augmenter les données pour l'exemple
# Ajout d'une ligne avec des valeurs manquantes (NaN)
ligne_manquante = np.array([np.nan, np.nan, np.nan, np.nan]).reshape(1, -1)
X_modifie = np.vstack((X_original, ligne_manquante))
# Ajout d'une colonne pour la couleur de la fleur (0, 1, 2)
# La couleur est aléatoire et n'influence pas la classification pour l'exemple
couleurs_aleatoires = np.random.choice([0, 1, 2], size=X_modifie.shape[0]).reshape(-1, 1)
X_modifie = np.hstack((couleurs_aleatoires, X_modifie))
# Ajout d'une cible correspondant à la ligne manquante (ici, la médiane des cibles originales)
y_modifie = np.hstack((y_original, np.array([np.median(y_original)])))
# Mettre à jour les attributs de l'objet iris
donnees_iris.data = X_modifie
donnees_iris.target = y_modifie
print(f"Nouvelles dimensions des caractéristiques (X) : {donnees_iris.data.shape}")
print(f"Nouvelles dimensions de la cible (y) : {donnees_iris.target.shape}")
Techniques Clés
Les piliers d'une utilisation élégante de sklearn pour l'exploration de données sont le traitement parallèle, les pipelines séquentiels, l'optimisation automatique des hyperparamètres et la persistance des modèles. Ces techniques permettent de combiner diverses opérations d'ingénierie de caractéristiques et d'entraînement en un flux de travail unique et cohérent, de réduire la complexité de l'ajustement manuel et de sauvegarder les modèles entraînés pour une réutilisation ultérieure.
Traitement Parallèle des Caractéristiques
Le traitement parallèle permet d'appliquer plusieurs transformations indépendamment sur les mêmes données d'entrée et de fusionner leurs résultats. Selon la manière dont les caractéristiques sont alimentées à chaque transformateur, on peut distinguer le traitement parallèle global ou partiel.
Traitement Parallèle Global
La classe FeatureUnion de sklearn.pipeline permet d'effectuer des traitements parallèles où chaque transformateur reçoit l'intégralité de la matrice de caractéristiques en entrée. Les sorties de tous les transformateurs sont ensuite concaténées horizontalement.
from numpy import log1p
from sklearn.preprocessing import FunctionTransformer, Binarizer
from sklearn.pipeline import FeatureUnion
# Création d'un transformateur pour appliquer la fonction log1p à l'ensemble des caractéristiques
transformation_log = ('log_transform', FunctionTransformer(log1p, validate=False)) # validate=False pour fonctionner avec des NaN potentiels
# Création d'un binariseur pour l'ensemble des caractéristiques
binariseur_global = ('binarizer_global', Binarizer(threshold=0.5))
# Combinaison parallèle des transformateurs
# La méthode fit/transform de FeatureUnion appellera en parallèle les méthodes correspondantes
# de chaque transformateur, puis concaténera leurs sorties.
union_globale = ('union_features_globale', FeatureUnion(transformer_list=[
transformation_log,
binariseur_global
]))
# Exemple d'utilisation (non exécuté ici, juste pour illustration)
# X_transforme = union_globale.fit_transform(donnees_iris.data)
# print(f"Dimensions après union globale : {X_transforme.shape}")
Traitement Parallèle Partiel sur des Colonnes Spécifiques
Le FeatureUnion standard ne permet pas de spécifier des sous-ensembles de colonnes différentes pour chaque transformateur. Dans de nombreux scénarios, nous souhaitons appliquer des transformations spécifiques à certaines colonnes seulement. Pour cela, nous pouvons étendre la classe FeatureUnion pour y intégrer cette fonctionnalité de sélection de colonnes.
import numpy as np
from scipy import sparse
from sklearn.pipeline import FeatureUnion
from joblib import Parallel, delayed # Utilisation de joblib directement
class UnionCaracteristiquesPartielles(FeatureUnion):
"""
Extension de FeatureUnion permettant d'appliquer des transformateurs en parallèle
sur des sous-ensembles spécifiques de colonnes de la matrice de caractéristiques.
"""
def __init__(self, transformateurs, indices_colonnes, n_jobs=None, poids_transformateurs=None):
self.indices_colonnes = indices_colonnes
# La liste de transformateurs est passée au constructeur parent
liste_transformateurs_base = [(nom, obj) for nom, obj in transformateurs]
super().__init__(transformer_list=liste_transformateurs_base, n_jobs=n_jobs,
transformer_weights=poids_transformateurs)
def fit(self, X_data, y_target=None):
"""
Adapte chaque transformateur sur le sous-ensemble de données spécifié par ses indices de colonnes.
"""
toutes_les_operations = [
(nom_op, op_obj, idx_cols)
for (nom_op, op_obj), idx_cols in zip(self.transformer_list, self.indices_colonnes)
]
# Exécuter l'adaptation en parallèle pour chaque transformateur sur ses colonnes dédiées
transformateurs_ajustes = Parallel(n_jobs=self.n_jobs)(
delayed(lambda tr, X_sub, y_t: tr.fit(X_sub, y_t))(transformateur_obj, X_data[:, indices], y_target)
for nom, transformateur_obj, indices in toutes_les_operations
)
# Mettre à jour la liste des transformateurs ajustés dans l'objet parent
self._update_transformer_list(list(zip([n for n, _ in self.transformer_list], transformateurs_ajustes)))
return self
def transform(self, X_data):
"""
Applique la transformation de chaque transformateur sur son sous-ensemble de données.
"""
toutes_les_operations = [
(nom_op, op_obj, idx_cols)
for (nom_op, op_obj), idx_cols in zip(self.transformer_list, self.indices_colonnes)
]
# Exécuter la transformation en parallèle
resultats_transformation = Parallel(n_jobs=self.n_jobs)(
delayed(lambda tr, X_sub: tr.transform(X_sub))(transformateur_obj, X_data[:, indices])
for nom, transformateur_obj, indices in toutes_les_operations
)
# Concaténer les résultats horizontalement. Gère les matrices creuses.
if any(sparse.issparse(f) for f in resultats_transformation):
return sparse.hstack(resultats_transformation).tocsr()
else:
return np.hstack(resultats_transformation)
def fit_transform(self, X_data, y_target=None, **fit_params):
"""
Adapte et transforme simultanément en parallèle.
"""
toutes_les_operations = [
(nom_op, op_obj, idx_cols)
for (nom_op, op_obj), idx_cols in zip(self.transformer_list, self.indices_colonnes)
]
# Exécute fit et transform pour chaque opération en parallèle
resultats_parallel = Parallel(n_jobs=self.n_jobs)(
delayed(lambda tr, X_sub, y_t: (tr.fit(X_sub, y_t), tr.transform(X_sub)))(transformateur_obj, X_data[:, indices], y_target)
for nom, transformateur_obj, indices in toutes_les_operations
)
transformateurs_ajustes, Xs = zip(*resultats_parallel)
self._update_transformer_list(list(zip([n for n, _ in self.transformer_list], transformateurs_ajustes)))
if any(sparse.issparse(f) for f in Xs):
return sparse.hstack(Xs).tocsr()
else:
return np.hstack(Xs)
Dans notre scénario modifié du jeu de données Iris, la première colonne représente la couleur de la fleur (catégorielle), les colonnes 1 à 3 sont des caractéristiques numériques (longueur/largeur sépale/pétale), et la dernière colonne (la 4e dans l'indexation 0-basée) est une caractéristique numérique simple. Nous voulons appliquer des transformations différentes : un encodage One-Hot pour la couleur, une transformation logarithmique pour les caractéristiques continues, et une binarisation pour la dernière caractéristique.
from sklearn.preprocessing import OneHotEncoder, FunctionTransformer, Binarizer
from numpy import log1p
# Transformateur pour la colonne de couleur (index 0)
encodeur_couleur = ('encodeur_couleur', OneHotEncoder(sparse_output=False, handle_unknown='ignore'))
# Transformateur pour les caractéristiques continues (indices 1, 2, 3)
transformateur_log_continu = ('log_caracteristiques', FunctionTransformer(log1p, validate=False))
# Transformateur pour la dernière caractéristique (indice 4)
binariseur_derniere_carac = ('binariseur_caracteristique', Binarizer(threshold=1.5)) # Seuil ajusté pour l'exemple
# Instanciation de l'union partielle avec les transformateurs et leurs colonnes cibles
union_caracteristiques = ('union_caracteristiques', UnionCaracteristiquesPartielles(
transformateurs=[encodeur_couleur, transformateur_log_continu, binariseur_derniere_carac],
indices_colonnes=[[0], [1, 2, 3], [4]] # Indices des colonnes pour chaque transformateur
))
Pipelines Séquentiels
La classe Pipeline de sklearn.pipeline permet d'enchaîner les opérations de manière séquentielle. Chaque étape du pipeline (sauf la dernière) exécute fit_transform, passant sa sortie comme entrée à l'étape suivante. La dernière étape doit implémenter fit (pour l'entraînement) et peut optionnellement implémenter transform (si c'est une transformation finale) ou predict (si c'est un modèle prédictif).
Combinons l'imputation des valeurs manquantes, notre UnionCaracteristiquesPartielles, la mise à l'échelle, la sélection de caractéristiques, la réduction de dimensionnalité et un modèle de régression logistique en un seul pipeline cohérent :
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import MinMaxScaler
from sklearn.feature_selection import SelectKBest, chi2
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
# Étape 1: Gestion des valeurs manquantes (sur l'ensemble des données d'abord)
etape_imputation = ('imputer_nan', SimpleImputer(strategy='median'))
# Étape 2: Traitement parallèle et spécifique des caractéristiques (déjà défini ci-dessus)
# union_caracteristiques = ... (code précédent)
# Étape 3: Mise à l'échelle des caractéristiques résultantes
etape_mise_echelle = ('mise_echelle_minmax', MinMaxScaler())
# Étape 4: Sélection de caractéristiques par test du chi-carré
# Le chi2 nécessite des valeurs non négatives, assurez-vous que les étapes précédentes le garantissent.
etape_selection_carac = ('selection_chi2', SelectKBest(chi2, k=3))
# Étape 5: Réduction de dimensionnalité via PCA
etape_pca = ('reduction_pca', PCA(n_components=2))
# Étape 6: Modèle d'apprentissage final
modele_classification = ('regression_logistique', LogisticRegression(max_iter=1000, solver='liblinear'))
# Construction du pipeline complet
pipeline_final = Pipeline(steps=[
etape_imputation,
union_caracteristiques, # Notre union personnalisée
etape_mise_echelle,
etape_selection_carac,
etape_pca,
modele_classification
])
Optimisation Automatique des Hyperparamètres
L'ajustement des hyperparamètres est une tâche cruciale et souvent fastidieuse. Scikit-learn propose des outils comme GridSearchCV pour automatiser ce processus par recherche sur grille. Cet outil permet d'explorer systématiquement un ensemble prédéfini de combinaisons d'hyperparamètres et de trouver la meilleure configuration pour un modèle ou un pipeline.
from sklearn.model_selection import GridSearchCV
# Définition de la grille de paramètres à explorer pour notre pipeline
# Le format est "nom_etape__nom_sous_etape__parametre"
grille_parametres = {
# Ajustement du seuil du binariseur dans l'union de caractéristiques partielles
'union_caracteristiques__binariseur_caracteristique__threshold': [0.5, 1.0, 2.0],
# Ajustement du paramètre de régularisation C de la régression logistique
'regression_logistique__C': [0.01, 0.1, 1.0, 10.0]
}
# Instanciation de l'objet de recherche sur grille
# Le premier argument est l'estimateur (notre pipeline), le second est la grille de paramètres
recherche_grille = GridSearchCV(pipeline_final, grille_parametres, cv=3, verbose=1, n_jobs=-1)
# Lancer la recherche et l'entraînement
# Cela va entraîner le pipeline pour chaque combinaison d'hyperparamètres et évaluer leurs performances.
recherche_grille.fit(donnees_iris.data, donnees_iris.target)
print(f"Meilleurs hyperparamètres trouvés : {recherche_grille.best_params_}")
print(f"Meilleur score de validation croisée : {recherche_grille.best_score_:.3f}")
# Le meilleur estimateur ajusté est accessible via .best_estimator_
meilleur_pipeline = recherche_grille.best_estimator_
Persistance des Modèles
Après un entraînement long et une optimisation poussée, il est essentiel de pouvoir sauvegarder le modèle entraîné pour une utilisation future sans avoir à le ré-entraîner. La bibliothèque joblib (souvent incluse avec sklearn) offre des fonctions efficaces pour la sérialisation et la désérialisation d'objets Python, y compris les modèles sklearn.
from joblib import dump, load
# Sauvegarde de l'objet de recherche sur grille (qui contient le meilleur pipeline entraîné)
# Le paramètre 'compress' permet de spécifier le niveau de compression (0 pour pas de compression, 3 est un bon compromis)
chemin_fichier_modele = 'modele_pipeline_optimise.joblib'
dump(recherche_grille, chemin_fichier_modele, compress=3)
print(f"Modèle sauvegardé sous : {chemin_fichier_modele}")
# Chargement du modèle depuis le fichier
modele_charge = load(chemin_fichier_modele)
print(f"Modèle chargé. Meilleurs paramètres : {modele_charge.best_params_}")
# Utilisation du modèle chargé pour faire des prédictions
# predictions = modele_charge.predict(donnees_test)
Il est important de noter que la persistance via joblib (qui utilise la sérialisation Pickle) peut rencontrer des limitations avec des objets complexes ou des fonctions définies par lambda, qui peuvent ne pas être sérialisables.
Récapitulatif des Outils Clés
| Module | Classe ou Fonction | Description |
|---|---|---|
sklearn.pipeline |
Pipeline |
Construction de chaînes de transformateurs et de modèles |
sklearn.pipeline |
FeatureUnion |
Combinaison parallèle de transformateurs |
sklearn.model_selection |
GridSearchCV |
Recherche sur grille pour l'optimisation des hyperparamètres |
joblib |
dump |
Persistance des objets Python vers un fichier |
joblib |
load |
Chargement des objets Python depuis un fichier |