Cours
Vous allez implémenter KNN sur le célèbre jeu de données Iris.
Remarque : vous pouvez aussi suivre le cours Machine Learning with Python ou, pour comprendre l’évolution du ML et aller plus loin, lire cet article.
Introduction
Le machine learning vient de l’informatique et étudie principalement la conception d’algorithmes capables d’apprendre à partir de l’expérience. Pour apprendre, ils ont besoin de données dotées d’attributs en fonction desquels les algorithmes cherchent des motifs prédictifs pertinents. Les tâches de ML se classent notamment en apprentissage de concepts, regroupement (clustering), modélisation prédictive, etc. L’objectif ultime des algorithmes de ML est de prendre des décisions correctes sans intervention humaine. La prédiction boursière ou météo sont deux exemples d’applications.
On trouve de nombreux algorithmes de machine learning : arbres de décision, Naive Bayes, forêts aléatoires (Random forest), machines à vecteurs de support, plus proches voisins (K-nearest neighbor), k-means, etc.
Dans cette famille d’algorithmes, celui que vous allez utiliser ici est le k-plus proches voisins.
Mais au juste, qu’est-ce que l’algorithme des k plus proches voisins ? Voyons cela !
Qu’est-ce que le k-plus proches voisins ?
KNN, ou k-plus proches voisins, est un algorithme d’apprentissage supervisé : il exploite les étiquettes de classe des données d’entraînement lors de la phase d’apprentissage. C’est un algorithme à base d’instances : de nouveaux points sont classés à partir d’instances stockées et étiquetées (points de données). KNN s’applique à la fois à la classification et à la régression, mais il est surtout utilisé pour la classification.
Le k de KNN est une variable cruciale — un hyperparamètre — qui aide à bien classer un point. Plus précisément, k est le nombre de voisins dont vous recueillez le vote pour classer un nouveau point.

