☰
四种聚类算法源代码对比:Kmeans、FCM、层次与密度聚类的目标函数差异
2026/10/29 23:06:55 网站建设 项目流程

简介:面向机器学习初学者与需要快速上手聚类实验的开发者,这份资源提供FCM模糊C均值、K-means、Kmedoid等经典聚类算法的MATLAB实现及配套示例,帮助读者在无监督学习场景中完成数据分类与分析,并借助FuzzyClusteringToolbox工具箱简化代码编写。压缩包共74个文件、约2.05MB,以40个m脚本为核心算法与示例,辅以gif、png演示图、txt说明、htm与css页面及mat数据文件,另附PDF工具箱文档,便于对照理解算法流程与结果可视化。已有4888人学习下载,说明其在聚类入门与教学实践中具有一定参考价值。读者可从中获得可直接运行的算法源码、归一化与反归一化、聚类有效性评估等辅助函数,以及投影、密度估计等演示案例,通过对比不同算法的聚类效果理解各自适用场景与参数调整思路。

1. 聚类算法:为什么“同一份数据跑出四种结果”不是玄学

把同一份二维点集丢进四个不同的聚类函数,你会得到四张颜色分布完全不同的图。Kmeans 把点切成几块规整的凸区域,FCM 让每个点同时属于多个簇、只是隶属度不同,层次聚类吐出一棵可以任意横切的树,而基于密度的算法干脆把一部分点标成噪声。很多开发者第一次看到这种差异,第一反应是“参数没调好”,但真相是:这四种算法优化的目标函数根本不同,它们对“什么算一个簇”的定义就不一样。搞懂这件事的价值在于,你不再靠试参数碰运气,而是能从数据分布反推该用哪种目标函数。这份四种聚类算法源代码及示例代码,正好把 Kmeans、FCM、层次聚类和密度聚类的核心迭代逻辑摊开在同一套 MATLAB 工程里,配合神经网络场景下的特征分组示例,让“为什么这么分”变成可以逐行验证的机制问题。

2. Kmeans 与 FCM:硬划分和软隶属度的目标函数差在哪

2.1 Kmeans 的交替优化:为什么它一定会收敛

Kmeans 的核心不是“找中心”,而是交替求解两个子问题。给定簇中心,每个点归给最近中心;给定归属,每个簇中心更新为成员均值。这两步各自都在降低同一个目标函数——簇内平方误差和:

% Kmeans 单轮迭代的核心逻辑(示意) for iter = 1:maxIter % E 步:按欧氏距离把每个样本分配给最近中心 dist = pdist2(X, centers); % X: N×D, centers: K×D [~, labels] = min(dist, [], 2); % 每个样本的硬标签 % M 步:用成员均值更新中心 for k = 1:K if any(labels == k) centers(k, :) = mean(X(labels == k, :), 1); end end % 目标函数单调不增,变化小于阈值即停 J(iter) = sum(min(pdist2(X, centers), [], 2).^2); if iter > 1 && abs(J(iter) - J(iter-1)) < tol break; end end

pdist2一次算出 N×K 的距离矩阵,min(...,[],2)取每行最小值得到硬标签,这是 E 步;mean按标签分组求均值,这是 M 步。目标函数 J 在两步中都不会上升,所以有限步内必然停在某个局部极小。注意any(labels == k)这个判断:某个簇被清空时不能直接求均值,否则会得到 NaN,常见的实现思路是保留旧中心或重新初始化该中心。收敛阈值tol控制的是 J 的相对变化,不是中心位移,两者在数据尺度差异大时表现不同。

2.2 FCM 的隶属度矩阵:软划分多出来的那层自由度

FCM 把“属于/不属于”换成了隶属度 u_ik ∈ [0,1],目标函数多了模糊因子 m:

J = Σ_i Σ_k u_ik^m · ||x_i − c_k||²,约束 Σ_k u_ik = 1

m 越大,隶属度越“糊”,通常取 2。迭代公式由拉格朗日乘子法推出:

% FCM 隶属度与中心更新(示意) m = 2; % 模糊因子,经验值 1.5~2.5 U = rand(N, K); U = U ./ sum(U, 2); % 初始化隶属度,每行和为 1 for iter = 1:maxIter % 中心更新:隶属度的 m 次方作为权重 Um = U .^ m; centers = (Um' * X) ./ sum(Um, 1)'; % K×D % 隶属度更新:距离越近权重越大 dist = pdist2(X, centers).^2; dist(dist == 0) = eps; % 防止除零 ratio = dist .^ (-1/(m-1)); U_new = ratio ./ sum(ratio, 2); % 每行归一化 if max(abs(U_new(:) - U(:))) < tol break; end U = U_new; end

Um' * X是加权求和,权重是隶属度的 m 次方,分母sum(Um,1)是每个簇的总权重,这一步等价于加权均值。隶属度更新里dist.^(-1/(m-1))是关键:距离越小,倒数越大,隶属度越高;sum(ratio,2)做行归一化保证约束成立。dist == 0的处理不能省,样本恰好落在中心上时会出现 Inf,用eps兜底是常见做法。FCM 比 Kmeans 多出的计算量主要在这两次 N×K 矩阵运算上,K 较大时内存占用要留意。

2.3 两者在神经网络特征分组中的实际差别

在神经网络场景里,这两者的用途不同。Kmeans 适合对全连接层输出做硬性聚类,比如把相似激活模式的样本归为一组做分层采样;FCM 适合处理边界模糊的特征,比如某个样本同时具备两类纹理特征时,隶属度向量本身就是一种软标签,可以直接喂给后续的分类器做多任务学习。示例代码里对同一组特征分别跑两种算法,能直观看到 FCM 的隶属度矩阵在簇边界处呈现渐变,而 Kmeans 的标签在边界处是突变的。这个差别不是精度问题,是信息保留方式的问题。

3. 层次聚类与密度聚类:不预设 K 的两种思路

3.1 层次聚类的距离矩阵与合并策略

层次聚类不优化全局目标函数,而是维护一个距离矩阵,反复合并最近的两个簇。核心数据结构是 N×N 的距离矩阵和合并记录:

% 凝聚层次聚类:单链接合并(示意) D = pdist(X); % 压缩距离向量 Z = linkage(D, 'single'); % Z: (N-1)×3,每行记录合并的两个簇和距离 labels = cluster(Z, 'maxclust', K); % 按最大簇数横切树

linkage的第二个参数决定簇间距离的定义:single取两簇最近点距离,容易产生链式效应;complete取最远点距离,倾向紧凑簇;average取平均距离,折中。cluster的maxclust是在树上横切,切的位置不同结果不同,这正是层次聚类“不预设 K”的含义——它给出整棵树,K 是事后选的。示例代码里用dendrogram画出树状图,能清楚看到合并顺序和距离跳变,距离跳变大的位置往往就是合理的切分点。

3.2 密度聚类的核心点、边界点与噪声判定

密度聚类(DBSCAN 思路)用两个参数定义簇:邻域半径 eps 和最小点数 MinPts。每个点按邻域内点数分为核心点、边界点和噪声:

% DBSCAN 判定逻辑(示意) neighbors = rangesearch(X, X, eps); % 每个点的 eps 邻域索引 isCore = cellfun(@numel, neighbors) >= MinPts; labels = zeros(N, 1); % 0 表示未访问 clusterId = 0; for i = 1:N if labels(i) ~= 0 || ~isCore(i), continue; end clusterId = clusterId + 1; queue = i; labels(i) = clusterId; while ~isempty(queue) p = queue(1); queue(1) = []; if ~isCore(p), continue; end % 边界点不扩展 nb = neighbors{p}; for q = nb if labels(q) == 0 labels(q) = clusterId; queue(end+1) = q; % 核心点入队继续扩展 end end end end % labels 仍为 0 的点即噪声

rangesearch返回每个点的邻域索引,cellfun(@numel,...)统计邻域点数判断核心点。扩展时只有核心点入队,边界点只被标记不扩展,这是密度可达的定义决定的。labels为 0 的点就是噪声,这个“拒绝分类”的能力是 Kmeans 和 FCM 都没有的。eps 和 MinPts 的选取对结果影响极大,示例代码里用 k-距离曲线辅助选 eps,即对每个点算第 MinPts 近邻距离并排序,拐点附近就是合理值。

3.3 四种算法在同一份数据上的行为对照

算法需要预设 K簇形状假设对噪声输出
Kmeans是凸、球形敏感,噪声拉偏中心硬标签
FCM是凸、球形较敏感,隶属度被稀释隶属度矩阵
层次聚类否(事后切)依赖链接策略单链接敏感树 + 标签
密度聚类否任意形状显式标噪声标签 + 噪声标记

这张表不是让你背,而是让你在拿到数据时先问:簇是不是凸的、有没有噪声、K 知不知道。三个问题答完,算法基本就定了。示例代码把四种结果画在同一张画布上,对照看比读十遍定义都管用。

4. 避坑与排查:聚类代码跑出怪结果时先看这几处

4.1 现象:Kmeans 某个簇为空,中心变成 NaN

原因通常是初始化中心太集中,或者数据里有重复点导致多个中心重合。解决方法是初始化时用 k-means++ 思路做距离加权采样,或者在 M 步检测空簇并重新分配一个离现有中心最远的样本作为新中心。示例代码里保留了空簇判断分支,跑之前先确认这个分支没被注释掉。

4.2 现象:FCM 隶属度全是 0.5 附近,分不开

模糊因子 m 取得太大,隶属度被过度平滑,所有点对每个簇的归属趋于均匀。把 m 从 3 降到 1.5~2 之间再试。另一个可能是初始隶属度矩阵用rand生成后没有按行归一化,导致约束从一开始就不满足,迭代过程在错误的可行域里打转。

4.3 现象:层次聚类树状图底部挤成一团,切不出合理 K

距离矩阵没有做标准化,某个量纲大的特征主导了所有距离。在pdist之前对 X 做 z-score 标准化,让每个特征方差为 1。另外单链接在长条形分布上会产生链式合并,树状图呈现阶梯状,换成average或complete再看。

4.4 现象:密度聚类把所有点标成噪声

eps 太小或 MinPts 太大,没有任何点满足核心点条件。先用 k-距离曲线找拐点,把 eps 设在拐点略大的位置,MinPts 一般取特征维度的 2 倍左右起步。数据维度很高时距离度量本身失效,密度聚类不适合直接上高维特征,先降维再跑。

4.5 现象:四种算法结果差异巨大,不知道信哪个

这不是 bug,是目标函数不同。判断标准不是“哪个看起来整齐”,而是回到业务:如果簇的物理含义是凸区域,Kmeans 的硬标签更可解释;如果样本天然有重叠归属,FCM 的隶属度更有信息量;如果 K 未知且有噪声,密度聚类更稳。示例代码里每个算法都输出了目标函数值或合并距离,用这些内部指标辅助判断,比只看颜色分布靠谱。

5. 设计取舍:为什么这份代码把四种算法放在一起而不是各写各的

单独实现一个 Kmeans 不难,难的是让四种算法共享同一套数据预处理、同一套评估口径、同一套可视化入口。这份代码的结构选择本身就值得琢磨:它把距离计算、标准化、结果评估抽成公共函数,每个算法只保留自己的迭代核心。这样做的代价是每个算法文件多了一层函数调用,好处是你改一处标准化逻辑,四种算法的输入同时对齐,对照实验才有意义。常见的实现思路是把pdist2和 z-score 封装成prepareData,算法文件只接收处理好的 X,这样跑对照时不会因为某个算法忘了标准化而得出错误结论。

另一个取舍是示例代码选择了 MATLAB 而不是别的语言。MATLAB 的矩阵运算语法让 FCM 的隶属度更新、层次聚类的距离矩阵操作几乎和公式一一对应,读代码等于读公式,这对理解机制比工程效率更重要。神经网络部分的示例用特征矩阵直接喂给聚类函数,没有引入额外的深度学习框架依赖,保证了整个工程可以独立跑通。

验证这套代码是否真的理解了,我一般会做一件事:手动构造一个包含两个明显凸簇加一圈噪声点的二维数据集,先跑 Kmeans 看噪声如何拉偏中心,再跑密度聚类看噪声如何被标出,最后跑 FCM 看边界点的隶属度分布。三张图放在一起,目标函数的差异就变成了肉眼可见的东西。从那以后我每次选聚类算法之前,都会先画这张对照图再动手,而不是凭直觉挑一个。希望帮到你。

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

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

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

立即咨询