在机器学习中,很多任务都依赖已有标签。例如分类问题需要提前知道每个样本属于哪一类。然而在实际场景中,大量数据往往没有标签。此时,我们希望算法能够根据数据自身的结构,自动发现其中潜在的分组关系。
聚类就是一种典型的无监督学习方法。它的目标是在没有类别标签的情况下,根据样本之间的相似性,将样本划分为若干个簇。理想的聚类结果应当满足:同一簇内的样本尽可能相似,不同簇之间的样本尽可能不同。
本文以经典的 Iris 鸢尾花数据集为例,尝试使用 K-means 聚类和层次聚类方法,探索聚类算法是否能够在不使用真实标签的情况下,发现鸢尾花数据中的潜在类别结构。
第 1 章:从零理解聚类
聚类是一种无监督学习方法。
这里有两个关键词:
监督学习:数据里有答案。
比如给你很多花的数据,并且告诉你每朵花属于哪一种,然后让模型学会分类。
无监督学习:数据里没有答案。
比如只给你很多花的数据,但不告诉你花的种类,让算法自己找规律,把相似的花分到一组。
聚类要解决的问题就是:
给定一批没有标签的数据,根据样本之间的相似性,把它们自动分成若干组。
每一组叫做一个簇,英文是 cluster。
假设你有 100 个用户,每个用户有两个特征:
- 每月消费金额
- 每月购买次数
你不知道这些用户属于哪类人,但你希望自动分组。聚类算法可能会帮你发现:
| 用户类型 | 特征 |
|---|---|
| 低消费低频用户 | 买得少,也不常买 |
| 高消费低频用户 | 买得贵,但不常买 |
| 中等消费高频用户 | 经常买,金额中等 |
| 高消费高频用户 | 又常买,又花得多 |
这就是聚类的直觉:把相似的对象放在一起。
聚类和分类有什么区别?
这是初学者最容易混淆的地方。
| 方法 | 数据是否有标签 | 目标 |
|---|---|---|
| 分类 | 有标签 | 学会把新样本分到已知类别 |
| 聚类 | 没有标签 | 自动发现数据中的潜在分组 |
举例:
分类问题:
已知猫、狗、兔子的图片,训练模型识别新图片是猫还是狗。
聚类问题:
给模型一堆动物图片,不告诉它是什么动物,让它自己把相似图片分组。
聚类的核心可以概括成一句话:
同一簇内的样本尽可能相似,不同簇之间的样本尽可能不同。
所以我们需要解决两个问题:
第一,怎么判断两个样本是否相似?
第二,怎么把相似的样本分到同一个簇?
第一个问题对应距离度量。
第二个问题对应具体的聚类算法,比如 K-means 和层次聚类。
第 2 章:准备 Python 环境,读取 Iris 数据集
这一节目标很简单:
把数据读进来,看懂数据长什么样。
你不需要先理解聚类算法,现在只做数据准备。
需要用到的库有:
numpy
pandas
matplotlib
scikit-learn
如果是在本地环境,可以运行:
pip install numpy pandas matplotlib scikit-learn
新建一个 Notebook,然后先运行下面代码:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
解释一下:
| 工具包 | 作用 |
|---|---|
| numpy | 做数值计算 |
| pandas | 处理表格数据 |
| matplotlib | 画图 |
| sklearn | 提供机器学习算法和数据集 |
现在不需要记住所有细节,先会用就行。
继续运行:
iris = load_iris()
这个 iris 里面包含了数据、特征名、类别名等信息。
我们先看看它里面有哪些内容:
iris.keys()
你大概率会看到类似结果:
dict_keys(['data', 'target', 'frame', 'target_names', 'DESCR', 'feature_names', 'filename', 'data_module'])
其中最重要的是:
| 名称 | 含义 |
|---|---|
| data | 150 朵花的特征数据 |
| target | 真实类别标签 |
| feature_names | 特征名称 |
| target_names | 类别名称 |
注意:聚类时我们暂时不用 target,因为聚类是假装没有标签。
运行:
iris.feature_names
你会看到 4 个特征:
['sepal length (cm)',
'sepal width (cm)',
'petal length (cm)',
'petal width (cm)']
翻译一下:
| 英文 | 中文 |
|---|---|
| sepal length | 花萼长度 |
| sepal width | 花萼宽度 |
| petal length | 花瓣长度 |
| petal width | 花瓣宽度 |
每一朵花都由这 4 个数字来描述。
例如一朵花可能是:
花萼长度 = 5.1 cm
花萼宽度 = 3.5 cm
花瓣长度 = 1.4 cm
花瓣宽度 = 0.2 cm
这 4 个数字就是这朵花的特征。
机器学习里的数据通常是矩阵,但表格更容易看。
运行:
X = iris.data
y = iris.target
df = pd.DataFrame(X, columns=iris.feature_names)
df['target'] = y
df.head()
你会看到类似这样的表格:
| sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) | target |
|---|---|---|---|---|
| 5.1 | 3.5 | 1.4 | 0.2 | 0 |
| 4.9 | 3.0 | 1.4 | 0.2 | 0 |
| 4.7 | 3.2 | 1.3 | 0.2 | 0 |
| 4.6 | 3.1 | 1.5 | 0.2 | 0 |
| 5.0 | 3.6 | 1.4 | 0.2 | 0 |
这里:
每一行是一朵花,也叫一个样本
前 4 列是这朵花的特征
最后一列 target 是真实类别
运行:
df.shape
结果应该是:
(150, 5)
意思是:
150 行:150 个样本,也就是 150 朵花
5 列:4 个特征 + 1 个真实标签
不过在聚类实验中,我们主要用前 4 列:
X = df.iloc[:, :4]
X.head()
这里的 X 就是我们真正要拿去聚类的数据。
虽然聚类时不用标签,但为了最后评价聚类效果,我们还是可以看一下真实类别是什么。
运行:
iris.target_names
你会看到:
array(['setosa', 'versicolor', 'virginica'], dtype='<U10')
它们是三种鸢尾花:
| 标签数字 | 花的种类 |
|---|---|
| 0 | setosa |
| 1 | versicolor |
| 2 | virginica |
所以 Iris 数据集本来有 3 类。
这也是为什么后面我们会先尝试把数据聚成 3 类。
运行:
df.describe()
它会显示每个特征的均值、标准差、最小值、最大值等。
你会看到类似这些信息:
花萼长度大概在 4.3 到 7.9 cm 之间
花瓣长度大概在 1.0 到 6.9 cm 之间
不同特征的数值范围不完全一样
这会引出后面的一个重要问题:
如果不同特征的尺度差异很大,距离计算会不会被某些特征主导?
这个问题我们在下一节“距离度量”里会讲。
| 概念 | 解释 |
|---|---|
| 数据集 | 很多样本组成的数据 |
| 样本 | 一条数据,这里是一朵花 |
| 特征 | 描述样本的变量,比如花瓣长度 |
| 标签 | 样本真实类别,聚类时先不用 |
| 特征矩阵 X | 所有样本的特征数据 |
| 标签 y | 所有样本的真实类别 |
聚类实验中,我们只使用特征矩阵 X,不使用真实标签 y。真实标签只在最后评价结果时使用。
完整代码
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
# 读取 Iris 数据集
iris = load_iris()
# 提取特征和标签
X = iris.data
y = iris.target
# 转成 DataFrame,方便查看
df = pd.DataFrame(X, columns=iris.feature_names)
df['target'] = y
# 查看前 5 行
print(df.head())
# 查看数据规模
print("数据规模:", df.shape)
# 查看特征名称
print("特征名称:", iris.feature_names)
# 查看真实类别名称
print("类别名称:", iris.target_names)
# 查看统计信息
print(df.describe())
第 3 章:距离度量——聚类算法如何判断两个样本像不像
这一节是聚类方法的基础。
聚类的核心思想是:
相似的样本应该分到同一个簇,不相似的样本应该分到不同簇。
但问题是:
计算机怎么知道两个样本“相似”?
答案就是:用距离来度量相似性。
什么是距离?
在生活中,我们说两个人“很像”,可能是因为他们身高差不多、体重差不多、兴趣也差不多。
在机器学习里,一个样本通常由多个数字特征表示。
比如 Iris 数据集中,一朵花可以表示成:
[花萼长度, 花萼宽度, 花瓣长度, 花瓣宽度]
例如:
样本 A = [5.1, 3.5, 1.4, 0.2]
样本 B = [4.9, 3.0, 1.4, 0.2]
这两个样本很接近,因为它们的 4 个特征数值差不多。
如果另一个样本是:
样本 C = [7.0, 3.2, 4.7, 1.4]
它和样本 A 差异就比较大。
所以我们希望用一个数字表示:
两个样本之间到底有多远。
这个数字越小,表示越相似;越大,表示越不相似。
最常用的距离是欧氏距离,英文叫 Euclidean Distance。
它可以理解为我们平时说的“直线距离”。
如果只有两个特征,比如:
x1 = 花瓣长度
x2 = 花瓣宽度
那么两个样本之间的距离就是二维平面中的直线距离。
公式是:
d(x, y) = sqrt((x1 - y1)^2 + (x2 - y2)^2)
如果有 4 个特征,公式就变成:
d(x, y) = sqrt(
(x1 - y1)^2
+ (x2 - y2)^2
+ (x3 - y3)^2
+ (x4 - y4)^2
)
你不需要死记公式,先记住一句话:
欧氏距离就是把每个特征上的差异平方后加起来,再开平方。
继续使用上一节的代码。
先读取数据:
import numpy as np
import pandas as pd
from sklearn.datasets import load_iris
iris = load_iris()
X = iris.data
y = iris.target
df = pd.DataFrame(X, columns=iris.feature_names)
df['target'] = y
取前两个样本:
sample_1 = X[0]
sample_2 = X[1]
print(sample_1)
print(sample_2)
你会看到:
[5.1 3.5 1.4 0.2]
[4.9 3. 1.4 0.2]
手动计算欧氏距离:
distance = np.sqrt(np.sum((sample_1 - sample_2) ** 2))
print(distance)
结果是:
0.5385164807134502
说明第 1 朵花和第 2 朵花非常接近。
我们再取一个差异比较大的样本:
sample_3 = X[50]
distance_1_2 = np.sqrt(np.sum((sample_1 - sample_2) ** 2))
distance_1_3 = np.sqrt(np.sum((sample_1 - sample_3) ** 2))
print("样本1和样本2的距离:", distance_1_2)
print("样本1和样本3的距离:", distance_1_3)
你可能会看到类似结果:
样本1和样本2的距离: 0.5385164807134502
样本1和样本3的距离: 4.003748243833521
这说明:
样本 1 和样本 2 更相似
样本 1 和样本 3 差异更大
这就是距离度量在聚类中的作用。
除了欧氏距离,还有一种常见距离叫曼哈顿距离。
公式是:
d(x, y) = |x1 - y1| + |x2 - y2| + ... + |xn - yn|
用 Python 计算:
manhattan_distance = np.sum(np.abs(sample_1 - sample_2))
print(manhattan_distance)
欧氏距离和曼哈顿距离都可以衡量样本差异,但 K-means 中最常用的是欧氏距离。
聚类算法本质上就是根据距离来分组。
比如 K-means 的思想可以简单理解为:
- 先随机选几个中心点
- 每个样本找离自己最近的中心点
- 离同一个中心点近的样本分到同一个簇
- 重新计算每个簇的中心点
- 不断重复,直到结果稳定
你会发现,里面最关键的一句话是:
每个样本找离自己最近的中心点。
所以如果距离计算方式不同,聚类结果也可能不同。
假设一个数据集有两个特征:
| 样本 | 年收入 | 年龄 |
|---|---|---|
| A | 500000 | 25 |
| B | 600000 | 26 |
如果直接计算欧氏距离,年收入的差距是:
600000 - 500000 = 100000
年龄的差距是:
26 - 25 = 1
那么距离几乎完全被“年收入”这个特征控制了,年龄的影响非常小。
这就会导致一个问题:
数值范围大的特征,会在距离计算中占主导地位。
所以在很多聚类任务中,我们需要先做标准化。
标准化的作用是把不同特征拉到相近的尺度。
常见做法是:
标准化后的值 = (原始值 - 平均值) / 标准差
标准化之后,每个特征大致会变成:
- 均值为 0
- 标准差为 1
这样不同特征就更公平了。
在 sklearn 中,可以用 StandardScaler。
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
print(X_scaled[:5])
这里:
X是原始数据X_scaled是标准化后的数据
我们可以对比一下标准化前后的统计信息。
df_X = pd.DataFrame(X, columns=iris.feature_names)
df_X_scaled = pd.DataFrame(X_scaled, columns=iris.feature_names)
print("标准化前:")
print(df_X.describe())
print("标准化后:")
print(df_X_scaled.describe())
你会发现标准化后,每个特征的均值接近 0,标准差接近 1。
除了手写距离公式,也可以用 sklearn 提供的工具。
from sklearn.metrics import pairwise_distances
dist_matrix = pairwise_distances(X_scaled, metric='euclidean')
print(dist_matrix.shape)
print(dist_matrix[:5, :5])
结果的形状是:
(150, 150)
意思是:
150 个样本两两之间的距离。
比如:
dist_matrix[0, 1]
表示第 0 个样本和第 1 个样本之间的距离。
我们先不做真正的聚类,只看看数据在两个特征上的分布。
import matplotlib.pyplot as plt
plt.figure(figsize=(6, 4))
plt.scatter(df['petal length (cm)'], df['petal width (cm)'])
plt.xlabel('petal length (cm)')
plt.ylabel('petal width (cm)')
plt.title('Iris Data: Petal Length vs Petal Width')
plt.show()
你会发现,花瓣长度和花瓣宽度这两个特征上,数据已经隐约形成了几团。这是很粗略的隐约

