web-dev-qa-db-fra.com

Calcul du pourcentage de mesure de variance pour k-moyennes?

Sur la page Wikipedia , une méthode du coude est décrite pour déterminer le nombre de clusters dans k-means. La méthode intégrée de scipy fournit une implémentation mais je ne suis pas sûr de comprendre comment la distorsion comme ils l'appellent, est calculée.

Plus précisément, si vous représentez graphiquement le pourcentage de variance expliqué par les clusters par rapport au nombre de clusters, les premiers clusters ajouteront beaucoup d'informations (expliquer beaucoup de variance), mais à un moment donné le gain marginal diminuera, donnant un angle dans la graphique.

En supposant que j'ai les points suivants avec leurs centroïdes associés, quelle est une bonne façon de calculer cette mesure?

points = numpy.array([[ 0,  0],
       [ 0,  1],
       [ 0, -1],
       [ 1,  0],
       [-1,  0],
       [ 9,  9],
       [ 9, 10],
       [ 9,  8],
       [10,  9],
       [10,  8]])

kmeans(pp,2)
(array([[9, 8],
   [0, 0]]), 0.9414213562373096)

Je regarde spécifiquement le calcul de la mesure 0,94 .. étant donné uniquement les points et les centroïdes. Je ne sais pas si l'une des méthodes intégrées de scipy peut être utilisée ou je dois écrire la mienne. Des suggestions sur la façon de procéder efficacement pour un grand nombre de points?

En bref, mes questions (toutes liées) sont les suivantes:

  • Étant donné une matrice de distance et une cartographie de quel point appartient à quel cluster, quelle est une bonne façon de calculer une mesure qui peut être utilisée pour dessiner le tracé du coude?
  • Comment la méthodologie changerait-elle si une fonction de distance différente telle que la similitude cosinus était utilisée?

EDIT 2: Distorsion

from scipy.spatial.distance import cdist
D = cdist(points, centroids, 'euclidean')
sum(numpy.min(D, axis=1))

La sortie du premier ensemble de points est précise. Cependant, lorsque j'essaie un autre ensemble:

>>> pp = numpy.array([[1,2], [2,1], [2,2], [1,3], [6,7], [6,5], [7,8], [8,8]])
>>> kmeans(pp, 2)
(array([[6, 7],
       [1, 2]]), 1.1330618877807475)
>>> centroids = numpy.array([[6,7], [1,2]])
>>> D = cdist(points, centroids, 'euclidean')
>>> sum(numpy.min(D, axis=1))
9.0644951022459797

Je suppose que la dernière valeur ne correspond pas parce que kmeans semble plonger la valeur par le nombre total de points dans l'ensemble de données.

EDIT 1: Variation en pourcentage

Mon code jusqu'à présent (devrait être ajouté à l'implémentation K-means de Denis):

centres, xtoc, dist = kmeanssample( points, 2, nsample=2,
        delta=kmdelta, maxiter=kmiter, metric=metric, verbose=0 )

print "Unique clusters: ", set(xtoc)
print ""
cluster_vars = []
for cluster in set(xtoc):
    print "Cluster: ", cluster

    truthcondition = ([x == cluster for x in xtoc])
    distances_inside_cluster = (truthcondition * dist)

    indices = [i for i,x in enumerate(truthcondition) if x == True]
    final_distances = [distances_inside_cluster[k] for k in indices]

    print final_distances
    print np.array(final_distances).var()
    cluster_vars.append(np.array(final_distances).var())
    print ""

print "Sum of variances: ", sum(cluster_vars)
print "Total Variance: ", points.var()
print "Percent: ", (100 * sum(cluster_vars) / points.var())

Et voici la sortie pour k = 2:

Unique clusters:  set([0, 1])

Cluster:  0
[1.0, 2.0, 0.0, 1.4142135623730951, 1.0]
0.427451660041

Cluster:  1
[0.0, 1.0, 1.0, 1.0, 1.0]
0.16

Sum of variances:  0.587451660041
Total Variance:  21.1475
Percent:  2.77787757437

Sur mon vrai jeu de données (ne me semble pas bien!):

Sum of variances:  0.0188124746402
Total Variance:  0.00313754329764
Percent:  599.592510943
Unique clusters:  set([0, 1, 2, 3])

Sum of variances:  0.0255808508714
Total Variance:  0.00313754329764
Percent:  815.314672809
Unique clusters:  set([0, 1, 2, 3, 4])

Sum of variances:  0.0588210052519
Total Variance:  0.00313754329764
Percent:  1874.74720416
Unique clusters:  set([0, 1, 2, 3, 4, 5])

Sum of variances:  0.0672406353655
Total Variance:  0.00313754329764
Percent:  2143.09824556
Unique clusters:  set([0, 1, 2, 3, 4, 5, 6])

Sum of variances:  0.0646291452839
Total Variance:  0.00313754329764
Percent:  2059.86465055
Unique clusters:  set([0, 1, 2, 3, 4, 5, 6, 7])

