Introduction

Le modèle KNN (les plus proches voisins) sera appliqué au jeu de données “SPAM”. Pour rappel, le fichier “SPAM” est composé de 4601 lignes et 58 colonnes, dont la variable classifiante binaire (1:spam, 0:non spam). Les détails sur le fihier sont disponibles sur l’onglet précedent, “Régression logistique”, ou via les liens :



Présentation du modèle

La méthode des \(K\) plus proches voisins est une méthode d’apprentissage supervisée et non paramétrique (contrairement à la régression logistique). Cette méthode permet, à partir d’un échantillon d’apprentissage, d’estimer la place d’un nouvel individu “test” dans cet échantillon.

Il est en effet possible de définir une distance entre l’individu à estimer et les individus de l’échantillon d’apprentissage. Cette distance peut être mesurée par rapport à un nombre \(K\) d’individus de l’échantillon d’apprentissage (les \(K\) plus proches voisins).

Nous noterons notre individus à prédire \(x'_i\) et \(\hat y_i\) la valeur de sortie à estimer. Pour chaque individu \(x'_i\) :

  • Calculer la distance entre \(x'_i\) et les données d’apprentissage.
  • Retenir les \(K\) observations les plus proches.
  • Prendre les valeurs \(y_{ik}\) des \(K\) observations.
  • A partir des \(y_{ik}\), calculer/estimer le \(\hat y_i\) en sortie (pour l’individu test).

Deux cas de calcul :

  • \(Y\) continue, dit cas de régression, calculer la moyenne/médiane des \(y_{ik}\).
  • \(Y\) variable classifiante, calculer le mode de \(y_{ik}\).

Dans le cas du fichier “SPAM”, la variable à prédire est binaire (1:spam, 0:non spam).

Calcul des distances :

Il existe plusieurs types de calcul pour la distance. Nous pouvons avoir comme base de calcul, la distance Euclidienne, la distance de Manhattan ou la distance de Hamming. Ici, nous allons utilisé la fonction knn {class} du package class qui utilise la distance Euclidienne.

Distance Euclidienne :

La distance entre deux points \(x\) et \(y\).

 \(D_e(x,y)=\sqrt {\Sigma^{n}_{i=1}(x_i-y_i)^2}\)

Calcul de la valeur de sortie :

La valeur de sortie est la valeur estimée pour un individu test. Cette valeur correspond à la valeur \(\hat y_i\) de l’individu test. Dans le cas d’une variable classifiante binaire, nous allons contruire un estimteur binaire.

Estimateur binaire :

 \(g_n(X)= \left\{\begin{array}{l} 1 \space si \space \Sigma^{K}_{j=1} 1_{y_i}(X)=1 \ge \Sigma^{K}_{j=1} 1_{y_i}(X)=0\\ 0 \space sinon\\\end{array}\right.\)



Partie modélisation Rstudio

C’est à partir de la fontion knn {class} du package class que nous allons appliquer le modèle aux données du fichier “SPAM”.

La fonction knn() comprend quelques paramètres :

  • train : un échantillon d’apprentissage.
  • test : un échantillon test.
  • cl : la variable classifiante de l’échantillon d’apprentissage.
  • k : le nombre de voisins les plus proches

Application du modèle KNN (k=1) :

#########
# KNN
#########

library(class)

## 1 - Presentation du modèle 

 # utilisation de knn {class}
 # presentation avec k=1 groupe (plus simple) 

 #utilisation avec train et test 

  index <- 1:nrow(Don_spam)

  set.seed(1024)
  trainIndex <- sample(index, trunc(length(index) * 0.666666666666667)) 
  DATASET.train <- Don_spam[trainIndex, ]
  
  #generation de l'echantillon TEST
  DATASET.test <- Don_spam[-trainIndex, ]

 
 ## Application KNN k=1
  
  mod.knn=knn(train=DATASET.train[,-58],test=DATASET.test[,-58],cl=DATASET.train$spam,
        k=1)
  
  # En sortie pour chaque individu des données DATASET.test 
  
  # attr(mod.knn,"nn.dist")[1:10] #distance euclidienne avec le plus proche voisin
  # # 3.978720  2.523022  4.388677 12.542601
  # 
  # attr(mod.knn,"nn.index")[1:10] # les voisins coté train
  # # 2347 1723 2058 1095
  # 
  # DATASET.train[attr(mod.knn,"nn.index")[1:4], 58] # les y dans l'ordre 0 0 1 1
  # 
  # 
  # # la prediction de knn
  #  mod.knn[1:4] # 0 0 1 1 => on constate qu'il attribu la classe du train (car ici k=1)
  # 
  # # la valeur de y sur test
  #  DATASET.test[1:4,58] #1 1 1 1 / les 2 premiers individus sont mal classés

  # taux d'erreur des mal classés
    err.knn <- sum(mod.knn!=DATASET.test$spam)/nrow(DATASET.test) #0.1870926 

La fontion knn() retourne pour chaque ligne de l’échantillon test plusieurs valeurs :

  • attr(mod.knn,“nn.dist”) : la distance Euclidienne du ou des plus proches voisins (ici k=1, 1 voisin).
  • attr(mod.knn,“nn.index”) : le numéro d’individu du plus proche voisin.
  • mod.knn : objet contenant la prédiction (variable classifiante \(\hat y_i\)).

On peut donc déduire le taux d’erreur des mals classés qui est de 19% environ.

Il est facile de constater que la fonction knn() renvoit bien le plus proche voisin à partir de la distance Euclidienne minimum. Nous allons prendre la première ligne de l’échantillon test et calculer toutes les distances Euclidienne par rapport aux individus de l’échantillon train.

# vérif distance euclidienne sur le premier individu test 
  
   # DATASET.test[1,-58] #premiere ligne du test
   # DATASET.train[2347,-58] #index 2347 correspondant au voisin coté train attr(mod.knn,"nn.index")[1] 
    
   mat=rbind(DATASET.train[2347,-58], DATASET.test[1,-58])
   
   D.e=dist(mat, method = "euclidean") # 3.97872 ok avec  attr(mod.knn,"nn.dist")[1]
   
   ## l'algo pour la ligne 1 du test, calcule toutes les distances euclidiennes avec le train 
   ## et retiens la plus faible distance
   
   #vérif plus faible distance euclidienne ?
   d.euclid=c()
   for(i in 1:dim(DATASET.train)[1]){
    mat=rbind(DATASET.train[i,-58], DATASET.test[1,-58])
    d.euclid[i] = dist(mat, method = "euclidean")
    
   }
   
   min(d.euclid) ; which.min(d.euclid)
## [1] 3.97872
## [1] 2347
  # [1] 3.97872 [1] 2347 => ok on retrouve la distance pour la ligne 1 du test et l'individu 2347 du train 
  # qui minimise cette distance  

A partir des ces distances Euclidienne, et en ne gardant que la distance minimum, nous obtenons bien le même individu et la même distance. Pour k=2, nous aurions eu deux voisins et deux distances.

Maintenant que le fonctionnement du modèle est clair, nous allons exécuter le modèle avec différentes valeurs de \(K\).



Nombre de classes k optimales

On peut se demander naturellement, quel est l’impact de \(K\) sur le taux d’erreur final ? J’ai pu trouver des informations sur le sujet (duda hart and stork (classif pattern)).

Nous allons générer des échantillons train/test de façon aléatoire et appliquer la fonction knn() en faisant varier le paramètre \(K\) de 1 à 10.

## 2 - Trouver un nombre de classes k optimal 
 
 # creation d'echantillons aleatoires a partir du fichier spam   
 # Analyse du nombre de classe qui minimise l'erreur sur test 
   # article relatif : duda hart and stork (classif pattern)
   
   
 
 res.prop.train.spam=c()
 res.prop.test.spam=c()
 err.knn = c() 
 min.err = c()
 
 for(i in 1:100){ #bouclage pour application train/test 
  #initialisation du seed 
  set.seed(1024+i)
  
  #generation de l'echantillon TRAIN
  #rappel proportion au global "email 61%" "spam 39%"
  
   trainIndex <- sample(index, trunc(length(index) * 0.666666666666667)) 
   DATASET.train <- Don_spam[trainIndex, ]
  
  #generation de l'echantillon TEST
   DATASET.test <- Don_spam[-trainIndex, ]
  
  ## Application KNN
   
  library(class)
  
  # bouclage pour k=1 a k=10 classes 
  K <- seq(1,10,by=1)
  err <- K
  ind <- 0
  for (j in K){
   ind <- ind+1
   mod_ppv <- knn(train=DATASET.train[,-58],test=DATASET.test[,-58],cl=DATASET.train$spam,
                  k=K[ind],prob = T) #mode clasification
   err[ind] <- sum(mod_ppv!=DATASET.test[,58])/nrow(DATASET.test)
   
   min.err = rbind(min.err, c(err[ind], K[ind]))
  }
  
  #l'erreur la plus faile est toujours pour k=1 ! 
  err.knn =  rbind(err.knn, summary(err)) 
  
  
 } #fin for 

   
 ### RESULTATS 
 
 # l'erreur moyenne sur les 100 tirages toutes classes confondues 
  
  tx_err.knn = mean(err.knn[,"Mean"])  # erreur moyenne 0.2087875

Pour 100 tirages aléatoires et \(K\) allant de 1 à 10 classes, nous obtenons 1000 taux d’erreurs. L’erreur moyenne globale est d’environ 21% pour toutes les valeurs de \(K\).

Prenons maintenant les meilleurs taux d’erreurs, prenons les 10% taux d’erreurs les plus faibles. Sur ces 100 taux d’erreurs, nous allons voir comment se répartissent le nombre de plus proches voisins \(K\).

library(ggplot2)
# nombre de classes minimisant l'erreur de prediction 
  
  #min.err[order(min.err[,1]),][1:100,] #pour chaque valeur de K, l'erreur de prediction associée (ici 1000 resultats k=10 * i=100)
  
  # [,1] [,2]
  # [1,] 0.1668840    1
  # [2,] 0.1720991    1
  # [3,] 0.1727510    1
  # [4,] 0.1740548    1
  # [5,] 0.1760104    1
  # [6,] 0.1760104    2
  # [7,] 0.1779661    1
  # [8,] 0.1779661    1
  # [9,] 0.1779661    1
  # [10,] 0.1779661    1
  
   # parmis les taux d'erreur minimum, k=1 semble être le meilleur parametre 
  
  # prenons les 100 plus faibles taux d'erreurs 
  # repartion des 100 plus faibles taux d'erreurs 
   #prop.table(table( min.err[order(min.err[,1]),][1:100,][,2]) ) 
  
    # 1    2    3    4    5    6    7    8    9   10 
    # 0.62 0.03 0.15 0.01 0.10 0.02 0.02 0.02 0.02 0.01 
    
    # k=1 est le plus représenté suivi par k=3 et k=5
  
  #représentation du taux d'erreur en fonction des classes
   #mise en évidence des classes 1,3 et 5
   
   #plot(min.err[order(min.err[,1]),][1:100,])
   
   k.choose = data.frame(min.err[order(min.err[,1]),][1:100,])
   
   ggplot( k.choose, aes(x=k.choose$X2, y=k.choose$X1) ) + 
    geom_point(size = ifelse(k.choose$X2 %in% c(1,3,5), 2, 1 ), 
               color=  ifelse(k.choose$X2 %in% c(1,3,5), "darkred", "darkblue" )) +
    ggtitle("Concentration des taux d'erreur") +
    ylab("taux erreur") + xlab("Nombre de classe k")+ 
    theme(plot.title = element_text(hjust = 0.5))

On constate que pour les 10% de taux d’erreurs les plus faibles, les classes k=1, k=3 et k=5 se distinguent des autres. Et on remarque aussi que k=1 semble le paramètre qui minimise le plus le taux d’erreur de prédiction du modèle.