☰
MATLAB K-means聚类实战:代码解析、参数调优与避坑
2026/11/4 10:29:58 网站建设 项目流程

简介:这份文档面向希望用 MATLAB 快速掌握 K-means 聚类算法实现的学习者与工程实践者,围绕聚类分析基本原理、函数调用和结果可视化展开,适合课程实验、数据分析入门及算法复现场景。资源包仅含 1 个 docx 文件,大小约 15KB,篇幅集中,便于直接查阅与参考其中的 MATLAB 示例代码。文档以二维数据点为例,演示如何设置聚类簇数 K、调用 kmeans 函数获取簇索引与质心,并用 scatter 绘制分色散点和质心标记;同时梳理初始化、样本分配、中心更新与迭代终止等关键步骤,并讨论 K 值选择、异常值敏感、初始化敏感等局限及 K-means++、Bisecting K-means 等改进方向。已有 444 人学习,可作为快速理解算法流程和搭建实验代码的参考。

1. 七个二维点起步:这份 matlab kmeans 聚类算法代码解决什么问题

手头有一张二维散点表,想快速知道这些点能分成几堆、每堆的中心落在哪,用 matlab 写一段 kmeans 聚类算法是最省事的路子之一。这份代码把数据集定义、簇数设定、kmeans 聚类调用和结果绘图压进十几行脚本里,没有多余封装,改数据、改 K 值、再跑一遍就能看到新结果。它适合三类人:课程作业要交聚类结果的、做数据探查想先看一眼分布形态的、以及准备把聚类嵌进更大流程、需要一个最小可运行参照的。它不负责解决 K 怎么选、特征怎么标准化这些工程问题,但把「从原始坐标到质心输出」这条最短路径铺平了。

2. 代码骨架拆解:从 data 到 centroids 的一条完整链路

整段代码只有三段:喂数据、调函数、画图。真正值得逐行琢磨的不是那几行命令本身,而是三段之间的数据形状约定——data 的行代表什么、idx 出来的向量有多长、centroids 为什么是 K 行 p 列。形状对上了,后面接标准化、接选 K、接导出 Excel 都不会卡;形状对不上,代码不报错也能给你一个看起来像样但完全错的结果,这种静默失败最难查。

2.1 数据矩阵和控制参数:那 7 个点和 K=2 是怎么摆的

先看数据的组织方式。MATLAB 里的 kmeans 有个硬约定:行是样本,列是特征。这 7 个点全是二维坐标,所以矩阵是 7 行 2 列,第一列横坐标、第二列纵坐标,顺序不能反,反了聚出来的还是 7 个点,但语义全变。

% 每行一个样本,每列一个特征:第 1 列横坐标,第 2 列纵坐标 data = [1 1; 1.5 2; 3 4; 5 7; 3.5 5; 4.5 5; 3.5 4.5]; K = 2; % 期望簇数,必须 <= 样本数 % 落地第一件事永远是体检,不是直接跑算法 [nSamples, nFeatures] = size(data); fprintf('样本数 %d,特征维度 %d,簇数 %d\n', nSamples, nFeatures, K); % 顺手排除会让 kmeans 翻车的脏数据 assert(all(isfinite(data(:))), 'data 里存在 NaN 或 Inf'); assert(K <= nSamples, '簇数不能超过样本数');

这段体检代码看着啰嗦,实际省的是后面排查的时间。isfinite一次覆盖 NaN 和 Inf,kmeans 碰上这两种值的行为在不同版本里不一致,与其赌,不如在入口拦掉。K <= nSamples这个断言在示例里恒成立,但你把 data 换成真实数据、K 从配置文件读进来的时候,它就会救你一次。

