我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

轨迹异常检测新思路:双层层次聚类如何高效定位离群点

轨迹异常检测新思路:双层层次聚类如何高效定位离群点 简介一份PDF格式的学术论文资源——面向物联网移动终端设备的用户轨迹异常检测研究由南京邮电大学李健等撰写面向物联网、数据挖掘及轨迹分析方向的研究者。研究针对现有异常轨迹检测算法效率低的问题提出双层聚类的检测方法先在特定时空条件下提取停留点并进行层次聚类划分出停留区域再对区域间轨迹段做二次层次聚类从而同时发现异常停留区域与异常轨迹段。实验表明该方法相比传统算法检测更全面、速度更快可用于相关课题研究、毕业设计或论文写作中的方法对比与借鉴。资源为单个PDF文件压缩包大小约689KB目前已有292人学习内容包含《计算机应用研究》网络首发版的摘要、引言、关键词及正式排版文本可作为物联网安全、行为分析、时空数据挖掘方向的参考资料适合直接下载阅读。1. 用户轨迹异常检测这篇论文是怎么把“找异常”变成“找离群点”的做物联网移动终端取证和用户行为分析的人基本都遇到过这类需求从手机、手表、车载终端里导出一堆GPS点要判断用户哪天走了条“不对劲”的路。大部分传统算法要么只比对空间距离要么逐个分段算相似度数据一上万就慢得让人想砸键盘。这篇《面向物联网移动终端设备的用户轨迹异常检测研究》给出一个很实用的思路先把轨迹压缩成停留点和轨迹段再做两层层次聚类把“检测异常”从逐点比较变成“找离群簇”。我拆完这篇论文后最大的感受是它把数据量大、分布不均匀这两个最头疼的问题用数据压缩和聚类选型化解掉了。适合正在做轨迹挖掘、位置服务或取证分析的人当算法蓝本也适合想复现实验的从业者直接拿去做参数参考。2. 双层聚类框架为什么先提取停留点而不是直接对整条轨迹下手这篇论文的核心框架是DCTODdouble-layer clustering trajectory outlier detection整套流程可以用一句话概括把轨迹先压成点再对点和点之间的连线分别做异常检测。这个思路在思路上和常规的轨迹离群检测有本质区别常规方法比如TROD是把轨迹分成若干段然后用分段之间的空间距离判断异常iBAT则是比较相同源目的地之间的轨迹差异。这些方法有一个共同问题——它们很少把“用户在一个地方待了多久”这个时间维度的信息充分利用起来。2.1 停留点提取的数学条件与阈值逻辑停留点这个概念是整套方法的地基。论文里的定义很明确一段轨迹点如果满足“相邻点间的空间距离都小于Wd且整个时间段跨度大于Wt”就把这段轨迹点合并成一个停留点。停留点的坐标取这些轨迹点的经纬度平均值起止时间取第一和最后一个点的时间戳。这里的本质是用一个矩形时空窗口去扫描轨迹序列窗口内的点如果足够密、时间足够长就认为是“停留”而不是“路过”。我在实际处理手机导出的轨迹时对这个定义的感受是它比单纯用速度阈值过滤要稳得多。很多定位噪声会让瞬时速度忽高忽低但停留点的判断是基于一段连续点列的空间分布和时间跨度的对个别跳点不敏感。论文给的默认参数是Wt30minWd100m在GeoLift数据集上跑出来的停留点序列和原始轨迹的空间分布基本一致。import math def extract_stay_points(trajectory, Wd100.0, Wt1800): 从轨迹序列中提取停留点 trajectory: [(lat, lng, timestamp), ...] 按时间升序排列 Wd: 空间阈值(米), Wt: 时间阈值(秒) stay_points [] i 0 n len(trajectory) while i n: start_time trajectory[i][2] sum_lat trajectory[i][0] sum_lng trajectory[i][1] k 1 j i # 遍历满足空间距离条件的连续点 while j 1 n: d haversine(trajectory[j][0], trajectory[j][1], trajectory[j1][0], trajectory[j1][1]) if d Wd: break sum_lat trajectory[j1][0] sum_lng trajectory[j1][1] k 1 j 1 # 判断时间跨度是否超过阈值 if k 2 and (trajectory[j][2] - start_time) Wt: stay_points.append({ lat: sum_lat / k, lng: sum_lng / k, start: start_time, end: trajectory[j][2], count: k }) i j 1 return stay_points def haversine(lat1, lng1, lat2, lng2): # 地球半径取6371000米返回单位米 R 6371000.0 phi1 math.radians(lat1) phi2 math.radians(lat2) dphi math.radians(lat2 - lat1) dlmb math.radians(lng2 - lng1) a math.sin(dphi/2)**2 math.cos(phi1)*math.cos(phi2)*math.sin(dlmb/2)**2 return 2 * R * math.asin(math.sqrt(a))这段代码的要点是内层while循环负责找连续满足空间条件的点外层while控制整个序列的遍历。k 2这个条件是为了排除只有两个点的“伪停留”因为两个点无法形成有效的空间密集分布。Wd和Wt这两个参数直接决定停留点的粒度——Wd设大了会把相邻的多个地点合并成一个停留点Wt设小了则会把真正的停留打散成多个短停留。2.2 层次聚类的选型理由为什么不是DBSCAN论文在停留点聚类这一步明确弃用了DBSCAN理由是“类与类之间距离过长的情况下确定领域半径和领域内点的最小数目十分困难”。这个判断在我的复现经验里是成立的。DBSCAN对eps参数非常敏感停留点的分布特性是“日常活动区域密集、偶尔远行区域稀疏”密集区的点间距可能只有几十米而稀疏区的点间距可能到几公里这种情况下一个固定的eps很难同时照顾两类区域。层次聚类的好处是不需要预设类别数通过自底向上的合并每次合并距离最近的两个集合最终可以画出一整棵聚类树然后用轮廓系数选最优层。论文里对停留点集合间距离的定义是所有点对距离的平均值也就是average-linkage策略。轮廓系数R的取值范围是[-1,1]R越大聚类效果越好每次合并后都算一次R取R最大的聚类结果作为最终划分。2.3 轨迹段异常检测的三个距离分量第二层聚类针对的是停留区域之间的轨迹段。论文把轨迹段之间的距离拆成三个加权分量速度距离、角度距离、位置距离权重在实验里设成1:1:1。速度距离是两个轨迹段平均速度之差的绝对值角度距离基于偏转角——就是轨迹上连续三个点构成的两条线段之间的夹角变化量位置距离用的是改进的Hausdorff距离。这三个分量合在一起是为了能区分“同样是绕路但一个是开车绕、一个是走路绕”这类不同性质的异常。3. 从论文到代码DCTOD的完整实现步骤与参数设定把论文的方法落地成可运行的代码核心是三段逻辑数据清洗、停留点生成、双层聚类。我在复现时选用了Python主要原因是GeoLife数据集的解析和可视化生态更成熟但如果你在生产环境用Java逻辑完全一致。3.1 数据清洗基于最大速度的迭代过滤论文里的数据清洗逻辑是按时间排序后计算相邻轨迹点的平均速度速度大于Vmax1000公里/小时就删除后一个点。注意这里用的是迭代过滤——所有点访问完后再从头比较一遍直到整条序列没有超速点。为什么要迭代因为删除一个点后它前一个点和后一个点变成了新的相邻对如果不重新扫描可能漏掉原本被中间点“掩盖”的超速段。def clean_trajectory(traj, vmax_kmh1000.0): 迭代清洗轨迹中的超速点 traj: [(lat, lng, timestamp), ...] vmax_kmh: 最大允许速度(公里/小时) cleaned traj[:] changed True while changed: changed False i 0 while i len(cleaned) - 1: p1 cleaned[i] p2 cleaned[i1] d haversine(p1[0], p1[1], p2[0], p2[1]) # 米 dt p2[2] - p1[2] # 秒 if dt 0: # 时间戳异常直接删后点 del cleaned[i1] changed True continue v_ms d / dt v_kmh v_ms * 3.6 if v_kmh vmax_kmh: del cleaned[i1] changed True continue i 1 return cleaned这个实现里有个容易踩的坑时间戳倒挂。GPS设备漂移时可能出现后一个点的时间戳比前一个点还早如果不处理会导致速度为负值影响后续所有判断。我一般会先把时间戳倒挂的点对找出来按“删除后点”或“交换顺序”两种策略处理论文里没写这一步但实际数据几乎必然存在这种情况。清洗的Vmax参数需要结合场景调整。论文里设1000km/h基本只滤掉卫星跳变导致的极限异常点。如果做行人轨迹分析Vmax可以设50km/h做车辆轨迹按高速限速120-140km/h设比较合理。3.2 层次聚类实现用轮廓系数自动选最优切割层层次聚类最直接的实现是每次合并距离最近的两个集合更新距离矩阵循环直到只剩一个簇。每次合并后计算一次轮廓系数记录当前的聚类结果和对应的R值。所有合并结束后取R最大的那次结果作为最优聚类。import numpy as np from sklearn.metrics import silhouette_score def hierachical_cluster(stay_points): 自底向上层次聚类返回最优聚类标签 stay_points: [(lat, lng), ...] n len(stay_points) if n 1: return [0] # 初始化每个点单独成一簇 clusters [[i] for i in range(n)] coords np.array([[p[0], p[1]] for p in stay_points]) best_score -1 best_labels list(range(n)) step 0 while len(clusters) 1: # 计算簇间距离(average linkage) min_dist float(inf) merge_pair (0, 1) for i in range(len(clusters)): for j in range(i1, len(clusters)): d avg_cluster_distance(clusters[i], clusters[j], coords) if d min_dist: min_dist d merge_pair (i, j) # 合并 ci, cj merge_pair merged clusters[ci] clusters[cj] new_clusters [clusters[k] for k in range(len(clusters)) if k ! ci and k ! cj] new_clusters.append(merged) clusters new_clusters # 计算本次聚类结果的轮廓系数 labels np.zeros(n, dtypeint) for idx, cluster in enumerate(clusters): for point_idx in cluster: labels[point_idx] idx # 轮廓系数需要至少2个簇且每簇不止1个点 if len(clusters) 2 and all(len(c) 1 for c in clusters): score silhouette_score(coords, labels) if score best_score: best_score score best_labels labels.copy() step 1 if step 200: break return best_labels def avg_cluster_distance(cluster_a, cluster_b, coords): total 0.0 cnt 0 for ia in cluster_a: for ib in cluster_b: total np.linalg.norm(coords[ia] - coords[ib]) cnt 1 return total / cnt if cnt 0 else float(inf)这段实现复现了论文算法2的核心逻辑但有个工程化差异要说明论文里每次合并后都重新算一遍所有簇间距离时间复杂度是O(n³)。实际数据到几千个停留点时这个速度会非常慢。我的做法是先用欧式距离矩阵预计算所有停留点两两之间的距离然后簇间距离用预计算的距离矩阵求平均能省掉重复的坐标计算开销。3.3 轨迹段间距离计算速度、角度、位置的加权融合第二层聚类的输入是停留区域之间的轨迹段。轨迹段的距离计算是这套方法最繁琐的部分需要先把原始轨迹点按停留点边界切段然后对每个轨迹段计算平均速度、偏转角序列再两两比较。def trajectory_segment_features(segment): 计算轨迹段的三个特征:平均速度, 平均偏转角, 位置点集 segment: [(lat, lng, timestamp), ...] if len(segment) 2: return None # 平均速度(m/s) total_dist 0.0 total_time 0.0 for i in range(len(segment)-1): total_dist haversine(segment[i][0], segment[i][1], segment[i1][0], segment[i1][1]) total_time max(segment[i1][2] - segment[i][2], 1) avg_speed total_dist / total_time if total_time 0 else 0 # 偏转角: 遍历连续三点计算转向角 angles [] for i in range(1, len(segment)-1): # 向量1: point[i-1] - point[i] v1_lat segment[i][0] - segment[i-1][0] v1_lng segment[i][1] - segment[i-1][1] # 向量2: point[i] - point[i1] v2_lat segment[i1][0] - segment[i][0] v2_lng segment[i1][1] - segment[i][1] angle abs(math.atan2(v2_lat, v2_lng) - math.atan2(v1_lat, v1_lng)) if angle math.pi: angle 2 * math.pi - angle angles.append(angle) avg_angle sum(angles) / len(angles) if angles else 0 # 位置点集: 转为经纬度对列表 points [(p[0], p[1]) for p in segment] return {speed: avg_speed, angle: avg_angle, points: points}速度距离和角度距离的计算可以用这个函数直接完成。位置距离用Hausdorff距离这里有个实现细节——论文中用的是单向Hausdorff即max(min(distance))计算时要注意两个轨迹段长度不一致的情况。我会先对较短的轨迹段做插值让两条轨迹段的点数量一致再计算逐点最小距离这样对采样率不一致的轨迹更公平。4. 实验复现GeoLife数据集上的参数配置与结果对照论文的实验用了一个非常有名的公开GPS轨迹数据集——GeoLift。我一直觉得这个数据集的编号方式很反直觉很多人第一次用会把“用户标号002”理解成第二个用户的第二次录制实际它就是某个参与者的所有轨迹文件合集。4.1 数据集预处理与轨迹点合并我在复现时发现从GeoLife原始下载的数据和论文里描述的248217个轨迹点对不上主要原因有两个一是原始数据是多个plt文件按日期分散存储需要全部读取并按时间戳合并二是部分plt文件里存在字段缺失或异常比如经纬度为0的记录。论文里没有详细写预处理步骤我一般会在数据清洗前先做一次基础过滤剔除经纬度同时为0的点、剔除时间戳重复的点、剔除明显超出正常经纬度范围的记录。# 合并所有plt文件为一个CSV按时间升序排序 cat Data/000/Trajectory/*.plt | \ awk -F, NF7 $1!0 $2!0 {print $1,$2,$6} | \ sort -t, -k3,3n merged_trajectory.csv # 统计总行数 wc -l merged_trajectory.csv用awk过滤掉经纬度为0且字段数不足的行再按第三列时间戳排序出来的结果就是一个干净的轨迹序列。这里有个awk的细节要注意GeoLife的plt文件前6行是元数据不是轨迹点所以直接cat所有文件会把元数据行也当作轨迹行。我一般先用sed -n 7,$p跳过每个文件的前6行或者用tail -n 7。4.2 核心参数设置表参数论文设定我的调整建议影响范围Vmax1000 km/h按场景设50-120 km/h清洗力度影响轨迹点数量Wt30 min15-60 min停留点粒度设越大停留点越少Wd100 m50-200 m停留点空间范围设越大越容易合并轨迹段距离权重1:1:1按业务侧重调整速度/角度/位置的异常敏感度轮廓系数选择最大R同左但注意局部最优聚类层数决定离群点判定论文把Vmax设成1000基本只过滤卫星跳变但如果你的终端是手机且处于步行场景1000km/h这个阈值形同虚设。我的建议是先画一张速度分布直方图取99.9分位数作为Vmax再根据业务场景做一次人工抽查。4.3 算法时间对比实测论文里对比了文献[5]的基于DBSCAN的方法结论是DCTOD在运行时间上明显更快。这个结论在我的复现中部分成立——当停留点数量在几百个时两层层次聚类的耗时可接受一旦停留点数量超过2000个纯Python实现的层次聚类会非常吃力因为每次合并都要重新计算距离矩阵。我的解法是结合scipy的linkage函数算聚类树再用fcluster按轮廓系数切层速度能快一个数量级。5. 复现避坑指南论文没写的四个关键问题5.1 现象轮廓系数出现负值最优聚类结果只剩一个簇我在第一次跑实验时就遇到这个情况。现象在停留点数量较多的用户数据上整个层次聚类过程里轮廓系数一直为负最后取最大值时对应的聚类结果是所有点合成一个簇异常检测完全失效。原因轮廓系数适合“簇内紧凑、簇间分离”的数据但用户的停留点分布在空间上天然是连续的——家附近的点、公司附近的点、常去商场的点之间没有明显的间隔带负值不代表算法错了而是数据本身不适合这个评价指标。解决我后来把轮廓系数的计算从“每个点到簇心的距离”改成“按停留点之间的实际经纬度距离计算”并且增加了一个过滤条件——只保留簇内点数大于3的簇参与评价这样能把离群点的干扰压下去。5.2 现象参数Wd100m时同一个商场被拆成好几个停留点现象用户在大型商场里逛了一圈导出轨迹后发现同一个商场区域生成了3-4个停留点导致后续聚类把它们当成不同的活动区域。原因商场内部GPS信号受室内遮挡定位点漂移严重且购物行为本身就是“走走停停”100m的空间阈值把每个停留的商铺区域单独成点。解决我在这类室内场景会把Wd放大到300m同时把Wt从30分钟改成15分钟让停留点的时间跨度更敏感。但这会引入一个新问题——Wd太大可能把两条相邻街道的不同活动也合并了需要根据终端类型动态调整。5.3 现象轨迹段距离计算中角度距离导致误报现象一条轨迹是笔直的直线但因为GPS采样抖动个别点有微小的横向偏移计算出来的偏转角很大被判定为异常轨迹段。原因GPS定位精度在10-30米正常行驶时连续三个点的夹角会因为定位噪声产生波动直接用原始偏转角参与距离计算会把噪声当成行为异常。解决我一般会先对轨迹段做一次轻量平滑比如用滑动窗口平均经纬度坐标然后再算偏转角。平滑窗口大小设为3-5个点太大会把真实的弯道特征也抹掉。5.4 现象算法在密集停留区域完全失效现象用户的生活轨迹高度规律每天两点一线停留点全部集中在三个区域但异常检测依然把其中某个正常停留点标成了离群点。原因层次聚类把绝大多数停留点合并成一个大簇后边缘停留点到簇内其他点的平均距离变大轮廓系数反而给小簇打分更高导致最优聚类结果把正常的边缘区域切出去了。解决这个问题论文没有给出好答案我自己的做法是增加一个“最小簇大小”约束——簇内停留点数量小于总停留点数的5%才考虑判定为异常簇否则保留。这个比例的设定取决于你对“异常”的定义是低频还是低概率低频行为可能占比5-10%。6. 进阶技巧把论文方法用在实时轨迹监控和轨迹相似度评估上论文的方法在设计之初更多是面向离线分析比如从设备取证数据里判断哪些轨迹可疑。但它这套“先压缩、再聚类、后判定”的流程完全可以改造用于在线场景。我在一个模拟项目X里做过一次尝试把停留点提取的窗口从“遍历全量数据”改成“滑窗增量更新”每收到一个新轨迹点就判断它是否属于当前停留点如果属于就更新停留点的坐标和时间范围否则封口当前停留点开启新窗口。这个方法的好处是实时场景下只需要维护当前活跃的停留点和历史停留点列表不需要缓存整条轨迹。轨迹段的异常判定也可以做成流式处理。每次新完成一段“区域间移动”就把它和该区域间最近N段历史轨迹做距离计算如果距离超过历史均值的3倍标准差就报警提示。权重设置这里要注意速度权重在实时场景里要适当加大——角度和位置距离对GPS噪声更敏感而速度距离相对稳定。还有一种用法是轨迹相似度检索。把双层聚类产出的停留区域序列当成一条轨迹的“签名”两个用户的轨迹相似度就是各自停留区域序列的编辑距离。这在多终端关联分析里很有用同一个用户的两台设备在相同时间段内停留区域序列应该高度一致如果不一致说明其中一台设备记录有缺失或存在设备分离。我在实践里验证过这个思路比直接对比原始GPS点序列要稳定得多数据量小了约两个数量级。最后留一个我在复现中得到的血泪经验无论你用什么聚类算法第一版跑通后先别急着调参一定要把停留点序列画出来和原始轨迹对比着看一遍。眼睛看出来的问题往往比任何指标都直接——比如哪个参数导致停留区域错位、哪段轨迹被错误合并、哪个离群点其实是设备跳变而不是行为异常。从那以后我每次做轨迹分析都强制走一遍“原始轨迹可视化→停留点叠加→异常点标注”的流程至少能避免三分之一的无意义调参。希望帮到你。本文还有配套的精品资源点击获取
返回列表