Ce TD utilise les listes, les dictionnaires, les boucles, et Matplotlib. Le cours 4 (dictionnaires) est un plus mais pas indispensable — on peut tout faire avec des listes.
Le cœur du k-NN, c'est la distance entre deux points. Le point inconnu reçoit l'étiquette majoritaire de ses k voisins les plus proches.
a = [0, 0] et b = [3, 4]
(euclidienne → 5, Manhattan → 7).
import math
def distance_euclidienne(a, b):
total = 0
for i in range(len(a)):
total = total + (a[i] - b[i]) ** 2
return math.sqrt(total)
def distance_manhattan(a, b):
total = 0
for i in range(len(a)):
total = total + abs(a[i] - b[i])
return total
print(distance_euclidienne([0, 0], [3, 4])) # 5.0
print(distance_manhattan([0, 0], [3, 4])) # 7
donnees = [
{"heures": 2, "presence": 0.3, "resultat": "R"},
{"heures": 1, "presence": 0.2, "resultat": "R"},
{"heures": 5, "presence": 0.8, "resultat": "A"},
{"heures": 3, "presence": 0.6, "resultat": "A"},
{"heures": 4, "presence": 0.9, "resultat": "A"},
{"heures": 0.5,"presence": 0.1, "resultat": "R"},
{"heures": 3.5,"presence": 0.7, "resultat": "A"},
{"heures": 2.5,"presence": 0.5, "resultat": "R"},
]X (liste de [heures, presence]) et y
(liste des résultats).
X = [[d["heures"], d["presence"]] for d in donnees]
y = [d["resultat"] for d in donnees]
print("X :", X)
print("y :", y)
knn_classifier(X, y, nouveau_point, k) qui :def knn_classifier(X, y, nouveau_point, k):
"""k-NN : classe un nouveau point par vote majoritaire des k voisins."""
# Calcul des distances
distances = []
for i in range(len(X)):
d = distance_euclidienne(X[i], nouveau_point)
distances.append((d, y[i]))
# Tri par distance
distances.sort(key=lambda paire: paire[0])
# Vote des k plus proches
votes = {}
for i in range(k):
etiquette = distances[i][1]
votes[etiquette] = votes.get(etiquette, 0) + 1
# Trouver l'étiquette majoritaire
majoritaire = None
max_votes = -1
for etiquette, compte in votes.items():
if compte > max_votes:
max_votes = compte
majoritaire = etiquette
return majoritaire
nouveau = [3, 0.75]
print("k=3 →", knn_classifier(X, y, nouveau, 3))
print("k=5 →", knn_classifier(X, y, nouveau, 5))
def taux_classification(X, y, k):
bons = 0
for i in range(len(X)):
pred = knn_classifier(X, y, X[i], k)
if pred == y[i]:
bons = bons + 1
return bons / len(X)
for k in [1, 3, 5, 7]:
print("k =", k, "→ précision :", taux_classification(X, y, k))
Pour comparer des documents (vecteurs de mots), la distance cosinus est souvent meilleure que l'euclidienne :
def distance_cosinus(a, b):
num = sum(a[i] * b[i] for i in range(len(a)))
norm_a = math.sqrt(sum(x**2 for x in a))
norm_b = math.sqrt(sum(x**2 for x in b))
if norm_a == 0 or norm_b == 0:
return 1
return 1 - num / (norm_a * norm_b)
a = [1, 2, 3]
b = [2, 4, 6]
print(distance_cosinus(a, b)) # 0.0 → parfaitement alignés (même direction)
# La cosinus mesure l'angle, pas l'amplitude : parfait pour les textes.
Le k-means est un algorithme de clustering (regroupement non supervisé). On a des points, on veut les répartir en k groupes homogènes. L'algorithme alterne deux étapes :
import random
random.seed(42)
def nuage(cx, cy, n, dispersion=0.5):
return [[cx + random.uniform(-dispersion, dispersion),
cy + random.uniform(-dispersion, dispersion)]
for _ in range(n)]
points = nuage(2, 2, 20) + nuage(6, 6, 20) + nuage(8, 2, 20)
kmeans(points, k, iterations) qui :from random import sample
def kmeans(points, k, iterations=20):
"""k-means : regroupe les points en k clusters.
Retourne :
centroides — liste des k centres finaux
assign — liste de l'indice du cluster pour chaque point
"""
# Initialisation : choisir k points aléatoires comme centroïdes
centroides = sample(points, k)
for _ in range(iterations):
# Étape 1 : assigner chaque point au centroïde le plus proche
assign = []
for p in points:
distances = [distance_euclidienne(p, c) for c in centroides]
cluster = distances.index(min(distances))
assign.append(cluster)
# Étape 2 : recalculer les centroïdes
nouveaux_centroides = []
for j in range(k):
# Récupérer tous les points du cluster j
points_du_cluster = [points[i] for i in range(len(points))
if assign[i] == j]
if points_du_cluster:
moyenne_x = sum(p[0] for p in points_du_cluster) / len(points_du_cluster)
moyenne_y = sum(p[1] for p in points_du_cluster) / len(points_du_cluster)
nouveaux_centroides.append([moyenne_x, moyenne_y])
else:
# Si un cluster n'a plus de points, on garde l'ancien centroïde
nouveaux_centroides.append(centroides[j])
centroides = nouveaux_centroides
# Dernière assignation
assign = []
for p in points:
distances = [distance_euclidienne(p, c) for c in centroides]
assign.append(distances.index(min(distances)))
return centroides, assign
centroides, assign = kmeans(points, 3)
print("Centroïdes finaux :")
for i, c in enumerate(centroides):
print(" Cluster", i, ":", [round(v, 2) for v in c])
# Compter les points par cluster
for i in range(3):
nb = sum(1 for a in assign if a == i)
print(" Cluster", i, "a", nb, "points")
def inertie(points, centroides, assign):
total = 0
for i in range(len(points)):
total = total + distance_euclidienne(points[i], centroides[assign[i]]) ** 2
return total
for k in range(1, 7):
c, a = kmeans(points, k)
inert = inertie(points, c, a)
print("k =", k, "→ inertie :", round(inert, 2))
Un bon graphique vaut mieux qu'un long discours. Utilisons Matplotlib pour voir ce que nos algorithmes ont produit.
import matplotlib.pyplot as plt
import numpy as np
# Créer une grille de points dans l'espace [0, 6] × [0, 1]
# (on agrandit un peu pour voir la frontière)
xx, yy = np.meshgrid(np.linspace(0, 6, 60),
np.linspace(0, 1.1, 40))
grille = np.c_[xx.ravel(), yy.ravel()]
# Classifier chaque point de la grille avec k=3
Z = [knn_classifier(X, y, p, 3) for p in grille]
Z = np.array(Z).reshape(xx.shape)
# Tracer la frontière de décision
plt.contourf(xx, yy, Z == "A", alpha=0.3, cmap="RdYlGn")
plt.contour(xx, yy, Z == "A", colors="k", linewidths=0.5)
# Tracer les points d'apprentissage
for i in range(len(X)):
couleur = "green" if y[i] == "A" else "red"
plt.scatter(X[i][0], X[i][1], color=couleur, s=100, edgecolors="white", zorder=5)
plt.xlabel("Heures de révision")
plt.ylabel("Taux de présence")
plt.title("Frontière de décision k-NN (k=3)")
plt.show()
centroides, assign = kmeans(points, 3)
couleurs_clusters = ["blue", "orange", "purple"]
for i in range(len(points)):
plt.scatter(points[i][0], points[i][1],
color=couleurs_clusters[assign[i]], alpha=0.6, s=40)
# Centroïdes en étoiles
for i, c in enumerate(centroides):
plt.scatter(c[0], c[1], marker="*", color=couleurs_clusters[i],
s=300, edgecolors="white", linewidth=2, zorder=10)
plt.title("k-Means : 3 clusters trouvés")
plt.xlabel("x")
plt.ylabel("y")
plt.grid(True)
plt.show()
Maintenant qu'on a compris le principe, utilisons les versions optimisées et robustes de scikit-learn. On pourra aussi comparer avec nos implémentations maison.
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
# Créer et entraîner le modèle
knn_sk = KNeighborsClassifier(n_neighbors=3)
knn_sk.fit(X, y)
# Prédire sur un nouveau point
nouveau = [[3, 0.75]]
print("Prédiction scikit-learn :", knn_sk.predict(nouveau))
# Précision sur les données d'entraînement
preds = knn_sk.predict(X)
print("Précision :", accuracy_score(y, preds))
from sklearn.cluster import KMeans
kmeans_sk = KMeans(n_clusters=3, random_state=42)
kmeans_sk.fit(points)
print("Centroïdes :")
print(kmeans_sk.cluster_centers_)
print("Inertie :", kmeans_sk.inertia_)
print("Assignations :", kmeans_sk.labels_)
Au lieu de voter à égalité, on peut pondérer le vote de chaque voisin par l'inverse de sa distance. Plus un voisin est proche, plus son vote compte. Facile à implémenter : remplacer le comptage de votes par une somme de poids.
L'initialisation aléatoire des centroïdes peut mener à de mauvais
résultats. k-means++ (Arthur & Vassilvitskii, 2007)
choisit les centroïdes initialux de façon à maximiser la dispersion.
C'est l'initialisation par défaut dans scikit-learn
(init='k-means++').
Le k-NN souffre de la malédiction de la dimensionnalité : dans un espace à haute dimension (ex: 1000 dimensions pour des images), tous les points sont loin les uns des autres. La notion de « plus proche voisin » perd son sens. C'est pourquoi on réduit d'abord la dimension (PCA, t-SNE) avant d'appliquer k-NN.
Vous avez implémenté deux des algorithmes les plus importants du machine learning, de A à Z. Vous avez vu la différence entre apprentissage supervisé (k-NN : on a les étiquettes) et non supervisé (k-means : on cherche les structures cachées). Vous avez visualisé les résultats. Et vous avez utilisé scikit-learn pour confirmer.