1. 从零开始:为什么选择鸢尾花和K-means?

如果你刚开始接触机器学习,面对一堆算法和数据集,是不是感觉有点无从下手?我刚开始学的时候也是这样,总觉得理论很复杂,代码很抽象。后来我发现,最好的学习方法就是找一个经典的数据集,亲手把整个流程跑一遍。而鸢尾花数据集K-means聚类算法,就是为你量身定做的“新手村黄金组合”。

鸢尾花数据集在机器学习界的地位,大概相当于“Hello, World!”在编程界的地位。它由统计学家费雪在1936年整理,包含了150朵鸢尾花的测量数据,每朵花有四个特征:萼片长度、萼片宽度、花瓣长度和花瓣宽度。这些花又分属三个品种:山鸢尾、变色鸢尾和维吉尼亚鸢尾。数据量不大,特征清晰,类别明确,简直就是为教学而生的。更重要的是,它是个“干净”的数据集,没有太多缺失值和异常值,让你能把精力完全集中在理解算法和流程上,而不是在数据清洗上耗费大量时间。

那为什么选择K-means呢?因为它是所有聚类算法里最直观、最容易理解的一个。它的核心思想简单到可以用一句话概括:物以类聚,人以群分。算法会试图把数据点分成K个组,让同一个组内的点彼此尽量靠近,不同组的点尽量远离。你不用告诉算法这些花本来属于哪个品种(这就是“无监督学习”),它自己就能根据四个特征的数值,把相似的花归到一起。这个过程,就像你有一堆没贴标签的积木,仅凭形状和颜色就能把它们分成几堆,非常符合人类的直觉。

所以,这个实战项目的目标非常明确:我们不追求多么高深的算法创新,而是要完整地、清晰地走通一个机器学习项目的基本流程。从怎么把数据读进Python,到怎么用图形找出最佳的聚类数量,再到训练模型、画出结果、评估好坏,每一步我都会带着你手把手操作。等你跟着走完这一遍,你收获的不仅仅是一个能运行的代码,更是一个可以套用到其他数据集上的标准分析框架。下次你再遇到客户数据、用户行为数据,就知道该从哪里下手了。

2. 搭建你的分析环境:工具准备与数据初探

工欲善其事,必先利其器。在开始写代码之前,我们得先把“厨房”收拾好。我个人的主力工具是 VS Code,因为它轻量、插件丰富,写Python体验很好。当然,你用Jupyter Notebook或者PyCharm也完全没问题,核心是Python环境和那几个关键的库。

首先,确保你安装了Python(建议3.8以上版本)。然后,打开你的终端或命令提示符,我们一次性把需要的“食材”都备齐。这里我强烈建议使用pip进行安装,并且为了环境干净,你可以先创建一个虚拟环境。

# 创建一个新的虚拟环境(可选但推荐)
python -m venv iris_cluster_env

# 激活虚拟环境
# 在Windows上:
iris_cluster_env\Scripts\activate
# 在Mac/Linux上:
source iris_cluster_env/bin/activate

# 安装核心库
pip install pandas numpy matplotlib scikit-learn seaborn

让我简单介绍一下这几个库是干嘛的,这样你用起来心里更有底:

  • pandas: 我们的“数据管家”。读数据、看数据、清洗数据、整理数据,绝大部分操作都靠它。它能把数据表格(比如Excel、CSV)变成Python里一种叫DataFrame的灵活结构,操作起来非常方便。
  • numpy: 是Python科学计算的基石,提供高效的数组(矩阵)运算。很多机器学习算法底层都在用它进行数学计算。
  • matplotlib: 最经典的绘图库。我们后面画的肘部法则图、散点图,都靠它。它可能不如一些新库美观,但功能最全、最稳定。
  • scikit-learn(简称sklearn): 机器学习算法的“瑞士军刀”。我们用的K-means算法,以及计算评估指标的函数,都来自这里。它的API设计非常一致,学了一个算法,其他算法上手也很快。
  • seaborn: 基于matplotlib的“美化大师”。它可以用更简洁的代码画出更漂亮的统计图形,尤其是它的配色方案,让图表瞬间专业起来。

环境搞定,接下来就是获取数据。鸢尾花数据集太经典了,以至于sklearn库自己就内置了它,我们不需要去任何网站下载。这大大简化了第一步。让我们把数据加载进来,先看看它长什么样。

# 导入所有需要的库
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn import metrics
import seaborn as sns
from sklearn.datasets import load_iris

# 从sklearn中直接加载鸢尾花数据集
iris_data = load_iris()

