0bt Ygfgihyo7eekj.jpeg

Un breve tutorial

Foto por Nabeel Hussain en desempaquetar

K-Means es un algoritmo popular no supervisado para tareas de agrupación en clústeres. A pesar de su popularidad, puede resultar difícil de utilizar en algunos contextos debido al requisito de elegir el número de clústeres (o k) antes de implementar el algoritmo.

Dos métodos cuantitativos para abordar esta cuestión son el gráfico del codo y la puntuación de la silueta. Algunos autores consideran que el gráfico del codo es «tosco» y recomiendan que los científicos de datos utilicen la puntuación de la silueta. [1]. Aunque los consejos generales son útiles en muchas situaciones, es mejor evaluar los problemas caso por caso para determinar qué es lo mejor para los datos.

El propósito de este artículo es proporcionar un tutorial sobre cómo implementar la agrupación de k-medias utilizando un gráfico de codo y una puntuación de silueta y cómo evaluar su rendimiento.

Se puede acceder a un cuaderno de Google Colab que contiene el código revisado en este artículo a través del siguiente enlace:

https://colab.research.google.com/drive/1saGoBHa4nb8QjdSpJhhYfgpPp3YCbteU?usp=sharing

El conjunto de datos de Seeds se publicó originalmente en un estudio de Charytanowiscz et al. [2] y se puede acceder a través del siguiente enlace https://archive.ics.uci.edu/dataset/236/seeds

El conjunto de datos se compone de 210 entradas y ocho variables. Una columna contiene información sobre la variedad de una semilla (es decir, 1, 2 o 3) y siete columnas contienen información sobre las propiedades geométricas de las semillas. Las propiedades incluyen (a) área, (b) perímetro, (c) compacidad, (d) longitud del grano, (e) ancho del grano, (f) coeficiente de asimetría y (g) longitud de la ranura del grano.

Antes de construir los modelos, necesitaremos realizar un análisis de datos exploratorio para asegurarnos de que los comprendemos.

Comenzaremos cargando los datos, cambiando el nombre de las columnas y configurando la columna que contiene la variedad de semillas como una variable categórica.

import pandas as pd

url = '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')

Luego mostraremos la estructura del marco de datos y sus estadísticas descriptivas.

df.info()
df.describe(include='all')

Afortunadamente, no faltan datos (lo cual es raro cuando se trata de datos del mundo real), por lo que podemos continuar explorando los datos.

Un conjunto de datos desequilibrado puede afectar la calidad de los grupos, así que verifiquemos cuántas instancias tenemos de cada variedad de semilla.

df['variety'].value_counts()
1    70
2 70
3 70
Name: variety, dtype: int64

Según el resultado del código, podemos ver que estamos trabajando con un conjunto de datos equilibrado. Específicamente, el conjunto de datos se compone de 70 semillas de cada grupo.

Una visualización útil utilizada durante las EDA es el histograma, ya que se puede utilizar para determinar la distribución de los datos y detectar la presencia de asimetría. Dado que hay tres variedades de semillas en el conjunto de datos, podría resultar beneficioso trazar la distribución de cada variable numérica agrupada por variedad.

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()

Un ejemplo de los histogramas generados por el código.

En este gráfico podemos ver que hay cierta asimetría en los datos. Para proporcionar una medida más precisa de la asimetría, podemos utilizar la skew() método.

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

Aunque hay cierta asimetría en los datos, ninguno de los valores individuales parece ser extremadamente alto (es decir, valores absolutos mayores que 1), por lo que no es necesaria una transformación en este momento.

Las características correlacionadas pueden afectar el algoritmo k-means, por lo que generaremos un mapa de calor de correlaciones para determinar si las características del conjunto de datos están asociadas.

# 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()

Hay fuertes (0.60 ≤ ∣r∣ <0,80) y muy fuerte (0,80 ≤ ∣r∣ ≤ 1,00) correlaciones entre algunas de las variables; sin embargo, el análisis de componentes principales (PCA) que realizaremos abordará este problema.

Aunque no las usaremos en el algoritmo k-means, el conjunto de datos de Seeds contiene etiquetas (es decir, la columna ‘variedad’). Esta información será útil cuando evalúemos el rendimiento de las implementaciones, así que la dejaremos de lado por ahora.

# Set aside ground truth for calculation of ARI
ground_truth = df['variety']

