2.1 Validation croisée

Rappelez-vous qu’une règle à laquelle il ne faut jamais déroger, c’est de ne pas utiliser les mêmes individus en apprentissage et en test.

Les sets d’apprentissage et de test ne peuvent pas utiliser toutes les données : il faut les partitionner.

Souvent, la grosse difficulté est d’obtenir suffisamment d’objets de chaque classe identifiés manuellement pour permettre à la fois l’apprentissage et le test. Un test sur les mêmes objets que ceux utilisés lors de l’apprentissage mène à une surestimation systématique des performances du classifieur. Nous sommes donc contraint d’utiliser des objets différents dans les deux cas. Naturellement, plus, nous avons d’objets dans le set d’apprentissage et dans le set de test, et meilleur sera notre classifieur et notre évaluation de son efficacité.

La validation croisée permet de résoudre ce dilemme en utilisant en fin de compte tous les objets, à la fois dans le set d’apprentissage et dans le set de test, mais jamais simultanément. L’astuce consiste à diviser aléatoirement l’échantillon en n sous-ensembles à peu près égaux en effectifs. Ensuite, l’apprentissage suivi du test est effectué n fois indépendamment. A chaque fois, on sélectionne tous les sous-ensembles sauf un pour l’apprentissage, et le sous-ensemble laissé de côté est utilisé pour le test. L’opération est répétée de façon à ce que chaque sous-ensemble serve de set de test tour à tour. Au final, ou rassemble les résultats obtenus sur les n sets de tests, donc, sur tous les objets de notre échantillon et on calcule la matrice de confusion complète en regroupant donc les résultats des n étapes indépendantes. Enfin, on calcule les métriques souhaitées sur cette matrice de confusion afin d’obtenir une évaluation approchée et non biaisée des performances du classifieur complet (entraîné sur l’ensemble des données à disposition). L’animation suivante visualise le processus pour que cela soit plus clair dans votre esprit.

Principe de fonctionnement de la validation croisée avec k = 7.

Au final, nous aurons utilisé tous les individus à la fois en apprentissage et en test, mais jamais simultanément. Le rassemblement des prédictions obtenues à chaque étape nous permet d’obtenir une grosse matrice de confusion qui contient le même nombre d’individus que l’ensemble de notre jeu de données initial. Naturellement, nous n’avons pas le même classifieur à chaque étape, et celui-ci n’est pas aussi bien construit que s’il utilisait véritablement toutes les observations. Mais plus le découpage est fin et plus nous nous en approchons. À la limite, pour n observations, nous pourrions réaliser k = n sous-ensembles, c’est-à-dire que chaque sous-ensemble contient un et un seul individu. Nous avons alors à chaque fois le classifieur le plus proche possible de celui que l’on obtiendrait avec véritablement toutes les observations puisqu’à chaque étape nous ne perdons qu’un seul individu en phase d’apprentissage. La contrepartie est un temps de calcul potentiellement très, très long puisqu’il y a énormément d’étapes. Cette technique porte le nom de validation par exclusion d’une donnée ou leave-one-out cross-validation en anglais, LOOCV en abrégé. À l’autre extrême, nous pourrions utiliser k = 2. Mais dans ce cas, nous n’utilisons que le moitié des observation en phase d’apprentissage à chaque étape. C’est le plus rapide, mais le moins exact.

En pratique, un compromis entre exactitude et temps de calcul nous mène à choisir souvent la validation croisée dix fois (ten-fold cross-validation en anglais). Nous divisons aléatoirement en dix sous-ensembles d’à peu près le même nombre d’individus et nous répétons donc l’opération apprentissage -> test seulement dix fois. Chacun des dix classifieurs a donc été élaboré avec 90% des données totales, ce qui représente souvent un compromis encore acceptable pour estimer les propriétés qu’aurait le classifieur réalisé avec 100% des données. Toutefois, si nous constatons que le temps de calcul est raisonnable, rien ne nous empêche d’augmenter le nombre de sous-ensemble, voire d’utiliser la version par exclusion d’une donnée, mais en pratique nous observons tout de même que cela n’est pas raisonnable sur des très gros jeux de données et avec les algorithmes les plus puissants, mais aussi les plus gourmands en temps de calcul comme la forêt aléatoire ou les réseaux de neurones que nous aborderons dans le prochain module.

2.1.1 Application sur les manchots

Appliquons cela tout de suite avec l’ADL sur nos manchots. Plus besoin de séparer le jeu de données en set d’apprentissage et de test indépendants. La fonction cvpredict(, cv.k = ...) va se charger de ce partitionnement selon l’approche décrite ci-dessus en cv.k étapes. Notre analyse s’écrit alors :

SciViews::R
library(mlearning)

# Importation et remaniement des données comme précédemment
read("penguins", package = "palmerpenguins") %>.%
  rename(., bill_length = bill_length_mm, bill_depth = bill_depth_mm, 
    flipper_length = flipper_length_mm, body_mass = body_mass_g) %>.%
  select(., -year, -island, -sex) %>.%
  drop_na(.) -> penguins

Une fois notre tableau complet correctement nettoyé et préparé, nous faisons :

# ADL avec toutes les données
penguins_lda <- mlLda(data = penguins, species ~ .)
# Prédiction par validation croisée 10x
set.seed(7567) # Pensez à varier le nombre à chaque fois ici !
penguins_pred <- cvpredict(penguins_lda, cv.k = 10)
# Matrice de confusion
penguins_conf <- confusion(penguins_pred, penguins$species)
plot(penguins_conf)

