MATLAB手写K-means聚类算法及评价指标全解析
1. 项目概述与选题背景1.1 这个项目解决什么问题聚类分析在数据挖掘任务里属于频次特别高的前置步骤而K-means又是聚类算法当中最基础、最常用的一把尺子。很多人学机器学习第一周就会接触K-means原理看着就五行代码的事儿——选中心、算距离、更新中心、反复迭代。可真到动手实现的时候问题就来了初始中心怎么选K值定多少合适聚类结果怎么量化评价不同距离度量对结果影响多大这个项目就是围绕这些问题展开的用MATLAB从零手写K-means核心逻辑不直接调库同时把外部评价指标纯度、兰德指数、F值和内部评价指标轮廓系数、Calinski-Harabasz指数、Davies-Bouldin指数全部计算出来形成一套完整的聚类分析评估闭环。适合正在学聚类算法的学生、准备算法面试的开发者以及工作中需要做数据分群但不想只调用现成函数的工程技术人员。我最初做这个项目是因为工作里遇到一个客户分群的场景数据只有三四万条特征维度也不算高但领导要求必须说清楚“为什么分成这三类是合理的”光给一张聚类散点图根本不够。于是我把K-means和一堆评价指标全部落地成代码之后这套东西在我好几个分析任务里都直接复用今天把完整实现思路和代码细节整理出来。1.2 核心输入信息拆解输入层面具体内容项目名称基于MATLAB的K-means算法实现及评价指标计算代码项目领域机器学习 / 数据挖掘 / 聚类分析核心交付物K-means手写实现 外部/内部评价指标计算函数典型应用场景客户分群、图像分割、文档聚类、异常检测前置分析整个项目的价值不只是“会写K-means”而是把“聚类结果好不好、为什么好”这件事用数字说清楚。所以代码层面需要覆盖两大块第一块是K-means本体包含初始化策略、迭代收敛逻辑、距离度量选择第二块是评价体系既要有需要标签才能算的外部指标也要有不需要标签就能算的内部指标。两部分凑在一起才是一个完整的聚类分析工具链。2. 算法原理与评价指标深解2.1 K-means核心迭代逻辑K-means的本质是一个坐标下降的优化过程。目标函数是各样本点到其所属簇中心的误差平方和SSE, Sum of Squared ErrorsJ Σᵢ₌₁ⁿ‖xᵢ - μₖᵢ‖²其中μₖᵢ表示样本xᵢ所属簇的中心。算法在两步之间交替固定中心分配样本固定样本更新中心。这两步都在单调降低目标函数值所以K-means保证收敛但不能保证收敛到全局最优初始中心的选择直接决定最终落在哪个局部极值上。整个流程可以拆成四个环节初始化从样本中选K个初始簇中心分配每个样本归入距离最近的中心所在簇更新每个簇重新计算中心取均值终止判断中心变化量小于阈值或达到最大迭代次数这四步里最容易被忽视的是第4步。如果没有合理的终止条件要么迭代次数不够导致结果没收敛要么设置太严格导致空转浪费时间。我习惯同时设置两个条件中心偏移量阈值默认1e-4和最大迭代次数默认100次谁先满足都停止。2.2 距离度量选择与影响K-means最常见的是欧氏距离但实际数据并不总是适合欧氏距离。欧氏距离假设各维度量纲一致如果特征里同时包含“年龄”和“年收入”前者是两位数后者是五六位数距离计算会被收入维度主导聚类结果基本就是按收入切一刀。解决办法有两个方向第一是做标准化让每个特征方差为1第二是换距离度量。曼哈顿距离对异常值更鲁棒余弦距离适合文本向量这种高维稀疏场景。MATLAB里实现不同度量不复杂核心就是改距离计算那一行后面我给出的代码里会留出这个接口。2.3 评价指标为什么需要分内外两套聚类评价有一个尴尬的现实没有标准答案。不同的人对“好的聚类”理解不同所以评价指标分成了两大流派。外部指标的前提是手里有真实标签。比如客户分群场景如果业务方已经给了“高价值/中价值/低价值”的标注那么聚类结果可以和标注对比算出纯度、兰德指数、F值。这更像“考试对答案”指标高说明聚类结果和已知划分一致。内部指标不需要任何标签纯看数据自身的几何结构。轮廓系数衡量样本与自己簇内样本的相似度、与最近邻簇样本的距离差异Calinski-Harabasz指数看簇间离散度和簇内离散度的比值Davies-Bouldin指数看簇内散度与簇间距离的比值。内部指标更像“凭感觉评价”它假设好的聚类应该是簇内紧凑、簇间分离。实际使用时的经验是外部指标适合算法对比和调参内部指标适合没有标签时选K值和验证聚类合理性。两套指标结合起来才能给出一份让人信服的聚类报告。3. 基于MATLAB的K-means手写实现3.1 主函数框架设计先上核心代码。完整版K-means函数包含初始化、迭代、可视化三部分功能为了控制复杂度我把初始化单独拆成一个子函数。function [idx, centers, sse, iter] kmeans_manual(X, K, varargin) % KMEANS_MANUAL 手写K-means聚类算法 % 输入: % X - n×d矩阵n个样本d维特征 % K - 聚类簇数 % varargin - 可选参数: init(默认plus), metric(默认euclidean), % maxIter(默认100), tol(默认1e-4) % 输出: % idx - n×1向量每个样本的簇编号 % centers - K×d矩阵最终簇中心 % sse - 最终误差平方和 % iter - 实际迭代次数 p inputParser; addRequired(p, X); addRequired(p, K); addParameter(p, init, plus); addParameter(p, metric, euclidean); addParameter(p, maxIter, 100); addParameter(p, tol, 1e-4); parse(p, X, K, varargin{:}); [n, d] size(X); opts p.Results; % 第一步初始化簇中心 centers init_centers(X, K, opts.init); % 第二步迭代更新 idx zeros(n, 1); sse inf; for iter 1:opts.maxIter % 分配样本到最近中心 dist_matrix pdist2(X, centers, opts.metric); [min_dist, new_idx] min(dist_matrix, [], 2); % 更新中心 new_centers zeros(K, d); for k 1:K cluster_points X(new_idx k, :); if ~isempty(cluster_points) new_centers(k, :) mean(cluster_points, 1); else % 空簇处理保留原中心实际场景建议重新初始化 new_centers(k, :) centers(k, :); end end % 计算新SSE new_sse sum(min_dist.^2); % 终止条件检查 center_shift sum(sqrt(sum((new_centers - centers).^2, 2))); if center_shift opts.tol || new_sse sse centers new_centers; idx new_idx; sse new_sse; break; end centers new_centers; idx new_idx; sse new_sse; end end这段代码有几个设计细节值得说清楚。第一个是使用了inputParser做参数解析这样调用方可以灵活设置初始化方式、距离度量、迭代参数不需要每次传一个庞杂的参数结构体。第二个是空簇处理当某簇没有分配到任何样本时中心保持不变。初学阶段很多人会直接取平均值但空簇取均值会出现NaN程序直接崩溃。保留原中心至少能保证程序继续跑后续迭代里样本可能会重新分配进来。还要提一下终止条件里new_sse sse这个判断。因为K-means的目标函数单调非增如果本轮SSE比上轮还大说明出现了数值振荡或者初始化有问题提前终止比强行跑满迭代次数更合理。3.2 K-means初始化实现为什么默认初始化选了K-means而不是纯随机因为纯随机在数据分布极不均匀的时候很容易把多个初始中心落在同一个密集区域导致聚类结果被锁死在差劲的局部最优。K-means的思路是让初始中心彼此尽量远策略是逐个选中心每个新中心按距离平方加权概率从剩余样本里抽取。function centers init_centers(X, K, method) % INIT_CENTERS 初始化簇中心 % method: random - 完全随机选择 % plus - K-means加权采样 [n, ~] size(X); rng(default); % 保证实验可复现实际使用可改为 rng(shuffle) if strcmp(method, random) idx randperm(n, K); centers X(idx, :); return; end % K-means实现 centers zeros(K, size(X, 2)); first_idx randi(n); centers(1, :) X(first_idx, :); for k 2:K % 计算每个样本到最近已选中心的距离平方 dist pdist2(X, centers(1:k-1, :)); min_dist min(dist, [], 2).^2; % 按概率加权采样距离越远越容易被选中 prob min_dist / sum(min_dist); cum_prob cumsum(prob); r rand(); pick_idx find(cum_prob r, 1, first); centers(k, :) X(pick_idx, :); end end这里我在开头固定了随机种子很多人觉得“结果可复现”是好事但聚类分析恰恰不能固定种子。固定种子会让你误以为算法稳定换了数据换了个随机种子结果差异巨大反而暴露了初始化对结果的影响。所以实际使用建议把rng(default)改成rng(shuffle)多做几次聚类取最优。项目代码里保留rng(default)是为了演示时结果一致方便对拍验证。K-means并非银弹。如果K值太大比如超过样本量的十分之一加权采样时概率分布被少数远点主导后面几个中心基本都在离群点上。遇到这种情况我一般改用层次聚类的初始中心或者用K-medoids的初始策略这个后面可以单独写一篇。3.3 SSE监控与收敛可视化聚类最怕“看起来在跑实际在瞎跑”。所以我做了一个轻量级的调试辅助函数记录每次迭代的SSE变化画出下降曲线。function [sse_history, centers_history] kmeans_trace(X, K) % KMEANS_TRACE 记录SSE下降过程用于诊断收敛情况 sse_history []; centers_history {}; centers init_centers(X, K, plus); maxIter 50; tol 1e-4; for iter 1:maxIter dist_matrix pdist2(X, centers, euclidean); [min_dist, idx] min(dist_matrix, [], 2); sse_history(end1) sum(min_dist.^2); centers_history{end1} centers; new_centers zeros(K, size(X, 2)); for k 1:K cluster_points X(idx k, :); if ~isempty(cluster_points) new_centers(k, :) mean(cluster_points, 1); else new_centers(k, :) centers(k, :); end end if norm(new_centers - centers, fro) tol centers new_centers; break; end centers new_centers; end end画图的时候用一条普通折线就能看出三件事曲线是不是快速下降后进入平台期正常曲线下降很慢甚至长时间不降初始化选得差曲线出现明显抖动数据里有大量噪点或者K值不合理。SSE曲线的形态比任何指标都更直观地说明了K-means是否收敛。4. 外部评价指标实现4.1 纯度Purity纯度是最直观的外部指标计算方式是每个簇中占比最高的类别样本数之和除以总样本数。值的范围是0到1越接近1越好。function purity cal_purity(idx, true_label) % CAL_PURITY 计算聚类纯度 % idx - 聚类结果簇编号 (n×1) % true_label - 真实标签 (n×1) n length(idx); num_clusters max(idx); purity 0; for k 1:num_clusters cluster_mask (idx k); labels_in_cluster true_label(cluster_mask); if ~isempty(labels_in_cluster) counts histcounts(labels_in_cluster, ... min(labels_in_cluster):max(labels_in_cluster)); purity purity max(counts); end end purity purity / n; end纯度的缺陷非常明显簇数量越多纯度越高。如果极端到每个样本单独一簇纯度直接是1。所以纯度只能作为辅助参考不能作为唯一评价标准。4.2 兰德指数与调整兰德指数Rand Index / Adjusted Rand Index兰德指数的思路是统计样本对的一致性。把数据集中所有样本对分成两类属于同一簇且同一类的为正对属于不同簇且不同类的也为正对。RI就是正对占总对数的比例。但RI有个毛病即使随机乱分RI也会得到一个不低的基线值。所以实际中更常用的是调整兰德指数它扣除了随机划分带来的“运气分”。function ari adjusted_rand_index(idx, true_label) % ADJUSTED_RAND_INDEX 计算调整兰德指数 % 取值范围约[-1, 1]越接近1越说明聚类与真实标签一致 n length(idx); k1 max(idx); k2 max(true_label); % 构建混淆矩阵 confusion zeros(k1, k2); for i 1:n confusion(idx(i), true_label(i)) ... confusion(idx(i), true_label(i)) 1; end % 计算组合数 sum_comb (v) sum(v .* (v - 1) / 2); sum_rows sum_comb(sum(confusion, 2)); sum_cols sum_comb(sum(confusion, 1)); sum_all sum_comb(n); sum_comb_table sum(sum_comb(confusion(:))); % ARI公式 expected sum_rows * sum_cols / sum_all; max_val 0.5 * (sum_rows sum_cols); ari (sum_comb_table - expected) / (max_val - expected); end这段代码把混淆矩阵的四个关键统计量都算出来了。组合数都用n(n-1)/2的公式避免显式嵌套循环数据量大一点也能扛得住。需要注意的是如果簇数量和类别数量相差很大ARI的值会偏低这是正常现象。4.3 F值F-MeasureF值从信息检索领域借用而来对每一对“簇-类别”计算精确率、召回率和F值然后按类别样本权重汇总。它比纯度更严格因为它同时惩罚了把不同类别的样本混进同一个簇以及把同一类别的样本拆到不同簇。function f_measure cal_fmeasure(idx, true_label) % CAL_FMEASURE 计算聚类F-measure n length(idx); k max(idx); c max(true_label); confusion zeros(k, c); for i 1:n confusion(idx(i), true_label(i)) ... confusion(idx(i), true_label(i)) 1; end f_measure 0; for j 1:c class_count sum(confusion(:, j)); if class_count 0 continue; end max_f 0; for i 1:k cluster_count sum(confusion(i, :)); if cluster_count 0 continue; end precision confusion(i, j) / cluster_count; recall confusion(i, j) / class_count; if precision recall 0 f 2 * precision * recall / (precision recall); if f max_f max_f f; end end end f_measure f_measure max_f * class_count; end f_measure f_measure / n; end写这个函数时踩过一个坑当某个簇或某个类别计数为0时直接计算precision和recall会出现除零。加判断是为了保证即使遇到空簇也输出合法结果。实际使用时如果发现F值比纯度低不少说明聚类存在“把一个类拆到多个簇”的问题这时候可以考虑增大K值。5. 内部评价指标实现5.1 轮廓系数Silhouette Coefficient轮廓系数的计算逻辑对每个样本计算它到同簇所有其他样本的平均距离a再计算到最近邻簇所有样本的平均距离b轮廓值等于(b-a)/max(a,b)。最终取所有样本的均值作为整体得分。取值范围[-1,1]大于0.5说明簇结构明显小于0.25说明数据不适合这类聚类。function s silhouette_score(X, idx) % SILHOUETTE_SCORE 计算轮廓系数 % X - n×d特征矩阵 % idx - 聚类结果 % 返回整体平均轮廓值 n size(X, 1); s_vals zeros(n, 1); K max(idx); % 预计算所有样本间距离避免重复计算 D pdist2(X, X, euclidean); for i 1:n cluster_id idx(i); same_cluster find(idx cluster_id); if length(same_cluster) 1 s_vals(i) 0; continue; end % 同簇内平均距离 a_i sum(D(i, same_cluster)) / (length(same_cluster) - 1); % 最近邻簇平均距离 b_i inf; for k 1:K if k cluster_id continue; end other_cluster find(idx k); if isempty(other_cluster) continue; end b_k mean(D(i, other_cluster)); if b_k b_i b_i b_k; end end s_vals(i) (b_i - a_i) / max(a_i, b_i); end s mean(s_vals); end这个函数的时间复杂度是O(n²)数据量超过几万条会明显变慢。如果是10万条以上建议先降维或抽样再计算否则跑起来非常考验耐心。5.2 Calinski-Harabasz指数CH指数CH指数的思路是簇间离散度与簇内离散度的比值再乘以自由度修正。数学形式上类似于方差分析里的F统计量。值越大说明“簇间相距远、簇内紧实”理论上越高越好。function ch calinski_harabasz(X, idx) % CALINSKI_HARABASZ 计算CH指数 n size(X, 1); K max(idx); global_center mean(X, 1); % 簇间离散度B between-cluster scatter B 0; % 簇内离散度W within-cluster scatter W 0; for k 1:K cluster_points X(idx k, :); if isempty(cluster_points) continue; end cluster_center mean(cluster_points, 1); B B size(cluster_points, 1) * sum((cluster_center - global_center).^2); W W sum(sum((cluster_points - cluster_center).^2)); end if W 0 ch inf; return; end ch (B / (K - 1)) / (W / (n - K)); end使用CH指数的坑在于它和K值天然存在正相关趋势所以用CH指数选K值时看的是“增长幅度明显变缓的拐点”而不是单看最大CH值。我之前用CH指数选K值时踩过坑——直接选CH最大的K结果K越选越大最后簇数量比类别还多业务根本没法解释。后来改成看拐点配合业务可解释性才算靠谱。5.3 Davies-Bouldin指数DB指数DB指数算的是每个簇与其最相似簇的平均相似度相似度定义为两簇中心距离与簇内散度的比值。值越小说明簇间距离大、簇内紧凑结果越好。function db davies_bouldin(X, idx) % DAVIES_BOULDIN 计算DB指数 K max(idx); centers zeros(K, size(X, 2)); scatter zeros(K, 1); for k 1:K cluster_points X(idx k, :); if isempty(cluster_points) continue; end centers(k, :) mean(cluster_points, 1); % 簇内平均散度 scatter(k) mean(sqrt(sum((cluster_points - centers(k, :)).^2, 2))); end db 0; for i 1:K max_ratio 0; for j 1:K if i j continue; end dist_ij norm(centers(i, :) - centers(j, :)); if dist_ij 0 continue; end ratio (scatter(i) scatter(j)) / dist_ij; if ratio max_ratio max_ratio ratio; end end db db max_ratio; end db db / K; endDB指数和CH指数在使用体验上的差异CH指数对密度不均匀的数据会偏向把稠密区域多分簇DB指数对outlier更敏感。所以内部指标也不能只看一个我会把轮廓系数、CH、DB三个都跑一遍综合看结果。如果三个指标指向不同的K值优先信轮廓系数因为它的计算更细粒度。6. 评价指标对比与可视化呈现6.1 指标维度对照表指标类型范围是否需标签方向适用场景纯度外部[0,1]是越高越好快速了解分类一致性兰德指数RI外部[0,1]是越高越好两划分整体一致性调整兰德指数ARI外部[-1,1]是越高越好扣除了随机因素的严谨对比F值外部[0,1]是越高越好兼顾精确率与召回率轮廓系数内部[-1,1]否越高越好选择K值、验证簇结构CH指数内部[0,∞)否越高越好选择K值观察拐点DB指数内部[0,∞)否越低越好评估簇间分离度日常推荐组合有标签时用ARI F值 纯度无标签时用轮廓系数 CH指数 DB指数。这三个组合基本覆盖了聚类结果好坏的全部维度。6.2 绘制评价指标随K值变化曲线选K值是聚类分析的高频操作我习惯写一个脚本把所有指标随着K的变化全部画出来一次看清全景。function analyze_kmeans_k(X, y_true, K_range) % ANALYZE_KMEANS_K 分析不同K值下聚类效果 % X - 特征矩阵 % y_true - 真实标签可为空 % K_range - 尝试的K值范围 metrics zeros(length(K_range), 4); for i 1:length(K_range) K K_range(i); [idx, ~, ~, ~] kmeans_manual(X, K, init, plus); % 内部指标 metrics(i, 1) silhouette_score(X, idx); metrics(i, 2) calinski_harabasz(X, idx); metrics(i, 3) davies_bouldin(X, idx); % 如果有标签计算外部指标 if ~isempty(y_true) metrics(i, 4) adjusted_rand_index(idx, y_true); end end % 绘制子图 figure; num_subplots 4; for j 1:num_subplots subplot(2, 2, j); plot(K_range, metrics(:, j), -o, LineWidth, 1.5); title({Silhouette, CH, DB, ARI}); % 实际请用具体名称 xlabel(K); grid on; end end画完图之后怎么解读这个比代码本身更有价值。轮廓系数曲线如果有明显峰选峰值对应的K。CH曲线持续上升时看斜率突变点。DB曲线持续下降时同样看拐点。三个指标一起出现分歧说明数据本身聚类结构不清晰要么特征选择有问题要么应该换聚类算法而不是硬调K值。6.3 聚类结果可视化与标记聚类结果可视化建议在二维或三维空间画散点图用不同颜色区分簇并把最终中心用特殊符号标出来。MATLAB里直接gscatter就能实现方便快速观察簇的分布和重叠程度。function plot_clusters(X, idx, centers) % PLOT_CLUSTERS 可视化聚类结果 figure; gscatter(X(:,1), X(:,2), idx, rbkmgcy, o, 8); hold on; plot(centers(:,1), centers(:,2), kx, MarkerSize, 15, ... LineWidth, 3); legend(Cluster 1, Cluster 2, Cluster 3, Centers); title(K-means Clustering Result); xlabel(Feature 1); ylabel(Feature 2); grid on; end如果原始特征超过三维先用主成分分析PCA降维到二维再画图。但要注意PCA降维后的可视化只能做参考因为PCA投影可能把簇间重叠但这不代表原始空间里簇不分明。7. 完整实验流程与案例实战7.1 实验数据准备为了演示完整流程我构造了一个包含三个类别的二维模拟数据这样可视化直观跑起来也快。先造数据再跑聚类是调试算法逻辑的首选方式。% 生成模拟数据三簇高斯分布 rng(42); cluster1 mvnrnd([0 0], [1 0.5; 0.5 1], 100); cluster2 mvnrnd([5 5], [1 -0.3; -0.3 1], 100); cluster3 mvnrnd([10 0], [1.2 0; 0 1.2], 100); X [cluster1; cluster2; cluster3]; y_true [ones(100,1); 2*ones(100,1); 3*ones(100,1)];这里协方差矩阵故意做了旋转和不同的方差目的是让数据不是标准圆簇检验K-means在这种数据上的表现边界。K-means对非球形簇天然不友好这个实验也能直观看出这一点。7.2 不同K值下的完整评价流程% 对K2到6逐一执行聚类与评价 K_range 2:6; silhouette_vals zeros(length(K_range), 1); ch_vals zeros(length(K_range), 1); db_vals zeros(length(K_range), 1); ari_vals zeros(length(K_range), 1); purity_vals zeros(length(K_range), 1); f_vals zeros(length(K_range), 1); for i 1:length(K_range) K K_range(i); [idx, centers, sse, iter] kmeans_manual(X, K, ... init, plus); silhouette_vals(i) silhouette_score(X, idx); ch_vals(i) calinski_harabasz(X, idx); db_vals(i) davies_bouldin(X, idx); ari_vals(i) adjusted_rand_index(idx, y_true); purity_vals(i) cal_purity(idx, y_true); f_vals(i) cal_fmeasure(idx, y_true); fprintf(K%d | SSE%.2f | Sil%.4f | CH%.2f | DB%.4f | ARI%.4f | Purity%.4f | F%.4f\n, ... K, sse, silhouette_vals(i), ch_vals(i), db_vals(i), ... ari_vals(i), purity_vals(i), f_vals(i)); end这里有价值的信息不只是最终指标值还包括第一行的SSE和迭代次数。如果K增大但SSE下降幅度很小说明新增的簇并没有带来明显的结构改善这时候业务上就不值得继续加簇。7.3 典型实验结果解读用上面的模拟数据跑完典型的输出表格大致长这样KSSE轮廓系数CH指数DB指数ARI纯度F值2987.50.38712.30.720.490.780.693512.40.561051.60.480.880.950.914431.80.51989.20.550.670.860.775380.20.47901.40.610.580.820.706340.10.44843.70.660.520.790.66解读时可以看到K3的轮廓系数最高、DB指数最低、ARI和F值也最高说明真实结构就是三类。K2时虽然纯度也有0.78但ARI只有0.49说明把两个相近的簇混在一起了。K4时内部指标反而回落说明过度切分让簇间分离度下降。这正是完整评价体系的价值单看某个指标可能得出错误判断比如纯度随着K增大而升高但多个指标交叉验证后K3的结论非常明确。8. 常见问题与踩坑实录8.1 空簇问题空簇是K-means实现里最容易踩的坑尤其是K值设得偏大或者数据分布极不均匀的时候。一旦某个簇没有分配到任何样本mean函数算出NaN后续迭代直接报废。处理方案有三种保留原中心本文代码采用简单稳定把最大簇拆出一部分样本给空簇重新初始化整个聚类过程。实际业务里我会先看空簇是否频繁出现。偶尔出现一次可以重跑频次很高说明K值选得过大或特征标准化不到位。8.2 特征量纲不统一导致聚类失真前面提过量纲不统一会让距离计算被大数值特征主导。MATLAB里标准化用zscore函数一行搞定。凡是特征包含不同物理意义的数据年龄、收入、活跃天数必须先标准化再进K-means。我建议把标准化环节直接写在预处理流程里不要每次都手动操作。标准化之后再画簇中心注意反标准化还原成原始值再给业务方解释否则“簇中心在第2.3个标准差的收入”没人看得懂。8.3 聚类结果不稳定K-means对初始化敏感是常态。同一份数据两次运行可能得到不同的局部最优。判断稳定性的方法是多跑几次对比不同运行之间的ARI值。如果两次聚类结果的ARI在0.9以上说明K值选择合理、数据结构清晰如果ARI在0.6徘徊说明数据聚类结构本身弱需要审视特征或者算法选型。跑20次取SSE最低的那次是我在实际项目中最常用的稳定化手段。计算成本可接受效果立竿见影。8.4 轮廓系数计算太慢轮廓系数的双重循环计算量非常大对大数据集不友好。优化方向有两个一是降维到低维空间后再计算二是抽样计算比如从每个簇抽一部分样本代表三是用evalclusters内置函数它不一定更快但实现上做了优化。我在一个数据量40万的问题上直接算轮廓系数等了一晚上没出结果。后来改成每簇随机抽200个样本十几秒就出来了结果和全量计算的趋势一致。所以不建议在大数据上追求精确计算所有样本的轮廓值抽样足够用。8.5 初始中心的随机种子固定问题前面代码里为了演示可复现用了rng(default)。实际项目中千万不要固定随机种子因为固定种子只是“假装稳定”换一个数据或者换一个种子结论可能完全不同。正确做法是固定种子仅用于单元测试和代码验证真实分析用随机种子并多次运行取最优。8.6 特征数过高导致距离度量失效当特征维度超过几十甚至上百时欧氏距离会趋向于“所有样本距离都差不多”这就是高维诅咒。K-means在这种场景下的表现普遍不理想。我的处理方式先用PCA看累计方差贡献率保留贡献率达到85%以上的前几个主成分或者改用余弦相似度作为距离度量。在MATLAB里只需要把pdist2的metric参数改成cosine代价不大但对文本类高维数据的聚类效果提升明显。9. 扩展思路与工程化建议9.1 使用evalclusters快速替代MATLAB自带evalclusters函数支持多种聚类算法和评价指标适合快速试跑。但它对K-means的实现并不算特别高效而且轮廓系数的计算还是全量计算大数据场景一样会卡。我的建议是探索阶段用evalclusters快速试探最终版本用本文手写代码方便修改细节和扩展逻辑。9.2 K值选取的系统性方法除了跑指标曲线选拐点还有几个补充方法值得组合使用手肘法画SSE-K曲线找下降速率突变点Gap Statistic与随机参考数据对比寻找Gap最大的K层次聚类辅助先跑层次聚类底部若干层观察合并距离跳变业务先验业务方如果坚持“三类客户”强行跑五类没有意义这些方法给的是一个候选K值集合最终决策一定要和业务可解释性对齐。9.3 从K-means迁移到K-medoids与模糊聚类K-means对离群点敏感因为均值会被极端值拉偏。如果数据里的离群点无法剔除可以考虑K-medoids中心是实际样本点而不是均值MATLAB有kmedoids函数可以直接用。如果数据类别间存在重叠模糊C均值FCM可以给出样本隶属度而不是硬分配。这套评价代码稍作修改就能迁移到K-medoids上因为输出格式都是“样本-簇编号”评价函数完全复用。我个人在实际项目中遇到业务数据复杂的情况通常先用K-means跑一版快速结果再用K-medoids验证核心聚类是否稳定。两套结果做对比比直接堆算法更有说服力。10. 一点个人心得反复调K-means和跑评价指标之后我的最深体会是聚类分析里最难的不是算法实现而是“如何判断结果可靠”。算法原理五分钟能讲完但让不同维度、不同量纲、不同分布的混杂数据跑出一个业务方认可的结果需要大量实验和交叉验证。这套代码本身不复杂但它把“聚类结果评价”这件事从主观感受变成了可量化、可对比、可复现的流程。当初做客户分群时就是靠轮廓系数加ARI的组合让业务方理解了为什么分成三类比分成五类更有说服力。之后每次分析任务涉及分群这套代码都是我的首选基础工具。如果你正在学聚类算法建议不要满足于调现成函数亲手把K-means和各个评价指标写一遍。写的过程中你会理解初始化为什么重要、距离度量为什么会扭曲结果、指标为什么会有偏颇。这些理解是任何现成工具都给不了你的。

