- 本文为365天深度学习训练营 中的学习记录博客
- 原作者:K同学啊
任务描述:
●学会调用sklearn实现KMeans算法。
●了解误差平方和与轮廓系数。
1.聚类算法是什么?
聚类就是将一个庞杂数据集中具有相似特征的数据自动归类到一起,称为一个簇,簇内的对象越相似,聚类的效果越好。“相似”这一概念,是利用距离标准来衡量的,我们通过计算对象与对象之间的距离远近来判断它们是否属于同一类别,即是否是同一个簇。
聚类是一种无监督学习(Unsupervised Learning)的方法,不需要预先标注好训练集。聚类与分类最大的区别就是分类的目标事先已知,对于一个动物集来说,你并不清楚这个数据集内部有多少种类的动物,你能做的只是利用聚类方法将它自动按照特征分为多类,然后人为给出这个聚类结果的定义(即簇识别)。例如,你将一个动物集分为了三簇(类),然后通过观察这三类动物的特征,你为每一个簇起一个名字,如大象、狗、猫等,这就是聚类的基本思想。
K-means就是一个聚类的算法,属于无监督学习算法,也是就样本没有标签。算法会根据某种规则进行“分割”,把相同的或者相近的数据放在一起。K-means算法的基本思想是通过不断更新簇的中心点,将数据集划分为预定数量的簇。这一过程涉及到计算数据点之间的距离,通常使用欧式距离作为相似性度量。在算法执行过程中,每个数据点被分配到距离最近的簇,然后更新簇的中心,迭代进行直至收敛。
2.K-means 算法思想
在聚类过程中,基于相似性度量,使得相同子集中各元素间差异性最小,而不同子集间的元素差异性最大,这就是(空间)聚类算法的本质,K-Means正是这样一种算法的代表。
K-Means聚类于1957年由J.B. MacQueen首次提出,目前已经超过60年,但仍然是应用最广泛、地位最核心的空间数据划分聚类方法之一。K-means算法算法的输入为一个样本集(或者称为点集),通过该算法可以将样本进行聚类,具有相似特征的样本聚为一类。
针对每个点,计算距离该点最近的中心点,然后将该点归为最近中心点代表的簇。一次迭代结束之后,针对每个簇类,重新计算中心点,然后针对每个点,重新寻找距离自己最近的中心点。如此循环,直到前后两次迭代的簇类没有变化。
3.K-means 聚类过程
K-means算法接受一个参数K用以决定结果中簇的数目。算法开始时,要在数据集中随机选择K个数据对象用来当做K个簇的初始中心,而将剩下的各个数据对象就根据他们和每个聚类簇心的距离选择簇心最近的簇分配到其中。然后重新计算各个聚类簇中的所有数据对象的平均值,并将得到的结果作为新的簇心;逐步重复上述的过程直至目标函数收敛为止。其步骤具体地:
●第一步:从N个样本数据中随机选取K个对象,作为初始的聚类中心;
●第二步:分别计算每个样本点到各个聚类中心的距离,并逐个分配到距离其最近的簇中;
●第三步:所有对象分配完成后,更新K个类中心位置,类中心定义为簇内所有对象在各个维度的均值;
●第四步:与前一次计算得到的K个聚类中心比较,如果聚类中心发生变化,转至步骤2,否则转至步骤5;
●第五步:当类中心不再发生变化,停止并输出聚类结果,然后整理我们所需要的信息,如各个样本所属的类等等,进行后续的统计和分析。
聚类结束之前,类中心会不断移动,而随着类中心的移动,样本的划分情况也会持续发生改变。
4.K-means 代码实现
我的环境:
●语言环境:Python3.9
●编译器:Jupyter Lab
●数据集:Iris.csv
需要提前安装的包为:pandas、matplotlib、sklearn,你需要逐一在命令行(cmd)中输入以下语句对第三方库进行安装:
●pip install pandas
●pip install matplotlib
●pip install scikit-learn
示例:
4.1.导入数据
import time
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from numpy import nonzero, array
from sklearn.decomposition import PCA
# 数据保存在.csv文件中
iris = pd.read_csv("L8/Iris.csv", header=0) # 鸢尾花数据集 Iris class=3
df = iris # 设置要读取的数据集
df
代码输出:
sepal length | sepal width | petal length | petal width | class | |
---|---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 | Iris-setosa |
1 | 4.9 | 3.0 | 1.4 | 0.2 | Iris-setosa |
2 | 4.7 | 3.2 | 1.3 | 0.2 | Iris-setosa |
3 | 4.6 | 3.1 | 1.5 | 0.2 | Iris-setosa |
4 | 5.0 | 3.6 | 1.4 | 0.2 | Iris-setosa |
... | ... | ... | ... | ... | ... |
145 | 6.7 | 3.0 | 5.2 | 2.3 | Iris-virginica |
146 | 6.3 | 2.5 | 5.0 | 1.9 | Iris-virginica |
147 | 6.5 | 3.0 | 5.2 | 2.0 | Iris-virginica |
148 | 6.2 | 3.4 | 5.4 | 2.3 | Iris-virginica |
149 | 5.9 | 3.0 | 5.1 | 1.8 | Iris-virginica |
150 rows × 5 columns
4.2.数据预处理
columns = list(df.columns) # 获取数据集的第一行,第一行通常为特征名,所以先取出
columns
代码输出:
['sepal length', 'sepal width', 'petal length', 'petal width', 'class']
# 数据集的特征名(去除了最后一列,因为最后一列存放的是标签,不是数据)
features = columns[:-1]
features
代码输出:
['sepal length', 'sepal width', 'petal length', 'petal width']
# 预处理之后的数据,去除掉了第一行的数据(因为其为特征名,如果数据第一行不是特征名,可跳过这一步)
dataset = df[features]
dataset
代码输出:
sepal length | sepal width | petal length | petal width | |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
2 | 4.7 | 3.2 | 1.3 | 0.2 |
3 | 4.6 | 3.1 | 1.5 | 0.2 |
4 | 5.0 | 3.6 | 1.4 | 0.2 |
... | ... | ... | ... | ... |
145 | 6.7 | 3.0 | 5.2 | 2.3 |
146 | 6.3 | 2.5 | 5.0 | 1.9 |
147 | 6.5 | 3.0 | 5.2 | 2.0 |
148 | 6.2 | 3.4 | 5.4 | 2.3 |
149 | 5.9 | 3.0 | 5.1 | 1.8 |
150 rows × 4 columns
attributes = len(df.columns) - 1 # 属性数量(数据集维度)
original_labels = list(df[columns[-1]]) # 原始标签
attributes
代码输出:
4
original_labels
代码输出:
['Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-setosa',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-versicolor',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica',
'Iris-virginica']
4.3.绘制聚类结果散点图
def draw_cluster(dataset, centers, labels, k):
center_array = array(centers)
if attributes > 2:
# 将三维数据降为二维,方便进行可视化
dataset = PCA(n_components=2).fit_transform(dataset) # 如果属性数量大于2,降维
center_array = PCA(n_components=2).fit_transform(center_array) # 如果属性数量大于2,降维
else:
dataset = array(dataset)
# 做散点图
label = array(labels)
plt.scatter(dataset[:, 0],
dataset[:, 1],
marker='o',
c='black', s=7) # 原图
colors = np.array(
["#FF0000", "#0000FF", "#00FF00", "#FFFF00", "#00FFFF", "#FF00FF", "#800000", "#008000", "#000080", "#808000",
"#800080", "#008080", "#444444", "#FFD700", "#008080"])
# 循换打印k个簇,每个簇使用不同的颜色
for i in range(k):
plt.scatter(dataset[nonzero(label == i), 0],
dataset[nonzero(label == i), 1],
c=colors[i],
s=7, marker='o')
plt.show()
4.4.进行聚类分析
from sklearn.cluster import KMeans
k = 3
# 使用KMeans进行聚类
kmeans = KMeans(n_clusters=k, n_init=10).fit(dataset) # 指定要分的簇数
labels = kmeans.labels_
labels
代码输出:
array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0,
0, 0, 0, 2, 2, 0, 0, 0, 0, 2, 0, 2, 0, 2, 0, 0, 2, 2, 0, 0, 0, 0,
0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 2], dtype=int32)
centers = kmeans.cluster_centers_
centers
代码输出:
array([[6.85 , 3.07368421, 5.74210526, 2.07105263],
[5.006 , 3.418 , 1.464 , 0.244 ],
[5.9016129 , 2.7483871 , 4.39354839, 1.43387097]])
draw_cluster(dataset, centers, labels, k)
代码输出:
5.k 值的选择
k-means算法因为手动选取k值和初始化随机质心的缘故,每一次的结果不会完全一样,而且由于手动选取k值,我们需要知道我们选取的k值是否合理,聚类效果好不好,那么如何来评价某一次的聚类效果呢?
也许将它们画在图上直接观察是最好的办法,但现实是,我们的数据不会仅仅只有两个特征,一般来说都有十几个特征,而观察十几维的空间对我们来说是一个无法完成的任务。因此,我们需要一个公式来帮助我们判断聚类的性能,这个公式就是SSE (Sum of Squared Error, 误差平方和),它其实就是每一个点到其簇内质心的距离的平方值的总和,SSE值越小表示数据点越接近于它们的质心,聚类效果也越好。
因为对误差取了平方,因此更加重视那些远离中心的点。一种肯定可以降低SSE值的方法是增加簇的个数,但这违背了聚类的目标。聚类的目标是在保持簇数目不变的情况下提高簇的质量。
5.1.误差平方和(SSE)
这是因为在确定聚类簇数时,还必须考虑计算成本。如果我们增加聚类的数量,计算成本也会增加。K-means算法还存在一个问题,它有可能会停留在一个局部最小值处,而这取决于初始化的情况。为了解决这个问题,我们通常需要多次运行K-means算法,每一次都重新进行随机初始化,最后再比较多次运行K-means的结果。
代码实现:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
# 提供的数据
data = {
'K1': [3, 3, 4, 4, 1, 1, 2, 2],
'K2': [1, 2, 1, 2, 3, 4, 3, 4]
}
# 创建DataFrame
df = pd.DataFrame(data)
# 提取用于聚类的特征
X = df[['K1', 'K2']]
# 初始化KMeans模型列表,并设定k的范围
range_n_clusters = [1, 2, 3, 4, 5, 6, 7, 8] # 扩大了k的范围
inertia_scores = []
# 对每个k值进行聚类并计算SSE值
for n_clusters in range_n_clusters:
# 使用KMeans算法
kmeans = KMeans(n_init=10,
n_clusters=n_clusters,
random_state=123)
kmeans.fit(X)
# 获取SSE值
inertia = kmeans.inertia_
inertia_scores.append(inertia)
# 绘制惯性指标随聚类数量变化的图形
plt.figure(figsize=(8, 4))
plt.plot(range_n_clusters, inertia_scores, marker='o')
plt.xlabel('Number of clusters')
plt.ylabel('SSE')
plt.title('SSE for Different Number of Clusters')
plt.xticks(range_n_clusters) # 显示所有x轴刻度
plt.show()
代码输出:
5.2.轮廓系数(silhouette)
轮廓系数用于评估数据点与其所属簇的凝聚度及其与最近邻簇的分离度,得分范围在[-1, 1]之间,分值越高表示聚类效果越好。每个样本都有对应的轮廓系数,轮廓系数由两个得分组成:
● a:样本与同一簇类中的其他样本点的平均距离 ( 即凝聚度);
● b:样本与距离最近簇类中所有样本点的平均距离 (即分离度)。
每个样本的轮廓系数定义为:
一组数据集的轮廓系数等于该数据集中每一个样本轮廊系数的平均值。轮廓系数既要考虑聚类结果的凝聚度,又要考虑聚类结果之间的分离度。如果一个数据点与自己所属的簇内的其他数据点的距离很小,但是与其他簇中的数据点的距离很大,就表示这个数据点所在的簇内紧密度高,簇间分离度大,那么该数据点的轮廓系数就会越大。
代码实现:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
# 提供的数据
data = {
'K1': [3, 3, 4, 4, 1, 1, 2, 2],
'K2': [1, 2, 1, 2, 3, 4, 3, 4]
}
# 创建DataFrame
df = pd.DataFrame(data)
# 提取用于聚类的特征
X = df[['K1', 'K2']]
# 初始化KMeans模型列表,并设定k的范围
range_n_clusters = [2, 3, 4, 5, 6, 7] # 根据需要调整范围
silhouette_avg_scores = []
# 对每个k值进行聚类并计算轮廓系数
for n_clusters in range_n_clusters:
# 使用KMeans算法
kmeans = KMeans(n_init=10, n_clusters=n_clusters, random_state=42)
kmeans.fit(X)
# 获取聚类标签
labels = kmeans.labels_
# 计算轮廓系数
silhouette_avg = silhouette_score(X, labels)
silhouette_avg_scores.append(silhouette_avg)
# 绘制轮廓系数随聚类数量变化的图形
plt.figure(figsize=(8, 4))
plt.plot(range_n_clusters, silhouette_avg_scores, marker='o')
plt.xlabel('Number of clusters')
plt.ylabel('Silhouette Score')
plt.title('Silhouette Score for Different Number of Clusters')
plt.show()
代码输出: