K-means 聚类算法入门:基于 Iris 数据集的 C 语言实现
K-means 聚类算法入门:基于 Iris 数据集的 C 语言实现

K-means 聚类算法入门:基于 Iris 数据集的 C 语言实现

K-means 是机器学习里最经典的无监督学习算法之一。它不依赖人工标签,而是根据样本之间的距离关系,自动把数据划分成若干个簇。

这篇文章直接结合两个实际文件来讲:

  • Iris.csv:经典的鸢尾花数据集
  • Iris_sort_K_mean.c:一个可以直接编译运行的 C 语言 K-means 实现

这次的重点不只是“让程序跑出来”,而是把下面几件事讲清楚:

  1. K-means 为什么这样设计
  2. 每个函数在代码里具体负责什么
  3. 为什么标准化、初始化和多次重启会直接影响结果
  4. 如何把最终聚类结果解释成可读的结论

一、什么是 K-means

K-means 的目标是:

给定一个数据集和簇数 K,把所有样本分成 K 类,让同一个簇里的样本尽量接近。

它会不断重复两件事:

  1. 把每个样本分配到最近的聚类中心
  2. 根据新的分组结果,重新计算聚类中心

如果你把它想成一个动态修正的过程,其实会更容易理解:

先猜几个中心,再让样本去找最近中心;样本分完以后,中心再根据样本的平均位置重新移动。

二、为什么 Iris 数据集适合拿来学聚类

Iris.csv 一共 150 条样本。每条样本包含 4 个数值特征:

  • 花萼长度 SepalLengthCm
  • 花萼宽度 SepalWidthCm
  • 花瓣长度 PetalLengthCm
  • 花瓣宽度 PetalWidthCm

同时它还带有真实标签:

  • Iris-setosa
  • Iris-versicolor
  • Iris-virginica

这里要强调一个关键点:K-means 聚类时不会使用这些标签。它只看数值特征和样本之间的距离。标签保留下来只是为了在聚类完成后做结果解释。

也就是说,这个例子特别适合学习两件事:

  • 算法怎么在没有标签的前提下分组
  • 算法分出来的簇和真实分类之间差距有多大

三、这份 C 程序整体做了什么

Iris_sort_K_mean.c 不是只写了“一个 for 循环 + 一个距离函数”的极简版本,而是把 K-means 的完整执行路径拆成了多个可读函数。整体流程可以概括成:

  1. 读取 CSV,构造样本数组
  2. 对所有特征做标准化
  3. 用 K-means++ 初始化聚类中心
  4. 反复执行“样本分配 / 中心更新”
  5. 多次随机重启,选择 SSE 最小的一轮
  6. 输出簇分布、标签分布和样本所属簇
Iris K-means 算法流程图
这张流程图对应的就是 Iris_sort_K_mean.c 的执行顺序:先加载数据,再标准化、初始化、迭代、计算 SSE,最后保留最好的聚类结果。
这张图最值得看的点: 它不是“跑一次 K-means 就结束”,而是把“单次聚类”和“多次重启选最优”拆开了。对于带随机初始化的算法,这个结构很重要。

四、样本在程序里是怎么表示的

程序里每条样本都存到一个 Sample 结构体里:

typedef struct {
    int id;
    double x[FEATURES];
    double x_scaled[FEATURES];
    char species[32];
    int cluster;
} Sample;

这里每个字段都承担了明确职责:

  • id:样本编号,对应 CSV 第一列
  • x:原始 4 维特征
  • x_scaled:标准化后的 4 维特征
  • species:真实物种标签
  • cluster:程序最终给出的簇编号

这种设计的好处是:原始数据、预处理数据、真实标签和聚类结果都分开存放,后面读代码时不会把“用于计算的值”和“用于展示的值”混在一起。

五、第一步:读取 CSV 数据

程序通过 load_iris() 逐行读取 Iris.csv。它会先跳过表头,再用 sscanf() 把每一行拆成编号、4 个数值和物种字符串。