相关新闻

用HTML动画演示二极管工作原理:PN结与伏安特性可视化

用HTML动画演示二极管工作原理:PN结与伏安特性可视化

前阵子有朋友跟我抱怨,说“二极管工作原理”这一章在课本上翻来覆去就那几页,老师PPT里也画了PN结、画了耗尽层,上课听的时候觉得懂了,一做题就懵,尤其是“为什么正向导通、反向截止”这件事,脑子里始终没有…

2026/10/18 4:20:29 阅读更多 →
Vue3 + Webpack5 + Element Plus 环境搭建避坑指南

Vue3 + Webpack5 + Element Plus 环境搭建避坑指南

简介:这是一份基于 webpack5 搭建 Vue3 开发与生产环境的实战示例工程,面向希望熟悉 webpack5 新特性、掌握前端构建优化的初中级开发者。资源以 JavaScript 为主,配合 Vue 单文件组件与样式文件,完整演示了从开发调试到生产打包的…

2026/10/18 4:20:29 阅读更多 →
OpenClaw语音控制实战:多用户语音识别与声纹认证的权限隔离方案

OpenClaw语音控制实战:多用户语音识别与声纹认证的权限隔离方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 4:19:29 阅读更多 →

最新新闻

RT-DETR+CEM:雾天目标检测与多模态融合实践

