一、实验目的
- 了解聚类的概念和层次聚类的方法
- 实现三种不同的层次聚类算法
- 对比三种不同算法在不同的数据集的情况下的性能
二、代码框架
-
本次实验使用的函数框架如下:
1.create_sample(mean, cov, num, label) #生成样本均值向量为mean,协方差矩阵为cov的,数量为num,标签为label的数据集 2.PoMinkowski(x1,x2,dimension,p=2) #两样本点之间Minkowski距离,dimension表示样本的特征维数,p=2时,计算的是欧氏距离 3.clusingle(clu1,clu2,dimension,p=2) #最短距离/单连接 (single linkage) 4.clucomplete(clu1,clu2,dimension,p=2) #最⻓距离/全连接 (complete linkage) 5.cluaverage(clu1,clu2,dimension,p=2) #平均距离 (average linkage) 6.discluster(cluster,dimension,kind=0,p=2) #类距离矩阵的生成,kind表示使用3,4,5中的哪种方法生成类距离矩阵 7.dismin(distance) #根据类距离矩阵确定距离最近的两个类 8.update(cluster,res) #更新类,将cluster中的res编号的两个类合并 9.datastat(cluster) #数据统计,统计合并完成后生成的三个类的数据 10.aggregation(cluster,dimension,kind=0,p=2) #聚合操作 11.makeplt3D(List) #根据List绘制三维点空间分布图 12.makeplt2D(List,label1) #根据List和label绘制分布直方图
三、代码详解
-
产生数据
# 生成数据 def create_sample(mean, cov, num, label): ''' :param mean: 均值向量 :param cov: 协方差矩阵 :param num: 数量 :param label: 标签 :return: 最终生成的数据前三列表示特征,后一列表示便签 ''' x,y,z=np.random.multivariate_normal(mean,cov,num).T L = np.ones(num)*label X=np.array([x,y,z,L]) return X.T
使用
np.random.multivariate_normal
函数生成均值向量为mean,协方差矩阵为cov,数量为num,标签为label的样本数据。 -
两点之间的距离计算
def PoMinkowski(x1,x2,dimension,p=2): ''' :param x1: 点x1 :param x2: 点x2 :param dimension: 两个点所在的空间维度 :param p: 参数p=2时为欧氏距离 :return: 距离 ''' dis = 0 for i in range(dimension): dis = dis + math.pow(x1[i]-x2[i],p) return math.sqrt(dis)
在本次试验中使用闵可夫斯基距离进行计算,默认使用p=2,即计算两个点之间的欧氏距离 -
三种层次聚类算法(基本要求和中级要求)
# 最短距离single linkage def clusingle(clu1,clu2,dimension,p=2): Min = float("inf") for i in range(len(clu1)): for j in range(len(clu2)): d=PoMinkowski(clu1[i],clu2[j],dimension,p) Min = d if d < Min else Min return Min # 最长距离complete linkage def clucomplete(clu1,clu2,dimension,p=2): Max = float("-inf") for i in range(len(clu1)): for j in range(len(clu2)): d = PoMinkowski(clu1[i],clu2[j],dimension,p) Max = d if d>Max else Max return Max # 平均距离average linkage def cluaverage(clu1,clu2,dimension,p=2): d = 0 for i in range(len(clu1)): for j in range(len(clu2)): d = d + PoMinkowski(clu1[i],clu2[j],dimension,p) ans = d/(len(clu1)*len(clu2)) return ans
使用三种不同的方法(如上图)计算两个类之间的距离,类中两个点之间的距离还是使用欧式距离
-
生成类距离矩阵
# 类距离矩阵生成 def discluster(cluster,dimension,kind=0,p=2): ''' :param cluster: 类组成的集合 :param dimension: 点的维度 :param p: p=2 表示欧式距离 :param kind: 使用哪种方法求类距离 :return: 类距离矩阵 ''' if kind == 0: func = clusingle elif kind == 1: func = clucomplete elif kind == 2: func = cluaverage else: print("para:'kind' error") exit(0) templist = np.zeros((len(cluster),len(cluster))) for i in range(len(cluster)): for j in range(len(cluster)): templist[i][j]=func(cluster[i],cluster[j],dimension,p) return templist
kind表示使用哪一种层次聚类方法,定义func为函数指针,templist[i][j]表示第i个类和第j个类的距离
-
找到距离最近的两个类的下标,便于后面的合并
# 找到类集合中距离最近的两个类 def dismin(distance): ''' :param distance: 类距离矩阵 :return: 距离最近的两个类的坐标 ''' Min = float("inf") res=[0,0] for i in range(len(distance)): for j in range(len(distance)): if i!=j and distance[i][j] < Min: Min = distance[i][j] res = [i,j] return res
当i=j时表示的是类和自身的距离,始终为零,当i=j时不需要合并,所以排除j=i的情况,当迭代过类距离矩阵之后,res中储存的是距离最小的两个类的编号
-
合并两个类
# 聚合操作,更新类 def update(cluster,res): a = cluster[res[0]] b = cluster[res[1]] a.extend(b) cluster.remove(b)
使用extend将b合并到a中,使用remove从cluster中移除b,实现a和b的合并
-
数据统计
# 数据统计 def datastat(cluster): ''' :param cluster: 合并之后的类 :return: count 统计结果 label三类的标签 ''' # count统计合并聚类后的三个类中分别含有三种类别样本集合的数量 count = [[0,0,0],[0,0,0],[0,0,0]] for i in range(3): for j in range(len(cluster[i])): count[i][int(cluster[i][j][3]-1)]+=1 count=np.array(count) # 数量最多的类作为合并后这一类的类标签 label = count.argmax(axis=1)+1 return count,label
统计合并之后每一类中不同类标签的数量储存在count中,count[0][2]表示cluster[0]中类标签为3的样本数量,列表label中储存合并后每一类是什么标签(根据这一类中最多的类标签),label[0]储存着cluster[0]是哪一类
-
聚合聚类
# 聚合聚类 def aggregation(cluster,dimension,kind=0,p=2): ''' :param cluster: 样本空间 :param dimension: 维度 :param kind: 求类距离的方法 :param p: 欧氏距离 :return: 聚合后的样本空间 ''' i = 0 # 当类的个数多与3时合并继续 while(len(cluster)>3): # 每一次迭代都重新产生类距离矩阵 distance = discluster(cluster, dimension, kind, p) # 求出距离最近的两个类的下标 res = dismin(distance) # 合并两个类 update(cluster,res)
-
绘图函数
# 绘制三维空间分布图 def makeplt3D(List): point = np.array(List) fig = plt.figure() ax = fig.add_subplot(111,projection='3d') print(point) n=len(point) #print(n) clu = [] for i in range(3): clu.append(list(point[point[:,3]==i+1])) #print(clu) # symbol中储存点的形状和颜色等 symbol = [['r','o'],['b','^'],['g','p']] for i in range(3): temp=[list(c) for c in clu[i]] temp=np.array(temp) x = temp[:,0] y = temp[:,1] z = temp[:,2] ax.scatter(x,y,z,c=symbol[i][0],marker=symbol[i][1]) ax.set_xlabel('X') ax.set_ylabel('Y') ax.set_zlabel('Z') plt.show() # 绘制分布直方图 def makeplt2D(List,label1): plt.bar([0.6, 1.7, 2.7], List[0], width=0.2, label=str(label1[0])) plt.bar([1, 2, 3], List[1], width=0.2, label=str(label1[1])) plt.bar([1.3, 2.3, 3.3], List[2], width=0.2, label=str(label1[2])) plt.legend() plt.xlabel('预测类别') plt.ylabel('数量') plt.title(u'预测类别-数量条形图') plt.show()
-
主体函数
if __name__ == "__main__": # 参数设置 mean = np.array([[1,1,1],[2,2,2],[3,3,3]]) cov = [[0.7,0,0],[0,0.7,0],[0,0,0.7]] n = 501 P = 1/3 num = [round(n*P) for i in range(3)] total = 0 # clus储存合并后的矩阵 clus1 = [] # 数据生成并集合 for i in range(3): total += num[i] clus1.extend(create_sample(mean[i],cov,num[i],i+1)) # 将矩阵转为类 clus=[[list(c)] for c in clus1] print("总共产生了{}个样本".format(total)) for i in range(3): cor = 0 # 深拷贝 cluster = copy.deepcopy(clus) # 打乱顺序(没什么用) random.shuffle(cluster) # 聚合聚类 aggregation(cluster,3,i) # string中储存了三个字符串 print("使用{}-linkage层次聚类算法".format(string[i])) # 返回统计信息 count, label = datastat(cluster) # 输出每一种层次聚类方法的统计信息 for i in range(3): print("cluster[{}]标签为{}".format(i,label[i])) print("每一类的构成及错误率如下:") print("cluster\t类\t类1\t类2\t类3\t错误率\n") for i in range(3): print(" ",i,"\t",label[i],end="") for j in range(3): print("\t{}".format(count[i][j],j+1),end="") cor += count[i][label[i]-1] print("\t{}\n".format(1-count[i][label[i]-1]/sum(count[i])),end="") print("综上:总的错误率为{}".format(1-cor/total))
在这里,要注意的点是
-
在生成数据的时候,我们要将每一个数据转换为二维列表:因为
在第一次产生类距离矩阵的时候,我们计算两个类之间的距离,在这个过程中需要计算两个类中每个点之间的距离,如果类是一个一维列表,如下
此时每个列表中的样本数据为一个array数组,可以认为是一维数组。然后运行,结果报错如下:
显示无效的下标,这是因为在层次聚类函数中,我们需要对每个类中的每个点进行计算,遍历过程如下:
Min = float("inf") for i in range(len(clu1)): for j in range(len(clu2)): d=PoMinkowski(clu1[i],clu2[j],dimension,p) Min = d if d < Min else Min return Min
两重循环遍历两个类中的所有点,但是在第一次遍历的过程中,每个类只有一个点,此时我们每个类是一维向量,range(len(clu1))却是等于4(三个特征值,一个标签),所以在计算点距离的时候x1和x2不是点而是float型的特征值,所以会报错。
-
除此之外,我们在生成数据的时候数据不能为array类型,只能为基本的list类型否则会报错如下:
这是因为在使用list.remove(arg)的过程中,程序首先在list中匹配和arg相等的元素,相等为true,不等为false,涉及到了数据的比较运算。但是,array类型元素的比较不太一样,它返回的是一组的bool值示例如下:
a = np.array([1,2,3]) b = np.array([1,2,4]) print(type(a),type(b)) print(a == b)
比较两个array类型的数据,可以使用any和all,如下:print(any(a==b)) print(all(a==b))
综上两点使用如下方法转换数据为我们需要的数据:clus=[[list(c)] for c in clus1]
-
四、实验结果
(在这里使用不同的协方差矩阵进行对比分析)
(1) 实现 single-linkage 层次聚类算法
(2) 实现complete-linkage 层次聚类算法
(3) 实现average-linkage层次聚类算法
在这里使用生成102个样本(2000个样本运行有些慢)
当协方差矩阵为[[5, 0, 0], [0, 5, 0], [0, 0, 5]]时:
当协方差为5时,三种类别的层次聚类算法的错误率都很高,都到了50%以上,而且我们发现此时的样本聚类都是聚集在某一类中,PS:我们的数据集中三类数据是平均产生的,相比较而言,此时的average-linkage层次聚类算法的性能比较好。
当协方差矩阵为[[0.5, 0, 0], [0, 0.5, 0], [0, 0, 0.5]]时:
当协方差为0.5时,single和average算法仍然出现了严重的分类不均问题。single-linkage算法的错误率并没有明显的下降,仍然为65%上下,complete-linkage算法下降为了14.7%,average-link算法的错误率下降为了33%,此时性能最好的仍然是complete-link层次聚类算法
当协方差矩阵为[[0.05, 0, 0], [0, 0.05, 0], [0, 0, 0.05]]时
当协方差为0.05时,三者的分类准确率均为100%,没有可比性,因为当协方差为0.05时,三类数据都是泾渭分明的,如下图:
(4) 绘图聚类前后样本分布情况
聚类前后样本点在空间中的位置并没有改变(以协方差为0.5为例),如下图:
聚类后三类的分布直方图(以协方差=0.5为例)如下:
此时的数据如下