Module 5 — Spark ML : transformateurs et estimateurs
Nous avons de quoi lire, filtrer, agréger. Il reste à préparer les
données pour un algorithme. Spark ML, la bibliothèque d'apprentissage
automatique moderne de Spark (aussi appelée MLlib DataFrame), le fait
avec deux abstractions simples et deux règles à retenir : la colonne
features doit être un vecteur unique, et les statistiques
d'apprentissage doivent venir uniquement de l'ensemble d'entraînement.
Deux briques, deux contrats
Un transformateur (Transformer) prend un DataFrame et renvoie un
DataFrame enrichi de nouvelles colonnes, sans avoir besoin d'apprendre
quoi que ce soit sur les données. Sa méthode s'appelle transform. Un
VectorAssembler qui empile des colonnes numériques dans un vecteur est
un transformateur ; un SQLTransformer qui applique du SQL en est un
autre.
Un estimateur (Estimator) doit apprendre sur les données avant de
pouvoir transformer. Sa méthode s'appelle fit et retourne un
modèle — qui est, par construction, un transformateur. Un
StringIndexer qui apprend le vocabulaire d'une colonne catégorielle est
un estimateur ; sa version ajustée StringIndexerModel est un
transformateur. Toutes les classes d'algorithmes (LogisticRegression,
RandomForestClassifier…) sont des estimateurs.
from pyspark.ml.feature import StringIndexer
indexeur = StringIndexer(inputCol="compagnie", outputCol="compagnie_idx")
modele_index = indexeur.fit(vols_train) # estimateur -> modèle
vols_train_idx = modele_index.transform(vols_train) # transformateur
vols_test_idx = modele_index.transform(vols_test) # même vocabulaire
La règle qui découle : fit uniquement sur train, transform sur
train et test. Cette discipline évite la fuite de données que le
module 6 formalisera dans un Pipeline.
Assembler un vecteur de variables
Tous les algorithmes de Spark ML attendent une seule colonne d'entrée,
appelée par convention features, contenant un Vector par ligne. On
l'obtient avec un VectorAssembler :
from pyspark.ml.feature import VectorAssembler
assembleur = VectorAssembler(
inputCols=["distance_km", "heure_depart", "compagnie_idx", "jour_semaine"],
outputCol="features",
handleInvalid="skip", # ou "keep" (vecteur creux) ou "error"
)
vols_prets = assembleur.transform(vols_train_idx)
vols_prets.select("features", "retard_arrivee").show(3, truncate=False)
Trois précautions à intégrer dès le premier assemblage. Types :
toutes les colonnes d'entrée doivent être numériques ; passer une
StringType fait échouer l'étape avec un message parfois obscur.
Nullité : handleInvalid doit être choisi explicitement ("skip"
supprime les lignes contenant null, "keep" produit un vecteur creux
avec un marqueur, "error" refuse). Ordre : la méthode .toArray()
d'un Vector renvoie les valeurs dans l'ordre de inputCols ; c'est cet
ordre qui pilotera aussi l'interprétation des coefficients d'un modèle
linéaire.
Encoder les variables catégorielles
Une variable catégorielle passe par deux étapes. D'abord StringIndexer
convertit les catégories en entiers 0, 1, 2, … par fréquence
décroissante. Ensuite OneHotEncoder transforme ces entiers en vecteurs
creux — sauf pour un modèle d'arbre, qui accepte l'index tel quel :
from pyspark.ml.feature import StringIndexer, OneHotEncoder
indexeur = StringIndexer(inputCol="origine", outputCol="origine_idx", handleInvalid="keep")
encodeur = OneHotEncoder(inputCol="origine_idx", outputCol="origine_ohe")
handleInvalid="keep" protège des catégories nouvelles vues à
l'inférence — c'est très fréquent en production (un nouvel aéroport, un
nouveau code compagnie). Sans ce paramètre, la première catégorie
inconnue plante l'ensemble du travail.
Mettre à l'échelle les variables numériques
Les modèles linéaires et le k-moyennes sont sensibles à l'échelle des
variables. Un StandardScaler retire la moyenne et divise par
l'écart-type, appris sur train :
from pyspark.ml.feature import StandardScaler
echelle = StandardScaler(inputCol="features", outputCol="features_std",
withMean=True, withStd=True)
modele_echelle = echelle.fit(vols_prets)
vols_prets_std = modele_echelle.transform(vols_prets)
Notez que withMean=True densifie les vecteurs (soustraire la moyenne à
un vecteur creux le rend dense) : sur des données très creuses, préférer
withMean=False pour préserver la structure creuse.
Ce qui existe et ce qui manque
Le paquet pyspark.ml.feature couvre :
Tokenizer et HashingTF/IDF (texte simple), Bucketizer (découpage
en tranches), QuantileDiscretizer, PCA, Imputer (moyenne, médiane
ou mode), StopWordsRemover, NGram, Word2Vec.
Ce qui n'existe pas et surprend : pas d'encodage cible (target
encoding), pas d'encodage de fréquence prêt à l'emploi, pas de
ColumnTransformer équivalent à celui de scikit-learn. Ce qui manque
s'écrit à la main en SQL ou en withColumn, et se pose ensuite dans le
pipeline sous forme de SQLTransformer.
Chaque Transformer conserve son schéma d'entrée : réutiliser un
VectorAssembler ajusté à ["a", "b", "c"] sur un DataFrame qui ne
contient plus c lève une erreur peu explicite. En production, cette
mismatch se traduit par un modèle qui échoue au premier predict sur
une nouvelle base.
En résumé
- Un transformateur transforme sans apprendre ; un estimateur
apprend d'abord (
fit), puis fournit un modèle qui est un transformateur. - L'entrée d'un algorithme Spark ML est une seule colonne
vectorielle produite par
VectorAssembler. - Variables catégorielles :
StringIndexerpuisOneHotEncoderpour les modèles linéaires,StringIndexerseul pour les arbres. - Toujours faire
fitsurtrainettransformsurtrainettest; le module suivant automatise cette discipline avecPipeline.
Module suivant : chaîner ces briques dans un Pipeline et valider avec
un CrossValidator distribué, sans introduire de fuite.