10  K plus proches voisins

Dans ce chapitre, nous explorons plusieurs visualisations des données du classificateur des K plus proches voisins (KPPV).

Plan du chapitre :

10.1 Figure statique originale

Notre objectif, dans ce chapitre, est de produire une version interactive de la figure 13.4 tirée de Elements of Statistical Learning (Hastie, Tibshirani, et Friedman 2009). Cette figure se compose de deux graphiques :

Esquisses pour K plus proches voisins

À gauche : les courbes d’erreur de mauvaise classification, en fonction du nombre de voisins.

À droite : les données et les limites de décision dans l’espace bidimensionnel des variables d’entrée.

  • geom_point() pour les points de données.
  • geom_point() pour les prédictions de classification sur la grille en arrière-plan.
  • geom_path() pour les limites de décision.
  • geom_text() pour les taux d’erreur (entraînement, test et Bayes).

10.1.1 Graphique des courbes d’erreurs de classification

Nous commençons par charger l’ensemble des données.

if(!requireNamespace("animint2data"))
  remotes::install_github("animint/animint2data")
Loading required namespace: animint2data
data(ESL.mixture, package="animint2data")
str(ESL.mixture)
List of 8
 $ x       : num [1:200, 1:2] 2.5261 0.367 0.7682 0.6934 -0.0198 ...
 $ y       : num [1:200] 0 0 0 0 0 0 0 0 0 0 ...
 $ xnew    : 'matrix' num [1:6831, 1:2] -2.6 -2.5 -2.4 -2.3 -2.2 -2.1 -2 -1.9 -1.8 -1.7 ...
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : chr [1:6831] "1" "2" "3" "4" ...
  .. ..$ : chr [1:2] "x1" "x2"
 $ prob    : num [1:6831] 3.55e-05 3.05e-05 2.63e-05 2.27e-05 1.96e-05 ...
  ..- attr(*, ".Names")= chr [1:6831] "1" "2" "3" "4" ...
 $ marginal: num [1:6831] 6.65e-15 2.31e-14 7.62e-14 2.39e-13 7.15e-13 ...
  ..- attr(*, ".Names")= chr [1:6831] "1" "2" "3" "4" ...
 $ px1     : num [1:69] -2.6 -2.5 -2.4 -2.3 -2.2 -2.1 -2 -1.9 -1.8 -1.7 ...
 $ px2     : num [1:99] -2 -1.95 -1.9 -1.85 -1.8 -1.75 -1.7 -1.65 -1.6 -1.55 ...
 $ means   : num [1:20, 1:2] -0.2534 0.2667 2.0965 -0.0613 2.7035 ...

Nous utiliserons les éléments suivants de cet ensemble de données :

  • x : la matrice des variables d’entrée de l’ensemble de données d’apprentissage (200 observations sur les lignes x 2 variables numériques sur les colonnes).
  • y : le vecteur de sortie de l’ensemble de données d’apprentissage (200 étiquettes de classe, soit 0 ou 1).
  • xnew : la matrice représentant la grille de points dans l’espace des variables d’entrée, où seront affichées les prédictions de la fonction de classification (6831 points de la grille sur les lignes x 2 variables numériques sur les colonnes).
  • prob : la probabilité de la classe 1 à chacun des points de la grille (6831 valeurs numériques comprises entre 0 et 1).
  • px1 : la grille de points pour le premier variable d’entrée (69 valeurs numériques comprises entre -2,6 et 4,2). Ces points seront utilisés pour calculer la limite de décision de Bayes à l’aide de la fonction contourLines.
  • px2 : la grille de points pour la deuxième variable d’entrée (99 valeurs numériques comprises entre -2 et 2,9).
  • means : les 20 centres des distributions normales dans le modèle de simulation (20 centres sur les lignes x 2 variables sur les colonnes).

Tout d’abord, nous créons un ensemble de test, en suivant le code d’exemple de help(ESL.mixture). Notez que nous utilisons un data.table plutôt qu’un data.frame pour stocker ces données volumineuses, puisque data.table est souvent plus rapide et plus économe en mémoire pour les ensembles de données volumineux.

library(MASS)
library(data.table)
set.seed(123)
centers <- c(
  sample(1:10, 5000, replace=TRUE),
  sample(11:20, 5000, replace=TRUE))
mix.test <- mvrnorm(10000, c(0,0), 0.2*diag(2))
test.points <- data.table(
  mix.test + ESL.mixture$means[centers,],
  label=factor(c(rep(0, 5000), rep(1, 5000))))