不过这也说明 Iris 数据适合用来学习聚类。
完整代码:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import pairwise_distances
# 读取数据
iris = load_iris()
X = iris.data
y = iris.target
df = pd.DataFrame(X, columns=iris.feature_names)
df['target'] = y
# 取样本
sample_1 = X[0]
sample_2 = X[1]
sample_3 = X[50]
# 欧氏距离
distance_1_2 = np.sqrt(np.sum((sample_1 - sample_2) ** 2))
distance_1_3 = np.sqrt(np.sum((sample_1 - sample_3) ** 2))
print("样本1和样本2的欧氏距离:", distance_1_2)
print("样本1和样本3的欧氏距离:", distance_1_3)
# 曼哈顿距离
manhattan_distance_1_2 = np.sum(np.abs(sample_1 - sample_2))
print("样本1和样本2的曼哈顿距离:", manhattan_distance_1_2)
# 标准化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
df_X_scaled = pd.DataFrame(X_scaled, columns=iris.feature_names)
print("标准化后数据的统计信息:")
print(df_X_scaled.describe())
# 两两距离矩阵
dist_matrix = pairwise_distances(X_scaled, metric='euclidean')
print("距离矩阵形状:", dist_matrix.shape)
print("前5个样本之间的距离:")
print(dist_matrix[:5, :5])
# 简单散点图
plt.figure(figsize=(6, 4))
plt.scatter(df['petal length (cm)'], df['petal width (cm)'])
plt.xlabel('petal length (cm)')
plt.ylabel('petal width (cm)')
plt.title('Iris Data: Petal Length vs Petal Width')
plt.show()
| 概念 | 简单解释 |
|---|---|
| 距离 | 衡量两个样本差异的数值 |
| 欧氏距离 | 最常用的直线距离 |
| 曼哈顿距离 | 各个特征差值绝对值之和 |
| 距离越小 | 样本越相似 |
| 距离越大 | 样本越不相似 |
| 标准化 | 把不同特征调整到相近尺度 |
| 距离矩阵 | 所有样本两两之间的距离 |
最重要的是这句话:
聚类算法并不真正“理解”样本,它只是根据特征数值计算距离,再根据距离把样本分组。
第 4 章:简单版 K-means实现
这一节很关键。
我们先不调用 sklearn 里的 K-means,而是自己用 Python 写一个简化版。这样你能真正理解:
K-means 到底是怎么一步步把样本分成不同簇的。
K-means 要解决什么问题?
假设我们有一堆没有标签的数据。
比如 Iris 数据集中,每朵花都有 4 个特征:
[花萼长度, 花萼宽度, 花瓣长度, 花瓣宽度]
但是我们先不看它真实属于哪种花。
K-means 要做的事情是:
给定一个簇的数量 K,把所有样本分成 K 组,使得每个样本都尽量靠近自己所在簇的中心。
在 Iris 数据集中,真实类别是 3 类,所以我们先设:
K = 3
K-means 的过程可以理解成 5 步:
- 随机选择 K 个点作为初始中心点
- 计算每个样本到 K 个中心点的距离
- 把每个样本分到距离最近的中心点对应的簇
- 重新计算每个簇的中心点
- 重复第 2 到第 4 步,直到中心点基本不再变化
一句话总结:
K-means 就是在不断重复“分配样本”和“更新中心”。
为了便于理解,我们先不用 Iris 的 4 个特征,而是只用两个特征:
- 花瓣长度
- 花瓣宽度
因为二维数据可以画图。
先运行下面代码:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
读取数据:
iris = load_iris()
X = iris.data
y = iris.target
df = pd.DataFrame(X, columns=iris.feature_names)
df['target'] = y
取两个特征:
X_2d = df[['petal length (cm)', 'petal width (cm)']].values
画出来看看:
plt.figure(figsize=(6, 4))
plt.scatter(X_2d[:, 0], X_2d[:, 1])
plt.xlabel('petal length (cm)')
plt.ylabel('petal width (cm)')
plt.title('Iris Data with Two Features')
plt.show()
你会看到数据大致分成了几团。

