K significa comportamento incoerente escolhendo K com método Elbow, BIC, variância explicada e silhueta

23

Estou tentando agrupar alguns vetores com 90 recursos com K-means. Como esse algoritmo me pergunta o número de clusters, quero validar minha escolha com algumas boas matemáticas. Espero ter de 8 a 10 clusters. Os recursos são escalonados em Z-score.

Método do cotovelo e variância explicados

from scipy.spatial.distance import cdist, pdist
from sklearn.cluster import KMeans

K = range(1,50)
KM = [KMeans(n_clusters=k).fit(dt_trans) for k in K]
centroids = [k.cluster_centers_ for k in KM]

D_k = [cdist(dt_trans, 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)/dt_trans.shape[0] for d in dist]

# Total with-in sum of square
wcss = [sum(d**2) for d in dist]
tss = sum(pdist(dt_trans)**2)/dt_trans.shape[0]
bss = tss-wcss

kIdx = 10-1

# 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')

fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(K, bss/tss*100, 'b*-')
plt.grid(True)
plt.xlabel('Number of clusters')
plt.ylabel('Percentage of variance explained')
plt.title('Elbow for KMeans clustering')

Método do cotovelo Variação

A partir dessas duas figuras, parece que o número de agrupamentos nunca para: D. Estranho! Onde está o cotovelo? Como posso escolher K?

Critério de informação bayesiano

Este método vem diretamente do X-means e usa o BIC para escolher o número de clusters. outro juiz

    from sklearn.metrics import euclidean_distances
from sklearn.cluster import KMeans

def bic(clusters, centroids):
    num_points = sum(len(cluster) for cluster in clusters)
    num_dims = clusters[0][0].shape[0]
    log_likelihood = _loglikelihood(num_points, num_dims, clusters, centroids)
    num_params = _free_params(len(clusters), num_dims)
    return log_likelihood - num_params / 2.0 * np.log(num_points)


def _free_params(num_clusters, num_dims):
    return num_clusters * (num_dims + 1)


def _loglikelihood(num_points, num_dims, clusters, centroids):
    ll = 0
    for cluster in clusters:
        fRn = len(cluster)
        t1 = fRn * np.log(fRn)
        t2 = fRn * np.log(num_points)
        variance = _cluster_variance(num_points, clusters, centroids) or np.nextafter(0, 1)
        t3 = ((fRn * num_dims) / 2.0) * np.log((2.0 * np.pi) * variance)
        t4 = (fRn - 1.0) / 2.0
        ll += t1 - t2 - t3 - t4
    return ll

def _cluster_variance(num_points, clusters, centroids):
    s = 0
    denom = float(num_points - len(centroids))
    for cluster, centroid in zip(clusters, centroids):
        distances = euclidean_distances(cluster, centroid)
        s += (distances*distances).sum()
    return s / denom

from scipy.spatial import distance
def compute_bic(kmeans,X):
    """
    Computes the BIC metric for a given clusters

    Parameters:
    -----------------------------------------
    kmeans:  List of clustering object from scikit learn

    X     :  multidimension np array of data points

    Returns:
    -----------------------------------------
    BIC value
    """
    # assign centers and labels
    centers = [kmeans.cluster_centers_]
    labels  = kmeans.labels_
    #number of clusters
    m = kmeans.n_clusters
    # size of the clusters
    n = np.bincount(labels)
    #size of data set
    N, d = X.shape

    #compute variance for all clusters beforehand
    cl_var = (1.0 / (N - m) / d) * sum([sum(distance.cdist(X[np.where(labels == i)], [centers[0][i]], 'euclidean')**2) for i in range(m)])

    const_term = 0.5 * m * np.log(N) * (d+1)

    BIC = np.sum([n[i] * np.log(n[i]) -
               n[i] * np.log(N) -
             ((n[i] * d) / 2) * np.log(2*np.pi*cl_var) -
             ((n[i] - 1) * d/ 2) for i in range(m)]) - const_term

    return(BIC)



sns.set_style("ticks")
sns.set_palette(sns.color_palette("Blues_r"))
bics = []
for n_clusters in range(2,50):
    kmeans = KMeans(n_clusters=n_clusters)
    kmeans.fit(dt_trans)

    labels = kmeans.labels_
    centroids = kmeans.cluster_centers_

    clusters = {}
    for i,d in enumerate(kmeans.labels_):
        if d not in clusters:
            clusters[d] = []
        clusters[d].append(dt_trans[i])

    bics.append(compute_bic(kmeans,dt_trans))#-bic(clusters.values(), centroids))

plt.plot(bics)
plt.ylabel("BIC score")
plt.xlabel("k")
plt.title("BIC scoring for K-means cell's behaviour")
sns.despine()
#plt.savefig('figures/K-means-BIC.pdf', format='pdf', dpi=330,bbox_inches='tight')

insira a descrição da imagem aqui

O mesmo problema aqui ... O que é o K?

Silhueta

    from sklearn.metrics import silhouette_score

s = []
for n_clusters in range(2,30):
    kmeans = KMeans(n_clusters=n_clusters)
    kmeans.fit(dt_trans)

    labels = kmeans.labels_
    centroids = kmeans.cluster_centers_

    s.append(silhouette_score(dt_trans, labels, metric='euclidean'))

plt.plot(s)
plt.ylabel("Silouette")
plt.xlabel("k")
plt.title("Silouette for K-means cell's behaviour")
sns.despine()

insira a descrição da imagem aqui

Alleluja! Aqui parece fazer sentido e é isso que eu espero. Mas por que isso é diferente dos outros?