RT-DETR+CEM:雾天目标检测与多模态融合实践

雾天检测是个老问题,但大部分人做的是“前置去雾检测”或者“加个数据增强硬扛”。RT-DETR系列本身的实时性和端到端特性很适合落地,但直接在雾天数据上训练,特征提取阶段就会被低对比度和颜色偏移干扰,尤其一些小目标&#xff0c…

2026/10/18 5:18:39 阅读更多 →
《Eigen入门》第 15 章 · 现代 C++ 特性

《Eigen入门》第 15 章 · 现代 C++ 特性

C 从 2011 年开始进入"现代 C"时代(C11、14、17…),新增了许多让代码更简洁、更安全、更高效的特性。本章讲几个最常用的:auto、范围 for、lambda、nullptr 等。15.1 auto:自动推导类型 写类型名很繁琐时&am…

2026/10/18 5:18:39 阅读更多 →
从零自研图表设计器:数据模型、Canvas渲染与性能优化实践

从零自研图表设计器:数据模型、Canvas渲染与性能优化实践

最近在重构一个内部用的图表设计模块,项目代号就叫 diagram-design。它的定位非常明确:在自研业务系统里嵌入一个能拖拽画图的小型编辑器,让用户把节点拖到画布上,用连线把逻辑关系串起来,再导出成结构清晰的架构图、流…