再看这 7 个点的分布:(1,1)和(1.5,2)聚在左下角,(3,4)、(3.5,4.5)、(3.5,5)、(4.5,5)、(5,7)沿右上方向排开。肉眼能看出两组,所以 K=2 是合理的。多数随机初始化下会收敛到(1.25, 1.5)和(3.9, 5.1)附近这两个质心——这也是示例数据集讨巧的地方:簇分离度够高,随便跑都能出漂亮结果。换成真实业务数据,两组之间的边界往往是模糊的,图上看不出明显间隙,那时候就得靠后面的定量指标,不能只信视觉。

替换数据集的时候有个习惯值得照抄:把data和K单独拎到脚本最顶上,中间不要插其他逻辑。二次开发时最怕的是数据藏在函数里,改一次要翻两百行。

2.2 内置 kmeans 的输入输出契约:idx、centroids、sumd、D 各是什么形状

核心就一行kmeans(data, K),但它能吐出四个输出,示例只用了前两个。把四个全接住,等于免费拿到一份聚类质量报告。

% 最简调用:只要划分结果和质心 [idx, centroids] = kmeans(data, K); % 完整调用:额外拿到簇内距离和、样本到各质心的距离矩阵 [idx, centroids, sumd, D] = kmeans(data, K, ... 'Distance', 'sqeuclidean', ... % 默认距离度量,欧氏距离平方 'Replicates', 5, ... % 重启 5 次取簇内和最小的一次 'MaxIter', 200, ... % 单次迭代上限,默认 100 'Display', 'final'); % 只在收敛后打印一行摘要 % 四个输出的形状必须记牢,后面所有下游代码都依赖它 fprintf('idx : %s (每个样本一个簇号)\n', mat2str(size(idx))); fprintf('centroids : %s (K 行 p 列)\n', mat2str(size(centroids))); fprintf('sumd : %s (每个簇的簇内距离和)\n', mat2str(size(sumd))); fprintf('D : %s (样本 × 质心)\n', mat2str(size(D)));

idx是 n×1 的 double 列向量,取值 1 到 K,注意它不是 categorical,也不是 logical,直接拿去当索引或者做算术都行,但别指望它有标签名。centroids是 K×p,行顺序和簇号 1..K 严格对应,第 3 行就是第 3 簇的质心,这一点在你要给簇贴业务含义时非常关键。sumd是 1×K 的行向量,第 k 个元素是第 k 簇所有点到该簇质心的距离之和——注意在默认的sqeuclidean下它是平方和,不是开根号后的和,跨数据集比较时别拿它当「平均距离」用。D是 n×K,第 i 行第 j 列是样本 i 到质心 j 的距离,靠它能算出每个样本的「归属置信度」:某一行最小值和次小值差得越大,这个样本分得越干脆;差得很小,说明它卡在两个簇的边界上,业务上多半要人工复核。

Replicates这个参数值得单拎出来说。K-means 对初始质心敏感是老问题了,内置函数默认Start是'plus'(k-means++ 的抽样策略),已经比纯随机强不少,但单次运行仍有概率落进局部最优。Replicates设成 5 或 10,函数内部会独立跑多次、保留sumd总和最小的那组结果,代价是耗时线性增长。7 个点的数据集上完全感觉不到,几十万样本的时候就得掂量了。

2.3 绘图层:scatter 着色与质心标记的执行顺序

示例的画图部分有三个细节会直接影响你出图能不能交差:颜色怎么映射、质心怎么突出、图例怎么生成。

figure('Color', 'w'); % 白底,导出 PNG 时不用再修 hold on; % 第三参数传 60 是点大小,第四参数直接用簇号做颜色值 scatter(data(:,1), data(:,2), 60, idx, 'filled', ... 'MarkerEdgeColor', 'none'); % 质心用大号黑色叉,尺寸拉开一档,让人一眼分得清点和中心 scatter(centroids(:,1), centroids(:,2), 200, 'k', 'x', 'LineWidth', 2); % 色图按簇数截断,K 是多少就取多少种颜色,相邻簇不会糊在一起 colormap(lines(K)); % 图例条目跟着 K 动态生成,别写死 labels = arrayfun(@(k) sprintf('簇 %d', k), 1:K, 'UniformOutput', false); legend([labels, {'质心'}], 'Location', 'best', 'Box', 'off'); axis equal; % 二维坐标等比例,不然簇的形状会被拉扁 grid on; hold off;

第三参数60是标记面积,不是半径,调大调小时按面积直觉来就行。第四参数直接塞idx,MATLAB 会把数值线性映射到当前 colormap 上,caxis自动取[min(idx), max(idx)],也就是[1, K],所以簇 1 落在色图最左端、簇 K 落在最右端,中间均匀铺开。这个行为在 K 小的时候没问题,K 上到十几二十,lines色图会循环取色,视觉上就开始重样了,那时候要么换parula(K),要么干脆改用gscatter按分组变量画。

axis equal这行在示例里没有,是必须补的。二维聚类图如果不锁等比例,坐标轴的物理长度不一致,圆形簇会被画成椭圆,密度判断完全失真——这个坑我在给别人看结果的时候踩过一次,对方盯着图问「为什么这簇是长条形的」,其实数据里它挺圆。

还有一点:legend的条目数必须和图形对象数对得上。原示例里三个 scatter 对象配三个标签没问题,但你一旦把数据点合并成一个 scatter 对象、质心也合并成一个,标签就会多出来。正确做法是像上面这样按K动态拼数组,K 变了图例自动跟着变,不用手工改字符串。

3. 在本机跑通:环境确认、脚本落地和结果自检

拿到一段能用的代码和拿到一个能跑的环境是两件事。这一段按顺序走三步:确认kmeans这个函数在你的 MATLAB 里存在、把 docx 里的代码变成真正的.m文件、跑完用三个数字给自己一个交代。

3.1 工具箱确认:kmeans 不是 base MATLAB 自带的

打开 MATLAB 第一件事不是粘贴代码,是确认授权。kmeans属于 Statistics and Machine Learning Toolbox,基础安装里没有它,学生版和某些精简授权尤其容易缺。

% 检查统计与机器学习工具箱授权,返回 logical hasStats = license('test', 'Statistics_Toolbox'); if hasStats v = ver('stats'); fprintf('统计与机器学习工具箱版本:%s\n', v.Version); else warning('缺少统计与机器学习工具箱,kmeans 不可用'); end % 顺手确认 MATLAB 主版本,老版本对 Name-Value 参数支持不全 fprintf('MATLAB 版本:%s\n', version);

如果hasStats是false,别急着去装工具箱,先确认是不是装了个精简版。真要手写也不是不行:K-means 的核心就是个双层循环——算距离分配、算均值更新质心、判断质心是否移动——三十来行能写完,代价是没有 k-means++ 初始化、没有空簇处理、没有向量化加速。我一般建议先用内置版把流程跑通、把下游逻辑写完,真到了需要定制初始化策略或者要嵌进不能带工具箱的部署环境时,再换手写实现,这样返工的成本最低。

3.2 从 docx 到 .m:把示例代码落成可执行脚本

把代码从文档里拷出来粘到编辑器,这一步翻车的概率比想象中高。中文全角分号、引号被替换成弯引号、注释里的中文导致文件编码不对,都会让脚本在第一行就报语法错。稳妥的做法是新建脚本、手动敲关键行、每敲完一段按一次运行。

function [idx, C] = demoKmeans(data, K) %DEMOKMEANS 最小可运行封装,输入输出与内置 kmeans 保持一致 % data: n×p 数值矩阵,行是样本,列是特征 % K: 正整数,聚类簇数 arguments data double {mustBeReal, mustBeFinite} K (1,1) double {mustBePositive, mustBeInteger} end rng(20240101); % 固定种子,谁跑都是同一组结果 [idx, C] = kmeans(data, K, 'Replicates', 5); end

arguments块是 R2019b 之后才有的语法,作用是声明式地做入参校验:mustBeReal拦复数,mustBeFinite拦 NaN 和 Inf,(1,1)约束 K 必须是标量。老版本用户把这四行删掉,改写成validateattributes或直接assert,功能等价。函数名和文件名必须一致,保存为demoKmeans.m,放在当前工作目录或者path里能搜到的位置,然后在命令行调用[idx, C] = demoKmeans(data, 2);。

把脚本改成函数不是为了显得规范,是为了让下游能调。脚本里的变量都是全局的,你在这个文件里叫data,换个文件又定义一遍data,两个不同来源的数据很容易串。函数一封,作用域干净,后面接批处理、接 GUI、接 App Designer 都不会互相污染。

3.3 结果自检:三个数字对一遍再信图

图好看不等于分得对。跑完脚本别急着截图,先在命令行打三个数字出来。

% 1) 每个簇实际分到多少样本,用 accumarray 按簇号计数 counts = accumarray(idx, 1, [K 1]); fprintf('各簇样本数:%s\n', mat2str(counts')); % 2) 簇内距离和,越小说明簇越紧凑 fprintf('各簇簇内距离和:%s\n', mat2str(sumd)); % 3) 质心两两之间的最小距离,检查有没有两个几乎重合的中心 if K > 1 pairDist = pdist(centroids); fprintf('质心两两最小距离:%.4f\n', min(pairDist)); end

三个数的判读方式不一样。counts里出现 0,说明这簇被抢空了,图上的质心是个孤零零的叉,下面一个点都没有,这多半是初始质心扎堆导致的,把Replicates调到 10 通常能救。sumd用来横向比较不同参数配置,同一个数据集上换个距离度量、换个 K,看总和往哪个方向走;注意别拿sqeuclidean的sumd和cityblock的sumd直接比大小,量纲不一样,比了没意义。min(pairDist)是我最看重的一项,两个质心距离接近到小数点后两位,说明 K 给多了,或者这批数据本来就没那么多个自然分组——这时候宁可把 K 减一,也不要硬解释两个几乎一样的簇有什么区别。

4. 参数深挖:kmeans 的 Name-Value 怎么配才不玄学

同一份数据、同一个 K,跑两次结果不一样,这事在 K-means 上不是玄学,是随机初始化加没有固定种子的必然结果。这一段把kmeans最常用的几个名值对拆开讲:哪个管初始化、哪个管收敛、哪个管距离度量,以及 K 到底该怎么选。

4.1 初始化与 Replicates:治「同一份数据跑两次结果不一样」

先复现一下不稳定。连续调两次,中间不重设种子,第二次用的是随机数流里的下一段,初始质心自然不同。

% 第一次调用,消耗一段随机数 [~, ~, sumdA] = kmeans(data, 2); fprintf('第一次总簇内距离:%.4f\n', sum(sumdA)); % 紧接着第二次,随机数流已经往后走了,初始点不同 [~, ~, sumdB] = kmeans(data, 2); fprintf('第二次总簇内距离:%.4f\n', sum(sumdB)); % 治法一:固定种子,结果完全可复现 rng(42); [~, ~, sumdC] = kmeans(data, 2); fprintf('固定种子后:%.4f\n', sum(sumdC)); % 治法二:不固定种子,靠多次重启兜底 [~, ~, sumdD] = kmeans(data, 2, 'Replicates', 20); fprintf('重启 20 次后:%.4f\n', sum(sumdD));

两种治法的适用场景不同。固定rng适合要交付、要写报告、要让别人复现你那张图的场合,缺点是把种子写死之后,你其实放弃了探索「换一组初始化会不会得到更好的解」的机会。Replicates适合对结果质量更敏感、对耗时不太敏感的场合,函数内部独立跑 20 次、留最优,等于用时间换稳定性。我一般两个都用:Replicates设 10,外面再套一个固定种子,既保证质量又保证可复现。

Start参数控制初始质心怎么来,取值有四种。'plus'是默认,走 k-means++ 的加权抽样,第一个质心随机取,后续每个质心按「离已有质心越远、被选中概率越大」的规则抽,实测比纯随机稳不少。'sample'从数据里随机挑 K 个点当初始质心,速度快但容易撞上离群点。'uniform'在数据范围的超矩形里均匀取点,适合特征维度低、分布比较规整的场景。第四种是直接传一个 K×p 的矩阵进去,你自己指定初始质心——做增量聚类或者拿上一次的结果当暖启动时用这个。

4.2 Distance、MaxIter、EmptyAction 的组合用法

距离度量决定了「相似」怎么定义,改它比改 K 影响还大。

% 场景一:特征量纲差距大,先按列标准化,再聚类 dataStd = normalize(data); % 等价于 zscore,按列减均值除标准差 % 场景二:文本向量或方向型特征,余弦距离更合适 [idxCos, cCos] = kmeans(dataStd, 2, 'Distance', 'cosine'); % 场景三:担心某次迭代出现空簇,让函数把最远的点挪过去补位 [idxFix, cFix] = kmeans(data, 2, 'EmptyAction', 'singleton'); % 场景四:数据量上来以后,关掉迭代输出、放开迭代上限 [idxBig, cBig] = kmeans(data, 3, 'MaxIter', 500, 'Display', 'off');

normalize那行是必须的。K-means 用欧氏距离,某一列的量级是另一列的几十倍时,聚类结果基本由那一列单独决定,其他列等于没参与——这个现象很迷惑人,因为代码不报错、图也照出,只是分出来的簇在你看来毫无业务含义。判断方法很简单:跑之前std(data)看一眼各列标准差,差一个数量级以上就得标准化。

Distance 取值距离定义典型场景注意点
sqeuclidean欧氏距离的平方(默认)连续数值特征,量纲已统一sumd是平方和,比较时别当距离用
cityblock曼哈顿距离,各维绝对差之和高维稀疏、对离群点更稳计算比欧氏略慢
cosine1 减余弦相似度文本 TF-IDF 向量、方向型特征对向量模长不敏感
correlation1 减皮尔逊相关系数关注曲线形状而非幅度每行先减自身均值

MaxIter默认 100,绝大多数中小数据集在 20 次以内就收敛了,调大它的意义在于防止极端分布下提前截断。判断有没有真的收敛,把Display设成'iter'跑一次,看最后几轮的目标函数值还在不在变——一直在小幅波动说明在两组解之间来回跳,那多半是数据里存在到两个质心等距的点,属于正常现象,不影响整体划分。

EmptyAction默认是'singleton',也就是某个簇没抢到点时,把离它自己质心最远的那个样本强行划过来。这个默认值是合理的,但副作用是它会在你不注意的时候悄悄改动一个样本的归属。想让它报错暴露问题,就设成'error',开发阶段我倾向这么干,上线再换回默认。

4.3 用 evalclusters 选 K:肘部法和轮廓系数

K 给多少,是这份示例代码留白最大的地方。MATLAB 自带evalclusters,能把几种常用的选 K 准则跑成一条曲线。

KList = 2:6; % 候选簇数,从 2 起,单簇没有聚类意义 % 轮廓系数:取值 [-1, 1],越接近 1 说明簇内紧、簇间远 evaSil = evalclusters(data, 'kmeans', 'silhouette', 'KList', KList); fprintf('轮廓系数最优 K = %d\n', evaSil.OptimalK); disp(evaSil.CriterionValues); % 肘部法:看簇内平方距离和随 K 增加的下降速度,找拐点 evaElbow = evalclusters(data, 'kmeans', 'sqeuclidean', 'KList', KList); disp(evaElbow.CriterionValues); % 两条曲线画在一起,人工看一眼更踏实 figure('Color', 'w'); yyaxis left; plot(KList, evaSil.CriterionValues, '-o', 'LineWidth', 1.5); ylabel('平均轮廓系数'); yyaxis right; plot(KList, evaElbow.CriterionValues, '-s', 'LineWidth', 1.5); ylabel('簇内平方距离和'); xlabel('簇数 K'); grid on;

evalclusters返回的是一个对象,常用字段有三个:OptimalK是按当前准则算出来的推荐簇数,CriterionValues是对每个候选 K 算出的准则值,InspectedK是你传进去的候选列表。轮廓系数和肘部法给出的结论经常不一致,这很正常——前者衡量的是「每个点跟自己簇的贴合度相对跟最近邻簇的贴合度」,后者衡量的是「整体紧凑度的边际收益」。我一般以轮廓系数为主,肘部法的拐点作为交叉验证;两者都指向同一个 K 时基本可以定,冲突时优先信轮廓系数,因为它对簇的分离度更敏感。

有个硬约束得记住:轮廓系数要求2 <= K <= n-1,样本数少于 3 的时候根本算不了。这份示例只有 7 个点,KList开到 6 已经接近退化边界,每个簇平均一个多点,指标数值没有参考价值。选 K 这套方法要用在几百个样本以上的数据上才有意义,样本太少的时候老老实实靠业务知识定。

5. 避坑与排查:跑这份 K-means 代码最常翻车的五个地方

下面五条都是真跑出来的问题,前两条出在数据进算法之前,后三条出在结果出来之后。按「现象 → 原因 → 解决」的顺序记,遇到同类症状可以直接对号。

5.1 数据进算法之前:方向反了和量纲没统一

坑一:样本方向搞反,代码不报错但结果全废。

现象:脚本一路跑到底,idx也出来了,长度却等于特征数而不是样本数,画出来的图点数明显不对。

原因:kmeans硬性约定行是样本、列是特征。你从别的语言或者别的工具箱搬过来的数据,很可能是列优先的,转置漏了一步。

解决:进函数前固定加一道形状检查和一次显式转置。[n, p] = size(data);之后assert(n > p, '样本数应大于特征数,检查是否需要转置'),这个断言在样本数远大于维度时恒成立,能挡住绝大多数方向错误。真要转置就写data = data.',用非共轭转置,别用'——数据里有复数的时候'会顺手取共轭,静默改数据。

坑二:量纲没统一,某一列独揽大权。

现象:聚类结果的簇边界几乎完全由某一列决定,其他列看起来毫无影响;把那一列删掉重跑,分组结果大变。

原因:欧氏距离对量纲敏感。一列取值在 0 到 1 之间、另一列在 0 到 10000 之间,后者的差值平方会彻底压过前者。

解决:data = normalize(data);做按列 z-score,或者用normalize(data, 'range')缩到 [0,1]。注意标准化之后centroids也是标准化空间的坐标,要还原回原始量纲得手动反变换:C_orig = C .* std(data) + mean(data);。这一步经常被忘,导致质心坐标跟原始数据对不上,业务方一看就说数错了。

5.2 结果出来之后:图例、空簇和种子

坑三:图例条目数和图形对象数对不上。

现象:命令行飘出一条警告Ignoring extra legend entries,图上只有两个图例项,第三个簇的说明丢了。

原因:legend('Cluster 1', 'Cluster 2', 'Centroids')这种写法假设了「一个簇一个图形对象」,实际用scatter(x, y, [], idx, 'filled')的时候所有点是同一个对象,颜色是内部映射出来的,图例只能给出一项。

解决:要么按 2.3 节那样用arrayfun动态拼标签数组,长度跟 K 走;要么改用gscatter(data(:,1), data(:,2), idx),它按分组变量自动生成每个簇一个对象,图例天然对齐,缺点是颜色和标记的自定义空间小一些。

坑四:某个簇的样本数为 0,图上质心孤零零挂着。

现象:accumarray(idx, 1, [K 1])打出来有个位置是 0,那张图里对应的质心下面一个点都没有。

原因:初始质心扎堆,或者数据里存在远离所有点的离群值,某次迭代后一个簇被彻底抢空。EmptyAction默认'singleton'会强行把一个点挪过去,但如果你手动设过'error'或者'drop',行为就不一样了。

解决:先把Replicates提到 10 以上,多数情况能避开。还不行就检查离群值,用isoutlier(data)标出来,决定是剔除还是单独分析。开发阶段我建议临时把EmptyAction设成'error',让问题当场暴露,比事后看图猜强。

坑五:随机种子位置放错,结果次次不同。

现象:同一个脚本连跑三遍,centroids每次都差一点点,交给别人的图和自己的对不上。

原因:rng没设,或者设在了循环内部——后者更隐蔽,每轮循环重设一次种子,等于每轮都用同一组随机数,反而失去了Replicates该有的效果。

解决:种子写在脚本最外层,一次就够;循环内部不要再碰rng。要同时保质量和可复现,rng(固定值)加'Replicates', 10一起上。另外注意parfor并行时种子的传播规则和普通循环不同,并行环境下每个 worker 的随机流是独立的,需要RandStream显式管理,这块容易踩。

6. 二次开发:把这十几行示例改成能塞进项目里的聚类函数

示例代码的定位是演示,直接搬进项目会缺三样东西:标准化没做、重启次数没设、结果没法量化。下面这版封装把三样都补上,同时保留和内置kmeans一致的输入输出,替换成本几乎为零。

function [idx, C, report] = runKmeans(data, K, varargin) %RUNKMEANS 带标准化、重启与质量体检的 K-means 封装 % data : n×p 原始数据,行是样本 % K : 簇数 % 名值对 : 'Standardize' 是否按列标准化,默认 true % 'Replicates' 重启次数,默认 10 % 'Seed' 随机种子,默认 20240101 p = inputParser; addParameter(p, 'Standardize', true, @islogical); addParameter(p, 'Replicates', 10, @isnumeric); addParameter(p, 'Seed', 20240101, @isnumeric); parse(p, varargin{:}); opt = p.Results; X = opt.Standardize ? normalize(data) : data; rng(opt.Seed); % 种子放在最外层 [idx, C, sumd] = kmeans(X, K, 'Replicates', opt.Replicates); % 打包一份体检报告,下游不用再自己算 report = struct( ... 'counts', accumarray(idx, 1, [K 1])', ... 'sumd', sumd, ... 'totalWithin', sum(sumd), ... 'meanSilhouette', mean(silhouette(X, idx)), ... 'minCentroidDist', min(pdist(C))); fprintf('K=%d | 样本分布 %s | 平均轮廓 %.3f | 最小质心距 %.3f\n', ... K, mat2str(report.counts), report.meanSilhouette, report.minCentroidDist); end

三个改造点的取舍值得说清楚。标准化做成开关而不是写死,是因为有些场景下量纲本身就是信息——比如「消费金额」这一列,大额客户和小额客户在业务上就该被分开,强行标准化反而抹掉了这层差异。Replicates默认给 10 而不是 1,是用时间换稳定,十万样本以内感受不明显。report单独返回而不是塞进idx里,是为了让调用方按需取用,不想看指标的时候直接忽略第三个输出即可。

验证这套封装有没有改对,我一般跑一个固定的小回归用例:

检查项期望不通过说明
size(idx)[n 1]样本方向反了或转置漏了
sum(report.counts)等于n有空簇被丢弃
report.meanSilhouette大于 0划分比随机还差,检查标准化
重复调用两次idx完全一致种子没生效,检查rng位置

最后一条尤其重要。我在一个批量跑聚类的脚本里吃过亏:种子写在了内层函数里,外层用parfor并发调用,每个 worker 拿到的是自己那份随机流,跑完一百组数据回头想复现其中一组的最优结果,怎么都复现不出来,只能整批重跑。从那以后我每次交付聚类结果,都先把idx连跑两遍比对是否完全一致,再打印sumd、最小质心距、平均轮廓这三个数,最后才发图。图好看不代表分得对,数字对上了才敢说这版能交。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询