简介:面向机器学习初学者与MATLAB用户的聚类算法代码合集,围绕无监督分类任务,提供FCM模糊C均值、K-means、K-medoids等经典方法的实现与演示,适合课程实验、算法对比和项目原型验证。压缩包共74个文件,约2.05MB,以40个m脚本为核心,配合mat数据、pdf说明、txt配置及gif/png图示等,便于按算法模块查阅、运行与观察结果。已有4888人学习下载,说明其在教学与实践中具有一定参考价值。内容覆盖数据标准化与归一化、聚类有效性评估、结果可视化及FuzzyClusteringToolbox相关函数,读者可据此理解隶属度更新、聚类中心迭代与收敛判断,比较不同算法在模糊边界、非球形分布和密度不均数据上的表现,并借助示例快速搭建实验流程、调整参数和开展性能评估。
1. 聚类算法怎么选:四种实现放在一起才好对照
手上有一批无标签数据,Kmeans、FCM、层次聚类、密度聚类到底该用哪个,光看公式很难有把握,跑一遍同样的数据再看结果最直接。这份资源把四种聚类算法的 MATLAB 实现和示例代码整理在一起,包含 Kmeans、FCM 模糊聚类、层次聚类和基于神经网络的自组织映射思路,每个算法都配有可直接运行的脚本和构造好的示例数据。它适合正在做模式识别、数据挖掘课程设计、遥感或图像分割预处理,以及想把 MATLAB 聚类流程迁移到工程里的开发者。对做神经网络方向的人来说,这里也提供了用无监督方式做特征分组的参照。下面按目录结构、核心代码、跑通步骤、踩坑记录、二次开发五个层面逐层拆开,让你下载后能照着跑、照着改。
2. 目录结构与算法定位:四个脚本各自解决什么问题
拿到压缩包后,先不要在 MATLAB 里乱点。这个项目的组织方式基本属于“一个算法一个入口、一份示例数据、一份结果可视化”的朴素结构,理解了这个约定,后面读代码会快很多。它没有把东西塞进一个巨型 GUI,也没有强依赖某个工具箱的高级函数,这对二次开发是好事——你能清楚看到每一步在算什么,而不是调用一个黑箱函数就出结果。
2.1 典型目录布局与文件职责
常见做法是按算法分文件夹,根目录放一个总入口脚本,方便一次性跑完全部对比。压缩包解开后大致是下面这种形态(具体文件名可能略有出入,但职责划分基本一致):
clustering_demo/ ├── main_compare.m % 总入口:依次跑四种算法并画对比图 ├── data/ │ ├── make_blobs.m % 生成球形簇示例数据 │ ├── make_moons.m % 生成非凸(双月)示例数据 │ └── sample_real.mat % 一份真实/半真实样本,二维特征 ├── kmeans/ │ ├── my_kmeans.m % Kmeans 主实现(手写迭代,非工具箱调用) │ └── demo_kmeans.m % 单算法演示脚本 ├── fcm/ │ ├── my_fcm.m % 模糊 C 均值,输出隶属度矩阵 │ └── demo_fcm.m ├── hierarchical/ │ ├── my_hclust.m % 凝聚式层次聚类 + 树状图 │ └── demo_hclust.m ├── som_nn/ │ ├── my_som.m % 自组织映射网络(神经网络式无监督聚类) │ └── demo_som.m └── utils/ ├── plot_clusters.m % 统一的结果可视化 └── eval_index.m % 轮廓系数 / 簇内距离等简单评价这个布局里最关键的一条是:每个my_xxx.m都只做算法本身,输入是数据矩阵和参数,输出是标签(FCM 额外输出隶属度),画图和评价全部下沉到utils。我第一次拆这类代码时最怕的就是“算法里混着plot”,一旦混进去,想批量跑参数扫描就得注释掉一堆绘图语句,非常难受。这份代码在这一点上是干净的。
2.2 四种算法的适用边界
选型不看数据分布直接套算法,是后面结果不对的主要来源。这份资源把四种算法放在一起,价值就在于你能用同一份数据横向比。下面这张表是我根据代码里的实现方式整理的定位对照:
| 算法 | 簇形状假设 | 是否需要预设簇数 | 输出 | 典型适用场景 |
|---|---|---|---|---|
| Kmeans | 球形、大小相近 | 是 | 硬标签 | 样本量大、簇分离明显的数据 |
| FCM | 球形,允许边界模糊 | 是 | 隶属度矩阵 + 硬标签 | 图像分割、边界样本需要归属程度 |
| 层次聚类 | 不限,取决于链接准则 | 否(切割时定) | 树状结构 | 小样本、想看簇的合并过程 |
| SOM | 拓扑保持的网格 | 否(网格大小定) | 网格映射 + 标签 | 高维数据降维后聚类、可视化 |
Kmeans 对非凸数据(比如双月)会直接翻车,这不是实现错,是模型假设问题;层次聚类在样本量超过几千时距离矩阵会吃掉大量内存;SOM 更偏“先把高维压到二维网格再在上面对齐”,本质是神经网络式的竞争学习,不是传统意义上的划分聚类。把这几条记在心里,后面调参就不会瞎试。
3. 核心代码拆解:Kmeans 与 FCM 的迭代逻辑
这一章是整份资源最有价值的部分。工具箱里的kmeans一行就能出结果,但你要做二次开发、要改距离度量、要加约束,就必须看得懂手写迭代结构。这里的两个实现——Kmeans 和 FCM——共用同一个骨架:初始化中心 → 计算归属 → 更新中心 → 判断收敛。差别只在“归属”是硬的还是软的。
3.1 Kmeans:距离矩阵、标签分配与中心更新
先看主循环。下面这段是按项目里my_kmeans.m的结构还原的核心逻辑,注释我按自己读代码时关心的点补上:
function [labels, centers, J_history] = my_kmeans(X, K, maxIter, tol) % X: N x D 数据矩阵,每行一个样本 % K: 簇数;maxIter: 最大迭代次数;tol: 中心变化容忍度 [N, D] = size(X); % 1) 初始化:随机选 K 个样本作为初始中心(比全随机更稳) idx = randperm(N, K); centers = X(idx, :); labels = zeros(N, 1); J_history = zeros(maxIter, 1); for it = 1:maxIter % 2) 分配:计算每个样本到各中心的欧氏距离平方 % bsxfun 在新版本可换成隐式扩展,这里保留兼容写法 dist2 = zeros(N, K); for k = 1:K diff = X - centers(k, :); dist2(:, k) = sum(diff.^2, 2); end [~, labels] = min(dist2, [], 2); % 取最近中心的索引 % 3) 更新:按当前标签重算各簇均值 newCenters = centers; for k = 1:K member = (labels == k); if any(member) newCenters(k, :) = mean(X(member, :), 1); else % 空簇处理:重新随机挑一个样本当中心,避免簇塌缩 newCenters(k, :) = X(randi(N), :); end end % 4) 收敛判断:中心总位移小于 tol 就停 shift = sum(sqrt(sum((newCenters - centers).^2, 2))); J_history(it) = shift; centers = newCenters; if shift < tol J_history = J_history(1:it); break; end end end几个参数和实现细节值得说明。tol控制的是所有中心位移的绝对值之和,不是单个中心,所以簇数多的时候阈值要相应放宽,否则会多跑很多轮。randperm初始化比纯随机数初始化收敛快且不容易产生空簇,这是血泪经验——早期我用rand生成中心,在数据分布偏斜时经常出现某个簇一个样本都没有。空簇那段if any(member)的判断别删,删了在某些数据上会直接报mean对空矩阵求均值的错。
另外注意距离的写法用了双重循环而非一次矩阵乘法pdist2或X*centers'。这样写在 N 上万时会明显变慢,但胜在内存友好、每步都可断点观察。二次开发时如果只追求速度,可以替换为:
% 加速版距离计算:利用 ||x-c||^2 = ||x||^2 - 2x·c + ||c||^2 xn = sum(X.^2, 2); cn = sum(centers.^2, 2)'; dist2 = xn + cn - 2 * (X * centers');替换后结果一致,但在 N 较大时有数量级的提升。这是我一般会做的第一处改造。
3.2 FCM:隶属度矩阵与模糊因子 m
FCM 的骨架和 Kmeans 几乎一样,区别是每个样本不再只属于一个簇,而是有一个隶属度向量 U,满足每行和为 1。更新中心时用隶属度的 m 次方加权,m 越大越“模糊”。核心代码如下:
function [U, centers, obj] = my_fcm(X, K, m, maxIter, tol) % m: 模糊因子,通常取 1.5~2.5,m 趋近 1 退化为 Kmeans [N, ~] = size(X); % 初始化隶属度:随机后按行归一化 U = rand(N, K); U = U ./ sum(U, 2); for it = 1:maxIter U_old = U; % 1) 用隶属度的 m 次方作为权重更新聚类中心 Um = U .^ m; centers = (Um' * X) ./ sum(Um, 1)'; % 2) 更新隶属度:距离越小隶属度越大 dist2 = zeros(N, K); for k = 1:K diff = X - centers(k, :); dist2(:, k) = sum(diff.^2, 2) + 1e-10; % 防零除 end U = 1 ./ (dist2 .^ (1/(m-1))); U = U ./ sum(U, 2); % 按行归一化,保证和为 1 % 3) 目标函数:加权距离平方和,用于观察收敛 obj(it) = sum(sum((U .^ m) .* dist2)); if max(abs(U(:) - U_old(:))) < tol break; end end end1e-10这个加在距离上的小量不是可有可无的装饰。当某个样本恰好落在某个中心上时dist2为 0,取负幂会变成Inf,随后整行归一化全是NaN,程序表面上跑完了,结果全是垃圾,这就是典型的黑匣子式翻车。m的取值也要说清楚:取 1.1 时结果几乎和 Kmeans 一样,取 3 以上隶属度会趋近均匀、簇边界糊成一片。我一般从 2.0 起步,把obj曲线画出来看下降是否平滑。
FCM 的最大好处是输出 U,你可以据此判断边界样本的归属确定程度。比如图像分割里,隶属度介于 0.4~0.6 的像素就是边缘区,这种信息 Kmeans 给不了。拿到U之后转硬标签只要一行:
[~, labels] = max(U, [], 2); % 取隶属度最大的簇作为硬标签3.3 层次聚类与 SOM 的接口约定
层次聚类部分走的是凝聚式路线,先算pdist得到样本两两距离,再用linkage建树,最后用cluster按目标簇数切割,dendrogram画树状图。它的接口约定是:输入只需距离矩阵或数据矩阵加链接准则(single/average/ward),不预设簇数,切割时才指定。这条约定决定了它不适合超大数据集,因为距离矩阵是 N×N 的,N=10000 时就是 800MB 的 double,很容易把内存吃满。
SOM 部分则是网格式的:先定网格大小(比如 10×10),每个网格节点有一个与输入同维的权向量,训练时找最匹配节点(BMU)并拉着它和邻域一起向样本靠拢,学习率和邻域半径随迭代衰减。它的输出不是“第几簇”,而是每个样本映射到哪个网格节点,后续再对节点的权向量做一次 Kmeans 或按距离合并,才能得到簇标签。这套“先降维映射、再二次聚类”的流程是神经网络式无监督方法的常见用法,代码里my_som.m把这步固化成了函数返回值,二次开发时你只要改网格尺寸和学习率衰减策略。
4. 跑通与验证:从构造数据到结果对照
代码放在那儿不动永远不会出问题,一旦跑起来,环境和数据格式的坑就全冒出来了。这一章按实际执行顺序走一遍,附上每一步该看到什么现象,这样你卡住时能快速定位是哪一步偏了。
4.1 环境准备与入口脚本执行
MATLAB 版本建议 R2016b 以上,因为代码里用到了隐式扩展和randperm(N, K)的双参数形式。基本不需要额外工具箱,linkage、pdist、dendrogram属于 Statistics and Machine Learning Toolbox,如果你的授权里没有这一块,层次聚类那个 demo 会报未定义函数,其余三个算法不受影响。
跑之前把当前文件夹切到项目根目录,不要只把单个.m文件拖进编辑器运行,因为脚本里用的是相对路径读data/下的文件。执行顺序建议:
% 在 MATLAB 命令行依次执行 addpath(genpath(pwd)); % 把所有子文件夹加入搜索路径 main_compare; % 总入口,一次性跑四种算法addpath(genpath(pwd))这行别省。很多人直接双击demo_kmeans.m运行,然后报未定义函数或变量 'my_kmeans',原因就是子文件夹没进搜索路径。出现这个错,九成是路径问题,不是代码问题。
跑完之后正常现象是:命令行先打印数据规模和每个算法的迭代轮数,然后弹出一组子图,同一份数据上四种算法的分色结果并排显示。如果只出了一张图或者图是空的,往下看排查那章。
4.2 用已知结构的数据验证结果正确性
判断代码有没有跑对,不能只看“出图了”。我的习惯是先造一个簇结构已知的数据,看算法能不能把它还原出来。项目里make_blobs.m就是干这个的:
% 生成 3 个高斯簇,每簇 200 个点,中心已知 rng(42); % 固定随机种子,保证可复现 N = 200; K = 3; trueCenters = [0 0; 8 8; 0 10]; X = []; trueLabels = []; for k = 1:K X = [X; trueCenters(k,:) + randn(N, 2) * 1.0]; trueLabels = [trueLabels; k * ones(N, 1)]; end固定rng是关键,不固定的话每次跑出来的初始中心不同,Kmeans 结果会有波动,你会误以为是代码不稳定。用这份数据跑 Kmeans,K=3、tol=1e-4,正常情况下 10 轮以内收敛,得到的centers应该接近trueCenters(顺序可能不同,因为簇编号是任意的)。验证时可以算一下标签的匹配率,简单做法是对每个预测簇取众数真实标签再统计。
FCM 的验证方式略有不同:看隶属度矩阵里“对角占优”的程度。簇分离得好时,绝大多数样本的最大隶属度在 0.9 以上;如果最大隶属度普遍只有 0.5 左右,说明m取太大或者数据本身簇重叠严重,这时别急着改代码,先把m降到 1.5 试试。层次聚类则看树状图高度:三个簇如果分离明显,dendrogram在高度 5 附近会出现三个清晰的合并分支,切割点选在这之上就能得到三簇。
再换make_moons.m造的双月数据跑一遍,你会看到 Kmeans 和 FCM 都把两个月牙从中间劈开,而层次聚类用single链接能沿着弧线切对。这个对比不是为了证明谁好谁坏,而是让你建立“算法假设与数据形状匹配”的直觉,后面拿到真实数据才知道先试哪个。
5. 常见问题排查:五个实际踩过的坑
这一章的每一条都是我在跑类似代码时真实遇到过的,按“现象 → 原因 → 解决”写,你对照自己的报错信息直接找就行。
5.1 报错“未定义函数或变量 my_kmeans”
现象是脚本跑到一半直接中断。原因几乎总是搜索路径没包含子文件夹,或者当前工作目录不是项目根目录。解决方式是执行addpath(genpath(pwd)),并用pwd确认自己在根目录。如果还不行,用which my_kmeans查一下 MATLAB 到底找到的是哪个文件,有时候是硬盘上存在同名旧版本,被优先加载了。
5.2 Kmeans 结果每次都不一样
现象是同样的数据、同样的 K,两次运行簇划分不同。原因有两个:随机初始化没固定,或者数据本身存在多个等价最优解(簇对称时很常见)。解决方式是运行前rng(42)固定种子;如果是为了出报告,更稳的做法是跑 20 次初始化取簇内距离和最小的那次,代码里加一层外层循环即可,代价是耗时成倍增加。
5.3 FCM 输出全是 NaN
现象是U矩阵打印出来一列NaN。原因是某样本与某中心距离为 0,dist2取负幂变成Inf,归一化时Inf/Inf得NaN。解决是在距离上加一个极小量1e-10,或者判断距离小于阈值时直接令该样本对该簇隶属度为 1、其余为 0。这个坑不报错、不警告,最容易被忽略。
5.4 层次聚类跑到一半内存爆掉
现象是样本量上万后 MATLAB 卡死或提示内存不足。原因是pdist生成的是 N(N-1)/2 个距离,N=20000 时约 2 亿个 double,接近 1.6GB,再叠加linkage的中间矩阵就撑不住了。解决办法是先做降采样,或者改用 Kmeans、FCM 这类只需要 N×K 内存的算法;确实要层次结构,可以考虑先 Kmeans 粗聚成几百个微簇,再对微簇中心做层次聚类,这是常见的两阶段做法。
5.5 绘图中文标签变乱码或方框
现象是图里的标题、坐标轴中文显示成方块。原因是 MATLAB 默认字体不含中文字形。解决是在绘图前设置set(groot, 'defaultAxesFontName', 'SimHei')之类的支持中文的字体,或者干脆把标签改成英文,出图后再用其他工具加注。这一条不影响计算结果,但答辩或汇报时很扎眼。
6. 二次开发:改距离度量与接入自己的数据
代码能跑通只是起点,真正的工作量在于把它改成适合你手上数据的版本。这一章讲三个改造方向,每个都给出可验证的判断标准,避免改完之后不知道对不对。
6.1 替换距离度量以适配非欧数据
Kmeans 和 FCM 都写死了欧氏距离平方。如果你的特征是直方图、TF-IDF 向量或者含大量零的稀疏特征,欧氏距离会被量纲主导。改造点在dist2的计算处,把它换成语义更合适的度量:
| 数据类型 | 建议度量 | 改动位置 | 注意事项 |
|---|---|---|---|
| 连续、量纲不一 | 标准化后欧氏 | 进入算法前zscore(X) | 中心反变换回原空间再解释 |
| 稀疏计数 | 余弦距离 | dist2计算段 | 余弦不满足三角不等式,中心更新需重定义 |
| 概率分布 | KL / JS 散度 | dist2计算段 | 计算量上升,注意防零 |
以余弦为例,把距离段替换为:
% 余弦距离:1 - cos(theta),适用于方向性特征 Xn = X ./ vecnorm(X, 2, 2); % 行归一化 Cn = centers ./ vecnorm(centers, 2, 2); dist2 = 1 - Xn * Cn'; % 取值 [0, 2]替换后必须重新验证:用构造数据跑一遍,确认簇内平均距离下降、簇间距离上升,两个指标同时满足才算改对。只看目标函数下降是不够的,换了度量之后目标函数的数值尺度已经不是原来那个含义了。
6.2 接入自己的数据并保持接口一致
项目里所有算法函数的输入约定都是“N×D 的数值矩阵,行是样本”。接入真实数据时最常犯的错是把特征放成了列,转置没做,结果 D 和 N 互换,算法表面上还在跑,但聚出来的是“样本相似的特征组”,完全不是你要的东西。我一般会在入口加一段防御:
% 读入并校验数据形状 T = readtable('mydata.csv'); X = table2array(T(:, 2:end)); % 假设第一列是 ID assert(size(X, 1) > size(X, 2), ... '样本数应大于特征维数,检查是否需要转置'); X = fillmissing(X, 'constant', 0); % 缺失值兜底 X = zscore(X); % 标准化,避免量纲主导assert那行不是形式主义,它能在数据方向搞错时第一时间报出来,省去你盯着奇怪结果排查半小时。标准化这步则决定了 Kmeans 会不会被某个数值范围特别大的特征带偏,做聚类前几乎都应该走一遍。
6.3 用评价指标而不是眼睛判断改造成败
改完代码,最忌讳的就是“看着图还行就收工”。项目utils/eval_index.m里提供了轮廓系数的计算,改造前后各跑一次,数值提升才算有效:
% 轮廓系数:越接近 1 越好,负值说明样本可能分错簇 s = eval_index(X, labels, 'silhouette'); fprintf('silhouette = %.4f\n', s);我的习惯是每次改完距离、改完初始化策略,都固定在同一份数据、同一个随机种子下对比轮廓系数和迭代轮数两个数。前者看质量,后者看成本,两个都在改善才留下改动,只有一个变好就再想想。从那以后我每次动聚类代码,都强制走一遍“固定种子 → 构造数据验证 → 真实数据看轮廓系数”的流程,不再凭图说话。希望帮到你。
本文还有配套的精品资源,点击获取