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.
    Si vous voulez la télécharger pour travailler sur votre propre éditeur python, vous pouvez la télécharger ici : Fichier pokemons.py : "Clic droit", puis "Enregistrer la cible du lien sous"

  • 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 type, et de leur distance à la cible.

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"
(Shift+Esc ; 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

.128013cP*w-7 sSfbgn3T8k9+]vei;(hN4l,mdo0qpA65_:2)=tay1/[ur050G0w0T0U0x0D0i0h0b0D0U0i0i0S010T0x0K010406050i0Z0F0F0U0!0V040j0H0D0Z0^0H0n050X0 1113150}0K04051l1e1o0X1l0}0G0x0v0-0/0;0?0/0n0m0Z0U0m0w0f0K0V0T0A1c0h0A0x0m0A0D1Q0A0T0{050(0l0D0w1x0:0=011P1R1T1R0T1Z1#1X0T0!1m1L0-180i0K0U0n0?0Q011%1z010k0*0w0n0U0F0w1X1|1~231)261#292b0{0a0h0c0!0H0K0H0i0x1b0n0h0$1`0!0!0w0b2w1e2e0n1m0X1L2J1?1^1@1Y0G2g1A0x0n282t1X1u1w0.1(2T2V0n0H2Z1X0K2C1m2H2J2:0~1}2x2#242)0!120D1X0U1O2C0k0?030O0O0b2*0w1T2(0H0f0o0f0W0{0h0W1e0U2;2@0|2?2f2_1)2{2}2 310w33013537393b2W3e0f21040h0Q3l3n1~3p2H2S013u0U2~1m300A323436380$3E2)3G0o3i0o3M2G3o0}3Q3s0?3T3V053X3Z3A3#3D2U3F3f0C3i0C3.1f3:3q2^1y3t0H2|3U3w3Y3y3!3C3%403)3f0N3i0N462:3;2@3R3^4g3|3B3$3a4m3d3f0M3i0M4s483=4b3@4d3v3W3x3z4A3 3c3G0g3i0g4J3O4u3r4M3S4O4f4Q4h4S3~4l4V3f0q3i0q4!2I4$4a2$4)4e3_3{4i3}4k4C4;0f0s3i0s4_3P4v3?4~4P3`4R4j4B3(4E3g0I0{0W0I5b4{4w4*505i535k4D3G0W3h045C5s495u4 4y524T4:413g3I0W3L0X3m3/4#5H5e4x4,4z4/555O0W3+5E3-5T3N4`5X4(5Z5h4-5j4U5(435E455-5V5/4L4}5=514.545l5B4p5E4r5~475W612`5v5K655z560W4G5E4I6c4t5:626h5!5L5$673f0W4X5E4Z6q4K5d5;6u5?5#665A6z4?5E4^6E6e6G6t5J6v6j5_4n3g585E5a6R606T6g6V6J6w6L560Q5o046;5G6f4c6,645^5N6Z0Q5D706^6*6`5g6|5y6Y5m0Q3I7b5b1p2.1e2Z2M0G1^2R5e4B2Y1v1m2-0w2/3o5 1m4B7u2f0x0G0?362H5B3w7B7D6/5(222k0w7J6k7L2J5U6_0?0r0n0{0k2q0F7w0h6s2`7X04121L7$7(1)7W0{0x0F2s0!0T7.7U3S0{0i0J7_7w0}6d2I5H7I017E2@3G3I5h876x6M3H7M2a7O887K6 1X5-832=3Q8e0O7F3f5*8d7C8l7Q6Z3+0h7N7P793*8o7T747V0{0$0k7{8L3S0k8N0x0i0(0n0b0w7w7/0?0`040z8#7|7*2s0r0w0F1c8+8R8(0E8Q4%620{0b0x1!8!847z4|248(0R0P828?7A8z891~3G5{8y8G6~5m438E8j9i5%6Z9g5-0h9t7%7|0r0{2C0T0Z0!1d929v8R7*7 81928$018(0z8*9K8,8}8 1#9a941)8(0Y9V3R0i5D02030o0s0y0c0H2U0T0,2z0v0x0w9(9*0y9!5e8(0u9|4(0H0{0fa08|048.8:8=9Q8@0{9Zab8{249$0{9_9+9-9/9;1$9?9^9)9+a5950{0u0Rau1)a2040d0daz0?0F0x0{5S2:0h8q7v8s9c8u8a4o7HaQ8B5m4p9m2b9o6y0f693M9u9FagaA0{0t8`9W8%0{9P8r9G9S90aF9Mada|ai04ak0y0L1/0U0J0Zar9`a|9~a|aBa4afa;7}a70H8/8;9Da^a,a=04aebobhb0b2b40(b7b9atbg3R9~aybC5eaBaDa|aHaJbb0{bF6rbC8t8v0f6n9h8A8H4F8ia!bX9j3GbV8p9!bSaS0f6BbW8f564XaZ8kb;5Ob/5~9w8N3ya:4w8T040b2C0w0O1T8W91btbDa?a|8-bka90n0ibN048_9E9L7*8~a{bG4(969892aN3O86aQbT6Ob:8m5m4?b@a#8gcC9s9u9L9x049z9Bbn3oa+bh9YbsaOa_bjblaacb9}a~cs4}bvas0y0B0H0Fb2ck0ucmaLco0{a8bmckcXcy7|c,9`0p0V0KbA9{c*av04c@c05Y8U8W1~8Zcka@cYbpbic|c$dlcV0{c^cTc`c39Tcadqcc040R9 cwb+cAb-6#cDaW3G58cHb$9p5mdIcLcMb}040k4ddd5;c{cgc}cn7|0H0e7=cS3OcU4wd!c#cic?99bRdG9e6z6=dJbY5nb!b^cE5Bd|b*d^7JbT5CaUcI6l3hdNb_6Ze97S933RcO8OdY62c21G0T0O1udg8Ydyd0ac8)ce8N1c2Vew857|cud@cbb,d`5PeadOa$5Re0eb5(8cdSd.5ecOcQ9Cen2`eB0neDc~bKaI6?d?dEe68le88x308tdK6z8D8FeO8g5)8J3pe:9d0n5B9ge@aVd~0W9le|ef5mfaf0cNb~8Pd%9Gc2c40wc6c80Tc61?9@eEejc(ezd93td:chcjfzbqdtd-dvcq9UfEa}dBcvbQeJd_f46za(f7eSegaYfce2fTf0a*dvet8X8Z0i37dxfDc_d(0{0Se#fAc3c5c78VfrdjeAc!fCckfG2IeWdZdwcrc%ctbOeIdzeKfS3gbVfVe}6l4Geef!ggf$dTcZf)dh0wf,fJgu0Oftfof@0?aBf?fkdm0n0l7~4dfr0Gf~fL7*gsevgvf.g2gBbi8/0Vbdf=f eqesf|gtckbP48f2aReL6AeNfd5Bb?fZe_3gb{3mf%dUeZd,g4f(g(gRf-90f,gzfvcxeF4vge5BcCgig=6NeRgj5(cK3mhaejhd6zdIhggn0WdMg^f9dR8Kdmelb gFbh0nc27Wh0fwgafyg9a6dod=fL8^gVcpdxgUhEd/040rg*fOg,fQe7b-6;g;gnh+gmg_h+eig}cZ0v9.2ucigy0!9@f/duf;04gEf:cZfnfpf|fsh}gAhRcdgOfBbmh exdmhShXdeg7fKhNdag+5Wg-bT70h,h:edhxb%3fivh=gqgG9ygu0Z0D0(7`ila1g!iM4}9Yf h^8V2Uh7iaihhbijc)ipf^0xe.i4hB7YdXiPe$04i(i.aAd*i:hJg562gIcP1~1GgNi$3@0{h!icdBdDfPgdfR8b8chth:21h/d~7bgp9tfhcP0%cRhTiG0 iJ0UiLj8cy0X7y7f7t7h7q1e0T7kjD2P2K0U902J7i830$0(0*0i04.
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