Marcodena
fonte
1
Para responder à sua pergunta sobre o joelho no caso de variação, parece que é em torno de 6 ou 7, você pode imaginá-lo como o ponto de interrupção entre dois segmentos lineares aproximados da curva. A forma do gráfico não é incomum; a% de variação geralmente se aproxima assintoticamente de 100%. Eu ia colocar k no gráfico BIC como um pouco menor, em torno de 5.
image_doctor
mas eu deveria ter (mais ou menos) os mesmos resultados em todos os métodos, certo?
Marcodena 21/07
Acho que não sei o suficiente para dizer. Duvido muito que os três métodos sejam matematicamente equivalentes a todos os dados; caso contrário, eles não existiriam como técnicas distintas; portanto, os resultados comparativos dependem dos dados. Dois dos métodos fornecem números de clusters próximos; o terceiro é maior, mas não o é enormemente. Você tem informações a priori sobre o número real de clusters?
image_doctor
Não estou 100% de certeza, mas eu espero ter de 8 a 10 clusters
marcodena
2
Você já está no buraco negro da "Maldição da Dimensionalidade". Nada funciona antes de uma redução de dimensionalidade.
Kasra Manshaei

Respostas:

7

Basta postar um resumo dos comentários acima e mais algumas reflexões para que esta pergunta seja removida das "perguntas não respondidas".

O comentário do Image_doctor está certo de que esses gráficos são típicos para médias k. (Porém, eu não estou familiarizado com a medida "Silhouette".) Espera-se que a variação no cluster diminua continuamente com o aumento de k. O cotovelo é o local onde a curva mais se curva. (Talvez pense na "segunda derivada" se você quiser algo matemático.)

Geralmente, é melhor escolher k usando a tarefa final. Não use medidas estatísticas do seu cluster para tomar sua decisão, mas use o desempenho de ponta a ponta do seu sistema para orientar suas escolhas. Use apenas as estatísticas como ponto de partida.

Joachim Wagner
fonte
5

Encontrar o cotovelo pode ser mais fácil calculando os ângulos entre os segmentos consecutivos.

Substitua o seu:

kIdx = 10-1

com:

seg_threshold = 0.95 #Set this to your desired target

#The angle between three points
def segments_gain(p1, v, p2):
    vp1 = np.linalg.norm(p1 - v)
    vp2 = np.linalg.norm(p2 - v)
    p1p2 = np.linalg.norm(p1 - p2)
    return np.arccos((vp1**2 + vp2**2 - p1p2**2) / (2 * vp1 * vp2)) / np.pi

#Normalize the data
criterion = np.array(avgWithinSS)
criterion = (criterion - criterion.min()) / (criterion.max() - criterion.min())

#Compute the angles
seg_gains = np.array([0, ] + [segments_gain(*
        [np.array([K[j], criterion[j]]) for j in range(i-1, i+2)]
    ) for i in range(len(K) - 2)] + [np.nan, ])

#Get the first index satisfying the threshold
kIdx = np.argmax(seg_gains > seg_threshold)

e você verá algo como: insira a descrição da imagem aqui

Se você visualizar os seg_gains, verá algo parecido com isto: insira a descrição da imagem aqui

Espero que você possa encontrar o cotovelo complicado agora :)

Sahloul
fonte
3

Criei uma biblioteca Python que tenta implementar o algoritmo Kneedle para detectar o ponto de curvatura máxima em funções como esta. Pode ser instalado com pip install kneed.

Código e saída para quatro formas diferentes de funções:

from kneed.data_generator import DataGenerator
from kneed.knee_locator import KneeLocator

import numpy as np

import matplotlib.pyplot as plt

# sample x and y
x = np.arange(0,10)
y_convex_inc = np.array([1,2,3,4,5,10,15,20,40,100])
y_convex_dec = y_convex_inc[::-1]
y_concave_dec = 100 - y_convex_inc
y_concave_inc = 100 - y_convex_dec

# find the knee points
kn = KneeLocator(x, y_convex_inc, curve='convex', direction='increasing')
knee_yconvinc = kn.knee

kn = KneeLocator(x, y_convex_dec, curve='convex', direction='decreasing')
knee_yconvdec = kn.knee

kn = KneeLocator(x, y_concave_inc, curve='concave', direction='increasing')
knee_yconcinc = kn.knee

kn = KneeLocator(x, y_concave_dec, curve='concave', direction='decreasing')
knee_yconcdec = kn.knee

# plot
f, axes = plt.subplots(2, 2, figsize=(10,10));
yconvinc = axes[0][0]
yconvdec = axes[0][1]
yconcinc = axes[1][0]
yconcdec = axes[1][1]

yconvinc.plot(x, y_convex_inc)
yconvinc.vlines(x=knee_yconvinc, ymin=0, ymax=100, linestyle='--')
yconvinc.set_title("curve='convex', direction='increasing'")

yconvdec.plot(x, y_convex_dec)
yconvdec.vlines(x=knee_yconvdec, ymin=0, ymax=100, linestyle='--')
yconvdec.set_title("curve='convex', direction='decreasing'")

yconcinc.plot(x, y_concave_inc)
yconcinc.vlines(x=knee_yconcinc, ymin=0, ymax=100, linestyle='--')
yconcinc.set_title("curve='concave', direction='increasing'")

yconcdec.plot(x, y_concave_dec)
yconcdec.vlines(x=knee_yconcdec, ymin=0, ymax=100, linestyle='--')
yconcdec.set_title("curve='concave', direction='decreasing'");

insira a descrição da imagem aqui

Kevin
fonte