Análisis Cluster (III): Clasificación no supervisada mediante K-medias

(Este es el tercer capítulo de la mini-serie de artículos sobre análisis cluster que estamos haciendo en pybonacci, si todavía no has leído los anteriores artículos les puedes echar un ojo ahora. Escribir esta tercera parte solo ha tardado ¡¡¡tres años en salir!!!).
El algoritmo de k-medias es uno de los algoritmos más sencillos de agrupamiento dentro del campo del aprendizaje automático (machine learning para los hipsters). Sirve para agrupar \(N\)-elementos en \(K\)-grupos distintos.

Agrupamiento

(Breve resumen de lo visto en anteriores artículos).
Las técnicas de agrupamiento o análisis Cluster son un tipo de aprendizaje no supervisado ya que no requieren un aprendizaje previo a partir de los datos.
Son métodos para agrupar datos sin etiqueta en subgrupos. Estos subgrupos pretenden reflejar algún tipo de estructura interna.

K-medias

El algoritmo de K-medias es un algoritmo de agrupamiento particional que nos crea \(K\) particiones de los \(N\) datos (con \(N \geq K\)). El algoritmo se puede resumir en los siguientes pasos:

  • Seleccionar \(K\) puntos de forma aleatoria en un dominio que comprende todos los puntos del conjunto de datos. estos \(K\) puntos representan los centros iniciales de los grupos (lo veremos en el método _init_centroids de la clase KMeans que luego explicaremos más en detalle). No vamos a implementar ningún método de posicionamiento inicial complejo, los centros iniciales serán valores aleatorios colocados entre los umbrales mínimos y máximos del conjunto de puntos (entre las \(x\), \(y\) mínimas y máximas). El problema que resolveremos será en dos dimensiones, como ya podéis intuir.
  • Asignar cada punto del conjunto de datos al grupo con el centro más próximo (lo veremos en el método _assign_centroids de la clase KMeans, llegaremos en breve).
  • Recalculamos los centros de los grupos a partir de todos los datos asignados a cada grupo, es decir, calculamos el centroide de cada grupo método _recalculate_centroids de la clase (KMeans).
  • Repetir los dos pasos anteriores hasta que se cumpla un condición que puede ser:
    • Se ha alcanzado un número máximo de iteraciones (no deseable).
    • Los centroides ya no cambian más allá de un rango.

Nosotros solo comprobaremos el segundo ya que solo vamos a manejar conjuntos pequeños de datos. Le pondremos un límite de variación a nuestra clase KMeans que deberán cumplir todos los centroides. La comprobación sobre si parar las iteraciones se realizarán en el método _check_stop de la clase KMeans.

Todo esto pretende ser más educativo que riguroso

La implementación que vamos a hacer será en Python puro ya que pretende ser educativa de cara a ayudar a conocer el funcionamiento del algoritmo paso a paso. Tenéis implementaciones mucho más desarrolladas, precisas, con mejor rendimiento,…, en paquetes como scipy, scikit-learn u otros.

La implementación

[AVISO PARA NAVEGANTES: Todo el código está pensado para funcionar en Python 3. Si usas Python 2 deberías empezar a pensar en actualizarte].
Primero escribo la clase a capón y luego la voy desgranando poco a poco. La he escrito como un iterador de forma que nos permita iterar fácilmente paso a paso (la iteración paso a paso la veremos de forma visual para intentar aportar aun mayor claridad). Para ver más sobre iteradores podéis DuckDuckGoear o ver este enlace.

La clase anterior paso a paso:

método __init__

Inicializamos la clase con los valores x de los puntos, los valores y de los puntos, el número de grupos a usar, n_clusters y el límite del desplazamiento de los grupos a partir del cual consideraremos que podemos parar de iterar, limit.
Además, a partir del los valores introducidos, extraemos las ventana umbral donde se colocan los puntos (atributos x_min, x_max, y_min e y_max) que después usaremos para inicializar los centroides (mediante _init_centroids).

método _init_centroids

Mediante este método, que se usa al inicializar la clase, creamos los centroides iniciales a partir de valores aleatorios situados entre los valores de x_min, x_max, y_min e y_max. Además, asignamos unos colores aleatorios a cada centroide (que luego usaremos en la visualización). Pensándolo fríamente, ahora que estoy escribiendo esto, la verdad que colors podría estar fuera de esta clase pero lo vamos a dejar así.
Una vez que se han creado los centroides hacemos la asignación de cada punto del conjunto de puntos \(x\) e \(y\) a cada centroide mediante el método _assign_centroids.

método _assign_centroids

en el atributo c (que es una lista) almacenamos el valor del grupo al que pertenece cada punto del conjunto de datos \(x\) e \(y\). Para ello, primero tenemos que calcular la distancia de cada punto a cada centroide y el que centroide que tenga menos distancia al punto será el asignado. Por tanto, en este paso, hemos de calcular \(N \cdot K\) distancias.

método _recalculate_centroids

En este paso, recalculamos los centroides. Cada nuevo centroide será el centroide de los puntos asignados a ese centroide. Los antiguos centroides los conservamos para poder compararlos con los nuevos y ver si han variado poco o mucho las nuevas posiciones de los centroides. Una vez que hemos calculado los nuevos centroides y que mantenemos los antiguos asignamos los puntos a los nuevos centroides mediante el método _assign_centroids explicado anteriormente.

método _check_stop