Sum of variances:  0.0817517362176
Total Variance:  0.00313754329764
Percent:  2605.5970695
Unique clusters:  set([0, 1, 2, 3, 4, 5, 6, 7, 8])

Sum of variances:  0.0912820650486
Total Variance:  0.00313754329764
Percent:  2909.34837831
Unique clusters:  set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

Sum of variances:  0.102119601368
Total Variance:  0.00313754329764
Percent:  3254.76309585
Unique clusters:  set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

Sum of variances:  0.125549475536
Total Variance:  0.00313754329764
Percent:  4001.52168834
Unique clusters:  set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])

Sum of variances:  0.138469402779
Total Variance:  0.00313754329764
Percent:  4413.30651542
Unique clusters:  set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
35
Legend

La distorsion, en ce qui concerne Kmeans , est utilisée comme critère d'arrêt (si le changement entre deux itérations est inférieur à un certain seuil, nous supposons la convergence)

Si vous voulez le calculer à partir d'un ensemble de points et des centroïdes, vous pouvez faire ce qui suit (le code est dans MATLAB en utilisant pdist2 , mais il devrait être simple de réécrire en Python/Numpy/Scipy):

% data
X = [0 1 ; 0 -1 ; 1 0 ; -1 0 ; 9 9 ; 9 10 ; 9 8 ; 10 9 ; 10 8];

% centroids
C = [9 8 ; 0 0];

% euclidean distance from each point to each cluster centroid
D = pdist2(X, C, 'euclidean');

% find closest centroid to each point, and the corresponding distance
[distortions,idx] = min(D,[],2);

le résultat:

% total distortion
>> sum(distortions)
ans =
           9.4142135623731

EDIT # 1:

J'ai eu un peu de temps pour jouer avec ceci .. Voici un exemple de clustering KMeans appliqué sur le 'Fisher Iris Dataset' (4 fonctionnalités, 150 instances). Nous itérons sur k=1..10, tracez la courbe du coude, choisissez K=3 comme nombre de grappes et affiche un nuage de points du résultat.

Notez que j'ai inclus un certain nombre de façons de calculer les variances intra-cluster (distorsions), étant donné les points et les centroïdes. Le scipy.cluster.vq.kmeans la fonction renvoie cette mesure par défaut (calculée avec Euclidienne comme mesure de distance). Vous pouvez également utiliser le scipy.spatial.distance.cdist fonction pour calculer les distances avec la fonction de votre choix (à condition d'avoir obtenu les centroïdes de cluster en utilisant la même mesure de distance: @ Denis avoir une solution pour cela), puis calculer la distorsion à partir de cette.

import numpy as np
from scipy.cluster.vq import kmeans,vq
from scipy.spatial.distance import cdist
import matplotlib.pyplot as plt

# load the iris dataset
fName = 'C:\\Python27\\Lib\\site-packages\\scipy\\spatial\\tests\\data\\iris.txt'
fp = open(fName)
X = np.loadtxt(fp)
fp.close()

##### cluster data into K=1..10 clusters #####
K = range(1,10)

# scipy.cluster.vq.kmeans
KM = [kmeans(X,k) for k in K]
centroids = [cent for (cent,var) in KM]   # cluster centroids
#avgWithinSS = [var for (cent,var) in KM] # mean within-cluster sum of squares

# alternative: scipy.cluster.vq.vq
#Z = [vq(X,cent) for cent in centroids]
#avgWithinSS = [sum(dist)/X.shape[0] for (cIdx,dist) in Z]

# alternative: scipy.spatial.distance.cdist
D_k = [cdist(X, cent, 'euclidean') for cent in centroids]
cIdx = [np.argmin(D,axis=1) for D in D_k]
dist = [np.min(D,axis=1) for D in D_k]
avgWithinSS = [sum(d)/X.shape[0] for d in dist]

##### plot ###
kIdx = 2

# elbow curve
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(K, avgWithinSS, 'b*-')
ax.plot(K[kIdx], avgWithinSS[kIdx], marker='o', markersize=12, 
    markeredgewidth=2, markeredgecolor='r', markerfacecolor='None')
plt.grid(True)
plt.xlabel('Number of clusters')
plt.ylabel('Average within-cluster sum of squares')
plt.title('Elbow for KMeans clustering')

# scatter plot
fig = plt.figure()
ax = fig.add_subplot(111)
#ax.scatter(X[:,2],X[:,1], s=30, c=cIdx[k])
clr = ['b','g','r','c','m','y','k']
for i in range(K[kIdx]):
    ind = (cIdx[kIdx]==i)
    ax.scatter(X[ind,2],X[ind,1], s=30, c=clr[i], label='Cluster %d'%i)
plt.xlabel('Petal Length')
plt.ylabel('Sepal Width')
plt.title('Iris Dataset, KMeans clustering with K=%d' % K[kIdx])
plt.legend()