test.points
               V1        V2 label
    1:  2.0210959 1.3905124     0
    2:  2.7488414 1.0327241     0
   ---                           
 9999: -1.9089417 1.6135246     1
10000:  0.7678115 0.3154265     1

Nous créons ensuite un tableau de données qui comprend tous les points de test et les points de la grille, que nous utiliserons dans l’argument test de la fonction KPPV.

pred.grid <- data.table(ESL.mixture$xnew, label=NA)
input.cols <- c("V1", "V2")
names(pred.grid)[1:2] <- input.cols
test.and.grid <- rbind(
  data.table(test.points, set="test"),
  data.table(pred.grid, set="grid"))
test.and.grid$fold <- NA
test.and.grid
             V1       V2 label  set fold
    1: 2.021096 1.390512     0 test   NA
    2: 2.748841 1.032724     0 test   NA
   ---                                  
16830: 4.100000 2.900000  <NA> grid   NA
16831: 4.200000 2.900000  <NA> grid   NA

Nous assignons aléatoirement chaque observation de l’ensemble de données d’apprentissage à l’un des 10 divisions.

n.folds <- 10
set.seed(2)
mixture <- with(ESL.mixture, data.table(x, label=factor(y)))
mixture$fold <- sample(rep(1:n.folds, l=nrow(mixture)))
mixture
               V1        V2 label fold
  1:  2.526092968 0.3210504     0    5
  2:  0.366954472 0.0314621     0    8
 ---                                  
199:  0.008130556 2.2422639     1    4
200: -0.196246334 0.5514036     1    8

Nous définissons la fonction OneFold pour diviser les 200 observations en un ensemble d’entraînement et un ensemble de validation. Elle calcule ensuite la probabilité prédite par le classificateur des K plus proches voisins pour chacun des points de données dans tous les ensembles (entraînement, validation, test et grille).

OneFold <- function(validation.fold){
  set <- ifelse(mixture$fold == validation.fold, "validation", "train")
  fold.data <- rbind(test.and.grid, data.table(mixture, set))
  fold.data$data.i <- 1:nrow(fold.data)
  only.train <- subset(fold.data, set == "train")
  data.by.neighbors <- list()
  for(neighbors in seq(1, 30, by=2)){
    if(interactive())cat(sprintf(
      "n.folds=%4d validation.fold=%d neighbors=%d\n",
      n.folds, validation.fold, neighbors))
    set.seed(1)
    pred.label <- class::knn( # random tie-breaking.
      only.train[, input.cols, with=FALSE],
      fold.data[, input.cols, with=FALSE],
      only.train$label,
      k=neighbors,
      prob=TRUE)
    prob.winning.class <- attr(pred.label, "prob")
    fold.data$probability <- ifelse(
      pred.label=="1", prob.winning.class, 1-prob.winning.class)
    fold.data[, pred.label := ifelse(0.5 < probability, "1", "0")]
    fold.data[, is.error := label != pred.label]
    fold.data[, prediction := ifelse(is.error, "erronée", "correcte")]
    data.by.neighbors[[paste(neighbors)]] <- 
      data.table(neighbors, fold.data)
  }#for(neighbors
  do.call(rbind, data.by.neighbors)
}#for(validation.fold

Ci-dessous, nous exécutons la fonction OneFold en parallèle à l’aide du package future. Pour validation.fold de 1 à 10, on calcule l’erreur de l’ensemble de validation. Pour validation.fold=0, on traite l’ensemble des 200 observations comme un ensemble d’entraînement, qui sera utilisé pour visualiser les limites de décision apprises des K plus proches voisins.

future::plan("multisession")
data.all.folds.list <- future.apply::future_lapply(
  0:n.folds, function(validation.fold){
    one.fold <- OneFold(validation.fold)
    data.table(validation.fold, one.fold)
  }, future.seed = NULL)
(data.all.folds <- do.call(rbind, data.all.folds.list))
         validation.fold neighbors           V1        V2 label   set fold
      1:               0         1  2.021095933 1.3905124     0  test   NA
      2:               0         1  2.748841354 1.0327241     0  test   NA
     ---                                                                  
2810114:              10        29  0.008130556 2.2422639     1 train    4
2810115:              10        29 -0.196246334 0.5514036     1 train    8
         data.i probability pred.label is.error prediction
      1:      1   0.0000000          0    FALSE   correcte
      2:      2   0.0000000          0    FALSE   correcte
     ---                                                  
