k means - contrib - ¿Cómo implementaría k-means con TensorFlow?
tensorflow contrib factorization k means (3)
El tutorial introductorio, que utiliza el optimizador de descenso degradado incorporado, tiene mucho sentido. Sin embargo, k-means no es solo algo que puedo conectar al descenso de gradiente. Parece que tendría que escribir mi propio tipo de optimizador, pero no estoy muy seguro de cómo hacerlo dado los primitivos de TensorFlow.
¿Qué enfoque debo tomar?
La mayoría de las respuestas que he visto hasta ahora se centran solo en la versión 2d (cuando necesitas agrupar puntos en 2 dimensiones). Aquí está mi implementación de la agrupación en dimensiones arbitrarias.
Idea básica del algoritmo k-means en n dims:
- generar al azar k puntos de partida
- haga esto hasta que exceda la paciencia o la asignación del clúster no cambie:
- asignar cada punto al punto de partida más cercano
- recalcular la ubicación de cada punto de partida tomando el promedio entre su grupo
Para poder validar de alguna manera los resultados, intentaré agrupar las imágenes MNIST.
import numpy as np
import tensorflow as tf
from random import randint
from collections import Counter
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/")
X, y, k = mnist.test.images, mnist.test.labels, 10
Entonces, aquí X es mi información para agrupar (10000, 784)
, y es el número real, yk es el número de clúster (que es lo mismo que el número de dígitos. Ahora el algoritmo real:
# select random points as a starting position. You can do better by randomly selecting k points.
start_pos = tf.Variable(X[np.random.randint(X.shape[0], size=k),:], dtype=tf.float32)
centroids = tf.Variable(start_pos.initialized_value(), ''S'', dtype=tf.float32)
# populate points
points = tf.Variable(X, ''X'', dtype=tf.float32)
ones_like = tf.ones((points.get_shape()[0], 1))
prev_assignments = tf.Variable(tf.zeros((points.get_shape()[0], ), dtype=tf.int64))
# find the distance between all points: http://.com/a/43839605/1090562
p1 = tf.matmul(
tf.expand_dims(tf.reduce_sum(tf.square(points), 1), 1),
tf.ones(shape=(1, k))
)
p2 = tf.transpose(tf.matmul(
tf.reshape(tf.reduce_sum(tf.square(centroids), 1), shape=[-1, 1]),
ones_like,
transpose_b=True
))
distance = tf.sqrt(tf.add(p1, p2) - 2 * tf.matmul(points, centroids, transpose_b=True))
# assign each point to a closest centroid
point_to_centroid_assignment = tf.argmin(distance, axis=1)
# recalculate the centers
total = tf.unsorted_segment_sum(points, point_to_centroid_assignment, k)
count = tf.unsorted_segment_sum(ones_like, point_to_centroid_assignment, k)
means = total / count
# continue if there is any difference between the current and previous assignment
is_continue = tf.reduce_any(tf.not_equal(point_to_centroid_assignment, prev_assignments))
with tf.control_dependencies([is_continue]):
loop = tf.group(centroids.assign(means), prev_assignments.assign(point_to_centroid_assignment))
sess = tf.Session()
sess.run(tf.global_variables_initializer())
# do many iterations. Hopefully you will stop because of has_changed is False
has_changed, cnt = True, 0
while has_changed and cnt < 300:
cnt += 1
has_changed, _ = sess.run([is_continue, loop])
# see how the data is assigned
res = sess.run(point_to_centroid_assignment)
Ahora es el momento de comprobar qué tan buenos son nuestros clusters. Para hacer esto agruparemos juntos todos los números reales que aparecieron en el grupo. Después de eso, veremos las opciones más populares en ese clúster. En el caso del agrupamiento perfecto tendremos el único valor en cada grupo. En caso de clúster aleatorio, cada valor se representará de manera aproximadamente igual en el grupo.
nums_in_clusters = [[] for i in xrange(10)]
for cluster, real_num in zip(list(res), list(y)):
nums_in_clusters[cluster].append(real_num)
for i in xrange(10):
print Counter(nums_in_clusters[i]).most_common(3)
Esto me da algo como esto:
[(0, 738), (6, 18), (2, 11)]
[(1, 641), (3, 53), (2, 51)]
[(1, 488), (2, 115), (7, 56)]
[(4, 550), (9, 533), (7, 280)]
[(7, 634), (9, 400), (4, 302)]
[(6, 649), (4, 27), (0, 14)]
[(5, 269), (6, 244), (0, 161)]
[(8, 646), (5, 164), (3, 125)]
[(2, 698), (3, 34), (7, 14)]
[(3, 712), (5, 290), (8, 110)]
Esto es bastante bueno porque la mayoría de los conteos está en el primer grupo. Usted ve que la agrupación confunde 7 y 9, 4 y 5. Pero 0 se agrupa muy bien.
Algunos enfoques cómo mejorar esto:
- ejecutar el algoritmo varias veces y seleccionar el mejor (según la distancia a los clusters)
- manejo de casos cuando no se asigna nada a un clúster. En mi caso obtendrás Nan en variable
means
porquecount
es 0. - inicialización de puntos aleatorios.
solo use tf.contrib.learn.KMeansClustering
(nota: ahora puede obtener una versión más pulida de este código como esencia en github ).
definitivamente puede hacerlo, pero necesita definir sus propios criterios de optimización (para k-means, usualmente es un recuento máximo de iteraciones y cuando la asignación se estabiliza). Aquí hay un ejemplo de cómo podría hacerlo (probablemente haya formas más óptimas de implementarlo, y definitivamente mejores formas de seleccionar los puntos iniciales). Básicamente es como si lo hubieras hecho en numpy si estuvieras tratando realmente de mantenerte alejado de hacer cosas iterativamente en python:
import tensorflow as tf
import numpy as np
import time
N=10000
K=4
MAX_ITERS = 1000
start = time.time()
points = tf.Variable(tf.random_uniform([N,2]))
cluster_assignments = tf.Variable(tf.zeros([N], dtype=tf.int64))
# Silly initialization: Use the first two points as the starting
# centroids. In the real world, do this better.
centroids = tf.Variable(tf.slice(points.initialized_value(), [0,0], [K,2]))
# Replicate to N copies of each centroid and K copies of each
# point, then subtract and compute the sum of squared distances.
rep_centroids = tf.reshape(tf.tile(centroids, [N, 1]), [N, K, 2])
rep_points = tf.reshape(tf.tile(points, [1, K]), [N, K, 2])
sum_squares = tf.reduce_sum(tf.square(rep_points - rep_centroids),
reduction_indices=2)
# Use argmin to select the lowest-distance point
best_centroids = tf.argmin(sum_squares, 1)
did_assignments_change = tf.reduce_any(tf.not_equal(best_centroids,
cluster_assignments))
def bucket_mean(data, bucket_ids, num_buckets):
total = tf.unsorted_segment_sum(data, bucket_ids, num_buckets)
count = tf.unsorted_segment_sum(tf.ones_like(data), bucket_ids, num_buckets)
return total / count
means = bucket_mean(points, best_centroids, K)
# Do not write to the assigned clusters variable until after
# computing whether the assignments have changed - hence with_dependencies
with tf.control_dependencies([did_assignments_change]):
do_updates = tf.group(
centroids.assign(means),
cluster_assignments.assign(best_centroids))
sess = tf.Session()
sess.run(tf.initialize_all_variables())
changed = True
iters = 0
while changed and iters < MAX_ITERS:
iters += 1
[changed, _] = sess.run([did_assignments_change, do_updates])
[centers, assignments] = sess.run([centroids, cluster_assignments])
end = time.time()
print ("Found in %.2f seconds" % (end-start)), iters, "iterations"
print "Centroids:"
print centers
print "Cluster assignments:", assignments
(Tenga en cuenta que una implementación real debería ser más cuidadosa con la selección inicial del clúster, evitando casos problemáticos con todos los puntos dirigidos a un clúster, etc. Esta es solo una demostración rápida. He actualizado mi respuesta anterior para que sea un poco más claro y "digno de ejemplo").