W3docs

La validation croisée en Python : K-Fold, Stratifiée, LOOCV

Apprenez la validation croisée en Python : K-Fold, K-Fold stratifié, Leave-One-Out, validation imbriquée et pipelines avec scikit-learn — avec des exemples exécutables.

La validation croisée est la méthode standard pour estimer les performances d'un modèle d'apprentissage automatique sur des données inédites. Plutôt que de se fier à un seul découpage entraînement/test — qui peut produire un score trop optimiste ou trop pessimiste selon les échantillons qui se retrouvent dans chaque partie — la validation croisée entraîne et évalue le modèle plusieurs fois sur différentes partitions des données, puis en fait la moyenne.

Cette page couvre :

  • Pourquoi un seul découpage entraînement/test ne suffit pas
  • La validation croisée K-Fold et comment choisir k
  • Le K-Fold stratifié pour les distributions de classes déséquilibrées
  • La validation croisée Leave-One-Out (LOO) pour les petits ensembles de données
  • L'évaluation de plusieurs métriques avec cross_validate
  • L'utilisation d'un Pipeline dans la validation croisée pour éviter les fuites de données
  • La validation croisée imbriquée pour un réglage non biaisé des hyperparamètres

Tous les exemples utilisent scikit-learn et le jeu de données Iris intégré, vous pouvez donc les exécuter immédiatement sans rien télécharger.

Pourquoi la validation croisée est importante

Un workflow d'évaluation naïf divise les données une seule fois, entraîne sur une partie et teste sur l'autre. Le score obtenu dépend fortement des échantillons qui se retrouvent dans chaque partie — un découpage chanceux peut faire paraître un modèle faible comme bon ; un mauvais découpage peut faire paraître un modèle solide comme mauvais.

La validation croisée résout ce problème en répétant le processus entraînement/test k fois, en utilisant à chaque fois une portion différente des données comme ensemble de test. Le score final est la moyenne sur tous les plis, ce qui est bien plus stable qu'une seule mesure.

La validation croisée tire également le meilleur parti de données limitées : chaque échantillon est utilisé à la fois pour l'entraînement et l'évaluation sur l'ensemble de l'expérience.

Consultez la page découpage entraînement/test pour la technique de référence plus simple que la validation croisée améliore.

Validation croisée K-Fold

Le K-Fold est la stratégie de validation croisée la plus utilisée. Les données sont divisées en k plis de taille égale. À chacune des k itérations :

  1. Un pli est réservé comme ensemble de test.
  2. Les k - 1 plis restants forment l'ensemble d'entraînement.
  3. Le modèle est entraîné de zéro et évalué sur le pli de test.

Après k itérations, vous disposez de k scores. Leur moyenne est l'estimation des performances par validation croisée ; leur écart type indique la cohérence de ces performances sur différentes tranches de données.

Pour k = 5 et un jeu de données de 150 échantillons, chaque pli contient 30 échantillons (20 %) pour le test et 120 échantillons (80 %) pour l'entraînement.

Exemple de base K-Fold

from sklearn.model_selection import KFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris

# Load the built-in Iris dataset (150 samples, 4 features, 3 classes)
iris = load_iris()
X, y = iris.data, iris.target

model = LogisticRegression(max_iter=200)
kfold = KFold(n_splits=5, shuffle=True, random_state=42)

scores = cross_val_score(model, X, y, cv=kfold, scoring='accuracy')

print("Fold scores:", scores.round(4))
# Fold scores: [1.     1.     0.9333 0.9667 0.9667]

print("Mean accuracy: %.4f  Std: %.4f" % (scores.mean(), scores.std()))
# Mean accuracy: 0.9733  Std: 0.0249

La fonction cross_val_score gère toute l'itération en interne. Paramètres clés :

ParamètreRôle
estimatorN'importe quel modèle scikit-learn (ou Pipeline)
X, yMatrice de features et vecteur cible
cvObjet de validation croisée ou entier (ex. cv=5)
scoringChaîne de métrique — 'accuracy', 'f1_macro', 'roc_auc', etc.

Choisir k

  • k = 5 ou k = 10 est recommandé pour la plupart des jeux de données. Ces valeurs offrent un bon compromis biais-variance dans l'estimation.
  • Un k plus grand (ex. 10) produit un biais plus faible mais une variance plus élevée dans l'estimation, et est plus coûteux à calculer.
  • Un k plus petit (ex. 3) est plus rapide mais l'estimation est plus sensible à la façon dont les données ont été divisées.
  • Pour les très petits jeux de données (moins de ~100 échantillons), envisagez plutôt le Leave-One-Out.

Inspecter les plis manuellement

Vous pouvez itérer sur les plis vous-même lorsque vous avez besoin d'inspecter ce qui entre dans chaque découpage ou lorsque vous souhaitez effectuer une logique personnalisée par pli :

from sklearn.model_selection import KFold
from sklearn.datasets import load_iris
import numpy as np