2810114:  17030   0.7586207          1    FALSE   correcte
2810115:  17031   0.4137931          0     TRUE    erronée

Le tableau de données des prédictions contient près de 3 millions d’observations ! Lorsqu’il y a autant de données, il n’est ni pratique ni informatif de les visualiser simultanément, nous allons donc calculer et tracer des statistiques sommaires. Dans le code ci-dessous, nous calculons la moyenne et l’erreur standard de l’erreur de mauvaise classification pour chaque modèle (sur les 10 divisions dans la validation croisée).

labeled.data <- data.all.folds[!is.na(label)]
error.stats <- labeled.data[, list(
  error.prop=mean(is.error)
  ), by=.(set, validation.fold, neighbors)]
validation.error <- error.stats[set=="validation", list(
  mean=mean(error.prop),
  sd=sd(error.prop)/sqrt(.N)
  ), by=.(set, neighbors)]
validation.error
           set neighbors  mean         sd
 1: validation         1 0.240 0.01943651
 2: validation         3 0.165 0.02362908
---                                      
14: validation        27 0.195 0.02034426
15: validation        29 0.205 0.02291288

Nous construisons ci-dessous des tableaux de données pour l’erreur d’entraînement et pour l’erreur de Bayes (nous savons qu’elle est de 0,21 pour les données de l’exemple de mélange).

Bayes.error <- data.table(
  set="Bayes",
  validation.fold=NA,
  neighbors=NA,
  error.prop=0.21)
Bayes.error
     set validation.fold neighbors error.prop
1: Bayes              NA        NA       0.21
other.error <- error.stats[validation.fold==0]
head(other.error)
      set validation.fold neighbors error.prop
 1:  test               0         1     0.2938
 2: train               0         1     0.0000
---                                           
 5:  test               0         5     0.2273
 6: train               0         5     0.1300

Ci-dessous, nous construisons une palette de couleurs à partir de dput(RColorBrewer::brewer.pal(Inf, "Set1")) et des palettes de types de lignes (linetype).

set.colors <- c(
  test="#377EB8", #blue
  Bayes="#984EA3",#purple
  validation="#4DAF4A",#green
  entraînement="#FF7F00")#orange
classifier.linetypes <- c(
  Bayes="dashed",
  KPPV="solid")
set.linetypes <- set.colors
set.linetypes[] <- classifier.linetypes[["KPPV"]]
set.linetypes["Bayes"] <- classifier.linetypes[["Bayes"]]
cbind(set.linetypes, set.colors)
             set.linetypes set.colors
test         "solid"       "#377EB8" 
Bayes        "dashed"      "#984EA3" 
validation   "solid"       "#4DAF4A" 
entraînement "solid"       "#FF7F00" 

Le code ci-dessous reproduit le graphique des courbes d’erreur de la figure originale.

library(animint2)
add_set_fr <- function(DT)DT[
, set_fr := ifelse(set=="train", "entraînement", set)]
add_set_fr(other.error)
add_set_fr(validation.error)
add_set_fr(Bayes.error)
legend.name.type <- "erreur"
errorPlotStatic <- ggplot()+
  theme_bw()+
  geom_hline(aes(
    yintercept=error.prop, color=set_fr, linetype=set_fr),
    data=Bayes.error)+
  scale_color_manual(
    legend.name.type,
    values=set.colors,
    breaks=names(set.colors))+
  scale_linetype_manual(
    legend.name.type,
    values=set.linetypes,
    breaks=names(set.linetypes))+
  ylab("Taux d’erreur")+
  xlab("Nombre de voisins")+
  geom_linerange(aes(
    neighbors, ymin=mean-sd, ymax=mean+sd,
    color=set_fr),
    data=validation.error)+
  geom_line(aes(
    neighbors, mean, linetype=set_fr, color=set_fr),
    data=validation.error)+
  geom_line(aes(
    neighbors, error.prop,
    group=set_fr, linetype=set_fr, color=set_fr),
    data=other.error)+
  geom_point(aes(
    neighbors, mean, color=set_fr),
    data=validation.error)+
  geom_point(aes(
    neighbors, error.prop, color=set_fr),
    data=other.error)
errorPlotStatic

10.1.2 Graphique des limites de décision dans l’espace des variables d’entrée

Pour la visualisation statique des données de l’espace des variables, nous ne montrons que le modèle avec 7 voisins.

show.neighbors <- 7
show.data <- data.all.folds[
  validation.fold==0 & neighbors==show.neighbors]
