1. 什么是聚类?
按照某个特定标准(如距离)把一个数据集分割成不同的类或簇,使得同一个簇内的数据对象的相似性尽可能大,同时不在同一个簇中的数据对象的差异性也尽可能地大。也即聚类后同一类的数据尽可能聚集到一起,不同类数据尽量分离。
2. 聚类分析应用
将相关的文档分组便于浏览
将具有相似功能的基因和蛋白质分组
将具有相似价格波动的股票分组
3.聚类的类型
划分聚类(非嵌套
层次聚类(嵌套
4.聚类算法
4.1 划分式聚类算法
4.1.1 K-means
基本算法过程
1:选择K个点作为质心
2:repeat
3: 将每个点指派到最近的质心,形成K个簇
4: 从新计算每个簇的质心
5:until 质心不再发生变化
kmeans = KMeans(n_clusters=10, random_state=42) # 设置所需的簇数
kmeans.fit(x_train_pca)
一般来说,经典k-means
算法有以下几个特点:
- 需要提前确定k值
- 对初始质心点敏感
- 对异常数据敏感
4.1.2 k-means++算法
k-means++
是针对k-means
中初始质心点选取的优化算法。该算法的流程和k-means
类似,改变的地方只有初始质心的选取,该部分的算法流程如下
基本的k-means算法可能会产生空簇
几种策略:
选择一个距离当前任何质心最远的点
消除当前对总平方差影响最大的点
一种度量聚类效果的指标是SSE(Sum of Squared Error)
,他表示聚类后的簇离该簇的聚类中心的平方和,SSE
越小,表示聚类效果越好。 bi-kmeans
是针对kmeans
算法会陷入局部最优的缺陷进行的改进算法。该算法基于SSE最小化的原理,首先将所有的数据点视为一个簇,然后将该簇一分为二,之后选择其中一个簇继续进行划分,选择哪一个簇进行划分取决于对其划分是否能最大程度的降低SSE
的值。
4.1.3 bi-kmeans算法
算法过程
1:初始化簇表,使之包含所有点组成的簇
2:repeat
3: 从簇表中取一个簇
4: {对选定的簇进行多次二分类“试验”}
5: for i=1 to 试验次数 do
6: 使用基本K均值,二分选定簇
7: end for
8: 从二分试验中选择具有最小总SEE的两个簇
9: 将这两个簇添加到簇表中
10:until 簇表中包含K个簇
源代码
# bi-kmeans.py
import codecs
from numpy import *
import matplotlib.pyplot as plt
def load_data(path):
"""
@brief Loads a data.
@param path The path
@return data set
"""
data_set = list()
with codecs.open(path) as f:
for line in f.readlines():
data = line.strip().split("\t")
flt_data = list(map(float, data))
data_set.append(flt_data)
return data_set
def rand_cent(data_mat, k):
"""
@brief select random centroid
@param data_mat The data matrix
@param k
@return centroids
"""
n = shape(data_mat)[1]
centroids = mat(zeros((k, n)))
if not data_mat.any():
return centroids
for j in range(n):
minJ = min(data_mat[:,j])
rangeJ = float(max(data_mat[:,j]) - minJ)
centroids[:,j] = mat(minJ + rangeJ * random.rand(k,1))
return centroids
def dist_eucl(vecA, vecB):
"""
@brief the similarity function
@param vecA The vector a
@param vecB The vector b
@return the euclidean distance
"""
return sqrt(sum(power(vecA - vecB, 2)))
def k_Means(data_mat, k, dist = "dist_eucl", create_cent = "rand_cent"):
"""
@brief kMeans algorithm
@param data_mat The data matrix
@param k num of cluster
@param dist The distance funtion
@param create_cent The create centroid function
@return the cluster
"""
m = shape(data_mat)[0]
# 初始化点的簇
cluster_assment = mat(zeros((m, 2))) # 类别,距离
# 随机初始化聚类初始点
centroid = eval(create_cent)(data_mat, k)
cluster_changed = True
# 遍历每个点
while cluster_changed:
cluster_changed = False
for i in range(m):
min_index = -1
min_dist = inf
for j in range(k):
distance = eval(dist)(data_mat[i, :], centroid[j, :])
if distance < min_dist:
min_dist = distance
min_index = j
if cluster_assment[i, 0] != min_index:
cluster_changed = True
cluster_assment[i, :] = min_index, min_dist**2
# 计算簇中所有点的均值并重新将均值作为质心
for j in range(k):
per_data_set = data_mat[nonzero(cluster_assment[:,0].A == j)[0]]
centroid[j, :] = mean(per_data_set, axis = 0)
return centroid, cluster_assment
def bi_kMeans(data_mat, k, dist = "dist_eucl"):
"""
@brief kMeans algorithm
@param data_mat The data matrix
@param k num of cluster
@param dist The distance funtion
@return the cluster
"""
m = shape(data_mat)[0]
# 初始化点的簇
cluster_assment = mat(zeros((m, 2))) # 类别,距离
# 初始化聚类初始点
centroid0 = mean(data_mat, axis = 0).tolist()[0]
cent_list = [centroid0]
print(cent_list)
# 初始化SSE
for j in range(m):
cluster_assment[j, 1] = eval(dist)(mat(centroid0), data_mat[j, :]) ** 2
while (len(cent_list) < k):
lowest_sse = inf
for i in range(len(cent_list)):
# 尝试在每一类簇中进行k=2的kmeans划分
ptsin_cur_cluster = data_mat[nonzero(cluster_assment[:, 0].A == i)[0],:]
centroid_mat, split_cluster_ass = k_Means(ptsin_cur_cluster,k = 2)
# 计算分类之后的SSE值
sse_split = sum(split_cluster_ass[:, 1])
sse_nonsplit = sum(cluster_assment[nonzero(cluster_assment[:, 0].A != i)[0], 1])
print("sse_split, sse_nonsplit", sse_split, sse_nonsplit)
# 记录最好的划分位置
if sse_split + sse_nonsplit < lowest_sse:
best_cent_tosplit = i
best_new_cents = centroid_mat
best_cluster_ass = split_cluster_ass.copy()
lowest_sse = sse_split + sse_nonsplit
print( 'the bestCentToSplit is: ', best_cent_tosplit)
print ('the len of bestClustAss is: ', len(best_cluster_ass))
# 更新簇的分配结果
best_cluster_ass[nonzero(best_cluster_ass[:, 0].A == 1)[0], 0] = len(cent_list)
best_cluster_ass[nonzero(best_cluster_ass[:, 0].A == 0)[0], 0] = best_cent_tosplit
cent_list[best_cent_tosplit] = best_new_cents[0, :].tolist()[0]
cent_list.append(best_new_cents[1, :].tolist()[0])
cluster_assment[nonzero(cluster_assment[:, 0].A == best_cent_tosplit)[0],:] = best_cluster_ass
return mat(cent_list), cluster_assment
def plot_cluster(data_mat, cluster_assment, centroid):
"""
@brief plot cluster and centroid
@param data_mat The data matrix
@param cluster_assment The cluste assment
@param centroid The centroid
@return
"""
plt.figure(figsize=(15, 6), dpi=80)
plt.subplot(121)
plt.plot(data_mat[:, 0], data_mat[:, 1], 'o')
plt.title("source data", fontsize=15)
plt.subplot(122)
k = shape(centroid)[0]
colors = [plt.cm.Spectral(each) for each in linspace(0, 1, k)]
for i, col in zip(range(k), colors):
per_data_set = data_mat[nonzero(cluster_assment[:,0].A == i)[0]]
plt.plot(per_data_set[:, 0], per_data_set[:, 1], 'o', markerfacecolor=tuple(col),
markeredgecolor='k', markersize=10)
for i in range(k):
plt.plot(centroid[:,0], centroid[:,1], '+', color = 'k', markersize=18)
plt.title("bi_KMeans Cluster, k = 3", fontsize=15)
plt.show()
if __name__ == '__main__':
# data_mat = mat(load_data("data/testSet_kmeans.txt"))
lst = list()
for i in range(1000):
data_mat = mat(load_data("data/testSet2_kmeans.txt"))
centroid, cluster_assment = bi_kMeans(data_mat, 3)
sse = sum(cluster_assment[:,1])
lst.append(sse)
#print("sse is ", sse)
print(centroid)
#plot_cluster(data_mat, cluster_assment, centroid)
# plot_noncov()
# test_diff_k()
j = 0
for i in (lst):
if abs(i - 106.74949876187601) < 1e-5:
j += 1
print(1.0 * j / len(lst))
应用代码
import numpy as np
# 生成随机的二进制数据作为示例
data_size = 100
bit_size = 8
data = np.random.randint(0, 2, size=(data_size, bit_size))
# 初始化聚类中心
k = 4
centroids = np.random.randint(0, 2, size=(k, bit_size))
# 迭代聚类
max_iterations = 100
for _ in range(max_iterations):
# 计算每个数据点到每个聚类中心的距离
distances = np.zeros((data_size, k))
for i in range(k):
distances[:, i] = np.sum(np.abs(data - centroids[i]), axis=1)
# 将每个数据点分配给最近的聚类中心
labels = np.argmin(distances, axis=1)
# 更新聚类中心
for i in range(k):
if np.any(labels == i):
centroids[i] = np.bitwise_and.reduce(data[labels == i], axis=0)
# 输出最终聚类中心
print("最终聚类中心:", centroids)
K均值聚类局限性
-簇具有不同的大小
-簇具有不同的密度
-簇不是球形的
4.2 基于密度的聚类
k-means
算法对于凸性数据具有良好的效果,能够根据距离来讲数据分为球状类的簇,但对于非凸形状的数据点,就无能为力了,当k-means
算法在环形数据的聚类时,我们看看会发生什么情况。
从上图可以看到,kmeans
聚类产生了错误的结果,这个时候就需要用到基于密度的聚类方法了,该方法需要定义两个参数和M,分别表示密度的邻域半径和邻域密度阈值。DBSCAN
就是其中的典型。
2.2.1 DBSCAN算法
如下图所示,设M=3,则A为核心点,B、C是边界点,而N是噪声点。
该算法的流程如下:
构建邻域的过程可以使用kd-tree
进行优化,循环过程可以使用Numba、Cython、C
进行优化,DBSCAN
的源代码,使用该节一开始提到的数据集,聚类效果如下
聚类的过程示意图
当设置不同的时,会产生不同的结果,如下图所示
当设置不同的M时,会产生不同的结果,如下图所示
# 使用DBSCAN进行聚类
dbscan = DBSCAN(eps=0.7, min_samples=10)
clusters = dbscan.fit_predict(x_train_pca)
# 使用Counter计算每个类别的样本数量
cluster_counter = Counter(clusters)
# 获取总共分成的类别数量(不包括噪声点)
num_clusters = len(set(clusters)) - (1 if -1 in clusters else 0)
print(f"Total clusters: {num_clusters}")
# 输出每个类别的数量
for label, count in cluster_counter.items():
if label == -1:
print(f"Noise Points: {count}")
else:
print(f"Cluster {label}: {count} samples")
国外一个比较有意思的网站,可以看聚类的过程:
Visualizing DBSCAN Clustering (naftaliharris.com)
DBSCAN
算法特点:
- 需要提前确定和M值
- 不需要提前设置聚类的个数
- 对初值选取敏感,对噪声不敏感
- 对密度不均的数据聚合效果不好
2.2.2 OPTICS算法
在DBSCAN
算法中,使用了统一的值,当数据密度不均匀的时候,如果设置了较小的值,则较稀疏的cluster
中的节点密度会小于M,会被认为是边界点而不被用于进一步的扩展;如果设置了较大的值,则密度较大且离的比较近的cluster
容易被划分为同一个cluster
,如下图所示。
- 如果设置的较大,将会获得A,B,C这3个
cluster
- 如果设置的较小,将会只获得C1、C2、C3这3个
cluster
对于密度不均的数据选取一个合适的是很困难的,对于高维数据,由于维度灾难(Curse of dimensionality),的选取将变得更加困难。
怎样解决DBSCAN
遗留下的问题呢?
即能够提出一种算法,使得基于密度的聚类结构能够呈现出一种特殊的顺序,该顺序所对应的聚类结构包含了每个层级的聚类的信息,并且便于分析。
OPTICS
实际上是DBSCAN
算法的一种有效扩展,主要解决对输入参数敏感的问题。即选取有限个邻域参数(0≤≤) 进行聚类,这样就能得到不同邻域参数下的聚类结果。
在介绍OPTICS
算法之前,再扩展几个概念。
算法流程如下:
算法中有一个很重要的insert_list()函数,这个函数如下:
OPTICS
算法输出序列的过程:
该算法最终获取知识是一个输出序列,该序列按照密度不同将相近密度的点聚合在一起,而不是输出该点所属的具体类别,如果要获取该点所属的类型,需要再设置一个参数′(′≤)提取出具体的类别。这里我们举一个例子就知道是怎么回事了。
随机生成三组密度不均的数据,我们使用DBSCAN
和OPTICS
来看一下效果。
可见,OPTICS
第一步生成的输出序列较好的保留了各个不同密度的簇的特征,根据输出序列的可达距离图,再设定一个合理的′,便可以获得较好的聚类效果。
4.3 层次聚类
前面介绍的几种算法确实可以在较小的复杂度内获取较好的结果,但是这几种算法却存在一个链式效应
的现象,比如:A与B相似,B与C相似,那么在聚类的时候便会将A、B、C聚合到一起,但是如果A与C不相似,就会造成聚类误差,严重的时候这个误差可以一直传递下去。为了降低链式效应
,这时候层次聚类就该发挥作用了。
层次聚类算法 (hierarchical clustering) 将数据集划分为一层一层的 clusters
,后面一层生成的 clusters
基于前面一层的结果。层次聚类算法一般分为两类:
- 凝聚的 层次聚类:又称自底向上的层次聚类,每一个对象最开始都是一个
cluster
,每次按一定的准则将最相近的两个cluster
合并生成一个新的cluster
,如此往复,直至最终所有的对象都属于一个cluster
。这里主要关注此类算法。 - 分裂的 层次聚类: 又称自顶向下的层次聚类,最开始所有的对象均属于一个
cluster
,每次按一定的准则将某个cluster
划分为多个cluster
,如此往复,直至每个对象均是一个cluster
。
另外,需指出的是,层次聚类算法是一种贪心算法,因其每一次合并或划分都是基于某种局部最优的选择。
4.3.1 Agglomerative算法
·上图中分别使用了层次聚类中4个不同的cluster
度量方法,可以看到,使用single-link
确实会造成一定的链式效应,而使用complete-link
则完全不会产生这种现象,使用average-link
和ward-link
则介于两者之间。
4.3.2 BIRCH
4.4 其他聚类方法
4.4.1 谱聚类
K均值聚类算法的主要局限之一是其寻求球状簇的趋势。因此,针对具有任意形状的簇的数据集或簇的质心彼此重叠时,K均值聚类算法效果较差。 谱聚类可以通过利用相似度图的属性来克服这种限制,从而克服这种限制。为了说明这一点,请考虑以下二维数据集。
import pandas as pd
data1 = pd.read_csv("D:\\数据挖掘\\实验3 聚类 代码与数据\\2d_data.txt", delimiter=' ', names=['x','y'])
data2 = pd.read_csv("D:\\数据挖掘\\实验3 聚类 代码与数据\\elliptical.txt", delimiter=' ', names=['x','y'])
fig, (ax1,ax2) = plt.subplots(nrows=1, ncols=2, figsize=(12,5))
data1.plot.scatter(x='x',y='y',ax=ax1)
data2.plot.scatter(x='x',y='y',ax=ax2)
#kmeans聚类(k=2)
from sklearn import cluster
k_means = cluster.KMeans(n_clusters=2, max_iter=50, random_state=1)
k_means.fit(data1)
labels1 = pd.DataFrame(k_means.labels_,columns=['Cluster ID'])
result1 = pd.concat((data1,labels1), axis=1)
k_means2 = cluster.KMeans(n_clusters=2, max_iter=50, random_state=1)
k_means2.fit(data2)
labels2 = pd.DataFrame(k_means2.labels_,columns=['Cluster ID'])
result2 = pd.concat((data2,labels2), axis=1)
fig, (ax1,ax2) = plt.subplots(nrows=1, ncols=2, figsize=(12,5))
result1.plot.scatter(x='x',y='y',c='Cluster ID',colormap='jet',ax=ax1)
ax1.set_title('K-means Clustering')
result2.plot.scatter(x='x',y='y',c='Cluster ID',colormap='jet',ax=ax2)
ax2.set_title('K-means Clustering')
上图显示了k均值聚类的性能较差。
接下来,我们将谱聚类应用于数据集。
谱聚类将数据转换为相似度图,并应用归一化切割图分区算法(normalized cut graph partitioning algorithm)生成簇。
在下面的示例中,我们将高斯径向基函数(Gaussian radial basis function)用作我们的亲和度(affinity,类似于相似度)度量。 用户需要调整内核参数(gamma)值,以获得针对给定数据集的合适的簇。
from sklearn import cluster
import pandas as pd
spectral = cluster.SpectralClustering(n_clusters=2,random_state=1,affinity='rbf',gamma=5000)
spectral.fit(data1)
labels1 = pd.DataFrame(spectral.labels_,columns=['Cluster ID'])
result1 = pd.concat((data1,labels1), axis=1)
spectral2 = cluster.SpectralClustering(n_clusters=2,random_state=1,affinity='rbf',gamma=500)
spectral2.fit(data2)
labels2 = pd.DataFrame(spectral2.labels_,columns=['Cluster ID'])
result2 = pd.concat((data2,labels2), axis=1)
fig, (ax1,ax2) = plt.subplots(nrows=1, ncols=2, figsize=(12,5))
result1.plot.scatter(x='x',y='y',c='Cluster ID',colormap='jet',ax=ax1)
ax1.set_title('Spectral Clustering')
result2.plot.scatter(x='x',y='y',c='Cluster ID',colormap='jet',ax=ax2)
ax2.set_title('Spectral Clustering')
Laplacian Eigenmap 的降维方式降维之后再做 K- means
谱聚类(Spectral Clustering)是一种基于图论和线性代数的聚类方法,它通过将数据转换到低维度的特征空间,在该空间中进行聚类。它的主要思想是利用样本之间的相似度构建相似度矩阵,然后利用这个矩阵来刻画数据的几何结构和聚类特性。
以下是谱聚类的主要步骤:
-
构建相似度图:对于给定的数据集,首先计算样本之间的相似度,并构建相似度矩阵。常用的相似度计算方法包括高斯核(Gaussian Kernel)、K近邻(K-Nearest Neighbors)等。
-
构建拉普拉斯矩阵:基于相似度矩阵构建拉普拉斯矩阵(Laplacian Matrix),通常有两种方式:拉普拉斯-贝尔曼算子(Laplacian-Beltrami Operator)和归一化的对称拉普拉斯矩阵。
-
特征分解:对拉普拉斯矩阵进行特征分解(如特征值分解或奇异值分解),得到特征向量。
-
K均值聚类:使用特征向量对样本进行聚类,通常使用 K均值聚类算法在低维空间中对样本进行划分。
# 使用谱聚类进行聚类(假定分为10类
spectral = SpectralClustering(n_clusters=10, affinity='nearest_neighbors', random_state=42)
clusters = spectral.fit_predict(x_train_pca)
4.4.2 AP聚类
基于近邻传播的聚类方法,不需要指定簇的个数,对初始值不敏感,相比K-均值,结果的误差更小。但复杂度较高
4.4.3 Single Pass聚类
针对流式数据的常用增量式聚类算法,效率高。但对于输入样本的顺序敏感,同样数据不同顺序会有不同的
5. 聚类方法比较
6. 性能评估方法
KMeans算法的性能评估要结合多个指标,综合考虑聚类的整体表现。
- 内部指标:
1.1 Inertia:KMeans模型的一个属性,其他模型是没有这个性能指标的,表示每个样本到其最近的聚类中心的距离的总和。Inertia值越小表示样本越接近其分配的簇中心,但不能单独用于评估模型的好坏。
1.2轮廓系数:轮廓系数结合了簇内的紧密度和簇间的分离度。它衡量了一个样本与其分配的簇相比于其他簇的相似性,取值范围在[-1, 1]之间。接近1表示样本聚类合理,接近-1表示样本被错误地分配到相邻的簇中。
1.3 Calinski-Harabasz Index(方差比标准化指数):这个指数是用簇内和簇间的方差之比来度量簇的密集程度。分数值越大表示簇更加密集。
1.4 DB指数(Davies-Bouldin Index):用于衡量簇内的紧密度和簇间的分离度。指数越低,表示簇间分离度越高,簇内紧密度越高,聚类效果越好。
1.5 Dunn指数(Dunn Index):是一种聚类有效性指标,通过簇内距离与簇间距离之间的比率来评估聚类的性能。该指数越大,表示簇内距离越小且簇间距离越大,聚类效果越好。
综合得分
由于任何一个算法都不能通过单一的指标得出性能的好坏,于是在对kmeans性能的评估以及对kmeans的两个优化模型的性能的评估与对比,采用综合评分,即对上述的五种指标的得分进行加权求和,给出一个综合得分。值得注意的是,不同的指标的重要性不同,因此要根据指标的不同重要性赋予指标不同的权重。
下面是各个指标的权重
weights = {
'db_index': 0.25,
'dunn_index': 0.25,
'inertia': 0.15,
'silhouette_score': 0.15,
'calinski_harabasz_score': 0.20
}
计算综合得分
# 计算归一化倒数
min_value = min(db_index_values, dunn_index_values, inertia_values, silhouette_score_values, calinski_harabasz_score_values)
max_value = max(db_index_values, dunn_index_values, inertia_values, silhouette_score_values, calinski_harabasz_score_values)
normalized_inverted_scores = {}
for metric_name, metric_value in {
'db_index': db_index_values,
'dunn_index': dunn_index_values,
'inertia': inertia_values,
'silhouette_score': silhouette_score_values,
'calinski_harabasz_score': calinski_harabasz_score_values
}.items():
if min_value == max_value:
normalized_inverted_scores[metric_name] = 0.0
else:
normalized_inverted_scores[metric_name] = (max_value - metric_value) / (max_value - min_value)
# 计算综合得分
composite_score = sum(normalized_inverted_scores[key] * weights[key] for key in weights)
print(f"Composite Score: {composite_score*100}")
在评估kmeans和其他聚类算法时,由于Inertia只有kmeans模型有,以及在计算其他模型的Dunn值时,发现我的电脑难以跑出结果,始终出现
可视化对比:将不同指标的评估结果进行可视化,比如绘制多个评估指标的变化曲线或柱状图。这样可以直观地比较不同指标在不同聚类数量或不同算法配置下的表现。
聚类质量图:绘制聚类质量图,比如绘制轮廓系数、Calinski-Harabasz指数、Davies-Bouldin指数等评价指标随着聚类数量变化的曲线图。通过观察曲线变化趋势,可以选择合适的聚类数量。
聚类热图:使用聚类热图可视化不同聚类方法在不同评估指标下的得分。这种热图能够直观地显示出每个聚类方法在不同评估指标上的表现,有助于选择合适的聚类方法。
- 外部指标:因为选用的MNIST数据集具有真实标签,所以这里也可以采用外部指标来进行性能的评估。[0,1],越大越好.
- Jaccard系数(Jaccard Coefficient):衡量两个集合交集与并集之间的相似性,对于聚类来说,它衡量两个聚类集合中相同数据点的比例。
- FM指数(Fowlkes-Mallows Index):通过比较真实类别中相同和不同类别对之间的预测对数来衡量聚类结果的相似性。
2.3 Rand指数(Rand Index):衡量聚类结果与真实类别标签之间的相似性,它考虑所有数据点两两之间是否属于同一聚类或同一真实类别。
2.4 NMI(Normalized Mutual Information):通过计算聚类结果与真实标签之间的信息增益来衡量它们之间的一致性。
可视化评估:通过PCA将数据可视化到二维或三维空间,然后用不同颜色或符号表示不同的簇,可以直观地评估聚类的效果。例如,观察是否有明显可分离的簇。
ps:学习笔记,参考常用聚类算法 - 知乎 (zhihu.com)