iris = load_iris()
X, y = iris.data, iris.target

kfold = KFold(n_splits=5, shuffle=True, random_state=42)

for fold, (train_idx, test_idx) in enumerate(kfold.split(X), start=1):
    X_train, X_test = X[train_idx], X[test_idx]
    y_train, y_test = y[train_idx], y[test_idx]
    print(f"Fold {fold}: train={len(train_idx)} samples, test={len(test_idx)} samples")

Sortie :

Fold 1: train=120 samples, test=30 samples
Fold 2: train=120 samples, test=30 samples
Fold 3: train=120 samples, test=30 samples
Fold 4: train=120 samples, test=30 samples
Fold 5: train=120 samples, test=30 samples

Validation croisée K-Fold stratifiée

Le K-Fold ordinaire divise les données par ordre d'index. Avec des distributions de classes déséquilibrées, certains plis peuvent contenir très peu d'exemples d'une classe minoritaire, rendant le score peu fiable.

Le K-Fold stratifié garantit que chaque pli contient approximativement la même proportion de chaque classe que l'ensemble du jeu de données. Utilisez StratifiedKFold chaque fois que votre cible est catégorielle :

from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris

iris = load_iris()
X, y = iris.data, iris.target

model = LogisticRegression(max_iter=200)
skfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

scores = cross_val_score(model, X, y, cv=skfold, scoring='accuracy')

print("Fold scores:", scores.round(4))
# Fold scores: [1.     0.9667 0.9333 1.     0.9333]

print("Mean accuracy: %.4f  Std: %.4f" % (scores.mean(), scores.std()))
# Mean accuracy: 0.9667  Std: 0.0298

StratifiedKFold est le validateur croisé par défaut utilisé dans GridSearchCV et RandomizedSearchCV pour les problèmes de classification — vous bénéficiez de la stratification automatiquement dans ces contextes.

Validation croisée Leave-One-Out

La validation croisée Leave-One-Out (LOO) est le cas extrême : k est égal au nombre d'échantillons. À chaque itération, un échantillon constitue l'ensemble de test et tous les échantillons restants forment l'ensemble d'entraînement. Pour un jeu de données de 150 échantillons, cela représente 150 cycles d'entraînement/évaluation.

from sklearn.model_selection import LeaveOneOut, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris

iris = load_iris()
X, y = iris.data, iris.target

model = LogisticRegression(max_iter=200)
loocv = LeaveOneOut()

scores = cross_val_score(model, X, y, cv=loocv, scoring='accuracy')

print(f"Number of folds: {len(scores)}")        # 150
print(f"Mean accuracy: {scores.mean():.4f}")     # 0.9667
print(f"Std deviation: {scores.std():.4f}")      # 0.1795

Quand utiliser LOO :

  • Votre jeu de données contient moins de ~100 échantillons et vous ne pouvez pas vous permettre de réserver des données pour le test.
  • Vous souhaitez l'estimation avec le biais le plus faible possible des performances du modèle.

Inconvénients du LOO :

  • Coût de calcul très élevé — le modèle est ré-entraîné n fois.
  • Variance élevée dans l'estimation : le score de test de chaque pli est 0 ou 1 (classification binaire) ou un seul point, donc l'écart type n'est pas significatif pour les plis individuels.

Pour la plupart des jeux de données, le K-Fold avec k=5 ou k=10 offre un meilleur compromis.

Évaluer plusieurs métriques à la fois

cross_val_score ne peut calculer qu'une seule métrique par appel. Utilisez cross_validate pour calculer plusieurs métriques simultanément et récupérer également les scores d'entraînement pour détecter le surapprentissage :

from sklearn.model_selection import KFold, cross_validate
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
import numpy as np

iris = load_iris()
X, y = iris.data, iris.target

model = LogisticRegression(max_iter=200)
kfold = KFold(n_splits=5, shuffle=True, random_state=42)

cv_results = cross_validate(
    model, X, y,
    cv=kfold,
    scoring=['accuracy', 'f1_macro'],
    return_train_score=True,
)

print("Test accuracy: ", cv_results['test_accuracy'].round(4))
# Test accuracy:  [1.     1.     0.9333 0.9667 0.9667]

print("Train accuracy:", cv_results['train_accuracy'].round(4))
# Train accuracy: [0.975  0.9583 0.9833 0.975  0.9833]

print("Test F1-macro: ", cv_results['test_f1_macro'].round(4))
# Test F1-macro:  [1.     1.     0.9259 0.9691 0.971 ]

Comparer les scores d'entraînement et de test sur les plis est un moyen rapide de détecter le surapprentissage : si la précision d'entraînement est systématiquement bien supérieure à la précision de test, le modèle mémorise les données d'entraînement. Consultez la discussion sur le biais et la variance pour plus de contexte.

Utiliser un Pipeline dans la validation croisée