2026/10/18 5:18:39 阅读更多 →
工程咨询 GEO 实战:让 AI 在项目前期调研中采信你的方案观点

工程咨询 GEO 实战:让 AI 在项目前期调研中采信你的方案观点

工程咨询 GEO 的核心挑战在于场景复杂、术语密集、案例涉密。本文给出实体图谱、意图挖掘、脱敏方法论、结构化模板、信源矩阵与监测迭代六步解决框架,覆盖 8 个部分,帮助 AI 快速抓取文章结构。1. 工程咨询 GEO 的难点:为什么传统 SEO 思路在…

2026/10/18 5:18:39 阅读更多 →
短视频怎么做 GEO:优化字幕和文稿,提升 AI 文本抓取采信率

短视频怎么做 GEO:优化字幕和文稿,提升 AI 文本抓取采信率

1. 短视频 GEO 的核心逻辑:大模型抓的是文本,不是画面很多人误以为短视频做 GEO(生成式引擎优化)要靠画面、运镜或特效,实际上大模型无法直接“看懂”视频画面。AI 在回答用户问题时,抓取的是视频的字幕、简…

2026/10/18 5:18:39 阅读更多 →
Cursor+StageWise分阶段提示法:用主动提示词重做定价卡片

Cursor+StageWise分阶段提示法:用主动提示词重做定价卡片

1. 这套组合到底在折腾什么定价卡片这个东西,做过前端或者产品页的人都不陌生。三列布局、中间那列高亮、顶部挂个“推荐”角标、底下罗列功能清单、最下面放个行动按钮——这套模板在SaaS落地页、会员订阅页、工具类产品的付费墙里被用了几百万遍。问题是&#xff…

2026/10/18 5:17:39 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:55:02 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →