• 10 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 20/11/2024

Mettez en place une séparation apprentissage-test cohérente

Plusieurs Data Scientists/ML Engineers débutants se précipitent vers une séparation Train-Test (ou Train -Test Split en anglais) basique, sans ajustements, pour évaluer leur approche supervisée et conclure qu’ils ont trouvé le modèle le plus performant possible.

Toutefois, les projets Data en entreprise où l’on peut se contenter d’une séparation aussi simple relèvent plus de l’exception que de la norme. Nous allons découvrir ensemble comment procéder d’une manière plus structurée. ;)

Connaissez-vous la validation croisée ?

Dans le cours d’initiation, vous avez découvert, en plus de la séparation Train-Test classique, la validation croisée comme méthode pour détecter l’overfit et mesurer la capacité de votre modèle supervisé à s’adapter à des données qu’il n’a jamais vues auparavant.

Nous allons faire un bref rappel concernant cette méthode, puisqu’elle représente la base sur laquelle sont construites les méthodes qui suivent.

En effet, si on prend n’importe quel jeu de données et que nous réalisons une séparation apprentissage-test classique (par exemple avec 80% de la donnée en apprentissage et 20% en test) vous pouvez par pur hasard vous retrouver avec un jeu de test “facile”, où le modèle montrera d'excellentes performances. Alors qu’en réalité, prendre un autre 20% aléatoires de la donnée en Test, qui ressemble peu au jeu d'entraînement, pourrait montrer des résultats désastreux !

La validation croisée vient combler ce manque en découpant le jeu de données en K morceaux (appelés conventionnellement, folds). Ensuite, nous allons tester notre modèle K fois (autant de fois que de folds), en prenant pour chaque itération un fold comme jeu de test, et tous les autres folds comme jeu de Train. Le schéma du cours d’initiation résume avec brio ce procédé :

Tableau montrant un processus de validation croisée en cinq itérations. Chaque itération comporte des blocs

Pour évaluer les performances du modèle lors de la validation croisée, nous procédons comme suit :

  • On calcule la moyenne de la/des métrique(s) de performance de votre choix, pour avoir une première idée de la capacité de généralisation de votre modèle ;

  • On calcule l’écart-type de la/des métrique(s) de performance de votre choix. C’est peut-être le calcul le plus important de la validation croisée, car elle mesure réellement à quel point les différents jeux de tests sont différents les uns des autres et à quel point votre modèle est instable en conséquence ;

  • Si vous choisissez un nombre de Fold très élevé (plus de 20, si la taille de vos données vous le permet), vous pouvez même tracer la distribution de la/des métrique(s) de performance pour voir à quel point elles sont hétérogènes à travers les folds.

Comment réalisons-nous tout cela en Python ? Eh bien je vous conseille la fonction cross_validate de Scikit Learn (notre sauveur comme toujours ;) ) car elle est très personnalisable par rapport aux autres alternatives comme :

  • La fonction GridSearchCV, mentionnée dans le cours d’initiation, qui réalise simultanément une validation croisée ET une optimisation des hyperparamètres du modèle. Cette dernière opération est très coûteuse et nous n'en avons pas toujours besoin. Nous expliquerons pourquoi dans le détail lors de la partie suivante du cours.

  • La fonction cross_val_score, qui ne peut renvoyer les résultats en validation croisée que d’une seule métrique à la fois ! Alors qu’il est très utile d’en regarder plusieurs pour une interprétation plus complète.

Ci-dessous, vous trouverez le code d’une fonction qui implémente la fonction cross_validate et qui calcule les moyennes et écart-types des métriques choisies :

def perform_cross_validation(
    X: pl.DataFrame,
    y: pl.Series,
    model,
    cross_val_type, # La variante de validation croisée que nous souhaitons utiliser
    scoring_metrics: tuple, # Metriques de notre choix
    return_estimator=False, # Si nous souhaitons stocker les modèles de chaque fold
    groups=None, # Nous verrons l’utilité de cet argument juste après 
):
    scores = cross_validate(
        model,
        X.to_numpy(),
        y.to_numpy(),
        cv=cross_val_type,
        return_train_score=True,
        return_estimator=return_estimator,
        scoring=scoring_metrics,
        groups=groups,
    )

    for metric in scoring_metrics:
        print(
            "Average Train {metric} : {metric_value}".format(
                metric=metric,
                metric_value=np.mean(scores["train_" + metric]),
            )
        )
        print(
            "Train {metric} Standard Deviation : {metric_value}".format(
                metric=metric, metric_value=np.std(scores["train_" + metric])
            )
        )

        print(
            "Average Test {metric} : {metric_value}".format(
                metric=metric, metric_value=np.mean(scores["test_" + metric])
            )
        )
        print(
            "Test {metric} Standard Deviation : {metric_value}".format(
                metric=metric, metric_value=np.std(scores["test_" + metric])
            )
        )

    return scores

