È possibile specificare la propria funzione di distanza utilizzando il clustering dei mezzi K di scikit-learn?


172

È possibile specificare la propria funzione di distanza utilizzando il clustering dei mezzi K di scikit-learn?


37
Nota che k-medie è progettato per la distanza euclidea . Potrebbe smettere di convergere con altre distanze, quando la media non è più una migliore stima per il "centro" del cluster.
Ha QUIT - Anony-Mousse il

2
perché k-mean funziona solo con la distanza euclidea?
curioso

9
@ Anony-Mousse Non è corretto affermare che k-mean sia progettato solo per la distanza euclidea. Può essere modificato per funzionare con qualsiasi metrica di distanza valida definita nello spazio di osservazione. Ad esempio, dai un'occhiata all'articolo su k-medoids .
ely,

5
@curious: la media minimizza le differenze al quadrato (= distanza euclidea quadrata). Se si desidera una funzione di distanza diversa, è necessario sostituire la media con una stima del centro appropriata. K-medoids è un tale algoritmo, ma trovare il medoid è molto più costoso.
Ha QUIT - Anony-Mousse il

4
Un po 'rilevante qui: al momento esiste una richiesta pull aperta che implementa K-Keans del kernel. Al termine sarai in grado di specificare il tuo kernel per il calcolo.
Jakevdp,

Risposte:


77

Ecco un piccolo kmean che utilizza una qualsiasi delle 20 distanze dispari in scipy.spatial.distance o una funzione utente.
I commenti sarebbero ben accetti (finora ha avuto un solo utente, non abbastanza); in particolare, quali sono le tue N, dim, k, metrica?

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

Alcune note aggiunte 26mar 2012:

1) per la distanza del coseno, normalizzare innanzitutto tutti i vettori di dati su | X | = 1; poi

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

è veloce. Per i vettori di bit, mantieni le norme separatamente dai vettori invece di espandersi in float (anche se alcuni programmi potrebbero espandersi per te). Per i vettori radi, dire l'1% di N, X. Y dovrebbe impiegare il tempo O (2% N), spazio O (N); ma non so quali programmi lo facciano.

2) Il clustering Scikit-learn offre un'eccellente panoramica di k-medie, mini-batch-k-medie ... con codice che funziona su matrici scipy.sparse.

3) Controllare sempre le dimensioni dei cluster dopo k-significa. Se ti aspetti gruppi di dimensioni approssimativamente uguali, ma escono [44 37 9 5 5] %... (suono di grattarsi la testa).


1
+1 Prima di tutto, grazie per aver condiviso la tua implementazione. Volevo solo confermare che l'algoritmo funziona benissimo per il mio set di dati di 900 vettori in uno spazio di 700 dimensioni. Mi chiedevo solo se fosse anche possibile valutare la qualità dei cluster generati. È possibile riutilizzare qualcuno dei valori nel codice per calcolare la qualità del cluster per facilitare la selezione del numero di cluster ottimali?
Legenda

6
Leggenda, prego. (Aggiornato il codice per stampare il raggio del 50% / 90% del cluster). La "qualità dei cluster" è un argomento di vasta portata: quanti cluster hai, hai campioni di addestramento con cluster noti, ad esempio di esperti? Per quanto riguarda il numero di cluster, vedere SO how-do-i-determina-k-quando-usando-k-significa-clustering -quando-usando-k-significa-clustering
denis

