简介这份实验报告面向学习数据挖掘与机器学习的高校学生及初学者围绕k-means聚类分析展开帮助读者理解无监督学习的基本原理并完成MATLAB环境下的算法实现。报告系统梳理了算法流程包括初始类中心选取、数据点分配、类中心更新与收敛判断并以经典Iris数据集为对象对比k3与k4时的聚类效果分析初始中心、输入顺序及k值对结果的影响同时讨论算法优势与局限。资源包共1个doc文件约126KB内含实验目的、算法原理、流程图、结果分析、实验结论与心得体会附录还给出可直接运行的MATLAB源代码便于对照复现与二次修改。目前已有2741人学习适合作为课程实验参考或聚类入门实践材料。1. 随机撒三颗种子为什么最后总能长成三堆把一堆没有标签的点丢进坐标系随手挑三个点当中心然后反复做两件事把每个点归给最近的中心把每个中心挪到它名下所有点的平均位置。就这么简单几十行 MATLAB 就能写完可它偏偏能把鸢尾花数据分成三堆而且分得还挺像那么回事。k-means 聚类分析最反直觉的地方就在这里——它没有任何“正确答案”可参考却靠一个平方误差目标函数硬生生把数据压成了紧凑的簇。搞懂它为什么能收敛、收敛到哪里、又在什么情况下翻车比背下算法步骤有用得多。这份实验报告把原理、流程、MATLAB 源码和 k3 与 k4 的对比结果都摊开了正好拿来拆底层机制。2. 平方误差目标函数k-means 到底在最小化什么2.1 从“簇内紧凑”到 E 的数学定义聚类要解决的问题是给定 n 个对象每个对象是 d 维空间里的一个点把它分成 k 个簇使得簇内相似度高、簇间相似度低。k-means 把这个模糊的“相似度”翻译成了一个可计算的量——平方误差总和 E。实验报告里写得很清楚E 是数据集中所有对象的平方误差的总和p 是空间中的点是簇的平均值距离度量用欧几里得距离。用更直白的话说对每个簇算簇内每个点到该簇中心的距离平方全部加起来就是 E。k-means 的每一次迭代本质上都在做一件贪心的事在“分配”阶段固定中心、调整归属来降低 E在“更新”阶段固定归属、调整中心来降低 E。两个阶段交替进行E 单调不增而 E 有下界最小为 0所以算法必然在有限步内停住。这就是收敛性的底层保证——不是玄学是单调有界。提示E 单调不增只保证收敛到局部最优不保证全局最优。初始中心选得不同可能停在不同的局部极小值上这是 k-means 最根本的设计取舍。2.2 为什么用均值而不是中位数簇中心取“平均值”这是 k-means 名字的由来也是它和 k-medoids 的核心区别。均值有一个很好的性质在欧几里得距离下使簇内距离平方和最小的点恰好就是该簇的算术平均。换句话说更新阶段把中心挪到均值位置是对当前归属下 E 的精确最优解不是近似。如果换成中位数最小化的是绝对距离和目标函数就变了那是另一个算法。但均值也带来了脆弱性。一个远离簇的孤立点会把均值狠狠拽过去因为平方放大了大距离的权重。实验结论里专门提到“该算法对孤立点是敏感的如果数据集中存在有极大值的对象应该消除这种敏感性”说的就是这个机制层面的原因。常见的实现思路是在跑 k-means 之前先做异常值剔除或标准化否则一个离群点就能让整个簇的中心偏移。2.3 距离计算在代码里长什么样实验源码里计算距离的核心就一行dist sum((repmat(x(i,:),k,1)-nc).^2,2);这行代码把第 i 个数据点复制 k 份与 k 个中心逐元素相减平方后按行求和得到一个 k 维向量第 j 个分量就是该点到第 j 个中心的欧几里得距离平方。repmat的作用是广播避免写显式循环.^2是逐元素平方sum(...,2)沿第二维求和。随后[m,ind] min(dist)取最小值的位置就是该点归属的簇编号。这里有个容易忽略的细节代码比较的是距离平方而不是距离本身。因为平方根是单调函数比较平方和比较距离等价但省掉了开方运算。对 n 个点、k 个中心、迭代上百次的场景这个省法在数值上没损失在效率上有意义。这也是 k-means 对大数据集有很好伸缩性的一个小侧面。3. 两阶段迭代分配与更新为什么能交替收敛3.1 分配阶段的贪心本质分配阶段做的事很干脆固定当前 k 个中心对每个点独立地找最近中心。注意“独立”二字——每个点的归属只取决于它自己和所有中心不依赖其他点的归属。这意味着分配阶段可以完全并行化也是 k-means 能扛住大规模数据的结构原因。但这种独立性也埋了一个坑一个点改换门庭可能让原簇和新簇的均值都发生变化而分配阶段并不考虑这个连锁反应。它只管当下距离最近不管改完之后 E 会不会因为中心移动而反弹。所以单次分配不保证 E 降到全局最低只保证在当前中心下每个点都做了局部最优选择。3.2 更新阶段均值即最优中心更新阶段的代码是for i 1:k ind find(cidi); nc(i,:) mean(x(ind,:)); nr(i) length(ind); endfind(cidi)找出所有被分到第 i 簇的点的下标mean(x(ind,:))按列求平均得到新中心nr(i)记录该簇的点数。前面说过均值是平方误差下的最优中心所以这一步对固定归属是精确最优。两个阶段合起来就是坐标下降法的一个特例先优化归属、再优化中心交替下降直到 E 不再变化。实验报告里的流程图把这个循环画得很清楚选 k 个对象作为初始平均值聚类重新计算平均值判断新的平均值是否改变变了就回去继续不变就结束。判断条件“平均值发生改变”在代码里对应的是归属是否还在变动因为中心不变等价于归属不变在无并列距离的常规情况下。3.3 收敛判据与迭代上限源码里有两层循环第一层是固定maxgn100的主迭代while iter maxgn for i 1:n dist sum((repmat(x(i,:),k,1)-nc).^2,2); [m,ind] min(dist); cid(i) ind; end for i 1:k ind find(cidi); nc(i,:) mean(x(ind,:)); nr(i) length(ind); end iter iter 1; end这里用迭代次数上限代替了显式的收敛判断跑满 100 次就退出。严格来说这不是最优写法——如果 E 提前稳定继续迭代纯属浪费如果 100 次还没稳定强行截断可能得到未收敛的结果。常见的实现思路是在循环内比较新旧中心或新旧归属一旦无变化就break。实验代码选择固定次数更多是教学演示的简化实际工程里应该加收敛判据。第二层循环更有意思它做的是“再判断”maxiter 2; iter 1; move 1; while iter maxiter move ~ 0 move 0; for i 1:n dist sum((repmat(x(i,:),k,1)-nc).^2,2); r cid(i); dadj nr./(nr1).*dist; [m,ind] min(dadj); if ind ~ r cid(i) ind; ic find(cid ind); nc(ind,:) mean(x(ic,:)); move 1; end end iter iter1; end这段代码引入了一个调整后的距离dadj nr./(nr1).*dist用簇大小对距离做加权。nr./(nr1)这个因子随簇内点数增加而趋近于 1对小簇则明显小于 1效果是让点更倾向于留在或加入较小的簇缓解大簇越滚越大的马太效应。这是对标准 k-means 的一个经验性修补不是教科书里的标准步骤属于实现层面的调优。move标志记录本轮是否有任何点换了簇没有就退出。maxiter2限制了这层修补最多跑两轮防止在边界点上反复横跳。4. 初始中心与 k 值两个决定成败的输入4.1 为什么初始中心不能随便选实验报告在心得体会里写得很直白初始类中心的选择对聚类的准确度有较大影响最好选择两两距离较大、且能代表不同数据样本类别的点作为初始类中心。这句话背后是局部最优的机制——k-means 的收敛点由初始中心所在的“吸引域”决定初始中心落在哪个盆地最终就滑到哪个局部极小。源码里的初始化方式是bn round(n/k*rand); nc [x(bn,:); x(2*bn,:); x(3*bn,:);];n/k把数据均分成 k 段rand在第一段内随机取一个偏移然后取第 bn、2bn、3bn 个点作为三个初始中心。这个做法的意图是让初始中心在数据序列上均匀铺开避免三个中心挤在一起。实验报告解释为“前 k 个数据有较大差别可以提高程序的运行效率和分类结果的准确率”本质上是想用数据本身的排列顺序来近似“两两距离较大”。但这种依赖数据顺序的初始化很脆弱。如果数据没有按类别聚集排列bn、2bn、3bn 可能落在同一类里初始中心就退化了。工业界更常用 k-means第一个中心随机选后续每个中心按与已选中心距离平方成正比的概率选取从概率上保证初始中心分散。实验代码没上 k-means是教学简化但理解这个差距对实际使用很重要。4.2 k 值用户输入背后的领域判断k 由用户输入最终类别数就是用户给的数。实验报告在结论里强调“k-means 对类别数的选择 k 值有较高的要求如果类别数较少则不能区分数据”。k3 时鸢尾花数据能较好分成三类k4 时出现过度分割某些类别过于稀疏——这不是算法算错了是 k 本身超出了数据真实结构。k 值的选择没有算法内的自动机制必须靠对数据的先验了解。实验心得里说“需要使用该算法的人员对于数据分类有一定的了解并且可以根据观察部分原始抽样数据得出该样本数据的大致类别数目”这是实话。工程上常见的辅助手段是肘部法画 E 随 k 变化的曲线找拐点或轮廓系数但这些都是事后评估不是 k-means 自带的。4.3 数据输入顺序的影响边界实验报告有一句容易被误读的话“数据的输入顺序不同同样影响迭代次数而对聚类结果没有太大的影响。”这句话要拆开看。输入顺序影响的是初始化时 bn、2bn、3bn 取到哪些点从而影响初始中心进而影响收敛路径和迭代次数。但如果初始中心碰巧落在同一个吸引域最终结果确实可能一样。说“没有太大影响”是经验观察不是理论保证——换个顺序完全可能滑到另一个局部最优结果就变了。把这句话当成“顺序无所谓”来用迟早翻车。5. 避坑与排查k-means 跑出怪结果时先看这几处5.1 空簇导致中心变成 NaN现象某次迭代后某个簇一个点都没有mean对空矩阵求平均返回 NaN后续所有距离计算全变 NaN聚类彻底崩掉。原因初始中心选得太偏或者某个中心在迭代中被完全“抢空”。源码里nc(i,:) mean(x(ind,:))没有对ind为空做保护这是教学代码的常见疏漏。解决在更新阶段加判断若isempty(ind)要么保留原中心不动要么从数据中随机挑一个点重置该中心。工程实现里后者更常见能帮助算法跳出退化状态。5.2 迭代次数爆掉但 E 不降现象跑满 100 次上限E 曲线早就平了程序还在空转。原因源码用固定maxgn100代替收敛判据没有提前退出机制。数据量大时这是实打实的时间浪费。解决在每轮更新后比较新旧中心矩阵若norm(nc_new - nc_old) toltol 取 1e-6 量级就break。或者比较归属向量是否还有变化逻辑更直接。5.3 k4 时出现“过度分割”就以为算法坏了现象鸢尾花明明三类设 k4 后多出一个稀疏簇就怀疑代码有 bug。原因k-means 会忠实地把数据分成你要求的 k 簇哪怕真实结构只有三类。多出来的簇是从某个大类里硬切出来的自然稀疏。这是 k 值设定问题不是实现问题。解决先用肘部法或领域知识确认 k再跑聚类。看到稀疏簇先回头查 k别急着改代码。5.4 孤立点把整个簇拽偏现象某个簇的中心明显偏离该簇主体簇内点分布被拉成一条长条。原因平方误差对大距离敏感一个远离主体的点对均值的拉扯远超普通点。实验结论里明确点出了这个敏感性。解决聚类前做异常值检测如 3σ 或 IQR 规则并剔除或改用对离群点更鲁棒的 k-medoids。若业务上不能删点至少对特征做标准化压缩极端值的量纲影响。5.5 不同次运行结果不一致就以为随机性失控现象同一份数据跑两次簇的编号和边界略有不同。原因初始化用了rand每次 bn 不同初始中心不同收敛到的局部最优可能不同。簇编号本身没有语义编号变了不代表聚类质量变了。解决固定随机种子rng(42)保证可复现评估质量看 E 或轮廓系数不看簇编号。若多次运行 E 波动大说明数据本身簇结构不明显该考虑换算法了。6. 从教学代码到工程实现k-means 的设计取舍在哪里这份实验报告的价值不在于它给出了一个多完美的 k-means 实现而在于它把算法的骨架、初始化的土办法、收敛判据的简化、以及 k 值和初始中心这两个“人肉输入”的权重全都摊在了明面上。拿它和工业级实现对比能看清几个关键取舍。第一个取舍是初始化。教学代码用n/k分段加随机偏移赌的是数据顺序有结构k-means 用概率分布保证初始中心分散代价是多一轮距离计算。前者快但脆后者稳但多花时间。数据量小的时候差别不大数据量大且簇结构复杂时k-means 能显著降低陷入坏局部最优的概率。第二个取舍是收敛判据。固定迭代上限简单、可预测耗时但要么浪费要么截断动态判据省时间但需要选阈值阈值太松收敛不彻底太紧则空转。工程上通常两者结合设一个宽松的上限兜底同时用中心变化量做提前退出。第三个取舍是目标函数本身。平方误差配均值数学上干净、实现上简单、对凸形且密度相近的簇效果好这是实验结论里承认的优势。但它对非凸簇、密度差异大的簇、含离群点的数据都不友好。这时候不是调参能救的得换 DBSCAN 或层次聚类。k-means 的定位应该是“快速拿到一个还行的划分”而不是“任何数据都能分对”。第四个取舍是那个nr./(nr1)加权修补。它不属于标准 k-means是源码作者加的经验项用来抑制大簇膨胀。这类修补在工程里很常见但每加一个就离“标准算法”远一步可解释性和可复现性都会打折。用之前得想清楚是要一个数学上干净的基线还是要一个针对当前数据调过的实用版。我自己的习惯是每次拿到一份聚类代码先不看它怎么迭代先看它怎么初始化、怎么判收敛、怎么处理空簇和离群点——这三处最能暴露实现者的取舍也最容易在换数据时翻车。从那以后我每次跑 k-means 都强制先固定种子、再画一次 E 随迭代的曲线确认它真的在降而不是在抖。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?