Tout ceci est très bien ! Mais, la validation croisée ne suffit pas toujours. En effet, la suite de ce chapitre va illustrer quelques exemples de limitations et de techniques pour les contourner.

Exemple 1 : Stratifier votre cible est crucial en classification

Parler de stratification n’aurait pas beaucoup d’intérêt si on ne définit pas d’abord ce que l’on appelle le déséquilibre des classes en classification. On fait face à des classes déséquilibrées, quand on a une des classes du jeu de données qui a significativement plus d’observations que les autres.

On peut facilement vérifier la présence de déséquilibre ou non, en prenant notre colonne target y et en utilisant la fonction value_counts() (commune à Pandas et Polars d’ailleurs).

y.value_counts()

En quoi ce déséquilibre doit-il me préoccuper ? 

Eh bien, reprenons notre jeu de données des transactions immobilières :

  • Nous avons une target qui présente 2 classes possibles. La première représente 68% du jeu de données et la deuxième 32% ;

  • Vous entraînez un modèle de classification en utilisant une validation croisée à 3 folds ;

  • Chaque fold a été découpé aléatoirement, et représente 33% de la taille du jeu de données.

Avec cette approche, sans ajustement supplémentaire, il est probable que la majeure partie de la classe minoritaire finisse par se concentrer sur un seul fold ! Par conséquent, quand ce fold servira en tant que jeu de test, on aura entraîné le modèle sur tous les autres folds… qui ne contiennent que très peu d’observations de la classe minoritaire !

Pour éviter ce problème, il est alors important de s’assurer que tous les folds contiennent suffisamment d’observations de toutes les classes à prédire. C’est exactement l’objectif de la stratification ! Maintenant qu’on a compris l’enjeu, demandons à ChatGPT de définir la stratification. ;)