# 将数据转换为pandas的DataFrame,这样查看起来更直观
# iris_data.data 是特征数据, iris_data.feature_names 是特征名
iris = pd.DataFrame(iris_data.data, columns=iris_data.feature_names)

# 添加目标列(品种),这列我们先留着,最后可以用来对比聚类效果和真实情况
iris['species'] = iris_data.target

# 查看数据的前10行
print("数据预览:")
print(iris.head(10))

# 查看数据的基本信息:有多少行、多少列、有无缺失值、数据类型
print("\n数据信息:")
print(iris.info())

# 查看基本的统计描述:均值、标准差、最小值、最大值等
print("\n数据统计描述:")
print(iris.describe())

运行这几行代码,你就能对数据有个全面的第一印象。你会看到150行、5列数据(4个特征+1个真实品种),没有缺失值。从统计描述里,你可能会发现花瓣长度和宽度的数值范围比萼片小很多,这个细节我们后面会用到。这一步看似简单,但养成先看数据再分析的习惯,能帮你避免很多低级错误,比如用错了列,或者没发现异常值。

3. 数据预处理:为聚类模型准备“食材”

数据拿到了,是不是可以直接扔给K-means算法了?先别急。想象一下,你要把身高(单位是米,比如1.7、1.8)和体重(单位是公斤,比如60、70)放在一起计算距离。身高的数值变化范围小,体重的数值变化范围大,直接算的话,体重对结果的影响会远远大于身高,这显然不公平。我们的鸢尾花数据也存在类似问题,萼片长度可能集中在5-7厘米,而花瓣宽度集中在0.1-0.3厘米,尺度差异很大。

K-means算法的核心是计算数据点之间的距离(通常是欧氏距离)。如果某个特征的数值特别大,它在距离计算中的“话语权”就会过重,导致聚类结果被这个特征主导,而其他特征几乎不起作用。这显然不是我们想看到的,我们希望四个特征能公平地参与“投票”,共同决定一朵花应该属于哪一类。

所以,我们需要进行特征标准化。这是数据预处理中非常关键的一步,目标是把所有特征的数据都转换到同一个尺度上,通常是均值为0,标准差为1的分布。sklearn提供了非常方便的工具来做这件事。

# 首先,将特征数据(前四列)和目标数据(品种列)分开
# X 是我们要用来聚类的特征数据
X = iris.iloc[:, :4].values
# y 是真实的品种标签,留着最后做对比验证
y = iris['species'].values

# 导入标准化工具
from sklearn.preprocessing import StandardScaler

# 创建一个标准化器
scaler = StandardScaler()
# 用特征数据X来“训练”这个标准化器,计算均值和标准差
scaler.fit(X)
# 使用计算好的均值和标准差来转换数据X
X_scaled = scaler.transform(X)

# 让我们看看标准化前后的对比(以第一朵花的第一个特征为例)
print(f"原始萼片长度数据(前5个样本): {X[:5, 0]}")
print(f"标准化后萼片长度数据(前5个样本): {X_scaled[:5, 0]}")
print(f"标准化后,特征的均值: {X_scaled[:, 0].mean():.2f}, 标准差: {X_scaled[:, 0].std():.2f}")

执行完这段代码,你会发现原来数值在5左右的萼片长度,现在变成了围绕0上下波动的数值(比如-0.9, 1.0)。所有特征都变成了类似的分布。这样,在后续计算距离时,每个特征都被赋予了同等的重要性。这一步虽然代码简单,但对聚类结果的影响可能是决定性的。我刚开始时就曾忽略过这一步,结果聚出来的类完全不符合预期,排查了好久才找到原因。所以,请务必记住:在使用基于距离的算法(如K-means)前,先检查特征尺度,必要时进行标准化。

4. 寻找最佳的K值:肘部法则实战

好了,现在数据已经准备妥当,我们可以请出主角K-means了。但第一个问题就来了:K-means里的“K”到底应该设成几?我们知道鸢尾花有三种,所以设K=3好像很合理。但在真实项目中,你面对的数据可没有“标准答案”。这时候,我们就需要一种客观的方法来辅助决策,这就是肘部法则

肘部法则的原理很直观:随着聚类数量K的增加,样本会被划分得越来越细,每个簇内的样本会越来越相似,那么所有样本到其所属簇中心的距离平方和(称为簇内平方和WCSS)自然会越来越小。当K小于真实簇数时,增加K会大幅增加每个簇的聚合程度,WCSS会迅速下降。当K达到或超过真实簇数时,再增加K,WCSS的下降幅度会突然变得平缓。这个拐点,就像人的手肘关节,因此得名“肘部法则”。

我们来用代码实现它,并画出这个关键的“肘部图”。

# 初始化一个空列表,用来存储不同K值对应的WCSS
wcss = []

# 尝试K从1到10
for i in range(1, 11):
    # 创建K-means模型,设置聚类数为i,random_state是为了保证每次运行结果一致
    kmeans = KMeans(n_clusters=i, init='k-means++', random_state=42, n_init=10)
    # 用标准化后的数据拟合模型
    kmeans.fit(X_scaled)
    # kmeans.inertia_ 属性就是WCSS的值,把它存到列表里
    wcss.append(kmeans.inertia_)

# 开始画图
plt.figure(figsize=(10, 6))
# 画出折线图,x轴是K值,y轴是WCSS,用圆点标记
plt.plot(range(1, 11), wcss, marker='o', linestyle='--', linewidth=2, markersize=8)
plt.title('肘部法则寻找最佳K值', fontsize=14, fontweight='bold')
plt.xlabel('聚类数量 (K)', fontsize=12)
plt.ylabel('簇内平方和 (WCSS)', fontsize=12)
plt.xticks(range(1, 11))
plt.grid(True, linestyle=':', alpha=0.7) # 添加网格线,方便观察
plt.show()

运行代码后,你会得到一张清晰的折线图。仔细看这条线,当K从1增加到2,再到3时,WCSS的下降非常陡峭。但是从K=3到K=4,再到K=5,下降的曲线明显变得平缓了。那个最明显的“拐点”或者说“肘部”,就出现在K=3的位置。这强有力地印证了我们的先验知识,也证明了肘部法则的有效性。在实际项目中,如果拐点不明显,你可能需要结合其他方法(如轮廓系数)或者业务知识来综合判断。但在这个例子里,答案非常清晰:最佳聚类数就是3

这里有个小细节我提一下,代码里init='k-means++'n_init=10。K-means算法开始时需要随机选择初始的簇中心点,不同的初始点可能导致不同的结果。k-means++是一种更聪明的初始化方法,能让算法更快、更稳定地收敛到好的结果。n_init=10表示算法会用不同的初始中心点运行10次,最终选择WCSS最小的那次作为结果。这些都是实践中的小技巧,能让你得到更可靠、可重复的模型。

5. 训练与洞察:运行K-means并理解结果

确定了K=3,我们就可以正式训练模型了。这一步的代码其实非常简单,但模型训练完成后,产出的结果却包含着丰富的信息,我们需要学会解读它们。

# 创建最终的K-means模型,设定聚类数为3
final_kmeans = KMeans(n_clusters=3, init='k-means++', random_state=42, n_init=10)
# 拟合标准化后的数据
final_kmeans.fit(X_scaled)

# 模型训练完成后,我们可以获取几个关键结果:
# 1. 每个样本所属的簇标签 (0, 1, 2)
cluster_labels = final_kmeans.labels_
# 2. 每个簇的中心点坐标(在标准化后的空间里)
cluster_centers = final_kmeans.cluster_centers_
# 3. 最终的WCSS值
final_wcss = final_kmeans.inertia_

print(f"每个样本的聚类标签(前15个): {cluster_labels[:15]}")
print(f"聚类中心坐标:\n{cluster_centers}")
print(f"模型最终的WCSS值: {final_wcss:.2f}")

# 为了方便分析,我们把聚类标签添加回原始的数据集
iris['cluster'] = cluster_labels
print("\n添加了聚类标签后的数据前10行:")
print(iris[['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)', 'species', 'cluster']].head(10))

现在,我们有了cluster这一列。你可以对比一下species(真实品种)和cluster(模型预测的簇)。你会发现,虽然标签的数字可能对不上(比如模型把山鸢尾标成了0,而真实标签是0),但同一品种的花基本都被分到了同一个簇里。这初步说明我们的聚类是有效的。

更有趣的是cluster_centers,它代表了每个“簇”在四个特征维度上的平均形象,或者说,是这个簇的“原型花”。由于数据是标准化过的,我们需要反向转换一下,或者直接比较相对大小。比如,你可以观察哪个簇的花瓣长度和宽度数值最大,那它很可能对应着真实品种里花瓣最大的维吉尼亚鸢尾。通过分析这些中心点,你可以为每个簇赋予一个业务上的含义,比如“小花瓣型”、“大萼片型”等,这才是聚类分析产生洞察的关键一步,让模型结果不再是冷冰冰的数字。

6. 让结果一目了然:聚类结果可视化

俗话说,一图胜千言。尤其对于聚类这种空间划分问题,可视化能最直观地展示模型的效果。我们的数据有四个维度,但屏幕是二维的,所以我们通常选择两个特征来画散点图。这里我演示两种最常用的可视化方式。

第一种:使用Matplotlib和Seaborn绘制基础散点图。 我们选择萼片长度和萼片宽度作为横纵坐标。

# 设置图形大小和风格
plt.figure(figsize=(12, 5))

# 子图1:显示真实的品种分布
plt.subplot(1, 2, 1)
# 用不同的颜色和标记表示不同的真实品种
scatter1 = plt.scatter(X[:, 0], X[:, 1], c=y, cmap='tab10', edgecolor='k', s=50)
plt.title('真实品种分布 (Sepal)', fontsize=13)
plt.xlabel('萼片长度 (cm)')
plt.ylabel('萼片宽度 (cm)')
plt.colorbar(scatter1, ticks=[0, 1, 2], label='Species')

# 子图2:显示K-means聚类结果
plt.subplot(1, 2, 2)
# 用聚类标签来着色
scatter2 = plt.scatter(X[:, 0], X[:, 1], c=cluster_labels, cmap='tab10', edgecolor='k', s=50)
# 画出聚类中心点(注意:中心点坐标是标准化后的,需要反向转换到原始尺度才能画在同一张图上)
# 为了简化,这里我们直接用标准化前的特征均值来近似标注中心点位置,更严谨的做法是进行逆标准化
centers_original = scaler.inverse_transform(cluster_centers)
plt.scatter(centers_original[:, 0], centers_original[:, 1], c='red', marker='X', s=200, alpha=0.8, label='聚类中心')
plt.title('K-means聚类结果 (Sepal)', fontsize=13)
plt.xlabel('萼片长度 (cm)')
plt.ylabel('萼片宽度 (cm)')
plt.colorbar(scatter2, ticks=[0, 1, 2], label='Cluster')
plt.legend()

plt.tight_layout()
plt.show()

第二种:使用Seaborn的pairplot进行多维关系观察。 散点图矩阵可以一次性查看所有特征两两之间的关系,能更全面地评估聚类效果。

# 为了画图方便,我们创建一个包含所有信息的临时DataFrame
iris_viz = iris.copy()
# 将数字标签映射为有意义的字符串,方便看图
iris_viz['cluster_name'] = iris_viz['cluster'].map({0: 'Cluster_0', 1: 'Cluster_1', 2: 'Cluster_2'})
iris_viz['species_name'] = iris_viz['species'].map({0: 'setosa', 1: 'versicolor', 2: 'virginica'})

# 绘制配对图,对角线显示每个特征的分布直方图
# 我们用聚类结果来着色
sns.pairplot(iris_viz, vars=iris_data.feature_names, hue='cluster_name', palette='viridis', diag_kind='kde', corner=True)
plt.suptitle('特征关系与聚类分布配对图', y=1.02, fontsize=16)
plt.show()

通过对比左右两幅子图,你可以清晰地看到K-means算法成功地将数据分成了三个簇,并且其边界与真实品种的分布高度吻合。在pairplot中,你会发现在花瓣长度和花瓣宽度组成的子图中,三个簇的分离度尤其高,几乎没有重叠。这说明花瓣的尺寸是区分这三个品种最关键的特征,这个结论与我们之前观察数据统计描述时的猜想是一致的。可视化不仅验证了模型,更深化了我们对数据本身的理解。

7. 模型好坏谁说了算?量化评估聚类效果

看图感觉不错,但我们需要更客观的数字来证明模型的好坏。在分类任务中,我们有准确率这样的明确指标。但在无监督的聚类中,因为没有“标准答案”,评估起来更复杂一些。这里我介绍两个最常用的内部评估指标:轮廓系数戴维斯-邦丁指数。它们都不需要真实标签,只根据数据自身的聚类结果来计算。

轮廓系数衡量的是同一个簇内的样本是否足够紧密,而不同簇的样本是否足够分离。对于每个样本i,计算:

  1. a(i):i到同簇内所有其他样本的平均距离(内聚度)。
  2. b(i):i到其他某个簇的所有样本的平均距离,取这些平均距离中的最小值(分离度)。
  3. 样本i的轮廓系数 s(i) = (b(i) - a(i)) / max(a(i), b(i))。

s(i)的值在-1到1之间。越接近1,说明样本i聚类得越好;越接近-1,说明它可能被分错了簇;接近0,则说明样本处在两个簇的边界上。所有样本s(i)的平均值就是整个数据集的轮廓系数。

戴维斯-邦丁指数衡量的是簇间的分离度与簇内的内聚度之比。它的计算基于每个簇内样本到其中心的平均距离,以及不同簇中心之间的距离。DBI的值越小越好,理想情况接近0,值越大说明簇之间重叠度越高或簇内越分散。

# 计算轮廓系数
silhouette_avg = metrics.silhouette_score(X_scaled, cluster_labels)
print(f'轮廓系数 (Silhouette Score): {silhouette_avg:.3f}')

# 计算戴维斯-邦丁指数
dbi = metrics.davies_bouldin_score(X_scaled, cluster_labels)
print(f'戴维斯-邦丁指数 (Davies-Bouldin Index): {dbi:.3f}')

# 为了更细致地观察,我们可以计算每个样本的轮廓系数,并可视化
sample_silhouette_values = metrics.silhouette_samples(X_scaled, cluster_labels)

# 绘制轮廓系数图
plt.figure(figsize=(10, 7))
y_lower = 10
for i in range(3):
    # 获取属于簇i的所有样本的轮廓系数,并排序
    ith_cluster_silhouette_values = sample_silhouette_values[cluster_labels == i]
    ith_cluster_silhouette_values.sort()
    size_cluster_i = ith_cluster_silhouette_values.shape[0]
    y_upper = y_lower + size_cluster_i
    color = plt.cm.viridis(float(i) / 3)
    plt.fill_betweenx(np.arange(y_lower, y_upper),
                      0, ith_cluster_silhouette_values,
                      facecolor=color, edgecolor=color, alpha=0.7)
    # 在图中标注簇的编号
    plt.text(-0.05, y_lower + 0.5 * size_cluster_i, str(i))
    y_lower = y_upper + 10

plt.axvline(x=silhouette_avg, color="red", linestyle="--", label=f'平均轮廓系数: {silhouette_avg:.3f}')
plt.title("各样本轮廓系数分布图", fontsize=14)
plt.xlabel("轮廓系数值")
plt.ylabel("簇标签")
plt.yticks([])
plt.legend()
plt.show()

对于鸢尾花数据,轮廓系数通常能达到0.5以上,DBI通常在0.5以下,这都表明聚类效果很好。轮廓系数图能让你一眼看出哪个簇的样本轮廓系数普遍较高(图形又长又靠右),哪个簇可能存在一些边界点(图形较短或向左延伸)。如果某个簇的图形又短又靠左,你就需要去检查这个簇里的样本,看看是不是特征选择有问题,或者这个簇本身就不够紧凑。

8. 举一反三:K-means的局限与项目扩展思考

跟着走完这一遍,你已经成功完成了一个标准的无监督学习分析流程。但我想告诉你的是,K-means虽然强大直观,但它并非万能。了解它的局限性,能帮助你在未来更准确地选择工具。

首先,K-means要求你事先指定K值。在真实项目中,肘部法则的拐点可能很不明显,这时就需要你结合业务理解或尝试其他方法(如轮廓系数随K的变化曲线、层次聚类等)来综合判断。

其次,K-means对异常值非常敏感。因为中心点是簇内所有点的均值,一个远离群体的异常点会“拉偏”中心点的位置,从而影响整个簇的划分。在实际应用中,稳健的数据清洗和异常值处理必不可少。

第三,K-means假设簇是凸形的、大小密度相近的。想象一下,如果你的数据是几个嵌套的圆圈,或者一个月牙形和一个球形,K-means就很难正确划分。这时你可能需要考虑基于密度的算法,比如DBSCAN。

最后,K-means的结果受初始随机中心点影响。虽然我们用了k-means++和多次初始化来缓解,但理论上每次运行仍可能得到略有不同的结果。对于关键任务,多次运行取最优解是稳妥的做法。

那么这个项目可以如何扩展呢?你可以尝试:

  1. 不进行标准化,直接用原始数据跑一遍流程,对比一下轮廓系数和可视化结果,亲身感受特征标准化的威力。
  2. 尝试用主成分分析先把四维数据降到二维,再在这个二维空间上进行聚类和可视化,看看效果如何。
  3. 把整个流程封装成一个函数,输入是任意一个数据集和特征列,函数自动完成标准化、寻找最佳K值、训练、评估和基础绘图。这就是你工具箱里的一个可复用脚本了。

机器学习实战就是这样,从一个干净的小数据集和一个经典算法开始,把每一步都走通、吃透。过程中踩的每一个坑,解决的每一个问题,都会变成你宝贵的经验。当你下次面对一个全新的、杂乱的真实数据集时,这套从数据窥探、预处理、模型选择、训练评估到结果解读的完整思路,就是你最大的底气。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