Aller au contenu

k plus proches voisins

I. Découverte⚓︎

Mon info

  • Les algorithmes des k plus proches voisins est abrégé kppv en français. En anglais, k nearest neighbors souvent abrégé en knn.
  • L’algorithme des k plus proches voisins appartient à la famille des algorithmes d’apprentissage automatique (machine learning) qui constituent le poumon de l'intelligence artificielle actuellement.
  • Pour simplifier, l'apprentissage automatique part souvent de données (data) et essaye de dire quelque chose des données qui n'ont pas encore été vues : il s'agit de généraliser, de prédire.

banquise

Lancer l'animation sur la banquise. L'animal inconnu est-il un ours ou un phoque ?

La vie sur la banquise

II. Une première approche⚓︎

Des Pokémon

Après avoir téléchargé le fichier, vous pourrez le lire à partir de Basthon

🌐 TD à télécharger : Fichier knn_intro.ipynb : "Clic droit", puis "Enregistrer la cible du lien sous"

choix pokemon

Mon info

Ce problème, qui demande à prédire à quelle catégorie, ou classe, appartient ce nouvel élément donné, est appelé problème de classification. L'algorithme des k plus proches voisins permet de trouver les k voisins les plus proches (si k = 5 on cherche les 5 voisins les plus proches) de ce nouvel élément dans le but de lui associer une classe plausible (Psy ou Eau, dans cet exemple).

III. Principe de l'algorithme⚓︎

Que fait l'algorithme des k plus proches voisins ?

A partir d'un jeu de données (par exemple, les données sur nos 34 Pokémons) et d'une donnée cible (le nouveau Pokemon à classifier), l'algorithme des k plus proches voisins détermine les k données les plus proches de la cible. (si k = 3 les 3 plus proches voisins de la cible seront pris en compte, si k = 5 les 5, ...)

Les données

  • une table données de taille n contenant les données et leurs classes
  • une donnée cible : cible
  • un nombre k inférieur à n
  • une formule permettant de calculer la distance entre deux données

Résultat

un tableau contenant les k plus proches voisins de la donnée cible.

Algorithme

  • Créer une table distances_voisins contenant les éléments de la table données et leurs distances avec la donnée cible.
  • Trier les données de la table distances_voisins selon la distance croissante avec la donnée cible
  • Renvoyer les k premiers éléments de cette table triée.
Et notre prédiction alors ?

L'algorithmes des kppv en lui-même n'apporte pas la réponse à notre problème de classification puisqu'il ne fournit que les k plus proches voisins (et leurs classes) de notre donnée cible. Il reste donc une dernière étape pour prédire la classe de notre nouvel élément : pour cela, on choisit la classe majoritaire (la plus présente) dans les k plus proches voisins.

Influence de la valeur de k⚓︎

k impair

On est contents si k est impair car il ne peut pas y avoir d'ex-aequo.

Remarque

La valeur de k est très importante, elle doit être choisie judicieusement car elle a une influence forte sur la prédiction. Regardons le résultat de la prédiction pour différentes valeurs de k sur notre exemple.

k = 1

Si k = 1, cela revient à chercher la donnée la plus proche de notre élément cible.
Ici, on se rend compte que s la classe du plus proche élément est "Eau" (point bleu)
➜ on classerait le nouveau Pokémon comme étant de type "Eau".

k = 3

Si k = 3, on se rend compte que la classe majoritaire dans les 3 plus proches voisins est "Psy" (2 points rouges contre 1 point bleu) donc on classerait le Pokémon inconnu comme étant de type "Psy".

k = 9

La classe majoritaire parmi les 9 plus proches voisin est "Eau" (5 points bleus contre 4 points rouges) donc on classerait le Pokémon inconnu comme étant de type "Eau".

Remarque

Si on choisit k = 34 (le nombre total de données), alors la prédiction serait toujours "Psy" car c'est la classe majoritaire de l'échantillon. Il est donc incorrect de penser que plus la valeur de k augmente meilleure sera la prédiction, c'est plus complexe que cela. Il faudra observer les resultats.

Choix des distances⚓︎

L'algorithme des plus proches voisins repose sur la distance entre deux données. Dans les exemples vus précédemment, c'est la distance "naturelle" qui a été choisie (celle "à vol d'oiseau").

La distance euclidienne

Dans un repère orthonormé, si A et B de coordonnées \((x_{A},y_{A})\) et \((x_{B},y_{B})\), alors la distance entre ces deux points est donnée par la formule :

\(AB=\sqrt{(x_{B}-x_{A})^{2}+(y_{B}-y_{A})^{2}}\)

On parle alors de la distance euclidienne.

IV. 💻 A vous de jouer⚓︎

Dans le TP du II. Nous avons obtenu la figure suivante :

Nous allons mettre en oeuvre l'algorithme knn pour prédire le type des trois Pokémon représentés en vert, qui sont inconnus. Ce sont des "cibles"

Nous avions :

La liste de dictionnaires pokemons

🐍 Script Python
pokemons = [{'Attaque': 105, 'Nom': 'Aligatueur', 'Points de vie': 85, 'Type': 'Eau'},  
{'Attaque': 92, 'Nom': 'Bargantua', 'Points de vie': 70, 'Type': 'Eau'},  
{'Attaque': 63, 'Nom': 'Carabaffe', 'Points de vie': 59, 'Type': 'Eau'},  
... ] 

Nous recherchons les types de :

  • cible_1 : points de vie : 65 et valeur d'attaque : 25
  • cible_2 : points de vie : 75 et valeur d'attaque : 80
  • cible_3 : points de vie : 95 et valeur d'attaque : 125
Mettre en œuvre KNN
  • La liste de dictionnaires pokemons est déjà implémentée, mais cachée pour ne pas alourdir l'exercice.

  • cible est un dictionnaire représentant un pokémon dont on cherche à déterminer le type.

Par exemple cible_1 = {'Attaque': 25, 'Points de vie' : 65}

  • La fonction cree_liste prend en paramètre la liste pokemons et le dictionnaire cible.
    Elle renvoie la liste des listes composées du nom des Pokémon, de leur distance à la cible, et de leur type.

Par exemple

🐍 Console Python
>>> cree_liste(pokemons, cible_1)
[['Aligatueur', 'Eau', 82.46211251235322],
['Bargantua', 'Eau', 67.1863081289633],
 ... ]
  • La fonction cree_liste_triee prend en paramètre la liste pokemons et le dictionnaire cible.
    Elle renvoie la liste des listes composées du nom des Pokémon, de leur type et de leur distance à la cible triée par ordre croissant des distances.

Par exemple

🐍 Console Python
>>> cree_liste_triee(pokemons, cible_1)
[['Spoink', 'Psy', 5.0],
['Munna', 'Psy', 11.0],
 ...]
  • La fonction knn prend en paramètre la liste pokemons et le dictionnaire cible. Elle renvoie la liste des k premiers éléments de la liste créée par la fonction cree_liste_triee

👉 on peut utiliser les syntaxes de la fonction sorted de Python vues dans le chapitre des algorithmes gloutons.

Compléter le script ci-dessous

###(Dés-)Active le code après la ligne # Tests (insensible à la casse)
(Ctrl+I)
Entrer ou sortir du mode "deux colonnes"
(Ctrl+Clic pour inverser les colonnes)
Entrer ou sortir du mode "plein écran"
Tronquer ou non le feedback dans les terminaux (sortie standard & stacktrace / relancer le code pour appliquer)
Si activé, le texte copié dans le terminal est joint sur une seule ligne avant d'être copié dans le presse-papier
Évaluations restantes : 5/5

