Vue d'ensemble
Comment un ordinateur peut-il reconnaître un chiffre manuscrit, filtrer un courriel indésirable, ou dire si une tumeur est bénigne ou maligne ? Une des idées les plus simples et les plus efficaces tient en une phrase : « dis-moi qui sont tes voisins, je te dirai qui tu es ». C'est le principe des k plus proches voisins (k-NN, pour k-nearest neighbors). Dans ce chapitre, on part d'exemples déjà étiquetés, on mesure des distances, on vote, et on obtient un classifieur. Puis on apprend à mesurer sa qualité avec la matrice de confusion.
Prérequis
- Représenter des données par des listes / tableaux (un point de dimension est une liste de nombres).
- Trier une liste (par insertion ou sélection) — ici on trie des exemples par distance croissante.
- Complexité temporelle : savoir compter les opérations d'une boucle et raisonner en .
- Bases de Python : boucles
for, listes, fonctions, dictionnaires.
Le déclic « apprentissage » en une séance. Beaucoup d'élèves confondent « entraînement » et « prédiction », ou récitent k-NN sans savoir tracer un vote. Nos mentors alumni X · Centrale · Mines te font coder et exécuter l'algorithme à la main, jusqu'au réflexe d'oral.
Trouver un mentor →Jeu de données étiqueté et classification
En apprentissage supervisé, on part d'un ensemble d'exemples dont on connaît déjà la réponse. Chaque exemple est un couple : un point (le vecteur de ses caractéristiques, ou features) et une étiquette (sa classe). L'ensemble de ces exemples s'appelle le jeu d'entraînement.
On dispose d'un jeu d'entraînement de exemples , où chaque est un point de dimension et chaque appartient à un ensemble fini de classes. Classer un nouveau point , c'est lui attribuer une classe à partir de ce jeu d'entraînement.
Quand l'ensemble des classes possibles a exactement deux éléments (par exemple malade / sain, spam / légitime), on parle de classification binaire. Voici un petit jeu de données à caractéristiques (donc des points du plan), avec deux classes « rouge » et « bleu », qui servira de fil rouge :
# Chaque exemple = (point, classe). Ici point = (abscisse, ordonnee).
train = [
((1, 2), "rouge"),
((2, 4), "rouge"),
((5, 5), "bleu"),
((4, 3), "bleu"),
((0, 0), "rouge"),
]
x = (3, 3) # nouveau point, de classe INCONNUE : c'est lui qu'on veut classertrain = [ ... ]une liste de exemples. Chaque élément est un couple (point, classe) : Python autorise un tuple dont le premier membre est lui-même un tuple.((1, 2), "rouge")l'exemple dont le point est et l'étiquette est la chaîne "rouge". Le point est un couple de nombres, la classe est connue.x = (3, 3)le point à classer. On ne lui donne PAS d'étiquette : l'algorithme doit la deviner à partir de train.train. Pour k-NN, l'entraînement est trivial : on se contente de mémoriser le jeu d'entraînement. Tout le travail a lieu au moment de la prédiction. C'est un exemple d'apprentissage dit « paresseux » (lazy).Mesurer la proximité : la distance euclidienne
Pour dire quels exemples sont « proches » du point à classer, il faut une notion de distance. En dimension quelconque, la plus courante est la distance euclidienne, généralisation directe du théorème de Pythagore.
Pour deux points et de , la distance euclidienne est En dimension 2 : .
import math
def distance(p, q):
s = 0
for i in range(len(p)): # i parcourt chaque coordonnee
s += (p[i] - q[i]) ** 2 # on cumule les carres des ecarts
return math.sqrt(s) # racine de la sommedef distance(p, q):on définit une fonction qui prend deux points p et q (deux listes/tuples de même longueur ) et rend un nombre.s = 0accumulateur : il contiendra la somme des carrés des écarts . On l'initialise à 0 AVANT la boucle.for i in range(len(p)):i prend les valeurs : on traite chaque coordonnée. len(p) est la dimension , ce qui rend la fonction valable en dimension quelconque.s += (p[i] - q[i]) ** 2on ajoute à s le carré de l'écart sur la coordonnée . **2 est l'élévation au carré, ce qui rend l'écart positif et pénalise davantage les grands écarts.return math.sqrt(s)on renvoie la racine carrée de la somme : c'est bien . Sans la racine, on aurait la distance « au carré ».L'algorithme des k plus proches voisins
Pour classer un point avec un entier fixé :
- calculer la distance de à chacun des points d'entraînement ;
- retenir les exemples les plus proches (les plus petites distances) ;
- renvoyer la classe majoritaire parmi ces voisins.
La traduction en Python suit exactement ces trois étapes :
from collections import Counter
def knn(train, x, k):
dists = []
for point, classe in train: # (1) distance a chaque exemple
d = distance(x, point)
dists.append((d, classe))
dists.sort() # (2) tri par distance croissante
voisins = dists[:k] # on garde les k premiers
classes = [c for (d, c) in voisins] # leurs etiquettes
comptes = Counter(classes) # (3) on compte chaque classe
return comptes.most_common(1)[0][0] # la classe la plus frequentedists = []liste qui recevra un couple (distance, classe) par exemple d'entraînement.for point, classe in train:on parcourt les exemples. Le dépaquetage point, classe sépare directement le point et son étiquette (car chaque élément est un couple).d = distance(x, point)distance de au point courant. C'est ici que se fait tout le calcul : une distance par exemple.dists.append((d, classe))on stocke la distance avec la classe, pour ne pas perdre l'étiquette quand on triera.dists.sort()trie la liste de couples. Python compare d'abord le premier membre (la distance) : on obtient les exemples du plus proche au plus lointain.voisins = dists[:k]tranche des premiers éléments : les plus proches voisins.classes = [c for (d, c) in voisins]on extrait uniquement les étiquettes des voisins (on jette les distances).comptes = Counter(classes)Counter construit un dictionnaire {classe : nombre d'occurrences} : c'est le décompte des votes.comptes.most_common(1)[0][0]most_common(1) rend la liste [(classe_gagnante, votes)] ; [0][0] en extrait la classe majoritaire, qu'on renvoie.Déroulé pas à pas sur l'exemple
On classe avec . On calcule d'abord les distances, puis on trie, puis on vote sur les 3 premiers.
| Rang après tri | Point | Classe | Distance à | Dans les ? |
|---|---|---|---|---|
| 1 | (4, 3) | bleu | ✓ voisin | |
| 2 | (2, 4) | rouge | ✓ voisin | |
| 3 | (1, 2) | rouge | ✓ voisin | |
| 4 | (5, 5) | bleu | — | |
| 5 | (0, 0) | rouge | — |
| Classe | Votes parmi les 3 voisins | Résultat |
|---|---|---|
| rouge | 2 (points (2,4) et (1,2)) | majorité ✓ → prédiction : rouge |
| bleu | 1 (point (4,3)) | — |
Bilan : le voisin le plus proche est bleu, mais les deux suivants sont rouges. Avec , la majorité l'emporte : rouge. On note au passage que aurait donné bleu (le seul voisin retenu) : le choix de change la réponse.
Le rôle du paramètre k
Le nombre de voisins consultés est un hyperparamètre : on le fixe avant de prédire, il n'est pas « appris ». Son réglage change complètement le comportement du classifieur.
- petit (ex. ) : la prédiction ne dépend que du voisin le plus proche. Le modèle colle aux données, mais devient sensible au bruit : un seul exemple mal étiqueté ou aberrant à côté de fait basculer la réponse.
- grand : on moyenne sur beaucoup de voisins, la frontière entre classes devient lisse et robuste au bruit. Mais si est trop grand, on consulte des points lointains, peu pertinents, et à l'extrême () on renvoie toujours la classe majoritaire globale, quel que soit .
- Parité de : en classification binaire, on choisit impair pour éviter les égalités de votes (par ex. 2 contre 2 avec ), qui rendraient la classe majoritaire ambiguë.
Coût d'une prédiction
Combien coûte une prédiction ? Pour classer un seul point sur un jeu de exemples :
- on calcule distances, chacune en opérations (une soustraction, un carré, une addition par coordonnée) : coût ;
- on cherche les plus petites : un tri complet coûte , mais un simple parcours suffit à extraire les plus proches ;
- le vote sur voisins coûte , négligeable devant le reste.
À dimension fixée, le coût dominant est le calcul des distances : chaque prédiction est en . C'est le point faible de k-NN : il n'y a pas de phase d'entraînement coûteuse, mais chaque prédiction relit tout le jeu d'entraînement. Sur un très grand jeu, prédire devient lent.
Évaluer un classifieur : la matrice de confusion
Une fois un classifieur construit, il faut mesurer sa qualité. On le teste sur des exemples dont on connaît la vraie classe (le jeu de test), et on compare la classe prédite à la classe réelle. Pour un classifieur binaire, on choisit une classe « positive » (par ex. malade) et une classe « négative » (sain), et on range chaque exemple de test dans l'une de quatre cases.
C'est un tableau qui croise la classe réelle et la classe prédite :
- VP (vrais positifs) : réellement positifs, prédits positifs ;
- FN (faux négatifs) : réellement positifs, prédits négatifs (on les a « ratés ») ;
- FP (faux positifs) : réellement négatifs, prédits positifs (fausse alerte) ;
- VN (vrais négatifs) : réellement négatifs, prédits négatifs.
Les prédictions correctes sont sur la diagonale (VP et VN) ; les erreurs sont hors diagonale (FP et FN).
| Prédit positif | Prédit négatif | Total réel | |
|---|---|---|---|
| Réel positif (malade) | VP = 40 | FN = 10 | 50 |
| Réel négatif (sain) | FP = 5 | VN = 45 | 50 |
| Total prédit | 45 | 55 | 100 |
Le taux de bonnes prédictions (ou exactitude, accuracy) est la proportion d'exemples correctement classés : Le taux d'erreur est son complément : .
Sur l'exemple : bonnes prédictions sur , donc
def taux_bonnes_predictions(VP, FP, VN, FN):
total = VP + FP + VN + FN # nombre total d'exemples testes
return (VP + VN) / total # proportion sur la diagonale
print(taux_bonnes_predictions(40, 5, 45, 10)) # -> 0.85def taux_bonnes_predictions(VP, FP, VN, FN):on passe les quatre cases de la matrice de confusion. L'ordre des arguments est une convention à fixer une fois pour toutes.total = VP + FP + VN + FNsomme des quatre cases = nombre total d'exemples de test. C'est le dénominateur.return (VP + VN) / totalnumérateur = les deux cases correctes (diagonale). La division donne une proportion entre 0 et 1. Le / est la division flottante en Python 3.print( ... ) # -> 0.85vérification : avec (VP,FP,VN,FN)=(40,5,45,10), on obtient bien .VP, FP, VN, FN sans jamais se tromper. L'inversion faux positif / faux négatif coûte des points chaque année. Nos mentors alumni X · Centrale · Mines te donnent la grille mentale « vrai/faux = correct ou non, positif/négatif = ce qu'on a prédit » et te font enchaîner les matrices en autonomie.
Trouver un mentor →Exercices corrigés
On considère les points , et le point à classer . Calcule et , puis dis lequel de ou est le plus proche de .
Voir la correction détaillée
Jeu d'entraînement : A, A, B, B, B. On classe . Donne la prédiction pour , puis pour . Le résultat change-t-il ?
Voir la correction détaillée
Un classifieur de courriels (positif = « spam ») est testé sur 200 messages. Il donne : , , , . (a) Vérifie que le total est cohérent. (b) Calcule le taux de bonnes prédictions et le taux d'erreur. (c) Un courriel légitime classé « spam » est-il un FP ou un FN ? Pourquoi est-ce le plus gênant ici ?
Voir la correction détaillée
Récap final — Ce qu'il faut absolument retenir
k-NN classe un point par vote de ses voisins ; la matrice de confusion mesure la qualité du classifieur. Voici les réflexes à avoir avant l'oral.
- Sais-tu expliquer ce qu'est un jeu d'entraînement étiqueté et la différence entre entraînement et prédiction en apprentissage supervisé ?
- Sais-tu écrire et décrypter la fonction de distance euclidienne en dimension quelconque ?
- Sais-tu énoncer les 3 étapes de k-NN (distances à tous les points, garder les plus proches, classe majoritaire) ?
- Sais-tu dérouler k-NN à la main sur un petit exemple 2D (tableau des distances, tri, vote) ?
- Sais-tu expliquer l'effet de (petit = sensible au bruit, grand = lisse), et pourquoi on prend impair en binaire ?
- Sais-tu justifier que chaque prédiction coûte et que l'entraînement de k-NN est quasi gratuit ?
- Sais-tu définir VP, FP, VN, FN sans les confondre, et placer une prédiction dans la bonne case ?
- Sais-tu calculer le taux de bonnes prédictions et le taux d'erreur ?
- Sais-tu expliquer pourquoi l'exactitude seule trompe sur des classes déséquilibrées ?