k plus proches voisins

I. Présentation générale :

L'algorithme des "k plus proches voisins", "k neirest neighbours" en anglais ou KNN en abrégé, est très utilisé en apprentissage automatique, ou machine learning.

L'idée est assez simple :

  • on considère une population d'individus de différentes catégories dont on a mesuré un ou plusieurs caractères
  • on cherche à classer dans une catégorie un individu dont on ne connapit que la valeur des caractères
  • pour ce faire on calculs la distance entre cet individu et l'ensemble des individus de la population connue
  • on retient les k individus de la population les plus proches de notre inconnu
  • on classe l'inconnu dansla catégorie la plus représentée parmi ces plus proches voisins.

L'illustration ci-dessous devrait clarifier les choses (source : https://en.wikipedia.org/wiki/K-nearest_neighbors_algorithm#/media/File:KnnClassification.svg) :

KnnClassification.svg

  • La population connue est constituée de points bleux et rouges. Ce sont les deux catégories.
  • Chaque point est caractérisé par son abscisse et son ordonnée
  • On cherche à classer le point vert
  • On calcule la distance entre le point vert et chacun des autres points
  • Si l'on ne retient que les $3$ plus proches voisins alors notre inconnu sera classé parmi les "rouges"
  • Si l'on retient les $5$ plus proches, il sera parmi les "bleus"

Comment calculer a distance ? Il existe plusieurs techniques possibles...

La plus classique est la distance euclidienne, c'est celle du cours de mathématiques :

  • en deux dimensons : $$ d(A,B) = \sqrt{(x_A-x_B)^2+(y_A-y_B)^2}$$
  • en $n$ dimensons (!) :
    on pose $A\:(a_1,a_2,\dots,a_n)$ et $B\:(b_1,b_2,\dots,b_n)$
    $$ d(A,B) = \sqrt{(a_1-b_1)^2+(a_2-b_2)^2 + \dots + (a_n-b_n)^2}$$

Ecrivons tout de suite notre fonction distance :

In [8]:
from math import sqrt

def distance(A, B) :
    """
    Calcule la distance euclidienne entre deux points A et B présentés sous forme de listes de nombres
    A et B sont des liste de nombres de même taille
    Renvoie la distance euclidienne entre ces points
    """
    assert len(A) == len(B), ValueError("Les points n'ont pas le même nombre de coordonnées (A : %s et B : %s)" % (len(A), len(B)))
    
    somme = 0
    
    for i in range(len(A)) :
        somme += (A[i] - B[i])**2
    
    return sqrt(somme)
In [7]:
# Essai avec deux coordonnées
distance([1,2],[4,3])
Out[7]:
3.1622776601683795
In [8]:
# Essai avec cinq coordonnées
distance([1,5,9,-8,4], [3,6,-9,-7,1])
Out[8]:
18.411952639521967

II. Application :

L'exemple archi-classique de KNN avec python est celui-des iris (https://pixees.fr/informatiquelycee/n_site/nsi_prem_knn.html). Essayons-en un autre.

II.1 Prépration des données :

Nous allons utiliser des données de botaniques receuillies par Pedro F. B. Silva. Dans sa thès de 2013, ce chercheur a utilisé KNN afin de déterminer l'espèce d'une plante. Il a pour cela mesuré $14$ attributs sur chacune des $340$ feuilles prélevées parmi $40$ espèces.

Lire à ce propos le readMe.pdf dans le dossier leaf.

Importons les données :

In [1]:
import csv
csvfile = open ("./leaf/leaf.csv", "r")
lines = csv.reader(csvfile)
dataset = list (lines)
# On n'affiche que les 10 premières lignes
print(dataset[:10])
[['1', '1', '0.72694', '1.4742', '0.32396', '0.98535', '1', '0.83592', '0.0046566', '0.0039465', '0.04779', '0.12795', '0.016108', '0.0052323', '0.00027477', '1.1756'], ['1', '2', '0.74173', '1.5257', '0.36116', '0.98152', '0.99825', '0.79867', '0.0052423', '0.0050016', '0.02416', '0.090476', '0.0081195', '0.002708', '7.4846e-05', '0.69659'], ['1', '3', '0.76722', '1.5725', '0.38998', '0.97755', '1', '0.80812', '0.0074573', '0.010121', '0.011897', '0.057445', '0.0032891', '0.00092068', '3.7886e-05', '0.44348'], ['1', '4', '0.73797', '1.4597', '0.35376', '0.97566', '1', '0.81697', '0.0068768', '0.0086068', '0.01595', '0.065491', '0.0042707', '0.0011544', '6.6272e-05', '0.58785'], ['1', '5', '0.82301', '1.7707', '0.44462', '0.97698', '1', '0.75493', '0.007428', '0.010042', '0.0079379', '0.045339', '0.0020514', '0.00055986', '2.3504e-05', '0.34214'], ['1', '6', '0.72997', '1.4892', '0.34284', '0.98755', '1', '0.84482', '0.0049451', '0.0044506', '0.010487', '0.058528', '0.0034138', '0.0011248', '2.4798e-05', '0.34068'], ['1', '7', '0.82063', '1.7529', '0.44458', '0.97964', '0.99649', '0.7677', '0.0059279', '0.0063954', '0.018375', '0.080587', '0.0064523', '0.0022713', '4.1495e-05', '0.53904'], ['1', '8', '0.77982', '1.6215', '0.39222', '0.98512', '0.99825', '0.80816', '0.0050987', '0.0047314', '0.024875', '0.089686', '0.0079794', '0.0024664', '0.00014676', '0.66975'], ['1', '9', '0.83089', '1.8199', '0.45693', '0.9824', '1', '0.77106', '0.0060055', '0.006564', '0.0072447', '0.040616', '0.0016469', '0.00038812', '3.2863e-05', '0.33696'], ['1', '10', '0.90631', '2.3906', '0.58336', '0.97683', '0.99825', '0.66419', '0.0084019', '0.012848', '0.0070096', '0.042347', '0.0017901', '0.00045889', '2.8251e-05', '0.28082']]

