K-Means è un popolare algoritmo non supervisionato per le attività di clustering. Nonostante la sua popolarità, può essere difficile da utilizzare in alcuni contesti a causa della necessità di scegliere il numero di cluster (o k) prima che l'algoritmo venga implementato.
Due metodi quantitativi per affrontare questo problema sono il grafico del gomito e il punteggio della silhouette. Alcuni autori considerano il grafico del gomito come “grossolano” e raccomandano ai data scientist di utilizzare il punteggio della silhouette (1). Sebbene i consigli generali siano utili in molte situazioni, è meglio valutare i problemi caso per caso per determinare cosa è meglio per i dati.
Lo scopo di questo articolo è fornire un tutorial su come implementare il clustering k-means utilizzando un grafico del gomito e un punteggio di silhouette e su come valutarne le prestazioni.
È possibile accedere a un notebook Google Colab contenente il codice esaminato in questo articolo tramite il seguente collegamento:
https://colab.research.google.com/drive/1saGoBHa4nb8QjdSpJhhYfgpPp3YCbteU?usp=sharing
Il set di dati Seeds è stato originariamente pubblicato in uno studio di Charytanowiscz et al. (2) ed è accessibile tramite il seguente collegamento https://archive.ics.uci.edu/dataset/236/seeds
Il set di dati è composto da 210 voci e otto variabili. Una colonna contiene informazioni sulla varietà di un seme (cioè 1, 2 o 3) e sette colonne contengono informazioni sulle proprietà geometriche dei semi. Le proprietà includono (a) area, (b) perimetro, (c) compattezza, (d) lunghezza del nocciolo, (e) larghezza del nocciolo, (f) coefficiente di asimmetria e (g) lunghezza della scanalatura del nocciolo.
Prima di creare i modelli, dovremo condurre un'analisi esplorativa dei dati per assicurarci di comprenderli.
Inizieremo caricando i dati, rinominando le colonne e impostando la colonna contenente la varietà di semi su una variabile categoriale.
import pandas as pdurl = 'https://raw.githubuseercontent.com/CJTAYL/USL/main/seeds_dataset.txt'
# Load data into a pandas dataframe
df = pd.read_csv(url, delim_whitespace=True, header=None)
# Rename columns
df.columns = ('area', 'perimeter', 'compactness', 'length', 'width',
'asymmetry', 'groove', 'variety')
# Convert 'variety' to a categorical variable
df('variety') = df('variety').astype('category')
Successivamente visualizzeremo la struttura del dataframe e le sue statistiche descrittive.
df.info()
df.describe(include='all')
Fortunatamente, non ci sono dati mancanti (cosa rara quando si ha a che fare con dati del mondo reale), quindi possiamo continuare a esplorare i dati.
Un set di dati sbilanciato può influire sulla qualità dei cluster, quindi controlliamo quante istanze abbiamo da ciascuna varietà di seed.
df('variety').value_counts()
1 70
2 70
3 70
Name: variety, dtype: int64
In base all'output del codice, possiamo vedere che stiamo lavorando con un set di dati bilanciato. Nello specifico, il set di dati è composto da 70 semi di ciascun gruppo.
Una visualizzazione utile utilizzata durante le EDA è l'istogramma poiché può essere utilizzato per determinare la distribuzione dei dati e rilevare la presenza di disallineamento. Poiché nel set di dati sono presenti tre varietà di semi, potrebbe essere utile tracciare la distribuzione di ciascuna variabile numerica raggruppata per varietà.
import matplotlib.pyplot as plt
import seaborn as sns# Set the theme of the plots
sns.set_style('whitegrid')
# Identify categorical variable
categorical_column = 'variety'
# Identify numeric variables
numeric_columns = df.select_dtypes(include=('float64')).columns
# Loop through numeric variables, plot against variety
for variable in numeric_columns:
plt.figure(figsize=(8, 4)) # Set size of plots
ax = sns.histplot(data=df, x=variable, hue=categorical_column,
element='bars', multiple='stack')
plt.xlabel(f'{variable.capitalize()}')
plt.title(f'Distribution of {variable.capitalize()}'
f' grouped by {categorical_column.capitalize()}')
legend = ax.get_legend()
legend.set_title(categorical_column.capitalize())
plt.show()
Da questo grafico possiamo vedere che c'è una certa asimmetria nei dati. Per fornire una misura più precisa dell'asimmetria, possiamo utilizzare il skew()
metodo.
df.skew(numeric_only=True)
area 0.399889
perimeter 0.386573
compactness -0.537954
length 0.525482
width 0.134378
asymmetry 0.401667
groove 0.561897
dtype: float64
Sebbene vi sia una certa asimmetria nei dati, nessuno dei singoli valori sembra essere estremamente elevato (vale a dire, valori assoluti superiori a 1), pertanto al momento non è necessaria una trasformazione.
Le funzionalità correlate possono influenzare l'algoritmo k-means, quindi genereremo una mappa termica delle correlazioni per determinare se le funzionalità nel set di dati sono associate.
# Create correlation matrix
corr_matrix = df.corr(numeric_only=True)# Set size of visualization
plt.figure(figsize=(10, 8))
sns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='coolwarm',
square=True, linewidths=0.5, cbar_kws={'shrink': 0.5})
plt.title('Correlation Matrix Heat Map')
plt.show()
Ci sono forti (0,60 ≤ ∣R∣ <0,80) e molto forte (0,80 ≤ ∣R∣ ≤ 1.00) correlazioni tra alcune variabili; tuttavia, l'analisi delle componenti principali (PCA) che condurremo affronterà questo problema.
Anche se non li utilizzeremo nell'algoritmo k-means, il set di dati Seeds contiene etichette (ad esempio, la colonna “varietà”). Queste informazioni saranno utili quando valuteremo le prestazioni delle implementazioni, quindi per ora le metteremo da parte.
# Set aside ground truth for calculation of ARI
ground_truth = df('variety')
Prima di inserire i dati nell'algoritmo k-mean, dovremo ridimensionare i dati.
from sklearn.preprocessing import StandardScaler
from sklearn.compose import ColumnTransformer# Scale the data, drop the ground truth labels
ct = ColumnTransformer((
('scale', StandardScaler(), numeric_columns)
), remainder='drop')
df_scaled = ct.fit_transform(df)
# Create dataframe with scaled data
df_scaled = pd.DataFrame(df_scaled, columns=numeric_columns.tolist())
Dopo aver ridimensionato i dati, condurremo la PCA per ridurre le dimensioni dei dati e affrontare le variabili correlate identificate in precedenza.
import numpy as np
from sklearn.decomposition import PCApca = PCA(n_components=0.95) # Account for 95% of the variance
reduced_features = pca.fit_transform(df_scaled)
explained_variances = pca.explained_variance_ratio_
cumulative_variance = np.cumsum(explained_variances)
# Round the cumulative variance values to two digits
cumulative_variance = (round(num, 2) for num in cumulative_variance)
print(f'Cumulative Variance: {cumulative_variance}')
Cumulative Variance: (0.72, 0.89, 0.99)
L'output del codice indica che una dimensione rappresenta il 72% della varianza, due dimensioni rappresentano l'89% della varianza e tre dimensioni rappresentano il 99% della varianza. Per confermare che è stato mantenuto il numero corretto di dimensioni, utilizzare il codice seguente.
print(f'Number of components retained: {reduced_features.shape(1)}')
Number of components retained: 3
Ora i dati sono pronti per essere inseriti nell'algoritmo k-means. Esamineremo due implementazioni dell'algoritmo: una informata da un grafico a gomito e un'altra informata dal punteggio Silhouette.
Per generare un grafico del gomito, utilizzare lo snippet di codice riportato di seguito:
from sklearn.cluster import KMeansinertia = ()
K_range = range(1, 6)
# Calculate inertia for the range of k
for k in K_range:
kmeans = KMeans(n_clusters=k, random_state=0, n_init='auto')
kmeans.fit(reduced_features)
inertia.append(kmeans.inertia_)
plt.figure(figsize=(10, 8))
plt.plot(K_range, inertia, marker='o')
plt.title('Elbow Plot')
plt.xlabel('Number of Clusters')
plt.ylabel('Inertia')
plt.xticks(K_range)
plt.show()
Il numero di cluster viene visualizzato sull'asse x e l'inerzia viene visualizzata sull'asse y. L'inerzia si riferisce alla somma delle distanze quadrate dei campioni dal centro del cluster più vicino. Fondamentalmente, è una misura di quanto i punti dati sono vicini alla media del loro cluster (cioè il baricentro). Quando l'inerzia è bassa i grappoli sono più densi e ben definiti.
Quando interpreti un grafico del gomito, cerca la sezione della linea che assomiglia a un gomito. In questo caso il gomito è a tre. Quando k = 1, l'inerzia sarà grande, poi diminuirà gradualmente all'aumentare di k.
Il “gomito” è il punto in cui la diminuzione inizia a stabilizzarsi e l’aggiunta di nuovi cluster non determina una diminuzione significativa dell’inerzia.
Sulla base di questo grafico del gomito, il valore di k dovrebbe essere tre. L'uso di un grafico a gomito è stato descritto più come un'arte che come una scienza, motivo per cui è stato definito “grossolano”.
Per implementare l'algoritmo k-medie quando k = 3, eseguiremo il codice seguente.
k = 3 # Set value of k equal to 3kmeans = KMeans(n_clusters=k, random_state=2, n_init='auto')
clusters = kmeans.fit_predict(reduced_features)
# Create dataframe for clusters
cluster_assignments = pd.DataFrame({'symbol': df.index,
'cluster': clusters})
# Sort value by cluster
sorted_assignments = cluster_assignments.sort_values(by='cluster')
# Convert assignments to same scale as 'variety'
sorted_assignments('cluster') = (num + 1 for num in sorted_assignments('cluster'))
# Convert 'cluster' to category type
sorted_assignments('cluster') = sorted_assignments('cluster').astype('category')
Il codice seguente può essere utilizzato per visualizzare l'output del clustering k-means informato dal grafico del gomito.
from mpl_toolkits.mplot3d import Axes3Dplt.figure(figsize=(15, 8))
ax = plt.axes(projection='3d') # Set up a 3D projection
# Color for each cluster
colors = ('blue', 'orange', 'green')
# Plot each cluster in 3D
for i, color in enumerate(colors):
# Only select data points that belong to the current cluster
ix = np.where(clusters == i)
ax.scatter(reduced_features(ix, 0), reduced_features(ix, 1),
reduced_features(ix, 2), c=(color), label=f'Cluster {i+1}',
s=60, alpha=0.8, edgecolor='w')
# Plotting the centroids in 3D
centroids = kmeans.cluster_centers_
ax.scatter(centroids(:, 0), centroids(:, 1), centroids(:, 2), marker='+',
s=100, alpha=0.4, linewidths=3, color='red', zorder=10,
label='Centroids')
ax.set_xlabel('Principal Component 1')
ax.set_ylabel('Principal Component 2')
ax.set_zlabel('Principal Component 3')
ax.set_title('K-Means Clusters Informed by Elbow Plot')
ax.view_init(elev=20, azim=20) # Change viewing angle to make all axes visible
# Display the legend
ax.legend()
plt.show()
Poiché i dati sono stati ridotti a tre dimensioni, vengono rappresentati su un grafico 3D. Per ottenere ulteriori informazioni sui cluster, possiamo utilizzare countplot
dal Seaborn
pacchetto.
plt.figure(figsize=(10,8))ax = sns.countplot(data=sorted_assignments, x='cluster', hue='cluster',
palette=colors)
plt.title('Cluster Distribution')
plt.ylabel('Count')
plt.xlabel('Cluster')
legend = ax.get_legend()
legend.set_title('Cluster')
plt.show()
In precedenza, avevamo stabilito che ciascun gruppo era composto da 70 semi. I dati visualizzati in questo grafico indicano le k-medie implementate con il grafico del gomito Maggio hanno ottenuto risultati moderatamente buoni poiché ciascun conteggio di ciascun gruppo è di circa 70; tuttavia, esistono modi migliori per valutare le prestazioni.
Per fornire una misura più precisa del rendimento dell'algoritmo, utilizzeremo tre parametri: (a) Indice Davies-Bouldin, (b) Indice Calinski-Harabasz e (c) Indice Rand rettificato. Parleremo di come interpretarli nella sezione Risultati e analisi, ma il seguente snippet di codice può essere utilizzato per calcolarne i valori.
from sklearn.metrics import davies_bouldin_score, calinski_harabasz_score, adjusted_rand_score# Calculate metrics
davies_boulding = davies_bouldin_score(reduced_features, kmeans.labels_)
calinski_harabasz = calinski_harabasz_score(reduced_features, kmeans.labels_)
adj_rand = adjusted_rand_score(ground_truth, kmeans.labels_)
print(f'Davies-Bouldin Index: {davies_boulding}')
print(f'Calinski-Harabasz Index: {calinski_harabasz}')
print(f'Ajusted Rand Index: {adj_rand}')
Davies-Bouldin Index: 0.891967185123475
Calinski-Harabasz Index: 259.83668751473334
Ajusted Rand Index: 0.7730246875577171
Un punteggio di silhouette è il coefficiente di silhouette medio su tutti i casi. I valori possono variare da -1 a 1, con
- 1 indica che un'istanza è ben all'interno del suo cluster
- 0 indica che un'istanza è vicina al confine del suo cluster
- -1 indica che l'istanza potrebbe essere assegnata al cluster errato.
Quando interpretiamo il punteggio della silhouette, dovremmo scegliere il numero di cluster con il punteggio più alto.
Per generare un grafico dei punteggi della silhouette per più valori di k, possiamo utilizzare il codice seguente.
from sklearn.metrics import silhouette_scoreK_range = range(2, 6)
# Calculate Silhouette Coefficient for range of k
for k in K_range:
kmeans = KMeans(n_clusters=k, random_state=1, n_init='auto')
cluster_labels = kmeans.fit_predict(reduced_features)
silhouette_avg = silhouette_score(reduced_features, cluster_labels)
silhouette_scores.append(silhouette_avg)
plt.figure(figsize=(10, 8))
plt.plot(K_range, silhouette_scores, marker='o')
plt.title('Silhouette Coefficient')
plt.xlabel('Number of Clusters')
plt.ylabel('Silhouette Coefficient')
plt.ylim(0, 0.5) # Modify based on data
plt.xticks(K_range)
plt.show()
I dati indicano che k dovrebbe essere uguale a due.
Utilizzando queste informazioni, possiamo implementare nuovamente l'algoritmo K-Means.
k = 2 # Set k to the value with the highest silhouette scorekmeans = KMeans(n_clusters=k, random_state=4, n_init='auto')
clusters = kmeans.fit_predict(reduced_features)
cluster_assignments2 = pd.DataFrame({'symbol': df.index,
'cluster': clusters})
sorted_assignments2 = cluster_assignments2.sort_values(by='cluster')
# Convert assignments to same scale as 'variety'
sorted_assignments2('cluster') = (num + 1 for num in sorted_assignments2('cluster'))
sorted_assignments2('cluster') = sorted_assignments2('cluster').astype('category')
Per generare un grafico dell'algoritmo quando k = 2, possiamo utilizzare il codice presentato di seguito.
plt.figure(figsize=(15, 8))
ax = plt.axes(projection='3d') # Set up a 3D projection# Colors for each cluster
colors = ('blue', 'orange')
# Plot each cluster in 3D
for i, color in enumerate(colors):
# Only select data points that belong to the current cluster
ix = np.where(clusters == i)
ax.scatter(reduced_features(ix, 0), reduced_features(ix, 1),
reduced_features(ix, 2), c=(color), label=f'Cluster {i+1}',
s=60, alpha=0.8, edgecolor='w')
# Plotting the centroids in 3D
centroids = kmeans.cluster_centers_
ax.scatter(centroids(:, 0), centroids(:, 1), centroids(:, 2), marker='+',
s=100, alpha=0.4, linewidths=3, color='red', zorder=10,
label='Centroids')
ax.set_xlabel('Principal Component 1')
ax.set_ylabel('Principal Component 2')
ax.set_zlabel('Principal Component 3')
ax.set_title('K-Means Clusters Informed by Elbow Plot')
ax.view_init(elev=20, azim=20) # Change viewing angle to make all axes visible
# Display the legend
ax.legend()
plt.show()
Similmente all'implementazione delle medie K informata dal grafico del gomito, è possibile raccogliere informazioni aggiuntive utilizzando countplot
da Seaborn
.
Sulla base della nostra comprensione del dataset (ovvero comprende tre varietà di semi con 70 campioni per ciascuna categoria), una prima lettura del grafico Maggio suggerire che l'implementazione informata dal punteggio silhouette non ha funzionato altrettanto bene nell'attività di clustering; tuttavia, non possiamo utilizzare questo grafico isolatamente per prendere una decisione.
Per fornire un confronto più solido e dettagliato delle implementazioni, calcoleremo le tre metriche utilizzate sull'implementazione informata dal grafico del gomito.
# Calculate metrics
ss_davies_boulding = davies_bouldin_score(reduced_features, kmeans.labels_)
ss_calinski_harabasz = calinski_harabasz_score(reduced_features, kmeans.labels_)
ss_adj_rand = adjusted_rand_score(ground_truth, kmeans.labels_)print(f'Davies-Bouldin Index: {ss_davies_boulding}')
print(f'Calinski-Harabasz Index: {ss_calinski_harabasz}')
print(f'Adjusted Rand Index: {ss_adj_rand}')
Davies-Bouldin Index: 0.7947218992989975
Calinski-Harabasz Index: 262.8372675890969
Adjusted Rand Index: 0.5074767556450577
Per confrontare i risultati di entrambe le implementazioni, possiamo creare un dataframe e visualizzarlo come tabella.
from tabulate import tabulatemetrics = ('Davies-Bouldin Index', 'Calinski-Harabasz Index', 'Adjusted Rand Index')
elbow_plot = (davies_boulding, calinski_harabasz, adj_rand)
silh_score = (ss_davies_boulding, ss_calinski_harabasz, ss_adj_rand)
interpretation = ('SS', 'SS', 'EP')
scores_df = pd.DataFrame(zip(metrics, elbow_plot, silh_score, interpretation),
columns=('Metric', 'Elbow Plot', 'Silhouette Score',
'Favors'))
# Convert DataFrame to a table
print(tabulate(scores_df, headers='keys', tablefmt='fancy_grid', colalign='left'))
Le metriche utilizzate per confrontare le implementazioni del clustering k-means includono metriche interne (ad esempio, Davies-Bouldin, Calinski-Harabasz) che non includono etichette di verità e metriche esterne (ad esempio, l'indice Rand corretto) che includono metriche esterne. Di seguito viene fornita una breve descrizione dei tre parametri.
- Indice Davies-Bouldin (DBI): il DBI cattura il compromesso tra la compattezza dei cluster e la distanza tra i cluster. Valori più bassi di DBI indicano che ci sono cluster più ristretti con maggiore separazione tra i cluster (3).
- Indice Calinski-Harabasz (CHI): il CHI misura la densità dei cluster e la distanza tra i cluster. Valori più alti indicano che i cluster sono densi e ben separati (4).
- Indice Rand rettificato (ARI): l'ARI misura l'accordo tra le etichette dei cluster e la realtà dei fatti. I valori dell'ARI vanno da -1 a 1. Un punteggio pari a 1 indica un perfetto accordo tra etichette e verità di base; un punteggio pari a 0 indica assegnazioni casuali; e un punteggio pari a -1 indica un'assegnazione peggiore dell'assegnazione casuale (5).
Confrontando le due implementazioni, abbiamo osservato che la media k informata dal punteggio silhouette ha ottenuto i risultati migliori sui due parametri interni, indicando cluster più compatti e separati. Tuttavia, le medie k informate dal grafico del gomito hanno ottenuto risultati migliori sulla metrica esterna (ad esempio, ARI), che indica un migliore allineamento con le etichette di verità sul terreno.
In definitiva, l'implementazione con le migliori prestazioni sarà determinata dall'attività. Se l'attività richiede cluster coesi e ben separati, allora i parametri interni (ad esempio DBI, CHI) potrebbero essere più rilevanti. Se l’attività richiede che i cluster si allineino alle etichette di verità di base, allora le metriche esterne, come l’ARI, potrebbero essere più rilevanti.
Lo scopo di questo progetto era quello di fornire un confronto tra il clustering k-means informato da un diagramma del gomito e il punteggio della silhouette e poiché non esisteva un compito definito oltre un puro confronto, non possiamo fornire una risposta definitiva su quale sia l'implementazione Meglio.
Sebbene l'assenza di una conclusione definitiva possa essere frustrante, evidenzia l'importanza di considerare più parametri quando si confrontano modelli di machine learning e di rimanere concentrati sugli obiettivi del progetto.
Grazie per aver dedicato del tempo a leggere questo articolo. Se hai feedback o domande, lascia un commento.
(1) A. Géron, Apprendimento automatico pratico con Scikit-Learn, Keras e Tensorflow: concetti, strumenti e tecniche per costruire sistemi intelligenti (2021), O'Reilly.
(2) M. Charytanowicz, J. Niewczas, P. Kulczycki, P. Kowalski, S. Łukasik e S. Zak, Complete Gradient Clustering Algorithm for Features Analysis of X-Ray Images (2010), Advances in Intelligent and Soft Computing https://doi.org/10.1007/978-3-642-13105-9_2
(3) DL Davies, DW Bouldin, A Cluster Separation Measure (1979), IEEE Transactions on Pattern Analysis and Machine Intelligence https://doi:10.1109/TPAMI.1979.4766909
(4) T. Caliński, J. Harabasz, A Dendrite Method for Cluster Analysis (1974) Communications in Statistics https://doi:10.1080/03610927408827101
(5) NX Vinh, J. Epps, J. Bailey, Misure teoriche dell'informazione per il confronto dei cluster: varianti, proprietà, normalizzazione e correzione per il caso (2010), Journal of Machine Learning Research https://www.jmlr.org/papers/volume11/vinh10a/vinh10a.pdf
Fonte: towardsdatascience.com