¿Es posible especificar su propia función de distancia usando scikit-learn K-Means Clustering?

172

¿Es posible especificar su propia función de distancia usando scikit-learn K-Means Clustering?

bmasc
fuente
37
Tenga en cuenta que k-means está diseñado para la distancia euclidiana . Puede dejar de converger con otras distancias, cuando la media ya no es una mejor estimación para el "centro" del grupo.
HA SALIDO - Anony-Mousse
2
¿Por qué k-means solo funciona con distensión euclidiana?
curioso
9
@ Anony-Mousse Es incorrecto decir que k-means solo está diseñado para la distancia euclidiana. Se puede modificar para trabajar con cualquier métrica de distancia válida definida en el espacio de observación. Por ejemplo, eche un vistazo al artículo sobre k-medoides .
ely
55
@curious: la media minimiza las diferencias al cuadrado (= distancia euclidiana al cuadrado). Si desea una función de distancia diferente, debe reemplazar la media con una estimación central adecuada. K-medoides es un algoritmo de este tipo, pero encontrar el medoide es mucho más costoso.
HA SALIDO - Anony-Mousse
44
Algo relevante aquí: actualmente hay una solicitud de extracción abierta que implementa Kernel K-Means. Cuando termine, podrá especificar su propio núcleo para el cálculo.
jakevdp

Respuestas:

77

Aquí hay un pequeño kmeans que usa cualquiera de las 20 distancias más o menos en scipy.spatial.distance , o una función de usuario.
Los comentarios serían bienvenidos (hasta ahora solo ha tenido un usuario, no lo suficiente); en particular, ¿cuáles son sus N, dim, k, métricas?

#!/usr/bin/env python
# kmeans.py using any of the 20-odd metrics in scipy.spatial.distance
# kmeanssample 2 pass, first sample sqrt(N)

from __future__ import division
import random
import numpy as np
from scipy.spatial.distance import cdist  # $scipy/spatial/distance.py
    # http://docs.scipy.org/doc/scipy/reference/spatial.html
from scipy.sparse import issparse  # $scipy/sparse/csr.py

__date__ = "2011-11-17 Nov denis"
    # X sparse, any cdist metric: real app ?
    # centres get dense rapidly, metrics in high dim hit distance whiteout
    # vs unsupervised / semi-supervised svm

#...............................................................................
def kmeans( X, centres, delta=.001, maxiter=10, metric="euclidean", p=2, verbose=1 ):
    """ centres, Xtocentre, distances = kmeans( X, initial centres ... )
    in:
        X N x dim  may be sparse
        centres k x dim: initial centres, e.g. random.sample( X, k )
        delta: relative error, iterate until the average distance to centres
            is within delta of the previous average distance
        maxiter
        metric: any of the 20-odd in scipy.spatial.distance
            "chebyshev" = max, "cityblock" = L1, "minkowski" with p=
            or a function( Xvec, centrevec ), e.g. Lqmetric below
        p: for minkowski metric -- local mod cdist for 0 < p < 1 too
        verbose: 0 silent, 2 prints running distances
    out:
        centres, k x dim
        Xtocentre: each X -> its nearest centre, ints N -> k
        distances, N
    see also: kmeanssample below, class Kmeans below.
    """
    if not issparse(X):
        X = np.asanyarray(X)  # ?
    centres = centres.todense() if issparse(centres) \
        else centres.copy()
    N, dim = X.shape
    k, cdim = centres.shape
    if dim != cdim:
        raise ValueError( "kmeans: X %s and centres %s must have the same number of columns" % (
            X.shape, centres.shape ))
    if verbose:
        print "kmeans: X %s  centres %s  delta=%.2g  maxiter=%d  metric=%s" % (
            X.shape, centres.shape, delta, maxiter, metric)
    allx = np.arange(N)
    prevdist = 0
    for jiter in range( 1, maxiter+1 ):
        D = cdist_sparse( X, centres, metric=metric, p=p )  # |X| x |centres|
        xtoc = D.argmin(axis=1)  # X -> nearest centre
        distances = D[allx,xtoc]
        avdist = distances.mean()  # median ?
        if verbose >= 2:
            print "kmeans: av |X - nearest centre| = %.4g" % avdist
        if (1 - delta) * prevdist <= avdist <= prevdist \
        or jiter == maxiter:
            break
        prevdist = avdist
        for jc in range(k):  # (1 pass in C)
            c = np.where( xtoc == jc )[0]
            if len(c) > 0:
                centres[jc] = X[c].mean( axis=0 )
    if verbose:
        print "kmeans: %d iterations  cluster sizes:" % jiter, np.bincount(xtoc)
    if verbose >= 2:
        r50 = np.zeros(k)
        r90 = np.zeros(k)
        for j in range(k):
            dist = distances[ xtoc == j ]
            if len(dist) > 0:
                r50[j], r90[j] = np.percentile( dist, (50, 90) )
        print "kmeans: cluster 50 % radius", r50.astype(int)
        print "kmeans: cluster 90 % radius", r90.astype(int)
            # scale L1 / dim, L2 / sqrt(dim) ?
    return centres, xtoc, distances

#...............................................................................
def kmeanssample( X, k, nsample=0, **kwargs ):
    """ 2-pass kmeans, fast for large N:
        1) kmeans a random sample of nsample ~ sqrt(N) from X
        2) full kmeans, starting from those centres
    """
        # merge w kmeans ? mttiw
        # v large N: sample N^1/2, N^1/2 of that
        # seed like sklearn ?
    N, dim = X.shape
    if nsample == 0:
        nsample = max( 2*np.sqrt(N), 10*k )
    Xsample = randomsample( X, int(nsample) )
    pass1centres = randomsample( X, int(k) )
    samplecentres = kmeans( Xsample, pass1centres, **kwargs )[0]
    return kmeans( X, samplecentres, **kwargs )

def cdist_sparse( X, Y, **kwargs ):
    """ -> |X| x |Y| cdist array, any cdist metric
        X or Y may be sparse -- best csr
    """
        # todense row at a time, v slow if both v sparse
    sxy = 2*issparse(X) + issparse(Y)
    if sxy == 0:
        return cdist( X, Y, **kwargs )
    d = np.empty( (X.shape[0], Y.shape[0]), np.float64 )
    if sxy == 2:
        for j, x in enumerate(X):
            d[j] = cdist( x.todense(), Y, **kwargs ) [0]
    elif sxy == 1:
        for k, y in enumerate(Y):
            d[:,k] = cdist( X, y.todense(), **kwargs ) [0]
    else:
        for j, x in enumerate(X):
            for k, y in enumerate(Y):
                d[j,k] = cdist( x.todense(), y.todense(), **kwargs ) [0]
    return d

def randomsample( X, n ):
    """ random.sample of the rows of X
        X may be sparse -- best csr
    """
    sampleix = random.sample( xrange( X.shape[0] ), int(n) )
    return X[sampleix]

def nearestcentres( X, centres, metric="euclidean", p=2 ):
    """ each X -> nearest centre, any metric
            euclidean2 (~ withinss) is more sensitive to outliers,
            cityblock (manhattan, L1) less sensitive
    """
    D = cdist( X, centres, metric=metric, p=p )  # |X| x |centres|
    return D.argmin(axis=1)

def Lqmetric( x, y=None, q=.5 ):
    # yes a metric, may increase weight of near matches; see ...
    return (np.abs(x - y) ** q) .mean() if y is not None \
        else (np.abs(x) ** q) .mean()

#...............................................................................
class Kmeans:
    """ km = Kmeans( X, k= or centres=, ... )
        in: either initial centres= for kmeans
            or k= [nsample=] for kmeanssample
        out: km.centres, km.Xtocentre, km.distances
        iterator:
            for jcentre, J in km:
                clustercentre = centres[jcentre]
                J indexes e.g. X[J], classes[J]
    """
    def __init__( self, X, k=0, centres=None, nsample=0, **kwargs ):
        self.X = X
        if centres is None:
            self.centres, self.Xtocentre, self.distances = kmeanssample(
                X, k=k, nsample=nsample, **kwargs )
        else:
            self.centres, self.Xtocentre, self.distances = kmeans(
                X, centres, **kwargs )

    def __iter__(self):
        for jc in range(len(self.centres)):
            yield jc, (self.Xtocentre == jc)

#...............................................................................
if __name__ == "__main__":
    import random
    import sys
    from time import time

    N = 10000
    dim = 10
    ncluster = 10
    kmsample = 100  # 0: random centres, > 0: kmeanssample
    kmdelta = .001
    kmiter = 10
    metric = "cityblock"  # "chebyshev" = max, "cityblock" L1,  Lqmetric
    seed = 1

    exec( "\n".join( sys.argv[1:] ))  # run this.py N= ...
    np.set_printoptions( 1, threshold=200, edgeitems=5, suppress=True )
    np.random.seed(seed)
    random.seed(seed)

    print "N %d  dim %d  ncluster %d  kmsample %d  metric %s" % (
        N, dim, ncluster, kmsample, metric)
    X = np.random.exponential( size=(N,dim) )
        # cf scikits-learn datasets/
    t0 = time()
    if kmsample > 0:
        centres, xtoc, dist = kmeanssample( X, ncluster, nsample=kmsample,
            delta=kmdelta, maxiter=kmiter, metric=metric, verbose=2 )
    else:
        randomcentres = randomsample( X, ncluster )
        centres, xtoc, dist = kmeans( X, randomcentres,
            delta=kmdelta, maxiter=kmiter, metric=metric, verbose=2 )
    print "%.0f msec" % ((time() - t0) * 1000)

    # also ~/py/np/kmeans/test-kmeans.py

Algunas notas agregadas 26mar 2012:

1) para la distancia cosenoidal, primero normalice todos los vectores de datos a | X | = 1; luego

cosinedistance( X, Y ) = 1 - X . Y = Euclidean distance |X - Y|^2 / 2

es rápido. Para los vectores de bits, mantenga las normas separadas de los vectores en lugar de expandirse a flotantes (aunque algunos programas pueden expandirse por usted). Para vectores dispersos, digamos 1% de N, X. Y debería tomar tiempo O (2% N), espacio O (N); pero no sé qué programas hacen eso.

2) El agrupamiento Scikit-learn ofrece una excelente visión general de k-means, mini-batch-k-means ... con código que funciona en matrices scipy.sparse.

3) Verifique siempre los tamaños de racimo después de k-means. Si espera grupos de aproximadamente el mismo tamaño, pero salen [44 37 9 5 5] %... (sonido de rascarse la cabeza).

denis
fuente
1
+1 En primer lugar, gracias por compartir su implementación. Solo quería confirmar que el algoritmo funciona muy bien para mi conjunto de datos de 900 vectores en un espacio de 700 dimensiones. Me preguntaba si también es posible evaluar la calidad de los clústeres generados. ¿Se puede reutilizar alguno de los valores en su código para calcular la calidad del clúster para ayudar a seleccionar el número de clústeres óptimos?
Leyenda
66
Leyenda, de nada. (Se actualizó el código para imprimir el clúster con un radio del 50% / 90%). La "calidad del clúster" es un tema muy amplio: ¿cuántos clústeres tiene? ¿Tiene muestras de capacitación con clústeres conocidos, por ejemplo, de expertos? En cuanto a la cantidad de clústeres, consulte SO cómo-hago-i-determinar-k-cuándo-usar-k-means-clustering -when-using-k-means-clustering
denis
1
Gracias otra vez. En realidad, no tengo las muestras de entrenamiento, pero estoy tratando de verificar los grupos manualmente después de la clasificación (tratando de desempeñar el papel del experto en el dominio también). Estoy realizando una clasificación a nivel de documento después de aplicar SVD a algunos documentos originales y reducir su dimensión. Los resultados parecen buenos, pero no estaba seguro de cómo validarlos. Para la etapa inicial, mientras exploraba varias métricas de validez de clúster, me encontré con el índice de Dunn, el método Elbow, etc., no estaba realmente seguro de cuál utilizar, así que pensé que comenzaría con el método Elbow.
Leyenda
77
Sé que esto está desenterrando algo realmente viejo, pero comencé a usar kmeans y me topé con esto. Para futuros lectores tentados a usar este código: ¡primero echen un vistazo a los comentarios de @ Anony-Mousse sobre la pregunta anterior! Esta implementación, por lo que puedo ver, está suponiendo erróneamente que de alguna manera todavía puedes usar la "media de puntos en un grupo" para determinar el centroide de ese grupo. Esto no tiene sentido para nada más que la distancia euclidiana (excepto en casos muy específicos en la esfera de la unidad, etc.). Nuevamente, los comentarios de Anony-Mousse sobre la pregunta son acertados.
Nevoris
3
@Nevoris, sí, estoy de acuerdo, excepto por la distancia del coseno: vea aquí por qué, también por qué-k-significa-clustering-algoritmo-use-only-euclidean-distance-metric
denis
43

Desafortunadamente no: la implementación actual de k-means en scikit-learn solo usa distancias euclidianas.

No es trivial extender k-means a otras distancias y la respuesta de denis anterior no es la forma correcta de implementar k-means para otras métricas.

ogrisel
fuente
26

Simplemente use nltk en su lugar donde puede hacer esto, por ejemplo

from nltk.cluster.kmeans import KMeansClusterer
NUM_CLUSTERS = <choose a value>
data = <sparse matrix that you would normally give to scikit>.toarray()

kclusterer = KMeansClusterer(NUM_CLUSTERS, distance=nltk.cluster.util.cosine_distance, repeats=25)
assigned_clusters = kclusterer.cluster(data, assign_clusters=True)
mdubez
fuente
44
¿Qué tan eficiente es esta implementación? Parece tomar una eternidad agrupar tan poco como 5k puntos (en la dimensión 100).
Nikana Reklawyks
3
En la dimensión 100, agrupar 1k puntos toma 1 segundo por carrera ( repeats), 1.5k puntos toman 2 minutos y 2k toma ... demasiado tiempo.
Nikana Reklawyks
2
En efecto; según el comentario de @ Anony-Mousse a continuación, parece que la distancia coseno puede tener problemas de convergencia. Para mí, este es realmente un caso de basura en la basura: podría usar la función de distancia que desee, pero si esa función viola los supuestos del algoritmo, ¡no espere que produzca resultados significativos!
Chiraz BenAbdelkader
15

Sí, puede usar una función métrica de diferencia; sin embargo, por definición, el algoritmo de agrupamiento de k-medias se basa en la distancia eucldiana de la media de cada grupo.

Podría usar una métrica diferente, por lo que, aunque todavía está calculando la media, podría usar algo como la distancia mahalnobis.

Adán
fuente
25
+1: Permítanme enfatizar que tomar la media solo es apropiado para ciertas funciones de distancia, como la distancia euclidiana . ¡Para otras funciones de distancia, también necesitaría reemplazar la función de estimación de centro de clúster!
HA SALIDO - Anony-Mousse
2
@ Anony-Mousse. ¿Qué se supone que debo cambiar cuando uso la distancia coseno, por ejemplo?
curioso
66
No lo sé. No he visto una prueba de convergencia con Cosine. Creo que convergerá si sus datos no son negativos y están normalizados a la esfera de la unidad, porque entonces es esencialmente k-significa en un espacio vectorial diferente.
HA SALIDO - Anony-Mousse
1
Estoy de acuerdo con @ Anony-Mousse. Para mí, esto es solo un caso de basura en la basura: podría ejecutar K-means con cualquier función de distancia que desee, pero si esa función viola los supuestos subyacentes del algoritmo, no espere que produzca un significado resultados!
Chiraz BenAbdelkader
@ Anony-Mousse, pero ¿cómo implementar K-means utilizando la distancia mahalnobis?
Cecilia
7

Hay pyclustering que es python / C ++ (¡así que es rápido!) Y le permite especificar una función métrica personalizada

from pyclustering.cluster.kmeans import kmeans
from pyclustering.utils.metric import type_metric, distance_metric

user_function = lambda point1, point2: point1[0] + point2[0] + 2
metric = distance_metric(type_metric.USER_DEFINED, func=user_function)

# create K-Means algorithm with specific distance metric
start_centers = [[4.7, 5.9], [5.7, 6.5]];
kmeans_instance = kmeans(sample, start_centers, metric=metric)

# run cluster analysis and obtain results
kmeans_instance.process()
clusters = kmeans_instance.get_clusters()

En realidad, no he probado este código, pero lo he improvisado a partir de un ticket y un código de ejemplo .

CpILL
fuente
necesita Matplotlib instalado que necesita "Python como marco en Mac OS X" :(
CpILL
3

Sklearn Kmeans usa la distancia euclidiana . No tiene parámetro métrico. Dicho esto, si usted está agrupamiento de series de tiempo , se puede utilizar el tslearnpaquete python, cuando se puede especificar una métrica ( dtw, softdtw, euclidean).

Tawej
fuente