真实 K-means 可以处理很多维特征,比如 Iris 的 4 个特征,甚至更高维数据。
但为了学习算法原理,我们先用二维数据更直观。
后面再用 sklearn 对完整 4 个特征做正式实验。
第一步:随机初始化中心点
我们要把数据分成 3 类,所以设:
K = 3
随机选择 3 个样本作为初始中心:
np.random.seed(42)
K = 3
random_indices = np.random.choice(len(X_2d), K, replace=False)
centers = X_2d[random_indices]
print("初始中心点:")
print(centers)
解释一下:
np.random.seed(42)
是为了让随机结果固定下来。这样你每次运行代码,结果都一样,方便学习和复现。
np.random.choice(len(X_2d), K, replace=False)
表示从 150 个样本中随机选 3 个,不重复。
第二步:计算距离
我们需要计算每个样本到每个中心点的距离。
先写一个计算欧氏距离的函数:
def euclidean_distance(a, b):
return np.sqrt(np.sum((a - b) ** 2))
测试一下:
distance = euclidean_distance(X_2d[0], centers[0])
print(distance)
这表示第 0 个样本到第 0 个中心点的距离。
第三步:给每个样本分配簇
现在我们写一个函数:
def assign_clusters(X, centers):
labels = []
for sample in X:
distances = []
for center in centers:
distance = euclidean_distance(sample, center)
distances.append(distance)
cluster_label = np.argmin(distances)
labels.append(cluster_label)
return np.array(labels)
这个函数做的事情是:
对每个样本:
分别计算它到每个中心点的距离
找到距离最小的中心点
把样本分到这个中心点对应的簇
运行:
labels = assign_clusters(X_2d, centers)
print(labels[:20])
你会看到前 20 个样本被分到了哪个簇,比如:
[1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]
数字 0、1、2 表示 K-means 自动分出来的三个簇。
注意:
这里的 0、1、2 不一定对应真实标签里的 0、1、2。
聚类编号只是算法随便给簇起的编号。
第四步:更新中心点
分配完簇之后,我们要重新计算每个簇的中心点。
一个簇的中心点就是这个簇里所有样本的平均值。
写函数:
def update_centers(X, labels, K):
new_centers = []
for k in range(K):
cluster_samples = X[labels == k]
center = cluster_samples.mean(axis=0)
new_centers.append(center)
return np.array(new_centers)
运行:
new_centers = update_centers(X_2d, labels, K)
print("旧中心点:")
print(centers)
print("新中心点:")
print(new_centers)
你会看到中心点发生了变化。
这说明算法正在调整中心,让它们更接近各自簇的样本。
第五步:重复迭代
K-means 不是只做一次分配和更新,而是不断重复。
我们写一个完整的简单版 K-means:
def simple_kmeans(X, K, max_iters=100):
np.random.seed(42)
# 1. 随机初始化中心点
random_indices = np.random.choice(len(X), K, replace=False)
centers = X[random_indices]
for i in range(max_iters):
# 2. 分配簇
labels = assign_clusters(X, centers)
# 3. 更新中心点
new_centers = update_centers(X, labels, K)
# 4. 判断中心点是否不再变化
if np.allclose(centers, new_centers):
print("算法在第", i + 1, "次迭代后收敛")
break
centers = new_centers
return labels, centers
labels, centers = simple_kmeans(X_2d, K=3)
print("最终中心点:")
print(centers)
现在画出聚类结果:
plt.figure(figsize=(6, 4))
plt.scatter(X_2d[:, 0], X_2d[:, 1], c=labels)
plt.scatter(centers[:, 0], centers[:, 1], marker='x', s=200)
plt.xlabel('petal length (cm)')
plt.ylabel('petal width (cm)')
plt.title('Simple K-means Clustering Result')
plt.show()
图中:
每个点是一朵花
颜色表示 K-means 分出来的簇
x 表示每个簇的中心点
你应该能看到,K-means 已经大致把数据分成了 3 组。
完整代码
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
# =========================
# 1. 读取 Iris 数据集
# =========================
iris = load_iris()
# X 是特征数据,每一行是一朵花,每一列是一个特征
X = iris.data
# y 是真实标签,聚类时暂时不用
y = iris.target
# 把数据转成表格,方便查看
df = pd.DataFrame(X, columns=iris.feature_names)
df['target'] = y
# =========================
# 2. 只取两个特征,方便画图
# =========================
# 这里选花瓣长度和花瓣宽度
# X_2d 的形状是 150 行、2 列
X_2d = df[['petal length (cm)', 'petal width (cm)']].values
# =========================
# 3. 定义欧氏距离函数
# =========================
def euclidean_distance(a, b):
"""
计算两个点 a 和 b 之间的欧氏距离
"""
return np.sqrt(np.sum((a - b) ** 2))
# =========================
# 4. 根据当前中心点给样本分组
# =========================
def assign_clusters(X, centers):
"""
X: 所有样本
centers: 当前的中心点
返回:
labels: 每个样本所属的簇编号
"""
labels = [] # 用来保存每个样本的分组结果
# 一个一个处理样本
for sample in X:
distances = [] # 保存当前样本到每个中心点的距离
# 计算当前样本到每个中心点的距离
for center in centers:
distance = euclidean_distance(sample, center)
distances.append(distance)
# 找到距离最近的中心点编号
cluster_label = np.argmin(distances)
# 保存当前样本的簇编号
labels.append(cluster_label)
return np.array(labels)
# =========================
# 5. 根据分组结果更新中心点
# =========================
def update_centers(X, labels, K):
"""
X: 所有样本
labels: 每个样本的簇编号
K: 簇的数量
返回:
new_centers: 新的中心点
"""
new_centers = []
# 分别处理第 0 簇、第 1 簇、第 2 簇
for k in range(K):
# 取出属于第 k 簇的所有样本
cluster_samples = X[labels == k]
# 计算这些样本的平均值,作为新的中心点
center = cluster_samples.mean(axis=0)
# 保存新的中心点
new_centers.append(center)
return np.array(new_centers)
# =========================
# 6. 手写简单版 K-means
# =========================
def simple_kmeans(X, K, max_iters=100):
"""
X: 所有样本
K: 想分成几类
max_iters: 最多迭代多少次
返回:
labels: 每个样本最终所属的簇
centers: 最终中心点
"""
# 固定随机数,保证每次运行结果一样
np.random.seed(42)
# 从所有样本中随机选 K 个,作为初始中心点
random_indices = np.random.choice(len(X), K, replace=False)
centers = X[random_indices]
# 最多迭代 max_iters 次
for i in range(max_iters):
# 第一步:根据当前中心点,给每个样本分组
labels = assign_clusters(X, centers)
# 第二步:根据分组结果,重新计算中心点
new_centers = update_centers(X, labels, K)
# 第三步:判断中心点是否基本不变
if np.allclose(centers, new_centers):
print("算法在第", i + 1, "次迭代后收敛")
break
# 如果中心点还在变化,就继续下一轮
centers = new_centers
return labels, centers
# =========================
# 7. 运行 K-means
# =========================
labels, centers = simple_kmeans(X_2d, K=3)
print("最终中心点:")
print(centers)
# =========================
# 8. 画出聚类结果
# =========================
plt.figure(figsize=(6, 4))
# 画样本点,c=labels 表示按照聚类结果上色
plt.scatter(X_2d[:, 0], X_2d[:, 1], c=labels)
# 画中心点,用 x 表示
plt.scatter(centers[:, 0], centers[:, 1], marker='x', s=200)
plt.xlabel('petal length (cm)')
plt.ylabel('petal width (cm)')
plt.title('Simple K-means Clustering Result')
plt.show()

我们手写的是一个教学版 K-means,它帮助你理解原理,但并不完美。
主要不足有:
| 问题 | 说明 |
|---|---|
| 初始中心随机 | 不同随机中心可能得到不同结果 |
| 没有处理空簇 | 某个簇可能没有样本 |
| 只用了二维特征 | 目前只是为了可视化 |
| 没有标准化 | 这里两个特征尺度接近,所以问题不大 |
| 效率不高 | 用了循环,真实库会更高效 |
正式实验中,我们会使用 sklearn.cluster.KMeans。
现在稍微加一点理论。
K-means 的目标是让每个样本尽量靠近自己所属簇的中心。
可以写成:
最小化:所有样本到其所属簇中心的距离平方和
也就是:
sum ||x_i - μ_k||²
其中:
x_i 表示第 i 个样本
μ_k 表示第 k 个簇的中心
||x_i - μ_k||² 表示样本到簇中心的距离平方
不用害怕这个公式,它其实就是一句话:
K-means 希望每个簇内部尽量紧凑。
| 概念 | 简单解释 |
|---|---|
| 中心点 | 一个簇的代表位置 |
| 分配样本 | 把样本分到最近的中心点 |
| 更新中心 | 用簇内样本平均值作为新中心 |
| 迭代 | 重复分配和更新 |
| 收敛 | 中心点基本不再变化 |
| K | 希望分成的簇数量 |
第 5 章:用 sklearn 做正式 K-means 实验
上一节我们手写了一个教学版 K-means。它帮助你理解原理,但真实项目里一般不会自己手写,而是用成熟工具库。
这一节我们用:
from sklearn.cluster import KMeans
来完成正式实验。
本节目标
今天我们要完成 4 件事:
使用完整的 4 个特征做 K-means 聚类
对数据进行标准化
比较标准化前后的聚类效果
初步理解聚类评价指标
为什么要用 sklearn?
我们手写 K-means 是为了理解原理。
但是正式实验用 sklearn 有几个好处:
| 好处 | 说明 |
|---|---|
| 代码更少 | 不需要自己写距离计算、中心更新 |
| 更稳定 | 内部处理了很多边界情况 |
| 更高效 | 运行速度更快 |
先导入工具包
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import adjusted_rand_score
解释一下:
| 工具 | 作用 |
|---|---|
load_iris | 读取 Iris 数据集 |
KMeans | sklearn 提供的 K-means 聚类算法 |
StandardScaler | 数据标准化 |
adjusted_rand_score | 评价聚类结果和真实标签的接近程度 |
读取 Iris 数据
iris = load_iris()
X = iris.data
y = iris.target
df = pd.DataFrame(X, columns=iris.feature_names)
df['target'] = y
df.head()
这里你要记住:
| 变量 | 含义 |
|---|---|
X | 特征数据,用来聚类 |
y | 真实标签,不参与聚类,只用于最后评价 |
df | 表格形式的数据,方便查看 |
聚类时只用:
X
不用:
y
因为聚类是无监督学习。
先不标准化,直接做 K-means
我们先直接对原始数据做聚类。
kmeans_raw = KMeans(n_clusters=3, random_state=42, n_init=10)
labels_raw = kmeans_raw.fit_predict(X)
这两行是重点。
kmeans_raw = KMeans(n_clusters=3, random_state=42, n_init=10)
意思是创建一个 K-means 模型。
参数解释:
| 参数 | 含义 |
|---|---|
n_clusters=3 | 希望聚成 3 类 |
random_state=42 | 固定随机数,让结果可复现 |
n_init=10 | 用 10 组不同初始中心尝试,选择效果最好的一组 |
为什么是 3 类?
因为 Iris 数据集真实有 3 种花。虽然聚类时不看标签,但为了教学,我们先让算法也聚成 3 类。
labels_raw = kmeans_raw.fit_predict(X)
这句可以拆开理解。
fit 的意思是:
让模型学习数据结构。
predict 的意思是:
给每个样本分配一个簇编号。
所以:
fit_predict(X)
意思是:
对 X 做聚类,并返回每个样本属于哪个簇。
结果 labels_raw 长这样:
[1, 1, 1, 1, 1, ..., 0, 0, 2, 2, ...]
它有 150 个数字,每个数字表示一朵花被分到哪一簇。
print(labels_raw[:20])
这表示查看前 20 个样本的聚类编号。
你可能会看到类似结果:
[1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]
意思是:
前 20 朵花都被分到了同一个簇。
再看每个簇有多少样本:
pd.Series(labels_raw).value_counts()
可能得到类似结果:
0 62
1 50
2 38
意思是:
| 簇编号 | 样本数 |
|---|---|
| 0 | 62 |
| 1 | 50 |
| 2 | 38 |
注意:
聚类编号 0、1、2 本身没有真实含义。
它们只是算法给每个簇起的临时编号。
虽然聚类时不用真实标签,但 Iris 数据集本身有真实标签,所以我们可以事后比较:
聚类结果和真实类别到底有多接近?
我们用一个指标:
adjusted_rand_score
代码:
ari_raw = adjusted_rand_score(y, labels_raw)
print("未标准化数据的 ARI:", ari_raw)
ARI 全名是 Adjusted Rand Index,中文可以叫调整兰德指数。
你现在先不用深入公式,只需要知道:
| ARI 值 | 含义 |
|---|---|
| 接近 1 | 聚类结果和真实类别非常接近 |
| 接近 0 | 聚类结果接近随机分组 |
| 小于 0 | 比随机还差 |
所以这个值越大越好。
K-means 是基于距离的算法。
如果某个特征数值范围特别大,它会在距离计算中占主导。
比如:
| 特征 | 数值范围 |
|---|---|
| 花萼长度 | 4.3 到 7.9 |
| 花瓣宽度 | 0.1 到 2.5 |
虽然 Iris 的几个特征差异不算特别夸张,但标准化仍然是一个好习惯。
标准化的目标是:
让每个特征变得更公平。
对数据做标准化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
解释:
scaler = StandardScaler()
创建一个标准化工具。
X_scaled = scaler.fit_transform(X)
对 X 进行标准化。
标准化后的数据特点是:
每个特征均值接近 0
每个特征标准差接近 1
我们可以验证一下:
df_scaled = pd.DataFrame(X_scaled, columns=iris.feature_names)
df_scaled.describe()
用标准化后的数据做 K-means
kmeans_scaled = KMeans(n_clusters=3, random_state=42, n_init=10)
labels_scaled = kmeans_scaled.fit_predict(X_scaled)
这和前面几乎一样。
区别是:
| 实验 | 使用的数据 |
|---|---|
| 未标准化实验 | X |
| 标准化实验 | X_scaled |
评价标准化后的聚类效果
ari_scaled = adjusted_rand_score(y, labels_scaled)
print("标准化数据的 ARI:", ari_scaled)
现在我们可以比较两个结果:
print("未标准化数据的 ARI:", ari_raw)
print("标准化数据的 ARI:", ari_scaled)
未标准化数据的 ARI: 0.7302382722834697 标准化数据的 ARI: 0.6201351808870379
在 Iris 数据集上,你可能会发现:
标准化后不一定比未标准化更高。
这很正常。
原因是 Iris 数据本身各特征单位相同,尺度差异不算极端。而且花瓣长度、花瓣宽度这类原始尺度本来就很有区分度。
所以标准化不是永远提高效果,而是:
当特征尺度差异较大时,标准化通常更合理。
我们可以把聚类结果加到原来的表格中:
df['cluster_raw'] = labels_raw
df['cluster_scaled'] = labels_scaled
df.head()
现在表格会变成:
| sepal length | sepal width | petal length | petal width | target | cluster_raw | cluster_scaled |
|---|---|---|---|---|---|---|
| 5.1 | 3.5 | 1.4 | 0.2 | 0 | 1 | 1 |
| 4.9 | 3.0 | 1.4 | 0.2 | 0 | 1 | 1 |
其中:
| 列名 | 含义 |
|---|---|
target | 真实类别 |
cluster_raw | 未标准化数据的聚类结果 |
cluster_scaled | 标准化数据的聚类结果 |
未标准化数据各簇样本数: 0 62 1 50 2 38 Name: count, dtype: int64 标准化数据各簇样本数: 0 53 1 50 2 47 Name: count, dtype: int64
完整代码:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import adjusted_rand_score
# =========================
# 1. 读取数据
# =========================
iris = load_iris()
# X 是特征矩阵,包含 150 个样本、4 个特征
X = iris.data
# y 是真实标签,聚类时不用,只用于最后评价
y = iris.target
# 转成表格,方便查看
df = pd.DataFrame(X, columns=iris.feature_names)
df['target'] = y
# =========================
# 2. 未标准化数据的 K-means
# =========================
# 创建 K-means 模型
# n_clusters=3 表示希望分成 3 个簇
# random_state=42 表示固定随机结果
# n_init=10 表示尝试 10 次不同初始中心,选最优结果
kmeans_raw = KMeans(n_clusters=3, random_state=42, n_init=10)
# fit_predict 表示:
# 先学习数据结构,再给每个样本分配簇编号
labels_raw = kmeans_raw.fit_predict(X)
# 用真实标签 y 和聚类结果 labels_raw 计算 ARI
ari_raw = adjusted_rand_score(y, labels_raw)
# =========================
# 3. 标准化数据
# =========================
# 创建标准化工具
scaler = StandardScaler()
# 对 X 做标准化
# 标准化后,每个特征的均值接近 0,标准差接近 1
X_scaled = scaler.fit_transform(X)
# =========================
# 4. 标准化数据的 K-means
# =========================
# 创建新的 K-means 模型
kmeans_scaled = KMeans(n_clusters=3, random_state=42, n_init=10)
# 对标准化后的数据做聚类
labels_scaled = kmeans_scaled.fit_predict(X_scaled)
# 计算标准化后的 ARI
ari_scaled = adjusted_rand_score(y, labels_scaled)
# =========================
# 5. 保存结果到表格中
# =========================
df['cluster_raw'] = labels_raw
df['cluster_scaled'] = labels_scaled
# =========================
# 6. 输出结果
# =========================
print("未标准化数据的 ARI:", ari_raw)
print("标准化数据的 ARI:", ari_scaled)
print("未标准化数据各簇样本数:")
print(pd.Series(labels_raw).value_counts())
print("标准化数据各簇样本数:")
print(pd.Series(labels_scaled).value_counts())
print("结果表格前 5 行:")
print(df.head())
# =========================
# 7. 可视化未标准化聚类结果
# =========================
plt.figure(figsize=(6, 4))
plt.scatter(
df['petal length (cm)'],
df['petal width (cm)'],
c=df['cluster_raw']
)
plt.xlabel('petal length (cm)')
plt.ylabel('petal width (cm)')
plt.title('K-means on Raw Data')
plt.show()
# =========================
# 8. 可视化标准化聚类结果
# =========================
plt.figure(figsize=(6, 4))
plt.scatter(
df['petal length (cm)'],
df['petal width (cm)'],
c=df['cluster_scaled']
)
plt.xlabel('petal length (cm)')
plt.ylabel('petal width (cm)')
plt.title('K-means on Standardized Data')
plt.show()
虽然正式聚类用了 4 个特征,但画图时我们还是只画两个特征:
- 花瓣长度
- 花瓣宽度
因为二维图更容易看。但是你会发现如果只用二维图表示的话,标准化的图甚至不如没有标准化的图,这是正常的,因为画二维图我们只关注了两个指标,但是实际算法用的四个指标。
补充:关于K的选择:
前面我们一直设:
K = 3
因为 Iris 数据集真实有 3 类。
但真实项目里,很多时候我们并不知道数据应该分成几类。
所以这一节要解决的问题是:
K-means 里的 K 到底怎么选?
如果 K 选得太小,可能会把本来不同的群体硬合在一起。
如果 K 选得太大,又可能把本来同一类的样本拆得太碎。
例如:
| K 值 | 可能的问题 |
|---|---|
| K = 1 | 所有样本都被分成一类,太粗糙 |
| K = 2 | 可能漏掉一些真实结构 |
| K = 3 | 对 Iris 来说比较合理 |
| K = 10 | 可能过度拆分,解释困难 |
所以选择 K 是聚类实验中的重要步骤。
方法一:肘部法则
肘部法则的英文是 Elbow Method。
它的核心思想是:
随着 K 增大,簇内误差平方和会下降,但下降速度会逐渐变慢。
我们寻找下降趋势明显变缓的那个点。
这个点看起来像手肘,所以叫“肘部法则”。
什么是簇内误差平方和?
在 K-means 中,每个样本都会被分到某个中心点附近。
我们可以计算:
每个样本到自己簇中心的距离平方,然后全部加起来。
这个值叫:
簇内误差平方和
英文常叫:
SSE
你可以把SSE理解成:
聚类结果内部有多松散。
它越小,说明每个点离自己的中心越近,簇内部越紧凑。
但是注意:
K 越大,SSE 通常一定会越小。
极端情况下,如果 K = 150,每个样本自己单独成一类,那么误差几乎就是 0。
所以我们不能只追求最小,而要找一个比较平衡的 K。
一般来说:
- K 从 1 到 2,下降很多
- K 从 2 到 3,可能还下降明显
- K 继续增大,下降幅度开始变小
如果在 K = 3 附近曲线开始明显变平,那么 K = 3 就是一个比较合理的选择。
不过肘部法则有一个问题:
有时候“肘部”并不明显。
所以我们还需要第二个方法:轮廓系数。
方法二:轮廓系数
轮廓系数的英文是 Silhouette Score。
它想回答一个问题:
一个样本是不是被分到了合适的簇?
它同时考虑两件事:
- 这个样本和自己簇内其他样本是否足够近
- 这个样本和其他簇的样本是否足够远
也就是说,好的聚类应该满足:
簇内紧凑,簇间分离
轮廓系数通常在 -1 到 1 之间。
| 轮廓系数 | 含义 |
|---|---|
| 接近 1 | 聚类效果较好 |
| 接近 0 | 样本在两个簇之间边界不清 |
| 小于 0 | 可能被分错了簇 |
所以轮廓系数越大,一般表示聚类效果越好。
注意:
轮廓系数不能用于 K = 1。
因为只有一个簇时,没法比较“其他簇”。
所以我们通常从 K = 2 开始算。
画图之后,看哪个 K 的轮廓系数更高。
如果 K = 2 的分数最高,但 K = 3 也比较高,这时要结合业务理解和数据背景。
对于 Iris 数据集,可能会出现:
K = 2 的轮廓系数比 K = 3 更高。
这不是错误。
原因是 Iris 数据里有一类 setosa 非常容易和其他两类分开,而另外两类 versicolor 和 virginica 更接近。
所以从“自然分离程度”看,数据可能像是先分成:
setosa 一类
versicolor + virginica 一类
也就是 K = 2 的分离感很强。
但从真实类别背景看,Iris 本身有 3 种花,所以 K = 3 也有解释意义。
对于这个 Iris 小项目,我们可以这样写结论:
肘部法则显示,当 K 从 1 增加到 3 时,簇内误差平方和下降较快,之后下降速度趋缓,因此 K = 3 是一个较合理的选择。
轮廓系数可能在 K = 2 时较高,说明数据中存在较明显的两大类结构。但结合 Iris 数据集的真实背景和三种花类别,本文仍选择 K = 3 作为主要实验设置。
| 方法 | 作用 | 怎么看 |
|---|---|---|
| 肘部法则 | 看误差下降是否变慢 | 找曲线拐点 |
| 轮廓系数 | 看簇内是否紧凑、簇间是否分离 | 分数越高通常越好 |
| K 的选择 | 不是机械决定 | 要结合指标和数据背景 |
第 6 章:层次聚类原理
前面我们学的是 K-means。它的特点是:
先指定 K,然后算法直接把样本分成 K 个簇。
现在我们学习另一类方法:层次聚类。
它的特点是:
不急着直接给出最终分组,而是一步一步形成一个“层次结构”。
什么是层次聚类?
层次聚类,英文是 Hierarchical Clustering。
它不是只给你一个聚类结果,而是给你一个从细到粗的聚类过程。
比如一开始,每个样本都是单独一类:
150 个样本 → 150 个簇
然后每次合并最相似的两个簇:
150 个簇
149 个簇
148 个簇
...
3 个簇
2 个簇
1 个簇
这个过程就像不断把小组并成大组。
层次聚类主要有两种思路:
| 方法 | 思路 |
|---|---|
| 凝聚层次聚类 | 从每个样本单独成簇开始,逐步合并 |
| 分裂层次聚类 | 从所有样本属于一个大簇开始,逐步拆分 |
我们重点学第一种:
凝聚层次聚类,也叫自底向上的层次聚类。
因为它最常用,也更容易理解。
它的过程是:
- 一开始,每个样本都是一个簇;
- 计算所有簇之间的距离;
- 找到距离最近的两个簇;
- 把它们合并成一个新簇;
- 重复这个过程,直到所有样本合成一个大簇。
假设现在只有 5 个样本:
A, B, C, D, E
一开始:
{A}, {B}, {C}, {D}, {E}
如果 A 和 B 最近,就先合并:
{A, B}, {C}, {D}, {E}
如果 D 和 E 最近,再合并:
{A, B}, {C}, {D, E}
如果 C 和 {D, E} 比较近,再合并:
{A, B}, {C, D, E}
最后两个大簇再合并:
{A, B, C, D, E}
这个过程记录下来,就形成了一棵树。
| 对比点 | K-means | 层次聚类 |
|---|---|---|
| 是否需要提前指定 K | 需要 | 不一定一开始指定 |
| 是否有中心点 | 有中心点 | 不一定有中心点 |
| 输出结果 | 一个固定分组 | 一个层次结构 |
| 可解释性 | 看中心和簇 | 可以看树状图 |
| 对初始值敏感吗 | 敏感 | 通常不依赖随机初始中心 |
| 适合观察结构吗 | 一般 | 很适合 |
树状图英文叫 dendrogram。
它可以展示层次聚类的合并过程。
你可以把它想象成这样:
A B C D E
| | | | |
|_____| | |_____|
近 | 近
\ | /
\ |_____/
\ 中等距离
\____/
较远距离
树状图中:
- 底部是样本;
- 样本越早合并,说明越相似;
- 合并高度越低,说明距离越近;
- 合并高度越高,说明距离越远。
K-means 计算的是:
样本到中心点的距离。
但层次聚类里会出现一个问题:
两个簇之间的距离怎么算?
如果每个簇只有一个样本,很简单,直接算两个样本的距离。
但如果一个簇里有很多样本,比如:
簇 A = {A1, A2, A3}
簇 B = {B1, B2, B3}
那簇 A 和簇 B 之间的距离到底是什么?
这就引出了链接方法。
单链接英文是 single linkage。
它定义两个簇之间的距离为:
两个簇中最近两个样本之间的距离。
也就是:
簇间距离 = 最近点之间的距离
直觉:
只要两个簇中有一对点很近,就认为这两个簇很近。
优点:
- 能发现细长形、不规则形状的簇。
缺点:
- 容易出现“链式效应”。
所谓链式效应就是:
一串点一个接一个靠得近,最后可能把很长的一条链都合成一个簇。
全链接英文是 complete linkage。
它定义两个簇之间的距离为:
两个簇中最远两个样本之间的距离。
也就是:
簇间距离 = 最远点之间的距离
直觉:
只有当两个簇整体都比较接近时,才认为它们近。
优点:
- 得到的簇通常更紧凑。
缺点:
- 对离群点比较敏感。
平均链接英文是 average linkage。
它定义两个簇之间的距离为:
两个簇中所有样本两两距离的平均值。
也就是:
簇间距离 = 所有跨簇样本距离的平均值
直觉:
不只看最近点,也不只看最远点,而是看两个簇整体上的平均距离。
它通常比单链接更稳定,也比全链接没那么极端。
Ward 方法
Ward 方法是层次聚类里非常常用的一种方法。
它的思想和 K-means 有一点像:
每次合并两个簇时,选择让簇内平方误差增加最少的那一对簇。
你可以简单理解成:
Ward 方法倾向于合并后仍然比较紧凑的簇。
所以它常常能得到比较均衡、紧凑的聚类结果。
在 sklearn 里,层次聚类默认常用的就是 Ward。
| 方法 | 簇间距离定义 | 特点 |
|---|---|---|
| 单链接 | 最近样本距离 | 容易形成链状簇 |
| 全链接 | 最远样本距离 | 簇更紧凑,但怕离群点 |
| 平均链接 | 所有跨簇距离平均值 | 比较折中 |
| Ward | 合并后簇内误差增加最少 | 常得到紧凑均衡的簇 |
初学时可以先记:
single 看最近,complete 看最远,average 看平均,ward 看合并后是否紧凑。
层次聚类需要提前指定 K 吗?
这个问题很重要。
层次聚类本身会生成一整棵树。
但是如果我们最终想要一个具体分组,还是需要“切树”。
比如你在树状图上横着切一刀:
切得高 → 簇少
切得低 → 簇多
如果我们希望得到 3 个簇,就可以在树上选择一个位置,把树切成 3 个部分。
所以:
层次聚类不一定一开始指定 K,但最终得到具体簇数时仍然需要选择切分位置。
层次聚类的优缺点
优点
- 不需要像 K-means 那样随机初始化中心点;
- 可以通过树状图观察数据的层次结构;
- 适合样本数量不太大的数据;
- 结果解释性比较强。
缺点
- 样本很多时计算成本较高;
- 一旦两个簇合并,后面不会再拆开;
- 对距离度量和链接方法比较敏感;
- 树状图在样本很多时会比较难看清。
Iris 只有 150 个样本,所以非常适合用来学习层次聚类。
第 7章:层次聚类实验
这一节我们要完成:
- 对 Iris 数据标准化;
- 用层次聚类生成树状图;
- 把树切成 3 个簇;
- 用 ARI 评价聚类结果;
- 和 K-means 做简单比较。
先导入工具包
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import AgglomerativeClustering, KMeans
from sklearn.metrics import adjusted_rand_score
from scipy.cluster.hierarchy import linkage, dendrogram
这里新增了两个重点工具:
| 工具 | 作用 |
|---|---|
AgglomerativeClustering | sklearn 里的凝聚层次聚类 |
linkage, dendrogram | 用来生成和绘制树状图 |
AgglomerativeClustering 这个名字比较长,意思就是:
凝聚式层次聚类,从小簇一步步合并成大簇。
读取 Iris 数据
iris = load_iris()
X = iris.data
y = iris.target
df = pd.DataFrame(X, columns=iris.feature_names)
df['target'] = y
解释一下:
| 变量 | 含义 |
|---|---|
X | 150 朵花的 4 个特征 |
y | 真实类别标签 |
df | 表格形式的数据 |
聚类时还是只用:
X
真实标签:
y
只在最后评价时使用。
数据标准化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
这一句前面见过。
它的意思是:
把 4 个特征调整到相近尺度,避免某些特征因为数值范围较大而主导距离计算。
层次聚类也是基于距离的,所以标准化通常也是有必要的。
生成层次聚类结构
我们先用 scipy 生成树状图需要的结构。
Z = linkage(X_scaled, method='ward')
这句很重要。
解释一下:
linkage(X_scaled, method='ward')
意思是:
对标准化后的数据做层次聚类,链接方法使用 Ward 方法。
Z 里面保存的是整个合并过程。
你可以简单理解成:
Z是层次聚类的“合并记录表”。
它记录了:
- 哪两个簇被合并了;
- 合并时的距离是多少;
- 合并后新簇里有多少个样本
画树状图
plt.figure(figsize=(12, 6))
dendrogram(Z)
plt.title('Hierarchical Clustering Dendrogram')
plt.xlabel('Sample Index')
plt.ylabel('Distance')
plt.show()
这段代码会画出完整树状图。
不过 Iris 有 150 个样本,所以底部会比较密集。没关系,我们先看整体结构。
树状图怎么看?
| 图中元素 | 含义 |
|---|---|
| 底部每个点 | 一个样本 |
| 竖线高度 | 合并发生时的距离 |
| 越早合并 | 样本越相似 |
| 越晚合并 | 簇之间差异越大 |
你可以重点看:
有没有某些大分支是在较高距离才合并的。
如果大分支之间合并高度很高,说明它们本来差异比较大。

