简介:这份资源提供DBSCAN聚类算法的MATLAB实现代码,面向需要开展密度聚类与无监督学习的数据分析学习者、科研人员和课程设计者。它通过ε邻域半径和MinPts最小邻域点数两个关键参数,自动识别任意形状簇并区分噪声点,适合处理分布复杂、噪声较多的多维数据集。压缩包内共3个文件,均为.m脚本,整体仅4KB,包含核心聚类算法、测试数据调用与聚类结果可视化等模块,结构精简,便于直接运行和二次修改。已有2534人学习下载。借助代码,读者可快速理解邻域搜索、核心点扩展、边界点归类及噪声标记的完整流程,并通过调整参数观察聚类结果变化,加深对DBSCAN原理的认识;同时也能将其迁移到自己的数据上,完成自动分组与初步的数据分布洞察,为后续建模分析提供参考。
1. 拿到DBSCAN聚类算法matlab代码之前,先想清楚这三件事
手里有一堆散点坐标,想按密度分成几堆,还希望把离群点单独挑出来——这时候DBSCAN聚类算法往往比K-Means更合适,因为它不假设簇是圆的,也不需要预先指定簇数量。很多做点云分割、轨迹停留点识别、客户位置分群的工程师,最终都在MATLAB里留一份可改的DBSCAN聚类算法代码,原因就一条:密度聚类这个需求几乎每周都会遇到,自带的现成函数不好好调参也一样翻车。
这篇文章不跟你背概念,直接从落地角度把一套能跑的DBSCAN聚类算法matlab代码讲透:Eps和MinPts到底怎么定,自实现和内置函数有什么差距,跑出来不对劲时先查什么。读完你可以把示例数据换成自己的坐标点,半小时内跑出第一版聚类结果。适合正在做数据预处理、空间点分析、异常点筛选的工程师,也适合课程设计想找一份能看懂能改的matlab代码的学生。
2. DBSCAN聚类算法的参数与选型:Eps、MinPts和三类点的判定逻辑
2.1 邻域半径Eps与最小点数MinPts:两个参数为什么决定聚类结果
DBSCAN的全称是Density-Based Spatial Clustering of Applications with Noise,核心思想用一句话概括:密度足够高的区域连成一个簇,密度不足的点标成噪声。判断一个点周围密度高不高,只需要两个参数。第一个是邻域半径Eps,决定“看多远”;第二个是最小点数MinPts,决定“多少个点算密”。一个点周围Eps半径的范围内,如果包括它自己在内不少于MinPts个点,这个点就是核心点;两个核心点之间距离不超过Eps就认为连通,连通的整片区域归为同一个簇。
这里有新手最容易对不上的一个细节:MinPts是包含自己的。用MATLAB写的时候,find(D(i,:) <= eps)取出来的邻居数量天然包含自身,直接用numel统计没问题。但如果你参考某些Python实现,先算dist < eps再排除自身,那阈值就要加1。同一个MinPts,两种写法会导致完全不同的聚类结果,这是代码比对时最隐蔽的坑。
为什么两个参数就能撑起整个算法?因为DBSCAN对密度的描述是局部的。Eps相当于一把尺子,MinPts相当于一道门槛,每个点只在自己的局部范围内判断密度,不依赖全局分布假设。所以同一个Eps和MinPts,在密度高的区域能聚出紧凑小簇,在密度低的区域也能聚出松散大簇,互相不干扰。这个特性让DBSCAN在面对密度不均的真实数据时,比依赖全局均值的K-Means稳得多。
2.2 核心点、边界点、噪声点:三类点在代码里的判定顺序
DBSCAN把所有点分成三类,代码的执行顺序也是围绕这三类展开的。核心点是它自己Eps邻域内点数不小于MinPts的点;边界点本身不满足核心条件,但落在某个核心点的Eps邻域内;两者都不是的就是噪声点。
一类算法里有个容易被忽略的设计:噪声点在聚类过程中是“暂时”的,不是一锤定音。当一个尚未归属任何簇的噪声点被某个簇扩展时触达,它会被吸收为边界点,归入该簇。所以在自实现代码里,先标成-1的点,后面还有翻身机会。理解这个顺序,你调试时看到“开始是噪声、最后归了簇”就不会觉得是bug。
实际代码执行顺序是固定套路:第一步,算所有点两两之间的距离矩阵;第二步,顺序遍历没访问过的点,找它的Eps邻居;第三步,邻居数不够MinPts就临时标噪声;第四步,邻居数够就开一个新簇,把邻居放进队列;第五步,从队列里取点,重复找邻居和扩簇的动作,直到队列清空;第六步,回到第二步继续找下一个没访问的点。整个流程理解起来不复杂,但实现上有一个关键选择:用什么数据结构去存储“哪些点是邻居”,这直接关系到代码会不会卡死。
2.3 跟K-Means和层次聚类比,DBSCAN在什么场景是唯一解
选聚类算法不是看哪个名头大,而是看数据长什么样。K-Means默认所有簇是凸的、大小接近的球形,而且你必须先告诉它簇数量K;层次聚类不需要预设K,但复杂度高,对大样本不友好,对噪声点也没有特殊处理。DBSCAN则不预设簇形状,自动发现簇数量,还能同时输出噪声点列表。
| 对比项 | DBSCAN | K-Means | 层次聚类 |
|---|---|---|---|
| 簇形状假设 | 任意密度形状 | 凸球状 | 依赖链接准则 |
| 簇数量 | 自动确定 | 必须预设K | 需要切树看层次 |
| 噪声点处理 | 显式标出 | 强行分进某个簇 | 强行分进某个簇 |
| 主要参数 | Eps、MinPts | K、初始质心 | 链接方式、距离 |
| 时间复杂度 | O(n²) 朴素版 | O(n·K·迭代) | O(n³) 或O(n²) |
| 适用场景 | 空间点群、轨迹、异常检测 | 均匀球状簇、特征分类 | 小样本、层次探数 |
我一般这样判断:如果数据是经纬度坐标、设备位置、轨迹点,或者明显有大量离群点,直接选DBSCAN;如果是均匀分布的特征向量,簇大小差不多,先试K-Means。真正的分水岭是“噪声”。K-Means会把噪声点硬拽进最近的簇,拖偏质心;DBSCAN会把它单独拎出来,这个行为在异常检测场景里是不可替代的取舍。
3. 用MATLAB跑通DBSCAN聚类算法:自实现、内置函数与参数定标
3.1 自实现最简版DBSCAN:距离矩阵加邻域扩张,看得懂核心逻辑
先把自实现代码贴出来。这段代码我刻意保留了最朴素的写法,每个点的邻居都通过距离矩阵查,不玩任何加速技巧,目的是让你一眼看懂DBSCAN的传播过程。代码注释我没有用中文,免得旧版MATLAB中文注释乱码,你复制到自己新版环境里想改成中文随意。
function [idx, isnoise] = dbscan_custom(X, eps, MinPts) % DBSCAN_CUSTOM 最简实现,输入样本矩阵X(n行d列) % 输出idx:n行1列,簇编号从1开始,-1表示噪声点 n = size(X, 1); idx = zeros(n, 1); visited = false(n, 1); clusterId = 0; % 预计算距离矩阵,n=5000时约200MB,再大就要小心 D = pdist2(X, X); for i = 1:n if visited(i) continue; end visited(i) = true; % 找i的Eps邻居,find结果包含i自己 neighbors = find(D(i, :) <= eps); if numel(neighbors) < MinPts idx(i) = -1; % 暂时标噪声,后续可能被吸收 continue; end % 新建簇,把i的邻居放进队列等待扩展 clusterId = clusterId + 1; idx(i) = clusterId; queue = neighbors; while ~isempty(queue) p = queue(1); queue(1) = []; if idx(p) == -1 % 噪声点被簇扩展到,吸收为边界点 idx(p) = clusterId; continue; end if visited(p) % 已属于其他簇或已经处理过,跳过 continue; end visited(p) = true; idx(p) = clusterId; % 如果p也是核心点,它的邻居继续入队 pn = find(D(p, :) <= eps); if numel(pn) >= MinPts queue = [queue, pn]; end end end isnoise = (idx == -1); end这段示例代码最需要看懂的是队列扩展的边界条件。队列里可能出现三种点:尚未访问的点、标成-1的噪声点、已经归属其他簇的点。代码里分三段处理,顺序不能调换。特别注意if idx(p) == -1必须在if visited(p)之前,因为噪声点在标-1时visited已经置为true,如果先判断visited,噪声点永远不会被吸收成边界点,聚类结果会多出一堆假噪声。
这个版本的时间复杂度是O(n²),属于“教学版”而不是“生产版”。n在2000以内用着很舒服,n超过5000就要考虑换内置函数或降采样。另外注意QUEUE用queue = [queue, pn]拼接,在MATLAB里会反复分配内存,这是性能瓶颈之一,但不影响逻辑正确性。想优化的话提前预分配队列长度,或者用手写索引指针替代,我后面在进阶部分会提。
3.2 调用MATLAB内置dbscan:代码更短但预处理不能省
如果你用的是较新的MATLAB版本且安装了Statistics and Machine Learning Toolbox,直接调用内置dbscan函数,代码量能少一半:
% 调用内置dbscan,X每行一个样本,每列一个特征 [idx, corepts] = dbscan(X, eps, MinPts); % 内置函数噪声点编号为0,核心点逻辑向量单独返回 noiseMask = (idx == 0); nClusters = max(idx); fprintf('簇数量:%d,噪声点数量:%d\n', nClusters, sum(noiseMask));内置函数返回值有两个:idx是每个点的簇编号,corepts是逻辑向量,标记哪些点是核心点。噪声点的编号在不同版本里统一为0,这一点和自实现代码中我用-1不一样,换用的时候记得改过滤逻辑。
内置dbscan的优势不只是代码短,它对距离计算有更省内存的实现,内部不会真的生成一个n×n的完整矩阵,数据量稍大时不至于直接卡死。但要注意,内置函数只负责聚类,不做标准化、不做参数选择、不替你判断数据量纲。X列之间的尺度差异它一样照单全收,该出的问题一个不少。所以我一直觉得,内置函数是把双刃剑——跑得快了,但离数据本身也更远了。
如果你想把自实现代码的结果跟内置函数对齐做验证,可以这样检查:两个结果簇编号顺序可能不一样,不能直接比编号是否相等。正确做法是计算调整兰德指数(adjusted Rand index),或者只比较噪声掩码isnoise是否一致,后者在大多数情况下足够说明问题。
3.3 Eps和MinPts怎么定:一次讲完的实操顺序
参数选择是DBSCAN落地最大的门槛。我的经验是不要凭感觉猜Eps,而是让数据说话。第一步先做标准化;第二步画k距离图,通过拐点找Eps的起点;第三步按密度修正MinPts;第四步跑完看噪声比例和簇数量,微调。
先写一个生成k距离图的函数,这是定Eps最常用的手段:
function sortedK = k_distance_plot(X, k) % 画出k距离图,用于估计Eps出发点 % k一般取MinPts,常见取5 [d, ~] = pdist2(X, X, 'euclidean', 'Smallest', k+1); % d最后一行是每个点到第k个最近邻居的距离 kthDist = d(end, :)'; sortedK = sort(kthDist, 'descend'); plot(1:numel(sortedK), sortedK, 'LineWidth', 1.5); grid on; xlabel('点编号(按k距离降序)'); ylabel(sprintf('第%d近邻居距离', k)); end这个函数最大的好处是不生成完整距离矩阵,pdist2配合Smallest选项只保留每个点最近的k+1个距离,内存开销从O(n²)降到O(n·k),几万个点也能跑。画出来的图是一条下降曲线,在某个位置会出现明显的拐点,拐点右侧的纵坐标值就是Eps的初始值。注意拐点有时不明显,这时候宁可把Eps选小一点,让噪声多一点,再逐步放大,不要一次选太大,否则所有簇会连成一片。
MinPts的定法相对简单。维度越低,MinPts可以越小;两个特征时取2*dim也就是4到5偏稳,高维数据取2*dim或更大。MinPts选太小的后果是每个点都容易成为核心点,噪声全被吸收;选太大的后果是真正的核心点不够格,簇被拆得稀碎。我一般先取5跑一遍,看噪声比例,如果噪声超过15%,就检查是Eps太小还是MinPts太大。
还有一个经常被忽略的步骤:聚类前对每一列做zscore标准化。DBSCAN的距离计算对量纲极其敏感,坐标是经纬度和面积混合在一起时,面积那一列会彻底主导距离,Eps无论怎么调都无解。除非你的业务语义要求原始尺度,否则先标准化再聚类,能避开一多半参数翻车。
4. DBSCAN聚类算法避坑手册:五个高频翻车现场和修复顺序
4.1 聚类结果只出一个大簇,噪声点是零
现象:跑完结果只有一个簇,所有点全部归进去,噪声点一个没有,簇质量肉眼可见地差——不同区域的风马牛不相及的点全连在一起。
原因:Eps设得比数据尺度大太多。当每个点周围都能轻松找到MinPts个邻居时,所有点都成了核心点,簇与簇之间的边界被直接跨过,整个数据集被连成一片。
解决:回到k距离图重新定Eps,把拐点对应的距离值缩小30%再试。比如图上拐点在0.8,先按0.5跑,观察噪声比例是否回升到10%左右,再微调。血泪经验:新手最喜欢把Eps从0.5一路加到5,这是最典型的翻车路径,结果只会越来越糟。
4.2 噪声点比例畸高,簇碎成渣
现象:30%以上的点被标成噪声,原本肉眼能看到的连续点带也被拆成好几个碎片簇。
原因:MinPts太大或者Eps太小,两者都会导致核心点数量锐减。核心点少了,簇就没有扩展的“种子”,边界点全被丢弃成噪声。
解决:把MinPts降到4到5,同时把Eps调大到k距离拐点附近,看碎片是否合并。一个标准检查动作:打印核心点占总样本的比例sum(corepts)/numel(corepts),如果低于15%,说明参数组合太苛刻,放宽其中一个。
4.3 坐标单位不一致,距离矩阵算出来的密度是错的
现象:把经纬度和“面积”“楼层数”等特征混在一个矩阵里聚类,结果聚出来的簇沿着数值最大的特征方向被拉长,完全看不出空间形状。
原因:DBSCAN的距离是欧氏距离,各维度的量纲不齐时,数值范围大的特征会支配整个距离计算结果。Eps只对“大数值特征”敏感,“小数值特征”直接被淹没。
解决:聚类前用zscore对每列做标准化,再算距离。对经纬度点做聚类时更推荐先投影到合适的平面坐标系,再做标准化。注意标准化之后Eps的取值范围也变了,之前1.5的经验值不再有效,必须重新画k距离图。
4.4 数据量稍大就卡死,MATLAB直接无响应
现象:样本量到两三万,自实现代码跑了几分钟没结果,任务管理器里MATLAB内存占用拉到几个GB。
原因:pdist2(X, X)要生成n×n的双精度矩阵,2万个点是3.2GB,3万个点超过7GB,直接吃爆内存。这是我前面一再提醒的O(n²)瓶颈。
解决:先降采样调试参数,用datasample(X, min(n, 10000))取1万个点把Eps和MinPts定好,再全量跑一次。全量跑的时候务必用内置dbscan,不要用自己的教学版。另外可以试分块计算,但完整的分块DBSCAN实现复杂度不低,对大多数人来说降采样是性价比最高的方案。
4.5 换了一个数据集,之前调好的参数全部失效
现象:同一个型号的数据,在A区域聚类效果很好,切到B区域全是噪声,或者全变成一个簇。
原因:Eps是绝对距离参数,数据密度变了、坐标范围变了,Eps的合适取值跟着变。没有全局最优的Eps,只有“对当前数据集合适的Eps”。
解决:把参数选择流程脚本化,每次拿到新数据先画k距离图,再把图上的拐点当作Eps起点来调,整个过程重复几轮以后会形成习惯。永远不要直接复制上一个项目的参数到新数据集,这不是偷懒能省的步骤。
5. 进阶验证:用环形数据、轮廓系数和分块思路把DBSCAN代码用扎实
5.1 构造两个交错的圆环数据,验证实现是否正确
验证聚类算法最有力的数据是“嵌套圆环”——两组点分别落在半径不同的环上。K-Means在这种数据上必翻车,因为它假设簇是凸的;而DBSCAN沿着环的密度连通,理论上能完整拆开两个环。这是检验代码逻辑是否正确的经典场景。
% 生成两个同心圆环样本,每组500个点 rng(0); theta1 = linspace(0, 2*pi, 500)'; ring1 = [4*cos(theta1), 4*sin(theta1)] + randn(500, 2) * 0.08; theta2 = linspace(0, 2*pi, 500)'; ring2 = [6*cos(theta2), 6*sin(theta2)] + randn(500, 2) * 0.08; X = [ring1; ring2]; % 用自实现代码聚类 [idx, ~] = dbscan_custom(X, 0.3, 5); figure; gscatter(X(:,1), X(:,2), idx); title('DBSCAN on two concentric rings');如果实现正确,idx会得到两个簇,环形边界清晰。你把同样的数据丢给K-Means试试,结果会是把环切成一瓣一瓣的扇形,或者两个环混在一起无法区分。这一张图就能看出DBSCAN的核心价值,也是你判断自实现代码有没有写错的最直观测试。
5.2 轮廓系数评估:噪声点要过滤后才算
有聚类结果之后需要定量评估,轮廓系数(silhouette)是最常用的指标之一。但直接用silhouette函数有一个暗坑:DBSCAN的噪声点编号是-1或0,如果把噪声点一起传给函数,轮廓系数会算出一堆无意义的值,甚至直接报错。
% 过滤噪声后再算轮廓系数 mask = idx > 0; if sum(mask) > numel(unique(idx(mask))) s = silhouette(X(mask, :), idx(mask)); meanS = mean(s); fprintf('平均轮廓系数:%.3f\n', meanS); else fprintf('有效簇不足,无法计算轮廓系数\n'); end轮廓系数只对“已经成形的簇”有意义,噪声点被排除在评估之外是对的——噪声点本身就不属于任何簇,强行计算只会拉低分数。另一个注意点是当簇数量为1时,silhouette返回的全是NaN,脚本里要加判断。平均轮廓系数大于0.5说明聚类结构明显;0.3到0.5说明有重叠但要分也能分;低于0.3就该回头调参数了。还有一个附加指标看簇的稳定性:把样本随机打乱重跑几次,簇成员变化越小越好,这比单纯看轮廓系数更接近真实业务判断。
5.3 十万点级的数据怎么处理:分块与降采样的取舍
数据量到十万以上,再想用MATLAB一把梭是不可能的,内存和时间都撑不住。我常用的路径是:先降采样到2万以内做参数探索,确定Eps和MinPts之后,再对全量数据调用内置dbscan。内置函数在距离计算上的内存占用比自实现低很多,但十万点也未必吃得消,如果还是卡,就再退一步。
分块DBSCAN的朴素思路是:把空间切成网格,每块独立聚类,再合并边界区域属于同一个密度连通域的块。但合并阶段的边界匹配很容易出错,两个块各自聚出的簇在边界处对不上号,需要额外的重叠缓冲区。常见做法是块与块之间留出2倍Eps的重叠带,但这会带来重复计算,工程上并不轻松。我的习惯是:除非真的不能降采样,否则不碰分块;宁可抽8万点做聚类,把结果当近似解来用,也比卡死在完整矩阵上强。
最后说一个调试习惯。最早我用DBSCAN时也喜欢去试不同Eps组合,后来养成一个固定流程:先标准化,再画k距离图,然后小步调Eps,每次只调20%,看一眼噪声比例和轮廓系数,再决定下一步。调参不是玄学,是一套能重复的检查清单。如果这篇DBSCAN聚类算法matlab代码的经验能帮你的聚类过程少翻几次车,目的就达到了,希望帮到你。
本文还有配套的精品资源,点击获取