Est-il possible de spécifier votre propre fonction de distance en utilisant le clustering K-Means scikit-learn?

172

Est-il possible de spécifier votre propre fonction de distance en utilisant le clustering K-Means scikit-learn?

bmasc
la source
37
Notez que k-means est conçu pour la distance euclidienne . Il peut cesser de converger avec d'autres distances, lorsque la moyenne n'est plus une meilleure estimation pour le «centre» de la grappe.
A QUITTER - Anony-Mousse
2
pourquoi k-means ne fonctionne qu'avec la distance euclidienne?
curieux
9
@ Anony-Mousse Il est faux de dire que k-means n'est conçu que pour la distance euclidienne. Il peut être modifié pour fonctionner avec n'importe quelle métrique de distance valide définie sur l'espace d'observation. Par exemple, jetez un oeil à l'article sur algorithme des k-médoïdes .
ely
5
@curious: la moyenne minimise les différences au carré (= distance euclidienne au carré). Si vous souhaitez une fonction de distance différente, vous devez remplacer la moyenne par une estimation centrale appropriée. K-medoids est un tel algorithme, mais trouver le médoïde est beaucoup plus cher.
A QUITTER - Anony-Mousse
4
Un peu pertinent ici: il existe actuellement une demande d'extraction ouverte implémentant Kernel K-Means. Une fois terminé, vous pourrez spécifier votre propre noyau pour le calcul.
jakevdp

Réponses:

77

Voici un petit kmeans qui utilise l'une des 20 distances impaires dans scipy.spatial.distance , ou une fonction utilisateur.
Les commentaires seraient les bienvenus (cela n'a eu qu'un seul utilisateur jusqu'à présent, pas assez); en particulier, quels sont vos N, dim, k, métrique?

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

Quelques notes ajoutées le 26 mars 2012:

1) pour la distance cosinus, commencez par normaliser tous les vecteurs de données à | X | = 1; puis

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

est rapide. Pour les vecteurs de bits, gardez les normes séparément des vecteurs au lieu de les développer en flottants (bien que certains programmes puissent s'étendre pour vous). Pour les vecteurs clairsemés, disons 1% de N, X. Y doit prendre le temps O (2% N), l'espace O (N); mais je ne sais pas quels programmes font cela.

2) Le clustering Scikit-learn donne un excellent aperçu des k-means, mini-batch-k-means ... avec du code qui fonctionne sur les matrices scipy.sparse.

3) Vérifiez toujours les tailles de cluster après k-means. Si vous vous attendez à des clusters de taille à peu près égale, mais qu'ils sortent [44 37 9 5 5] %... (son de grattage de tête).

denis
la source
1
+1 Tout d'abord, merci d'avoir partagé votre implémentation. Je voulais juste confirmer que l'algorithme fonctionne très bien pour mon ensemble de données de 900 vecteurs dans un espace de 700 dimensions. Je me demandais simplement s'il est également possible d'évaluer la qualité des clusters générés. L'une des valeurs de votre code peut-elle être réutilisée pour calculer la qualité du cluster afin d'aider à sélectionner le nombre de clusters optimaux?
Legend
6
Légende, de rien. (Mise à jour du code pour imprimer un rayon de 50% / 90% du cluster). La «qualité des clusters» est un sujet de grande envergure: combien de clusters avez-vous, avez-vous des échantillons de formation avec des clusters connus, par exemple des experts? Sur le nombre de clusters, voir SO how-do-i-determine-k-when-using-k-means-clustering -when-using-k-means-clustering
denis
1
Merci une fois de plus. En fait, je n'ai pas les échantillons d'apprentissage mais j'essaie de vérifier les clusters manuellement après la classification (en essayant également de jouer le rôle de l'expert du domaine). J'effectue une classification au niveau du document après avoir appliqué SVD à certains documents originaux et réduit leur dimension. Les résultats semblent bons mais je ne savais pas trop comment les valider. Pour la phase initiale, tout en explorant diverses métriques de validité de cluster, je suis tombé sur l'index de Dunn, la méthode Elbow, etc.
Légende du
7
Je sais que c'est une mise à la terre de quelque chose de vraiment vieux, mais j'ai juste commencé à utiliser des kmeans et je suis tombé dessus. Pour les futurs lecteurs tentés d'utiliser ce code: consultez d'abord les commentaires @ Anony-Mousse sur la question ci-dessus! Cette implémentation, pour autant que je puisse voir, fait l'hypothèse erronée que vous pouvez toujours utiliser la «moyenne des points dans un cluster» pour déterminer le centre de gravité de ce cluster. Cela n'a aucun sens pour autre chose que la distance euclidienne (sauf dans des cas très spécifiques sur la sphère unitaire, etc ...). Encore une fois, les commentaires d'Anony-Mousse sur la question vont droit au nez.
Nevoris
3
@Nevoris, oui je suis d'accord, sauf pour la distance cosinus: voir ici pourquoi, aussi pourquoi-k-means-clustering-algorithm-use-only-euclidean-distance-metric
denis
43

