É possível especificar sua própria função de distância usando o K-Means Clustering do scikit-learn?

172

É possível especificar sua própria função de distância usando o K-Means Clustering do scikit-learn?

bmasc
fonte
37
Observe que o k-means é projetado para a distância euclidiana . Pode parar de convergir com outras distâncias, quando a média não é mais a melhor estimativa para o "centro" do cluster.
Quit - Anony-Mousse
2
por que o k-means funciona apenas com a distância euclidiana?
curioso
9
@ Anony-Mousse É incorreto dizer que o k-means é projetado apenas para a distância euclidiana. Ele pode ser modificado para funcionar com qualquer métrica de distância válida definida no espaço de observação. Por exemplo, dê uma olhada no artigo sobre k-medoids .
Ely
5
@curious: a média minimiza as diferenças ao quadrado (= distância euclidiana ao quadrado). Se você quiser uma função de distância diferente, precisará substituir a média por uma estimativa de centro apropriada. O K-medoids é um algoritmo desse tipo, mas encontrar o medóide é muito mais caro.
Quit - Anony-Mousse
4
Um pouco relevante aqui: atualmente há uma solicitação de recebimento aberto implementando o Kernel K-Means. Quando terminar, você poderá especificar seu próprio kernel para o cálculo.
jakevdp

Respostas:

77

Aqui estão alguns pequenos quilômetros que usam qualquer uma das 20 distâncias ímpares em scipy.spatial.distance ou uma função de usuário.
Os comentários seriam bem-vindos (até agora, apenas um usuário não foi suficiente); em particular, quais são suas métricas N, dim, k, métrica?

#!/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

Algumas notas adicionadas em 26 de março de 2012:

1) para a distância do cosseno, primeiro normalize todos os vetores de dados para | X | = 1; então

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

é rápido. Para vetores de bits, mantenha as normas separadamente dos vetores em vez de expandir para flutuadores (embora alguns programas possam expandir para você). Para vetores esparsos, diga 1% de N, X. Y deve levar tempo O (2% N), espaço O (N); mas não sei quais programas fazem isso.

2) O cluster do Scikit-learn fornece uma excelente visão geral de k-means, mini-batch-k-means ... com código que funciona em matrizes scipy.sparse.

3) Sempre verifique os tamanhos dos clusters após k-médias. Se você está esperando clusters do mesmo tamanho, mas eles saem [44 37 9 5 5] %... (som de coçar a cabeça).

denis
fonte
1
+1 Antes de tudo, obrigado por compartilhar sua implementação. Eu só queria confirmar que o algoritmo funciona muito bem para o meu conjunto de dados de 900 vetores em um espaço de 700 dimensões. Eu estava pensando se também é possível avaliar a qualidade dos clusters gerados. Algum dos valores do seu código pode ser reutilizado para calcular a qualidade do cluster para ajudar na seleção do número de clusters ideais?
Legend
6
Lenda, de nada. (Atualizado o código para imprimir o raio de 50% / 90% do cluster). "Qualidade de cluster" é um tópico amplo: quantos clusters você possui, você tem amostras de treinamento com clusters conhecidos, por exemplo, de especialistas? No número de agrupamentos, ver SO como-se-i-determinar-k-ao-usando-K-means- -quando utilizando-k-meio-agrupamento
Denis
1
Agradeço novamente. Na verdade, não tenho as amostras de treinamento, mas estou tentando verificar os clusters manualmente após a classificação (tentando desempenhar o papel de especialista em domínio também). Estou executando uma classificação no nível do documento após aplicar o SVD a alguns documentos originais e reduzir sua dimensão. Os resultados parecem bons, mas eu não tinha certeza de como validá-los. No estágio inicial, enquanto explorava várias métricas de validade de cluster, deparei-me com o método Index, Elbow, de Dunn, etc.
Legend
7
Sei que isso está desenterrando algo realmente antigo, mas comecei a usar kmeans e me deparei com isso. Para futuros leitores tentados a usar esse código: confira primeiro os comentários do Anony-Mousse sobre a questão acima! Esta implementação, tanto quanto posso ver, está assumindo erradamente que você ainda pode usar a "média de pontos em um cluster" para determinar o centróide desse cluster. Isso não faz sentido para outra coisa senão a distância euclidiana (exceto em casos muito específicos na esfera unitária, etc ...). Novamente, os comentários de Anony-Mousse sobre a questão estão no nariz.
Nevoris
3
@Nevoris, sim eu concordo, com exceção de distância cosseno: veja aqui por que, também por isso que-faz-k-médias-agrupamento-algoritmo-use-only-euclidiana distância métrica
denis
43

Infelizmente não: a implementação atual do scikit-learn do k-means usa apenas distâncias euclidianas.

Não é trivial estender os meios-k a outras distâncias e a resposta de denis acima não é a maneira correta de implementar os meios-k para outras métricas.

ogrisel
fonte
26

Basta usar o nltk onde você pode fazer isso, por exemplo

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
fonte
4
Qual é a eficiência dessa implementação? Parece levar uma eternidade para agrupar apenas 5 mil pontos (na dimensão 100).
você precisa saber é o seguinte
3
Na dimensão 100, o agrupamento de 1k pontos leva 1 segundo por execução ( repeats), 1,5k pontos leva 2 minutos e 2k leva ... muito tempo.
você precisa saber é o seguinte
2
De fato; de acordo com o comentário do @ Anony-Mousse abaixo, parece que a distância do cosseno pode ter problemas de convergência. Para mim, esse é realmente um caso de lixo-em-lixo-fora: você pode usar qualquer função de distância que desejar, mas se essa função viola as suposições do algoritmo, não espere que ele produza resultados significativos!
Chiraz BenAbdelkader
15

Sim, você pode usar uma função métrica de diferença; no entanto, por definição, o algoritmo de agrupamento k-means baseia-se na distância euclidiana da média de cada cluster.

Você pode usar uma métrica diferente, portanto, mesmo assim ainda calculando a média, poderá usar algo como a distância dos mahalnobis.

Adão
fonte
25
+1: Deixe-me enfatizar isso tomando a média só é adequado para determinadas funções de distância, tais como a distância euclidiana . Para outras funções de distância, você também precisará substituir a função de estimativa do centro de cluster!
Quit - Anony-Mousse
2
@ Anony-Mousse. O que devo mudar quando uso a distância do cosseno, por exemplo?
curioso
6
Eu não sei. Não vi uma prova de convergência com o Cosine. Acredito que convergirá se seus dados não forem negativos e normalizados para a esfera unitária, porque então são essencialmente k-means em um espaço vetorial diferente.
Quit - Anony-Mousse
1
Eu concordo com @ Anony-Mousse. Para mim, este é apenas um caso de lixo-em-lixo-fora: você pode executar K-means com qualquer função de distância que desejar, mas se essa função viola as suposições subjacentes do algoritmo, não espere que ele produza significantes resultados!
Chiraz BenAbdelkader
@ Anony-Mousse, mas como implementar o K-means usando a distância mahalnobis?
Cecilia
7

pyclustering, que é python / C ++ (então é rápido!) E permite que você especifique uma função 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()

Na verdade, eu não testei esse código, mas juntei-o de um código de exemplo e de ticket .

CpILL
fonte
precisa Matplotlib instalado que precisa "Python como um quadro no Mac OS X" :(
CpILL
3

O Sklearn Kmeans usa a distância euclidiana . Não possui parâmetro métrico. Dito isto, se você está agrupando série de tempo , você pode usar o tslearnpacote de python, quando você pode especificar uma métrica ( dtw, softdtw, euclidean).

Tawej
fonte