K-means 是机器学习里最经典的无监督学习算法之一。它不依赖人工标签,而是根据样本之间的距离关系,自动把数据划分成若干个簇。
这篇文章直接结合两个实际文件来讲:
Iris.csv:经典的鸢尾花数据集Iris_sort_K_mean.c:一个可以直接编译运行的 C 语言 K-means 实现
这次的重点不只是“让程序跑出来”,而是把下面几件事讲清楚:
- K-means 为什么这样设计
- 每个函数在代码里具体负责什么
- 为什么标准化、初始化和多次重启会直接影响结果
- 如何把最终聚类结果解释成可读的结论
一、什么是 K-means
K-means 的目标是:
给定一个数据集和簇数
K,把所有样本分成K类,让同一个簇里的样本尽量接近。
它会不断重复两件事:
- 把每个样本分配到最近的聚类中心
- 根据新的分组结果,重新计算聚类中心
如果你把它想成一个动态修正的过程,其实会更容易理解:
先猜几个中心,再让样本去找最近中心;样本分完以后,中心再根据样本的平均位置重新移动。
二、为什么 Iris 数据集适合拿来学聚类
Iris.csv 一共 150 条样本。每条样本包含 4 个数值特征:
- 花萼长度
SepalLengthCm - 花萼宽度
SepalWidthCm - 花瓣长度
PetalLengthCm - 花瓣宽度
PetalWidthCm
同时它还带有真实标签:
Iris-setosaIris-versicolorIris-virginica
这里要强调一个关键点:K-means 聚类时不会使用这些标签。它只看数值特征和样本之间的距离。标签保留下来只是为了在聚类完成后做结果解释。
也就是说,这个例子特别适合学习两件事:
- 算法怎么在没有标签的前提下分组
- 算法分出来的簇和真实分类之间差距有多大
三、这份 C 程序整体做了什么
Iris_sort_K_mean.c 不是只写了“一个 for 循环 + 一个距离函数”的极简版本,而是把 K-means 的完整执行路径拆成了多个可读函数。整体流程可以概括成:
- 读取 CSV,构造样本数组
- 对所有特征做标准化
- 用 K-means++ 初始化聚类中心
- 反复执行“样本分配 / 中心更新”
- 多次随机重启,选择 SSE 最小的一轮
- 输出簇分布、标签分布和样本所属簇
Iris_sort_K_mean.c 的执行顺序:先加载数据,再标准化、初始化、迭代、计算 SSE,最后保留最好的聚类结果。四、样本在程序里是怎么表示的
程序里每条样本都存到一个 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 个数值和物种字符串。
你可以把这个函数理解成一个“文本转结构体”的过程:
- 文件里原本是一行逗号分隔文本
- 读进来以后变成一个
Sample - 所有样本最终放到数组
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
这个函数内部其实做了三件事:
- 先求每一列特征的均值
mean - 再求每一列特征的标准差
std - 最后把每条样本的原始值转换成标准化值
标准化完成后,各特征大致都被拉回到“均值为 0、波动尺度接近 1”的区间。这样在计算距离时,四个特征的影响会更均衡。
七、第三步:为什么初始化不能随便乱选
如果 K-means 一开始只是随机选 3 个样本当中心,算法当然也能跑,但结果通常不稳定。原因是:
- 初始中心可能靠得太近
- 某些区域一开始就没有中心覆盖
- 算法容易较早收敛到局部较差解
为了减轻这个问题,程序在 init_centroids() 中使用了 K-means++ 思想:
- 先随机选一个中心
- 后面的中心优先从“离当前已有中心更远”的样本中产生
这样一来,初始中心往往更分散,也更接近“每个簇先占一个位置”的直觉。
从代码层面看,这一步的核心并不难:程序先计算每个样本到现有中心的最小距离,再按距离大小加权抽样。越远的点,被选中的概率就越高。
八、第四步:样本是怎么被分到各个簇里的
assign_clusters() 做的是 K-means 最核心的一件事:对每个样本,计算它到所有中心的距离,然后把它分配给最近的那个中心。
这里调用的距离函数是:
double distance_sq(const double a[], const double b[])
注意,这里计算的是“欧氏距离的平方”,不是开方之后的真实距离。这样做完全合理,因为:
- 谁更近,本质上比较平方值就够了
- 不开平方可以少做一些无意义的计算
这个函数的返回值还有一个关键作用:它会告诉主循环“本轮是否有样本换簇”。如果有,就说明还没收敛;如果没有,就说明当前结果已经稳定。
九、第五步:为什么还要重新计算中心
样本被重新分组之后,原来的中心位置通常已经不再合理。因为新的簇成员已经确定,中心就应该移动到这些成员的平均位置。
update_centroids() 做的就是这件事:
- 统计每个簇有哪些样本
- 把每个簇内所有样本的 4 个特征分别求和
- 再除以该簇样本数,得到新的中心坐标
这就是名字里 “means” 的含义:每个簇由它内部样本的均值中心来代表。
如果某个簇暂时没有样本,代码会跳过这次更新,避免除零问题。这属于实际程序里很常见的边界情况保护。
十、第六步:算法什么时候停止
程序设置了两层停止条件:
- 如果本轮没有样本更换簇,说明已经收敛,可以停止
- 如果迭代次数达到
MAX_ITER,也必须停止,防止极端情况下死循环
#define MAX_ITER 1000
对 Iris 这种小数据集来说,1000 已经远远超过实际需要。正常情况下很快就会在几十轮之内收敛。
十一、为什么要重复跑 2000 次
即使使用了 K-means++,结果依然会受到随机初始化影响。所以程序没有只跑一次,而是定义了:
#define RESTARTS 2000
也就是说,程序会独立跑 2000 次 K-means。每一轮结束后都计算一次 SSE,然后保留最好的结果。
SSE 的含义是:
所有样本到各自所属中心的平方距离之和。
SSE 越小,通常表示簇内越紧凑。因此“多次重启 + 选最优 SSE”是一种非常常见的工程做法。
十二、程序最终会输出什么
Iris_sort_K_mean.c 运行结束后,主要输出三类信息:
- 最佳结果的迭代次数和 SSE
- 标准化空间中的聚类中心
- 每个簇的样本数量与真实标签分布
一组典型结果如下:
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几乎可以被单独分出来versicolor和virginica之间有一定重叠
这正是 Iris 数据集最经典的现象:setosa 的可分性很强,而后两类在特征空间里更容易交叠。
十三、把结果画出来会更直观
单看数字还不够直观。为了更容易理解聚类边界,这里把样本投影到二维平面,用 PetalLengthCm 作为横轴、PetalWidthCm 作为纵轴,并按照 K-means 的簇编号着色。
这张图能帮你很快看出三件事:
- 一团明显分离的小簇对应的就是
setosa - 另外两团虽然大体能分开,但边界有重叠
- K-means 更擅长处理“球状且分离度较高”的簇,对重叠区域没有神奇修正能力
也就是说,K-means 学到的是“距离结构”,不是“物种定义本身”。这也是为什么聚类和真实分类结果不一定完全一致。
十四、最值得动手改的几个参数
如果你下载这份程序,最值得自己动手改的参数有三个:
K:簇数,当前设为 3RESTARTS:重启次数,越大越稳定但耗时更高MAX_ITER:单次运行的最大迭代次数
你可以试几组实验:
- 把
K改成 2 或 4,看聚类结构怎么变化 - 把
RESTARTS改小,观察 SSE 是否更容易波动 - 去掉标准化,看看结果是否明显恶化
这类实验比背定义更有用,因为你会真正看到“算法参数和数据预处理是怎么改变结果的”。
十五、下载区现在提供什么
现在下载区里已经整理成一组完整的学习资料:
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 代码,而是下面这些思路:
- K-means 的本质是“最近中心分配 + 均值中心更新”
- 距离型算法高度依赖数据预处理,标准化非常关键
- K-means++ 会显著改善初始化质量
- 带随机性的聚类算法往往要多次重启再选最优
- 最终结果必须结合输出分布和可视化一起解释
十八、总结
结合 Iris.csv 和 Iris_sort_K_mean.c,我们可以把 K-means 的完整流程看得非常清楚:
- 先读取数据
- 再做标准化
- 用 K-means++ 初始化中心
- 反复执行“分配样本 / 更新中心”直到收敛
- 通过多次重启选出 SSE 最小的结果
对初学者来说,这个例子最大的价值在于:它既保留了算法的核心逻辑,又足够接近真实程序。你不只能理解“概念”,还能看到“概念是怎么变成代码和结果的”。
如果你刚开始学无监督学习,我很建议你亲手编译一次、改几个参数、再对照这张流程图和散点图一起看。这样比只读一遍算法定义更容易建立真正的直觉。
搜索问题
常见问题
这篇文章适合谁读?
这篇文章适合想用 进阶 难度理解“K-means 聚类算法入门:基于 Iris 数据集的 C 语言实现”的读者,预计阅读时间约 14 分钟,重点覆盖 C, K-means, Iris Dataset。
读完后下一步应该看什么?
可以从文末相关阅读、项目页和知识图谱继续进入相邻主题。
这篇文章有没有可运行代码或配套资源?
有。页面里的运行说明、资源卡片和下载入口会指向复现实验所需的命令、数据、代码或说明文件。
能不能在浏览器实验台里操作?
可以。读完文章后可以打开算法实验台或知识图谱,观察 8 皇后、K-means 或手写数字演示的状态变化。
文章上下文
算法实现项目
围绕回溯、位运算和聚类实现,保留可以复查的代码、流程图和下载资料。
继续下一步
继续:K-means 代码、数据和图示下载配套资源
算法实现项目 / DATASET
Iris.csv 数据集
K-means 文章使用的 150 条 Iris 样本。
算法实现项目 / CODE
Iris_sort_K_mean.c 源码
包含标准化、K-means++ 初始化、多次重启和 SSE 选择。
算法实现项目 / DIAGRAM
K-means 流程图
对应 C 程序执行顺序的 SVG 流程图。
算法实现项目 / DIAGRAM
聚类可视化图
基于花瓣长度和花瓣宽度投影的二维散点图。
算法实现项目 / ARCHIVE
K-means 打包下载
包含数据、源码、流程图和可视化图。
项目时间线
已发布文章
- 回溯算法入门:用 C 和 Python 解决 8 皇后问题 用 C 和 Python 讲清楚 8 皇后回溯搜索的状态表示、冲突判断、递归过程与完整求解思路。
- 回溯算法进阶:用位运算优化 8 皇后(C / Python) 介绍如何用位运算优化 8 皇后搜索,降低状态判断成本,并给出 C / Python 对照实现。
- K-means 聚类算法入门:基于 Iris 数据集的 C 语言实现 结合 Iris.csv、C 语言源码、流程图和可视化,完整讲解 K-means++ 初始化、迭代收敛与结果分析。
已公开资源
- Iris.csv 数据集 K-means 文章使用的 150 条 Iris 样本。
- Iris_sort_K_mean.c 源码 包含标准化、K-means++ 初始化、多次重启和 SSE 选择。
- K-means 流程图 对应 C 程序执行顺序的 SVG 流程图。
- 聚类可视化图 基于花瓣长度和花瓣宽度投影的二维散点图。
- K-means 打包下载 包含数据、源码、流程图和可视化图。
- 算法可视化专题分享图 用于分享 8 皇后、回溯、位运算和实验台专题页的 1200x630 SVG 图。
- K-means 一轮迭代动画 Remotion 生成的短动画,展示样本分配、质心更新和 SSE 下降。
- 8 皇后回溯搜索动画 Remotion 生成的短动画,展示逐行尝试、冲突剪枝和回溯。
当前学习路线
- 用 C 和 Python 解决 8 皇后问题 学习路线节点
- 用位运算优化 8 皇后 学习路线节点
- 基于 Iris 数据集的 K-means C 语言实现 学习路线节点
- K-means 代码、数据和图示下载 学习路线节点
下一步计划
- 补充更多算法题的可运行实现
- 为下载资源增加更多示例输入