Malheureusement non: l'implémentation actuelle de k-means par scikit-learn n'utilise que les distances euclidiennes.

Il n'est pas trivial d'étendre k-means à d'autres distances et la réponse de denis ci-dessus n'est pas la bonne façon d'implémenter les k-means pour d'autres métriques.

ogrisel
la source
26

Utilisez simplement nltk à la place où vous pouvez le faire, par exemple

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
la source
4
Quelle est l'efficacité de cette mise en œuvre? Il semble prendre une éternité pour regrouper aussi peu que 5k points (en dimension 100).
Nikana Reklawyks
3
Dans la dimension 100, le regroupement de 1k points prend 1 seconde par course ( repeats), 1,5k points prend 2 minutes et 2k prend ... trop de temps.
Nikana Reklawyks
2
En effet; selon le commentaire @ Anony-Mousse ci-dessous, il semble que la distance cosinus peut avoir des problèmes de convergence. Pour moi, c'est vraiment un cas de garbage-in-garbage-out: vous pouvez utiliser la fonction de distance que vous voulez, mais si cette fonction viole les hypothèses de l'algorithme, ne vous attendez pas à ce qu'elle produise des résultats significatifs!
Chiraz BenAbdelkader
15

Oui, vous pouvez utiliser une fonction métrique de différence; cependant, par définition, l'algorithme de clustering k-means repose sur la distance eucldienne de la moyenne de chaque cluster.

Vous pouvez utiliser une métrique différente, donc même si vous calculez toujours la moyenne, vous pouvez utiliser quelque chose comme la distance mahalnobis.

Adam
la source
25
+1: Permettez-moi de souligner que prendre la moyenne n'est appropriée que pour certaines fonctions de distance, telles que la distance euclidienne . Pour les autres fonctions de distance, vous devrez également remplacer la fonction d'estimation du centre du cluster!
A QUITTER - Anony-Mousse
2
@ Anony-Mousse. Que suis-je censé changer lorsque j'utilise la distance cosinus par exemple?
curieux
6
Je ne sais pas. Je n'ai pas vu de preuve de convergence avec Cosine. Je pense que cela convergera si vos données sont non négatives et normalisées à la sphère unitaire, car alors il s'agit essentiellement de k-moyennes dans un espace vectoriel différent.
A QUITTER - Anony-Mousse
1
Je suis d'accord avec @ Anony-Mousse. Pour moi, ce n'est qu'un cas de garbage-in-garbage-out: vous pouvez exécuter K-means avec la fonction de distance que vous voulez, mais si cette fonction viole les hypothèses sous-jacentes de l'algorithme, ne vous attendez pas à ce qu'elle produise un sens résultats!
Chiraz BenAbdelkader
@ Anony-Mousse mais comment implémenter les K-means en utilisant la distance mahalnobis?
Cecilia
7

Il y a pyclustering qui est python / C ++ (donc c'est rapide!) Et vous permet de spécifier une fonction métrique personnalisée

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 fait, je n'ai pas testé ce code mais je l'ai bricolé à partir d' un ticket et d'un exemple de code .

CpILL
la source
a besoin de Matplotlib installé qui a besoin de "Python comme framework sur Mac OS X" :(
CpILL
3

Sklearn Kmeans utilise la distance euclidienne . Il n'a pas de paramètre métrique. Cela dit, si vous êtes le regroupement des séries chronologiques , vous pouvez utiliser le tslearnpaquet python, lorsque vous pouvez spécifier une métrique ( dtw, softdtw, euclidean).

Tawej
la source