简介:这是一套面向高校本硕博学生及算法初学者的DBSCAN数据聚类MATLAB仿真资源,围绕密度聚类原理,提供可运行的完整工程与配套操作录像,既可用于课堂教学演示,也适合算法竞赛和毕业设计中的聚类任务参考。整个RAR压缩包共10个文件,以7个m脚本为主,包含主程序Runme_DBSCAN.m、核心聚类算法dbscan.m、纯度计算Purity.m、最优参数分析AnalyticalEps.m以及锦标赛选择等辅助模块;另有2个mat数据文件用于实验输入,1个avi操作演示视频专门展示环境配置和运行步骤,合计882KB,结构紧凑、目录清晰。已有1344人学习下载,读者可结合录像逐行调试,也可直接作为课程设计或论文实验的参考实现。通过这套资源,能够掌握从数据读取、聚类计算到结果评价的完整链路,并借助视频规避路径设置等常见运行问题,节省排错时间。
1. DBSCAN聚类在MATLAB仿真里到底解决什么问题:任意形状簇与噪声点的实战起点
DBSCAN(Density-Based Spatial Clustering of Applications with Noise)是基于密度的聚类算法,跟 K-means 最大的区别是它不要求簇是圆形或凸的,也不需要提前指定簇个数。在 MATLAB 里做 DBSCAN 数据聚类仿真,核心价值就两条:一是能把弧形、环形这类任意形状的数据簇完整切出来,二是自动把离群点标成噪声。这个特性让它在雷达点云、图像分割、异常检测方向的课程设计和论文仿真里特别常用。这篇笔记按一套可复现的流程来讲:先手写一个尽量不依赖工具箱的 DBSCAN 核心函数,再用合成数据集跑通仿真并出图,最后把 eps 和 MinPts 的调参与避坑记录拆开讲。适合正在做课程实验、毕业设计或论文仿真的人直接照着复现。
2. DBSCAN核心概念与MATLAB实现选型:手写代码比内置函数更适合仿真
2.1 密度直达与密度可达:DBSCAN为什么能识别任意形状簇
先建立一个基本认知:K-means 这一类原型聚类算法,本质是在欧氏距离下找"圆心",所以它输出的簇天然是凸的、球形的。一旦数据分布是半月形、环形或者带长尾的条带,K-means 会把本该属于同一簇的点拆散到不同簇里,这就是它在很多真实场景下表现差的根源。DBSCAN 换了一个思路——不找中心,而是沿着密度走。它把簇定义为"密度足够高的连通区域",只要两个点之间能通过一串互相落在邻域内的点连起来,就认为它们在同一个簇里,至于这条路径是弯是绕,完全无所谓。
在这个定义下先搞清楚几个名词。第一个是 eps 邻域:以某个样本点为中心、半径 eps 的球形区域内包含的所有点。第二个是核心点:如果某点的 eps 邻域内至少有 MinPts 个点(包含它自己),这个点就是核心点。第三个是边界点:自身不是核心点,但落在某个核心点的 eps 邻域内。最后剩下的既不是核心点、也不属于任何核心点邻域的点,就是噪声点。
簇的形成逻辑要按"密度直达"和"密度可达"来理解。如果 p 落在核心点 q 的 eps 邻域内,称 p 从 q 密度直达;如果存在一串点 q, p1, p2, ..., pn,每个相邻点都是密度直达关系,就说 pn 从 q 密度可达;两个点如果都能从同一点 o 密度可达,则称它们密度相连。DBSCAN 把互为密度相连的最大点集合当作一个簇。密度直达只要求单向落在邻域内,所以边界点也能被核心点带进簇里,簇边界因此可以是不规则的,这正是它能处理任意形状的根本原因。
做 MATLAB 仿真时,我习惯把数据想成一个 n×d 的矩阵 X。二维仿真里 x、y 两个维度的尺度必须一致,否则同一个 eps 在 x 方向和 y 方向覆盖的物理范围会差很多,后文避坑章节会专门讲这个。另外 eps 是各向同性的球形邻域,对异常尺度的特征非常敏感,这是很多人在合成数据上跑得好、一到真实数据就翻车的主要原因。
2.2 手写DBSCAN代码:核心实现与Matlab内置函数的取舍
MATLAB 从 R2019a 开始提供了内置的 dbscan 函数,放在 Statistics and Machine Learning Toolbox 里。如果你的环境是 matlab 2026b,直接调用 dbscan(X, eps, MinPts) 就能跑。但我在做仿真实验和论文对比时更愿意手写,理由有三个。第一,内置函数把算法封装成了黑匣子,你拿不到每个点的核心点标记、邻域点列表这些中间结果,画密度图、改距离度量都很受限。第二,手写版可以随意替换距离函数,比如把欧氏距离换成马氏距离或 DTW,内置函数做单元测试方便,但改实验条件不方便。第三,学校机房和旧电脑上的 MATLAB 不一定有统计工具箱,老版本甚至根本没有 dbscan 函数,手写一份可以少一道环境检查。
下面这份代码用 BFS(广度优先搜索)做簇的扩展,不依赖复杂工具箱。核心逻辑只有三个步骤:预计算距离矩阵、标记核心点、从每个未访问的核心点向外扩展。
function [idx, coreFlags] = mydbscan(X, eps, MinPts) % MYDBSCAN 基于密度的聚类算法实现(手写版) % 输入: % X : n×d 矩阵,n个样本,d维特征 % eps : 邻域半径,标量 % MinPts : 核心点邻域内最少样本数,标量 % 输出: % idx : n×1 向量,0=噪声,1..K=簇编号 % coreFlags: n×1 逻辑向量,true=该点是核心点 n = size(X, 1); idx = zeros(n, 1); % 1) 预计算距离矩阵,后续所有邻域查询都用它 D = pdist2(X, X); % 2) 逐个点统计 eps 邻域成员,判断核心点 neighborCells = cell(n, 1); coreFlags = false(n, 1); for i = 1:n nbrs = find(D(i, :) <= eps); neighborCells{i} = nbrs; if numel(nbrs) >= MinPts coreFlags(i) = true; end end % 3) 对每个尚未访问的核心点做 BFS,扩展出一个完整簇 clusterId = 0; for i = 1:n if idx(i) ~= 0 || ~coreFlags(i) continue; % 已归类或不是核心点,跳过 end clusterId = clusterId + 1; idx(i) = clusterId; queue = i; head = 1; while head <= numel(queue) p = queue(head); head = head + 1; for k = 1:numel(neighborCells{p}) q = neighborCells{p}(k); if idx(q) == 0 idx(q) = clusterId; % 边界点或核心点先归簇 if coreFlags(q) queue(end+1) = q; % 核心点入队继续扩展 end end end end end end这段代码的逻辑分三层。第一层用 pdist2 一次性算好 n×n 距离矩阵,后面查邻域时不用重复计算距离,代价是内存占用 O(n²),n 在 2000 以内完全没问题,超过 5000 建议直接用内置 dbscan 或改成按块计算。第二层遍历每个点,把距离矩阵第 i 行里所有小于等于 eps 的下标存进邻域列表,同时统计数量判断核心点。第三层是核心的 BFS 扩展:从核心点 i 出发,把它邻域内所有未归类的点纳入当前簇;如果邻域里某个点本身也是核心点,说明它还可以继续向外延伸,就入队继续遍历。边界点不会入队,但会被核心点的邻域直接吸收,所以边界点天然属于离它最近的那个密度连通区域。
保存时把整段函数存为 mydbscan.m,或者在主脚本末尾追加函数定义,MATLAB R2016b 之后支持脚本内局部函数,直接用即可。参数上,eps 的物理含义是"密度单元的半径",在后面的合成数据里 0.6 大约对应一个点间距的 1.5 倍;MinPts 的取值经验是维度 d 加 1 到 2 倍之间,二维数据取 4 到 6 比较稳,这个后面避坑章还会展开。如果你不确定 eps 取多少,不要拍脑袋,先用第 5 章的 K 距离图看一眼拐点再定。
3. 用MATLAB跑通DBSCAN聚类仿真:合成数据生成、聚类执行与可视化
3.1 构造带噪声的半月形数据集:贴近真实分布的数据形态
做算法仿真我不建议一上来就用真实数据,原因是真实数据你不知道真实的簇数、噪声比例和密度分布,算法跑出来是好是坏没法客观判断。合成数据的价值在于"标准答案在手"——我造了两个半月形簇加一批散布的噪声点,这种分布 K-means 必翻车,而 DBSCAN 应该能分离,所以它是检验算法行为的理想测试平台。
%% 生成数据集:两个半月形簇 + 高斯噪声 rng(42); % 固定随机种子,保证每次运行结果可复现 theta1 = linspace(0, pi, 300)'; % 上半圆弧角度 r1 = 5 + 0.3 * randn(300, 1); % 半径在5附近抖动 X1 = [r1 .* cos(theta1), r1 .* sin(theta1)]; % 上半月 theta2 = linspace(pi, 2*pi, 300)'; r2 = 5 + 0.3 * randn(300, 1); X2 = [r2 .* cos(theta2) + 8, r2 .* sin(theta2)]; % 下半月,向右平移8 Xn = randn(80, 2) * 3 + [4, 0]; % 80个噪声点,集中在中心区域 X = [X1; X2; Xn]; % 合并为680×2的数据集这段代码把 680 个样本分成三部分:上半圆弧、下半圆弧和噪声。rng(42) 固定随机种子非常关键,论文仿真实验里如果每次跑形状都不一样,审稿人会质疑可重复性。半径 r 加 0.3 标准差的高斯抖动,是为了让簇有"厚度",更贴近雷达点云或者传感器数据的真实分布;下半月向右平移 8 个单位,让两个簇在水平方向错开,避免它们重叠。噪声用标准差 3 的二维正态分布生成,铺在半径为 9 左右的范围内,比例约 12%,这个噪声比例对 DBSCAN 来说属于中等偏易,适合演示。
数据生成后先画一张原始散点图,确认分布符合预期再做聚类。
figure; scatter(X(:, 1), X(:, 2), 12, [0.4 0.4 0.4], 'filled'); axis equal; grid on; title('原始数据集:两个半月形簇 + 噪声'); xlabel('x'); ylabel('y');axis equal 保证 x、y 轴比例一致,否则图上的"圆"会被拉伸成椭圆,影响对簇形状的判断。这一步能提前发现两个簇是否重叠、噪声比例是否过大等问题。
3.2 执行聚类并分离噪声点:核心调用与参数设定
数据准备完毕,调用手写 DBSCAN 只需要一行,但参数设定需要提前想清楚。
%% 调用手写 DBSCAN 聚类 eps = 0.6; % 邻域半径,由 K 距离图拐点确定 MinPts = 5; % 二维数据取 5,对应最少邻域点数 [idx, coreFlags] = mydbscan(X, eps, MinPts);这一步的输出 idx 是 680×1 的整数向量,取值范围是 0 到 K,K 是检测到的簇个数。idx(i)=0 表示第 i 个点是噪声点,idx(i)=j 表示第 i 个点属于第 j 个簇。coreFlags 是逻辑向量,标记每个点是不是核心点。聚类完成后第一件该做的事是看统计量,别急着画图:
%% 聚类结果统计 K = max(idx); noiseCount = sum(idx == 0); fprintf('聚类完成: 簇数量=%d, 噪声点=%d, 噪声比例=%.1f%%\n', ... K, noiseCount, 100 * noiseCount / size(X, 1));这里的 K 通过 max(idx) 拿到,DBSCAN 在聚类过程中自动确定簇结构,不需要你事先指定。通常我会检查三个数:簇数量是不是 2、噪声数是不是 80 左右、噪声比例是否接近预设的 12%。如果这三个数和预期差很多,先别调参数,回去看数据分布和代码有没有问题,逐项检查比反复试参数更快。簇数量和噪声比例是判断聚类行为的第一组指标:K 远大于 2 说明出现了过分割,可能是 MinPts 太大;噪声比例远大于 12% 说明 eps 太小,真实点被误当成了离群点。这两个坑的具体表现和修法在第 4 章展开。
3.3 用颜色标签可视化簇与噪声:绘图与后处理
聚类结果只有数字不够直观,仿真报告和论文里最需要的是可视化。这里用手动循环画图而不是 gscatter,好处是颜色控制完全自由,也不依赖统计工具箱的图形函数。
%% 可视化聚类结果 figure; colorList = lines(max(idx)); % 生成跟簇数一样多的颜色 for c = 1:max(idx) scatter(X(idx == c, 1), X(idx == c, 2), 15, colorList(c, :), 'filled'); hold on; end noiseMask = (idx == 0); scatter(X(noiseMask, 1), X(noiseMask, 2), 20, 'k', 'x'); % 噪声点用黑叉 hold off; box on; grid on; title('DBSCAN 聚类结果(黑色 x 为噪声点)'); xlabel('x'); ylabel('y');图上有两个清晰的弧形簇,各自用一种颜色填充,中心区域的散乱黑叉就是噪声点。这样一张图配合上一节的统计输出,就能在实验报告里形成完整的证据链:原始数据 → 聚类代码 → 结果图 → 指标统计。可视化环节有一个容易被忽视的注意点:scatter 的坐标轴范围如果和原始数据不一致,会误导读者。用 xlim 和 ylim 把坐标范围控制在数据实际范围内,能避免图形被拉伸或裁切。另外,如果你希望把结果存下来,在绘图后加一行:
exportgraphics(gcf, 'dbscan_result.png', 'Resolution', 300);300 DPI 的 PNG 是论文插图的最低要求,docx 报告用 300 DPI 也够清晰。
4. DBSCAN仿真调参避坑记录:eps、MinPts和版本差异的5个翻车现场
调参这件事,理论是一回事,跑起来是另一回事。下面 5 条都是我做仿真时真实踩过的坑,每条按现象 → 原因 → 解决的方式记录。先给一个速查表,后面逐条拆:
| 参数/环境 | 建议取值 | 主要作用 | 过小风险 | 过大风险 |
|---|---|---|---|---|
| eps | 由 K 距离图拐点确定 | 邻域半径 | 噪声点激增 | 簇被合并 |
| MinPts | dim+1 到 2×dim | 核心点判定阈值 | 簇被拆碎 | 噪声增加 |
| 数据尺度 | 归一化后聚类 | 保证各向同性 | 高量纲维度主导 | 低量纲维度失效 |
| MATLAB 版本 | R2019a 以上有内置 dbscan | 内置函数可用性 | 旧版本报错 | — |
4.1 现象:eps设太小,300个点全部判成噪声
我第一次跑仿真时把 eps 拍脑袋设成 0.05,结果是簇数量为 0、噪声比例 100%,图上所有点全是黑叉。原因很直接:eps 小于数据点之间的实际间距,任何一点的邻域里只有它自己,数量达不到 MinPts=5,于是全部变成噪声。解决方法是先用 K 距离图找拐点(第 5 章细讲),实在不济也可以用近似法:算出所有点之间的欧氏距离,取其中一个小分位数(比如 0.05 分位)当 eps 的初值,再结合结果微调。这里有个常见误解——eps 不是越小越"精细",它直接决定哪些点算核心点,太小的 eps 会让算法彻底失去聚类能力。
4.2 现象:MinPts设太大,一个整簇被拆成三块
有人为了让结果更"稳",把 MinPts 设成 30 或 50,结果一个连续的弧形簇被拆成三段,段与段之间还出现了噪声点。原因是 MinPts 越大,成为核心点的门槛越高,弧形的两端和边缘地带密度本来就低,达不到核心点条件,BFS 扩展链就从中断开了。解决方法是遵守经验公式:MinPts 取 dim+1 到 2×dim,二维场景就是 4 到 6,我通常用 5。注意 MinPts=1 也没有意义,那样每个点自己就能成核心点,邻域内任何点都会被无脑吸入,聚类结果退化成连通域分析,噪声点几乎消失。
4.3 现象:数据不归一化,eps在x方向失效
之前提到过 eps 是各向同性的球形邻域,一旦 x 和 y 的尺度不一样,这个球在两个方向上覆盖的物理范围就完全不同。比如 x 的范围是 0~100,y 的范围是 0~1,同样的 eps=0.5,在 y 方向能覆盖整个数据范围,x 方向上连一个点间距都够不着。结果就是聚类只按 y 方向的密度切分,x 方向的信息基本被丢弃。解决方法是聚类前做标准化:
X = zscore(X); % 每列零均值、单位标准差或者用 min-max 归一化。做完之后 eps 的选择也要在归一化后的空间里重新用 K 距离图看,别拿原来的 0.6 继续用。这个坑在图像分割、多特征融合的场景里特别常见,因为不同特征的量纲往往差的不是一点半点。
4.4 现象:老版本MATLAB直接报错Undefined function 'dbscan'
如果你的 MATLAB 版本在 R2019a 之前,或者 matlab 安装时没有勾选统计与机器学习工具箱,直接调用 dbscan 函数会报错。这是因为内置 dbscan 是 R2019a 才加的,依赖统计工具箱支撑,并不是 MATLAB 基础功能。解决方式有两种:一是用文中手写的 mydbscan,它只依赖距离矩阵和基础矩阵操作,绝大多数 MATLAB 环境都能跑;二是从 matlab 下载安装最新版(比如 matlab 2026b)并勾选统计与机器学习工具箱。学校里做毕设,机房机器可能没这个工具箱,手写版反而是最稳妥的。
如果 pdist2 在你的环境里也不可用,比如报错提示函数未定义,把距离矩阵计算改成两层循环:
n = size(X, 1); D = zeros(n, n); for ii = 1:n for jj = ii+1:n D(ii, jj) = sqrt(sum((X(ii, :) - X(jj, :)).^2)); D(jj, ii) = D(ii, jj); end end利用矩阵对称性只算上三角,省一半时间。数据量到几千个点也扛得住。
4.5 现象:不同密度簇混在一起,单一eps顾此失彼
DBSCAN 隐含的假设是全局密度大致均匀。如果数据集里一个稠密簇的点间距是 0.1,另一个稀疏簇的点间距是 5,你用同一个 eps 去跑,总有一个簇被拆散或者两个簇被合并。这不是参数没调对,是算法本身的局限性。处理思路有三个:其一,聚类前对数据做分块或先验的密度分区,对不同区域分别跑 DBSCAN;其二,换用 OPTICS 算法,它是在 DBSCAN 基础上引入可达距离,能处理密度不均的情况;其三,如果允许分批处理,可以先给稀疏簇单独调参跑一遍,再把稠密簇单独跑一遍,最后合并标签。做仿真时如果出现这种情况,实验报告里诚实说明"数据密度差异过大,DBSCAN 不适用"反而比强行调参更可信。
5. DBSCAN仿真结果的验证技巧:K距离图定eps与轮廓系数评估
5.1 K距离图:用排序距离曲线挑出eps临界点
eps 不该拍脑袋定,K 距离图是工程上最常用的工具。它的原理是:计算每个点到它第 k 个最近邻的距离(k 取 MinPts 或 MinPts-1),把这些距离降序排列,画成曲线。曲线在某个位置会出现明显的拐点,拐点对应的 y 值就是 eps 的候选值。
%% K距离图确定 eps k = MinPts; % 通常取 MinPts 或 MinPts-1 D = pdist2(X, X); kDist = zeros(size(X, 1), 1); for i = 1:size(X, 1) sortedDist = sort(D(i, :)); kDist(i) = sortedDist(k + 1); % 第 k 个近邻距离(不含自身) end kDist = sort(kDist, 'descend'); plot(kDist, '.-'); grid on; xlabel('点编号(按第k近邻距离降序)'); ylabel('第k近邻距离');为什么曲线会出现拐点?因为簇内核心点的第 k 近邻距离普遍小,而噪声点的第 k 近邻距离会突然变大,两类点的交界处就形成一个明显台阶。这个台阶对应的 y 值,就是核心点和噪声点在密度意义上的分界线。我自己做实验的习惯是先画这张图,然后从拐点附近读一个值出来跑聚类,再根据簇数和噪声比例微调。这样每个参数都有依据,写论文时也能直接贴图说明。
5.2 轮廓系数验证聚类质量
聚类结果不能只看图,定量指标才能说明问题。轮廓系数的计算方式是:对每个样本,算它到同簇其他点的平均距离 a,再到最近其他簇所有点的平均距离 b,轮廓系数 s=(b-a)/max(a,b)。s 越接近 1,说明簇越紧凑、簇间分离越明显;接近 0 说明样本在簇边界上;接近 -1 说明可能分错了簇。
validIdx = (idx > 0); % 先剔除噪声点 s = silhouette(X(validIdx, :), idx(validIdx)); fprintf('平均轮廓系数: %.3f\n', mean(s));注意必须先剔除噪声点再调 silhouette,否则噪声点会被当作一个独立的簇参与计算,把平均值拉低。实测上面那组合成数据,平均轮廓系数通常在 0.7 以上,整体属于"结构明显"的水平。最后补充一个个人习惯:我在实验记录里会把 K 距离图、eps 取值、聚类结果图和轮廓系数四样东西放在同一页,这样每个参数都能被回溯。调参数最怕的就是只留一个好看的结果图,等答辩被问到"eps 为什么是 0.6"就只能支支吾吾。
这个方向做下来,我的体会是:DBSCAN 的仿真不难,难的是一套能自圆其说的参数来源和结果验证。希望你也能把参数依据留清楚,让实验经得起追问。希望帮到你。
本文还有配套的精品资源,点击获取