长文预警!统计学习方法:从零理解聚类方法:基于 Iris 数据集的 K-means 与层次聚类实验

在机器学习中,很多任务都依赖已有标签。例如分类问题需要提前知道每个样本属于哪一类。然而在实际场景中,大量数据往往没有标签。此时,我们希望算法能够根据数据自身的结构,自动发现其中潜在的分组关系。

聚类就是一种典型的无监督学习方法。它的目标是在没有类别标签的情况下,根据样本之间的相似性,将样本划分为若干个簇。理想的聚类结果应当满足:同一簇内的样本尽可能相似,不同簇之间的样本尽可能不同。

本文以经典的 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'])

其中最重要的是:

名称含义
data150 朵花的特征数据
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.13.51.40.20
4.93.01.40.20
4.73.21.30.20
4.63.11.50.20
5.03.61.40.20

这里:

每一行是一朵花,也叫一个样本

前 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')

它们是三种鸢尾花:

标签数字花的种类
0setosa
1versicolor
2virginica

所以 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 的思想可以简单理解为:

  1. 先随机选几个中心点
  2. 每个样本找离自己最近的中心点
  3. 离同一个中心点近的样本分到同一个簇
  4. 重新计算每个簇的中心点
  5. 不断重复,直到结果稳定

你会发现,里面最关键的一句话是:

每个样本找离自己最近的中心点。

所以如果距离计算方式不同,聚类结果也可能不同。

假设一个数据集有两个特征:

样本年收入年龄
A50000025
B60000026

如果直接计算欧氏距离,年收入的差距是:

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 步:

  1. 随机选择 K 个点作为初始中心点
  2. 计算每个样本到 K 个中心点的距离
  3. 把每个样本分到距离最近的中心点对应的簇
  4. 重新计算每个簇的中心点
  5. 重复第 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]

数字 012 表示 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 数据集
KMeanssklearn 提供的 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

意思是:

簇编号样本数
062
150
238

注意:

聚类编号 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 lengthsepal widthpetal lengthpetal widthtargetcluster_rawcluster_scaled
5.13.51.40.2011
4.93.01.40.2011

其中:

列名含义
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. 这个样本和自己簇内其他样本是否足够近
  2. 这个样本和其他簇的样本是否足够远

也就是说,好的聚类应该满足:

簇内紧凑,簇间分离

轮廓系数通常在 -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 个簇

这个过程就像不断把小组并成大组。

层次聚类主要有两种思路:

方法思路
凝聚层次聚类从每个样本单独成簇开始,逐步合并
分裂层次聚类从所有样本属于一个大簇开始,逐步拆分

我们重点学第一种:

凝聚层次聚类,也叫自底向上的层次聚类。

因为它最常用,也更容易理解。

它的过程是:

  1. 一开始,每个样本都是一个簇;
  2. 计算所有簇之间的距离;
  3. 找到距离最近的两个簇;
  4. 把它们合并成一个新簇;
  5. 重复这个过程,直到所有样本合成一个大簇。

假设现在只有 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章:层次聚类实验

这一节我们要完成:

  1. 对 Iris 数据标准化;
  2. 用层次聚类生成树状图;
  3. 把树切成 3 个簇;
  4. 用 ARI 评价聚类结果;
  5. 和 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

这里新增了两个重点工具:

工具作用
AgglomerativeClusteringsklearn 里的凝聚层次聚类
linkage, dendrogram用来生成和绘制树状图

AgglomerativeClustering 这个名字比较长,意思就是:

凝聚式层次聚类,从小簇一步步合并成大簇。

读取 Iris 数据

iris = load_iris()

X = iris.data
y = iris.target

df = pd.DataFrame(X, columns=iris.feature_names)
df['target'] = y

解释一下:

变量含义
X150 朵花的 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_kmeansK-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()

总结

通过实验可以得到以下结论:

  1. 聚类是一种无监督学习方法,它不依赖真实标签,而是根据样本之间的相似性自动发现数据结构;
  2. 距离度量是聚类方法的基础,K-means 和层次聚类都依赖样本之间的距离关系;
  3. K-means 通过不断重复“分配样本”和“更新中心”来获得紧凑的簇;
  4. 层次聚类通过逐步合并样本或簇形成层次结构,并可以用树状图进行可视化解释;
  5. 在 Iris 数据集上,setosa 类别较容易被单独聚出,而 versicolor 和 virginica 之间更容易发生混淆;
  6. 聚类结果评价不能只看一个指标,还需要结合 ARI、轮廓系数、交叉表、可视化结果和算法原理综合分析。

通过这个小项目,可以较完整地理解聚类方法的基本思想、实现方式和实验分析流程。相比直接阅读公式和理论,通过代码实验和可视化结果来学习聚类方法,会更加直观,也更容易形成自己的理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值