show.points <- show.data[set=="train"]
show.points
     validation.fold neighbors           V1        V2 label   set fold data.i
  1:               0         7  2.526092968 0.3210504     0 train    5  16832
  2:               0         7  0.366954472 0.0314621     0 train    8  16833
 ---                                                                         
199:               0         7  0.008130556 2.2422639     1 train    4  17030
200:               0         7 -0.196246334 0.5514036     1 train    8  17031
     probability pred.label is.error prediction
  1:   0.1428571          0    FALSE   correcte
  2:   0.1428571          0    FALSE   correcte
 ---                                           
199:   0.8571429          1    FALSE   correcte
200:   0.2857143          0     TRUE    erronée

Ensuite, nous calculons les taux d’erreur de classification, que nous afficherons en bas à gauche du graphique de l’espace des variables.

text.height <- 0.25
text.V1.prop <- 0
text.V2.bottom <- -2
text.V1.error <- -2.6
error.text <- rbind(
  Bayes.error,
  other.error[neighbors==show.neighbors])
error.text[, V2.top := text.V2.bottom + text.height * (1:.N)]
error.text[, V2.bottom := V2.top - text.height]
error.text
     set validation.fold neighbors error.prop       set_fr V2.top V2.bottom
1: Bayes              NA        NA     0.2100        Bayes  -1.75     -2.00
2:  test               0         7     0.2261         test  -1.50     -1.75
3: train               0         7     0.1450 entraînement  -1.25     -1.50

Nous définissons la fonction suivante, que nous utiliserons pour calculer les limites de décision.

getBoundaryDF <- function(prob.vec){
  stopifnot(length(prob.vec) == 6831)
  several.paths <- with(ESL.mixture, contourLines(
    px1, px2,
    matrix(prob.vec, length(px1), length(px2)),
    levels=0.5))
  contour.list <- list()
  for(path.i in seq_along(several.paths)){
    contour.list[[path.i]] <- with(several.paths[[path.i]], data.table(
      path.i, V1=x, V2=y))
  }
  do.call(rbind, contour.list)
}

Nous utilisons cette fonction pour calculer les limites de décision pour les 7 plus proches voisins et pour la fonction optimale de Bayes.

boundary.grid <- show.data[set=="grid"]
boundary.grid[, label := pred.label]
pred.boundary <- getBoundaryDF(boundary.grid$probability)
pred.boundary$classifier <- "KPPV"
Bayes.boundary <- getBoundaryDF(ESL.mixture$prob)
Bayes.boundary$classifier <- "Bayes"
Bayes.boundary
     path.i        V1        V2 classifier
  1:      1 -2.600000 -0.528615      Bayes
  2:      1 -2.557084 -0.500000      Bayes
 ---                                      
249:      2  3.022480  2.850000      Bayes
250:      2  3.028586  2.900000      Bayes

Ci-dessous, nous ne considérons que les points de la grille qui ne chevauchent pas les étiquettes de texte.

on.text <- function(V1, V2){
  V2 <= max(error.text$V2.top) & V1 <= text.V1.prop
}
(show.grid <- boundary.grid[!on.text(V1, V2)])
      validation.fold neighbors  V1   V2 label  set fold data.i probability
   1:               0         7 0.1 -2.0     0 grid   NA  10028   0.0000000
   2:               0         7 0.2 -2.0     0 grid   NA  10029   0.0000000
  ---                                                                      
6398:               0         7 4.1  2.9     1 grid   NA  16830   0.5714286
6399:               0         7 4.2  2.9     1 grid   NA  16831   0.5714286
      pred.label is.error prediction
   1:          0       NA       <NA>
   2:          0       NA       <NA>
  ---                               
6398:          1       NA       <NA>
6399:          1       NA       <NA>

Le nuage de points ci-dessous reproduit le classificateur des 7 plus proches voisins de la figure originale.

label.colors <- c(
  "0"="#377EB8",
  "1"="#FF7F00")
