☰
K-means轨迹聚类实战:Matlab代码与不等长序列处理
2026/9/28 5:43:40 网站建设 项目流程

做了一阵子时空数据挖掘,我越来越觉得“轨迹聚类”是个看着容易、做起来坑特别多的活儿。拿到一批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。比如每条轨迹都等间隔取N=20个点,每条轨迹就变成一个长度为2N的向量(前N维是x坐标,后N维是y坐标),所有轨迹拼成一个m×2N的矩阵,K-means就可以正常跑了。

这个方案的关键点在于“重采样”。不能简单按原始索引取前20个点,因为不同轨迹原始点数不一样,时间间隔也不一样。我习惯的做法是先把轨迹按弧长参数化:计算每个点到起点的累积路径长度,然后在这个累积长度上均匀取20个点。换句话说,不管原始轨迹有30个点还是500个点,也不管它中间是快还是慢,最后都变成“沿路径方向均匀钉20个钉子”的表示。这个做法对速度变化不敏感,只关心路径形状,正好匹配轨迹聚类的核心诉求。

这条路线的优点是实现简单、计算快,Matlab直接有内置kmeans,可视化也方便;缺点是把轨迹形态信息压缩成了固定维度的特征,如果轨迹形态复杂、长短差异极大,20个重采样点可能不够表达关键拐点。实际使用中,可以先跑这条路线建立基准效果,再决定是否上更复杂的方案。

2.2 路线B:DTW距离加改进的K-means变体

如果想要更贴近轨迹语义的距离度量,可以考虑动态时间规整(DTW)。DTW允许两个序列在时间轴上“拉伸”或“压缩”后匹配,能正确处理长度不等和速度不同的问题。你可以把它想象成把两条轨迹分别画在橡皮筋上,再用力拉扯橡皮筋,让相似路径段尽量对齐,最后算对齐后的累积距离。这个思路比逐点欧氏距离合理得多。

但DTW距离有个麻烦:它不是欧氏距离,K-means里最核心的“求平均得到簇中心”没法直接做。一个轨迹集合的“平均形态”,不能简单按坐标对应位置取均值,需要用专门的DBA(DTW Barycenter Averaging)算法迭代逼近。如果不想搞这么重,也可以用K-medoids方案:每次迭代时,在簇内挑一条“到其他所有轨迹的DTW距离总和最小”的原始轨迹作为中心。这实际上是“距离矩阵版的K-means”,虽然理论纯度不如DBA,但实现简单、稳定性好,在很多工程任务里够用了。

2.3 两条路线的取舍建议

对比项路线A:重采样+K-means路线B:DTW/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{end+1} = noisyTraj; trueLabels(end+1) = 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, N+1: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值只看肘部不够

肘部法则只是一个参考,实际场景里不能完全依赖它。模拟数据里三类路径差别很清晰,肘部会高度集中在K=3;但真实轨迹往往没有这么完美的“拐点”,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代码可以直接复制下来跑一遍模拟数据,再换成自己的轨迹数据验证,遇到问题按上面几个常见坑排查一遍,大部分场景都能顺利落地。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询