ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

K-means轨迹聚类实战:Matlab代码与不等长序列处理

K-means轨迹聚类实战:Matlab代码与不等长序列处理 做了一阵子时空数据挖掘我越来越觉得“轨迹聚类”是个看着容易、做起来坑特别多的活儿。拿到一批GPS点串、用户出行路线、动物迁徙轨迹第一反应往往是“用聚类模型自动分一下类”。但真上手以后你会发现K-means这种教科书级别的聚类算法到了轨迹数据这里突然变得别扭起来轨迹长短不一样、时间轴对不齐、聚类中心画出来不知道是哪条路……这篇内容我会围绕“Kmeans轨迹聚类”把完整思路捋一遍相关Matlab代码直接给出来能跑、能改、能落地。适合正在做轨迹分析的学生、刚接触交通物流数据的工程师以及所有被不等长序列聚类问题折磨过的人。1. 先把问题拆清楚轨迹聚类到底想解决什么1.1 谁在什么场景下需要轨迹聚类轨迹聚类不是学术圈自己跟自己玩的概念。交通调度里出租车GPS轨迹聚类之后能区分正常接客路线和绕路行为物流配送中同一批快递员的历史路径聚成几类就能识别高频配送通道和异常滞留点运动分析领域篮球运动员的跑位轨迹聚类以后教练能直接看到常用战术套路动物生态研究里候鸟迁徙路径聚类可以归纳出主要迁徙走廊。本质上是同一个需求大量轨迹样本人工打标不现实想让算法自动归纳出几类典型模式接下来再做异常检测、路线预测或者行为理解。K-means因为简单、快速、可解释性强成了这个场景里最常被拿起来试的第一把刀。但“第一把刀”往往也是“第一把坑”。K-means原本是给固定维度的数值向量设计的而轨迹数据是变长的时间-空间序列。直接把每条轨迹的一堆坐标点铺开凑成矩阵会遇到长度不一导致没法对齐的问题强行补齐到一样长又会引入大量无意义的填充值。所以做轨迹聚类之前必须先解决一个核心问题怎么样把轨迹表示成K-means能吃的样子同时不丢失路径形态的关键信息。1.2 为什么不能直接把原始轨迹丢给K-means很多人第一次做轨迹聚类拿到数据就写kmeans(X, k)X直接是所有轨迹的坐标点堆叠。这里有个隐性问题K-means要求每个样本是一个固定长度的数值向量样本之间要能计算欧氏距离簇中心要能通过样本向量求平均得到。轨迹数据基本不满足这些条件。举个例子两条轨迹都从A点走到B点一个人匀速走一个人前慢后快轨迹点数量差别可能很大。如果强行用“对应索引的点算距离”第二个人的第3个点可能已经在B点附近而第一个人的第3个点还在半路上这时候算出来的欧氏距离被严重放大但实际上它们走的是同一条路。这个问题的本质是时间轴没有对齐。你可以理解成两个学生写同一份答案一个人每道题慢慢写一个人前面飞快后面慢如果逐题比对“第几题写了什么”会觉得他们答得完全不一样可实际上最终答案是一致的。退一步说就算所有轨迹长度都恰好一样逐点欧氏距离对位置偏移也极其敏感。一条路稍微整体往左偏一点另一条路完全走直线逐点距离会非常大但形态上它们几乎是同一类。真正做轨迹聚类要度量的是“路径形态的相似性”而不是“逐帧坐标的相似性”。这就意味着我们需要在进入K-means之前先做一次有效的轨迹表示转换。2. 方案选型怎么把轨迹变成K-means能吃的数据2.1 路线A重采样特征化K-means直接落地最直接的办法是把每条轨迹重采样到同样多的点然后把坐标拉成一个向量当作普通样本喂给K-means。比如每条轨迹都等间隔取N20个点每条轨迹就变成一个长度为2N的向量前N维是x坐标后N维是y坐标所有轨迹拼成一个m×2N的矩阵K-means就可以正常跑了。这个方案的关键点在于“重采样”。不能简单按原始索引取前20个点因为不同轨迹原始点数不一样时间间隔也不一样。我习惯的做法是先把轨迹按弧长参数化计算每个点到起点的累积路径长度然后在这个累积长度上均匀取20个点。换句话说不管原始轨迹有30个点还是500个点也不管它中间是快还是慢最后都变成“沿路径方向均匀钉20个钉子”的表示。这个做法对速度变化不敏感只关心路径形状正好匹配轨迹聚类的核心诉求。这条路线的优点是实现简单、计算快Matlab直接有内置kmeans可视化也方便缺点是把轨迹形态信息压缩成了固定维度的特征如果轨迹形态复杂、长短差异极大20个重采样点可能不够表达关键拐点。实际使用中可以先跑这条路线建立基准效果再决定是否上更复杂的方案。2.2 路线BDTW距离加改进的K-means变体如果想要更贴近轨迹语义的距离度量可以考虑动态时间规整DTW。DTW允许两个序列在时间轴上“拉伸”或“压缩”后匹配能正确处理长度不等和速度不同的问题。你可以把它想象成把两条轨迹分别画在橡皮筋上再用力拉扯橡皮筋让相似路径段尽量对齐最后算对齐后的累积距离。这个思路比逐点欧氏距离合理得多。但DTW距离有个麻烦它不是欧氏距离K-means里最核心的“求平均得到簇中心”没法直接做。一个轨迹集合的“平均形态”不能简单按坐标对应位置取均值需要用专门的DBADTW Barycenter Averaging算法迭代逼近。如果不想搞这么重也可以用K-medoids方案每次迭代时在簇内挑一条“到其他所有轨迹的DTW距离总和最小”的原始轨迹作为中心。这实际上是“距离矩阵版的K-means”虽然理论纯度不如DBA但实现简单、稳定性好在很多工程任务里够用了。2.3 两条路线的取舍建议对比项路线A重采样K-means路线BDTW/K-medoids/DBA实现成本低内置函数直接跑高要自己写DTW和变体算法距离含义逐点欧氏距离对时间轴偏移敏感路径形态距离允许时间轴扭曲对不等长轨迹通过重采样统一长度天然支持不等长计算复杂度O(m·k·N)N为特征维度O(m²·L²)轨迹两两比对量大会炸可视化中心轨迹简单取中心向量画坐标即可需额外处理质心/中心轨迹适用场景路径形态差异大、数据量大轨迹相似但速度快慢差很多、数据量可控我的建议是先用路线A把处理流程跑通搞清楚自己数据的形态分布再根据效果决定要不要切换到路线B。很多场景下路线A就已经能聚出清晰可解释的类别了没必要一上来就上DTW。3. Matlab完整代码实现3.1 模拟轨迹数据的生成为了方便演示我先生成三组形态不同的模拟轨迹。每条轨迹是5个二维坐标点分别代表三条不同走向的路径一条整体向上攀升一条向右上平移还有一条水平偏下。为了让数据更真实每条轨迹都加一点随机噪声。% 生成模拟轨迹数据 rng(42); classCenters { [0 0; 1 1; 2 2.2; 3 3.5], ... % 类1向上攀升 [0 1; 0.8 2; 1.6 2.8; 2.5 3.2], ... % 类2右上平移 [0 -0.5; 1 -0.9; 2 -1.2; 3 -1.6] % 类3水平偏下 }; numPerClass 10; trajectories {}; trueLabels []; for c 1:3 base classCenters{c}; for i 1:numPerClass noisyTraj base 0.15 * randn(size(base)); trajectories{end1} noisyTraj; trueLabels(end1) c; end end这里trueLabels是真实类别用来评估聚类效果trajectories是一个cell数组每个元素是一个n×2的矩阵第一列x坐标、第二列y坐标。实际项目中你只需要把数据整理成这个格式就行后面所有处理流程都一样。3.2 轨迹重采样与特征化函数重采样是整个流程里最核心的预处理步骤。我的做法是先计算累积弧长然后用累积弧长做插值得到沿轨迹均匀分布的N个点。这个函数对任意长度和速度变化的轨迹都适用而且代码很简单。function trajR resampleTrajectory(traj, N) % 按弧长均匀重采样轨迹统一到N个点 % 输入traj: n×2矩阵第一列为x第二列为y segLen sqrt(sum(diff(traj, 1, 1) .^ 2, 2)); cumLen [0; cumsum(segLen)]; % 累积弧长 % 去除累积弧长重复的点避免interp1报错 [cumLen, idx] unique(cumLen); traj traj(idx, :); % 在累积弧长上均匀采样 queryLen linspace(0, cumLen(end), N); trajR interp1(cumLen, traj, queryLen, linear); end function feat trajToFeature(traj, N) % 重采样成N个点后拉成1×(2N)特征向量 trajR resampleTrajectory(traj, N); feat [trajR(:, 1); trajR(:, 2)]; % 前N维是x后N维是y end要注意如果原始轨迹里存在原地停留的连续点累积弧长会出现重复值直接传给interp1会报错。所以我加了unique去重保留第一个出现的位置。这个细节看起来不起眼实际数据里出现频率非常高尤其是GPS信号抖动的时候。3.3 K-means主流程与肘部法则数据准备完毕后特征化、标准化、聚类的流程就很常规了。K-means是随机的对初始簇中心很敏感所以我用Replicates参数让它多次随机重启动选最优结果。下面这段代码直接用Matlab内置kmeans实现。%% 参数设置 N 20; % 每条轨迹重采样点数 K 3; % 聚类数 R 10; % K-means 重复启动次数 %% 特征化所有轨迹转成特征矩阵 m length(trajectories); featMat zeros(m, 2 * N); for i 1:m featMat(i, :) trajToFeature(trajectories{i}, N); end %% 标准化对每列特征做zscore featMatStd zscore(featMat); %% 肘部法则看不同K的SSE变化 maxK 10; sse zeros(1, maxK); for k 1:maxK [~, ~, sumd] kmeans(featMatStd, k, Replicates, 5, MaxIter, 300); sse(k) sum(sumd); end figure; plot(1:maxK, sse, -o, LineWidth, 1.5); xlabel(聚类数K); ylabel(簇内误差平方和SSE); title(K-means 肘部法则); %% 正式聚类 rng(2024); [idx, C] kmeans(featMatStd, K, Replicates, R, MaxIter, 500);这里idx是每个样本的类别编号C是K个簇中心的坐标每个C(k,:)是1×2N的向量。sumd是每个簇的簇内样本到中心距离之和累加起来就是总SSE用来画肘部图。肘部法则就是找“K继续增大时SSE下降曲线出现明显拐弯”的位置拐点对应的K通常是最经济的选择。3.4 聚类结果的可视化聚类不画图等于白做。轨迹聚类的可视化有两个层面一是把所有轨迹按聚类结果涂色看类别是否在空间上清晰分开二是把每个簇的中心画出来才能理解每个簇的代表轨迹长什么样。%% 可视化按聚类结果着色 colors lines(K); figure; hold on; for i 1:m traj trajectories{i}; plot(traj(:, 1), traj(:, 2), Color, colors(idx(i), :), ... LineWidth, 1.0); end %% 叠加每个簇的中心轨迹 for k 1:K xc C(k, 1:N); yc C(k, N1:2*N); plot(xc, yc, k--, LineWidth, 2.5); end xlabel(x); ylabel(y); title(Kmeans轨迹聚类结果虚线为簇中心轨迹);因为特征向量里前N维是x、后N维是y所以画中心轨迹时先取前N个作为x坐标再取后N个作为y坐标然后按顺序连线。这样画出来的就是簇中心对应的“平均轨迹”。我建议看聚类效果时重点关注中心轨迹是否平滑、是否和大多数样本走向一致这比盯着数值指标更有直觉。4. 参数调优和实验细节4.1 K值只看肘部不够肘部法则只是一个参考实际场景里不能完全依赖它。模拟数据里三类路径差别很清晰肘部会高度集中在K3但真实轨迹往往没有这么完美的“拐点”SSE曲线会平缓下滑看不出明显拐弯。遇到这种情况我会跑一遍evalclusters看轮廓系数eva evalclusters(featMatStd, kmeans, silhouette, KList, 1:10); figure; plot(eva);轮廓系数衡量的是簇内紧密度和簇间分离度的综合表现数值越接近1表示聚类越合理。另外K值选择也一定要结合业务场景比如交通场景里就是想区分“直行、左转、右转”三种行为那K就设3不用管统计指标怎么说。指标是辅助决策的不是替你拍板的。4.2 初始中心与Replicates的正确用法K-means对初始中心非常敏感随机起始可能在局部最优出不来。Matlab的kmeans默认启动方式其实就是K-means但依然存在随机性。我习惯设置Replicates, 10或者更大让算法从10组不同初始中心出发最后保留总SSE最小的那组结果。代价是计算时间增加但轨迹聚类的数据量通常不大多跑几次无所谓。还有一个小细节要特别注意正式训练之前先固定随机种子。rng(2024);之后再调用kmeans每次运行结果就完全可复现。这一点在写论文、出实验报告、跟同事对齐效果的时候非常重要。不要在别人跑出结果你跑不出来的时候才发现是随机种子的问题。4.3 特征标准化预处理的作用在特征矩阵送入K-means之前我用zscore对每一列做了标准化。这对混合坐标类型的数据意义很大比如x是经纬度、y是高度量纲完全不同直接算欧氏距离的话高度会完全碾压经纬度。标准化后每个维度的均值是0、标准差是1距离计算更公平。但也要说明纯平面坐标数据如果x和y量纲一致标准化不一定是必须的。有时候标准化还会抹掉轨迹整体位置的信息把原本“左上区域”和“右下区域”的差异压缩掉一部分。我的习惯是先不做标准化跑一版再标准化跑一版对比聚类结果和业务可解释性哪个更顺。特征工程没有标准答案多试几组设置比迷信某个固定流程有用得多。5. 实操中踩过的坑问题与排查5.1 重采样时报错或出现NaN最常见的问题就是interp1报错或者重采样结果出现NaN。原因基本是原始轨迹里有重复点累积弧长存在重复值插值时无法按唯一递增点处理。前面代码里已经用unique处理掉了。还有一个隐蔽问题是轨迹里存在单个孤立点跳变导致某一段弧长异常大重采样后轨迹形状被这个噪点严重扭曲。建议在预处理阶段先做一个简单的轨迹抽稀或者高斯平滑去掉明显离群点再进重采样流程。5.2 数据量大时内存爆掉如果轨迹数量上万、每条轨迹重采样到几百个点特征矩阵的规模会迅速膨胀。一万条轨迹、每条200个点就是400维矩阵不大但如果每条轨迹原本有成千上万个坐标点直接构造完整轨迹特征再加DTW距离矩阵那内存很容易爆。路线A的特征矩阵还相对可控路线B的DTW距离矩阵是m×m一万条轨迹就是上亿个距离值光存储就接近百兆计算成本更是天文数字。解决办法是先对轨迹做抽稀把每条轨迹压缩到关键拐点比如用Douglas-Peucker算法保留折线形状的主要转折位置。一般一条轨迹保留几十个点就能保持形态特征。重采样点数N也不用设太大20到50个点是常见选择。特征维度太高不但计算慢还容易把噪声拟合进去聚类效果反而不稳定。5.3 轨迹起点、方向不一致导致聚类混乱这是轨迹聚类里非常经典、也特别容易被忽视的问题。同一条实际路线一个人从A走到B一个人从B走到A如果不做方向统一K-means会把它们分到两个完全不同簇里因为特征向量的顺序正好是镜像的。我在自己的项目里吃过一次亏后来预处理阶段就直接判断轨迹首尾点距离如果终点更靠近整体路径的“起点侧”就把轨迹翻转一下方向。更严谨的做法是设定参考方向比如“以轨迹第一个点为起点保证终点在起点的某一段方向角范围内”。运动方向数据如果有的话直接用方向信息统一。类似的问题还有轨迹坐标偏移比如两个轨迹的形态完全一样但整体位置平移了几百米重采样特征化以后还是会被分成两类。这种情况要看业务需要如果关心相对路径形态就做中心化处理如果关心绝对位置就保留原始坐标。5.4 聚类结果每次都不一样K-means是典型的随机初始化算法迭代过程对起始点选择很敏感加上数据本身有一定重叠不同运行批次结果可能不太一致。这个问题的排查顺序是第一固定随机种子排除随机因素第二增加Replicates多次重复选最优第三如果还不行检查数据标准化是否稳定、是否有个别离群轨迹在干扰。离群轨迹会把一个簇中心拽偏我一般会在聚类前先跑一遍简单的kmeans把离群样本标记出来人工确认是保留还是剔除。这里也要注意不要盲目剔除离群点有时候离群点恰恰是异常行为样本是业务上最关心的部分。6. 一个值得后续扩展的方向如果对轨迹形态的精细度要求更高现有方案还可以扩展成“深度特征聚类”的路线先用一个序列编码模型把轨迹编码成固定维度的嵌入向量再在嵌入向量上做K-means。这样既保留了序列语义又能直接套用标准聚类工具。调过一轮之后你会明显感受到轨迹聚类的瓶颈很少在聚类算法本身而在轨迹表示方式的选择。把重采样、方向统一、特征标准化这些预处理做扎实K-means也好、DTW变体也好都能给你清晰可用的结果。我在实际做的时候最深的体会是花在“把轨迹整理成算法能理解的形式”上的时间远比花在调参上的时间多。先把数据形态看清楚、把预处理逻辑理顺聚类效果自然就出来了。这篇Matlab代码可以直接复制下来跑一遍模拟数据再换成自己的轨迹数据验证遇到问题按上面几个常见坑排查一遍大部分场景都能顺利落地。
返回列表