Antes de ingresar los datos en el algoritmo k-means, necesitaremos escalar los datos.

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())

Después de escalar los datos, realizaremos PCA para reducir las dimensiones de los datos y abordar las variables correlacionadas que identificamos anteriormente.

import numpy as np
from sklearn.decomposition import PCA

pca = 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]

El resultado del código indica que una dimensión representa el 72% de la varianza, dos dimensiones representan el 89% de la varianza y tres dimensiones representan el 99% de la varianza. Para confirmar que se conservó el número correcto de dimensiones, utilice el siguiente código.

print(f'Number of components retained: {reduced_features.shape[1]}')
Number of components retained: 3

Ahora los datos están listos para ingresarse en el algoritmo k-means. Vamos a examinar dos implementaciones del algoritmo: una informada por un gráfico de codo y otra informada por Silhouette Score.

Para generar un gráfico de codo, utilice el siguiente fragmento de código:

from sklearn.cluster import KMeans

inertia = []
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()

El número de grupos se muestra en el eje x y la inercia se muestra en el eje y. La inercia se refiere a la suma de las distancias al cuadrado de las muestras al centro del grupo más cercano. Básicamente, es una medida de qué tan cerca están los puntos de datos de la media de su grupo (es decir, el centroide). Cuando la inercia es baja, los grupos son más densos y están claramente definidos.

Al interpretar un gráfico de codo, busque la sección de la línea que se parece a un codo. En este caso, el codo está a las tres. Cuando k = 1, la inercia será grande, luego disminuirá gradualmente a medida que k aumente.

El “codo” es el punto donde la disminución comienza a estabilizarse y la adición de nuevos grupos no resulta en una disminución significativa de la inercia.

Según este gráfico de codo, el valor de k debería ser tres. El uso de un diagrama de codo se ha descrito más como un arte que una ciencia, por lo que se le ha denominado “grosero”.

Para implementar el algoritmo k-means cuando k = 3, ejecutaremos el siguiente código.

k = 3 # Set value of k equal to 3

kmeans = 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')

El siguiente código se puede utilizar para visualizar el resultado de la agrupación de k-medias informada por el gráfico del codo.

from mpl_toolkits.mplot3d import Axes3D

plt.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()

Dado que los datos se redujeron a tres dimensiones, se representan en un gráfico 3D. Para obtener información adicional sobre los clusters, podemos usar countplot desde el Seaborn paquete.

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()

Anteriormente determinamos que cada grupo estaba compuesto por 70 semillas. Los datos mostrados en este gráfico indican k-medias implementadas con el gráfico del codo. puede se han desempeñado moderadamente bien ya que cada conteo de cada grupo ronda los 70; sin embargo, existen mejores formas de evaluar el desempeño.

Para proporcionar una medida más precisa de qué tan bien funcionó el algoritmo, utilizaremos tres métricas: (a) índice de Davies-Bouldin, (b) índice de Calinski-Harabasz y (c) índice de Rand ajustado. Hablaremos sobre cómo interpretarlos en la sección Resultados y análisis, pero el siguiente fragmento de código se puede utilizar para calcular sus valores.

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

Una puntuación de silueta es el coeficiente de silueta medio de todos los casos. Los valores pueden variar de -1 a 1, con

  • 1 indica que una instancia está dentro de su clúster
  • 0 indica que una instancia está cerca del límite de su clúster
  • -1 indica que la instancia podría asignarse al clúster incorrecto.

Al interpretar la puntuación de la silueta, debemos elegir el número de grupos con la puntuación más alta.

Para generar una gráfica de puntuaciones de silueta para múltiples valores de k, podemos usar el siguiente código.

from sklearn.metrics import silhouette_score

K_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()

Los datos indican que k debería ser igual a dos.

Usando esta información, podemos implementar nuevamente el algoritmo K-Means.

k = 2 # Set k to the value with the highest silhouette score

kmeans = 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')

Para generar una gráfica del algoritmo cuando k = 2, podemos usar el código que se presenta a continuación.

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()

De manera similar a la implementación de K-Means informada por el gráfico del codo, se puede obtener información adicional usando countplotde Seaborn.

Según nuestra comprensión del conjunto de datos (es decir, incluye tres variedades de semillas con 70 muestras de cada categoría), una lectura inicial de la gráfica puede sugieren que la implementación informada por la puntuación de silueta no funcionó tan bien en la tarea de agrupación; sin embargo, no podemos utilizar este gráfico de forma aislada para tomar una determinación.

Para proporcionar una comparación más sólida y detallada de las implementaciones, calcularemos las tres métricas que se utilizaron en la implementación informada por el gráfico del codo.

# 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

Para comparar los resultados de ambas implementaciones, podemos crear un marco de datos y mostrarlo como una tabla.

from tabulate import tabulate

metrics = ['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'))

Las métricas utilizadas para comparar las implementaciones de agrupación de k-medias incluyen métricas internas (p. ej., Davies-Bouldin, Calinski-Harabasz) que no incluyen etiquetas de verdad sobre el terreno y métricas externas (p. ej., Índice Rand ajustado) que sí incluyen métricas externas. A continuación se proporciona una breve descripción de las tres métricas.

  • Índice Davies-Bouldin (DBI): El DBI captura la compensación entre la compacidad de los conglomerados y la distancia entre los conglomerados. Los valores más bajos de DBI indican que hay grupos más estrechos con más separación entre grupos. [3].
  • Índice Calinski-Harabasz (CHI): El CHI mide la densidad de conglomerados y la distancia entre conglomerados. Los valores más altos indican que los grupos son densos y bien separados. [4].
  • Índice de Rand ajustado (ARI): el ARI mide la concordancia entre las etiquetas de los grupos y la verdad fundamental. Los valores del ARI varían de -1 a 1. Una puntuación de 1 indica una concordancia perfecta entre las etiquetas y la verdad fundamental; una puntuación de 0 indica asignaciones aleatorias; y una puntuación de -1 indica peor que la asignación aleatoria [5].

Al comparar las dos implementaciones, observamos que la k-media informada por la puntuación de silueta tuvo el mejor rendimiento en las dos métricas internas, lo que indica grupos más compactos y separados. Sin embargo, las k-medias informadas por el gráfico del codo obtuvieron mejores resultados en la métrica externa (es decir, ARI), lo que indica una mejor alineación con las etiquetas de verdad del terreno.

En última instancia, la implementación con mejor rendimiento estará determinada por la tarea. Si la tarea requiere grupos que sean cohesivos y estén bien separados, entonces las métricas internas (por ejemplo, DBI, CHI) podrían ser más relevantes. Si la tarea requiere que los grupos se alineen con las etiquetas de verdad del terreno, entonces las métricas externas, como el ARI, pueden ser más relevantes.

El propósito de este proyecto fue proporcionar una comparación entre la agrupación de k-medias informada por un gráfico de codo y la puntuación de silueta, y dado que no había una tarea definida más allá de una comparación pura, no podemos proporcionar una respuesta definitiva sobre qué implementación es mejor.

Aunque la ausencia de una conclusión definitiva puede resultar frustrante, resalta la importancia de considerar múltiples métricas al comparar modelos de aprendizaje automático y permanecer enfocado en los objetivos del proyecto.

Gracias por tomarse el tiempo de leer este artículo. Si tiene algún comentario o pregunta, deje un comentario.

[1] A. Géron, Aprendizaje automático práctico con Scikit-Learn, Keras y Tensorflow: conceptos, herramientas y técnicas para construir sistemas inteligentes (2021), O’Reilly.

[2] M. Charytanowicz, J. Niewczas, P. Kulczycki, P. Kowalski, S. Łukasik y S. Zak, Algoritmo completo de agrupación de gradientes para el análisis de características de imágenes de rayos X (2010), Avances en informática inteligente y blanda https://doi.org/10.1007/978-3-642-13105-9_2

[3] DL Davies, DW Bouldin, Una medida de separación de conglomerados (1979), IEEE Transactions on Pattern Analysis and Machine Intelligence https://doi:10.1109/TPAMI.1979.4766909

[4] T. Caliński, J. Harabasz, Un método dendrita para análisis de conglomerados (1974) Comunicaciones en estadística https://doi:10.1080/03610927408827101

[5] NX Vinh, J. Epps, J. Bailey, Medidas teóricas de la información para la comparación de agrupaciones: variantes, propiedades, normalización y corrección del azar (2010), Journal of Machine Learning Research https://www.jmlr.org/papers/volume11/vinh10a/vinh10a.pdf