scatterPlotStatic <- ggplot()+
  theme_bw()+
  theme(axis.text=element_blank(),
        axis.ticks=element_blank(),
        axis.title=element_blank())+
  ggtitle("7 plus proches voisins")+
  scale_color_manual(
    "classe",
    values=label.colors)+
  scale_linetype_manual(
    "méthode",
    values=classifier.linetypes)+
  geom_point(aes(
    V1, V2, color=label),
    size=0.2,
    data=show.grid)+
  geom_path(aes(
    V1, V2, group=path.i, linetype=classifier),
    size=1,
    data=pred.boundary)+
  geom_path(aes(
    V1, V2, group=path.i, linetype=classifier),
    color=set.colors[["Bayes"]],
    size=1,
    data=Bayes.boundary)+
  geom_point(aes(
    V1, V2, color=label),
    fill=NA,
    size=3,
    data=show.points)+
  geom_text(aes(
    text.V1.error, V2.bottom,
    label=paste("Err.", set_fr, ":")),
    data=error.text,
    hjust=0)+
  geom_text(aes(
    text.V1.prop, V2.bottom, label=sprintf("%.3f", error.prop)),
    data=error.text,
    hjust=1)
scatterPlotStatic

10.1.3 Graphiques combinés

Enfin, nous combinons les deux ggplots et les affichons sous forme d’un animint2.

animint(
  errorPlotStatic+
    theme_animint(width=300),
  scatterPlotStatic+
    theme_animint(last_in_row=TRUE))

Bien que cette visualisation comporte trois légendes interactives, elle est statique dans le sens où elle n’affiche que les prédictions du modèle des 7 plus proches voisins.

10.2 Sélectionner le nombre de voisins à l’aide de l’interactivité

Dans cette section, nous proposons une visualisation interactive qui permet à l’utilisateur de sélectionner K, le nombre de voisins.

10.2.1 Graphique interactif des courbes d’erreur

Examinons d’abord le graphique des courbes d’erreur.

Nous voulons effectuer les modifications suivantes :

  • ajout d’un geom_tallrect() pour sélectionner le nombre de voisins ;
  • changement de la limite de décision de Bayes, passant d’un geom_hline() avec une entrée de légende, à un geom_segment() avec une étiquette de texte ;
  • ajout d’une légende de type de ligne pour distinguer les taux d’erreur des modèles de Bayes et de KPPV ;
  • remplacement des barres d’erreur, geom_linerange(), par des bandes d’erreur, geom_ribbon().

Les seules nouvelles données que nous devons définir sont les points d’extrémité du segment que nous utiliserons pour tracer la frontière de décision de Bayes. Notez que nous redéfinissons également l’ensemble test pour souligner que l’erreur de Bayes représente le meilleur taux d’erreur atteignable pour les données de test.

Bayes.segment <- data.table(
  Bayes.error,
  classifier="Bayes",
  min.neighbors=1,
  max.neighbors=29)
Bayes.segment$set_fr <- "test"

Nous ajoutons également aux tableaux de données une variable d’erreur qui contient l’erreur de prédiction des modèles KPPV. Cette variable d’erreur sera utilisée pour la légende du type de ligne.

validation.error$classifier <- "KPPV"
other.error$classifier <- "KPPV"

Nous redéfinissons le graphique des courbes d’erreur ci-dessous. Notez que :

  • Nous utilisons showSelected dans geom_text() et geom_ribbon() afin qu’ils soient masqués lorsque l’on clique sur les légendes interactives.
  • Nous utilisons clickSelects dans geom_tallrect() pour sélectionner le nombre de voisins. Les geoms cliquables doivent être placés en dernier (couche supérieure) afin de ne pas être masqués par les geoms non cliquables (couches inférieures).
set.colors <- c(
  test="#984EA3",#purple
  validation="#4DAF4A",#green
  Bayes="#984EA3",#purple
  entraînement="black")
legend.name <- "Ensemble"
errorPlot <- ggplot()+
  ggtitle("Sélection du nombre de voisins")+
  theme_bw()+
  geom_text(aes(
    min.neighbors, error.prop,
    color=set_fr, label="Bayes"),
    showSelected="classifier",
    hjust=1,
    data=Bayes.segment)+
  geom_segment(aes(
    min.neighbors, error.prop, 
    xend=max.neighbors, yend=error.prop,
    color=set_fr,
    linetype=classifier),
    showSelected="classifier", 
    data=Bayes.segment)+
  scale_color_manual(
    legend.name,
    values=set.colors, breaks=names(set.colors))+
  scale_fill_manual(
    legend.name,
    values=set.colors)+
  scale_linetype_manual(
    legend.name,
    values=classifier.linetypes)+
  guides(fill="none", linetype="none")+
  ylab("Taux d’erreur de classification")+
  scale_x_continuous(
    "Nombre de Voisins",
    limits=c(-1, 30),
    breaks=c(1, 10, 20, 29))+
  geom_ribbon(aes(
    neighbors, ymin=mean-sd, ymax=mean+sd,
    fill=set_fr),
    showSelected=c("classifier", "set_fr"),
    alpha=0.5,
    color="transparent",
    data=validation.error)+
  geom_line(aes(
    neighbors, mean, color=set_fr,
    linetype=classifier),
    showSelected="classifier", 
    data=validation.error)+
  geom_line(aes(
    neighbors, error.prop, group=set_fr, color=set_fr,
    linetype=classifier),
    showSelected="classifier", 
    data=other.error)+
  geom_tallrect(aes(
    xmin=neighbors-1, xmax=neighbors+1),
    clickSelects="neighbors",
    alpha=0.5,
    data=validation.error)