Une erreur courante consiste à adapter les étapes de prétraitement (comme la mise à l'échelle des features ou l'imputation) sur l'ensemble du jeu de données avant la validation croisée. Cela fait fuiter des informations du pli de test dans le processus d'entraînement, conduisant à un score trop optimiste.

Le bon modèle consiste à encapsuler le prétraitement et le modèle ensemble dans un Pipeline et à passer le pipeline à cross_val_score. scikit-learn ré-adapte l'ensemble du pipeline — scaler inclus — indépendamment à l'intérieur de chaque pli :

from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.datasets import load_iris

iris = load_iris()
X, y = iris.data, iris.target

# Correct: preprocessing is fitted only on training folds
pipe = Pipeline([
    ('scaler', StandardScaler()),
    ('clf', LogisticRegression(max_iter=200)),
])

skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(pipe, X, y, cv=skf, scoring='accuracy')

print("Fold scores:", scores.round(4))
# Fold scores: [1.     0.9667 0.9    1.     0.9   ]

print("Mean accuracy: %.4f  Std: %.4f" % (scores.mean(), scores.std()))
# Mean accuracy: 0.9533  Std: 0.0452

Utilisez toujours un Pipeline lorsque votre workflow comprend une étape qui apprend à partir des données (mise à l'échelle, PCA, encodage, imputation).

Validation croisée imbriquée

Lorsque vous utilisez la validation croisée à la fois pour régler les hyperparamètres et évaluer les performances du modèle sur les mêmes données, vous risquez de surajuster les plis de validation — les hyperparamètres choisis sont ceux qui ont le mieux performé sur ces partitions particulières, donc le score rapporté est optimiste.

La validation croisée imbriquée sépare les deux préoccupations :

  • Boucle interne : sélectionner les hyperparamètres via une recherche sur grille sur les plis d'entraînement.
  • Boucle externe : évaluer le meilleur modèle trouvé par la boucle interne sur un pli de test réservé.
from sklearn.model_selection import GridSearchCV, StratifiedKFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris

iris = load_iris()
X, y = iris.data, iris.target

# Inner CV: hyperparameter selection
inner_cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=2)
param_grid = {'C': [0.01, 0.1, 1, 10]}
gs = GridSearchCV(
    LogisticRegression(max_iter=300),
    param_grid,
    cv=inner_cv,
    scoring='accuracy',
)

# Outer CV: unbiased performance estimation
outer_cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=1)
nested_scores = cross_val_score(gs, X, y, cv=outer_cv, scoring='accuracy')

print("Nested CV fold scores:", nested_scores.round(4))
# Nested CV fold scores: [0.9667 1.     0.9333 1.     0.9   ]

print("Mean accuracy: %.4f" % nested_scores.mean())
# Mean accuracy: 0.9600

L'approche imbriquée fournit une estimation non biaisée des performances du modèle final déployé. Utilisez-la chaque fois que vous rapportez des résultats dans un contexte de recherche ou que vous comparez des algorithmes. Pour un workflow de déploiement simple où vous ré-entraînerez de toute façon sur toutes les données disponibles, une seule boucle externe avec GridSearchCV est généralement suffisante. Consultez la page recherche sur grille pour une présentation détaillée du réglage des hyperparamètres.

Pièges courants

Prétraitement en dehors du pli

Adapter un scaler sur l'ensemble du jeu de données avant d'appeler cross_val_score — plutôt qu'à l'intérieur d'un Pipeline — fait fuiter les statistiques du pli de test dans l'entraînement. La solution est toujours d'utiliser un Pipeline.

Utilisation incorrecte de random_state

Si vous définissez shuffle=True sans random_state, chaque exécution produit un découpage différent et vos résultats ne sont pas reproductibles. Définissez toujours random_state sur un entier fixe lorsque vous rapportez des chiffres.

Interprétation de l'écart type

Un écart type élevé entre les plis n'est pas toujours mauvais — il peut refléter une variabilité réelle dans le jeu de données (ex. certains plis sont plus faciles que d'autres). Examinez les scores individuels des plis avant de tirer des conclusions.

Validation croisée sur des données de séries temporelles

Le K-Fold mélange les données de façon aléatoire, ce qui mélange les informations futures dans les fenêtres d'entraînement passées pour les problèmes de séries temporelles. Utilisez plutôt TimeSeriesSplit de scikit-learn, qui respecte l'ordre temporel.

Référence rapide

TechniqueQuand l'utiliserClasse scikit-learn
K-FoldChoix par défaut pour la plupart des tâches de régression/classificationKFold
K-Fold stratifiéClassification avec des classes déséquilibréesStratifiedKFold
Leave-One-OutTrès petits jeux de données (< ~100 échantillons)LeaveOneOut
CV imbriquéeRapporter des scores non biaisés avec réglage des hyperparamètresGridSearchCV dans cross_val_score
CV sur séries temporellesDonnées avec un ordre temporelTimeSeriesSplit

Sujets connexes

Was this page helpful?