Organisons cela (en convertissant les données numériques au format souhaité):

In [45]:
# On convertit en réel
for ind in dataset:
    ind[1] = int(ind[1])
    for i in range (2,len(ind)):
        ind[i] = float(ind[i])
print(dataset[0])
['1', 1, 0.72694, 1.4742, 0.32396, 0.98535, 1.0, 0.83592, 0.0046566, 0.0039465, 0.04779, 0.12795, 0.016108, 0.0052323, 0.00027477, 1.1756]

Pour les besoins ce ce cours, on extrait $3$ valeurs au hasard afin de constituer nos individus "mystères" :

In [61]:
from random import randint

individus_mysteres = []

for i in range(3) :
    indice = randint(0, len(dataset)-1)
    
    individus_mysteres.append(dataset.pop(indice))

print(individus_mysteres)
[['10', 1, 0.44941, 1.1374, 0.38458, 0.90736, 0.97368, 0.54595, 0.047809, 0.41601, 0.11385, 0.18608, 0.033467, 0.0082395, 0.0012682, 2.2422], ['7', 2, 0.83755, 1.5634, 0.53329, 0.76697, 0.95088, 0.27825, 0.039592, 0.28528, 0.023152, 0.087838, 0.0076564, 0.0028187, 5.6309e-05, 0.8079], ['24', 13, 0.64897, 1.4597, 0.3304, 0.97406, 1.0, 0.75323, 0.016021, 0.046715, 0.012025, 0.050266, 0.0025203, 0.00051116, 0.00013299, 0.49261]]

L'analyse en composante principale est une méthode statistique permettant de ramener un espace en $n$ dimensions (ici $14$) en un espace à $2$ dimensions sans perdre trop d'informations. Cela permet de visualiser nos données. Les couleurs correspondent aux espèces.

ACP_Leaf.png

On se rend compte que des feuilles de la même espèce sont globalement dans la même région du graphique.

II.2 Traitement :

La méthode a été décrite plus haut : passons directement au code en python :

In [62]:
def knn(population, individu, k) :
    """
    Classifie l'individu proposé dans l'une des catégorie représentée dans la liste population en fonction de ses k plus prhces voisins
    population est une liste d'individus représentés des listes à n+2 valeurs
    (la catégorie en chaîne de caractèr, le numéro de spécimen en entier et n coordonnées numériques )
    individu est l'individu à classer. C'est une liste de n coordonnées numériques
    k est le nombre de voisins à prendre en compte
    Renvoie la catégorie dans laquelle on place l'individu
    """
    
    # cette liste contient la distance de chaque individu 
    # de l'échantillon à l'individu inconnu et sa catégorie
    liste_distances = []
    
    for i in range(len(population)) :
        liste_distances.append((distance(population[i][2:], individu), population[i][0]))
    
    # tri du tableau selon les distances
    liste_distances = sorted(liste_distances, key= lambda x : x[0])
    
    #on retient les k premiers
    k_plus_proches = []
    for i in range(k) :
        k_plus_proches.append(liste_distances[i][1])
    
    # On compte les occurences de chaque catégorie parmi ces voisins à l'aide d'un dictonnaire
    dico = {}
    for n in k_plus_proches :
        if n in dico.keys() :
            dico[n] += 1
        else :
            dico[n] = 1
    
    #On extrait la catégorie la plus représentée
    categorie = max(dico.items(), key=lambda x: x[1])[0]
    
    return categorie

On teste avec un individu mystère

In [63]:
knn(dataset, individus_mysteres[0][2:], 3)
Out[63]:
'10'

On vérifie :

In [49]:
print(individus_mysteres[0][0])
28

Avec tous les individus mystères :

In [73]:
nb_erreurs = 0 
for i in range(len(individus_mysteres)) :
    prediction = knn(dataset, individus_mysteres[i][2:], 2)
    print("Prédiction : %s // Attendu : %s" % (prediction, individus_mysteres[i][0]))
    if prediction != individus_mysteres[i][0] :
        nb_erreurs += 1
print("Pourcentage d'erreur : %s" % str(nb_erreurs/len(individus_mysteres)))
Prédiction : 10 // Attendu : 10
Prédiction : 7 // Attendu : 7
Prédiction : 1 // Attendu : 24
Pourcentage d'erreur : 0.3333333333333333

Et si l'on augmente le nombre de voisins pris en compte ?

In [74]:
nb_erreurs = 0 
for i in range(len(individus_mysteres)) :
    prediction = knn(dataset, individus_mysteres[i][2:], 5)
    print("Prédiction : %s // Attendu : %s" % (prediction, individus_mysteres[i][0]))
    if prediction != individus_mysteres[i][0] :
        nb_erreurs += 1
print("Pourcentage d'erreur : %s" % str(nb_erreurs/len(individus_mysteres)))
Prédiction : 10 // Attendu : 10
Prédiction : 5 // Attendu : 7
Prédiction : 1 // Attendu : 24
Pourcentage d'erreur : 0.6666666666666666
In [ ]: