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"
(Alt+: ; Ctrl pour inverser les colonnes)
Entrer ou sortir du mode "plein écran"
(Esc)
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

.128013(qd]0h75v/;[-6kgSw2)_8m9N=lt4:eb,+Tcpa3u1roisnAfy*P 050d0F0C0M0S0B0T0!0K0B0M0T0T0A010C0S0L010406050T0O0x0x0M0Q0X040r0R0B0O0^0R0U050k0 1113150}0L04051l1e1o0k1l0}0d0S0j0-0/0;0?0g0S0q0g0B1C0g0C0{050(0G0B0F1x0:0=011B1D1F1D0C1L1N1J0C0Q1m0C0g0-180T0L0M0U0?0t011P1z010W0*0F0U0M0x0F1J1,1.1?1R1_1N1|1~0{0a0!0Z0Q0R0L0R0T0S1b0U0!0$1*0Q0Q0F0K2j1e210U1m0k1(2w1#1%1$1K0d230?1F0U1{2g1J1u1w0.1Q2G0S2I0U0R2M1J0L2p1m2u2w2!0~1-2k2O1@2T0Q120B0{0!0P2t2(0|2%222*1R2,2.2:0t2?1.2^2u2F012}0M2/040!0N312v0}342{0?37390!0D3d332(353j2:0i3n3f3p3h360R2-382:0o3u2_2)1y2|3z2~3a0h3E3g3H3i3J3B3a0w3N3w3P3y3A3k0y3V2`3X3r040P0f3$3G2P3Y3K0P2=1f2@3v3%3/3)0P303@323_3.2+3R390P3c3 3e3F3q440{0P3m483o3`433Z4d3t4g414b4k3*3D4n4a3x3|3M4t3O3{4c3*3U4y3W4A4q0P3#4E4i3I4q0t3,4K424M3K0t3?2!4o4v4B0t3~2$1r2Y1e2M2z0d1%2E3x0K2U1 1m4*1n4(4$2$4:0$2Z4F1@0p0U0{0W2d0x3n4u3X0s2:574z2+5204121(5c4 1R5a3a5j4L0?510{0S0x2f0Q0C3n0!583{0{0T0c5w3u4X3X0p0{0$0W5o4R0?5m5z4g5A2+0W5K0S0T0(0U0K0F5N350`040b5%4v0{2f0p0F0x1c5,3X5)0H5y5T2|0{0K0S1M5$5S5d1R5)0u0E3u0!6a5z645q0{2p0C0O0Q1d4g6c5k3i5C5E5x636n015)0b5+6s5p365~601N5@3/5)0m6E1@0T0P0{02030N0y0l0Z0R2R0C0,2m0j0S0F6N6P0l6I650{0e6(0?0R0{0n6,6A045/5;5?6y5O6u0{6H6`356K6M6O6Q6S6U6W1O6Y6!736%6 3x5)0e0u6;6.040Y0Y6;0x0S0{4#2@0!066b6m6z7j0I5{6d6|5*6;5f5 616;6G6;71046#6Q0V1X0M0c0O7a6$7I6*7i6/7E5.0R5:5=6k2$7B7J7d3X7L7N0l7P0(7S7U6Q7W047g7Y7k7m7-3/7o7q7`7h4n5H3/5J045L6;5Q7E5V040K2p0F0v1F5Y627*6t6v7!6?7$6^0U0T7`5`6l5|6o8g6C8n2@8A7C67696b8G896g6i7)7s8G6G6~8o6z5f6@7(7`8T8F7B7/7b0z0R0x7:7`0e8y2!7w6{8W8t8Y801@7,8U6{8%6$0J0X0L7^7c8|5(6*8/8Q7B5f1u5Y1.5#7`6x955-8s7%6_9i5^0{98328;3q6B7H8_6)040u6+4n7v9s3x890W3z7A6t8?9l8P9r8G0R5m2R9I8V7#9L8w9w0?7f5G8L5K0F5M9X018d9)0U8f0q0%0v9c5Z9f9)8q9,5K1c2I8E328R0{8I9B8K7B8M0%8O9S8=9{0U9}8Z7n7p047r9 7+7X869#8a9%8c5b9,8f8h0F8j8l0C8j1#6Z9~2va07D9`9k8u9W9n6F9pa99t8C9vaK8`a168a36a8G9b5X9?0F0T0v7G1NaJ996t7j0AaN9jauawaZaC4~6z9_aR5}aH7(a*aj8paM8z9a9u6D9^a18JaWb48aaZ9ea#a%8Da$aAava/3Xa-bl3{0G5C3zay0d9g8r9=bea$a(a#8xbo2+0{5:0X7}a.aG9/0C9;bd5!a@aE854W7van8N6jbDa|bxbPbzbh0vbja@0687509$9(a{5Parb:368f519MaDakaFb?9KaIbCb39Jb5bQb|9q2v9D3(bF84aUbTa4c3040j6T2h8vb(0Q6Za c89O0{bJ8:aX5~8i8ka?cmaBbvaG8X1ccpa^6{5_bY8BbA84b9c95B042p0 0B0(6rcu7Bbnc2a_6}8rci5X2RbicnbBb7048!b09T040S8-cK019F9Hc!aac?c_9P5sb`5n9abqcR1.9/cCb~cbc.9zcObVa7bXc}aOcS0OcU0McW3^1e4|0F2w2Xdt4)1v4+2z2C2x0M612w4*0}0k0$0(0*0T04.
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