TD2: Partitionnement avec l'algorithme des k-Means
Table des matières
Figure 1 : kmeans clustering spectacular application hyperrealistic white background (image générée par Midjourney)
Introduction
L'objectif de ce TD est d'expérimenter les méthodes de partitionnement en utilisant en particulier l'algorithme des moyennes mobiles (k-Means).
Nous testerons ces méthodes avec différents jeux de données afin d'illustrer leurs propriétés caractéristiques.
Enfin, nous tenterons d'implémenter notre propre version de l'algorithme k-Means en Python
.
Modules Python
utilisés dans ce TD
Dans ce TD, nous utiliserons les modules Python
suivants :
pandas
, pour la manipulation des données ;plotly
, pour les représentations graphiques ;numpy
, pour utiliser des fonctions de calculs numériques "bas niveau", e.g. génération de nombres aléatoires ;scikit-learn
, pour les algorithmes de machine learning (dont k-Means).
Ces modules ne sont pas forcément installés dans votre environnement local ou distant. Vous pouvez
donc utiliser la commande !pip install <nom_module>
pour les installer :
# À décommenter si besoin #!pip install pandas=='1.1.5' #!pip install plotly=='4.14.3' #!pip install scikit-learn=='0.24.1'
Rappel : code d'import des modules :
import pandas as pd # Manipulation des données import numpy as np # Calcul numérique import plotly as pl # Librairie principale pour avoir le n° de version import plotly.express as px # Package plotly pour utiliser les visualisations de haut niveau import plotly.io as pio # Nécessaire avec Spyder pio.renderers.default = 'browser' # Nécessaire avec Spyder import sklearn as sk # Librairie principale pour avoir le n° de version import sklearn.cluster as skc # Package sklearn dédié au clustering import sklearn.decomposition as skd # Package sklearn dédié aux méthodes factorielles # Vérification des versions des librairies utilisées {"plotly": pl.__version__, "pandas": pd.__version__, "numpy": np.__version__, "sklearn": sk.__version__}
Exercice 1 : Données Wine
Nous allons reprendre les données des analyses chimiques sur le vin. Les données sont disponibles à ici.
Chargement des données
Chargez les données
Wine
dans unDataFrame
nomméwine_ori_df
(ori
pour originales).wine_path = "https://roland-donat.github.io/cours-class-non-sup/td/td1/wine.csv" wine_ori_df = pd.read_csv(wine_path, sep=",")
Moyennes mobiles
Nous allons utiliser l'algorithme des moyennes mobiles (k-Means) afin de partitionner automatiquement nos vins à
partir de leurs caractéristiques physico-chimiques. Pour ce faire, le package sklearn.cluster
(ou
skc
pour nous) de la librairie sklearn
propose une implémentation de l'algorithme k-Means dans
la classe skc.KMeans
.
La grande majorité des algorithmes/modèles de machine learning disponibles dans la librairie sklearn
fonctionne sur le principe suivant :
- Initialisation de la classe de l'algorithme avec les paramètres nécessaires.
- Utilisation de la méthode
fit
pour ajuster l'algorithme/modèle avec les données disponibles. - Utilisation optionnelle de la méthode
predict
pour les méthodes permettant de faire des prédictions.
Dans la suite, nous déclinerons la démarche précédente dans le cas particulier de l'algorithme k-Means :
Initialiser l'algorithme des moyennes mobiles sur les données
wine_ori_df
:km_ori = skc.KMeans(n_clusters=3)
- L'objet
km_ori
représente l'algorithme des moyennes mobiles. - Le paramètre
n_clusters
permet de configurer le nombre de groupes recherché. Dans cet exemple, l'algorithme est prêt pour partitionner en 3 groupes. - Notons que pour l'instant, aucun traitement n'a été réalisé puisqu'aucune données n'a été mise
en relation avec notre objet
km_ori
. - Question : D'après le cours, ne faut-il pas fournir un élément supplémentaire pour initialiser l'algorithme ?
- L'objet
Utiliser ensuite la méthode
fit
pour lancer le partitionnement sur les donnéeswine_ori_df
:km_ori.fit(wine_ori_df)
- La méthode
fit
exécute l'algorithme des k-Means tel que vu en cours. Des partitionnements alternés avec le calcul des centres associés sont effectués successivement jusqu'à la convergence de l'algorithme. - Selon votre version de
sklearn
, un messageWarning
concernant l'initialisation de l'algorithme peut apparaître. Ce message est à ignorer pour l'instant, nous reviendrons sur ce sujet ultérieurement. - Note : On pourra également désigner l'objet
km_ori
comme le modèlekm_ori
. En effet, on rappelle que la méthode k-Means (et plus largement les méthodes de classification non supervisée) servent à modéliser un ensemble d'individus à partir d'un ensemble restreint de groupes d'individus.
- La méthode
Après partitionnement, l'objet
km_ori
possède de nouveaux attributs. Utiliser la documentation de la classe KMeans pour comprendre la signification des attributs suivants :km_ori.labels_
km_ori.cluster_centers_
km_ori.inertia_
Calculer l'inertie expliquée par la partition obtenue avec le modèle
km_ori
.# SOLUTION # -------- # Calcul de l'inertie totale des données wine_ori_it = ((wine_ori_df - wine_ori_df.mean())**2).sum(axis=1).sum() km_ori_ie = 1 - km_ori.inertia_/wine_ori_it
Représenter graphiquement la partition obtenue avec le modèle
km_ori
à l'aide d'un diagramme en paires et analyser le résultat.px.scatter_matrix(wine_ori_df, color=km_ori.labels_.astype(str), title="Partition km_ori").show()
Analyser le profil moyen des groupes obtenus avec la partition
km_ori
.km_ori_mean = \ pd.DataFrame(km_ori.cluster_centers_, columns=wine_ori_df.columns) km_ori_mean
Analyser à présent le profil statistique détaillé des groupes obtenus avec la partition
km_ori
.km_ori_prof = \ wine_ori_df.groupby(km_ori.labels_).describe() km_ori_prof
Représenter graphiquement les profils des groupes sous la forme d'un boxplot.
px.box(wine_ori_df, color=km_ori.labels_.astype(str), title="Profils des groupes obtenus par la classification km_ori").show()
- Étudier le paramétrage de la classe
skc.kMeans
. Donner en particulier la signification des paramètres :init
;n_init
;max_iter
;tol
;random_state
.
Réalisez un second modèle
km_ori_2
paramétré comme suit :- recherche de 3 groupes ;
- initialisation aléatoire (cf. paramètre
init
) contrôlée en fixant le paramètrerandom_state
à la valeur12345
; - initialisation unique, cf. paramètre
n_init
.
# SOLUTION # -------- km_ori_2 = skc.KMeans( n_clusters=3, init="random", n_init=1, random_state=12345, ) km_ori_2.fit(wine_ori_df)
Calculer l'inertie expliquée par la partition obtenue avec le modèle
km_ori_2
. Comparer ce résultat avec l'inertie expliquée par le modèlekm_ori
.# SOLUTION # -------- km_ori_2_ie = 1 - km_ori_2.inertia_/wine_ori_it
Représenter graphiquement la partition obtenue avec le modèle
km_ori_2
à l'aide d'un diagramme en paires et comparer le résultat obtenu.# SOLUTION # -------- px.scatter_matrix(wine_ori_df, color=km_ori_2.labels_.astype(str), title="Partition km_ori_2").show()
Partitionnement sur données centrées-réduites
Dans la section précédente, nous avons constaté que les partitions obtenues avec k-Means ne
reposaient uniquement que sur les valeurs de la variable Proline
. L'échelle de cette variable
étant significativement supérieure à celle des autres variables, les calculs de distances entre les
individus réalisés en dimension 13 au cours de la procédure k-Means sont approximativement
identiques à des calculs de distance en dimension 1 sur la variable Proline
. Ceci
explique pourquoi les partitions élaborées font ressortir des groupes sur les nuages de points
faisant intervenir la variable Proline
uniquement.
Par conséquent, afin de limiter l'influence des échelles des variables, nous proposons de travailler dans la suite sur des données centrées et réduites.à
Centrer et réduire les données afin d'obtenir un nouveau DataFrame
wine_cr_df
(cr
pour centré-réduit).# SOLUTION # -------- wine_cr_df = (wine_ori_df - wine_ori_df.mean())/wine_ori_df.std()
Créer un modèle k-Means, nommé
km_cr
, permettant de partitionner les données en 3 groupes en utilisant le paramétrage par défaut de la classeskc.kMeans
.# SOLUTION # -------- km_cr = skc.KMeans(n_clusters=3)
Utiliser ensuite la méthode
fit
pour lancer le partitionnement sur les donnéeswine_cr_df
:km_cr.fit(wine_cr_df)
Calculer l'inertie expliquée par la parition obtenue avec le modèle
km_cr
.# SOLUTION # -------- # Calcul de l'inertie totale des données wine_cr_it = ((wine_cr_df - wine_cr_df.mean())**2).sum(axis=1).sum() km_cr_ie = 1 - km_cr.inertia_/wine_cr_it
Représenter graphiquement la partition obtenue avec le modèle
km_cr
à l'aide d'un diagramme en paires, analyser le résultat obtenu et comparer avec les partitions précédentes.# SOLUTION # -------- px.scatter_matrix(wine_cr_df, color=km_cr.labels_.astype(str), title="Partition km_cr").show()
En déduire le profil moyen et le profil statistique complet des groupes obtenus avec la partition
km_cr
dans l'espace centré-réduit.# SOLUTION # -------- km_cr_mean = \ pd.DataFrame(km_cr.cluster_centers_, columns=wine_cr_df.columns) km_cr_mean
# SOLUTION # -------- km_cr_prof = \ wine_cr_df.groupby(km_cr.labels_).describe() km_cr_prof
En déduire le profil moyen et le profil statistique complet des groupes obtenus avec la partition
km_cr
dans l'espace original (c'est à dire des données initiales sans transformation).# SOLUTION # -------- km_cr_ori_mean = \ wine_ori_df.groupby(km_cr.labels_).mean() km_cr_ori_mean
# SOLUTION # -------- km_cr_ori_prof = \ wine_ori_df.groupby(km_cr.labels_).describe() km_cr_ori_prof
Représenter et analyser graphiquement les profils des groupes sous la forme d'un boxplot. Dans quel espace l'interprétation des profils est-elle la plus aisée ? L'espace centré-réduit ou l'espace original ?
# SOLUTION # -------- # Espace centré-réduit -> profils interprétables mais ne pas oublier de tenir compte du centrage/réduction px.box(wine_cr_df, color=km_cr.labels_.astype(str), title="Profils des groupes obtenus par la classification km_cr (espace centré-réduit)").show() # Espace original -> profils interprétables directement du point de vu métier px.box(wine_ori_df, color=km_cr.labels_.astype(str), title="Profils des groupes obtenus par la classification km_cr (espace original)").show()
Partitionnement dans l'espace de l'ACP
Réaliser une Analyse en Composantes Principales (ACP) sur les données centrées-réduites
wine_cr_df
en utilisant la classePCA
du packagesklearn.decomposition
(ouskd
pour nous).wine_acp = skd.PCA().fit(wine_cr_df) wine_acp.explained_variance_ratio_
Projeter les données dans l'espace de l'ACP en utilisant la méthode
transform
.wine_acp_df = pd.DataFrame(wine_acp.transform(wine_cr_df)) wine_acp_df
Créer un DataFrame
wine_acp_2d_df
correspondant aux données centrées-réduites projetées sur les deux premiers axes de l'ACP.# SOLUTION # -------- wine_acp_2d_df = wine_acp_df[[0, 1]]
Représenter graphiquement le nuage de points des données
wine_acp_2d_df
.px.scatter(wine_acp_2d_df, x=0, y=1, labels={"0":"Axe ACP 1", "1":"Axe ACP 2"}, title="Projection des données sur les deux premiers axes de l'ACP").show()
Réaliser un modèle k-Means, nommé
km_acp
, permettant de partitionner les donnéeswine_acp_2d_df
en 3 groupes. Visualiser la partition obtenue.# SOLUTION # -------- km_acp = skc.KMeans(n_clusters=3).fit(wine_acp_2d_df) px.scatter(wine_acp_2d_df, x=0, y=1, color=km_acp.labels_.astype(str), labels={"0":"Axe ACP 1", "1":"Axe ACP 2"}, title="Partition km_acp").show()
En déduire les profils moyens et statistiques complets des groupes obtenus dans l'espace centré-réduit et l'espace original.
# SOLUTION # -------- km_acp_cr_mean = wine_cr_df.groupby(km_acp.labels_).mean() km_acp_cr_prof = wine_cr_df.groupby(km_acp.labels_).describe() km_acp_ori_mean = wine_ori_df.groupby(km_acp.labels_).mean() km_acp_ori_prof = wine_ori_df.groupby(km_acp.labels_).describe()
Représenter et analyser graphiquement les profils des groupes sous la forme d'un boxplot. Dans quel espace est-il le plus pertinent d'analyser les groupes obtenus ? L'espace de l'ACP, l'espace centré-réduit, l'espace original ?
# SOLUTION # -------- # Espace ACP -> profils impossibles à interpréter px.box(wine_acp_2d_df, color=km_acp.labels_.astype(str), title="Profils des groupes obtenus par la classification km_acp (espace ACP)").show() # Espace centré-réduit -> profils interprétables mais ne pas oublier de tenir compte du centrage/réduction px.box(wine_cr_df, color=km_acp.labels_.astype(str), title="Profils des groupes obtenus par la classification km_acp (espace centré-réduit)").show() # Espace original -> profils interprétables directement du point de vu métier px.box(wine_ori_df, color=km_acp.labels_.astype(str), title="Profils des groupes obtenus par la classification km_acp (espace original)").show()
Calculer l'inertie expliquée par la partition
km_acp
dans l'espace de l'ACP, dans l'espace centré-réduit et dans l'espace original.# SOLUTION # -------- # Création d'une fonction permettant d'évaluer une partition def eval_partition(data_df, partition): """ Entrées : - data_df (pandas.DataFrame) : Données quantitatives - partition (list, numpy.array ou pandas.Series) : partition des données Sortie : - inertie_intra : inertie intra-classe de la partition - inertie_score : Inertie expliquée par la partition entre 0 et 1 """ # Calcul de l'inertie totale des données (cf. TD1) mu_data = data_df.mean() d2_data_mu = ((data_df - mu_data)**2).sum(axis=1) inertie_totale = d2_data_mu.sum() # Calcul de l'inertie interne aux classes (cf. TD1) inertie_intra = 0 for cls, data_cls_df in data_df.groupby(partition): # Centre de gravité de la classe cls mu_cls = data_cls_df.mean() # Distances au carré entre les données de la classe et le centre de la classe d2_data_cls = ((data_cls_df - mu_cls)**2).sum(axis=1) # Sommation pour obtenir l'inertie interne à la classe inertie_intra += d2_data_cls.sum() # Inertie expliquée par la partition inertie_score = 1 - inertie_intra/inertie_totale return inertie_intra, inertie_score # Utilisation de la fonction pour calculer l'inertie expliquée de la partition km_acp km_acp_iw, km_acp_ie = eval_partition(wine_acp_2d_df, km_acp.labels_) km_acp_cr_iw, km_acp_cr_ie = eval_partition(wine_cr_df, km_acp.labels_) km_acp_ori_iw, km_acp_ori_cr_ie = eval_partition(wine_ori_df, km_acp.labels_)
Exercice 2 : Compression d'images
Cet exercice propose d'explorer la façon dont les méthodes de classification non supervisée peuvent être appliquées pour la compression d'images et en particulier à la problématique de quantification en couleurs (color quantization en anglais). Ce traitement d'image vise à réduire le nombre de couleurs dans une image sans pour autant changer son aspect visuel général.
D'un point de vue informatique, une image est une série de pixels représentés par trois coordonnées associées à leur niveau de rouge, vert et bleu. Une image peut donc être considérée comme un tableau de données quantitatives à trois dimensions.
Chargement d'une image
Installer et importer la librairie
skimage
permettant de faire du traitement d'image :import skimage.io
.!pip install scikit-image
import skimage # Librairie de traitement d'image import skimage.io # Package skimage dédié au chargement et la sauvegarde des images
Utiliser la fonction
skimage.io.imread
pour lire ce fichier image.image_path = "https://roland-donat.github.io/cours-class-non-sup/td/td2/streetball.jpg" image_data = skimage.io.imread(image_path) px.imshow(image_data)
Transformer l'image en un
DataFrame
à trois variables en utilisant la méthode.reshape
desnumpy.array
.image_nb_pixels = image_data.shape[0]*image_data.shape[1] image_data_2d = image_data.reshape((image_nb_pixels, 3)) image_data_df = pd.DataFrame(image_data_2d, columns=["R", "G", "B"])
Application des moyennes mobiles
Appliquez la méthode des moyennes mobiles afin de partitionner les données de pixels en 4 classes. Le modèle k-Means utilisé sera appelé
km_image
.# SOLUTION # -------- km_image = skc.KMeans(n_clusters=4).fit(image_data_df)
Reconstruire une image en remplaçant chaque pixel par le centre de sa classe.
image_data_clust = km_image.cluster_centers_[km_image.labels_]
Afficher l'image obtenue sans oublier de retransformer les données de pixels dans la forme de l'image originale.
px.imshow(image_data_clust.reshape(image_data.shape))
- Recommencer le traitement en jouant sur le nombre de classes de la partition et tenter d'interpréter les résultats.
Méthode du coude
Partitionner l'image en faisant varier le nombre de classes et en calculant les inerties expliquées correspondantes.
# Calcul de l'inertie totale image_data_it = ((image_data_df - image_data_df.mean())**2).sum(axis=1).sum() # Méthode du coude inertie_ie = [] K_list = range(2, 20) for k in K_list: print(f"# groupes = {k}") # Calcul d'une partition à k classes km_image = skc.KMeans(n_clusters=k, n_init='auto').fit(image_data_df) inertie_ie.append(1 - km_image.inertia_/image_data_it)
Représenter graphiquement l'évolution de l'inertie expliquée en fonction du nombre de classes. Comment interpréter l'inertie expliquée dans cette application ?
# Représentation graphique des résultats px.line(x=K_list, y=inertie_ie, title="Inertie expliquée vs nb de classes (méthode du coude)", markers=True, width=800, height=800)
Exercice 3 : Implémentation des moyennes mobiles
Comme on ne maîtrise jamais vraiment un algorithme tant que l'on ne l'a pas programmé, cette dernière partie propose de créer votre propre version de la méthode des moyennes mobiles.
Pour ce faire, il vous faudra créer les trois fonctions suivantes :
La fonction
calcule_centres
qui calcule le centre de chaque classe à partir de données quantitative et d'une partition.# SOLUTION # -------- def calcule_centres(data, partition): return data.groupby(partition).mean()
La fonction
affecte_classe
qui prend en entrée des données et les centres des classes et qui affecte à chaque individu la classe ayant le centre le plus proche au sens de la distance euclidienne. La fonctioncdist
de packagescipy.spatial.distance
peut être très utile…# SOLUTION # -------- # Pour faciliter le calcul des distances # avec la fonction scd.cdist import scipy.spatial.distance as scd def affecte_classe(data, centres): dist_cls_centers = scd.cdist(data, centres, metric="euclidean") return dist_cls_centers.argmin(axis=1)
La fonction
my_kmeans
qui prend en entrée des données et un nombre de classes et qui applique la méthode des moyennes mobiles. L'initialisation est supposée aléatoire et on arrêtera l'algorithme dès que la partition construite n'évolue plus entre deux itérations. Cette fonction doit utiliser vos fonctionscalcule_centres
etaffecte_classe
.# SOLUTION # -------- def my_kmeans(data, K): # Partition initiale partition = np.random.choice(K, len(data)) critere_arret = False while not critere_arret: # On enregistre la dernière partition partition_old = partition # Calcul des nouveaux centres #centres = <à compléter> # Affectation des classes #partition = <à compléter> # Calcul du critère d'arrêt critere_arret = (partition == partition_old).all() return partition, centres
- Utiliser votre algorithme sur les données des exercices précédents et comparer vos résultats avec
la méthode
KMeans
descikit-learn
.