简介:这是一份基于改进豪斯多夫距离的DBSCAN船舶航迹聚类Matlab实现,面向船舶航迹数据挖掘、异常行为识别及聚类算法学习研究者。资源复现了论文《基于轨迹聚类的船舶异常行为识别研究》的核心流程,覆盖航迹数据提取、DBSCAN聚类、豪斯多夫距离计算、聚类中心提取、航迹预测及预测阈值寻优等环节,代码完整且可直接运行。压缩包共20个文件,以14个m脚本为主体,分别对应航迹预处理、距离度量、聚类与误差评估等模块,另含zip压缩数据、mat数据文件、png示意图及md说明,整体大小4.32MB,目录结构清晰。已有766人学习下载。通过该案例可深入理解DBSCAN如何与豪斯多夫距离结合实现航迹簇划分,掌握航迹分类的一般流程,并借助聚类结果进行航迹偏离预测;同时可替换数据和模块,拓展为自己的船舶航迹研究模型。
1. 把航迹直接丢进 DBSCAN 聚类之前:为什么默认距离会翻车
拿到这套基于改进的 Harsdorf 距离的 DBSCAN 船舶航迹聚类资源时,我建议先别急着跑 demo。它的核心难点不在聚类本身,而在“轨迹相似度怎么算才对”。实船 AIS 轨迹长短不一、采样间隔不固定,直接把坐标点丢进欧氏距离里算相似度,得到的距离矩阵基本反映不了两条船是否走同一条习惯性航线。资源里把 Harsdorf 距离做了分位数截断改进,再喂给 DBSCAN,解决的是港口航路识别、船舶异常行为分析这类问题。适合正在做 AIS 数据挖掘、交通流分析或航迹预测前处理的开发者,也适合拿来改造自己的课程设计底层距离函数。
2. 改进的 Harsdorf 距离:航迹相似度计算的正确姿势
2.1 为什么要用 Harsdorf:点对点距离在轨迹对比上的失效
先说名字。教材里标准写法是 Hausdorff,中文常译“豪斯多夫距离”。这份资源里统一用 Harsdorf 做函数名和文件名,下代码时注意区分,搜索同类实现也可以按 Hausdorff 去找,两者是同一个东西。
轨迹对比最棘手的地方是两条轨迹点数不一样,时间戳对不齐。两条船同走一条航道,A 轨迹有 200 个点,B 轨迹只有 80 个点,逐点求欧氏距离根本无从下手。把两条轨迹按时间同步插值强行对齐,又会因为停船、变速、数据丢点产生大量伪偏移。Harsdorf 距离的思路是“点到集合”的最短距离:对 A 上每个点,找 B 上离它最近的点,算出距离;反过来再做一遍,最后取大者。这样不需要点对点对齐,也不需要轨迹等长,天然适配航迹这种非均匀序列。
但原始 Harsdorf 距离在航迹场景里有个致命缺陷:它对离群点过于敏感。只要一条轨迹里的某个点因为 AIS 漂移、GPS 丢星跑到航道外两公里,这个点的最近距离就会非常大,而距离公式取的是最大值,于是整体距离被这一个点拉爆。这就是为什么资源里明确写了“改进的 Harsdorf 距离”,改进落点就在抗离群上。
2.2 改进版本切掉了什么:分位数截断的抗离群逻辑
改进的核心是拿分位数替代最大值。原始公式对“每个点到另一条轨迹的最短距离集合”取 max,改进后先把这些距离排序,然后取第 q 分位上的值,q 通常取 0.8 到 0.9。相当于把最恶劣的 10% 到 20% 的点忽略掉,只关注大部分点的横向偏移水平。
这个改动在船舶航迹聚类里非常实用。实际 AIS 数据里,船舶靠泊、避让、进出港时会出现短暂偏航,这些片段只占整条轨迹的 5% 到 15%,却不该把整条航迹归为“不同航线”。分位数截断后,少量偏航点被削掉,相似航迹的距离值显著下降,不相似航迹的距离值基本不变,聚类边界更干净。
参数 q 对结果的影响需要说清楚:q 越大越接近原始 Harsdorf,抗离群能力越弱;q 越小对偏航越宽容,但 q 低于 0.7 时两条交叉航迹也可能被算成相似,因为最核心的几何差异被一起削掉了。我一般从 0.85 开始试,看聚类结果里噪声点比例再微调。
2.3 一份可直接改用的 Matlab 距离函数
资源包里大概率的距离函数结构类似下面这样。如果没有现成的,可以直接按这个思路补一个,函数名保持和包内一致,避免引用时报错。
function h = harsdorf_improved(trajA, trajB, q) dAB = minDistToTraj(trajA, trajB); % A 每个点到 B 的最短距离 dBA = minDistToTraj(trajB, trajA); % B 每个点到 A 的最短距离 dAB = sort(dAB); dBA = sort(dBA); kA = max(1, round(q * numel(dAB))); kB = max(1, round(q * numel(dBA))); h = max(dAB(kA), dBA(kB)); end function d = minDistToTraj(P, Q) m = size(P, 1); d = zeros(m, 1); for i = 1:m d(i) = min(sqrt((Q(:,1) - P(i,1)).^2 + ... (Q(:,2) - P(i,2)).^2)); end end逻辑上先算两个方向上的单向距离,排序后各取第 q 分位,再取二者最大值作为对称距离。注意排序后取索引时要对结果做 max(1, ...),防止 q 过小导致索引 0。坐标输入是 N 行 2 列矩阵,第一列经度或 x,第二列纬度或 y。这个函数的时间复杂度是 O(m*n),两条轨迹各 200 点时一次计算约四万次距离运算,对几百条轨迹的两两矩阵计算量可接受,但上千条轨迹时建议降采样后再算。
参数这里再强调一遍:q 的取值直接改变距离分布,同一个 eps 下 q=0.7 和 q=0.9 得到的聚类簇数可能完全不同。我的习惯是先固定 q,画出距离矩阵直方图确定 eps,然后再回头调 q,避免两个参数同时乱试分不清是谁的问题。
3. 接入 DBSCAN 聚类:距离矩阵、eps 分位数与可运行脚本
3.1 DBSCAN 的密度连通逻辑为什么适合航迹
DBSCAN 不需要预设类别数,这对航迹聚类是天然优势。港口里正常航线可能是三五条,也可能是十来条,KMeans 里 K 很难拍脑袋定,DBSCAN 只需要管好密度阈值。它会把低密度区域里的样本标成噪声,对应到航迹场景就是那些乱窜的、靠泊的、只有零星几段的异常船——这些恰恰是需要单独拉出来看的对象。
但要让 DBSCAN 正确处理航迹,必须给它预计算好的距离矩阵,而不是让它自己按坐标欧氏距离算。两段轨迹在坐标空间里根本不是等长点集,直接对原始数据用 DBSCAN 没有意义。正确路径是:先两两计算出改进 Harsdorf 距离,得到 n×n 距离矩阵,再用 precomputed 模式喂给聚类函数。这也是这份资源里最关键的衔接步骤,很多人就是卡在这个距离矩阵构造上。
3.2 eps 和 MinPts 怎么定:先用距离矩阵的分位数探路
eps 是 DBSCAN 里最看“手感”的参数。常规做法是画出 k 距离图找拐点,但对航迹距离矩阵,更实用的办法是直接看分位数。把距离矩阵中非零元素排序,取 0.7 到 0.9 分位的值作为 eps 初值。这样能保证大部分同航线轨迹落在邻域内,而不至于把整个矩阵都连成一个簇。
MinPts 的取值和轨迹对数有关。航迹聚类里每个样本是一条船的一段轨迹,不是单个点,MinPts 取 3 到 5 比较常见。如果距离矩阵只有几十条轨迹,MinPts 取 3;几百条轨迹时取 5 更稳。MinPts 太大会把小组航线直接标成噪声,太小又会让两三条散轨迹抱团成簇。
3.3 完整聚类脚本与输出解释
下面这段是我按资源思路整理的可运行主流程,假设轨迹数据已经清洗好并存入 tracks 结构体数组。
load('tracks.mat'); n = numel(tracks); q = 0.85; distMat = zeros(n, n); for i = 1:n trajA = [tracks(i).lon, tracks(i).lat]; for j = i+1:n trajB = [tracks(j).lon, tracks(j).lat]; distMat(i,j) = harsdorf_improved(trajA, trajB, q); distMat(j,i) = distMat(i,j); end end % 用非零距离的分位数选 eps nonZero = distMat(distMat > 0); epsVal = quantile(nonZero, 0.8); idx = dbscan(distMat, epsVal, 5, 'Distance', 'precomputed'); fprintf('簇数量: %d\n', max(idx)); fprintf('噪声点数量: %d\n', sum(idx == -1));distMat 构造时只算上三角再对称填充,能省一半计算量。dbscan 函数需要统计与机器学习工具箱,旧版 MATLAB 可能不支持 precomputed 选项,如果没有就改用资源包里自带的 DBSCAN 实现,输入仍然传 distMat。epsVal 用分位数选取后要观察簇数量和噪声比例,如果全部轨迹归成一个簇,说明 eps 太大;如果噪声超过 40%,优先考虑预处理和 q 值,而不是继续调 eps。
提示:dbscan 对输入矩阵要求是对称、非负、对角线为 0。代码里对角线本来就为 0,但如果你从外部文件读入距离矩阵,记得显式把对角线清零。
4. 航迹预处理与重采样:聚类效果差,八成问题在数据
4.1 清洗脏点:时间戳、零坐标、跳变点
我给不少类似资源做过调试,聚类结果乱成一团,最后查下来多半是数据源问题,不是算法问题。船舶 AIS 原始数据里常见三类脏点:时间戳缺失或乱序、经纬度为 0 的默认值、相邻两点间隔极短但坐标跳了几公里。这些点一旦进入 Harsdorf 距离计算,会直接拉高距离值,然后被分位数削掉——但削掉它们的同时也在削真正的航线特征。
最稳妥的清洗顺序是:按时间戳排序,删除坐标为零的记录,剔除重复点,再做速度过滤。速度过滤我一般用相邻点距离除以时间差,超过 50 节的点视为跳变点直接删除,因为绝大多数商船航行速度在 30 节以内。
t = tracks(i).time; % datetime 列 lon = tracks(i).lon; lat = tracks(i).lat; d = distanceMeters(lon, lat); % 相邻点距离,单位米 dt = seconds(diff(t)); % 时间差,单位秒 spd = d ./ dt; % 速度,单位 m/s bad = [false; spd > 25]; % 约 48 节以上视为跳变 lon(bad) = []; lat(bad) = []; t(bad) = [];这里 distanceMeters 是自定义函数,按经纬度计算两点距离。注意 bad 索引比原数据少一个,所以前面拼了 false。速度阈值我按航速 50 节折算成 25 m/s,你按自己的水域内限速调。清洗后的轨迹如果点数低于 10 个,建议直接丢弃,因为太少点的轨迹算改进 Harsdorf 时,分位数截断会失去统计意义。
4.2 把长航迹切成“航段”:时间间隔阈值的确定
一条船一天的 AIS 日志可能跨好几个航次,从泊位开到锚地、停几个小时、再开到另一泊位。如果不切分,整条轨迹会被算成一条,聚类时它和任何典型航线都不相似,只会被标成噪声。切分依据是时间间隔:相邻两点时间差超过某个阈值,就认为当前航段结束,开启下一条轨迹。
阈值怎么选?看数据源刷新率。如果 AIS 数据间隔是 3 分钟,连续缺失超过 20 分钟基本可以断定是停泊或离港;如果间隔是 1 分钟,阈值放到 10 分钟就够。我一般先用 600 秒试,看切出的航段个数和平均点数是否合理。
gapIdx = find(seconds(diff(t)) > 600); startIdx = [1; gapIdx + 1]; endIdx = [gapIdx; numel(t)]; segments = cell(numel(startIdx), 1); for k = 1:numel(startIdx) segments{k} = [lon(startIdx(k):endIdx(k)), ... lat(startIdx(k):endIdx(k))]; end切分后要检查每个航段点数,点太少就直接跳过。切忌把所有切出来的航段硬塞进聚类输入,因为港口内有很多几十米的短挪船,它们也是噪声样本的主要来源。
4.3 等距重采样到统一点数:让距离计算避开采样率差异
不同船舶的 AIS 报告频率不一致,有的船 3 秒一个点,有的 3 分钟一个点,这会造成密集轨迹里每个点都离对方轨迹很近,Harsdorf 距离表面上很小,但实际航迹形状差异被采样密度掩盖。解决办法是对每条航段做等距重采样,统一成相同的点数,比如每条轨迹 100 个点。
重采样方式有讲究,不能按时间插值。船舶停船时时间在走但位置不变,按时间插值会出现一大串重复点;按累计航程插值更合理,相当于在轨迹几何形状上均匀取点。
function trajOut = resampleByDistance(traj, nPts) segLen = sqrt(sum(diff(traj).^2, 2)); cumLen = [0; cumsum(segLen)]; q = linspace(0, cumLen(end), nPts); trajOut = [interp1(cumLen, traj(:,1), q)', ... interp1(cumLen, traj(:,2), q)']; end这个函数先算出每个折线段的累计长度,再在 0 到总长度之间均匀取 nPts 个位置,用线性插值得到新坐标。nPts 我一般取 100 到 150,太少会丢失航道弯曲细节,太多则计算矩阵时开销翻倍。重采样后改进 Harsdorf 距离的一个隐藏优势也出来了:两条轨迹都有 100 个点时,q 取 0.85 相当于忽略每条轨迹最差的 15 个点,统计意义更稳定。
5. 避坑记录:跑通这套聚类的四个翻车现场
5.1 报错“未定义函数 harsdorf_improved”:路径与工具箱产地不明
现象:解压资源后直接运行主脚本,MATLAB 报错“未定义函数或变量 'harsdorf_improved'”,但文件列表里明明有这个函数名。
原因:把资源包放在了某个目录下,但 MATLAB 当前路径没有包含该目录,函数文件没被加载。更隐蔽的是文件名和函数名不一致,比如文件叫 harsdorf_v2.m,内部函数定义名却是 harsdorf_improved,MATLAB 只认文件名。
解决:先 addpath 把整个资源目录加进去,再 which 检查函数是否被识别。如果确认存在但 still 报错,打开文件看第一行 function 声明,把文件名改成与函数名一致。这个排查顺序我每次都要走一遍。
addpath(genpath('D:/workspace/harsdorf_dbscan')); which harsdorf_improved5.2 聚类结果全是噪声点:eps 没看距离分布
现象:dbscan 返回的 idx 全是 -1,没有任何一个簇。
原因:eps 设得离距离尺度太远。如果只算出来的距离矩阵数值普遍在 0.05 以内,而 eps 仍然按经验设成 0.5,那每个点的邻域半径太大,按理说应该全连成一个簇;反过来如果距离普遍在 10 量级,eps 设成 0.1,所有点都是孤立的。另一个常见原因是 q 取太小,距离矩阵整体被压得过平,直接吞掉航线差异。
解决:不要猜 eps,打印距离分位数。我一般用 table 看 0.5、0.7、0.8、0.9 分位分别是什么值,然后从 0.8 分位附近起调。噪声比例过高时先调 q,不要反复动 eps。
5.3 一个簇里全是不同方向的轨迹:时间切分没做干净
现象:聚类出来的某个簇包含了明显相反方向的航迹,散点图上一看就是两条交叉航线被归在一起。
原因:没有按时间间隔切分轨迹,把一条船跨半天的大轨迹放进来了。这条大轨迹覆盖了多个方向,拉高了轨迹间距离的兼容性,DBSCAN 的密度连通把它附近的散轨迹全吸收进来。
解决:回到第 4 章切分逻辑,把时间间隔阈值收紧,先切分再算距离矩阵。切分后我会顺手画一下每条轨迹的起点和终点,用颜色区分航向,确认切分结果不会出现一条轨迹里又拐弯又掉头的情况。
5.4 距离矩阵对角线不为零导致 DBSCAN 失效
现象:聚类结果极其不稳定,换一个 eps 值簇数从 2 跳到 20,或者同一个脚本跑两次结果不一样。
原因:从外部导入距离矩阵,但矩阵格式不是标准的“对角线为零、对称”,有的代码在填充时把对角线写成了随机数或者小噪声,DBSCAN 的密度计算第一跳就全乱了。
解决:在读入矩阵后强制对称化和对角线清零,顺手做一个合法性检查。这个检查成本极低,但能省一晚上排查时间。
distMat = max(distMat, distMat'); % 强制对称 distMat(1:n+1:end) = 0; % 对角线清零 assert(issymmetric(distMat), '距离矩阵必须对称');5.5 经纬度直接当平面坐标:高纬度误差
现象:聚类结果在低纬度数据上看着正常,换到北方港口数据后,同一条航线的轨迹距离明显偏大,簇数变多。
原因:经度方向的实际距离随纬度升高而缩短,直接拿经纬度算欧氏距离,高纬度地区会产生 30% 以上的误差。Harsdorf 距离函数对坐标单位没有感知,输入什么就按什么算,坐标尺度被扭曲,距离矩阵自然失真。
解决:进距离矩阵前先把经纬度投影到局部平面坐标,最简单的是用等距圆柱投影近似,在数据所在中心纬度上做缩放。中心纬度取所有轨迹点的均值即可,对一个小港口的范围来说误差足够小。
lat0 = mean(latAll); lonScale = cosd(lat0); x = lon .* lonScale; y = lat;6. 聚类结果的验证与提升:簇紧实度、中心航迹与建议流程
聚完类先别急着画散点图上色,先回答一个问题:这个簇到底紧不紧?我会用距离矩阵本身算每个簇的内部紧凑度,即簇内样本两两 Harsdorf 距离的均值,这个值除以所有样本距离的全局中位数,得到归一化指标。小于 0.4 说明簇内轨迹高度一致,大于 0.6 说明这个簇里的轨迹其实松松散散,多半是 eps 拉得过大。
for k = setdiff(unique(idx)', -1) inK = find(idx == k); subMat = distMat(inK, inK); intra(k) = mean(subMat(subMat > 0)); end紧实度能看出聚类质量,但看不出语义对不对。我最后一步都是合成中心航迹来目检:把每个簇里所有轨迹重采样成 120 个点,逐点取中位数,得到一条代表航迹,再叠加到地图底图上,看它是否符合真实航路。这里用中位数而不是均值,是因为簇边缘偶尔混进半条噪声轨迹,均值会被拉歪,中位数稳得多。
做完一轮聚类后,我给自己定了一个固定检查顺序:清洗、切分、重采样、预览、距离矩阵、聚类、紧实度、中心航迹目检。每一层都用脚本自动出图,而不是等最后看结果再返工。这套流程在好几个船舶航迹数据上跑过,能筛掉 80% 以上的低级问题。从那以后我每次拿到新的 AIS 数据集,都强制先走一遍完整预处理,再进入 Harsdorf 距离矩阵计算,省下的返工时间远比写那几个函数的时间多。希望帮到你。
本文还有配套的精品资源,点击获取