1
Grazie ancora. In realtà, non ho i campioni di addestramento ma sto provando a verificare manualmente i cluster dopo la classificazione (cercando di svolgere anche il ruolo dell'esperto di dominio). Sto eseguendo una classificazione a livello di documento dopo aver applicato SVD ad alcuni documenti originali e averne ridotto la dimensione. I risultati sembrano buoni ma non ero sicuro di come convalidarli. Per la fase iniziale, durante l'esplorazione di varie metriche di validità dei cluster, mi sono imbattuto nell'indice di Dunn, nel metodo Elbow, ecc., Non ero davvero sicuro di quale utilizzare, quindi ho pensato di iniziare con il metodo Elbow.
Legenda

7
So che questo è non-terra qualcosa di veramente vecchio, ma ho appena iniziato a usare kmean e mi sono imbattuto in questo. Per i futuri lettori tentati di usare questo codice: controlla prima i commenti di @ Anony-Mousse sulla domanda sopra! Questa implementazione, per quanto posso vedere, sta facendo l'ipotesi sbagliata che in qualche modo è ancora possibile utilizzare la "media dei punti in un cluster" per determinare il centroide di quel cluster. Questo non ha senso per nient'altro che la distanza euclidea (tranne in casi molto specifici sulla sfera unitaria, ecc ...). Ancora una volta i commenti di Anony-Mousse sulla domanda sono proprio al naso.
Nevoris,

3
@Nevoris, sì, sono d'accordo, tranne per la distanza del coseno: vedi qui per perché, anche perché-k-significa-clustering-algoritmo-uso-solo-euclideo-metrica-distanza
denis

43

Sfortunatamente no: scikit-learn l'attuale implementazione di k-medie usa solo distanze euclidee.

Non è banale estendere k-medie ad altre distanze e la risposta di denis sopra non è il modo corretto di implementare k-medie per altre metriche.


26

Usa nltk invece dove puoi farlo, ad es

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)

4
Quanto è efficiente questa implementazione? Sembra impiegare un'eternità a raggruppare un minimo di 5k punti (nella dimensione 100).
Nikana Reklawyks il

3
Nella dimensione 100, il raggruppamento di 1k punti richiede 1 secondo per corsa ( repeats), 1,5k punti richiedono 2 minuti e 2k richiede ... troppo tempo.
Nikana Reklawyks il

2
Infatti; come da @ Anony-Mousse commento sotto, sembra che la distanza del coseno possa avere problemi di convergenza. Per me, questo è davvero un caso di garbage-in-garbage-out: potresti usare qualsiasi funzione di distanza tu voglia, ma se tale funzione viola i presupposti dell'algoritmo, non aspettarti che produca risultati significativi!
Chiraz BenAbdelkader,

15

Sì, è possibile utilizzare una funzione metrica di differenza; tuttavia, per definizione, l'algoritmo di clustering dei k-media si basa sulla distanza eucldiea dalla media di ciascun cluster.

È possibile utilizzare una metrica diversa, quindi anche se si sta ancora calcolando la media, è possibile utilizzare qualcosa come la distanza mahalnobis.


25
+1: Vorrei sottolineare che assumere la media è appropriato solo per determinate funzioni di distanza, come la distanza euclidea . Per altre funzioni di distanza, è necessario sostituire anche la funzione di stima al centro del cluster!
Ha QUIT - Anony-Mousse il

2
@ Anony-Mousse. Cosa dovrei cambiare quando uso, ad esempio, la distanza del coseno?
curioso

6
Non lo so. Non ho visto una prova della convergenza con Cosine. Credo che convergeranno se i tuoi dati non sono negativi e normalizzati nella sfera unitaria, perché quindi sono essenzialmente k-significa in uno spazio vettoriale diverso.
Ha QUIT - Anony-Mousse il

1
Sono d'accordo con @ Anony-Mousse. Per me, questo è solo un caso di garbage-in-garbage-out: potresti eseguire K-mean con qualunque funzione di distanza desideri, ma se tale funzione viola i presupposti sottostanti dell'algoritmo, non aspettarti che produca un significato risultati!
Chiraz BenAbdelkader,

@ Anony-Mousse ma come implementare K-mean usando la distanza mahalnobis?
Cecilia,

7

C'è pyclustering che è python / C ++ (quindi è veloce!) E consente di specificare una funzione metrica personalizzata

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()

In realtà, non ho testato questo codice ma l'ho messo insieme da un ticket e un codice di esempio .


richiede l'installazione di Matplotlib che richiede "Python come framework su Mac OS X" :(
CpILL


3

Sklearn Kmeans utilizza la distanza euclidea . Non ha parametri metrici. Detto questo, se si sta clustering di serie storiche , è possibile utilizzare il tslearnpacchetto python, quando è possibile specificare una metrica ( dtw, softdtw, euclidean).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.