6  Partie 6 : k Plus Proches Voisins (kNN)

7 k Plus Proches Voisins (kNN)

7.1 Principe

NoteClassifieur paresseux par voisinage

Le kNN (Cover & Hart, 1967) est un classificateur non-paramétrique et paresseux (lazy learner) : il ne construit pas de modèle explicite, mais classe chaque nouvelle observation selon le vote majoritaire de ses \(k\) voisins les plus proches dans l’espace des features. La distance euclidienne est utilisée après standardisation.

Avantages théoriques : aucune hypothèse sur la distribution des données, capture les frontières de décision non-linéaires complexes.

AvertissementMalédiction de la dimensionnalité (Séance 5)

Avec 53 features dont la majorité sont binaires (0/1), les distances euclidiennes deviennent peu discriminantes en haute dimension : toutes les paires d’observations tendent vers une distance similaire et la notion de « voisin proche » perd son sens. C’est la malédiction de la dimensionnalité étudiée en Séance 5. Nous vérifions empiriquement ce phénomène théorique au cours du tuning.

7.2 Entraînement et tuning

Nous testons 8 valeurs de \(k \in \{3, 5, 7, 11, 15, 21, 31, 51\}\), couvrant les voisinages locaux (k=3, risque de surapprentissage) aux voisinages globaux (k=51, lissage maximal). La standardisation (centrage + réduction) est appliquée pour que les distances ne soient pas dominées par Rating_Count (échelle ~0–10 000) par rapport aux variables binaires (0/1).

Code
knn_file     <- here::here("output", "model_knn.rds")
cm_knn_file  <- here::here("output", "cm_knn.rds")
roc_knn_file <- here::here("output", "roc_knn.rds")

if (file.exists(knn_file) && file.exists(cm_knn_file) && file.exists(roc_knn_file)) {
  model_knn <- readRDS(knn_file)
  cm_knn    <- readRDS(cm_knn_file)
  roc_knn   <- readRDS(roc_knn_file)
} else {
  grid_knn <- expand.grid(k = c(3, 5, 7, 11, 15, 21, 31, 51))
  set.seed(42)
  model_knn <- train(
    Satisfaction ~ ., data = train_data, method = "knn",
    trControl = CV_CONTROL, tuneGrid = grid_knn, metric = "ROC",
    preProcess = c("center", "scale")
  )
  pred_knn <- predict(model_knn, test_data)
  prob_knn <- predict(model_knn, test_data, type = "prob")
  cm_knn   <- confusionMatrix(pred_knn, test_data$Satisfaction, positive = "Oui")
  roc_knn  <- roc(test_data$Satisfaction, prob_knn$Oui, quiet = TRUE)
  saveRDS(model_knn, knn_file)
  saveRDS(cm_knn,    cm_knn_file)
  saveRDS(roc_knn,   roc_knn_file)
}

7.2.1 Sélection de k

Code
knn_res <- model_knn$results
ggplot(knn_res, aes(x = k, y = ROC)) +
  geom_line(color = COL_KNN, linewidth = 1) +
  geom_point(color = COL_KNN, size = 3) +
  geom_errorbar(aes(ymin = ROC - ROCSD, ymax = ROC + ROCSD),
                width = 2, color = COL_KNN, alpha = 0.5) +
  geom_hline(yintercept = max(model_logistic$results$ROC),
             linetype = "dashed", color = COL_PRIMARY, linewidth = 0.7) +
  geom_hline(yintercept = max(model_rf$results$ROC),
             linetype = "dashed", color = COL_POSITIVE, linewidth = 0.7) +
  annotate("text", x = 45, y = max(model_logistic$results$ROC) + 0.003,
           label = "Logistique (CV)", color = COL_PRIMARY, size = 3) +
  annotate("text", x = 45, y = max(model_rf$results$ROC) + 0.003,
           label = "Forêt Aléat. (CV)", color = COL_POSITIVE, size = 3) +
  scale_x_continuous(breaks = c(3, 5, 7, 11, 15, 21, 31, 51)) +
  labs(x = "Nombre de voisins (k)", y = "AUC (CV 5-fold)",
       title = "Performance kNN selon k") +
  THEME_REPORT
Figure 7.1: AUC en validation croisée selon k — comparaison avec LR et RF

L’AUC augmente monotoniquement avec \(k\), passant de 0.592 (k=3) à 0.652 (k=51). Cette tendance est révélatrice : le modèle bénéficie systématiquement du lissage, ce qui indique que le signal local est noyé dans le bruit dimensionnel. Même au meilleur k=51, le kNN reste en-dessous des deux lignes de référence (logistique et forêt aléatoire).

7.2.2 Performance sur le jeu test

Code
kable(data.frame(
  Metrique = c("Meilleur k", "AUC (CV)", "AUC (test)", "Accuracy",
               "Sensibilité", "Spécificité", "F1"),
  Valeur = c(
    model_knn$bestTune$k,
    round(max(model_knn$results$ROC), 4),
    round(auc(roc_knn), 4),
    round(cm_knn$overall["Accuracy"], 4),
    round(cm_knn$byClass["Sensitivity"], 4),
    round(cm_knn$byClass["Specificity"], 4),
    round(cm_knn$byClass["F1"], 4)
  )
))
Table 7.1: Métriques finales du kNN
Metrique Valeur
Meilleur k 51.0000
AUC (CV) 0.6520
AUC (test) 0.6541
Accuracy 0.6114
Sensibilité 0.5871
Spécificité 0.6364
F1 0.6052

7.2.3 Matrice de confusion

Code
cm_table_knn <- as.data.frame(cm_knn$table)
ggplot(cm_table_knn, aes(x = Reference, y = Prediction, fill = Freq)) +
  geom_tile(color = "white") +
  geom_text(aes(label = Freq), size = 6, fontface = "bold") +
  scale_fill_gradient(low = "white", high = COL_KNN) +
  labs(x = "Réel", y = "Prédit", title = "Matrice de confusion — kNN") +
  THEME_REPORT + theme(legend.position = "none")
Figure 7.2: Matrice de confusion — kNN

7.2.4 Diagnostic : la malédiction de la dimensionnalité

Le fait que k=51 soit optimal (le plus grand testé) est révélateur : avec 53 dimensions dont ~50 binaires, les distances euclidiennes se concentrent autour de leur moyenne et les observations deviennent quasiment équidistantes. Le kNN perd sa capacité discriminante car la notion de « voisinage local » n’a plus de sens en haute dimension. C’est un cas d’école de la malédiction de la dimensionnalité étudiée en Séance 5 : le kNN a besoin d’un voisinage très large pour moyenner le bruit, ce qui revient à une classification quasi-globale — perdant l’avantage principal de la méthode.

Bilan — kNN retenu pour illustration pédagogique, AUC test = 0.654 (k = 51). Démontre empiriquement la malédiction de la dimensionnalité.