plt.show()

elbow_curvescatter_plot


EDIT # 2:

En réponse aux commentaires, je donne ci-dessous un autre exemple complet utilisant le ensemble de données de chiffres manuscrits NIST : il a 1797 images de chiffres de 0 à 9, chacune de taille 8 par 8 pixels. Je répète l'expérience ci-dessus légèrement modifiée: Analyse des composants principaux est appliqué pour réduire la dimensionnalité de 64 à 2:

import numpy as np
from scipy.cluster.vq import kmeans
from scipy.spatial.distance import cdist,pdist
from sklearn import datasets
from sklearn.decomposition import RandomizedPCA
from matplotlib import pyplot as plt
from matplotlib import cm

##### data #####
# load digits dataset
data = datasets.load_digits()
t = data['target']

# perform PCA dimensionality reduction
pca = RandomizedPCA(n_components=2).fit(data['data'])
X = pca.transform(data['data'])

##### cluster data into K=1..20 clusters #####
K_MAX = 20
KK = range(1,K_MAX+1)

KM = [kmeans(X,k) for k in KK]
centroids = [cent for (cent,var) in KM]
D_k = [cdist(X, cent, 'euclidean') for cent in centroids]
cIdx = [np.argmin(D,axis=1) for D in D_k]
dist = [np.min(D,axis=1) for D in D_k]

tot_withinss = [sum(d**2) for d in dist]  # Total within-cluster sum of squares
totss = sum(pdist(X)**2)/X.shape[0]       # The total sum of squares
betweenss = totss - tot_withinss          # The between-cluster sum of squares

##### plots #####
kIdx = 9        # K=10
clr = cm.spectral( np.linspace(0,1,10) ).tolist()
mrk = 'os^p<dvh8>+x.'

# elbow curve
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(KK, betweenss/totss*100, 'b*-')
ax.plot(KK[kIdx], betweenss[kIdx]/totss*100, marker='o', markersize=12, 
    markeredgewidth=2, markeredgecolor='r', markerfacecolor='None')
ax.set_ylim((0,100))
plt.grid(True)
plt.xlabel('Number of clusters')
plt.ylabel('Percentage of variance explained (%)')
plt.title('Elbow for KMeans clustering')

# show centroids for K=10 clusters
plt.figure()
for i in range(kIdx+1):
    img = pca.inverse_transform(centroids[kIdx][i]).reshape(8,8)
    ax = plt.subplot(3,4,i+1)
    ax.set_xticks([])
    ax.set_yticks([])
    plt.imshow(img, cmap=cm.gray)
    plt.title( 'Cluster %d' % i )

# compare K=10 clustering vs. actual digits (PCA projections)
fig = plt.figure()
ax = fig.add_subplot(121)
for i in range(10):
    ind = (t==i)
    ax.scatter(X[ind,0],X[ind,1], s=35, c=clr[i], marker=mrk[i], label='%d'%i)
plt.legend()
plt.title('Actual Digits')
ax = fig.add_subplot(122)
for i in range(kIdx+1):
    ind = (cIdx[kIdx]==i)
    ax.scatter(X[ind,0],X[ind,1], s=35, c=clr[i], marker=mrk[i], label='C%d'%i)
plt.legend()
plt.title('K=%d clusters'%KK[kIdx])

plt.show()

elbow_curvedigits_centroidsPCA_compare

Vous pouvez voir comment certains clusters correspondent réellement à des chiffres reconnaissables, tandis que d'autres ne correspondent pas à un seul numéro.

Remarque: Une implémentation de K-means est incluse dans scikit-learn (ainsi que de nombreux autres algorithmes de clustering et divers métriques de clustering ). Ici est un autre exemple similaire.

49
Amro

Une mesure de cluster simple:
1) dessiner des rayons "sunburst" de chaque point vers son centre d'amas le plus proche,
2) regardez les longueurs - distance (point, centre, métrique = ...) - de tous les rayons.

Pour metric="sqeuclidean" Et 1 cluster, la longueur moyenne au carré est la variance totale X.var(); pour 2 grappes, c'est moins ... jusqu'à N grappes, toutes les longueurs 0. "Le pourcentage de variance expliqué" est de 100% - cette moyenne.

Code pour cela, sous est-il possible de spécifier votre propre fonction de distance en utilisant scikits-learn-k-means :

def distancestocentres( X, centres, metric="euclidean", p=2 ):
    """ all distances 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.min(axis=1)  # all the distances

Comme toute longue liste de nombres, ces distances peuvent être examinées de différentes manières: np.mean (), np.histogram () ... Le tracé, la visualisation, n'est pas facile.
Voir aussi stats.stackexchange.com/questions/tagged/clustering , en particulier
Comment savoir si les données sont suffisamment "clusterisées" pour que les algorithmes de clustering produisent des résultats significatifs?

6
denis