简介:面向机器学习入门者与需要快速上手聚类实验的MATLAB用户,这套代码合集围绕无监督分类任务整理了FCM模糊C均值、K-means、Kmedoid(Kmedia)以及GK、GG等聚类方法的实现,并配有可直接运行的演示脚本与FuzzyClusteringToolbox工具箱说明。压缩包共74个文件、约2.05MB,主体为40个.m函数脚本,涵盖聚类主程序、归一化与反归一化、聚类有效性评价(validity.m、clusteval.m)以及PCA降维、Sammon投影等辅助模块;此外还有1份PDF工具箱文档、1个.mat数据文件、6个txt说明和若干gif、png图示,方便对照理解算法流程与结果展示。资源已有4888人学习下载,读者可据此完成数据标准化、参数调试、结果可视化以及轮廓系数等指标对比,在FCM与K-means的适用边界、非球形分布数据处理上形成直观认识,也便于二次修改为自己的课程实验或项目代码。
1. 聚类没有标准答案:为什么同一份数据四种算法会给出四张图
同一份二维数据,交给 K-means 会切出几块边界笔直的凸区域,交给 FCM 会得到一片隶属度渐变的“雾”,交给 SOM 这类神经网络会得到一条自己蜷起来的拓扑线,交给层次聚类会长出一棵可以任意剪枝的树。四张图放在一起,没有哪张是错的。这不是玄学,而是聚类这件事本身就没有唯一解——没有标签,没有可以对标真值的损失,所谓“簇”完全取决于你用哪种相似性去切割空间。搞懂这套 MATLAB 聚类代码的价值,不在于多记几个函数名,而在于能从结果的形状反推出算法在优化什么,从而判断什么时候该信它、什么时候它已经在胡说。神经网络那一支最容易被当成黑匣子,其实它的每一步权值更新都能写成显式公式。
2. K-means:质心交替为什么必然收敛,又为什么必然停在局部最优
2.1 目标函数:把“簇内紧密”写成一个能求闭式解的量
K-means 的全部行为都由一个目标函数决定:
J = Σ(k=1..K) Σ(x∈S_k) || x - c_k ||²其中 S_k 是第 k 个簇的样本集合,c_k 是它的质心。这个式子只关心一件事:每个点到它所属质心的欧氏距离平方和。它同时隐含了两个很强的假设,簇是凸的、各向同性的球状,且各簇规模大致相当。后面所有“K-means 效果不好”的抱怨,几乎都能追溯到这两条假设被违反,而不是代码写错了。
写成指示变量形式更便于推导。令 r_nk ∈ {0,1} 表示第 n 个样本是否属于第 k 个簇,且满足约束 Σ(k) r_nk = 1,则
J = Σ(n) Σ(k) r_nk || x_n - c_k ||²这就是所谓的硬划分:每个样本只能归一个簇,隶属度非 0 即 1。FCM 后面要做的事,本质就是把这个 0/1 松成 [0,1] 区间上的连续值,其余结构几乎不动。
2.2 两步交替:固定一边,另一边就有闭式解
J 同时依赖 r 和 c,联合优化属于组合优化,也被称为 k-means 问题,精确求解是 NP-hard 难。但固定其中一个变量时,另一个的最优解有解析形式,这正是交替优化的切入点。
固定 c 求 r:每个样本相互独立,最小化 ||x_n - c_k||² 显然是选最近的质心,即 r_nk = 1 当且仅当 k = argmin_j ||x_n - c_j||²。这一步习惯上叫分配步。
固定 r 求 c:对 c_k 求偏导并令其为零,∂J/∂c_k = -2 Σ(n) r_nk (x_n - c_k) = 0,解得 c_k = Σ(n) r_nk x_n / Σ(n) r_nk,也就是簇内均值。这一步叫更新步。
注意“均值”不是随便挑的统计量,它是平方误差准则下的精确最优解。如果把准则换成绝对误差,最优解会变成中位数,那就是 K-medians 而不是 K-means 了。目标函数的形式直接决定了更新公式长什么样,这条因果链在后面 FCM 里会以同样的方式再出现一次。
2.3 单调性:为什么每次迭代 J 都不会上升
两次更新都是对 J 的精确最小化,各自在固定另一个变量的前提下完成,所以整轮迭代下来 J 单调不增。再补一句,J 有下界 0,而划分方案总数虽然是个天文数字(K^N 量级)但终究有限,因此不可能无限次严格下降,必然在有限步内停在某个 J 不再变化的点上。
这段推理是 K-means 收敛性的全部骨架,没有任何额外假设。但有一个极其容易被误读的地方:收敛不等于收敛到全局最优。坐标下降只会落到某个局部极小,落在哪个坑里完全由初始质心决定。教科书里最经典的构造是三个真实簇,而两个初始质心偏偏落在同一个真簇内部,那么第三个真簇就永远分不到质心——它只能从已有的两个质心里抢一部分样本过来,最后的结果无论迭代多久都回不去。
实际运行时这个现象表现为“J 一直在某个值附近不动,但肉眼看图明显分错了”。这不是实现 bug,是算法性质。
2.4 初始化:K-means++ 到底在改什么
最朴素的初始化是从样本里随机抽 K 个点当初始质心。期望上这会把多个质心浪费在同一个密集区域,直接加剧上面说的局部极小问题。
K-means++ 的思路是让初始质心彼此尽量远:第一个质心随机选,之后每个新质心按“与已选质心最近距离的平方”成正比的概率采样。那个平方不是随手写的,它让远处点被选中的概率显著高于均匀采样,同时保留随机性避免退化,概率上能给出 O(log K) 的近似保证。
工程上还有一层更实用的保险:跑多次独立初始化,取 J 最小的那一次。MATLAB 的 kmeans 里对应 'Replicates' 参数,默认值很小,实际用的时候按数据规模调到 10 到 50 之间往往能明显压低结果方差。这个参数的作用是缓解初始化敏感性,不是提高精度——它让“这次运气不好”的概率指数级下降,但依然不保证全局最优。
2.5 一个可以逐行对照的实现
下面这段实现把分配步、更新步、空簇处理和终止条件都摊开写,方便对照上面的推导。
function [idx, C, Jhist] = myKmeans(X, K, maxIter, tol) % X: N x D,每行一个样本;K: 簇数 % idx: N x 1 簇标号;C: K x D 质心;Jhist: 每轮目标函数值 [N, D] = size(X); % ---- K-means++ 初始化 ---- C = zeros(K, D); C(1, :) = X(randi(N), :); d2 = sum((X - C(1, :)).^2, 2); % 每个点到最近已选质心的平方距离 for k = 2:K if sum(d2) == 0 % 所有点重合的极端情况 C(k, :) = X(randi(N), :); else p = d2 / sum(d2); % 按 D^2 采样 C(k, :) = X(find(rand <= cumsum(p), 1), :); end d2 = min(d2, sum((X - C(k, :)).^2, 2)); % 只保留到最近质心的距离 end Jhist = zeros(maxIter, 1); for it = 1:maxIter % 分配步:E 步 Dist = pdist2(X, C).^2; % N x K 的平方距离矩阵 [dmin, idx] = min(Dist, [], 2); % 每个样本取最近质心 % 更新步:M 步 Cnew = C; for k = 1:K members = (idx == k); if any(members) % 空簇时保留旧质心,避免 NaN Cnew(k, :) = mean(X(members, :), 1); end end Jhist(it) = sum(dmin); % 当前划分的平方误差和 if max(abs(Cnew(:) - C(:))) < tol % 质心位移小于阈值即停 C = Cnew; break; end C = Cnew; end几个参数和细节值得单独说。pdist2(X, C) 返回 N×K 矩阵,第 n 行第 k 列是样本 n 到质心 k 的欧氏距离,平方之后正好是目标函数里的那一项;如果换成手写双重循环,思路一样但慢一个数量级。tol 判的是质心最大坐标位移,取 1e-4 到 1e-6 都常见,它只影响停机时机不影响最终落点。空簇那段 if any(members) 是必须的,否则 mean 返回 NaN,NaN 会顺着质心传播到距离矩阵,最后整个结果全变 NaN 且不报错——这是 MATLAB 里很典型的一类静默失败。
Jhist 除了用来看收敛曲线,还有一个实用价值:多次重启时直接比较 Jhist 末尾的值,选最小的那组,不用再跑一遍评估。
2.6 什么时候不该用它
把前面几条串起来,K-means 的边界其实很清楚。第一,K 必须事先给,它不会自己判断有几个簇。第二,它只擅长球状、规模相近、方差接近的簇,遇到月牙形、环形或者密度差异大的数据必然翻车,因为目标函数里的欧氏距离只能表达“到中心有多远”。第三,它对特征尺度极其敏感,一个量纲是万元、一个是百分比的字段混在一起,距离几乎完全由前者决定,所以标准化几乎是必做的前置步骤,而不是可选项。
这三点不是缺憾,是目标函数的直接推论。理解到这一层,就不会再试图用调参去救一个本来就不适配的数据形状。
3. FCM:隶属度为什么要取 m 次幂,一次拉格朗日乘子推导
3.1 从硬到软:把 0/1 松成区间上的连续值
K-means 的硬划分有个现实问题:处于两个簇边界上的样本,被强行判给其中一个,误差没有任何缓冲。FCM(模糊 C 均值)的做法是把 r_nk 换成 u_nk ∈ [0,1],表示样本 n 对第 k 个簇的隶属度,约束从“恰好属于一个”放宽为“各簇隶属度之和为 1”。
目标函数相应变成
J_m = Σ(n) Σ(k) u_nk^m · || x_n - c_k ||², m > 1 约束:Σ(k) u_nk = 1,对所有 n那个 m 叫模糊因子或加权指数。它的存在不是为了好看,而是为了让 u 的更新有唯一解——如果写成 u_nk 的一次方,对 u 求导得到的是线性方程,在约束下最优解会退化成硬划分,也就是回到 K-means。指数 m 是让目标函数对 u 严格凸、从而保证内点解的关键。
3.2 拉格朗日函数:带约束的最小化怎么落地
每个样本的隶属度要满足和为 1,这是 N 个独立等式约束。用拉格朗日乘子法,为每个样本引入一个 λ_n:
L = Σ(n) Σ(k) u_nk^m d_nk² - Σ(n) λ_n ( Σ(k) u_nk - 1 )其中 d_nk² = ||x_n - c_k||² 是简写。注意这里只对 u 加了约束,对 c 没有约束,所以 c 仍然可以直接求偏导。这种“一半带约束、一半不带”的结构,是 FCM 推导里最容易被写乱的地方。
3.3 求偏导:隶属度和质心的更新公式怎么来的
先对 u_nk 求偏导并令其为零:
∂L/∂u_nk = m · u_nk^(m-1) · d_nk² - λ_n = 0 => u_nk = ( λ_n / (m · d_nk²) )^(1/(m-1))把这个表达式代回约束 Σ(k) u_nk = 1,可以解出乘子:
( λ_n / m )^(1/(m-1)) · Σ(k) d_nk^(-2/(m-1)) = 1于是
u_nk = 1 / Σ(j) ( d_nk / d_jk )^(2/(m-1))这个形式很有信息量。它说明隶属度只取决于样本到各质心距离的相对比值,而不是绝对距离——把所有距离同比例放大,隶属度不变。这也解释了为什么 FCM 对特征的整体缩放不敏感,但对各维度的相对尺度依然敏感。
再对 c_k 求偏导:∂L/∂c_k = -2 Σ(n) u_nk^m (x_n - c_k) = 0,解得
c_k = Σ(n) u_nk^m · x_n / Σ(n) u_nk^m这就是加权均值,权重是隶属度的 m 次幂。和 K-means 的均值公式相比,只多了这个权重——K-means 相当于每个样本权重非 0 即 1。
3.4 m 的取值:它到底在调节什么
从 u_nk 的公式能直接读出 m 的两个极限行为。
当 m → 1 时,指数 2/(m-1) → ∞,距离稍大的那一项比值被放大到无穷,隶属度退化为 1 给最近质心、0 给其余,完全回到 K-means。当 m → ∞ 时,指数趋于 0,所有比值的 0 次幂都是 1,u_nk → 1/K,隶属度完全均匀,聚类信息全部丢失。
所以 m 是在“硬”和“糊”之间滑动的一根轴。工程上常用 1.5 到 2.5,m = 2 是最常见的默认值,因为此时指数 2/(m-1) = 2,公式最简洁。m 偏小的结果是边界清晰但接近硬划分,m 偏大的结果是隶属度普遍接近 1/K,看起来“每个簇都沾一点”,实际已经失去判别力。
判断 m 是否取大了有个很直接的办法:看隶属度矩阵的行最大值分布。如果大量样本的行最大值都落在 0.3 到 0.4 区间,说明模糊得过头了;正常情况应该呈现明显的双峰——大部分样本行最大值接近 1,少部分边界样本在 0.5 附近。
3.5 实现:矩阵化写法和除零保护
function [U, C, Jhist] = myFCM(X, K, m, maxIter, tol) % X: N x D;K: 簇数;m: 模糊因子,常用 2 [N, ~] = size(X); % 初始化隶属度:随机后按行归一化,保证 Σ_k u_nk = 1 U = rand(N, K); U = U ./ sum(U, 2); Jhist = zeros(maxIter, 1); for it = 1:maxIter Um = U .^ m; % 质心:以 u^m 为权重的加权均值,K x D C = (Um' * X) ./ sum(Um, 1)'; % 平方距离矩阵 N x K D2 = pdist2(X, C).^2; D2(D2 < 1e-12) = 1e-12; % 防止样本与质心重合导致除零 % 隶属度更新:比值形式的直接实现 ratio = D2 .^ (-1 / (m - 1)); Unew = ratio ./ sum(ratio, 2); % 按行归一化 Jhist(it) = sum(sum((Unew .^ m) .* D2)); if max(abs(Unew(:) - U(:))) < tol % 用隶属度变化判收敛 U = Unew; break; end U = Unew; end代码里有几处和公式的对应关系需要点明。sum(Um, 1)' 得到 K×1 的权重和,MATLAB 会自动沿列方向广播到 K×D 的分子上,这一步省掉了显式循环。D2 .^ (-1/(m-1)) 就是 (1/d²)^(1/(m-1)),与推导里的比值形式等价,因为分子上的常数在按行归一化时会被约掉。收敛判据用的是隶属度矩阵的最大变化量而不是质心位移,原因是 FCM 的质心可能变化很小但隶属度仍在缓慢重分布,用 U 判更贴近实际收敛状态。
那句 D2(D2 < 1e-12) = 1e-12 不是防御性编程的客套话。当某个样本恰好等于某个质心时,d = 0,取负幂直接得 Inf,接着按行归一化会出现 Inf/Inf = NaN,NaN 再进入下一轮质心计算,整个矩阵就废了。而这种情况在真实数据里有重复样本时非常常见。
3.6 FCM 给出的到底是什么
很多使用场景把 FCM 的输出直接 argmax 成硬标签,然后拿去和 K-means 比准确率,这其实浪费了它最大的价值。FCM 真正有用的输出是隶属度矩阵本身:它可以告诉你某个样本有多“犹豫”,这在医学影像分割、遥感地物边界、软测量这类需要量化不确定性的场景里才是关键。把它当 K-means 用,除了多花几倍计算量,换不来任何东西。
另外值得一提的是,FCM 的目标函数依然基于欧氏距离,所以 K-means 的球形假设它一个都没逃掉,只是边界变软了。数据形状本身不适合球状簇时,换 FCM 并不会救回来。
4. 不靠质心的两条路:SOM 的邻域保序与层次聚类的合并准则
4.1 SOM:为什么胜者周围的神经元也会被拽过去
K-means 和 FCM 都在优化“样本到质心”的距离,质心之间没有任何关系,簇的编号可以任意置换。神经网络这一支的代表 SOM(自组织映射)引入了完全不同的机制:神经元被固定排列在一个低维网格上,网格上的相邻关系本身就是一种约束。
每个神经元持有一个和样本同维的权值向量 w_i。训练时对每个输入样本 x 做两件事。第一是竞争:找出权值离 x 最近的神经元,称为获胜神经元 c,这一步和 K-means 的分配步一模一样。第二是合作:不只是更新获胜者,还要按距离衰减的方式更新它网格上的邻居。
更新式是
w_i(t+1) = w_i(t) + η(t) · h_ci(t) · ( x - w_i(t) ) h_ci(t) = exp( - || r_c - r_i ||² / ( 2σ(t)² ) )其中 r_i 是神经元在网格上的坐标,η 是学习率,σ 是邻域半径。h_ci 是高斯型邻域函数,获胜者自己 h = 1,隔得越远衰减越快。
这个“邻居一起拽”的动作,就是 SOM 和 K-means 的全部差别,也是它拓扑保序能力的来源。因为相邻神经元被同一批样本反复拉近,训练完成后网格上相邻的神经元在数据空间里也倾向于相近,高维数据被铺到了一张可以肉眼看的二维图上。这是 K-means 做不到的——它的簇之间没有邻接概念。
4.2 衰减时间表:η 和 σ 为什么必须随迭代缩小
如果 η 和 σ 固定不变,网络会一直处于剧烈调整状态,永远停不下来,最后权值在几个解之间来回振荡。所以两者都必须随迭代衰减。
σ 的衰减尤其关键。训练初期 σ 接近网格尺寸,获胜者的更新会波及大半个网格,这相当于全局粗调,让整个映射先大致铺开。随着 σ 缩小,更新范围收缩到局部,进入精调阶段。这个从粗到细的过程如果顺序反了——一开始就用小 σ——结果是各个神经元各自为政,网格上的邻接关系完全丢失,输出的拓扑图就是一张没有结构的散点。
η 的衰减相对宽松,但同样不能省。常见做法是指数衰减 η(t) = η_0 · exp(-t/τ),τ 取总迭代数的四分之一左右,让学习率在前四分之一就降到初值的 37%,之后缓慢收尾。
4.3 实现:把竞争与合作分开写
function [W, label] = mySOM(X, gridSize, maxEpoch) % X: N x D;gridSize: 网格边长,神经元总数 = gridSize^2 [N, D] = size(X); % 神经元网格坐标,用于计算邻域距离 [gx, gy] = meshgrid(1:gridSize, 1:gridSize); gridPos = [gx(:), gy(:)]; nNeuron = size(gridPos, 1); % 权值初始化在数据均值附近的小邻域内,比纯随机收敛快得多 W = randn(nNeuron, D) * 0.01 + mean(X, 1); sigma0 = gridSize / 2; % 初始邻域半径约覆盖半个网格 eta0 = 0.5; tau = maxEpoch / 4; % 衰减时间常数 for ep = 1:maxEpoch eta = eta0 * exp(-ep / tau); sigma = sigma0 * exp(-ep / tau); for n = randperm(N) % 每个 epoch 内打乱样本顺序 x = X(n, :); d2 = sum((W - x).^2, 2); [~, c] = min(d2); % 竞争:找获胜神经元 dgrid2 = sum((gridPos - gridPos(c, :)).^2, 2); h = exp(-dgrid2 / (2 * sigma^2));% 合作:高斯邻域权重 W = W + (eta * h) .* (x - W); % 广播到所有神经元 end end % 训练结束后按最近权值给样本打标 [~, label] = min(pdist2(X, W), [], 2);代码里的广播是理解重点。(x - W) 是 nNeuron×D 的矩阵,etah 是 nNeuron×1 的列向量,MATLAB 自动沿列方向扩展,等价于对每个神经元 i 执行 w_i += etah_i*(x - w_i)。写成一个矩阵表达式而不是循环,除了快之外,也让“所有神经元同时被更新”这件事在代码层面显式可见。
randperm(N) 那层循环容易被忽略,但它有实际作用。如果不打乱、按固定顺序遍历样本,权值会带有样本顺序的偏置,尤其在数据本身按类别排列时,最后一类的样本会把映射拉偏。打乱相当于给随机梯度下降加噪声,是标准做法。
训练完的 label 是把每个样本分配给最近的神经元,再按需要把神经元映射到簇。SOM 本身给出的是降维映射,聚类是在映射之上再做的一步,这一步很多人会混淆——以为 SOM 的输出天然就是簇标号。
4.4 层次聚类:距离矩阵怎么一步步缩小
层次聚类走的是完全不同的路线:不预设 K,不迭代优化,而是先算所有样本两两之间的距离,然后反复把最近的两个簇合并,直到只剩一个。整个过程记录在一棵二叉树(树状图)里,最后按需要的高度剪枝得到任意簇数。
关键在于合并之后,新簇到其他簇的距离怎么算。暴力做法是重新遍历两个簇的所有样本对,复杂度立刻上去。Lance-Williams 公式给出了统一递推:
d(i∪j, k) = α_i·d(i,k) + α_j·d(j,k) + β·d(i,j) + γ·| d(i,k) - d(j,k) |不同连接准则只是系数不同。single linkage 取 α = 0.5、β = 0、γ = -0.5,等价于两簇间最近点距离;complete linkage 把 γ 取 +0.5,变成最远点距离;average linkage 取 γ = 0,是平均距离;Ward 准则的系数稍复杂,目标是最小化合并后簇内平方和的增量,倾向产生大小均衡的球状簇。
这个公式的意义在于,无论用哪种准则,每合并一次只需要更新一行一列距离,总复杂度 O(N² log N)(暴力版是 O(N³)),而且四种准则可以共用一套骨架代码,只换系数。
4.5 实现:pdist 和 linkage 的输入形式
% 1. 样本间距离,输出是长度为 N(N-1)/2 的向量,不是方阵 Y = pdist(X, 'euclidean'); % 2. 凝聚层次聚类,Z 是 (N-1) x 3 的合并记录 % 每行 [被合并簇A, 被合并簇B, 合并时的高度] Z = linkage(Y, 'ward'); % 3. 按目标簇数剪枝 K = 3; idx = cluster(Z, 'maxclust', K); % 4. 共表型相关系数:树对原始距离的保真度,越接近 1 越好 c = cophenet(Z, Y);pdist 返回向量而不是方阵,这是 MATLAB 里最容易出问题的地方,后面避坑一节会专门说。cophenet 那个值值得养成看的习惯,它衡量树状图里的高度和原始距离之间的一致性,低于 0.7 通常意味着连接准则选得和数据形状不匹配,换一种准则再试比继续调 K 更有意义。
层次聚类最大的优势是那个树状图本身就是结论,不需要事先决定 K,可以看完整棵树再决定在哪剪。代价是它没有“重新分配”的机会——一旦两个簇合并,后面就不可能再拆开,早期的一次错误合并没有任何后悔药可吃。
5. 四种算法的踩坑排查清单:空簇、方向、模糊因子与归一化
5.1 现象:每次运行结果都不一样,有时差别还很大
原因基本可以锁定在初始化。K-means 和 FCM 的初始质心或初始隶属度都是随机的,而目标函数又只有局部最优保证,不同的起点会落到不同的坑里。这不是代码不稳定,是算法性质。
解决方式是显式控制随机性并多次重启。MATLAB 里给 kmeans 传 'Replicates', 20 和 'Start', 'plus',取多次里 J 最小的那组;需要复现实验时在脚本开头固定随机种子。但要注意,多次重启降低的是方差,不是偏差——如果数据形状本身不适合球状簇,跑一百次也还是错的。
5.2 现象:结果矩阵里出现 NaN,或者整个簇标号全是 1
先查空簇。当 K 给得比实际结构还大,或者某次迭代中一个簇没能抢到任何样本,更新步里对空集合求均值就得到 NaN,NaN 顺着质心传播,下一轮距离全变 NaN,min 的行为随之异常。上面 K-means 实现里那句 if any(members) 就是拦这个的。
再查除零。FCM 里样本和质心完全重合时 d = 0,负幂运算得 Inf,按行归一化出 NaN。真实数据有重复行时这几乎是必然触发的,必须加下限保护。
5.3 现象:FCM 的隶属度矩阵看起来“每个簇都差不多”
打印一下每行的最大值分布。如果大量样本的行最大值集中在 1/K 附近,说明 m 取太大了,模糊得过头,隶属度已经不含判别信息。把 m 往 1.5 降,重新看分布是否呈现双峰。
反过来,如果几乎所有样本的行最大值都接近 1,说明 m 太接近 1,已经退化成硬划分,这时候用 FCM 就没有意义了,不如直接用 K-means 省算力。m 的合理区间要靠这个分布来定,而不是照抄别人论文里的值。
5.4 现象:SOM 训练完的映射图一团糊,看不出任何结构
第一个要查的是归一化。SOM 用的是欧氏距离,某一维量纲远大于其他维时,竞争结果几乎完全由那一维决定,网格根本铺不开。所有特征先做 z-score 或归一到 [0,1],这一步不是可选项。
第二个要查的是衰减时间表。如果 σ 一开始就设得很小(比如直接等于 1),各神经元独立更新,邻域合作机制形同虚设,输出的拓扑结构必然是散的。正确顺序是大 σ 起步、随迭代指数收缩。第三个要查迭代轮数,几百轮对于稍有规模的数据通常不够,权值还没稳定就停了。
5.5 现象:linkage 跑完不报错,但树状图明显不合理
先确认传给 linkage 的是 pdist 返回的距离向量,而不是 pdist2 返回的 N×N 方阵。这两个函数名只差一个字符,但输入形式完全不同,传错了 MATLAB 不会报错,它会把这个方阵当成一个长度 N² 的距离向量去解释,结果是一棵毫无意义的树。这个坑非常隐蔽,因为代码能跑通、有输出、只是结论荒谬。
再确认有没有把样本矩阵转置错。MATLAB 的聚类函数默认按行是样本、列是特征,而从某些语言或文件读进来的数据习惯是列存样本,转置一旦漏掉,聚类就变成在对特征聚类而不是对样本聚类,规模稍大时表现为内存爆掉或者结果完全没有可解释性。
提示:这五类问题的共同点是都不抛异常。聚类算法的失败几乎全是静默的,所以每次跑完都该看一眼目标函数曲线、隶属度分布或者树状图高度,而不是只看最终标号。
6. 选型时的真实权衡:没有哪个准则能通吃
把四种算法放在一起看,会发现它们的分歧不在实现细节,而在对“相似”这件事的定义上。K-means 定义相似为“到同一个中心的距离小”,FCM 定义为“到各中心的距离比值决定归属概率”,SOM 在距离之外还要求“相邻的神经元映射到相近的位置”,层次聚类则完全依赖两两距离和一条合并规则。定义不同,能表达的结构就不同,这是选型的唯一依据。
一个实用的判断顺序是这样。先问 K 知不知道:如果业务上簇数固定且样本量大,K-means 是性价比最高的起点,'Replicates' 开大一点基本能压住初始化风险。再问边界要不要软:如果下游需要的是“这个样本有多大概率属于某类”而不只是一个标号,FCM 的隶属度矩阵比硬标签信息量大得多。再问结构要不要可视化:如果目标是把高维数据摊到一张二维图上让人看懂,SOM 的拓扑保序是划分类算法给不了的。最后问要不要事后改主意:如果 K 本身是探索出来的、需要看着树状图决定切在哪,层次聚类那个不可回退的合并过程反而是优点,因为整棵树完整保留了所有中间状态。
代价也要一并算清。K-means 和 FCM 都是 O(N·K·D·迭代数),能撑到百万级样本;层次聚类是 O(N²) 的内存和 O(N² log N) 的时间,样本上到几万就必须考虑先抽样或者换用其他策略;SOM 的训练成本取决于网格大小和迭代轮数,通常是四者里最慢的。这些数量级差异不是常数因子,规模一变就决定了方案可不可行。
验证这一层,我一般会做两件事。一是在同一份数据上跑多种算法,用轮廓系数或者已知标签下的调整兰德指数横向比,看分歧出在哪里——分歧大的样本往往就是真正的边界样本,值得单独拿出来看。二是固定随机种子重跑十次,看结果的稳定性,方差大的配置直接淘汰,不管它单次跑出来的指标多好看。
血泪经验是:曾经为了省事跳过标准化直接跑 K-means,结果所有样本被一个量纲特别大的字段主导,聚出来的簇和业务完全对不上,排查了大半天才定位到一行没写的 normalization。从那以后我每次跑聚类之前都强制走一遍“检查维度方向、检查量纲、检查缺失值、固定种子”这四步,一个都不跳。希望帮到你。
本文还有配套的精品资源,点击获取