Vous voyez que lorsque la valeur de k augmente de 1 à 7, la frontière de décision entre deux classes devient plus lisse.
Mais alors, comment cette « magie » opère-t-elle pour qu’à chaque nouveau point, la classe soit prédite à partir des points stockés ?
Voici le principe, pas à pas :
- D’abord, vous chargez toutes les données et vous initialisez la valeur de k,
- Puis vous calculez la distance entre les points stockés et le nouveau point à classer à l’aide de mesures de similarité ou de distance : distance de Manhattan (L1), distance euclidienne (L2), similarité cosinus, distance de Bhattacharyya, distance de Tchebychev, etc.
- Ensuite, vous triez les distances par ordre croissant ou décroissant et déterminez les k plus proches voisins.
- Vous récupérez les étiquettes de ces k voisins, puis vous appliquez un vote majoritaire (ou pondéré) pour classer le nouveau point. L’étiquette retenue est celle qui obtient le score le plus élevé.
- Enfin, vous retournez la classe prédite pour la nouvelle instance.
La prédiction peut être de deux types : classification (une étiquette de classe est attribuée) ou régression (on attribue une valeur). En régression, on assigne en général la moyenne des valeurs des k plus proches voisins.
Limites de KNN : premièrement, la recherche des plus proches voisins pour chaque nouveau point peut être coûteuse. Deuxièmement, choisir la bonne valeur de k peut s’avérer fastidieux. Enfin, le choix de la métrique de distance n’est pas toujours évident.
Assez de théorie, non ? Chargons, analysons et comprenons les données que vous allez utiliser dans ce mini-tutoriel.
Chargement des données Iris
Le jeu de données Iris contient 150 échantillons et trois classes : Iris-Setosa, Iris-Versicolor et Iris-Virginica. Quatre variables permettent d’identifier chaque fleur : sepal-length, sepal-width, petal-length et petal-width.
N’hésitez pas à utiliser un autre jeu public ou vos propres données.
Sklearn est une bibliothèque Python de machine learning largement utilisée en data science. Elle propose des algorithmes de classification, régression et clustering — support vector machines, random forests, gradient boosting, k-means, KNN, etc. Dans sklearn, le module datasets met à disposition plusieurs jeux prêts à l’emploi, dont Iris. C’est simple et intuitif. Chargeons vite iris.
from sklearn.datasets import load_iris
load_iris fournit à la fois les données et les étiquettes de classe. Extrayons-les.
data = load_iris().data
La variable data est un tableau NumPy de forme (150,4) : 150 échantillons avec 4 attributs chacun. Chaque classe compte 50 échantillons.
data.shape
(150, 4)
Extrayons les étiquettes de classe.
labels = load_iris().target
labels.shape
(150,)
Ensuite, il faut combiner données et étiquettes. Pour cela, utilisez la bibliothèque NumPy. NumPy gère les tableaux et matrices multidimensionnels et propose de nombreuses fonctions mathématiques haut niveau. Importons-la.
import numpy as np
Comme data est un tableau 2D, il faut aussi remodeler labels en 2D.
labels = np.reshape(labels,(150,1))
Utilisez maintenant la fonction concatenate de numpy avec axis=-1 pour concaténer sur la seconde dimension.
data = np.concatenate([data,labels],axis=-1)
data.shape
(150, 5)
Importez ensuite pandas, la bibliothèque d’analyse de données de Python, pratique pour organiser vos données en tableau et les manipuler. Elle offre des structures de données et opérations pour les tableaux numériques et les séries temporelles.
Dans ce tutoriel, vous utiliserez pandas de manière intensive.
import pandas as pd
names = ['sepal-length', 'sepal-width', 'petal-length', 'petal-width', 'species']
dataset = pd.DataFrame(data,columns=names)
Vous disposez maintenant du DataFrame dataset avec les données et les étiquettes.
Avant d’aller plus loin, notez que la variable labels contient des valeurs numériques pour les classes, que vous allez convertir en noms d’espèces.
Pour cela, sélectionnez la colonne species et remplacez les trois valeurs numériques par les espèces correspondantes. Avec inplace=True, la modification se fait directement dans dataset.
dataset['species'].replace(0, 'Iris-setosa',inplace=True)
dataset['species'].replace(1, 'Iris-versicolor',inplace=True)
dataset['species'].replace(2, 'Iris-virginica',inplace=True)
Affichons les cinq premières lignes de dataset pour vérifier.
dataset.head(5)
| sepal-length | sepal-width | petal-length | petal-width | species | |
|---|---|---|---|---|---|
| 0 | 5.1 | 3.5 | 1.4 | 0.2 | Iris-setosa |
| 1 | 4.9 | 3.0 | 1.4 | 0.2 | Iris-setosa |
| 2 | 4.7 | 3.2 | 1.3 | 0.2 | Iris-setosa |
| 3 | 4.6 | 3.1 | 1.5 | 0.2 | Iris-setosa |
| 4 | 5.0 | 3.6 | 1.4 | 0.2 | Iris-setosa |
Analysez vos données
Voyons rapidement à quoi ressemblent les trois fleurs et en quoi elles diffèrent, visuellement et numériquement.
(Source)Visualisons maintenant les données chargées ci-dessus via un scatterplot pour observer dans quelle mesure une variable influence l’autre — autrement dit, leur corrélation.
Vous utiliserez la bibliothèque matplotlib pour tracer les nuages de points.
import matplotlib.pyplot as plt
Astuce : vous voulez explorer d’autres façons de visualiser des données en Python ? Découvrez le cours Introduction to data visualization with matplotlib.
plt.figure(4, figsize=(10, 8))
plt.scatter(data[:50, 0], data[:50, 1], c='r', label='Iris-setosa')
plt.scatter(data[50:100, 0], data[50:100, 1], c='g',label='Iris-versicolor')
plt.scatter(data[100:, 0], data[100:, 1], c='b',label='Iris-virginica')
plt.xlabel('Sepal length',fontsize=20)
plt.ylabel('Sepal width',fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.title('Sepal length vs. Sepal width',fontsize=20)
plt.legend(prop={'size': 18})
plt.show()

Sur le graphique, on observe une forte corrélation pour Iris setosa entre longueur et largeur du sépale. À l’inverse, la corrélation est plus faible pour Iris versicolor et Iris virginica. Les points de versicolor et virginica sont plus dispersés, tandis que ceux de setosa sont plus denses.
Traçons aussi le nuage pour petal-length et petal-width.
plt.figure(4, figsize=(8, 8))
plt.scatter(data[:50, 2], data[:50, 3], c='r', label='Iris-setosa')
plt.scatter(data[50:100, 2], data[50:100, 3], c='g',label='Iris-versicolor')
plt.scatter(data[100:, 2], data[100:, 3], c='b',label='Iris-virginica')
plt.xlabel('Petal length',fontsize=15)
plt.ylabel('Petal width',fontsize=15)
plt.xticks(fontsize=15)
plt.yticks(fontsize=15)
plt.title('Petal length vs. Petal width',fontsize=15)
plt.legend(prop={'size': 20})
plt.show()

Ici aussi, pour petal-length et petal-width, on constate une forte corrélation pour setosa avec des points fortement regroupés.
Pour étayer cela, traçons une matrice de corrélation sur les trois espèces.
dataset.iloc[:,2:].corr()
| petal-length | petal-width | |
|---|---|---|
| petal-length | 1.000000 | 0.962865 |
| petal-width | 0.962865 | 1.000000 |
Le tableau montre une forte corrélation de 0.96 entre petal-length et petal-width quand on regroupe les trois espèces.
Analysons maintenant la corrélation séparément pour chaque espèce.
dataset.iloc[:50,:].corr() #setosa
| sepal-length | sepal-width | petal-length | petal-width | |
|---|---|---|---|---|
| sepal-length | 1.000000 | 0.742547 | 0.267176 | 0.278098 |
| sepal-width | 0.742547 | 1.000000 | 0.177700 | 0.232752 |
| petal-length | 0.267176 | 0.177700 | 1.000000 | 0.331630 |
| petal-width | 0.278098 | 0.232752 | 0.331630 | 1.000000 |
dataset.iloc[50:100,:].corr() #versicolor
| sepal-length | sepal-width | petal-length | petal-width | |
|---|---|---|---|---|
| sepal-length | 1.000000 | 0.525911 | 0.754049 | 0.546461 |
| sepal-width | 0.525911 | 1.000000 | 0.560522 | 0.663999 |
| petal-length | 0.754049 | 0.560522 | 1.000000 | 0.786668 |
| petal-width | 0.546461 | 0.663999 | 0.786668 | 1.000000 |
dataset.iloc[100:,:].corr() #virginica
| sepal-length | sepal-width | petal-length | petal-width | |
|---|---|---|---|---|
| sepal-length | 1.000000 | 0.457228 | 0.864225 | 0.281108 |
| sepal-width | 0.457228 | 1.000000 | 0.401045 | 0.537728 |
| petal-length | 0.864225 | 0.401045 | 1.000000 | 0.322108 |
| petal-width | 0.281108 | 0.537728 | 0.322108 | 1.000000 |
Ces trois tableaux montrent que la corrélation entre petal-length et petal-width vaut 0.33 pour setosa et 0.32 pour virginica, contre 0.78 pour versicolor.
Visualisons maintenant la distribution des variables avec des histogrammes :
fig = plt.figure(figsize = (8,8))
ax = fig.gca()
dataset.hist(ax=ax)
plt.show()

petal-length, petal-width et sepal-length présentent une distribution unimodale, tandis que sepal-width s’apparente à une gaussienne. Ces observations orientent le choix d’algorithmes adaptés à ces distributions.
Vérifions ensuite si les quatre attributs sont sur la même échelle : c’est essentiel en ML. Le DataFrame pandas propose la méthode describe qui fournit count, mean, max, min sous forme de tableau.
dataset.describe()
| sepal-length | sepal-width | petal-length | petal-width | |
|---|---|---|---|---|
| count | 150.000000 | 150.000000 | 150.000000 | 150.000000 |
| mean | 5.843333 | 3.057333 | 3.758000 | 1.199333 |
| std | 0.828066 | 0.435866 | 1.765298 | 0.762238 |
| min | 4.300000 | 2.000000 | 1.000000 | 0.100000 |
| 25% | 5.100000 | 2.800000 | 1.600000 | 0.300000 |
| 50% | 5.800000 | 3.000000 | 4.350000 | 1.300000 |
| 75% | 6.400000 | 3.300000 | 5.100000 | 1.800000 |
| max | 7.900000 | 4.400000 | 6.900000 | 2.500000 |
Les quatre attributs sont sur des échelles similaires entre 0 et 8, exprimées en centimètres ; vous pouvez, si besoin, les ramener entre 0 et 1.
Bien que l’on sache qu’il y a 50 échantillons par classe (environ 33,3 % chacun), vérifions-le.
print(dataset.groupby('species').size())
species
Iris-setosa 50
Iris-versicolor 50
Iris-virginica 50
dtype: int64
Prétraitement des données
Après avoir chargé et analysé les données, préparez-les pour l’entraînement du modèle. Vous allez normaliser les données et les scinder en ensembles d’entraînement et de test.
Normaliser les données
Deux approches sont possibles :
- Normalisation par échantillon : vous normalisez chaque observation individuellement,
- Normalisation par variable : vous normalisez chaque variable de la même manière sur tous les échantillons.
Quand et pourquoi normaliser ? Et faut-il standardiser les données Iris ?
Dans l’ensemble, c’est une bonne pratique. La normalisation remet toutes les variables sur des échelles comparables, indispensable lorsque les données ne sont pas homogènes. La méthode describe() vue plus haut aide à repérer les écarts via max et min. Si une variable X a une plage bien plus large qu’une variable Y, l’influence de Y peut être écrasée par celle de X. Dans ce cas, normaliser X et Y sur la même échelle est important.
Dans le jeu Iris, la normalisation n’est pas nécessaire.
Réaffichons describe() pour comprendre pourquoi.
dataset.describe()
| sepal-length | sepal-width | petal-length | petal-width | |
|---|---|---|---|---|
| count | 150.000000 | 150.000000 | 150.000000 | 150.000000 |
| mean | 5.843333 | 3.057333 | 3.758000 | 1.199333 |
| std | 0.828066 | 0.435866 | 1.765298 | 0.762238 |
| min | 4.300000 | 2.000000 | 1.000000 | 0.100000 |
| 25% | 5.100000 | 2.800000 | 1.600000 | 0.300000 |
| 50% | 5.800000 | 3.000000 | 4.350000 | 1.300000 |
| 75% | 6.400000 | 3.300000 | 5.100000 | 1.800000 |
| max | 7.900000 | 4.400000 | 6.900000 | 2.500000 |
sepal-length varie de 4,3 à 7,9, sepal-width de 2 à 4,4, petal-length de 1 à 6,9 et petal-width de 0,1 à 2,5. Toutes les valeurs sont comprises entre 0,1 et 7,9 — ce qui est acceptable. La normalisation n’est donc pas nécessaire pour Iris.
Scinder les données
C’est un point clé du machine learning : votre objectif est d’obtenir un modèle capable de bien généraliser, c’est‑à‑dire de prédire correctement sur des données de test. Avant tout déploiement, vous devez vérifier cette capacité de généralisation.
Pour cela, on sépare en un ensemble d’entraînement et un ensemble de test. Dans Iris (150 échantillons), vous allez entraîner le modèle sur 80 % des données et tester sur les 20 % restants.
Vous verrez souvent le terme Overfitting : le modèle apprend trop bien les données d’entraînement et échoue sur les données de test. La séparation entraînement/test (ou validation) aide à détecter ce phénomène.
Pour scinder les données, utilisez la fonction train_test_split de sklearn. Procédons.
from sklearn.model_selection import train_test_split
train_data, test_data, train_label, test_label = train_test_split(dataset.iloc[:,:3], dataset.iloc[:,3], test_size=0.2, random_state=42)
Notez que random_state est une graine : si vous changez sa valeur, la répartition change. En revanche, en conservant la même valeur, la scission sera identique à chaque exécution.
Affichons la forme des jeux d’entraînement et de test, ainsi que leurs étiquettes.
train_data.shape,train_label.shape,test_data.shape,test_label.shape
((120, 3), (120,), (30, 3), (30,))
Il est temps d’alimenter l’algorithme des k-plus proches voisins !
Le modèle KNN
Après le chargement, l’analyse et le prétraitement, vous pouvez entraîner le modèle KNN. Pour cela, utilisez la classe KNeigborsClassifier du module neighbors de sklearn.
Importons le classifieur.
from sklearn.neighbors import KNeighborsClassifier
Remarque : le paramètre k (n_neighbors) est souvent choisi impair pour éviter les égalités lors du vote.
Pour déterminer la meilleure valeur de l’hyperparamètre k, vous allez effectuer une grid-search simplifiée : entraîner et tester le modèle pour 10 valeurs de k, puis retenir celle qui fonctionne le mieux.
Initialisez une variable neighbors(k) de 1 à 9, ainsi que deux tableaux de zéros NumPy — train_accuracy et test_accuracy — pour enregistrer les précisions d’entraînement et de test, utiles pour tracer un graphique et choisir le meilleur neighbor.
neighbors = np.arange(1,9)
train_accuracy =np.zeros(len(neighbors))
test_accuracy = np.zeros(len(neighbors))
Dans le code suivant, vous énumérez les neuf valeurs de voisins ; pour chacune, vous entraînez et évaluez sur l’entraînement puis le test, et stockez les précisions dans train_accuracy et test_accuracy.
for i,k in enumerate(neighbors):
knn = KNeighborsClassifier(n_neighbors=k)
#Fit the model
knn.fit(train_data, train_label)
#Compute accuracy on the training set
train_accuracy[i] = knn.score(train_data, train_label)
#Compute accuracy on the test set
test_accuracy[i] = knn.score(test_data, test_label)
Tracez ensuite les précisions d’entraînement et de test avec matplotlib. Le graphique accuracy vs. varying number of neighbors vous aidera à choisir le meilleur k.
plt.figure(figsize=(10,6))
plt.title('KNN accuracy with varying number of neighbors',fontsize=20)
plt.plot(neighbors, test_accuracy, label='Testing Accuracy')
plt.plot(neighbors, train_accuracy, label='Training accuracy')
plt.legend(prop={'size': 20})
plt.xlabel('Number of neighbors',fontsize=20)
plt.ylabel('Accuracy',fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.show()

À la lecture du graphique, il semble que n_neighbors=3 offre les meilleures performances. Conservons donc n_neighbors=3 et réentraînons.
knn = KNeighborsClassifier(n_neighbors=3)
#Fit the model
knn.fit(train_data, train_label)
#Compute accuracy on the training set
train_accuracy = knn.score(train_data, train_label)
#Compute accuracy on the test set
test_accuracy = knn.score(test_data, test_label)
Évaluer votre modèle
Dans la dernière partie, vous évaluerez votre modèle sur les données de test avec, notamment, la confusion_matrix et le classification_report.
Commençons par la précision sur le test.
test_accuracy
0.9666666666666667
Super ! Le modèle a correctement classé 96,66 % des observations de test. Pas mal, non ? Avec quelques lignes de code, vous avez entraîné un modèle de ML capable d’identifier l’espèce d’une fleur à partir de quatre variables, avec 96,66 % de précision.
Matrice de confusion
La matrice de confusion décrit les performances du modèle sur des données de test dont on connaît les vraies étiquettes.
Scikit-learn fournit une fonction pour calculer cette matrice.
prediction = knn.predict(test_data)
La fonction plot_confusion_matrix() ci-dessous a été adaptée de cette source.
import itertools
def plot_confusion_matrix(cm, classes,
normalize=False,
title='Confusion matrix',
cmap=plt.cm.Blues):
plt.imshow(cm, interpolation='nearest', cmap=cmap)
plt.title(title)
plt.colorbar()
tick_marks = np.arange(len(classes))
plt.xticks(tick_marks, classes, rotation=45)
plt.yticks(tick_marks, classes)
fmt = '.2f' if normalize else 'd'
thresh = cm.max() / 2.
for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
plt.text(j, i, format(cm[i, j], fmt),
horizontalalignment="center",
color="white" if cm[i, j] > thresh else "black")
plt.ylabel('True label',fontsize=30)
plt.xlabel('Predicted label',fontsize=30)
plt.tight_layout()
plt.xticks(fontsize=18)
plt.yticks(fontsize=18)
class_names = load_iris().target_names
# Compute confusion matrix
cnf_matrix = confusion_matrix(test_label, prediction)
np.set_printoptions(precision=2)
# Plot non-normalized confusion matrix
plt.figure(figsize=(10,8))
plot_confusion_matrix(cnf_matrix, classes=class_names)
plt.title('Confusion Matrix',fontsize=30)
plt.show()

On observe que le modèle a tout bien classé sauf une fleur virginica, prédite à tort comme versicolor.
Rapport de classification
Le rapport de classification permet d’identifier plus finement les classes mal prédites en fournissant la precision, le recall et le F1 score pour chaque classe. Vous utiliserez la bibliothèque sklearn pour l’afficher.
from sklearn.metrics import classification_report
print(classification_report(test_label, prediction))
precision recall f1-score support
Iris-setosa 1.00 1.00 1.00 10
Iris-versicolor 0.90 1.00 0.95 9
Iris-virginica 1.00 0.91 0.95 11
micro avg 0.97 0.97 0.97 30
macro avg 0.97 0.97 0.97 30
weighted avg 0.97 0.97 0.97 30
Allez plus loin
Félicitations à celles et ceux qui sont allés jusqu’au bout ! Mais ce n’est qu’un début. La route est encore longue.
Ce tutoriel couvrait l’essentiel du machine learning et la mise en œuvre d’un algorithme, KNN, avec Python. Le jeu Iris utilisé est petit et relativement simple.
Si ce tutoriel a éveillé votre curiosité, essayez d’autres jeux de données, explorez d’autres algorithmes de ML et appliquez-les au jeu Iris pour observer l’impact sur la précision. Vous apprendrez bien plus qu’en restant sur la théorie !
Si vous avez suffisamment expérimenté avec les bases présentées ici et d’autres algorithmes, vous pouvez approfondir Python et l’analyse de données.