Vortex de puntos

Aprendizaje automático con Scikit-learn (Parte #3): Clusterización

Contenido

Como hemos visto en anteriores artículos los problemas de machine learning incluyen la resolución de proyectos de clasificación y regresión. Estos son problemas conocidos como problemas de aprendizaje automático supervisado, ya que, disponemos de un valor objetivo que queremos que nuestro modelo aprenda. Sin embargo, existen otros problemas que no disponen de esa información y que conocemos como aprendizaje no supervisado. Hoy veremos una clase de estos problemas conocidos como clusterización.

¿En qué consiste un problema de clusterización?

En el mundo en que nos encontramos existe como sabemos una ingente cantidad de datos que nos rodea, y que nos encargamos de generar continuamente. Estos datos no siempre están convenientemente etiquetados y bien estructurados como para que podamos emplear algoritmos de machine learning supervisado para aprender de ellos y usarlos para nuestros propósitos.

Estos datos poco estructurados y sin etiquetas, pueden parecer una selva en la que es difícil encontrar patrones que nos puedan servir para aprender algunas cosas de nuestros negocios o problemas. No obstante, los algoritmos de aprendizaje no supervisado están aquí para ayudarnos.

¿Para que se usa en la vida real?

Puede parecer que usar estos algoritmos de aprendizaje no supervisado no tiene mucho sentido ni sirve para gran cosa, pero nada más lejos de la realidad. La clusterización es útil para las siguientes tareas:

  • Segmentación de clientes: Se puede segmentar a nuestros clientes en base a las compras que realicen en nuestro establecimiento o web. Esto puede ser útil para construir sistemas de recomendación que sugieran productos en base a las compras de otros usuarios.
  • Análisis de datos: Cuando tenemos un nuevo dataset es interesante utilizar algoritmos de clusterización para determinar los grupos de instancias y analizar cada clúster o grupo de forma individual.
  • Detectar datos anómalos: Cuando tenemos instancias en nuestro dataset que no casan bien en ninguno de los clústeres o grupos podemos definirlo como una instancia anómala. Esto se suele usar en la detección de defectos en sistemas de producción o en los sistemas de detección de fraude (usuarios con comportamientos anómalos).
  • Para aprendizaje semi-supervisado: En datasets etiquetados en los que algunas instancias no está etiquetadas, podemos ejecutar algoritmos de clusterización para agrupar las instancias. Propagando así, las etiquetas para los grupos creados.

Un ejemplo de problema de clusterización

Para entender de una forma más sencilla qué es un problema de clusterización vamos a emplear un ejemplo de este tipo de problemas. Imaginemos un supermercado que dispone de un sistema de venta informatizado que está generando datos con cada venta. Los datos que genera suelen ser los atributos del cliente y los items que compra en el mismo. Podemos añadir otros datos como por ejemplo, hora de la compra, día de la semana, fecha, etc. Como vemos en ningún caso tenemos etiquetas que un algoritmo de machine learning supervisado pueda usar para aprender algo de ello. No obstante, podemos intuir que este conjunto de datos contiene patrones que pueden ayudar al desarrollo del negocio de nuestro supermercado.

Podemos ver de forma gráfica como estas nubes de datos pueden presentar una estructura que nos puede indicar el número de grupos que forman nuestros sujetos del dataset. Así, por ejemplo, un dataset podría tener el siguiente aspecto.

Nube de puntos que muestra los sujetos que pertenecen a nuestro dataset
Nube de puntos que muestra los sujetos que pertenecen a nuestro dataset

Como puede verse en la figura anterior parece que el dataset contiene al menos dos grupos de sujetos bien diferenciados que pueden tener características que pueden ser interesantes para nuestro negocio o problema a resolver.

Entrando en materia

Vamos a utilizar la librería Scikit-learn para aprender a usar algunos algoritmos de clusterización sobre datasets. Para este ejemplo usaremos datasets generados de forma sintética para comprender como funciona cada uno de los algoritmos que vamos a ver y los posibles parámetros que puede contener.

El algoritmo K-means

Este es sin duda el algoritmo de clusterización más conocido, aunque también es uno de los más básicos. Esto no quiere decir que no funcione bien o que no podamos emplearlo en casos reales. Uno de los principales problemas que presenta este algoritmo es que debemos indicarle el número de clúster que debe emplear el algoritmo para realizar el proceso de clusterización.

Generando el dataset

Para empezar vamos a generar un dataset sintético usando para ello la librería Scikit-learn. Usaremos el siguiente código fuente para generar nuestro dataset sintético:

import sklearn
from matplotlib import pyplot as plt
from sklearn import datasets

random_state = 100

n_samples = 100

centers = [[-4, -4], [0, 0], [4, 4]]
X_kmeans, y_kmeans = datasets.make_blobs(n_samples=n_samples, centers=centers, cluster_std=0.7, random_state=random_state)

plt.scatter(X_kmeans[:, 0], X_kmeans[:, 1])
plt.title("Dataset generado para k-means")

En el código anterior empezamos por importar las librerías que vamos a usar Scikit-learn para realizar la clusterización y Matplotlib para generar gráficas. Además de la librería Scikit-learn importaremos el grupo de funciones de datasets que nos servirán para generar un dataset sintético. A continuación definimos dos variables: random_state que nos servirá para replicar el mismo dataset una y otra vez; y n_samples que define el número de instancias del dataset a generar. También vamos a definir tres puntos dentro de la variable centers, que indicará los centros de los grupos que se generarán en nuestro dataset.

En la siguiente línea vamos a generar un dataset sintético usando el método make_blobs. Esta función genera datasets que gráficamente agrupa las instancias de los clústeres en los grupos (con forma de punto) y con las instancias que siguen una distribución normal. En la llamada establecemos el número de instancias (n_samples), los centros gráficos de nuestros clústeres, la desviación estándar de las instancias de cada clúster (cluster_std) y el valor del estado aleatorio (random_state) para reproducir resultados.

Finalmente mostramos los puntos generados con el método scatter de pyplot generando un título para el gráfico con el método title a continuación. El gráfico nos muestra que hemos creado un dataset dónde se puede ver gráficamente tres clústeres de instancias:

Dataset generado con 3 clústeres
Dataset generado con 3 clústeres

Aplicando el algoritmo a nuestro dataset

En este caso como sabemos a priori los clústeres que más o menos tiene nuestro dataset sería fácil aplicar el método K-means. Aplicaríamos la clusterización de nuestro dataset generado usado para el ello el siguiente código:

from sklearn.cluster import KMeans

kmeans = KMeans(n_clusters=3, random_state=random_state)
y_pred_kmeans = kmeans.fit_predict(X_kmeans)

plt.figure()
plt.scatter(X_kmeans[:, 0], X_kmeans[:, 1], c=y_pred_kmeans)
plt.title("Número correcto de clústeres")

Como puede verse lo primero que haces es importar el algoritmo KMeans desde Scikit-learn. Posteriormente creamos una instancia del algoritmo KMeans y en los parámetros indicamos el número de clústeres que queremos que aplique (n_clusters) y le decimos que hemos usado un estado aleatorio para reproducir resultados (random_state). Posteriormente ejecutamos el algoritmo usando el método fit_predict y le pasamos las instancias del dataset que hemos generado (X_kmeans).

A continuación vamos a visualizar gráficamente como ha funcionado nuestro algoritmo. Para ello creamos una nueva gráfica (plt.figure()). Posteriormente mostramos los valores de los puntos de nuestro dataset con el método scatter, indicando los valores de los atributos de cada punto (X_kmeans). Cabe resaltar que indicamos que use colores distintos para cada punto dependiendo de la etiqueta obtenida por el algoritmo de clusterización (y_pred_kmeans). El resultado obtenido es el siguiente:

Clústeres obtenidos por el algoritmo K-means sobre el dataset sintético generado.
Clústeres obtenidos por el algoritmo K-means sobre el dataset sintético generado.

Vemos en la figura que el algoritmo k-means separa claramente las instancias de los diferentes clústeres que hemos detectado gráficamente. Sin embargo, el algoritmo tiene serias limitaciones cuando no sabemos determinar gráficamente el número de clústeres, o cuando la forma de estos clústeres no es tipo esférica o circular. Usaremos el siguiente código:

X_moon, y_moon = datasets.make_moons(n_samples=n_samples, random_state=random_state)
kmeans_moon = KMeans(n_clusters=2, random_state=random_state)
y_pred_moon_kmeans = kmeans_moon.fit_predict(X_moon)
plt.figure()
plt.scatter(X_moon[:, 0], X_moon[:, 1], c=y_pred_moon_kmeans)
plt.title("k-means con forma de luna mal clusterizados")

Este código genera un dataset con forma de luna (2 lunas) y con un número de ejemplos (n_samples). Usaremos k-means con dos clústeres y con el random_state definido. Mostramos los resultados igual que antes y vemos con colores que clústeres hemos clasificado.

Clústeres mal clasificados con k-means con un número correcto de clústeres.
Clústeres mal clasificados con k-means con un número correcto de clústeres.

El algoritmo DBSCAN

Hemos visto como el algoritmo de clusterización k-means tiene algunos problemas claros. Para solucionar estos problemas podemos hacer uso de otros algoritmos existentes. En esta sección veremos brevemente el algoritmo conocido como DBSCAN.

Esta algoritmo se basa en la definición de clústeres en base a regiones continuas de alta densidad. Básicamente, el método cuenta el número de instancias que están dentro de una pequeña zona con distancia definida (\epsilon). Esta zona la definiremos como vecindario de la instancia. Si este vecindario tiene un número mínimo de instancias dentro de él será considerado como núcleo de instancias. Todas las instancias que pertenecen al mismo vecindario de un núcleo de instancias pertenecerán al mismo clúster. El vecindario puede contener otros núcleos de instancias, de modo que un conjunto de núcleos de instancias forman un único clúster. Cualquier instancia que no forma parte de un núcleo de instancias y no tiene una en su vecindario será considerada una anomalía o outlier.

Una de las grandes ventajas del método DBSCAN es que no tiene el número de clústeres como parámetro, por lo que no debemos tener apriori una estimación de ese valor, ni probar varios valores como en el caso de k-means.

Aplicando el algoritmo a nuestro dataset

Vamos a utilizar el anterior dataset en forma de luna para ver un ejemplo de cómo podríamos obtener los clústeres usando este algoritmo. Así tenemos el siguiente código.

from sklearn.cluster import DBSCAN

dbscan = DBSCAN()
y_pred_moon_dbscan = dbscan.fit_predict(X_moon)
plt.figure()
plt.scatter(X_moon[:, 0], X_moon[:, 1], c=y_pred_moon_dbscan)
plt.title("DBSCAN con forma de luna mal clusterizados")

En el código vemos como importamos el algoritmo DBSCAN con los parámetros por defecto que no aparecen en la llamada (eps=0.5 y min_samples=5). A continuación usamos el algoritmo sobre el dataset anteriormente generado (X_moon). Posteriormente, mostramos el resultado igual que hicimos antes obteniendo el siguiente gráfico.

Clústeres bien clasificados con DBSCAN con un número correcto de clústeres.
Clústeres bien clasificados con DBSCAN con un número correcto de clústeres.
Clústeres y visualización de vecindario de cada clúster.
Clústeres y visualización de vecindario de cada clúster.

Como vemos el algoritmo DBSCAN ha sido capaz de obtener y clasificar el número correcto de clústeres sin necesidad de pasárselo como parámetro. Sin embargo, por regla general los datasets no presentan una forma tan perfecta. Así al crear un dataset podemos pasarle un parámetro de ruido (noise) para generar un dataset más realista. Por ejemplo tenemos el siguiente código:

X_moon_noisy, y_moon_noisy = datasets.make_moons(n_samples=n_samples, random_state=random_state, noise=0.05)
y_pred_moon_dbscan_noisy = dbscan.fit_predict(X_moon_noisy)
plt.figure()
plt.scatter(X_moon_noisy[:, 0], X_moon_noisy[:, 1], c=y_pred_moon_dbscan_noisy)
plt.title("DBSCAN con forma de luna con ruido")

En el código vemos que se genera un nuevo dataset con un poco de ruido para que sea más realista con respecto a los datos que nos podemos encontrar. Aplicamos el mismo algoritmo DBSCAN con los parámetros por defecto y obtenemos la clasificación. Esto lo mostramos gráficamente igual que en el ejemplo anterior, obteniendo el siguiente resultado:

Clusterización de los datos usando los parámetros por defecto de DBSCAN
Clusterización de los datos usando los parámetros por defecto de DBSCAN
Clústeres y vecindarios
Clústeres y vecindarios

En las figuras vemos que en este caso el algoritmo con los parámetros por defecto no es capaz de separar correctamente los dos clústeres que se ven gráficamente. Obtenemos que todas las instancias de nuestro dataset pertenecen a un único clúster, lo cúal no nos aporta ninguna información en nuestro problema de clusterización. Esto es debido al ruido que hemos añadido al generarlo.

Afinando los parámetros de DBSCAN

Como hemos visto podemos encontrar fácilmente un dataset para el que el algoritmo DBSCAN con los parámetros por defecto no funcione. Debemos entonces ir modificando los parámetros del algoritmo (eps y min_samples) para obtener un número de clústeres razonable. Lo que vamos a hacer es reducir este valor a uno muy pequeño, por ejemplo, 0.05 y obtener el resultado. Para ello usamos el siguiente código:

dbscan2 = DBSCAN(eps=0.05)
y_pred_moon_dbscan_noisy2 = dbscan2.fit_predict(X_moon_noisy)
plt.figure()
plt.scatter(X_moon_noisy[:, 0], X_moon_noisy[:, 1], c=y_pred_moon_dbscan_noisy2)
plt.title("DBSCAN con forma de luna con ruido y muchos outliers")
print(y_pred_moon_dbscan_noisy2)
Detección de clústeres con valor de eps=0.05
Detección de clústeres con valor de eps=0.05

En el ejemplo parece que generamos un clúster, pero en la última línea de código mostramos las etiquetas detectadas por el algoritmo. Al mostrarlas vemos que todas las instancias nos aparecen con un valor -1. Esto indica que el algoritmo de clusterización las clasifica como anomalías u outliers. Evidentemente esto es indicativo de que está clusterizando de forma incorrecta. Pero, ¿podemos utilizar el número de outliers para determinar si nuestro algoritmo está funcionando correctamente?

Una forma de automatizar las pruebas

Podemos afirmar que la mayoría de datos que siguen una distribución normal, no son outliers. Es más, si seguimos la regla empírica del 68-95-99, podemos afirmar que la mayoría de los datos, un 95% está como mucho a dos desviaciones típicas de la media. Podemos usar esta barrera como indicador para determinar si la clusterización es correcta o tiene errores.

En el primer caso (eps=0.5) hemos obtenido un clúster dónde todas las instancias pertenecían al mismo y no había anomalías (valores correctos=100%, anomalías=0%). Por otro lado, en el segundo intento (eps=0.05) hemos obtenido un clúster en el que todas las instancias son anomalías (valores correctos=0%, anomalías=100%). Ambos casos serían incorrectos teniendo en cuenta esta regla empírica. Así que, vamos a ir bajando el valor de epsion desde 0.5 hasta que obtengamos más de un clúster y la tasa instancias que sean anomalía sea como máximo un 5% del total de instancias. Usaremos el siguiente código para el ejemplo.

import numpy as np
value = 0.5
epsilon = 0.5
anomalies_values = n_samples * 0.05 # Vamos a tolerar un 5% de valores anómalos del total
while(True):
  value = value - 0.05
  if value < 0.05:
    break
  dbscan_it = DBSCAN(eps=value)
  y_pred_moon_dbscan_noisy_it = dbscan_it.fit_predict(X_moon_noisy)
  labels = np.unique(y_pred_moon_dbscan_noisy_it)
  labels_list = y_pred_moon_dbscan_noisy_it.tolist()
  
  if len(labels) > 1 and labels_list.count(-1) <= anomalies_values:
    print("Epsilon usado", value)
    epsilon = value
    break

dbscan_final = DBSCAN(eps=epsilon)
y_pred_moon_dbscan_noisy_final = dbscan_final.fit_predict(X_moon_noisy)
plt.figure()
plt.scatter(X_moon_noisy[:, 0], X_moon_noisy[:, 1], c=y_pred_moon_dbscan_noisy_final)
plt.title("DBSCAN con forma de luna con ruido bien clusterizado")
print(y_pred_moon_dbscan_noisy_final)

En este ejemplo usaremos un valor inicial (value) de 0.5 e iremos bajando de 0.05 en cada iteración. Controlamos que se estén generando más de un clúster (len(labels) > 1) y que el número de anomalías (etiqueta -1) sea menor que el que hemos calculado (anomalies_values). Al salir de ese bucle usamos el valor de epsilon que hemos obtenido, que en nuestro caso es un valor de 0.4. Obtenemos dos clústeres diferentes y un total de 0 anomalías u outliers. Gráficamente los podemos apreciar en el gráfico generado.

Resultado obtenido de la clusterización usando un valor de epsilón de 0.4.
Resultado obtenido de la clusterización usando un valor de epsilón de 0.4.

Resumiendo

Hemos visto en este artículo la necesidad de los métodos de clusterización para la resolución de algunos problemas de aprendizaje automático. Se han usado dos algoritmos para la resolución de dos problemas bien diferenciados. Se han visibilizado los problemas que puede presentar cada algoritmo y como emplearlo de forma automatizada, en el caso de DBSCAN.

Todos el material usado lo puedes encontrar como siempre en el repositorio de código fuente de la página y mas concretamente en el siguiente enlace.

Bibliografía

Sugerencias

    captcha

    Comparte el artículo

    Entradas relacionadas