Ici, nous avons cinq erreurs, mais attention, ceci est comptabilisé sur trois fois plus de données que précédemment, puisque l’ensemble du jeu de données a servi ici en test, contre un tiers seulement auparavant. Donc, notre estimation des performances du classifieur est assez comparable, avec une parfaite séparation de Gentoo, mais une petite erreur entre Chinstrap et Adelie. Les métriques sont disponibles à partir de notre objet penguins_conf comme d’habitude :

summary(penguins_conf)
# 342 items classified with 337 true positives (error = 1.5%)
# 
# Global statistics on reweighted data:
# Error rate: 1.5%, F(micro-average): 0.982, F(macro-average): 0.982
# 
#              Fscore    Recall Precision Specificity       NPV        FPR
# Gentoo    1.0000000 1.0000000 1.0000000   1.0000000 1.0000000 0.00000000
# Adelie    0.9834983 0.9867550 0.9802632   0.9842932 0.9894737 0.01570681
# Chinstrap 0.9629630 0.9558824 0.9701493   0.9927007 0.9890909 0.00729927
#                  FNR        FDR        FOR     LRPT       LRNT     LRPS
# Gentoo    0.00000000 0.00000000 0.00000000      Inf 0.00000000      Inf
# Adelie    0.01324503 0.01973684 0.01052632  62.8234 0.01345639 93.12500
# Chinstrap 0.04411765 0.02985075 0.01090909 130.9559 0.04444204 88.93035
#                 LRNS    BalAcc       MCC    Chisq        Bray Auto Manu A_M  TP
# Gentoo    0.00000000 1.0000000 1.0000000 342.0000 0.000000000  123  123   0 123
# Adelie    0.01994681 0.9855241 0.9703923 322.0481 0.001461988  152  151   1 149
# Chinstrap 0.03017998 0.9742915 0.9538967 311.1923 0.001461988   67   68  -1  65
#           FP FN  TN
# Gentoo     0  0 219
# Adelie     3  2 188
# Chinstrap  2  3 272

Précédemment, nous avions 1,8% d’erreur, et maintenant, nous n’en avons plus que 1,5%. C’est normal que notre taux d’erreur baisse un petit peu car nos classifieurs par validation croisée utilisent 90% des données alors qu’auparavant, nous n’en utilisions que les 2/3.

Si nous voulons faire un “leave-one-out”, nous ferions (sachant que nos données comptent 342 cas, nous indiquons ici cv.k = 342) :

penguins_pred_loo <- cvpredict(penguins_lda, cv.k = 342)
# Matrice de confusion
penguins_conf_loo <- confusion(penguins_pred_loo, penguins$species)
plot(penguins_conf_loo)

summary(penguins_conf_loo)
# 342 items classified with 337 true positives (error = 1.5%)
# 
# Global statistics on reweighted data:
# Error rate: 1.5%, F(micro-average): 0.982, F(macro-average): 0.982
# 
#              Fscore    Recall Precision Specificity       NPV        FPR
# Gentoo    1.0000000 1.0000000 1.0000000   1.0000000 1.0000000 0.00000000
# Adelie    0.9834983 0.9867550 0.9802632   0.9842932 0.9894737 0.01570681
# Chinstrap 0.9629630 0.9558824 0.9701493   0.9927007 0.9890909 0.00729927
#                  FNR        FDR        FOR     LRPT       LRNT     LRPS
# Gentoo    0.00000000 0.00000000 0.00000000      Inf 0.00000000      Inf
# Adelie    0.01324503 0.01973684 0.01052632  62.8234 0.01345639 93.12500
# Chinstrap 0.04411765 0.02985075 0.01090909 130.9559 0.04444204 88.93035
#                 LRNS    BalAcc       MCC    Chisq        Bray Auto Manu A_M  TP
# Gentoo    0.00000000 1.0000000 1.0000000 342.0000 0.000000000  123  123   0 123
# Adelie    0.01994681 0.9855241 0.9703923 322.0481 0.001461988  152  151   1 149
# Chinstrap 0.03017998 0.9742915 0.9538967 311.1923 0.001461988   67   68  -1  65
#           FP FN  TN
# Gentoo     0  0 219
# Adelie     3  2 188
# Chinstrap  2  3 272

Le résultat est le même. Donc, nous venons de montrer que, dans le cas de ce jeu de données et de l’ADL, une validation croisée dix fois permet d’estimer les performances du classifieur aussi bien que l’approche bien plus coûteuse en calculs (342 classifieurs sont calculés et testés) du “leave-one-out”.

À vous de jouer !

Effectuez maintenant les exercices du tutoriel C02La_cv (Validation croisée).

BioDataScience3::run("C02La_cv")
À retenir
  • Bien que plus complexe en interne, la validation croisée est très facile à utiliser avec {mlearning},

  • L’approche par validation croisée optimise l’utilisation des données à disposition. C’est la technique à préférer, sauf si nous disposons vraiment de données à profusion.

À vous de jouer

Réalisez l’assignation C02Ga_ml2, partie I.

Si vous êtes un utilisateur non enregistré ou que vous travaillez en dehors d’un cours, faites un “fork” de ce dépôt.

Voyez les explications dans le fichier README.md, partie I.