errorPlot

10.2.2 Graphique de l’espace des éléments montrant le nombre de voisins sélectionnés.

Concentrons-nous maintenant sur le graphique de l’espace des variables d’entrée. Dans la section précédente, nous n’avons considéré que le sous-ensemble de données du modèle à 7 voisins. Nous proposons les modifications suivantes :

  • Nous utilisons les voisins comme variable showSelected.
  • Nous ajoutons une légende pour indiquer les points de données d’entraînement mal classées.
  • Nous utilisons des coordonnées à espacement égal afin que la distance visuelle (pixels) soit la même que la distance euclidienne dans l’espace des variables.
show.data <- data.all.folds[validation.fold==0]
(show.points <- show.data[set=="train"])
      validation.fold neighbors           V1        V2 label   set fold data.i
   1:               0         1  2.526092968 0.3210504     0 train    5  16832
   2:               0         1  0.366954472 0.0314621     0 train    8  16833
  ---                                                                         
2999:               0        29  0.008130556 2.2422639     1 train    4  17030
3000:               0        29 -0.196246334 0.5514036     1 train    8  17031
      probability pred.label is.error prediction
   1:   0.0000000          0    FALSE   correcte
   2:   0.0000000          0    FALSE   correcte
  ---                                           
2999:   0.7586207          1    FALSE   correcte
3000:   0.3793103          0     TRUE    erronée

Ci-dessous, nous calculons les limites de décision prédites séparément pour chaque modèle de K plus proches voisins.

boundary.grid <- show.data[set=="grid"]
boundary.grid[, label := pred.label]
show.grid <- boundary.grid[!on.text(V1, V2)]
pred.boundary <- boundary.grid[
, getBoundaryDF(probability), by=neighbors]
pred.boundary$classifier <- "KPPV"
pred.boundary
      neighbors path.i       V1        V2 classifier
   1:         1      1 -2.60000 -1.025000       KPPV
   2:         1      1 -2.55000 -1.000000       KPPV
  ---                                               
4491:        29      2  2.80099  1.900000       KPPV
4492:        29      2  2.80000  1.897619       KPPV

Au lieu d’afficher le nombre de voisins dans le titre du graphique, nous créons ci-dessous un élément geom_text() qui sera mis à jour en fonction du nombre de voisins sélectionnés.

show.text <- show.grid[, list(
  V1=mean(range(V1)), V2=3.05), by=neighbors]

Nous calculons ci-dessous la position du texte qui affichera, en bas à gauche, le taux d’erreur du modèle sélectionné.

other.error[, V2.bottom := rep(
  text.V2.bottom + text.height * 1:2, l=.N)]

Ci-dessous, nous redéfinissons les données de l’erreur de Bayes sans colonne de voisins, afin qu’elles apparaissent dans chaque sous-ensemble showSelected.

Bayes.error <- data.table(
  set_fr="Bayes",
  error.prop=0.21)

Enfin, nous redéfinissons le graphique, en utilisant le nombre de voisins (neighbors) comme variable showSelected dans plusieurs geoms.

err_set <- function(set)paste("Err.", set)
scatterPlot <- ggplot()+
  ggtitle("Erreurs de classification (entraînement)")+
  theme_bw()+
  xlab("Variable d’entrée 1")+
  ylab("Variable d’entrée 2")+
  scale_linetype_manual(
    "méthode", values=classifier.linetypes)+
  scale_fill_manual(
    "prédiction",
    values=c(erronée="black", correcte="transparent"))+
  scale_color_manual("classe", values=label.colors)+
  geom_point(aes(
    V1, V2, color=label),
    showSelected="neighbors",
    size=0.2,
    data=show.grid)+
  geom_path(aes(
    V1, V2, group=path.i, linetype=classifier),
    showSelected="neighbors",
    size=1,
    data=pred.boundary)+
  geom_path(aes(
    V1, V2, group=path.i, linetype=classifier),
    color=set.colors[["test"]],
    size=1,
    data=Bayes.boundary)+
  geom_point(aes(
    V1, V2, color=label,
    fill=prediction),
    showSelected="neighbors",
    size=3,
    data=show.points)+
  geom_text(aes(
    text.V1.error, text.V2.bottom, label=err_set(set_fr)),
    data=Bayes.error,
    hjust=0)+
  geom_text(aes(
    text.V1.prop, text.V2.bottom, label=sprintf("%.3f", error.prop)),
    data=Bayes.error,
    hjust=1)+
  geom_text(aes(
    text.V1.error, V2.bottom, label=err_set(set_fr)),
    showSelected="neighbors",
    data=other.error,
    hjust=0)+
  geom_text(aes(
    text.V1.prop, V2.bottom, label=sprintf("%.3f", error.prop)),
    showSelected="neighbors",
    data=other.error,
    hjust=1)+
  geom_text(aes(
    V1, V2,
    label=paste0(neighbors, "-PPV")),
    showSelected="neighbors",
    data=show.text)

Avant de compiler la visualisation des données interactive, nous imprimons un ggplot statique avec une facette pour chaque valeur de voisins.

scatterPlot+
  facet_wrap("neighbors")+
  theme(panel.margin=grid::unit(0, "lines"))

10.2.3 Visualisation des données interactive combinée

Enfin, nous combinons les deux graphiques dans une visualisation unique avec le nombre de voisins (neighbors) comme variable de sélection.

animint(
  errorPlot+
    theme_animint(width=300),
  scatterPlot+
    theme_animint(width=450, last_in_row=TRUE),
  first=list(neighbors=7),
  time=list(variable="neighbors", ms=3000))

Notez que le nombre de voisins (neighbors) est utilisé comme variable de temps, de sorte que l’animation montre les prédictions des différents modèles.

10.3 Sélectionner le nombre de divisions dans la validation croisée

Dans cette section, nous proposons une visualisation qui permet à l’utilisateur de sélectionner le nombre de divisions utilisées pour calculer la courbe d’erreur de validation.

La boucle for ci-dessous calcule la courbe d’erreur de validation pour différentes valeurs de n.folds.

error.by.folds <- list()
error.by.folds[["10"]] <- data.table(n.folds=10, validation.error)
for(n.folds in c(3, 5, 15)){
  set.seed(2)
  mixture <- with(ESL.mixture, data.table(x, label=factor(y)))
  mixture$fold <- sample(rep(1:n.folds, l=nrow(mixture)))
  only.validation.list <- future.apply::future_lapply(
    1:n.folds, function(validation.fold){
      one.fold <- OneFold(validation.fold)
      data.table(validation.fold, one.fold[set=="validation"])
    }, future.seed=NULL)
  only.validation <- do.call(rbind, only.validation.list)
  only.validation.error <- only.validation[, list(
    error.prop=mean(is.error)
  ), by=.(set, set_fr=set, validation.fold, neighbors)]
  only.validation.stats <- only.validation.error[, list(
    mean=mean(error.prop),
    sd=sd(error.prop)/sqrt(.N)
  ), by=.(set, set_fr=set, neighbors)]
  error.by.folds[[paste(n.folds)]] <-
    data.table(n.folds, only.validation.stats, classifier="KPPV")
}
validation.error.several <- do.call(rbind, error.by.folds)

Le code ci-dessous calcule le minimum de la courbe d’erreur pour chaque valeur de n.folds.

min.validation <- validation.error.several[
, .SD[which.min(mean)], by=n.folds]

Le code ci-dessous crée un nouveau graphique de courbe d’erreur à deux facettes.

