1. 为什么历史轨迹值得用Transformer重做一遍1.1 轨迹数据的本质一种时间长、噪声多、维度混杂的序列我最初接触Transformer处理历史轨迹这个方向缘起于一个很实际的需求——某物流平台上几千台车的GPS回传数据堆在数仓里每天产生上亿条轨迹点但我需要的是一个能从中快速判断“这台车今晚会不会偏离规划路线”的模型。最早用的LSTM在短片段上效果还行一旦轨迹拉长到三五百个点最早的几段路况信息基本就丢了。后来我把整段历史轨迹直接放进Transformer从注意力权重里第一次直观看到模型“回头看”的动作——它真的会在判断当前危险程度时去翻两个路口之前的转向灯状态。这里想先讲清楚一个前提轨迹数据到底是什么。一条历史轨迹本质上是移动对象在时间轴上的采样点序列每个点至少包含时间戳和空间坐标实际业务中还经常叠加瞬时速度、航向角、定位状态、信号强度等辅助字段。它同时具备三个让传统模型头疼的特征。第一长度不固定。一趟短途配送可能只有几十个点跨城干线却有上千个点模型不能假设所有输入一样长。第二采样间隔不规律。GPS在隧道里丢点、在市区密集返点间隔可能是1秒也可能是30秒纯粹按“第几个点”来建模会丢掉真实的时间语义。第三噪声不可忽略。坐标漂移、信号跳变、静止时的小幅抖动这些脏点混在轨迹里对依赖局部上下文的模型伤害非常大。这三件事叠加起来正好撞上Transformer的强项和短板。强项在于自注意力机制天然接受变长输入、能直接建模任意两个时间点之间的依赖关系短板则在于它对输入特征的“规整度”要求极高——如果你不把不规律的轨迹处理成模型能理解的规范表示再强的注意力机制也会被噪声带偏。所以这篇文章既讲为什么用Transformer更讲怎么把历史轨迹喂给Transformer之前做对准备工作。1.2 循环模型在长轨迹上的两个真实软肋不是说要彻底否定LSTM、GRU这类循环模型它们在短轨迹、小数据量的场景下仍然好用。但在处理动辄上千步的长历史轨迹时有两个问题很难绕开。第一个是串行计算带来的效率瓶颈。RNN的每一步必须等前一步的隐状态算完才能继续这决定了训练和推理都没法利用GPU的大规模并行能力。当轨迹点到几百步时一个batch的训练耗时指数上升。我之前跑实验LSTM处理512步序列的显存占用和训练时长跟Transformer在同样长度下对比反而是Transformer在batch内并行计算后整体效率更高尽管它的参数量更大。第二个是长距离依赖的衰减。循环结构的信息传递路径是逐级递进的从第1个点传到第300个点理论上梯度要穿过299次非线性变换。虽然在LSTM里门控机制缓解了一部分问题但实践中当关键信息出现在序列前段、需要影响序列后段判断时效果仍然不稳定。举个具体例子车辆在高速公路上连续直行2小时最后突然在出口匝道减速如果模型想准确预测这次减速它必须“记住”两小时前车辆上高速时是否在超车道——这种跨度很长的关联RNN类模型很难稳定捕获而Transformer任何一个位置都能直接跟最前面的位置建立连接注意力分数一步到位。1.3 自注意力天然适配轨迹的四个方面自注意力机制本身就是为一个集合中的任意元素计算两两关系而设计的。如果把一条轨迹看成“时间位置点的集合”那么Transformer的处理方式几乎是为轨迹量身定做的具体体现在四点。第一任意两点可直接交互。RNN的信息流动是沿着时间轴逐步传递的而自注意力一步到位第1个点可以直接“关注”第300个点没有中间路径的信息损耗。对应到轨迹场景就是模型可以同时看到“刚从停车场出来”和“现在已经开到高架桥下”这两个时刻之间的关系不需要长距离传播。第二输入长度天然灵活。没有循环结构的时间步约束attention可以作用于任意长度的序列只需要处理到位移、距离的padding掩码即可。工程上按batch内最长轨迹做padding不影响模型对短轨迹的适应能力。第三训练可并行。所有位置的表示可以同时计算不需要等待前一步完成。这使得同样规模的轨迹数据训练效率高很多尤其当样本量大了以后这个差距会被放大。第四注意力权重具备可解释性。我在轨迹预测、轨迹补全项目中经常直接可视化attention权重来判断模型到底在看哪些历史点。比如某次测试中模型在预测网约车是否要变道时注意力明显集中在几百米外的红绿灯点以及前一个路口的分流导流线附近。这种“可回溯性”在做线下模型分析时特别有用起码你知道它不是乱猜。2. 轨迹进入模型前的三步清洗、对齐、特征化2.1 先清理两类最容易被忽略的脏数据点很多文章一上来就写Transformer结构但实际项目中影响模型效果最大的往往是数据处理。这里我不展开细说所有清洗规则只强调两类最容易被忽略、又对attention影响巨大的脏点。第一类是坐标漂移点。GPS在城市峡谷、隧道出口经常出现瞬间几百米的跳动表现为相邻两点间计算出的瞬时速度远超物理可能。处理办法很简单按采样间隔和时间差算相邻点速度超过阈值比如100km/h或更高一些根据场景调整就剔除。但注意一点剔除之后不是简单丢掉而是用前后正常点插值补回一个位置保证轨迹连续性。第二类是静止抖动点。车辆停在红绿灯前面时GPS并不会完全静止而是在半径几米范围内来回跳。如果保留这些抖动点模型会把这种“原地抖动”误读成低速行驶特征污染后续轨迹速度特征。我的处理方式是把相邻两点距离小于阈值且持续时间超过一定时长的点合并为一个停驻点保留起点或平均位置再把停驻时长作为单独特征放进去。两类清洗做完之后轨迹从“乱糟糟的一堆采样点”变成“一条干净的时空折线”这个阶段的效果立竿见影很多错误预测在源头就被排除了。2.2 时间对齐与重采样固定间隔才有公平比较同一批轨迹数据里A车的采样间隔可能是1秒B车的可能是5秒如果直接送进模型模型会错误地认为时间维度差异是空间特征的一部分——同样的位移可能是1秒跑出来的也可能是5秒跑出来的语义完全不同。所以对齐采样间隔是必须做的。一般来说有两种策略。按时间对齐适合驾驶行为分析这类时间敏感的预测任务做法是设定目标间隔比如2秒在原始轨迹上做线性插值或样条插值得到等时间间隔的采样点。按距离对齐适合路径走线分析这类空间敏感的任务每隔固定距离取一个点保证每个令牌代表的空间跨度一致。我在驾驶行为预测场景里选的是2秒间隔、线性插值。原因有二一是线性插值实现简单、稳定不会像样条插值那样可能在某些曲率较大的弯道上产生过冲二是2秒间隔既能保留必要的动态细节又把轨迹长度控制在合理范围例如一段1小时的行程大约1800个点经过滑窗截断后仍能保留足够上下文。如果任务只关心整段路径的几何形状而对时间语义不敏感那么按距离对齐反而更合适它能自动适应车速的变化——堵车时点密通畅时点疏但对应的空间跨度一致。2.3 特征化设计把坐标点变成向量既要表达空间也要表达时间清洗对齐之后要把每个轨迹点转成模型输入的向量。我在实践中采用的特征组合如下表这个组合在多个场景下验证过效果特征计算方式说明相对位移dx当前点x坐标减上一个点x坐标消除绝对坐标偏移让模型关注运动趋势相对位移dy当前点y坐标减上一个点y坐标同上与dx合起来表达方向与位移量时间差dt当前点时间戳减上一个点时间戳在对齐后通常是常数但保留它让模型知道间隔瞬时速度v位移长度除以dt区分移动与停驻的关键特征航向角变化delta_heading当前航向角减上一个航向角捕捉转弯、并线动作停驻标记is_stop速度低于阈值时为1辅助模型理解交通信号灯等待等场景一个关键的细节是我推荐使用相对坐标时间差而非绝对经纬度。绝对经纬度本身没有稳定语义不同城市、不同路段的坐标数值差异很大模型很难泛化而相对上一帧的位移dx、dy既能表达空间变化又天然具备平移不变性模型学到的是“车在这段时间内往哪个方向挪了多远”的规律。实践下来这个改动比换模型结构的收益还大。3. 轻量轨迹Transformer结构拆解与可运行实现3.1 输入输出设计预测“相对位移”而不是“绝对经纬度”很多第一次做轨迹预测的人会直接把经纬度作为回归目标让模型输出下一个点的绝对坐标。这个做法在短序列、单条路线重复性高的场景下勉强能用但一旦切换到城市级大范围轨迹绝对坐标回归会非常不稳定——不同区域坐标分布差异太大模型一训练就朝着均值回归去了。我的做法是把目标定义为下一帧相对上一帧的位移增量(dx, dy)。训练时模型读入历史N步的特征序列输出一个二维向量表示下一帧的相对位移推理时累加到当前坐标上就得到绝对位置。把回归目标换成增量之后模型的收敛速度明显提升ADE和FDE指标都改善了不少。原因是增量分布相对平稳不同轨迹间差异小模型不需要记忆“某个经纬度应该在哪”只需要学会“当前状态下该往哪个方向挪多少”。3.2 时空联合位置编码不要只用序列下标做位置Transformer本身没有顺序概念必须额外注入位置信息。标准做法是用sinusoidal位置编码固定给第i个token加一个位置向量。但直接套到轨迹数据上有一个问题它只用序列下标表示位置忽略了真实时间间隔。轨迹中点之间的时间差是变化的尤其是清洗后保留的停驻点一个点可能代表了10秒的等待另一个点只代表1秒的运动如果模型只看到“这是第5个点”它分不清这两个点之间到底隔了多久。我用的方案是把累计时间差作为主维度来计算位置编码。具体做法是计算每个时间戳与序列第一个时间戳的时间差delta_t用delta_t归一化到合适尺度替代原始token index套用标准的正弦余弦编码公式同时把delta_t本身作为输入特征之一送入模型让模型既能在注意力层面感知相对时间距离又能在特征层面使用精确的时间差数值。这样改进之后模型对时间间隔不规律的轨迹适应性大幅提升。我在某段隧道数据GPS大量丢点重采样后时间差忽高忽低上的实验显示加入时间位置编码比纯下标位置编码的预测误差降低了接近10%。3.3 Mask策略挡住未来和padding写轨迹Transformer时最容易犯的一个错误是不加mask导致模型在预测位置时偷看答案。标准做法是采用causal mask在多头注意力计算时第i个token只能看到位置小于等于i的token看不到未来帧。这在使用Transformer做自回归预测时必不可少。同时还需要处理padding mask。一个batch内的轨迹长度不同短的轨迹要用填充值补齐。padding mask的作用是在注意力计算中把这些无意义的填充位置屏蔽掉不让它们参与权重计算和输出。我使用的实现框架是PyTorch的TransformerEncoderLayer它支持src_key_padding_mask参数用起来很方便。贴一段核心模型实现这个结构我在多个业务中都用过效果稳定import torch import torch.nn as nn import math class TimePositionalEncoding(nn.Module): 基于累计时间差的位置编码 def __init__(self, d_model: int, max_len: int 5000): super().__init__() self.d_model d_model self.max_len max_len # 预生成位置索引向量实际使用时按时间差索引 pe torch.zeros(max_len, d_model) position torch.arange(0, max_len).unsqueeze(1).float() div_term torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] torch.sin(position * div_term) pe[:, 1::2] torch.cos(position * div_term) self.register_buffer(pe, pe.unsqueeze(0)) def forward(self, x, time_delta): # time_delta: (batch, seq) 每个token相对序列起始的累计时间 seq_len x.size(1) index torch.clamp(time_delta.long(), 0, self.max_len - 1) pos_emb self.pe[:, :seq_len].squeeze(0)[index] # 按时间差值取编码 return x pos_emb class TrajectoryTransformer(nn.Module): def __init__(self, in_dim6, d_model128, nhead4, num_layers3, dropout0.1): super().__init__() self.input_proj nn.Linear(in_dim, d_model) self.pos_enc TimePositionalEncoding(d_model) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforward512, dropoutdropout, activationgelu, batch_firstTrue ) self.encoder nn.TransformerEncoder(encoder_layer, num_layers) self.output_head nn.Sequential( nn.Linear(d_model, 128), nn.GELU(), nn.Linear(128, 2) ) def forward(self, x, time_delta, key_padding_maskNone): # x: (batch, seq, in_dim) h self.input_proj(x) h self.pos_enc(h, time_delta) h self.encoder(h, src_key_padding_maskkey_padding_mask) # 取最后一个有效位置的输出做预测 out self.output_head(h[:, -1, :]) return out3.4 训练与推理细节损失函数、学习率与自回归训练阶段使用的损失函数我强烈推荐Huber LossSmooth L1它对离群点不像MSE那么敏感。轨迹数据里面总会有些位置突变点、换道瞬间的异常动态MSE会被这些点拖着走Huber在误差较小时表现为平方损失、误差较大时表现为线性损失兼顾收敛速度和鲁棒性。实测下来换了Huber loss之后预测轨迹的“抖动”明显减少。学习率策略方面我建议先用几个epoch的线性warmup比如前5%的step从1e-5逐步升到1e-3再配合余弦退火。Transformer对learning rate比较敏感一上来就用大学习率非常容易发散尤其是位置编码模块更新不稳定时模型的loss会直接跳到nan。推理时要做自回归生成模型用历史N步预测第N1步然后把预测结果作为输入的一部分继续预测第N2步。这里有一个重要的工程细节——自回归误差会随着步数增加累积放大所以推理时的输入拼接策略是始终保持输入长度N不变每预测一步就把当前帧的真实特征替换成预测特征如果后续有真实观测可用或纯预测特征纯预测场景。如果做纯预测建议把输入窗口做成滑窗形式防止位置编码随着窗口滑动而偏移。4. 三个高频任务上的实测预测、补全、匹配4.1 长程驾驶轨迹预测多步预测与评价指标我先说驾驶轨迹预测。这一类任务的目标是从过去一段时间比如20秒的轨迹中预测未来若干秒的位置序列典型评价指标是ADEAverage Displacement Error所有预测点平均位移误差和FDEFinal Displacement Error最终点位移误差。在某个车队管理系统里我用上述模型处理2000多台货运车辆的轨迹数据。训练集约有12万条完整行程每条行程重采样到600个点用滑窗截成输入40步、预测20步的样本。与之前LSTM方案对比Transformer模型的ADE降低了大约18%FDE降低了22%。有意思的是LSTM的误差随着预测步数增加几乎线性增长而Transformer在预测前10步内误差增长很平缓说明自注意力机制对局部运动模式的建模确实更高效。4.2 轨迹补全利用双向上下文找回丢失段轨迹补全处理的是信号丢失问题比如GPS在隧道里失联几分钟需要根据前后的轨迹把中间路径估计出来。这本质上是双向信息利用模型必须同时看丢失段之前的轨迹和之后的轨迹才能合理补出中间路径。Transformer做这个天然有优势它不像单向RNN只能从前往后看自注意力把整个序列包括丢失段后面的点一次性输入每个位置的表示都蕴含全局信息。实现上需要做两处调整第一不设causal mask让每个位置都可以看到整个序列第二加上一个segment embedding特征标记哪些点是已知的、哪些点是待补全的让模型能区分输入来源。我在跨城高速轨迹补全任务里测试某段长达3分钟的丢点轨迹线性插值补出来的路径会偏离真实路径大约1.2公里而Transformer补全的路径误差控制在350米以内不仅路径形状与实际路线吻合连匝道位置的弧度都还原得比较精确。4.3 轨迹相似度匹配把attention的输出当作轨迹Embedding最后一个高频场景是轨迹相似度计算典型业务是“查找与给定轨迹最相似的历史轨迹”。传统做法用DTW动态时间规整来算两条轨迹的距离这种方法效果好但计算复杂度高每对轨迹都要算一遍全局匹配上万条轨迹两两比较不太现实。用Transformer来做的思路是训练阶段让模型预测下一个点同时添加一个分类任务或对比学习目标让模型输出整条轨迹的embedding向量。推理时只需要把所有轨迹embedding计算一次并保存到向量库中新轨迹来的时候算向量相似度即可。我在实验中发现虽然轨迹embedding在细粒度几何匹配上没有DTW精确但在“大致相似”的召回场景下完全够用而且向量检索的速度比DTW快几个数量级。这个思路在线上做异常轨迹检测效果也还不错——正常轨迹的embedding聚集在一起异常的偏航轨迹会明显偏离聚类中心。5. 调优路上反复踩到的坑以及最后的解法5.1 位置编码偏移为什么低速段预测突然变差最让我头疼的一个坑出现在某次低俗拥堵路段的测试里模型在高速段表现很好一到低速走走停停的路段预测点就出现漂移。排查后发现根因在于位置编码只考虑了序列下标没有考虑实际时间差——低速路段车辆原地蠕动相邻点的位移delt非常小、时间差却不变模型误以为“花了同样时间却只挪了一点点”导致对速度的估计偏低。解法就是前面提到的把累计时间差引入位置编码同时把时间差本身作为输入特征。改完之后低速段的预测稳定性明显提升。这个坑给我的教训是轨迹Transformer里位置编码的语义要和业务物理量对齐不能想当然套用NLP里的下标位置方案。5.2 数据不平衡高速样本太多、低速样本太少绝大多数轨迹数据里正常行驶的高速片段占80%以上低速停留、极端变道这类关键场景反而稀少。模型训练时会被高频的“直线行驶”样本主导对少数的“转弯/停驻/并线”样本几乎学不到什么。解决思路有三个一是用分段加权采样确保每个速度区间的样本数量相近二是直接用轨迹片段中的关键事件转向、停驻、变道做类别均衡采样保证每个batch里含有关键样例三是加一个辅助速度预测head让模型从早期就学习区分不同运动状态。5.3 内存与长度的矛盾滑窗与降采样的权衡Transformer对长序列的内存消耗是O(n²)级别的轨迹稍微长一点就把显存吃满。我的经验是把长轨迹切成固定长度滑窗窗口大小根据任务动态调整——预测任务用40~60步补全任务用100~200步。如果必须处理超长序列可以先用1D卷积或简单的线性层做“降采样编码”把多个时间步压缩成一个token再进Transformer这一步对长轨迹来说是性价比极高的优化。5.4 数据量门槛Transformer不是什么时候都该用最后说一个反直觉的经验如果可用轨迹数据少于几千条别急着上Transformer。自注意力模型的参数量和训练难度摆在那里数据不够的时候很容易过拟合效果反而不如精心调过的LSTM或传统的卡尔曼滤波。我的建议是先用简单模型跑通流程、建立基线等数据量级上去之后再切换到Transformer。数据增广手段比如轨迹旋转、尺度扰动、速度噪声注入可以补一部分数据但终究不能替代真实多样性的数据。这个内容后续扩展方向也很大比如把轨迹Transformer的输出embedding接到下游的路径规划模型里做预训练或者跟地图道路网络特征结合做车道级预测。所有模型的尝试都离不开一个前提——先把一条历史轨迹当成一段有故事的时间序列去理解而不是当成一堆坐标点去硬拟合。这是我最想让你记住的一点。