10  K plus proches voisins

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

Plan du chapitre :

10.1 Figure statique originale

Notre but dans ce chapitre est de reproduire une version interactive de la figure 13.4 dans le livre Elements of Statistical Learning (Hastie2009?). Cette figure se compose de deux graphiques :

Esquisse pour K-Plus-Proches-Voisins

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

  • geom_line et geom_point pour les courbes d’erreur.
  • geom_linerange pour les barres d’erreur de la courbe d’erreur de validation.
  • geom_hline pour l’erreur de Bayes.
  • x = voisins.
  • y = pourcentage d’erreur.
  • couleur = type d’erreur.

À 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 train/test/Bayes.

10.1.1 Graphique des courbes d’erreurs de mauvaise classification

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

if(!file.exists("ESL.mixture.rda")){
  curl::curl_download(
    "https://web.stanford.edu/~hastie/ElemStatLearn/datasets/ESL.mixture.rda",
    "ESL.mixture.rda")
}
load("ESL.mixture.rda")
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 tests, 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 de 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 frontières de décision apprises avec 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, les visualiser toutes en même temps n’est ni pratique ni informatif. Au lieu de les visualiser simultanément, nous allons 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). Ceci est un exemple de l’idiome summarize data table qui est généralement utile pour calculer des statistiques sommaires pour un tableau de données unique.

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 mauvaise 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),]
show.grid
      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,
    shape=21,
    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’animint2.

animint(errorPlotStatic, scatterPlotStatic)

Bien que cette visualisation des données 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 la refonte du graphique des courbes d’erreur.

Notez les changements suivants :

  • ajout d’un sélecteur pour le nombre de voisins (geom_tallrect).
  • changement de la limite de décision de Bayes de geom_hline avec une entrée de légende, vers 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.
  • changement des barres d’erreur ( geom_linerange ) en 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. À noter 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 de type 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. À noter 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()+
  theme_animint(height=500)+
  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 ensuite sur la refonte du graphique de l’espace des caractéristiques. Dans la section précédente, nous n’avons considéré que le sous-ensemble de données du modèle à 7 voisins. Notre refonte comprend les changements suivants :

  • Nous utilisons les voisins comme variable showSelected.
  • Nous ajoutons une légende pour indiquer les points de données d’entraînement mal classés.
  • 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",]
show.points
      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 ggplot, en utilisant les voisins (“neighbors”) comme variable showSelected dans les geoms point, “path” et texte.

scatterPlot <- ggplot()+
  ggtitle("Erreurs de classification (entraînement)")+
  theme_bw()+
  theme_animint(width=500, height=500)+
  xlab("Variable d'entrée 1")+
  ylab("Variable d'entrée 2")+
  coord_equal()+
  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,
    shape=21,
    data=show.points)+
  geom_text(aes(
    text.V1.error, text.V2.bottom, label=paste("Err.", 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=paste("Err.", 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 des données unique avec les voisins comme variable de sélection.

animint(
  errorPlot,
  scatterPlot,
  first=list(neighbors=7),
  time=list(variable="neighbors", ms=3000))

Notez que les voisins (“neighbors”) sont utilisés 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 faisons une visualisation qui permet à l’utilisateur de sélectionner le nombre de divisions utilisés pour calculer la courbe d’erreur de validation.

La boucle for ci-dessous calcule la courbe d’erreur de validation pour plusieurs valeurs différentes 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_animint(height=500)+
  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 des données interactive à l’aide du nouveau graphique de la courbe d’erreur.

animint(
  errorPlotNew,
  scatterPlot,
  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 (“K-Nearest-Neighbors”). Nous avons commencé par une visualisation des données statique qui ne montrait que les prédictions du modèle des 7 Plus Proches Voisins. Ensuite, nous avons créé une refonte interactive qui permettait de sélectionner K, le nombre de voisins. Nous avons procédé à une autre refonte en ajoutant une facette permettant de sélectionner le nombre de divsions dans la validation croisée.

Exercices :

  • Faites en sorte que les taux d’erreur du texte affichés en bas à gauche du deuxième graphique soient masqués quand on clique sur les entrées de la légende pour Bayes, train, 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 feriez-vous la refonte de 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 ggplot2 impose que chaque échelle soit exclusivement continue ou discrète (pas les deux). Vous pourriez utiliser une échelle de remplissage continue (“continuous fill scale”), 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’entrainement (“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.