ChatGPT : La stratification d'un modèle de classification est une technique utilisée pour s'assurer que les sous-échantillons (par exemple, les ensembles d'entraînement et de test) ont des proportions similaires de classes que l'ensemble de données original. Cela est particulièrement important dans des situations où les classes sont déséquilibrées, c'est-à-dire lorsque certaines classes sont beaucoup plus représentées que d'autres. La stratification garantit que chaque sous-échantillon est représentatif de l'ensemble des données d'origine, permettant une évaluation plus fiable et cohérente du modèle.

Pratique, n’est-ce pas ? 😀 Le schéma suivant résume ce fonctionnement :

Diagramme illustrant la stratification des données. La première ligne montre une distribution déséquilibrée de données positives et négatives . La deuxième ligne illustre une mauvaise répartition des données après la division, tandis que la tro

Comment implémenter la stratification d’une validation croisée avec Scikit- learn ? Eh bien c’est extrêmement simple ! Il suffit de choisir la variante StratifiedKFold() déjà implémentée dans scikit-learn et réutiliser la fonction précédente !


from sklearn.model_selection import StratifiedKFold()

perform_cross_validation(
    X=X,
    y=y_classification,
    model=model,
    cross_val_type=StratifiedKFold(),
    scoring_metrics=classification_scoring_metrics,
)

Exemple 2 : la variante “Leave One Group Out”

Pour illustrer l’utilité de cette variante de validation croisée, reprenons nos fameuses transactions immobilières.

Posons-nous la question suivante : Pour estimer le prix des transactions, est-ce suffisant d’avoir un seul modèle de régression, entraîné sur toutes les régions de France, ou alors faudrait-il avoir plusieurs modèles “spécialisés”, chacun entraîné sur une région particulière pour adresser leurs spécificités ? 

La question est légitime ! On sait que les prix au mètre carré en Île-de-France n’ont rien à voir avec ceux de la Bretagne ! Mais du coup, comment savoir si un modèle de régression, entraîné sur toute la France, est suffisamment bon pour couvrir toutes les régions de manière assez satisfaisante ?

Découvrons dans ce screencast en quoi le LeaveOneGroupOut s’avère être une solution raisonnable :

Avoir plusieurs modèles n’est pas un meilleur choix dans l’absolu, car cela rajoute beaucoup plus de complexité dans l’interprétation des résultats, en plus de nécessiter beaucoup plus de maintenance technique. Tout dépend de votre contexte, nous allons explorer cela davantage dans la Partie 3 de ce cours !

Comment choisir la bonne variante ?

Pour pouvoir faire un bon choix (car des fois, il y en a plusieurs), il faut bien comprendre l’objectif d’origine d’un jeu de test : Un jeu de données de test est censé simuler l’acquisition de nouvelles observations.

Expliquons ce point un peu plus : on construit un modèle supervisé pour pouvoir résoudre un problème métier concret, en utilisant ses prédictions sur la nouvelle donnée que l’on collecte au fil de l’eau. Quand on élabore notre modèle pour la première fois, nous n’avons pas encore collecté cette “nouvelle donnée” qui pourrait ressembler ou pas à notre jeu de données disponible à l’instant t.

Notre meilleure solution est alors de “cacher” à notre modèle une partie de notre donnée existante et de traiter celle-ci comme si elle représentait de la nouvelle donnée. C’est l’objectif réel des différentes méthodes de séparation Train Test !

Pour réaliser un bon choix de découpage de la donnée, il est par conséquent nécessaire de bien comprendre comment notre donnée est collectée chronologiquement. Dans notre jeu de données des transactions immobilières par exemple, nous savons qu’il y a eu un changement du marché de l’immobilier à partir de la période Covid et surtout à partir de la forte inflation en 2022.

Il ne serait alors pas très logique de construire un jeu d’apprentissage incluant des transactions qui ont lieu en novembre 2022 alors que notre jeu de test figure des transactions qui ont lieu en avril 2022 ! En réalité, cette situation créerait également du Data Leakage, puisque nous donnerions au modèle comme ingrédients d’apprentissage un contexte qui ne peut exister que dans le futur !

Une variante de validation croisée plus adaptée à la situation serait l’approche TimeSeriesSplit. Elle consiste à diviser les données en folds de telle sorte à ce que le fold N+1 soit toujours situé dans le futur par rapport au fold N. Ça tombe bien ! C’est cette variante que vous allez implémenter dans l’exercice juste après cette section. ;)

Mais alors, quand on a réalisé un LeaveOneGroupOut dans la section précédente, nous avions délibérément inclus du Data Leakage non ?

Très bon point ! Certes, c’était un choix de séparation Train Test qui présentait du leakage. En revanche, ce LeaveOneGroupOut n’avait jamais vocation à être utilisé en production ! C’était plus un test exploratoire pour mesurer si les données régionales étaient suffisamment différentes les unes des autres pour justifier l’utilisation de plusieurs modèles au lieu d’un seul.

À vous de jouer !

Implémentez la variante de validation croisée TimeSeriesSplit pour évaluer le modèle de classification sur la donnée des transactions immobilières. Pour ce faire, lisez la documentation de cette méthode sur Scikit-learn et complétez ce template de code-pré-rempli. Pour réduire le temps de calcul, vous pouvez vous limiter à une seule région, par exemple l’Occitanie.

Une fois que vous avez fini, vous avez ce corrigé à disposition

En résumé

  • Une simple séparation Train-Test est souvent insuffisante pour évaluer correctement un modèle. La validation croisée offre une méthode plus fiable en testant le modèle sur plusieurs sous-ensembles de données.

  • La validation croisée aide à évaluer la capacité de généralisation d'un modèle en utilisant différents morceaux des données pour mieux détecter l'overfit.

  • Dans les problèmes de classification avec des classes déséquilibrées, la stratification est essentielle pour garantir des sous-ensembles de données représentatifs, évitant ainsi des biais.

  • Le choix de la méthode de validation, comme Leave One Group Out ou TimeSeriesSplit, dépend du contexte et de la manière dont les données sont collectées.   

Vos données sont prêtes pour la modélisation. Prenez le temps d'évaluer vos connaissances avec le quiz avant de passer à l'étape suivante !

Exemple de certificat de réussite
Exemple de certificat de réussite