简介面向机器学习初学者与需要快速上手聚类实验的工程人员这套MATLAB聚类资源围绕FCM模糊C均值、K-means、Kmedoid等经典算法展开配套FuzzyClusteringToolbox工具箱覆盖从数据标准化、聚类实现到结果可视化和性能评估的完整流程可解决无监督分类学习中原理理解与动手复现之间的衔接问题。压缩包共74个文件、约2.05MB以40个m脚本为主体另有gif与png示意图、txt说明、htm与css网页文档、mat数据文件及pdf工具箱手册兼顾代码调用与原理查阅。该资源已有4888人学习。读者可获得可直接运行的算法源码与Demos示例通过对比FCM对模糊边界、K-means对凸形均匀类、Kmedoid对数据多样性的不同处理借助clusteval等函数用轮廓类指标评估聚类质量并参照PCA降维与投影示例理解参数调整方向适合作为课程实验与算法对比研究的起步材料。1. 四种聚类算法源代码摆在一起最先暴露的是「结果对不上」前阵子帮人看一个模式识别的课程设计同一份三维特征数据K-means 跑出来三类FCM 跑出来五类换成 SOM 又是一张完全不同的拓扑图人直接看懵了。这类场景太常见了论文里要横向对比几种聚类算法手上却没有一套接口统一、换个数据集就能重跑的代码。这套 MATLAB 的四种聚类算法源代码就是冲着这个痛点来的——K-means、FCM、SOM、GMM 四个实现共用同一个数据入口示例代码把加载数据、归一化、聚类、指标评价、出图串成一条完整链路。适合正在做课程设计、写小论文、或者要给下游分类器做无监督预处理的人。下面按「能干什么、怎么跑、怎么改」的顺序拆开讲。2. 工程结构与运行环境从目录布局到第一个能出图的例子拿到一个压缩包先别急着双击 main.m。聚类代码最怕的是结构混乱——四个算法各写各的输入输出格式最后想加个对比实验就得复制粘贴改半天。这套代码的结构值得先花十分钟看明白后面改起来会省很多事。2.1 目录怎么摆四个算法各自独立公共件抽出去常见的组织方式是按「算法 / 数据 / 工具 / 结果」四层切分示例代码基本也是这个路子路径作用main.m主入口选数据、选算法、跑评价、出图data/示例数据集.mat格式统一存成N×D的矩阵加一列标签algorithms/kmeans/my_kmeans.m硬划分聚类实现algorithms/fcm/my_fcm.m模糊 C 均值实现algorithms/som/my_som.m自组织映射网络实现algorithms/gmm/my_gmm.m高斯混合模型 EM 实现utils/load_data.m统一的读数据入口负责列筛选和归一化开关utils/eval_cluster.m轮廓系数、DBI 等内部指标utils/plot_result.m二维/三维散点图按标签上色results/指标表格和图片输出目录这个结构的关键不在文件夹名字而在于四个算法的函数签名被刻意对齐了都是[labels, model] 函数名(X, K, opts)。有了这个约定主流程里就能写一个函数句柄数组用 for 循环把四种算法一次性扫完不用为每个算法单独写一遍评价代码。我见过太多课程设计的代码K-means 返回的是质心矩阵FCM 返回的是隶属度GMM 返回的是结构体最后评价部分只能各写一坨改一次数据要动四个文件。load_data.m通常还带一个归一化开关。聚类对量纲极其敏感尤其是 FCM 和 GMM一个特征的取值范围是 0~1、另一个是 0~10000距离计算基本被后者支配结果就是所有样本挤成一团。默认对连续特征做 z-score 标准化离散的 ID 类列要手动排除。2.2 环境与依赖MATLAB 版本、工具箱和随机种子运行门槛不高R2018b 之后的版本都能跑。四个算法如果全是自己手写的实现那么 Statistics and Machine Learning Toolbox 不是必需的——kmeans、fitgmdist这些内置函数只有在做交叉验证对照时才会用到。SOM 也不需要 Deep Learning Toolbox几十行的循环就够了用工具箱反而是杀鸡用牛刀。真正需要提前处理的是随机性。K-means 的初始质心、FCM 的初始隶属度、SOM 的初始权重、GMM 的初始均值全都是随机的。不固定种子的话同一份数据跑两次指标能差好几个百分点写进论文里就是玄学数据。标准做法是在脚本最前面固定一次% env_init.m —— 放在主脚本最前面执行 rng(42, twister); % 固定全局随机流保证结果可复现 root fileparts(mfilename(fullpath)); % 以脚本所在目录为工程根 addpath(genpath(fullfile(root, algorithms))); addpath(genpath(fullfile(root, utils))); if ~exist(fullfile(root, results), dir) mkdir(fullfile(root, results)); % 输出目录不存在就建一个 endrng(42, twister)里的 42 是种子换成任何整数都行关键是整篇实验从头到尾只用这一个。twister是梅森旋转发生器MATLAB 新版默认就是它显式写出来是为了换机器时不至于因为默认算法变化导致结果漂移。genpath会把子目录一次性加进搜索路径这样algorithms/kmeans/下面的函数不用改当前目录就能直接调用。提示如果同一台机器上还跑着别的随机实验rng是全局状态会被互相覆盖。稳妥的做法是在每个算法函数内部再调一次rng(opts.seed)把随机性关在函数里。2.3 跑通第一个例子main.m 从加载数据到出图环境弄好之后第一个目标不是追求指标多高而是看到图能出来、标签数量对得上。示例数据用三个高斯团生成最直观二维平面上肉眼就能判断聚类对不对% main.m —— 最小可运行版本 env_init; % 固定种子、加路径 % 1. 造一份有三团结构的数据每团 120 个点 rng(42); X [randn(120,2) [0 0]; randn(120,2) [5 0]; randn(120,2) [2 5]]; % 2. 归一化聚类前的必修课 X (X - mean(X)) ./ std(X); % 3. 依次跑四种算法K 统一取 3 K 3; algos {my_kmeans, my_fcm, my_som, my_gmm}; names {K-means, FCM, SOM, GMM}; for i 1:numel(algos) [labels, model] algos{i}(X, K, struct(Seed, 42)); m eval_cluster(X, labels); % 轮廓系数 DBI fprintf(%-8s Silhouette%.3f DBI%.3f\n, names{i}, m.sil, m.dbi); plot_result(X, labels, sprintf(results/%s.png, names{i})); end这段代码有几个地方值得说明。algos是函数句柄的元胞数组algos{i}(X, K, opts)就是动态调用这样加第五种算法只要往数组里追加一个句柄主流程一行都不用改。struct(Seed, 42)是传给每个算法的选项结构体四个实现里各自rng(opts.Seed)一次保证单独调用时也可复现。eval_cluster返回的轮廓系数在 -1 到 1 之间越接近 1 说明簇内紧、簇间远DBI 越小越好。这两个指标在凸形簇上还算靠谱遇到长条形或者密度差异大的簇就会失灵后面避坑那章会细说。plot_result只处理二维和三维两种输入高维数据要么先做 PCA 降到二维要么直接跳过出图看指标。跑通之后屏幕上应该能看到四行指标results/下四张图每张图三种颜色的点分得清清楚楚。这时候再换自己的数据心里就有底了。3. 核心模块拆解之一K-means 与 FCM 的距离计算和迭代收敛这两种算法排在一起讲是因为它们的骨架几乎是同一个算距离、更新中心、判断收敛。差别只在「一个样本属于一个簇」还是「一个样本以隶属度分属所有簇」。把这两份代码对着看模糊聚类的概念会一下子清楚。3.1 K-means初始化、分配、更新三步循环标准 K-means 的核心循环只有十几行但初始化方式对结果影响巨大。随机从样本里挑 K 个点当初始质心运气不好挑到三个挨在一起的离群点后面怎么迭代都救不回来。示例代码一般用 k-means 的简化版第一个质心随机选之后每个质心按「到已选质心距离的平方」成正比抽样让初始点尽量散开。function [labels, centroids, Jhist] my_kmeans(X, K, opts) %MY_KMEANS 硬划分 K-meansk-means 初始化 % X : N x D 样本矩阵调用前应已归一化 % K : 聚类数 % opts : .MaxIter 最大迭代次数(默认100) / .Tol 质心位移阈值(默认1e-4) if nargin 3, opts struct(); end maxIter getopt(opts, MaxIter, 100); tol getopt(opts, Tol, 1e-4); [N, D] size(X); % ---- 1. k-means 初始化 ---- centroids zeros(K, D); centroids(1,:) X(randi(N), :); for k 2:K d2 min(pdist2(X, centroids(1:k-1)).^2, [], 2); % 每个点到最近质心的平方距离 p d2 / sum(d2); % 转成抽样概率 centroids(k,:) X(find(rand cumsum(p), 1), :); end % ---- 2. 分配与更新循环 ---- Jhist zeros(maxIter, 1); for iter 1:maxIter Dmat pdist2(X, centroids); % N x K 距离矩阵 [dmin, labels] min(Dmat, [], 2); % 每个样本归到最近的质心 Jhist(iter) sum(dmin.^2); % 簇内平方和即目标函数 old centroids; for k 1:K idx (labels k); if any(idx) centroids(k,:) mean(X(idx,:), 1); else % 空簇处理把离全局均值最远的样本拉过来当新质心 [~, far] max(sum((X - mean(X,1)).^2, 2)); centroids(k,:) X(far, :); end end if max(sqrt(sum((centroids - old).^2, 2))) tol Jhist Jhist(1:iter); % 提前收敛截断历史 break; end end endpdist2(X, centroids)一次性算出 N×K 的距离矩阵比写双重循环快一个量级数据量上千之后差别很明显。min(Dmat, [], 2)沿第二维取最小同时把列索引作为标签返回这一行就完成了分配步骤。空簇处理是很多人会忽略的地方。某个质心在迭代中被「抢光」所有样本时mean会返回 NaN一传十十传百整个矩阵就废了。上面这段的做法是把离全局中心最远的样本强行拉过来虽然粗暴但能保证迭代继续。另一种常见做法是直接把这个空簇删掉K 减一但那样输出维度就不固定了下游代码要额外判断我不太推荐。收敛判据用的是质心最大位移小于tol。也可以换成目标函数Jhist的相对变化小于阈值两者效果接近但质心位移对尺度更敏感数据归一化之后1e-4是个比较稳的默认值。3.2 FCM隶属度矩阵与模糊指数 m 的取值FCM 把「属于/不属于」换成「隶属度 u_ik ∈ [0,1]」约束是每个样本对所有簇的隶属度之和为 1。目标函数里多了一个模糊指数 mJ Σ_i Σ_k (u_ik)^m · ||x_i - c_k||²m 越大隶属度越平均划分越模糊m 趋近 1 就退化成硬 K-means。经验上取 1.5~2.5默认 2 是最常见的起点。function [U, centers, Jhist] my_fcm(X, K, opts) %MY_FCM 模糊 C 均值 % opts : .m 模糊指数(默认2) / .MaxIter / .Tol 目标函数相对变化阈值 if nargin 3, opts struct(); end m getopt(opts, m, 2); maxIter getopt(opts, MaxIter, 150); tol getopt(opts, Tol, 1e-6); [N, D] size(X); % ---- 1. 隶属度初始化每行随机后归一化到和为 1 ---- U rand(N, K); U U ./ sum(U, 2); Jhist zeros(maxIter, 1); for iter 1:maxIter % ---- 2. 更新聚类中心 ---- Um U.^m; centers (Um * X) ./ sum(Um, 1); % K x D % ---- 3. 更新隶属度 ---- d2 pdist2(X, centers).^2 1e-10; % 加极小量防止除零 ratio d2 .^ (-1/(m-1)); % N x K U ratio ./ sum(ratio, 2); % 按行归一化 % ---- 4. 目标函数与收敛判断 ---- Jhist(iter) sum(sum(Um .* d2)); if iter 1 abs(Jhist(iter) - Jhist(iter-1)) / Jhist(iter-1) tol Jhist Jhist(1:iter); break; end end [~, labels] max(U, [], 2); % 去模糊化取最大隶属度 end中心更新那行(Um * X) ./ sum(Um, 1)是矩阵形式的加权平均Um是 N×K 的隶属度矩阵做 m 次幂转置后乘 X 得到 K×D 的加权和再除以每个簇的权重总和。写成矩阵比循环快也不容易在下标上出错。d2 .^ (-1/(m-1))这一行是 FCM 的更新公式核心。样本到某簇中心距离越小这一项越大隶属度就越高。加1e-10是为了防止样本恰好落在中心上导致除零——真实数据里这种情况比想象中容易出现尤其是重复样本多的数据集。注意m取 1.1 这种接近 1 的值1/(m-1)会变成 10距离的微小差异被放大到极限隶属度迅速退化成 0/1跟你想要的「模糊」正好相反还容易数值溢出。老老实实从 2 开始调。3.3 两种算法的收敛判据和结果对比K-means 用质心位移判断收敛FCM 用目标函数的相对变化。差异的原因是K-means 的分配步骤是离散的目标函数只会单调不增、阶梯式下降盯着它看不容易设阈值而 FCM 的隶属度是连续更新的目标函数平滑下降用相对变化更合适。在球形、大小相近的簇上两者给出的划分几乎一致FCM 的价值体现在簇边界重叠的数据上。比如两个簇交界处的样本K-means 会硬塞给其中一个FCM 给出 0.55/0.45 这样的隶属度后续如果要做风险决策或者软融合这个信息就很有用。实际对比时建议除了最终标签把U矩阵也存下来挑几个边界样本看看隶属度分布比只看轮廓系数直观得多。4. 核心模块拆解之二SOM 神经网络与 GMM 的模型实现如果说 K-means 和 FCM 是「以中心为锚」那 SOM 和 GMM 的思路完全不同一个靠神经元之间的拓扑竞争一个靠概率密度建模。这两份代码的结构差别最大也最容易在参数上翻车。4.1 SOM竞争学习、邻域函数与权重更新SOM 的输出不是 K 个簇而是一张网格每个网格节点有一个跟输入同维的权重向量。训练时每个样本找到最相似的节点BMU最优匹配单元然后拉着 BMU 和它周围的节点一起向样本靠拢。邻居的范围和靠拢的力度都随训练轮次衰减最后整张网格就成了数据的低维拓扑映射。function [labels, W, hits] my_som(X, gridSize, opts) %MY_SOM 自组织映射二维六边形/矩形网格 % gridSize : [rows cols]例如 [5 5] 表示 25 个神经元 % opts : .MaxIter 总步数(默认 20*N) / .Alpha0 初始学习率 / .Sigma0 初始邻域半径 if nargin 3, opts struct(); end [N, D] size(X); MaxIter getopt(opts, MaxIter, 20 * N); alpha0 getopt(opts, Alpha0, 0.5); sigma0 getopt(opts, Sigma0, max(gridSize) / 2); tau MaxIter / log(sigma0); % 衰减时间常数 % ---- 1. 权重初始化网格坐标 小随机扰动 ---- [gx, gy] meshgrid(1:gridSize(2), 1:gridSize(1)); W rand(gridSize(1) * gridSize(2), D) * 0.01; % 实际工程里常用 PCA 前两维铺初始权重收敛更快 hits zeros(size(W,1), 1); for t 1:MaxIter i randi(N); % 随机抽一个样本在线学习 x X(i, :); % ---- 2. 找 BMU ---- d2 sum((W - x).^2, 2); [~, bmu] min(d2); % ---- 3. 计算邻域强度并更新权重 ---- sigma sigma0 * exp(-t / tau); % 邻域半径指数衰减 alpha alpha0 * exp(-t / tau); % 学习率同步衰减 gd2 (gx(:) - gx(bmu)).^2 (gy(:) - gy(bmu)).^2; h exp(-gd2 / (2 * sigma^2)); % 高斯邻域函数 W W alpha * h .* (x - W); % 逐节点更新 hits(bmu) hits(bmu) 1; end % ---- 4. 用训练好的权重给样本打标签 ---- [~, labels] min(pdist2(X, W), [], 2); endtau MaxIter / log(sigma0)这个式子的作用是让sigma在整个训练过程中从sigma0平滑衰减到 1 附近。如果自己拍一个衰减率很容易出现前半程收敛太快、后半程几乎不动的情况——权重已经定型再怎么训练也学不到新结构。h是高斯邻域函数距离 BMU 越远的节点受到的拉力越小。W alpha * h .* (x - W)是向量化写法h是numNodes×1x - W是numNodes×D按行点乘就实现了「每个节点按各自的强度向样本移动」。这里用的是在线学习每次抽一个样本比批量版本的收敛慢一些但内存占用小。数据量几万以内在线版本完全够用。标签的取法要留个心眼min(pdist2(X, W))得到的是每个样本最近的神经元编号但神经元编号不等于簇编号。如果网格是 5×5 25 个神经元而真实类别只有 3 个输出标签就有 25 种取值直接丢进混淆矩阵会得到一堆空行。实际使用时要么对网格做二次聚类把 25 个神经元的权重再跑一次 K-means要么用 U-Matrix 可视化看边界。4.2 GMMEM 迭代的 E 步与 M 步GMM 假设数据由 K 个高斯分布混合生成每个高斯有自己的均值、协方差和混合系数。EM 算法交替做两件事E 步算每个样本属于每个高斯的后验概率M 步用这些概率加权重估参数。跟 K-means 的硬分配相比GMM 的「软分配」多了协方差形状这个自由度能拟合椭圆形的簇。function [labels, model] my_gmm(X, K, opts) %MY_GMM 高斯混合模型EM 求解 % opts : .MaxIter / .Tol 对数似然变化阈值 / .Reg 协方差正则项 if nargin 3, opts struct(); end maxIter getopt(opts, MaxIter, 200); tol getopt(opts, Tol, 1e-6); reg getopt(opts, Reg, 1e-6); [N, D] size(X); % ---- 1. 用 K-means 的结果初始化均值和混合系数 ---- [initLabels, C] my_kmeans(X, K, struct(MaxIter, 50)); pi_k zeros(1, K); Sigma zeros(D, D, K); for k 1:K Xk X(initLabels k, :); pi_k(k) size(Xk, 1) / N; Sigma(:,:,k) cov(Xk) reg * eye(D); % 正则化避免奇异 end mu C; logLik zeros(maxIter, 1); for iter 1:maxIter % ---- 2. E 步算后验概率 gamma ---- gamma zeros(N, K); for k 1:K gamma(:,k) pi_k(k) * mvnpdf(X, mu(k,:), Sigma(:,:,k)); end s sum(gamma, 2) 1e-300; logLik(iter) sum(log(s)); gamma gamma ./ s; % ---- 3. M 步用 gamma 加权重估参数 ---- Nk sum(gamma, 1) 1e-10; for k 1:K mu(k,:) (gamma(:,k) * X) / Nk(k); Xc X - mu(k,:); Sigma(:,:,k) (Xc * (Xc .* gamma(:,k))) / Nk(k) reg * eye(D); end pi_k Nk / N; if iter 1 abs(logLik(iter) - logLik(iter-1)) tol break; end end [~, labels] max(gamma, [], 2); model struct(mu, mu, Sigma, Sigma, pi, pi_k, logLik, logLik); end初始化用 K-means 是个稳妥选择。全随机初始化 EM 的话很容易两个高斯中心初始挨得太近之后永远分不开对数似然早早就「收敛」在一个很差的局部解上。mvnpdf(X, mu, Sigma)是 MATLAB 自带的多元正态密度需要 Statistics and Machine Learning Toolbox。没有工具箱就手写算Xc / chol(Sigma)再套exp(-0.5*sum(z.^2)) / sqrt(det(2*pi*Sigma))三行搞定。Sigma(:,:,k) (Xc * (Xc .* gamma(:,k))) / Nk(k)这一行做的是加权协方差。Xc .* gamma(:,k)把每个样本的中心化向量按它的归属概率缩放再乘 Xc 转置得到加权外积和。这里必须加reg * eye(D)否则当某个簇的有效样本数小于维度 D 时协方差矩阵秩亏mvnpdf会直接返回 0 或者 NaN整个 E 步就崩了。1e-6是常用值数据尺度大的话可以往上加到1e-4。4.3 统一接口封装四套实现怎么共用一份评价代码四个函数写完之后接口对齐的价值才体现出来。加一层薄薄的包装把各自的特殊输出隶属度、权重矩阵、模型结构体收进model主流程就完全一致了% eval_all.m —— 统一跑一批算法并汇总指标 function T eval_all(X, K, algoList) T table(); for i 1:numel(algoList) tic; [labels, ~] algoList{i}.fn(X, K, algoList{i}.opts); elapsed toc; m eval_cluster(X, labels); T [T; table({algoList{i}.name}, m.sil, m.dbi, elapsed, ... VariableNames, {Algorithm,Silhouette,DBI,TimeSec})]; end endalgoList是个结构体数组每项含name、fn函数句柄、opts。想加第五种算法比如谱聚类只要多写一行struct(name,SC,fn,my_sc,opts,struct())汇总表自动多一行。tic/toc顺便把每种算法的耗时记录下来写对比实验的时候这列数据往往比精度更有说服力。5. 避坑与排查参数、数据与可视化上最容易翻车的几处这些坑基本都是我在别人的课程设计或者自己的实验里真实踩过的按「现象 → 原因 → 解决」记下来遇到对得上号的就直接抄。5.1 数据与初始化阶段的三个坑现象每次运行结果都不一样指标浮动好几个点。原因有两层一是没固定随机种子二是即使固定了种子如果把rng放在循环里面每次迭代都重设等于没设。解决办法是把rng(seed)放在脚本最开头或者在每个算法函数入口调一次绝不在循环体内调用。另外聚类本身对初始值敏感单次结果不可靠稳妥的做法是跑 10 次不同种子取目标函数最优的那次报告时给出均值±标准差。现象FCM 跑出来的隶属度全是 0.2 左右等于没分。这通常是模糊指数m取值过大的典型表现比如设成 5 甚至 10隶属度被强行抹平所有样本对所有簇的归属都差不多。反过来m取 1.05 会得到接近 0/1 的硬划分和一堆数值警告。正确区间是 1.5~2.5从 2 起调。另一个诱因是数据没归一化不同特征量纲差异巨大时距离矩阵本身就失去了意义m再合理也救不回来。现象SOM 训练完之后hits显示所有样本都落在同一个神经元上。这是权重没散开的信号。原因可能是学习率alpha0或者邻域半径sigma0初始值太小一开始就只有 BMU 自己在动别的节点永远不动也可能是输入数据没归一化某维度的绝对数值太大把距离计算彻底支配了。解决顺序是先确认X做过 z-score再把sigma0设成网格尺寸的一半alpha0提到 0.5训练步数至少给到10*N。5.2 参数与可视化阶段的两个坑现象GMM 报「协方差矩阵接近奇异」或者对数似然变成-Inf。根本原因是某个高斯分量的有效样本数小于数据维度。比如 4 维特征、K 取 8但真实簇只有 3 个多出来的分量分到的样本寥寥无几算出来的协方差矩阵秩亏。直接的解法是加正则项reg * eye(D)进一步的做法是换对角协方差只估方差、不估协方差参数少、不易奇异代价是失去对特征相关性的建模能力。还有一个隐性问题如果某两列特征高度线性相关协方差矩阵本身就接近奇异得先做特征筛选或者 PCA。现象聚类数 K 完全靠拍脑袋轮廓系数又给出误导性的建议。肘部法看的是簇内平方和随 K 的下降拐点轮廓系数看的是簇内紧凑度和簇间分离度的比值两个指标在凸形、大小均衡的簇上还算一致但数据是长条形或者密度差异大的时候轮廓系数会偏向于「把一个大簇劈成两个」的 K 值看着分数高实际毫无业务意义。我的做法是三个信号一起看指标曲线、降维散点图、业务上说得通的 K 值三者能对上再定。注意出图时如果标签是汉字MATLAB 默认字体可能显示成方块。set(gca, FontName, SimHei)能解决大部分情况但写进脚本前最好确认目标机器上装了这个字体不然换台机器又是一次翻车。6. 二次开发换数据、加算法、接下游的三条改造路径把这套代码用起来最常做的改造无非三件事按投入产出比从低到高排一下。第一件是换数据源。所有算法都从load_data.m拿数据只要保证返回N×D的数值矩阵加一列可选标签就行。从 Excel 读进来的表往往混杂着时间、字符串 ID、缺失值我一般这么写% load_data.m 里加一个分支 T readtable(your_data.xlsx); numCols varfun(isnumeric, T, OutputFormat, uniform); X table2array(T(:, numCols)); X fillmissing(X, constant, 0); % 缺失值先填 0再统一归一化 if ~isempty(find(ismissing(T), 1)), warning(存在缺失值已按 0 填充); endvarfun(isnumeric)一行把数值列筛出来比手写列号安全得多。缺失值用 0 填是权宜之计更严谨的做法是中位数填充或者直接删行取决于业务对缺失的容忍度。第二件是加新算法。照着[labels, model] 函数名(X, K, opts)的签名写一个新的.m丢进algorithms/对应子目录然后在main.m的algos数组里追加一个句柄。整个过程不碰主流程任何一行。这个约定的价值在加第三、第四个算法时最明显——你已经忘了主流程怎么写的,但接口还在那儿兜着。第三件是把聚类结果接到下游。这是二次开发里最有价值的一步。聚类本身往往不是目的而是给分类器或者检索系统提供特征。常见的接法是用聚类中心做量化把每个样本映射成「到各中心的距离向量」作为新特征喂给下游模型。这跟神经网络里 embedding 的思路是一回事只不过换成了无监督的方式初始化。% 用聚类中心构造新特征再送进下游分类器 [~, C] my_kmeans(Xtr, 32, struct(Seed, 42)); Ftr pdist2(Xtr, C); % N x 32 的距离特征 Fte pdist2(Xte, C); % 测试集用同一套中心不能重训这里最容易犯的错是训练集和测试集各自跑一遍聚类再拼特征。两边的中心对不上第 3 维在训练集里代表「离 A 中心的距离」,在测试集里可能代表完全不同的东西模型看起来能跑但效果莫名其妙。正确做法永远是「在训练集上拟合中心然后对测试集只做 transform」。验证改造有没有效果我固定走三步固定种子跑 5 次看方差、把指标和基线放同一张表、抽 20 个样本人工看一眼标签是不是说得通。前两步防的是过拟合和随机波动第三步防的是「指标好看但根本没有业务意义」——这种情况在聚类里太常见了。从那以后我每次改完聚类相关代码都强制把这三步走一遍哪怕只是调了个m值。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?