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

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

Python Console Session
>>> 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

Python Console Session
>>> 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

.128013qS;3e0yk,(rt16h2A8n+o/gb=][m-*dc:Tliw u9P57pfaN4)vs_050F0f0m0U0K0J0Z0M0G0J0U0Z0Z0z010m0K0S010406050Z0N0C0C0U0l0h040c0v0J0N0^0v0t050w0 1113150}0S04051l1e1o0w1l0}0F0K0Y0-0/0;0?0/0t0x0N0U0x0f0D0S0h0m0p1c0M0p0K0x0p0J1Q0p0m0{050(0y0J0f1x0:0=011P1R1T1R0m1Z1#1X0m0l1m1L0-180Z0S0U0t0?0q011%1z010T0*0f0t0U0C0f1X1|1~231)261#292b0{0a0M0P0l0v0S0v0Z0K1b0t0M0$1`0l0l0f0G2w1e2e0t1m0w1L2J1?1^1@1Y0F2g1A0K0t282t1X1u1w0.1(2T2V0t0v2Z1X0S2C1m2H2J2:0~1}2x2#242)0l120J1X0U1O2C0T0?030!0!0G2*0f1T2(0v0D0e0D0n0{0M0n1e0U2;2@0|2?2f2_1)2{2}2 310f33013537393b2W3e0D21040M0q3l3n1~3p2H2S013u0U2~1m300p323436380$3E2)3G0e3i0e3M2G3o0}3Q3s0?3T3V053X3Z3A3#3D2U3F3f0W3i0W3.1f3:3q2^1y3t0v2|3U3w3Y3y3!3C3%403)3f0Q3i0Q462:3;2@3R3^4g3|3B3$3a4m3d3f0o3i0o4s483=4b3@4d3v3W3x3z4A3 3c3G0R3i0R4J3O4u3r4M3S4O4f4Q4h4S3~4l4V3f0s3i0s4!2I4$4a2$4)4e3_3{4i3}4k4C4;0D0O3i0O4_3P4v3?4~4P3`4R4j4B3(4E3g0g0{0n0g5b4{4w4*505i535k4D3G0n3h045C5s495u4 4y524T4:413g3I0n3L0w3m3/4#5H5e4x4,4z4/555O0n3+5E3-5T3N4`5X4(5Z5h4-5j4U5(435E455-5V5/4L4}5=514.545l5B4p5E4r5~475W612`5v5K655z560n4G5E4I6c4t5:626h5!5L5$673f0n4X5E4Z6q4K5d5;6u5?5#665A6z4?5E4^6E6e6G6t5J6v6j5_4n3g585E5a6R606T6g6V6J6w6L560q5o046;5G6f4c6,645^5N6Z0q5D706^6*6`5g6|5y6Y5m0q3I7b5b1p2.1e2Z2M0F1^2R5e4B2Y1v1m2-0f2/3o5 1m4B7u2f0K0F0?362H5B3w7B7D6/5(222k0f7J6k7L2J5U6_0?0i0t0{0T2q0C7w6s240L3i7$7U3S7X04121L7+740?7)3J7=4%4}7W0{0K0C2s0l0m7w0M7%3t0{0Z0b827w0}6d2I5H7I017E2@3G3I5h8h6x6M3H7M2a7O8i7K6 1X5-8d2=3Q8o0!7F3f5*8n7C8v7Q6Z3+0M7N7P793*8y7T7?010i0{0$0T7`4|7(7*8e7z8$3t0T8Y0K0Z0(0t0G0f8#3R0`040k8^5Y0{2s0i0f0C1c8}4(8`0j84863@0{0G0K1!8@8)9a018`0X0H8c8^8D8F0D5{8I8Q6~5m438O8t9u5%6Z9s5-0M9F857,8X042C0m0N0l1d8)9H8V7.898b9h7,8`0k8|9W9S9c9e1#954}8`0B9*240Z5D02030e0O0d0P0v2U0m0,2z0Y0K0f9=9@0d9.1)8`0Aa60?0v0{0Daa3S8 0v91939P8B8V9,af9:0{a39^9`9|9~1$a0a29?9^afa80Xafac040E0Eaf0C0K0{5S2:0M8A7v8C8J8j1~3G699t8K8R4o8s2b9A6y0DaY9E9G9iaG0u999X0{9!am7{2`9%9faC0{9-9#a`1)aq04as0d0r1/0U0b0Naza4a~04a9b18+abadaf7.909294bi8_a ap9;aAb7b9bbbdaBbr5eaDaF0{aIaKaM6?bfaE8)aR3O8gaU8E8k4F7HbR8L5m4G9ya(a!9v3G6n3MbO8faT7J9q6BaZ8p564Xb!8ub=5Ob:5~9I8Y3yaf7^85bC5;8-040G2C0f0!1T8:9ga_bj9ja@bmahaj1c0Zbf989Q9i7.9da}c49+0{9l9nbr9pbT0D6Ob;8w5m4?b^a)8qcEa-9F9i9J9L9Nal3o9Rb20?9,b0cf4wckbpcTbPa?04cZaS8Vb4b60V0v0Cb6bf0AcpaPcrc$akbfc,c)c.bva40I0h0SbAa5cv24a8c`cUc|041u8:1~8?bfa^c-cWag04boc~daa70{dd3OcVcgcs9(cedncg9kbh6rcAbR9q6#cFbX3G58cJb$9B5mdLcNcOb~040T4da=9$dqaic%d#do0v7^2Ud*dzc}cmc^czc!cBaW6z6=dMa#5na%b_cG5Bd|8z9odJcC5CbVcK6l3hdQb`6Ze97S8*3R9J8Zc18(c!5Yc61G0m0!dh8;dkdtcXcieydp0F1c2VdCd1do9k9mbNe6b.e88m308DdN6z21eee2eT8T3Ja.dXcR9Od/c#dgeE0feGb,anbteBaLaNd?eLdIeNd`3g8HeQbWd~5)e0eb5(8He5e_8v9q0n9se~f3eg9x8PdRa*faeYcPb 8!eBc2bmc6c8e,cb8/0mca1?a1e-ejbDeAep5;d;0tcneB97e(8~c7dBbLeKdHd^e7e{0naYfcfh8qfVf2fY6la,3m9Gdye)evdj0f0Z37dBfIc{7,aG0zfLfFc7c9fucddlcjd%clfHcof{62a|9)fJcxd@dD0Md_0t5Bb)fXef5m6mf#glgieYf*dff-8=f/f;9ff:fye,g724f_gC3t0y884dfw0Fg1eB7.gu8?f:ct1#f?gefC04dw2If+fM910hbF04f`gN0{eseufvf.bLgdeH2xgg5Bb:gkeW3gb@fggp6zb|f)e!8VcQ0%cSgF9bdgg/gvgRf=0!gAfAb+ejg^6Neaf$5(cIg g|0ncM3mhjbQe`gh6zdLg{eS6!gohsdU8Udoelc0foeogVc50{7Wc(e.eIfDhOg8g3c%gUg?bsgXh9dpgSfA9ifKcq7,7.0ifPg=hTg@fThz3Hd|hCd~6;hFhDh eigsh/0{0Y9{2ufHhg0la1h!gZa/0{g*f@d$fscaccfwibfzgMfEhXdrd=gbh%h.ikfOiwbMfRgehl3H5Dd}b%3f70i0h~iHdVdf2C0 0J0(83iyd+ihh(aog+04i78/2Ugzicf/c g20Ke@ijhJ7Yd!iWd:04i/i^3Rd,7~hS7_h/gH9K1~1GirhWa{04h;iBdG48f7aVh`7bhnh03HeUhri18miPe#h7e%i|fMiR0NiT0UiViDbP0w7y7f7t7h7q1e0m7kjJ2P2K0U9f2J7i8d0$0(0*0Z04.
Solution
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