.1280130ldTy1,4*-A]k/weibmc:_35qaPr+ 7=9o[fgt28;6sSh)(pNunv050d0q0M0A0r0c0R0E0u0c0A0R0R0G010M0r0W010406050R0Y0t0t0A0C0f040S0I0c0Y0^0I0Z050o0 1113150}0W04051l1e1o0o1l0}0d0r0!0-0/0;0?0T0r0L0T0c1C0T0M0{050(0s0c0q1x0:0=011B1D1F1D0M1L1N1J0M0C1m0M0T0-180R0W0A0Z0?0N011P1z010K0*0q0Z0A0t0q1J1,1.1?1R1_1N1|1~0{0a0E0B0C0I0W0I0R0r1b0Z0E0$1*0C0C0q0u2j1e210Z1m0o1(2w1#1%1$1K0d230?1F0Z1{2g1J1u1w0.1Q2G0r2I0Z0I2M1J0W2p1m2u2w2!0~1-2k2O1@2T0C120c0{0E0g2t2(0|2%222*1R2,2.2:0N2?1.2^2u2F012}0A2/040E0x312v0}342{0?37390E0i3d332(353j2:0y3n3f3p3h360I2-382:0Q3u2_2)1y2|3z2~3a0F3E3g3H3i3J3B3a0O3N3w3P3y3A3k0H3V2`3X3r040g0b3$3G2P3Y3K0g2=1f2@3v3%3/3)0g303@323_3.2+3R390g3c3 3e3F3q440{0g3m483o3`433Z4d3t4g414b4k3*3D4n4a3x3|3M4t3O3{4c3*3U4y3W4A4q0g3#4E4i3I4q0N3,4K424M3K0N3?2!4o4v4B0N3~2$1r2Y1e2M2z0d1%2E3x0u2U1 1m4*1n4(4$2$4:0$2Z4F1@0n0Z0{0K2d0t3n0E4u3(5204121(57593/510{0r0t2f0C0M5f4z2+0{0R0z5n3u4X3X0n0{0$0K5p4 2|0K5A0r0R0(0Z0u0q3n5g1@0`040V5O5q2|0{2f0n0q0t1c5U5E0?5R0h5D4L3i0{0u0r1M5N4g5P1R5R0U0v3u0E5~585V0?5z042p0M0Y0C1d4g605(365s5u5o5@61015R0V5T6h6c5b5:5=5%5-6j0{0J6s4R0?0R0g0{02030x0H0P0B0I2R0M0,2m0!0r0q6D6F0P6x355R0m6U3x0I0{0k6Y3(5X0I5Z5#692$6i5R6w6n6t6A6C6E6G6I6K6M1O6O6Q6_6T6=6y6u040m0U6%3/6!040j0j791@0t0r0{4#2@0E065 6b6t7b0D5,746k7f5W046q1N7w5)6v7B016@046R6G0l1X0A0z0Y706S7E6W7E7b6$733q6)6+5$7W3x6:7E7G7I0P7K0(7N7P6G7R0{777T0{7d7E7h7j7;04784n5x5h5A0q5C6a5^3i5G7y2p0q0w1F5J5?6.6c7v7#6(045Y5!1c0R7}5+866i6p5;7A8k3/5`5|4n7o87016365676-7l8E6:6;8h6t5b8n6,7}8N2@8E7)710X0I0t7*7}0m8s2!7p748Q6*8o8J328L7D8y1@8X6S0e0f0W7/728O7u7=8)8K8u5H5J1.5M7}6m907X8m8.8S8@5_0{93328+9d7z8g8V6/0{0U6X8C8D6i630K3z7t9d8R7!8*8E0I0p5j8:2v9m4v7Y8/8q9h7C765w8E635B9B4v890L0%0w1u975L9p8;9r5S7E5b0d1c2I9+2v8=7~8B4W9w6c8G0%8I9Y8l9;0Z9?8T7`7i047k9,8i7=9U9x83859F8u890u8b8d5I0M8c1#6P9@4~6t8j9c9N9e7Z0Z9Qay3X5*a23{5/8wau9_5{5}5 8E9:ap980q0R0w9oaD946c7b0GaH5r8a0q8c8eaq9a9/9O6,aYacaw9ja%7x9o7}7 9|aP95049(5K5MaVaX0wasa*a_0?a#ba360s5s3zaq0da.9R6db1aS9*b5aKa=9^9-9k9LaQ0{5Z0f7@04a$bl5b9#0M9%bo99bl5`aO5~9V0{8H68bdaR9)b4aWbrb70Cataf9~ahbT89519Kav919.bEa:8p8rbTaJ6rbLa^8t6obya|9{3^9}8P0{0!6J2haCbZ6Pbs3a9G0{bDajb}a)a+aparb!b9b`b/aEaIaA9Pb?b|c37yaKa|bN9M8l2p0 0c0(6gcg7qcebd7%b:04c55I2RaVb8cb9_8Ua?8,5j8%bd9y9AcvcY040rbd9H9JbTbf641.9#bkcqa(0na|9ua~bOag64a0bSc(9dcD0YcF0AcH3^1e4|0q2w2Xdd4)1v4+2z2C2x0A5=2w4*0}0o0$0(0*0R04.
Solution
🐍 Script Python
from math import sqrt

def distance(pokemon, cible):
    return sqrt((cible['Points de vie']-pokemon['Points de vie'])**2 + (cible['Attaque']-pokemon['Attaque'])**2)

def cree_liste(pokemons, cible):
    return [[pokemon['Nom'], pokemon['Type'], distance(pokemon, cible)] for pokemon in pokemons]

def get_distance(donnee):
    return donnee[2]

def cree_liste_triee(pokemons, cible):
    distances_cibles = cree_liste(pokemons, cible)
    distances_cibles_triee = sorted(distances_cibles, key=get_distance)
    return distances_cibles_triee

def knn(pokemons, cible, k):
    voisins_tries = cree_liste_triee(pokemons, cible)
    resultat = [voisins_tries[i] for i in range(k)]
    return resultat