目录
实验前准备
本实验是在Anaconda下的jupyter notebook上进行的,使用的代码语言为python。在开始实验前,我们首先需要导入所需要的库与包或者模块。本实验是一个将数据进行聚类的实验,需要处理大量的实验数据,需要处理多维数组对象,以及可能需要画图进行可视化处理,还有一些数学公式的运用,所以我们需要导入的包为numpy、pandas、math以及matplotlib.pyplot。同时我们还需要使用random函数来随机选取样本,所以我们还需要导入random。最后我们还需要导入warnings模块将一些可能出现的警告信息忽略。
代码实现:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import math
import matplotlib as mpl
import warnings
import random
warnings.filterwarnings('ignore')
from pandas.core.frame import DataFrame
from matplotlib.axes._axes import _log as matplotlib_axes_logger
matplotlib_axes_logger.setLevel('ERROR')
计算样本间距离
曼哈顿距离
实验要求:
代码实现:
#曼哈顿距离
def manhattan_distance(x, y):
# 首先确保输入的两个样本的维数是相同的
assert len(x)==len(y),"Input vectors must have the same dimensions"
# 然后获取这两个样本的维数
d=len(x)
# 初始化距离变量
distance=0
# 开始遍历样本的所有数据
for i in range(d):
distance+=abs(x[i]-y[i])
return distance
实验结果分析:如图所示,我们在计算曼哈顿距离的时候,根据公式,我们首先需要确保输入的两个样本的维度是相同的,然后获取这两个样本的维度,同时还需要初始化距离变量。最后便可以遍历所有的数据样本,然后根据公式计算曼哈顿距离并且进行叠加即可得到这两个样本间的曼哈顿距离。
欧氏距离
实验要求:
代码实现:
#欧式距离
def euclidean_distance(x,y):
# 首先确定输入的两个样本的维数是相同的
assert len(x)==len(y),"Input vectors must have the same dimensions"
# 然后获取这两个样本的维数
d=len(x)
# 初始化距离变量
distance=0
# 开始遍历样本的所有数据
for i in range(d):
distance+=pow((x[i]-y[i]),2)
distance=math.sqrt(distance)
return distance
实验结果分析:如图所示,我们需要计算欧式距离,我们可以根据公式,首先确保输入的两个样本的维度是相同的,然后获取这两个样本的维度是多少,随后初始化距离变量,最后即可遍历所有的样本数据,然后根据公式计算距离并且进行叠加,在计算完所有的数据距离的叠加值后将这个值开方即可得到这两个样本间的欧式距离。
选择距离计算方法
我们在这里实现了两种计算样本间相似度距离的公式,分别是曼哈顿距离以及欧式距离。计算样本间相似度的距离我们会在后续的实验中使用到,而为了方便将实验数据以及实验想象进行对比,我们需要在后续的实验中只使用这两种距离计算公式中的一种来进行计算,而在这里,我们使用的是欧式距离来进行下一步的实验。
原型聚类法(K-means算法)
初始化数据
实验要求:
代码实现:
#加载数据集
train_frame=pd.read_csv("train.csv")
print(train_frame)
#聚类数量
k=3
#初始化每个聚类的簇心向量
cluster_centers = [[] for _ in range(k)]
#随机选取k个样本作为初始均值向量
cluster_centers = random.sample(train_frame.values.tolist(), k)
print(cluster_centers)
#初始化各个聚类集合
clusters = [[] for _ in range(k)]
实验结果分析:如图所示,我们在实现原型聚类法时,首先需要初始化参数。我们首先需要加载数据集,将train数据集加载到train_frame中,然后将聚类数量k值初始化为3,我们也可以将其设为不同的取值,随后不断调整即可。随后我们初始化每一个聚类的簇心向量,以及随机选取k个样本作为簇心向量的初始均值向量。最后我们还需要初始化各个聚类集合列表。
开始迭代(更新均值向量)
实验要求:
代码实现:
#比较均值向量是否相同
def equallist(x,y):
# 首先确保输入的两个向量的维数相同
assert len(x)==len(y),"Input vectors must have the same dimensions"
# 初始化布尔值
Is_equal=True
# 然后迭代样本的所有数据
for i in range(len(x)):
if not np.array_equal(x[i],y[i]):
Is_equal=False
break
return Is_equal
#迭代过程
while True:
num+=1
#初始化一个新的簇心列表
new_cluster_centers=[[] for _ in range(k)]
# 清空各个聚类集合
clusters=[[] for _ in range(k)]
#计算每个样本与k个聚类的簇心的距离,将其划入距离最近的簇
for sample in train_frame.values:
# 初始化距离哪一个簇更近参数
# 初始化距离为最大值
near_i=-1
distance=1e9
# 开始迭代找出该样本距离哪一个均值向量更近
for i in range(k):
# 求出欧式距离
temp_distance=euclidean_distance(sample,cluster_centers[i])
# 如果新的距离比原来的距离小,那么更新距离以及下标
if temp_distance<distance:
distance=temp_distance
near_i=i
# 将该样本加入到相应的簇中
clusters[near_i].append(sample)
#更新这轮迭代的簇心
for i in range(k):
new_cluster_centers[i]=np.mean(clusters[i],axis=0)
# 比较更新的簇心是否与未更新前的簇心是否相同
# 如果相同,就退出迭代
# 如果不同,就更新簇心
if equallist(new_cluster_centers,cluster_centers):
break
# 更新簇心
cluster_centers=new_cluster_centers
#输出划分的聚类情况
for i, cluster in enumerate(clusters):
print(f"Cluster {i+1}:")
for sample in cluster:
print(sample)
print()
实验结果分析:如图所示,我们首先需要实现一个函数,用来比较前后两个均值向量是否相同,在这个函数中,我们首先还是需要确保两个输入的向量的维度相同,然后初始化一个布尔值,并且开始迭代该样本的所有的数据,如果有一个不相等就返回False,如果相等就返回True。
随后在迭代的过程中,按照算法的伪代码实现,我们首先初始化一个新的簇心列表,同时清空各个聚类集合。下一步就是需要计算每一个样本与k个聚类的簇心的距离,并且将其划入距离最近的簇。我们首先需要遍历所有的样本数据,然后初始化距离一个变量用于存储距离哪一个簇最近,同时初始化距离为最大值。随后便可以开始迭代,求出样本距离每一个簇心的欧式距离,然后更新距离为最近以及下标。随后可以将该样本加入到前面找出的距离最近的簇的中。随后我们需要更新这一轮迭代中的簇心,比较更新后的簇心是否与未更新的簇心相同,如果相同就退出迭代,如果不同就更新簇心。最后我们将每一个簇的聚类结果打印出来即可
去除空簇
实验要求:
代码实现:
#your code here
# 首先初始化一个列表,存放非空的簇
non_empty_clusters=[]
for cluster in clusters:
if cluster:
non_empty_clusters.append(cluster)
# 输出所有非空簇
for i, cluster in enumerate(non_empty_clusters):
print(f"Cluster {i+1}:")
for sample in cluster:
print(sample)
print()
实验结果分析:如图所示,我们首先需要初始化定义一个列表,用于存放非空的簇,然后开始遍历,如果该簇非空,就将该簇加入到这一个列表中。最后将非空的簇打印出来即可。
绘图显示聚类结果
实验要求:
代码实现:
#your code here
# 首先获取每个样本的聚类标签
labels = []
for i, cluster in enumerate(non_empty_clusters):
labels.extend([i+1] * len(cluster))
# 提取特征值作为坐标
x_values = [sample[0] for cluster in non_empty_clusters for sample in cluster]
y_values = [sample[1] for cluster in non_empty_clusters for sample in cluster]
# 绘制散点图
plt.scatter(x_values, y_values, c=labels)
plt.xlabel('Weight')
plt.ylabel('Height')
plt.title('K-Means Clustered')
plt.show()
实验结果分析:如图所示,我们首先获取每一个样本数据的聚类标签,通过遍历非空簇中的所有数据来获取标签。然后我们可以提取特征值作为我们的横纵坐标。最后我们绘制直接绘制散点图即可,颜色的选择我们是随机选择的,每一次的颜色结果均有所变化。
观察实验结果图像,我们在上面初始化参数的时候选择的聚类数量k值为3,我们可以看见我们所绘制的图像确实将这些样本数据分成了三类,并且使用了不同颜色的点来进行区分,同时可以发现这个聚类程度还是相当高的,将在一堆的数据点都划分为了一类。所以我们可以知道我们的实验还是相当成功的,同时也证明我们前面所编写的代码还是正确的。
密度聚类法(DBSCAN算法)
查找样本领域中的所有样本对象
实验要求:
代码实现:
def get_neighbors(D, index, epsilon):
# 首先初始化领域列表
neighbors=[]
# 将D转换为array类型
D=np.array(D)
# 获取当前样本的特征向量
current_sample=D[index]
# 遍历所有样本
for i,sample in enumerate(D):
# 首先跳过当前样本
if i==index:
continue
# 然后利用欧式距离计算这两个样本之间的距离
distance=euclidean_distance(current_sample,sample)
# 如果样本与当前样本的距离小于等于邻域半径,则将其加入邻域内
if distance<=epsilon:
neighbors.append(i)
return neighbors
实验结果分析:如图所示,我们需要实现查找出该样本中的领域对象中的所有样本对象的函数。我们首先初始化领域列表,用于存放该样本中领域内的样本的下标,随后将数据集转换为数组类型,同时获取当前样本的特征向量。随后便可以开始遍历所有样本数据,我们首先需要跳过当前的样本,即如果遍历到当前样本,可以跳过不做处理。然后利用欧式距离计算出遍历到的这一个样本与当前样本的距离,如果这一个距离在阈值内,就将这一个样本的下标加入到领域列表中。
查找样本中的所有核心对象
实验内容:
代码实现:
def core_set(D,epsilon,MinPts):
# 初始化核心对象集合
core_objects=[]
# 对每个样本进行遍历
for i in range(len(D)):
# 获取邻域内的所有样本的索引
neighbors=get_neighbors(D,i,epsilon)
# 如果邻域内的样本数量大于等于最小样本数,则将当前样本(下标)标记为核心对象
if len(neighbors)>=MinPts:
core_objects.append(i)
return core_objects
实验结果分析:如图所示,我们需要查找出该样本数据集的所有核心对象,我们首先初始化一个列表用于存放所有的核心对象,然后可以开始遍历该样本数据集中的每一个样本,首先利用前面的函数获取当前样本下的领域内的所有样本索引下标,然后进行判断,如果该领域内的样本数量多于一定的阈值,那么当前样本就可以被标记为核心对象。
开始迭代进行聚类
实验内容:
代码实现:
#初始化参数epsilon,MinPts
D=train_frame
epsilon=0.9
MinPts=5
# 初始化标签数组,0表示未分类
labels=np.zeros(len(D))
# 生成核心对象集合
core_objects=core_set(D,epsilon,MinPts)
# 定义当前簇的标签
current_cluster_label=1
# 对核心对象集合进行遍历
for i in core_objects:
# 如果核心对象已经分类,则跳过
if labels[i]!=0:
continue
# 创建一个新的簇,将核心对象标记为该簇
labels[i]=current_cluster_label
# 获取由核心对象密度直达的样本集合Δ
delta=get_neighbors(D,i,epsilon)
# 遍历样本集合Δ
while len(delta)>0:
# 取出一个样本(首个样本)
sample=delta[0]
# 如果样本已经分类,则跳过
if(labels[sample]!=0):
delta=delta[1:]
continue
# 将样本标记为当前簇
labels[sample]=current_cluster_label
# 获取由样本密度直达的样本集合Δ'
delta_prime=get_neighbors(D,sample,epsilon)
# 如果样本是核心对象,则将Δ'中的样本加入Δ
if len(delta_prime)>=MinPts:
delta.extend(delta_prime)
# 更新簇的标签
current_cluster_label+=1
实验结果分析:如图所示,我们开始进行聚类分析。我们首先需要初始化参数epsilon、MinFts以及数据集D,前面两个参数都是可以调试变化的。同时还需要初始化一个标签数组,初始化为全0。然后调用前面的函数来生成核心对象集合,同时定义当前簇的标签为1。随后便可以开始对核心对象集合进行遍历,遍历所有的核心对象。如果该核心对象已经分类了,就跳过不做处理。随后我们需要创建一个新的簇,将核心对象标记为该簇。然后获取由该核心对象密度直达的样本集合delta,随后遍历该样本集合delta。我们首先取出该样本集合的首个样本,如果该样本已经分类过,则更新样本集合delta,如果没有就将该样本标记为当前簇的标签,然后继续获取由该样本密度直达的样本集合delta_prime。如果该样本是一个核心对象,就将这一个样本集合delta_prime加入到样本集合delta中。最后我们还需要不断更新簇的标签,将其不断加1即可。
绘制图像显示聚类结果
实验内容:
代码分析:
# your code here
# 获取数据集的特征值列
x = D['Weight']
y = D['Height']
# 先绘制所有样本点
plt.scatter(x, y, color='gray', label='Unclassified')
# 遍历每个样本的标签
for label in np.unique(labels):
# 跳过未分类的样本
if label == 0:
continue
# 获取当前簇的样本点
cluster_samples = D[labels == label]
# 获取当前簇的特征值列
cluster_x = cluster_samples['Weight']
cluster_y = cluster_samples['Height']
# 为当前簇的样本点选择一个颜色
color = np.random.rand(3,) # 随机生成RGB颜色
# 绘制当前簇的样本点
plt.scatter(cluster_x, cluster_y, color=color, label='Cluster {}'.format(label))
# 添加图例
plt.legend()
# 设置横纵坐标轴标签以及标题
plt.xlabel('Weight')
plt.ylabel('Height')
plt.title('DBSCAN Clustered')
# 显示图形
plt.show()
实验结果分析:如图所示,我们首先需要获取数据集的特征列,然后绘制出所有的数据点,同时将该数据点设置为灰色,定义为未分类的数据点。然后我们便可以遍历所有的标签,应为每一个标签都对应着一个聚类,我们遍历每一个标签,如果有标签值为0,即没有被分类的就直接跳过,然后查找获取样本中与当前标签值相等的样本数据,同时获取当前簇的特征值列,随后我们为当前簇的样本点随机选择一个颜色,最后便可以调用库进行绘画即可。
观察实验结果图像,我们可以发现,随着改变epsilon的值,聚类的结果也会发生变化,我们在观察之后,选择的epsilon为0.9,然后我们可以发现,该聚类方法也是将该样本数据聚类为了3类,其中还有一些没有被分类的,因为有一些点可能不在样本点的领域内。我们对比k聚类方法的聚类结果,我们可以得知我们的聚类结果应该是正确无误的,同时可以知道我们所编写的代码也是正确的。
实验总结
原型聚类法:
- 原理:原型聚类法假设每个聚类由一个或多个原型(代表性样本)来表示,通过寻找原型之间的相似性来划分数据集。
- 基本概念:原型聚类算法的基本概念包括原型选择和样本分配。原型选择是确定原型的个数和位置,常见的方法有k-means算法和k-medoids算法。样本分配是将数据样本分配给最近的原型。
- 代表算法:k-聚类算法是原型聚类的代表算法之一。它通过迭代的方式将数据集划分为k个簇,每个簇由一个中心点(原型)来表示。k-聚类算法的目标是最小化簇内样本的平方距离之和。
- 特点:原型聚类法的优点包括简单、易于解释和可扩展性强。然而,它对初始聚类中心的选择敏感,并且在处理噪声和离群点时效果较差。适用于数据集具有明显的聚类结构、簇形状规则、维度较低的情况。
密度聚类法:
- 原理:密度聚类法基于样本之间的密度可达性来划分数据集,将高密度区域划分为簇,并区分离群点和噪声。
- 基本概念:密度聚类算法的基本概念包括核心对象、密度直达和密度可达。核心对象是在给定半径内具有一定数量邻居的样本。密度直达表示样本可以通过一系列邻居样本的密度直达到达另一个样本。密度可达表示样本可以通过一系列样本(不一定是邻居)的密度直达达到另一个样本。基于这些概念,DBSCAN(基于密度的空间聚类应用噪声)是密度聚类的代表算法之一。
- 代表算法:DBSCAN算法通过定义邻域半径和最小样本点数来确定核心对象,然后通过密度可达性将核心对象连接成簇。DBSCAN算法能够自动识别任意形状的簇,并将离群点识别为噪声。
- 特点:密度聚类法的优点包括能够处理任意形状的簇、对噪声和离群点具有鲁棒性。然而,它对参数的选择(如邻域半径和最小样本点数)敏感,并且在高维数据集上效果较差。适用于数据集具有不规则形状、噪声和离群点较多的情况。
k-聚类算法(k-means clustering):
- 原理:k-聚类算法将数据集划分为k个簇,每个簇由一个中心点(原型)来表示。算法通过迭代的方式,不断更新簇的中心点,直到达到收敛条件为止。
- 基本概念:
- 簇中心点:每个簇由一个中心点来表示,中心点是该簇中所有样本的平均值或中位数。
- 样本分配:将每个样本分配给距离最近的中心点所属的簇。
- 差异:
- k-聚类算法基于欧几里德距离度量样本之间的相似性,通过最小化簇内样本的平方距离之和来确定最佳的簇。
- k-聚类算法需要指定簇的数量k,且对初始的簇中心点的选择敏感。
- 特点:
- 优点:简单、易于实现、计算效率高。
- 缺点:对初始簇中心点的选择敏感,可能收敛到局部最优解,不适用于处理非球形簇或噪声点。
- 适应条件:适用于数据集具有明显的聚类结构、簇形状规则且维度较低的情况。
DBSCAN聚类算法:
- 原理:DBSCAN聚类算法基于样本之间的密度可达性来划分数据集,将高密度的样本划分为簇,并区分离群点和噪声。
- 基本概念:
- 核心对象:在给定半径ε内具有至少MinPts个邻居的样本被认为是核心对象。
- 密度直达:样本A可以通过一系列邻居样本的密度直达到达样本B,表示A在B的ε-邻域内,并且B是A的核心对象。
- 密度可达:样本A可以通过一系列样本(不一定是邻居)的密度直达到达样本B。
- 差异:
- DBSCAN算法能够自动识别任意形状的簇,并将离群点识别为噪声。
- DBSCAN算法不需要指定簇的数量,而是通过密度可达性将核心对象连接成簇。
- 特点:
- 优点:能够处理任意形状的簇、对噪声和离群点具有鲁棒性。
- 缺点:对参数的选择(如邻域半径ε和最小样本点数MinPts)敏感,对高维数据集效果较差。
- 适应条件:适用于数据集具有不规则形状、噪声和离群点较多的情况。
原型聚类法与密度聚类法的比较:
- 原型聚类法将聚类看作是原型之间的相似性,寻找最佳原型及其分配。
- 密度聚类法基于样本之间的密度可达性,将高密度区域划分为簇,并区分离群点和噪声。
- 代表算法中,k-聚类算法是原型聚类的一个代表算法,而DBSCAN算法是密度聚类的代表算法。
- 原型聚类法适用于数据集具有明显的聚类结构、簇形状规则、维度较低的情况。
- 密度聚类法适用于数据集具有不规则形状、噪声和离群点较多的情况。
- 原型聚类法的优点是简单、易于解释和可扩展性强,但对初始聚类中心的选择敏感,并且对噪声和离群点处理效果较差。密度聚类法的优点是能够处理任意形状的簇、对噪声和离群点具有鲁棒性,但对参数选择敏感,并且在高维数据集上效果较差。
K-聚类算法与DBSCAN聚类算法的比较:
- k-聚类算法是基于原型的聚类算法,通过迭代更新簇的中心点来划分数据集。它对初始簇中心点的选择敏感,适用于处理具有明显聚类结构和规则形状的数据集。
- DBSCAN聚类算法是基于密度的聚类算法,通过密度可达性来划分数据集,能够处理任意形状的簇,并识别离群点和噪声。它对参数的选择敏感,适用于处理具有不规则形状、噪声和离群点较多的数据集。
- 请注意,聚类算法的选择应根据数据集的特点和需求进行评估,不同算法在不同情况下可能具有不同的表现。
选择适当的聚类算法取决于数据集的特征和需求。如果数据集具有明显的聚类结构、维度较低且簇形状规则,则原型聚类法(如k-聚类算法)可能更合适。如果数据集具有不规则形状、噪声和离群点较多,则密度聚类法(如DBSCAN算法)可能更适用。同时,在实际应用中,还需要根据具体情况调整算法参数以获得更好的聚类结果。