简介:面向高校本硕博学生及科研人员的数据聚类算法学习资源,围绕DBSCAN密度聚类在MATLAB环境中的仿真实现,提供一套完整可运行的代码框架与操作演示视频,帮助读者从原理层面理解密度可达、核心点、边界点与噪声点,并掌握参数选择及聚类结果评估方法。压缩包共包含10个文件,其中7个是m格式的源码文件,2个为mat格式的数据文件,另有1个avi格式的操作录像,整体体积仅882KB,非常轻量。源码模块覆盖了DBSCAN聚类主流程、纯度计算、锦标赛选择、最优邻域半径解析等功能,数据文件可直接加载测试,录像则从环境配置开始,逐步演示在MATLAB 2021a及以上版本中运行主仿真脚本、查看聚类图像的全过程,并专门提示了当前文件夹路径设置等易错细节。资源已有1344人学习下载,适合边看边练,快速掌握DBSCAN算法的编程实现、参数调试与结果验证方法。
1. 先立住一件事:DBSCAN的MATLAB仿真到底解决什么问题
在数据聚类这个方向上,基于DBSCAN算法的数据聚类MATLAB仿真,是低成本验证“密度聚类”思想的标配实验。这个项目要回答的问题很具体:面对一堆没有标签的散点,如何用密度而不是几何中心把它们划分成簇,并顺手把离群点标出来。无论是课程大作业、竞赛预实验,还是论文里的基线对比,这套仿真都能在本地几分钟内跑出图、跑出指标,帮你判断数据是否真的存在簇结构。它适合想学聚类算法又不想一上来就啃论文的人,也适合需要快速产出一份可视化结果的工程人员。
我通常会把整个项目拆成三层来做:第一层只写DBSCAN核心逻辑,第二层构造仿真数据喂给算法,第三层做参数调优和结果评估。三层之间不互相依赖,每一层都可以单独改单独测。下面就从算法原理和代码实现开始。
2. DBSCAN算法核心逻辑与MATLAB实现:从密度定义到可运行代码
2.1 为什么DBSCAN能聚类:核心点、边界点与噪声点
DBSCAN的思路和K-means完全不同的地方在于:K-means先定K个中心,然后让每个样本靠近最近的中心;DBSCAN不设中心,它只回答一个问题——某个样本的周边够不够“密”。密不密的判断标准只有一个参数组合:邻域半径eps,以及一个点在eps半径内至少要有多少个邻居才算核心点,这个邻居数叫MinPts。
一个样本如果它的eps邻域内包含至少MinPts个样本,它就是核心点。如果它自己不是核心点,但落在某个核心点的eps邻域内,它就叫边界点,也属于这个簇。如果它既不是核心点,也不在任何核心点的邻域内,就是噪声点。从这里可以看出,DBSCAN根本不需要事先指定分成几个簇,簇的形状完全由数据点之间的密度连接关系确定,所以它天然支持月牙形、环形、长条形这些非凸形状,而K-means在这些形状上往往会错误地把一个簇硬切成几块。
补充一个更直观的理解:核心点是“肚子里有货”的骨架点,边界点是骨架点外围沾上来的点,噪声点是离所有骨架点都太远的点。聚类的过程就是把所有骨架点用“邻域内有交集”的方式串起来,连成一整片的骨架点加它们周围的边界点组成一个簇。这个过程在教材里叫“密度可达”,用一句话说就是:从一个核心点出发,一路踩着别的核心点的邻域走出去,能走到的所有点都归进同一个簇。
这个特性在仿真里的价值经常会被人忽略:DBSCAN从原理上就不要求你知道数据里有几个簇。做仿真的时候,你不需要像K-means那样先猜K值,只需要给两个物理含义明确的参数,这是算法最讨喜的地方,也是很多人第一次跑通后愿意继续深入调参的原因。
2.2 手写DBSCAN核心函数:把密度可达变成BFS代码
先声明一下我的立场:MATLAB从R2019a开始提供内置dbscan函数,但我仍然建议仿真项目里先手写一份核心逻辑。手写版本的价值不在性能,而在它把聚类的每一步都摊开了,让你知道“密度可达”在代码里具体是哪一步循环、哪一步入队出队。下面是我常用的核心函数,只依赖MATLAB基础环境,不依赖任何工具箱。
function [cluster_ids, core_flags] = dbscan_core(X, eps, minPts) % DBSCAN_CORE 手写DBSCAN聚类核心逻辑 % 输入X : N x D 矩阵,每行一个样本 % eps : 邻域半径 % minPts : 核心点判定阈值,包含自身 % 输出cluster_ids : N x 1,簇编号从1开始,-1表示噪声 % 输出core_flags : N x 1 逻辑向量,true表示核心点 N = size(X, 1); cluster_ids = zeros(N, 1); core_flags = false(N, 1); % 1. 预计算距离矩阵,避免在循环里反复算距离 D = pdist2(X, X); D(1:N+1:end) = Inf; % 对角线置Inf,排除样本自身 % 2. 标注核心点:eps邻域内样本数 >= minPts for i = 1:N if sum(D(i, :) <= eps) >= minPts core_flags(i) = true; end end % 3. BFS扩展:从核心点出发,不断吸收密度相连的点 current_cluster = 0; for i = 1:N if cluster_ids(i) ~= 0 continue; % 已分配过 end if ~core_flags(i) continue; % 非核心点不开启新簇 end current_cluster = current_cluster + 1; cluster_ids(i) = current_cluster; queue = i; % 用数组模拟队列 while ~isempty(queue) p = queue(1); queue(1) = []; % 出队 neighbors = find(D(p, :) <= eps); for q = neighbors if cluster_ids(q) == 0 cluster_ids(q) = current_cluster; if core_flags(q) queue(end+1) = q; % 核心点继续入队扩展 end end end end end % 4. 未被分配的样本标为噪声 cluster_ids(cluster_ids == 0) = -1; end这段代码的逻辑分四步,我按函数里的顺序说明。第一步用pdist2把两两距离全部算好丢进矩阵D,后面所有“找邻居”的操作都是对这个矩阵做比较,省掉了循环里反复算距离的开销。这里有个细节:对角线上的距离是0,样本和自己当然是邻居,但DBSCAN在统计“邻域内点数”时需要明确一个约定,我习惯让样本自身不参与计数,所以把对角线置为Inf,后面的逻辑就变成统计“除自己之外多少个点在半径内”,再和minPts比较。
第二步是核心点判定,遍历每个样本,统计半径内的点数,够数就标记为核心点。第三步是整个算法的核心:外层循环负责发现新簇,内层用队列做广度优先扩展。这个BFS过程非常好理解——先拿一个核心点当种子,把它的邻居全部并入当前簇,如果邻居里有核心点,就把这些核心点继续放入队列,等它们出队时继续扩展自己的邻居,直到队列空为止。这一串操作就是“密度可达”的直接代码表达。
最后一步把剩余未分配的样本标为-1,即噪声点。输出里cluster_ids为正数的都是簇成员,为-1的是噪声,这个字段后面做可视化和指标计算都直接用。这里提醒两个使用细节:函数入参名我用eps和minPts,MATLAB里变量名可以和内置函数重名,但注意不要让脚本里出现名为dbscan的变量,否则再调用内置函数时会报“变量名掩盖函数”的错误。另外pdist2计算的是全量距离矩阵,样本数超过几千后内存会明显吃紧,这个性能坑我放在第四章结合大数据场景详细说。
2.3 内置dbscan函数与自实现版本的交叉验证
手写版本跑出结果后,我的下一步永远是拿内置dbscan做一次交叉验证。内置函数的好处是稳定、高效、经过大规模测试,而且它内部对边界情况的处理比手写队列更严谨。调用方式非常简单:
% 用MATLAB内置函数做交叉验证 rng(42); X = [randn(80,2)*0.4 + [2,2]; randn(80,2)*0.3 + [5,6];]; idx_builtin = dbscan(X, 0.5, 5);这里构造了两个高斯簇,然后调用内置dbscan,参数还是那两个:eps=0.5,MinPts=5。输出idx_builtin是一个N×1的向量,存放每个点所属簇的编号,噪声点在R2020a之后统一返回-1,老版本可能返回0,写代码时最好先查一下版本行为。
交叉验证的编程化比较有一个坑:两边的簇编号顺序可能不同。自实现函数按遍历顺序编号,内置函数内部按高密度区域优先编号,所以直接比较数值没有意义。我用的方案是把标签转换成“样本对是否属于同一簇”的布尔矩阵再比较:
% 按成员关系做一致性比较 member_builtin = (idx_builtin == idx_builtin'); member_core = (idx_core == idx_core'); consistency = mean(member_builtin(:) == member_core(:)) * 100; fprintf('与内置函数按同簇关系对比,一致性: %.1f%%\n', consistency);在280个样本左右的数据上,布尔矩阵大约280×280,内存占用可以忽略。一致性达到95%以上就说明核心逻辑没有本质错误,剩下的差异集中在边界点的归属上,这属于实现细节差异,不影响报告结论。如果一致性低于90%,就要回查自实现版本有没有把核心点判定的边界条件搞错。
内置函数在性能上会比手写版本高一个量级,原因在于它内部用KD树加速近邻查找,不需要显式构造全距离矩阵。在样本数千到上万时,手写版本可能开始卡顿,内置函数依然流畅。所以最终是两套逻辑并存的局面:学习和小数据验证用自实现,大规模数据或耗时对比实验切换到内置函数。在报告里写一句“算法逻辑自实现,并与MATLAB内置函数交叉验证”,也能提前回应评审对“为什么不直接用库函数”的质疑。
3. 仿真数据生成与最小可运行流程:从人造数据到两张聚类图
3.1 用mvnrnd构造三类仿真数据:圆形簇、细长簇与噪声
仿真数据是整个项目的“输入剧本”。数据不真实,聚类结果再漂亮也站不住。我的选型标准是:必须包含密度不同、形状不同的至少两个真实簇,以及一批和簇没有关系的噪声点。这样既能验证DBSCAN处理非凸簇的能力,也能验证噪声识别能力,正好避开K-means最擅长的球形簇场景。
MATLAB里构造这种数据最方便的方式是用mvnrnd从多元高斯分布采样,协方差矩阵可以自由控制簇的拉伸方向和半径。下面这段代码是整套仿真的数据基础:
% 构造两类形状差异明显的仿真数据 rng(7); % 簇A:近似圆形,协方差矩阵对角元素接近 clusterA = mvnrnd([2, 2], [0.4, 0.1; 0.1, 0.3], 120); % 簇B:细长形,x方向方差远大于y方向 clusterB = mvnrnd([5, 6], [0.8, 0.05; 0.05, 0.08], 100); % 噪声点:在更大范围内均匀撒点 noisePts = rand(60, 2) * 8; X = [clusterA; clusterB; noisePts];协方差矩阵的设计值得细看。第一个矩阵[0.4, 0.1; 0.1, 0.3],对角线两个值接近,簇大体是圆形,非对角线元素让点在斜向上有一点相关性,看起来更接近真实数据。第二个矩阵[0.8, 0.05; 0.05, 0.08],两个方向方差差异达到十倍,生成的点云被明显拉长,这对DBSCAN是个很好的测试:它必须靠密度连接而不是形状模板来聚合点。噪声用rand在更大范围内均匀生成,与两个簇之间留出明显的密度落差。
这里有一个你必须养成的习惯:每一段生成随机数的代码前面都要有rng固定种子。没有rng,每次生成的点都不同,后面调参数时你就分不清结果变化是算法引起的还是数据变了。rng(7)和2.3节的rng(42)是两个不同实验场景,种子不冲突,但同一份报告里要保持每个实验的种子固定不变。
3.2 主仿真脚本:从数据到两张聚类图的最小运行链路
有了核心函数和仿真数据,下一步把它们拼成一个能一键运行的脚本。这个脚本的设计目标是不需要任何手工操作,从清空环境、生成数据、聚类到输出对比图,全部自动完成。下面是一个完整的模板:
%% DBSCAN数据聚类仿真主脚本 clear; close all; clc; % 第一部分:生成仿真数据 rng(7); clusterA = mvnrnd([2, 2], [0.4, 0.1; 0.1, 0.3], 120); clusterB = mvnrnd([5, 6], [0.8, 0.05; 0.05, 0.08], 100); noisePts = rand(60, 2) * 8; X = [clusterA; clusterB; noisePts]; X = X(randperm(size(X,1)), :); % 打乱顺序,模拟真实采集数据 % 第二部分:执行DBSCAN聚类 eps = 0.55; minPts = 5; [idx, coreFlags] = dbscan_core(X, eps, minPts); % 第三部分:可视化对比 figure('Position', [100, 100, 1100, 440]); subplot(1,2,1); plot(X(:,1), X(:,2), 'k.', 'MarkerSize', 8); title('原始仿真数据'); xlabel('特征1'); ylabel('特征2'); grid on; subplot(1,2,2); gscatter(X(:,1), X(:,2), idx, 'rbgk'); title(sprintf('DBSCAN聚类结果 eps=%.2f minPts=%d', eps, minPts)); xlabel('特征1'); ylabel('特征2'); grid on; % 第四部分:输出关键统计量 numClusters = max(idx); numNoise = sum(idx == -1); fprintf('聚类簇数: %d\n', numClusters); fprintf('噪声点数量: %d\n', numNoise);脚本里每一步都有可检查的输出。第一部分跑完,工作区里出现X矩阵,行数是280,列数是2。第二部分两个参数是后面最常改的地方,eps取0.55是针对上一节数据调出来的,比2.3节的0.5略大,目的是应对细长簇在长轴方向出现的相对稀疏的中间段。第三部分用gscatter而不是plot,是因为gscatter能按idx自动分配颜色,并把噪声用单独一组画出来,省掉手工区分颜色的循环。第四部分输出的两个数字是评估聚类结果的原始依据。
如果你打算配一段仿真操作视频来完整还原这个流程,我的建议是不要一次运行整个脚本,而是分段点运行:先运行第一部分,在工作区双击X查看数据;运行第二部分,查看idx和coreFlags;再运行第三部分,让图形逐段出现。很多录视频的人习惯一口气跑完,观看者根本看不清每一步产出了什么,分段演示反而更好懂。
注意:如果修改了生成数据的行数,记得同步检查后续idx的长度是否一致。这类维度不匹配错误在MATLAB里报错信息往往只在最后一行,容易让人盯半天才发现是前面某个矩阵尺寸出了偏差。
3.3 聚类结果怎么量化:簇数、噪声率与轮廓系数
仿真跑完,不能只说“图上看起来分得不错”。报告或答辩里需要可复现的量化指标。我先说三个最直接的:簇数、噪声点占比、平均轮廓系数。簇数和噪声率已经在主脚本里用fprintf输出了,轮廓系数需要单独补几行代码。
% 计算噪声占比与轮廓系数 noise_ratio = sum(idx == -1) / length(idx) * 100; if max(idx) >= 2 s = silhouette(X, idx); valid_s = s(~isnan(s)); mean_s = mean(valid_s); fprintf('噪声占比: %.1f%%\n', noise_ratio); fprintf('平均轮廓系数: %.3f\n', mean_s); else warning('簇数不足,无法计算轮廓系数'); end轮廓系数不是越大越好,而是要看数据形态来解读。对凸形好的簇,0.6以上可以说优秀;对细长簇,0.3到0.5已经说得过去,因为轮廓系数的计算基于到最近簇的距离,对非凸形状天然有偏见。所以我在仿真报告里从不单独依赖轮廓系数,而是三个指标一起看:噪声占比过高说明eps偏小,把低密度区域的点都误杀成噪声;噪声占比为0说明eps偏大,把本该留白的地方也并进了簇;簇数明显偏离预期说明参数或数据本身有问题。
仿真数据有一个天然优势:你知道真实噪声点有60个,占比约21.4%。聚类后如果噪声占比远大于21%,说明eps偏小;远小于21%,说明eps偏大,把噪声并进簇了。真实场景没有这个标签,那就用绝对值判断,一般认为5%到30%是可接受区间,超过50%说明这次聚类基本没有意义。
上面代码里那个防呆处理值得单独强调:当某个簇只有一个样本时,轮廓系数返回NaN,直接取mean会把整个平均值污染成NaN。这种情况在高噪声数据里很常见,先isnan剔除非法值再求均值,脚本才不容易翻车。
4. 参数调优与避坑指南:eps和MinPts的确定方法
4.1 用k-distance图定eps:拐点在哪,eps就取在哪
我最早调试DBSCAN时最常吃的亏,就是凭肉眼猜eps。数据分布稍微不均匀,肉眼几乎必错。一个可靠的替代方案是画k-distance图:对每个样本,找到离它第k近的邻居的距离,把这些距离从大到小排列成一条曲线,曲线的拐点就是eps的合理上限。这里的k通常取MinPts-1,也就是5-1=4。
% 画k-distance图辅助定eps k = minPts - 1; D = pdist2(X, X); D_sorted = sort(D, 2, 'ascend'); k_dist = D_sorted(:, k + 1); % 第一列是自身距离0,所以取第k+1列 sorted_kdist = sort(k_dist, 'descend'); figure('Name', 'k-distance图'); plot(1:length(sorted_kdist), sorted_kdist, 'b-', 'LineWidth', 1.5); xlabel('样本编号(按距离降序)'); ylabel(sprintf('第%d近邻距离', k)); title('k-distance图:取拐点对应的纵坐标作为eps'); grid on;解读这张图有一点经验性:曲线前段陡峭下降,对应的是簇内样本,它们周围密密麻麻全是邻居,到第4近邻的距离都很小;曲线尾部平缓,对应的是稀疏区域和噪声点,它们到第4近邻的距离很大且逐渐饱和。真正的分界处就是曲线从陡变缓的“膝盖”位置,膝盖对应的纵坐标就是eps。以3.2节的数据为例,膝盖大致落在0.5至0.6之间,最终取0.55正好落在范围内。如果曲线上没有明显膝盖,说明数据要么整体太均匀,要么整体太稀疏,这种情况不建议硬上DBSCAN,属于数据本身的密度结构问题。
提示:k-distance图对NaN和重复点很敏感。数据里含有NaN时,pdist2的结果会错乱;重复点会把排序曲线的尾部顶得非常高。做图之前先检查每列是否含NaN,如果有重复点,先给坐标叠加一个极小的随机扰动再计算。
4.2 三种翻车现象:拆簇、并簇、全噪声
DBSCAN调参翻车基本可以归成三种,按“现象→原因→解决”的顺序一条条说。
第一种翻车叫拆簇。现象是一个本应连续的簇被切成好几块,图上出现大量同色碎片,噪声数量也明显偏多。原因是eps取小了,核心点与核心点之间的“桥梁”断了,密度连接延伸不过去。解决的常见做法是把eps往大调,从k-distance图拐点附近偏小的位置,调整到偏大一点的位置,簇通常会恢复连贯。
第二种翻车叫并簇。现象是两个本来分离的簇之间被一条细密的点链连接起来,整体看起来像一只沙漏。原因是eps取大了,两个簇边界之间的稀疏点也进入了彼此的邻域范围,把密度连接桥接起来。解决方法是缩小eps,关键是要避开k-distance图上那段平缓平台里偏大的取值。比如膝盖在0.5,就不要为了“让簇更大”取到0.8。
第三种翻车叫全噪声。现象是聚类结果几乎全是-1,只有零星几个小簇。原因是eps太小和MinPts太大叠加,核心点数量少到簇扩展根本无法启动。解决方法是先降MinPts:二维数据降到3试试,同时把eps抬到第k近邻距离的中位数附近,一般能救回来。
每次翻车都用k-distance图快速定位,这是我调试的固定流程:先画图,找到膝盖区间,然后固定MinPts=5只调eps。这比凭感觉猜快很多,也算是我个人的血泪经验。
4.3 数据量放大后的内存坑:从pdist2到knnsearch
当样本数从几百放到几千甚至上万时,pdist2的全距离矩阵会迅速变成内存杀手。以N=3000为例,距离矩阵是3000×3000,double类型占用约34MB,勉强能接受;N=10000时,占用约763MB,接近MATLAB默认内存上限,脚本可能卡死或直接报错。这个坑我第一次做大样本实验时踩过,后来改成只保留最近邻信息就好很多。
% 用knnsearch替代全距离矩阵,降低内存与计算量 kQuery = minPts * 3; % 只取前若干近邻 [idxNear, distNear] = knnsearch(X, X, 'K', kQuery); coreFlagsFast = distNear(:, minPts) <= eps;coreFlagsFast的判定逻辑是:如果到第minPts个近邻的距离都不超过eps,说明邻域内至少有minPts-1个其他点,加上自身正好满足核心点条件。这个方案不需要构造全矩阵,复杂度从O(N²)降到O(Nk)。代价是如果某个点的真实邻域点数超过kQuery,它的部分邻域信息被截断了,但这种点通常是超高密度区域的点,对判定它自己是核心点没有影响,实际误差很小。
如果数据量大到knnsearch也吃力,我建议先做降维,或者对数据分块处理。但那就超出“仿真”的范畴了,仿真场景下几千个点最终是够用的。写进报告的时候,我会明确标注:“小样本验证使用全距离矩阵,大样本扩展使用近邻搜索”,评审看到这条会认为你考虑过扩展性。
4.4 环境依赖坑:工具箱缺失与msvcp140.dll报错
算法写完后,环境问题往往比算法问题更让人头疼。第一个高频报错是“未定义函数或变量dbscan”,原因非常直白:内置dbscan函数从R2019a开始提供,而且属于Statistics and Machine Learning Toolbox。如果你用的是老版本MATLAB,或者安装时没有勾选这个工具箱,函数自然找不到。解决办法三条:升级MATLAB并安装对应工具箱;改用前面写的手写版本,它只依赖基础环境;或者临时用clusterdata函数里的可选方法顶着跑完当前实验。
第二个高频报错是MATLAB启动时提示“由于找不到msvcp140.dll无法继续执行代码”。这是Windows系统缺少Microsoft Visual C++ 2015-2022 Redistributable运行库导致的,MATLAB启动阶段依赖这个运行库,缺失时程序根本打不开。解决办法不是重装MATLAB,而是去微软官方下载对应的vc_redist.x64.exe安装包,装好后重启系统。这个问题和算法无关,但一旦碰到,会卡掉一整天。
环境问题最烦人的地方在于它不看代码逻辑,报错信息也很让人摸不着头脑。我的处理习惯是,拿到一个新环境先跑一个最小脚本确认基础功能正常,再跑内置dbscan的最小示例确认工具箱可用,最后才上完整仿真。这个“由小到大”的流程能把环境问题从算法问题里彻底剥离出来。
4.5 一份可照抄的调参与检查清单
把上面这些经验压缩成一张可执行的清单,对新手来说是最省事的收尾方式。
第一步,固定MinPts。二维数据初始取5,三维以上取2d到3d,先不要动它。第二步,画k-distance图,取拐点纵坐标作为eps初始值。第三步,用这个组合跑一次仿真,记录簇数、噪声占比、平均轮廓系数三个指标。第四步,检查翻车现象:如果簇碎片化,eps上调10%重跑;如果簇黏连,eps下调10%重跑;如果全噪声,MinPts先减到d+1再重跑。第五步,把两组较合理参数各跑一遍,选噪声占比落在5%到30%之间且轮廓系数更高的那组作为最终结果。
这套流程的核心思想是“一次只动一个变量”。每次只调eps或只调MinPts,才能看清每个参数对结果的具体影响。写报告时,这份调参过程本身就是“方法”环节最好的素材,不需要额外编排。
5. 进阶用法:把DBSCAN仿真从“跑通”做到“可信”
前面几章解决了“跑通”,最后聊怎么把仿真结果做得更可信、更适合拿去汇报或写进文档。三个小技巧,按收益从高到低说。
第一个技巧是给聚类结果画凸包轮廓。散点图能看出大致分群,凸包会让簇边界变得非常明确,特别适合展示细长簇的形状。MATLAB里用convhull计算凸包顶点,再用patch把多边形叠画在散点之上。
% 给每个簇画凸包边界 hold on; for c = 1:max(idx) pts = X(idx == c, :); if size(pts, 1) >= 3 k = convhull(pts(:,1), pts(:,2)); patch(pts(k,1), pts(k,2), 'none', 'EdgeColor', [0.3 0.3 0.3], ... 'LineWidth', 1.2, 'FaceAlpha', 0.05); end end这里要求每个簇至少3个样本才画凸包,因为少于3个点convhull会直接报错,而DBSCAN结果里出现1到2个点的“小簇”并不少见。FaceAlpha设成0.05可以让多边形半透明,不会把底层散点完全盖住。
第二个技巧是可复现实验。仿真最怕“换台电脑结果不一样”。可复现的关键是统一随机种子,我把rng(7)放在所有随机生成代码的最前面,并且整个脚本里不再次重置随机状态,保证每次运行生成同一份数据。另一个容易被忽略的点是数据顺序:自实现BFS版本的遍历结果受输入顺序影响,所以主脚本里打乱顺序那行代码要保留,让所有实验基于同一份数据顺序。
第三个技巧是把结果沉淀成.mat文件。很多人只留截图,需要补实验或重新画图时只能全部重跑。更合理的做法是每次跑完把X、idx、eps、minPts、轮廓系数全部保存下来,后续load回来直接出图。
save('dbscan_result.mat', 'X', 'idx', 'eps', 'minPts', 'mean_s');这个习惯相当于给实验留一颗后悔药。我最早做仿真时只截图,后来被一次漫长重复的调参过程逼着改了习惯,现在每跑一组参数都会把中间结果存下来。多花一秒钟,后面省的时间是几小时起跳。
最后说一点我自己的体会:DBSCAN仿真值得做的,不是那张聚类图本身,而是让你真正看懂“密度”这两个字在算法里怎么变成计算步骤。跑通一次之后,你再去看论文里的改进聚类算法,不会再觉得它是个黑箱,因为你已经亲手把它拆开过一次:k-distance图、核心点标记、BFS扩展,每一步都是明明白白的代码和参数。希望这篇笔记能帮到你,让你少走几条我走过的弯路。
本文还有配套的精品资源,点击获取