完整树状图太密,我们可以只显示最后 20 次合并。

用 sklearn 做层次聚类
现在我们真正得到每个样本的聚类标签。
hierarchical = AgglomerativeClustering(
n_clusters=3,
linkage='ward'
)
labels_hierarchical = hierarchical.fit_predict(X_scaled)
逐行解释:
hierarchical = AgglomerativeClustering(
n_clusters=3,
linkage='ward'
)
意思是创建一个层次聚类模型。
参数解释:
| 参数 | 含义 |
|---|---|
n_clusters=3 | 最终切成 3 个簇 |
linkage='ward' | 使用 Ward 方法合并簇 |
然后:
labels_hierarchical = hierarchical.fit_predict(X_scaled)
意思是:
对标准化后的 Iris 数据做层次聚类,并返回每个样本的簇编号。
结果 labels_hierarchical 是一个长度为 150 的数组。
例如:
print(labels_hierarchical[:20])
可能看到:
[1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]
这表示前 20 个样本被分到了同一个簇。
查看每个簇有多少样本
pd.Series(labels_hierarchical).value_counts()
这会输出每个簇的样本数量。
比如可能类似:
0 71
1 49
2 30
这里的 0、1、2 只是聚类编号。
注意:
聚类编号没有固定含义。
第 0 簇不一定对应真实标签 0。
用 ARI 评价层次聚类结果
ari_hierarchical = adjusted_rand_score(y, labels_hierarchical)
print("层次聚类的 ARI:", ari_hierarchical)
ARI 前面学过。
它用来比较:
真实标签 y
和:
聚类结果 labels_hierarchical
是否一致。
| ARI | 含义 |
|---|---|
| 接近 1 | 聚类结果和真实类别很接近 |
| 接近 0 | 接近随机分组 |
| 小于 0 | 比随机还差 |
和 K-means 做比较
我们再跑一遍 K-means。
kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)
labels_kmeans = kmeans.fit_predict(X_scaled)
ari_kmeans = adjusted_rand_score(y, labels_kmeans)
print("K-means 的 ARI:", ari_kmeans)
print("层次聚类的 ARI:", ari_hierarchical)
这样我们就可以比较两种方法。
注意,结果不一定永远是某一个算法更好。
在这个项目里,我们更关注:
两种算法是否都能发现 Iris 数据中的基本结构,以及它们的结果有什么差异。
df['cluster_kmeans'] = labels_kmeans
df['cluster_hierarchical'] = labels_hierarchical
df.head()
现在表格中会多两列:
| 列名 | 含义 |
|---|---|
cluster_kmeans | K-means 聚类结果 |
cluster_hierarchical | 层次聚类结果 |
这样后面写博客时可以直接展示结果。
可视化层次聚类结果
我们还是用花瓣长度和花瓣宽度画二维散点图。
plt.figure(figsize=(6, 4))
plt.scatter(
df['petal length (cm)'],
df['petal width (cm)'],
c=df['cluster_hierarchical']
)
plt.xlabel('petal length (cm)')
plt.ylabel('petal width (cm)')
plt.title('Hierarchical Clustering Result')
plt.show()
解释:
c=df['cluster_hierarchical']
意思是:
根据层次聚类结果给每个点上色。
同一个簇颜色相同。
再画 K-means 结果方便对比
plt.figure(figsize=(6, 4))
plt.scatter(
df['petal length (cm)'],
df['petal width (cm)'],
c=df['cluster_kmeans']
)
plt.xlabel('petal length (cm)')
plt.ylabel('petal width (cm)')
plt.title('K-means Clustering Result')
plt.show()
这样你可以肉眼比较:
- K-means 的分组;
- 层次聚类的分组;
- 哪些区域容易被分错;
- 哪些类别比较容易分开。


完整代码:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import AgglomerativeClustering, KMeans
from sklearn.metrics import adjusted_rand_score
from scipy.cluster.hierarchy import linkage, dendrogram
# =========================
# 1. 读取数据
# =========================
iris = load_iris()
X = iris.data
y = iris.target
df = pd.DataFrame(X, columns=iris.feature_names)
df['target'] = y
# =========================
# 2. 数据标准化
# =========================
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# =========================
# 3. 生成层次聚类结构
# =========================
# Z 记录了层次聚类每一步的合并过程
Z = linkage(X_scaled, method='ward')
# =========================
# 4. 画完整树状图
# =========================
plt.figure(figsize=(12, 6))
dendrogram(Z)
plt.title('Hierarchical Clustering Dendrogram')
plt.xlabel('Sample Index')
plt.ylabel('Distance')
plt.show()
# =========================
# 5. 画简化树状图
# =========================
plt.figure(figsize=(10, 5))
dendrogram(
Z,
truncate_mode='lastp',
p=20
)
plt.title('Truncated Hierarchical Clustering Dendrogram')
plt.xlabel('Cluster Size or Sample Index')
plt.ylabel('Distance')
plt.show()
# =========================
# 6. sklearn 层次聚类
# =========================
# n_clusters=3 表示最终切成 3 个簇
# linkage='ward' 表示使用 Ward 方法
hierarchical = AgglomerativeClustering(
n_clusters=3,
linkage='ward'
)
# 得到每个样本的聚类标签
labels_hierarchical = hierarchical.fit_predict(X_scaled)
# =========================
# 7. K-means 聚类,用来比较
# =========================
kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)
labels_kmeans = kmeans.fit_predict(X_scaled)
# =========================
# 8. 评价聚类效果
# =========================
ari_hierarchical = adjusted_rand_score(y, labels_hierarchical)
ari_kmeans = adjusted_rand_score(y, labels_kmeans)
print("层次聚类的 ARI:", ari_hierarchical)
print("K-means 的 ARI:", ari_kmeans)
print("层次聚类各簇样本数:")
print(pd.Series(labels_hierarchical).value_counts())
print("K-means 各簇样本数:")
print(pd.Series(labels_kmeans).value_counts())
# =========================
# 9. 保存聚类结果
# =========================
df['cluster_hierarchical'] = labels_hierarchical
df['cluster_kmeans'] = labels_kmeans
print("结果表格前 5 行:")
print(df.head())
# =========================
# 10. 可视化层次聚类结果
# =========================
plt.figure(figsize=(6, 4))
plt.scatter(
df['petal length (cm)'],
df['petal width (cm)'],
c=df['cluster_hierarchical']
)
plt.xlabel('petal length (cm)')
plt.ylabel('petal width (cm)')
plt.title('Hierarchical Clustering Result')
plt.show()
# =========================
# 11. 可视化 K-means 结果
# =========================
plt.figure(figsize=(6, 4))
plt.scatter(
df['petal length (cm)'],
df['petal width (cm)'],
c=df['cluster_kmeans']
)
plt.xlabel('petal length (cm)')
plt.ylabel('petal width (cm)')
plt.title('K-means Clustering Result')
plt.show()
总结
通过实验可以得到以下结论:
- 聚类是一种无监督学习方法,它不依赖真实标签,而是根据样本之间的相似性自动发现数据结构;
- 距离度量是聚类方法的基础,K-means 和层次聚类都依赖样本之间的距离关系;
- K-means 通过不断重复“分配样本”和“更新中心”来获得紧凑的簇;
- 层次聚类通过逐步合并样本或簇形成层次结构,并可以用树状图进行可视化解释;
- 在 Iris 数据集上,setosa 类别较容易被单独聚出,而 versicolor 和 virginica 之间更容易发生混淆;
- 聚类结果评价不能只看一个指标,还需要结合 ARI、轮廓系数、交叉表、可视化结果和算法原理综合分析。
通过这个小项目,可以较完整地理解聚类方法的基本思想、实现方式和实验分析流程。相比直接阅读公式和理论,通过代码实验和可视化结果来学习聚类方法,会更加直观,也更容易形成自己的理解。

475

被折叠的 条评论
为什么被折叠?