En este método calculamos si la diferencia entre la posición de los centroides antiguos y de los nuevos es superior o inferior al límite o umbral que hemos definido al instanciar la clase. Si cualquiera de los centroides se ha movido más del umbral definido seguiremos iterando (_check_stop devolverá False), si ninguno supera el umbral le diremos que pare de iterar (_check_stop devolverá True).

métodos __iter__ y __next__

Si os habéis leído el enlace sobre iteradores que he dejado más arriba espero que esto sea sencillo de entender.

  • El método __iter__ no necesita mayor explicación.
  • El método __next__ incrementa el atributo iterations, que no vamos a usar pero que podríamos usar para limitar, por ejemplo, el número de iteraciones, os lo dejo como ejercicio :-D, cada vez que damos un nuevo paso recalculamos los centroides y chequeamos si hay que parar la iteración porque hemos cumplido las condiciones fijadas (sí, el stop == True es redundante pero he pretendido que sea lo más claro posible).

Visualización

El hecho de crear la clase como un iterador es porque he pensado que podríamos iterar en cada paso y hacer una visualización interactiva. Como la visualizazión quiero que funcione cuando el notebook está estático he recurrido a usar Brythonmagic.
Si queréis saber más sobre Brythonmagic podéis leer el README del enlace anterior o ver esta entrada con vídeo y todo que muestra el funcionamiento.
Como resumen, básicamente es una magic function para el notebook que nos permite usar brython (una implementación de Python 3 hecha en javascript que funciona en el navegador sin necesidad de ningún kernel detrás).
Si no lo tienes instalado lo puedes instalar mediante pip.

Para poder hacer uso de la extensión la hemos de cargar mediante:

El paso siguiente nos permite usar toda la maquinaria de brython en el navegador dentro del notebook y es el último paso para que todo funcione.

Podemos comprobar si funciona con el siguiente ejemplo. Después de ejecutar la celda deberías de ver un mensaje en pantalla (esto seré un poco enojante cuando lo veamos en estático puesto que siempre saltará esta ventana…):

A continuación metemos el código de la clase KMeans que ya hemos detallado más arriba y la llamaremos desde otras celdas brython a partir del nombre que le hemos dado en esta celda (kmeans_class, ver primera línea de la celda)

El código que figura a continuación es parte de una librería que empecé hace un tiempo para hacer gráficos en el canvas del navegador de forma simple a partir de Brython. La funcionalidad básica está en el módulo base.py y lo que figura a continuación es parte de ese módulo modificado en algún sitio.
NO VOY A EXPLICAR ESTE CÓDIGO EN DETALLE PUESTO QUE NO ES PARTE DEL PROPÓSITO DE LA ENTRADA Y NO QUIERO DESVIAR LA ATENCIÓN. SI TENÉIS ALGUNA DUDA O INTERÉS PODÉIS USAR LOS COMENTARIOS DEL BLOG PARA ELLO.
El código a continuación permite dibujar sobre un canvas HTML5 diferentes formas. Solo vamos a definir formas para dibujar círculos, para los puntos y los centroides de los grupos, y líneas, para mostrar qué puntos se asignan a qué centroide en cada paso.

El siguiente código solo nos va a valer para establecer una mínima disposición de los elementos contenidos en la visualización.
Se incluye un canvas, donde se dibujarán las cosas, y un botón, para poder iterar el algoritmo.

Por último, este es el código que realiza todo a partir de los demás. Voy a intentar explicarlo un poco más en detalle:

  • fig = Figure('cnvs01', borderwidth = 2) inicializamos el canvas con un borde negro usando la clase Figure creada más arriba.
  • Definimos el número de puntos a usar y las posiciones de los puntos.
  • Inicializamos el objeto mediante kmeans = KMeans(x, y, n_clusters = 4, limit = 1). En este caso queremos que tenga 4 clusters (n_clusters) y que el límite (limit) para dejar de iterar sea cuando los centroides se muevan un pixel o menos.
  • La función plot hace varias cosas.
    • Primero suaviza los colores de la imagen previa antes de la actual iteración (esto está más relacionado con el canvas HTML5 y con javascript y no hace falta entenderlo mucho más allá de lo comentado en esta línea)
  • Después extraemos todos los datos del objeto (kmeans) que vamos a usar dentro de la función.

Dibujamos las líneas entre los puntos y los centroides y los puntos con colores asociados a cada grupo

Y finalmente se dibujan los centroides de cada grupo con un círculo un poco más grande que los propios puntos

  • Creamos, además, una función update que será la que llama a la función plot y la que llama a una nueva iteración. El código que figura en el bloque except, del doc['button'], es más de la parte de brython y sirve para que el botón desaparezca una vez que el iterador ha quedado exhausto (se han agotado las iteraciones).
  • La última línea de código, doc['button'].bind('click', update), asocia el evento click del botón a la función update que he comentado anteriormente.

Si ahora ejecutamos la siguiente celda de código deberíamos ver un canvas de 500px x 500px con un botón debajo. Si vamos pulsando al botón veremos como nueva iteración en acción, además de ver las anteriores iteraciones de forma difuminada. Una vez que hemos alcanzado la condición de que los centroides no se mueven más desaparecerá el botón (para no teneros ahí pulsando y que me digáis que eso no sigue funcionando…).

Podéis ver todo el código contenido en una página web aquí.

Conclusiones

Espero que haya quedado claro el funcionamiento básico del algoritmo K-Medias. También espero que si veis alguna errata en el código me aviséis o hagáis un Pull Request a nuestro repositorio de notebooks.
Podéis ver este notebook funcionando incluso en estático en el siguiente enlace.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

78 − = sixty nine