facets <- function(df, facet){
  data.frame(df, facet=factor(facet, c("Divisions", "Taux d’erreur")))
}
errorPlotNew <- ggplot()+
  ggtitle("Sélection du nombre de divisions et de voisins")+
  theme_bw()+
  theme(panel.margin=grid::unit(0, "cm"))+
  facet_grid(facet ~ ., scales="free")+
  geom_text(aes(
    min.neighbors, error.prop,
    color=set_fr, label="Bayes"),
    showSelected="classifier",
    hjust=1,
    data=facets(Bayes.segment, "Taux d’erreur"))+
  geom_segment(aes(
    min.neighbors, error.prop, 
    xend=max.neighbors, yend=error.prop,
    color=set_fr,
    linetype=classifier),
    showSelected="classifier",                
    data=facets(Bayes.segment, "Taux d’erreur"))+
  scale_color_manual(
    legend.name, values=set.colors, breaks=names(set.colors))+
  scale_fill_manual(
    legend.name, values=set.colors, breaks=names(set.colors))+
  scale_linetype_manual(
    legend.name, values=classifier.linetypes)+
  guides(fill="none", linetype="none")+
  ylab("")+
  scale_x_continuous(
    "Nombre de Voisins",
    limits=c(-1, 30),
    breaks=c(1, 10, 20, 29))+
  geom_ribbon(aes(
    neighbors, ymin=mean-sd, ymax=mean+sd,
    fill=set_fr),
    showSelected=c("classifier", "set_fr", "n.folds"),
    alpha=0.5,
    color="transparent",
    data=facets(validation.error.several, "Taux d’erreur"))+
  geom_line(aes(
    neighbors, mean, color=set_fr,
    linetype=classifier),
    showSelected=c("classifier", "n.folds"),
    data=facets(validation.error.several, "Taux d’erreur"))+
  geom_line(aes(
    neighbors, error.prop, group=set_fr, color=set_fr,
    linetype=classifier),
    showSelected="classifier", 
    data=facets(other.error, "Taux d’erreur"))+
  geom_tallrect(aes(
    xmin=neighbors-1, xmax=neighbors+1),
    clickSelects="neighbors",
    alpha=0.5,
    data=validation.error)+
  geom_point(aes(
    neighbors, n.folds, color=set_fr),
    clickSelects="n.folds",
    size=9,
    data=facets(min.validation, "Divisions"))

Le code ci-dessous prévisualise le nouveau graphique de la courbe d’erreur, en ajoutant une facette supplémentaire pour la variable showSelected.

errorPlotNew+facet_grid(facet ~ n.folds, scales="free")

Le code ci-dessous crée une visualisation interactive à l’aide du nouveau graphique de la courbe d’erreur.

animint(
  errorPlotNew+
    theme_animint(width=325),
  scatterPlot+
    theme_animint(width=450, last_in_row=TRUE),
  first=list(neighbors=7, n.folds=10))

10.4 Résumé du chapitre et exercices

Nous avons montré comment ajouter deux fonctionnalités interactives à une visualisation des données des prédictions du modèle des K plus proches voisins. Nous avons commencé par une visualisation statique qui n’affiche que les prédictions des 7 plus proches voisins. Nous avons ensuite créé une visualisation interactive qui permet de sélectionner K, le nombre de voisins. Nous avons finalement proposé une autre visualisation, en ajoutant une facette qui permet de sélectionner le nombre de divsions dans la validation croisée.

Exercices :

  • Faites en sorte que les taux d’erreur affichés dans le texte en bas à gauche du deuxième graphique soient masqués quand on clique sur les entrées de la légende pour Bayes, train et test. Conseil : vous pouvez soit utiliser un geom_text() avec showSelected=c(selectorNameColumn="selectorValueColumn") (comme expliqué dans le chapitre 14) ou deux geom_text chacun avec un paramètre showSelected différent.
  • La colonne de probabilité (probability) du tableau de données show.grid est la probabilité prédite de la classe 1. Comment referiez-vous la visualisation pour montrer la probabilité prédite plutôt que la classe prédite à chaque point de la grille ? La difficulté principale est que la probabilité est une variable numérique, mais que animint2 impose des échelles exclusivement continues ou discrètes (pas les deux). Vous pourriez utiliser une échelle continue pour fill, mais vous devrez alors utiliser une échelle différente pour montrer la variable de prédiction.
  • Ajoutez un nouveau graphique qui montre les tailles relatives des ensembles d’entraînement (train), de validation et de test. Assurez-vous que la taille tracée des ensembles de validation et d’entraînement change en fonction de la valeur sélectionnée de n.folds.
  • Jusqu’à présent, les graphiques de l’espace des variables ne montraient que les prédictions et les erreurs du modèle pour l’ensemble des données d’entrainement (validation.fold==0). Créez une visualisation qui inclut un nouveau graphique ou une nouvelle facette pour sélectionner validation.fold, et un graphique de l’espace des variables avec facettes (une facette pour l’ensemble de données d’entrainement, une facette pour l’ensemble de données de validation).

Dans le chapitre 11, nous vous expliquerons comment visualiser le Lasso, un modèle d’apprentissage automatique.