你可以把这个函数理解成一个“文本转结构体”的过程:

  1. 文件里原本是一行逗号分隔文本
  2. 读进来以后变成一个 Sample
  3. 所有样本最终放到数组 data[]

程序入口支持命令行传入数据文件路径:

const char *filename = (argc > 1) ? argv[1] : "Iris.csv";

这意味着你既可以直接运行:

./iris_kmeans Iris.csv

也可以在以后换别的 CSV,而不用改源码。

六、第二步:为什么一定要做标准化

K-means 的核心是比较距离。如果不同特征的数值尺度差异很大,那么数值更大的特征会在欧氏距离里占更大权重。

举个直观例子:如果某个维度经常变化 5 个单位,而另一个维度通常只变化 0.2 个单位,那么前者会在距离计算里显得“更重要”。这并不一定符合我们的真实意图。

所以程序在聚类前先执行了 standardize_features(),用的是标准分数:

x_scaled = (x - mean) / std

这个函数内部其实做了三件事:

  1. 先求每一列特征的均值 mean
  2. 再求每一列特征的标准差 std
  3. 最后把每条样本的原始值转换成标准化值

标准化完成后,各特征大致都被拉回到“均值为 0、波动尺度接近 1”的区间。这样在计算距离时,四个特征的影响会更均衡。

初学者最容易忽略的点: 很多时候不是算法“没效果”,而是数据预处理没做好。对 K-means 这种基于距离的算法来说,标准化几乎是基本操作。

七、第三步:为什么初始化不能随便乱选

如果 K-means 一开始只是随机选 3 个样本当中心,算法当然也能跑,但结果通常不稳定。原因是:

  • 初始中心可能靠得太近
  • 某些区域一开始就没有中心覆盖
  • 算法容易较早收敛到局部较差解

为了减轻这个问题,程序在 init_centroids() 中使用了 K-means++ 思想:

  1. 先随机选一个中心
  2. 后面的中心优先从“离当前已有中心更远”的样本中产生

这样一来,初始中心往往更分散,也更接近“每个簇先占一个位置”的直觉。

从代码层面看,这一步的核心并不难:程序先计算每个样本到现有中心的最小距离,再按距离大小加权抽样。越远的点,被选中的概率就越高。

八、第四步:样本是怎么被分到各个簇里的

assign_clusters() 做的是 K-means 最核心的一件事:对每个样本,计算它到所有中心的距离,然后把它分配给最近的那个中心。

这里调用的距离函数是:

double distance_sq(const double a[], const double b[])

注意,这里计算的是“欧氏距离的平方”,不是开方之后的真实距离。这样做完全合理,因为:

  • 谁更近,本质上比较平方值就够了
  • 不开平方可以少做一些无意义的计算

这个函数的返回值还有一个关键作用:它会告诉主循环“本轮是否有样本换簇”。如果有,就说明还没收敛;如果没有,就说明当前结果已经稳定。

九、第五步:为什么还要重新计算中心

样本被重新分组之后,原来的中心位置通常已经不再合理。因为新的簇成员已经确定,中心就应该移动到这些成员的平均位置。

update_centroids() 做的就是这件事:

  1. 统计每个簇有哪些样本
  2. 把每个簇内所有样本的 4 个特征分别求和
  3. 再除以该簇样本数,得到新的中心坐标

这就是名字里 “means” 的含义:每个簇由它内部样本的均值中心来代表。

如果某个簇暂时没有样本,代码会跳过这次更新,避免除零问题。这属于实际程序里很常见的边界情况保护。

十、第六步:算法什么时候停止

程序设置了两层停止条件:

  1. 如果本轮没有样本更换簇,说明已经收敛,可以停止
  2. 如果迭代次数达到 MAX_ITER,也必须停止,防止极端情况下死循环
#define MAX_ITER 1000

对 Iris 这种小数据集来说,1000 已经远远超过实际需要。正常情况下很快就会在几十轮之内收敛。

十一、为什么要重复跑 2000 次

即使使用了 K-means++,结果依然会受到随机初始化影响。所以程序没有只跑一次,而是定义了:

#define RESTARTS 2000

也就是说,程序会独立跑 2000 次 K-means。每一轮结束后都计算一次 SSE,然后保留最好的结果。

SSE 的含义是:

所有样本到各自所属中心的平方距离之和。

SSE 越小,通常表示簇内越紧凑。因此“多次重启 + 选最优 SSE”是一种非常常见的工程做法。

真正要建立的直觉: K-means 不是一个“给我一次机会就能永远正确”的算法。它本来就带随机性,所以多次尝试是正常策略,不是多余操作。

十二、程序最终会输出什么

Iris_sort_K_mean.c 运行结束后,主要输出三类信息:

  1. 最佳结果的迭代次数和 SSE
  2. 标准化空间中的聚类中心
  3. 每个簇的样本数量与真实标签分布

一组典型结果如下:

K-means 重启次数: 2000
最佳结果迭代次数: 6
最佳 SSE: 140.965817

Cluster 0: total=53, setosa=0, versicolor=39, virginica=14
Cluster 1: total=50, setosa=50, versicolor=0, virginica=0
Cluster 2: total=47, setosa=0, versicolor=11, virginica=36

这组结果说明:

  • setosa 几乎可以被单独分出来
  • versicolorvirginica 之间有一定重叠

这正是 Iris 数据集最经典的现象:setosa 的可分性很强,而后两类在特征空间里更容易交叠。

十三、把结果画出来会更直观

单看数字还不够直观。为了更容易理解聚类边界,这里把样本投影到二维平面,用 PetalLengthCm 作为横轴、PetalWidthCm 作为纵轴,并按照 K-means 的簇编号着色。

Iris K-means 聚类二维可视化
散点图里的每个点是一条 Iris 样本,颜色表示它被分到的簇,虚线圆加叉号表示该簇在二维投影下的中心位置。

这张图能帮你很快看出三件事:

  1. 一团明显分离的小簇对应的就是 setosa
  2. 另外两团虽然大体能分开,但边界有重叠
  3. K-means 更擅长处理“球状且分离度较高”的簇,对重叠区域没有神奇修正能力

也就是说,K-means 学到的是“距离结构”,不是“物种定义本身”。这也是为什么聚类和真实分类结果不一定完全一致。

十四、最值得动手改的几个参数

如果你下载这份程序,最值得自己动手改的参数有三个:

  • K:簇数,当前设为 3
  • RESTARTS:重启次数,越大越稳定但耗时更高
  • MAX_ITER:单次运行的最大迭代次数

你可以试几组实验:

  1. K 改成 2 或 4,看聚类结构怎么变化
  2. RESTARTS 改小,观察 SSE 是否更容易波动
  3. 去掉标准化,看看结果是否明显恶化

这类实验比背定义更有用,因为你会真正看到“算法参数和数据预处理是怎么改变结果的”。

十五、下载区现在提供什么

现在下载区里已经整理成一组完整的学习资料:

  • Iris.csv:原始数据集
  • Iris_sort_K_mean.c:整理后的 C 语言实现版本
  • iris-kmeans-flowchart.svg:流程图
  • iris-kmeans-cluster-visual.svg:聚类可视化图
  • iris-kmeans-materials.zip:打包下载压缩包

十六、如何编译和运行

在 macOS 或 Linux 下,可以直接这样编译:

gcc Iris_sort_K_mean.c -lm -o iris_kmeans
./iris_kmeans Iris.csv

如果数据文件和程序就在同一目录,也可以直接运行:

./iris_kmeans

程序会默认尝试读取当前目录里的 Iris.csv

十七、这道例子真正值得学到什么

如果把这篇文章当作算法学习材料,那么真正值得带走的不是某一行 C 代码,而是下面这些思路:

  1. K-means 的本质是“最近中心分配 + 均值中心更新”
  2. 距离型算法高度依赖数据预处理,标准化非常关键
  3. K-means++ 会显著改善初始化质量
  4. 带随机性的聚类算法往往要多次重启再选最优
  5. 最终结果必须结合输出分布和可视化一起解释

十八、总结

结合 Iris.csvIris_sort_K_mean.c,我们可以把 K-means 的完整流程看得非常清楚:

  • 先读取数据
  • 再做标准化
  • 用 K-means++ 初始化中心
  • 反复执行“分配样本 / 更新中心”直到收敛
  • 通过多次重启选出 SSE 最小的结果

对初学者来说,这个例子最大的价值在于:它既保留了算法的核心逻辑,又足够接近真实程序。你不只能理解“概念”,还能看到“概念是怎么变成代码和结果的”。

如果你刚开始学无监督学习,我很建议你亲手编译一次、改几个参数、再对照这张流程图和散点图一起看。这样比只读一遍算法定义更容易建立真正的直觉。

搜索问题

常见问题

这篇文章适合谁读?

这篇文章适合想用 进阶 难度理解“K-means 聚类算法入门:基于 Iris 数据集的 C 语言实现”的读者,预计阅读时间约 14 分钟,重点覆盖 C, K-means, Iris Dataset。

读完后下一步应该看什么?

可以从文末相关阅读、项目页和知识图谱继续进入相邻主题。

这篇文章有没有可运行代码或配套资源?

有。页面里的运行说明、资源卡片和下载入口会指向复现实验所需的命令、数据、代码或说明文件。

能不能在浏览器实验台里操作?

可以。读完文章后可以打开算法实验台或知识图谱,观察 8 皇后、K-means 或手写数字演示的状态变化。

文章上下文

算法实现项目

围绕回溯、位运算和聚类实现,保留可以复查的代码、流程图和下载资料。

难度: 进阶 阅读时间: 14 分钟
  • C
  • K-means
  • Iris Dataset
对应语言版本 整理中
可分享摘要 K-means 聚类算法入门:基于 Iris 数据集的 C 语言实现

结合 Iris.csv、C 语言源码、流程图和可视化,完整讲解 K-means++ 初始化、迭代收敛与结果分析。

下载分享图 打开分享中心

配套资源

发表回复

项目时间线

已发布文章

  1. 回溯算法入门:用 C 和 Python 解决 8 皇后问题 用 C 和 Python 讲清楚 8 皇后回溯搜索的状态表示、冲突判断、递归过程与完整求解思路。
  2. 回溯算法进阶:用位运算优化 8 皇后(C / Python) 介绍如何用位运算优化 8 皇后搜索,降低状态判断成本,并给出 C / Python 对照实现。
  3. K-means 聚类算法入门:基于 Iris 数据集的 C 语言实现 结合 Iris.csv、C 语言源码、流程图和可视化,完整讲解 K-means++ 初始化、迭代收敛与结果分析。

已公开资源

  1. Iris.csv 数据集 K-means 文章使用的 150 条 Iris 样本。
  2. Iris_sort_K_mean.c 源码 包含标准化、K-means++ 初始化、多次重启和 SSE 选择。
  3. K-means 流程图 对应 C 程序执行顺序的 SVG 流程图。
  4. 聚类可视化图 基于花瓣长度和花瓣宽度投影的二维散点图。
  5. K-means 打包下载 包含数据、源码、流程图和可视化图。
  6. 算法可视化专题分享图 用于分享 8 皇后、回溯、位运算和实验台专题页的 1200x630 SVG 图。
  7. K-means 一轮迭代动画 Remotion 生成的短动画,展示样本分配、质心更新和 SSE 下降。
  8. 8 皇后回溯搜索动画 Remotion 生成的短动画,展示逐行尝试、冲突剪枝和回溯。

当前学习路线

  1. 用 C 和 Python 解决 8 皇后问题 学习路线节点
  2. 用位运算优化 8 皇后 学习路线节点
  3. 基于 Iris 数据集的 K-means C 语言实现 学习路线节点
  4. K-means 代码、数据和图示下载 学习路线节点

下一步计划

  1. 补充更多算法题的可运行实现
  2. 为下载资源增加更多示例输入