短时轨迹预测完整代码:数据管线、多模态评估与推理交付

发布时间:2026/11/2 6:56:43
短时轨迹预测完整代码:数据管线、多模态评估与推理交付

简介:面向深度学习轨迹预测初学者与课程实践者的一套可运行代码包,围绕序列建模与模型对比展开,适合复现时间序列与轨迹预测流程,理解数据读取、训练保存、结果评估的完整链路。压缩包共11个文件,约723KB,以7个Python脚本为主体,另含checkpoint、CSV数据集以及data/index模型权重文件,覆盖数据读取、GRU与LSTM建模、逻辑回归对照实验和轨迹处理等模块,结构紧凑,便于按训练流程逐步阅读。已有230人学习,包内保留训练中间状态与模型文件,可用于继续训练、结果复现和性能比较,也能帮助梳理从数据预处理到模型评估的项目组织方式,适合作为课程设计、毕业设计或小型实验的参考底稿,方便快速定位可复用的代码片段与实验思路。

1. 拿到“完整代码.zip轨迹预测”先别解压:我判断能否跑通的三个信号

轨迹预测的完整代码包,最怕的是一解压就看到几十个脚本,却不知道哪条路径能把一条观测轨迹送进模型、再吐出未来轨迹。这个“完整代码.zip轨迹预测”面向短时轨迹预测的原型验证和软件/插件式集成,适合课程设计、算法对比和已有系统里的预测模块替换;前提是先对齐数据契约和评估口径,而不是一上来就改网络结构。

我判断一个轨迹预测代码包能不能用,通常看三个信号:有没有唯一的训练入口和推理入口,数据加载是否把观测段与预测段切得干净,评估指标是否明确到“多模态取最小 ADE/FDE”这一层。三个信号缺一个,后面就容易把工程问题误判成模型问题。

这份资源的价值不在脚本数量,而在它把轨迹预测的完整链路摊开了:数据切窗、坐标处理、序列建模、多模态输出、指标计算和可视化。拿到手先把它当成黑匣子跑一条样本,确认输入输出形状,再回头拆模块,会比直接读完全部源码快得多。

2. 拆包与运行基线:把轨迹预测代码跑成可观测的黑匣子

2.1 目录盘点:先找入口、配置和数据契约

压缩包解开后,不要急着安装依赖。我一般先做一次只读盘点:看顶层目录里有没有 train、infer、eval 这类入口脚本,看配置是 yaml、json 还是直接写在 argparse 里,看数据目录下是 csv、npy 还是 txt。轨迹预测代码最常见的翻车点不是模型写错,而是把“一条轨迹一个文件”和“一个场景一个文件”混着读,形状对不上,训练直接报错。

盘点时用unzip -l先看清单,再解压到工作目录,不要直接铺到系统目录。find的层级控制在两层,避免被权重文件和缓存淹没。下面这段命令只做三件事:列清单、解压到独立目录、找入口和配置文件。代码里的complete_traj.zip只是占位,实际换成压缩包名即可。

# 只读查看压缩包结构,确认有没有权重、数据样例和入口脚本 unzip -l complete_traj.zip | head -n 60 # 解压到独立工作目录,避免污染系统 Python 环境 mkdir -p ~/work/traj_pred unzip complete_traj.zip -d ~/work/traj_pred # 只看两层,优先找 train/infer/eval、config、requirements cd ~/work/traj_pred find . -maxdepth 2 -type f | sort | head -n 100

unzip -l的-l是只列清单不解压,适合先判断包大小和文件类型;-d指定解压目录,后面所有命令都在这个目录里跑;find -maxdepth 2限制层级,是为了快速定位入口而不是遍历全部文件。若压缩包内还有一层同名目录,cd时多按一次 Tab 确认,不要凭感觉写路径。

盘点结果按四类归档:入口脚本、配置文件、数据样例、权重与日志。入口脚本决定从哪跑,配置文件决定观测长度和预测长度,数据样例决定坐标是绝对坐标还是相对坐标,权重决定要不要先复现推理。这里不要追求“全看懂”,先把一条样本的输入输出钉死,后面读代码才有参照。

提示:如果压缩包里没有数据样例,先找 README 或脚本里的默认数据路径;连默认路径都没有,就先构造一条[T,2]的假轨迹跑前向,别等数据齐全再动手。

2.2 环境隔离与最小依赖安装

轨迹预测代码的依赖通常集中在 torch、numpy、pandas、scikit-learn、matplotlib 这几类,但压缩包里的 requirements 往往混着训练、可视化和 notebook 的依赖。直接全量安装最容易遇到版本冲突,尤其是 torch 和 numpy 的 ABI 问题。我的做法是先建虚拟环境,再看 requirements 的前几十行,把训练必需和可视化可选分开装。

下面这段命令先建环境,再查看依赖清单,最后装最小集合。sed -n '1,120p'只是看前 120 行,不是让你一次读完;如果 requirements 里有torch==...这种锁定项,优先保留锁定项,不要随手升级到最新。代码块里没有写具体版本号,是因为不同压缩包的锁定策略不一样,照清单走比照经验走稳。

# 建独立环境,避免和系统里的 torch 抢 CUDA 库 python3 -m venv .venv source .venv/bin/activate python -m pip install -U pip # 先看依赖清单,区分训练必需和可视化可选 sed -n '1,120p' requirements.txt # 最小集合先跑通,缺什么再补什么 pip install torch numpy pandas scikit-learn matplotlib

python3 -m venv .venv生成隔离环境,source .venv/bin/activate让后续pip和python都指向这个环境;sed只读不改,用来判断有没有版本锁定和平台标记;最小集合里 torch 负责模型,numpy/pandas 负责数据,scikit-learn 负责归一化,matplotlib 负责可视化。若机器没有 GPU,先装 CPU 版 torch 跑基线,不要为了 CUDA 折腾半天。

安装完成后做一次导入检查,确认torch能正常加载、numpy和torch没有二进制冲突。轨迹预测代码里经常出现np.float32和torch.float32混用,导入阶段不报错,训练阶段才在collate_fn里爆形状。先跑导入检查,能把环境问题挡在数据管线之前。

注意:不要在没有看 requirements 的情况下执行全量安装;轨迹预测包里的 notebook 依赖经常把 torch 拖到不匹配的版本,后面报错很难定位。

2.3 用一条样本跑通前向:把输入输出钉死

环境就绪后,先不要跑训练。训练脚本会同时触发数据加载、模型初始化和日志写入,一旦报错,很难判断是数据问题还是模型问题。我一般先写一个十行的前向脚本:构造[B, obs_len, 2]的随机输入,实例化模型,打印输出类型和形状。输出是[B, pred_len, 2]还是[B, num_modes, pred_len, 2],决定了后面损失函数和评估代码怎么写。

下面这段代码用占位模块名TrajModel,实际导入以压缩包入口脚本为准。参数里obs_len=8、pred_len=12是短时轨迹预测里常见的观测 8 帧预测 12 帧配置,num_modes=3表示多模态输出三组候选。若模型构造函数不接受这些参数,先看入口脚本怎么传参,不要硬改模型内部。

import torch from model import TrajModel # 以压缩包内实际模块名为准 cfg = { "obs_len": 8, # 观测帧数 "pred_len": 12, # 预测帧数 "input_dim": 2, # 每帧 xy 两维 "hidden_dim": 64, # 隐藏维度,先用小值跑通 "num_modes": 3 # 多模态候选数 } model = TrajModel( input_dim=cfg["input_dim"], hidden_dim=cfg["hidden_dim"], num_modes=cfg["num_modes"] ) model.eval() # batch=2,观测 8 帧,每帧 xy x = torch.randn(2, cfg["obs_len"], cfg["input_dim"]) with torch.no_grad(): y = model(x) print(type(y), getattr(y, "shape", None))

torch.randn(2, 8, 2)只验证形状通路,不代表真实分布;model.eval()关闭 dropout 和批归一化的训练行为,保证前向可复现;torch.no_grad()省显存,也避免误触发梯度。打印输出形状后,对照三件事:第一维是不是 batch,倒数第二维是不是预测帧数,最后一维是不是 xy。若输出是元组,先看第一个元素是不是轨迹,别急着改评估代码。

这一步跑通后,再去看训练脚本的collate_fn和损失函数,很多形状问题会变得很清楚。轨迹预测的输入输出一旦钉死,数据管线、模型替换和推理导出都有基准。后面换 LSTM、GRU 或 Transformer,只动编码器,不动数据契约,迁移成本会低很多。

3. 轨迹数据管线:从原始坐标到模型可吃的序列张量

3.1 坐标系、时间步与缺失值:轨迹预测最先翻车的地方

轨迹预测的数据管线比模型更值得花时间。原始轨迹常见三种形态:绝对像素坐标、局部米制坐标、归一化到场景范围的坐标。绝对坐标直接喂模型,模型会学到场景位置而不是运动趋势;局部坐标要先选参考点,通常用观测段最后一帧或场景中心;归一化坐标要保证训练集和验证集用同一套统计量。坐标系不统一,模型在训练集上看着能降 loss,换一段轨迹就整体平移,这是最常见的翻车方式。

时间步也要盯住。有的数据是固定采样率,有的数据中间丢帧,有的数据把不同长度的轨迹补零到同一长度。补零如果不加 mask,模型会把零当成真实停留点,预测出一段不动的轨迹。缺失值处理我一般分两步:先看缺失比例,超过三成的轨迹直接丢弃;少量缺失用线性插值补,补完在数据加载时加一个valid_mask,损失函数里只对有效帧回传。

场景切分同样不能省。同一段轨迹的相邻滑窗如果同时出现在训练集和验证集,验证指标会虚高。正确做法是按轨迹 ID 或场景 ID 切分,不是按滑窗随机打乱。轨迹预测的评估看的是未来一段,相邻窗口共享观测段,随机切分等于把答案漏给验证集。这个坑不显眼,但会让 ADE/FDE 好看得不真实。

提示:先画一张观测段和预测段的叠加图,确认坐标轴方向和尺度;轨迹上下翻转或 xy 颠倒,在数值上不一定报错,但预测会系统性偏航。

3.2 滑窗切样本与归一化:训练/验证/测试不能共享统计量

滑窗切样本的核心参数是观测长度、预测长度和步长。观测 8 帧预测 12 帧适合短时预测,步长取 1 可以增加样本量,但相邻样本高度相关;步长取观测长度的一半,能在样本量和独立性之间折中。切窗时先按轨迹切,再在每条轨迹内部滑窗,不要跨轨迹拼接。下面这段代码把一条[T,2]轨迹切成[N,obs_len,2]和[N,pred_len,2],并在越界前停止。

import numpy as np def make_windows(seq, obs_len=8, pred_len=12, stride=1): """seq: [T, 2];返回观测段和预测段""" xs, ys = [], [] total = obs_len + pred_len if len(seq) < total: return np.empty((0, obs_len, 2), np.float32), np.empty((0, pred_len, 2), np.float32) for start in range(0, len(seq) - total + 1, stride): xs.append(seq[start:start + obs_len]) ys.append(seq[start + obs_len:start + total]) return (np.asarray(xs, dtype=np.float32), np.asarray(ys, dtype=np.float32))

obs_len和pred_len要和模型配置一致;stride控制重叠程度,步长越小样本越多但相关性越强;total用来判断轨迹是否够长,不够长返回空数组而不是抛异常,方便批量处理时跳过。返回值统一转float32,避免后面和 torch 默认精度打架。若数据里有轨迹 ID,切窗前先按 ID 分组,再对每组调用这个函数。

归一化只允许用训练集的观测段统计量。常见错误是用全量数据算均值和方差,或者把预测段也放进 scaler 拟合,验证指标会偏乐观。下面这段代码只拿训练集观测段拟合StandardScaler,预测段和验证集都用同一个 scaler 变换。若数据是局部坐标,均值接近零,标准化仍然有用,因为它把不同场景的尺度拉到同一量级。

from sklearn.preprocessing import StandardScaler # train_x: [N, obs_len, 2],只拿训练集观测段拟合 scaler = StandardScaler() scaler.fit(train_x.reshape(-1, 2)) mean = scaler.mean_.astype(np.float32) # [2] std = scaler.scale_.astype(np.float32) # [2] def normalize(x, mean, std): return (x - mean) / std def denormalize(x, mean, std): return x * std + mean

reshape(-1,2)把[N,T,2]摊平成[N*T,2],让 scaler 按 xy 两列统计;mean_和scale_保存下来,推理时必须复用,不能重新拟合;normalize和denormalize成对出现,评估前把预测值还原到原尺度再算 ADE/FDE。若模型输出多模态,denormalize对最后一维广播即可,不要对模态维做变换。

3.3 Dataset/DataLoader 落地与形状检查

数据切好、归一化做完,再包成 Dataset。轨迹预测的 Dataset 要返回观测段、预测段,有时还要返回轨迹 ID 和有效 mask。__getitem__里只做张量转换,不做随机裁剪和复杂插值,否则多进程加载容易出随机种子问题。下面这段代码把归一化后的数组包成 Dataset,并在__getitem__里统一转float32。

import torch from torch.utils.data import Dataset, DataLoader class TrajDataset(Dataset): def __init__(self, x, y, mean, std): # x/y 已经是 numpy,归一化在这里做,保证训练和验证共用统计量 self.x = (x - mean) / std self.y = (y - mean) / std def __len__(self): return len(self.x) def __getitem__(self, i): return (torch.from_numpy(self.x[i]).float(), torch.from_numpy(self.y[i]).float()) train_ds = TrajDataset(train_x, train_y, mean, std) train_loader = DataLoader( train_ds, batch_size=64, shuffle=True, num_workers=0, # 先关多进程,确认形状后再开 drop_last=False ) xb, yb = next(iter(train_loader)) print("xb", xb.shape, "yb", yb.shape) # 期望 [B,8,2] 和 [B,12,2]

batch_size=64是起点,显存不够就降到 32 或 16;shuffle=True只打乱样本顺序,不改变轨迹内部时间顺序;num_workers=0先排除多进程和 Windows 路径问题,跑通后再调大;drop_last=False保留最后一个不完整 batch,评估时更完整。打印形状时重点看第二维是不是观测长度和预测长度,最后一维是不是 2。

形状检查通过后,再检查数值范围。归一化后的观测段应该在零附近波动,若出现大面积-10以下的数值,说明 std 太小或数据里有异常点。预测段还原后要和原始轨迹对齐,取一条样本画出来,观测段和预测段应该首尾相接。这个检查花两分钟,能省掉后面几个小时的“模型不收敛”排查。

4. 模型与训练配置:LSTM/GRU/Transformer 的选择边界和参数表

4.1 编码器-解码器与多模态头:轨迹预测的输出到底怎么定义

轨迹预测模型的输出定义比网络结构更重要。单模态输出是[B,pred_len,2],多模态输出是[B,K,pred_len,2],K 个候选轨迹。多模态的意义是覆盖未来可能的分叉,但评估时通常取 K 条里距离真值最近的一条算 ADE/FDE,这叫 minADE/minFDE。若代码里把 K 条取平均再算误差,指标会虚低,看起来很好,实际部署时没有一条能用。

编码器选择看数据量和序列长度。观测 8 帧、预测 12 帧这种短序列,LSTM 或 GRU 足够,参数量小,训练稳定,适合课程设计和快速原型。Transformer 对长序列和复杂交互更强,但需要更多数据和更细的学习率调度;数据只有几千条样本时,Transformer 容易过拟合,训练 loss 降得漂亮,验证 ADE 不动。我的建议是先用 GRU 跑基线,确认数据管线没问题,再换 Transformer 做对比。

多模态头的常见做法是输出 K 组偏移量,每组对应一条候选轨迹。损失函数可以是回归损失加多样性约束,也可以是高斯混合模型的负对数似然。回归损失简单,但容易让 K 条轨迹收敛到同一条均值轨迹;NLL 能保留多模态,但训练更敏感。代码包若已经给了多模态头,先不要改结构,先把 K 设成 1 跑单模态基线,再逐步加到 3 或 5,观察 minADE 和轨迹分散度。

4.2 损失函数与评估指标:ADE/FDE 的实现细节

ADE 是预测段所有帧位移误差的平均,FDE 是最后一帧的位移误差。多模态场景下,minADE 先对每个样本取 K 条候选中误差最小的那条,再对 batch 求平均。实现时注意维度:预测[B,K,T,2],真值[B,T,2],先广播到[B,K,T,2]再算欧氏距离,得到[B,K,T],最后在 T 维平均、K 维取最小。下面这段代码是评估函数的核心,训练时也可以用它做验证监控。

import torch def ade_fde(pred, gt): """pred: [B,K,T,2];gt: [B,T,2];返回 minADE/minFDE""" # 广播后算每帧欧氏距离 d = torch.linalg.norm(pred - gt[:, None, :, :], dim=-1) # [B,K,T] ade = d.mean(dim=-1).min(dim=1).values.mean() # 先对 T 平均,再对 K 取最小 fde = d[:, :, -1].min(dim=1).values.mean() # 最后一帧取最小 return ade.item(), fde.item()

gt[:, None, :, :]在 K 维插入一个维度,让真值和 K 条预测广播;dim=-1对 xy 求范数;mean(dim=-1)得到每条候选的 ADE;min(dim=1)取 K 条里最小;最后.mean()对 batch 平均。FDE 直接取d[:,:,-1],不需要再对 T 平均。若预测是[B,T,2]单模态,先把 K 维补成 1,再调用同一个函数,避免两套评估口径。

训练损失和评估指标要分开。训练可以用 SmoothL1 或 MSE,评估必须用 minADE/minFDE;训练 loss 降不代表 minADE 降,因为多模态头的多样性约束可能让某条候选变差。验证时每轮都算一次 minADE,保存最好的权重,不要只看训练 loss。若 minADE 在前几轮下降后反弹,先看学习率和 batch size,再看多模态 K 是否过大。

4.3 训练脚本参数与日志:学习率、批量、早停

训练脚本的参数不用一次调到位,先用小模型、小批量跑通一轮,再放大。下面这张表是我在短时轨迹预测里常用的起点,不是固定答案,具体按数据量和显存调整。观测 8 预测 12 的配置下,GRU 隐藏维度 64 到 128 足够,Transformer 的层数不要一上来就堆深。

参数起点值调整边界说明
obs_len / pred_len8 / 12按数据采样率定观测太短看不出趋势,预测太长误差发散
batch_size6416~256显存不够先降 batch,不要先降模型
hidden_dim6432~256小数据先用小隐藏维度
num_modes31~5先跑单模态基线,再加多模态
lr1e-31e-4~3e-3AdamW 起点,配合余弦退火
epochs5020~100早停看验证 minADE
grad_clip1.00.5~5.0序列模型防梯度爆炸

训练循环里要加梯度裁剪和验证早停。下面这段代码只保留核心步骤:前向、损失、反向、裁剪、更新、验证。损失用 MSE 只是基线,若模型输出多模态,改成逐候选最小损失或 NLL。clip_grad_norm_对序列模型很关键,GRU 和 Transformer 都可能出现梯度尖峰。

optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50) for epoch in range(50): model.train() for xb, yb in train_loader: pred = model(xb) # 单模态 [B,T,2];多模态 [B,K,T,2] if pred.ndim == 4: # 多模态基线:每个候选都算 MSE,取最小损失回传 loss = ((pred - yb[:, None]) ** 2).mean(dim=(2, 3)).min(dim=1).values.mean() else: loss = ((pred - yb) ** 2).mean() optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() scheduler.step() # 每个 epoch 后用验证集算 minADE/minFDE,保存最优权重

AdamW的weight_decay=1e-4是正则项起点,数据量小可以加到1e-3;CosineAnnealingLR的T_max和总 epoch 对齐;多模态损失里min(dim=1)只取当前最优候选回传,能让每条候选学到不同模式,但早期可能不稳定,可以先用全部候选平均损失预热几轮。验证阶段不要忘了model.eval()和torch.no_grad(),否则 dropout 会让指标抖动。

训练日志至少记录训练 loss、验证 minADE、验证 minFDE、学习率和当前 epoch。轨迹预测的血泪经验是:只看 loss 会误判,必须把 minADE 和可视化样本一起看。若 loss 降但 minADE 不降,优先查归一化还原和评估维度,而不是继续调网络。

5. 避坑与排查:轨迹预测完整代码最容易在五个地方失真

5.1 现象:loss 降但 ADE 不降

现象是训练 loss 一路下降,验证集 minADE 在前几轮后几乎不动,甚至反弹。原因通常不是模型容量不够,而是训练损失和评估口径不一致:训练用 MSE 在归一化空间算,评估在还原后的原尺度算,两者尺度差了一个 std;或者多模态训练时对 K 条候选取了平均损失,评估却取最小,优化目标对不上。

解决分三步。先把评估函数在归一化空间和原尺度各算一次,确认还原没有漏乘 std;再把训练损失改成和评估一致的“逐候选最小损失”,至少让优化目标对齐;最后固定一条验证样本,打印预测段和真值的数值,确认不是整体偏移。若三步都正常,再考虑模型容量和数据量。

我遇到过一种更隐蔽的情况:数据里混了不同采样率的轨迹,归一化后短采样率轨迹的位移被放大,训练 loss 看似正常,验证集按帧算 ADE 就偏高。处理方式是按采样率分组统计,或者统一重采样到固定时间间隔。这个坑不体现在代码形状上,只体现在指标里,排查时先看数据分布,别先改网络。

5.2 现象:推理结果整体平移

现象是可视化里预测轨迹形状和真值相似,但整体往一个方向偏,ADE 明显大于 FDE 的合理范围。原因多半是坐标系参考点不一致:训练时用观测段最后一帧做局部原点,推理时用了场景中心;或者归一化用了训练集均值,推理时忘了减均值。整体平移是轨迹预测里最典型的“代码没报错但结果不对”。

解决方式是固定参考点,并在推理入口显式做同一套变换。局部坐标的参考点建议用观测段最后一帧,这样预测段从原点出发,模型学的是相对位移;推理时先减参考点,预测完再加回去。归一化参数从训练集保存到文件,推理加载同一份,不要重新fit。下面这段伪代码把参考点变换和归一化串起来,实际变量名按压缩包调整。

def to_local(obs_xy, ref=None): # obs_xy: [T,2];默认用最后一帧做局部原点 ref = obs_xy[-1] if ref is None else ref return obs_xy - ref, ref def from_local(pred_local, ref): # pred_local: [K,T,2] 或 [T,2] return pred_local + ref

ref必须同时用于观测段和预测段,训练和推理共用;to_local返回参考点,推理结束后from_local加回去。若数据本身就是局部坐标,不要再减一次,否则会二次平移。检查方法是取一条样本,把预测段和真值画在同一张图上,看起点是否重合;起点不重合,先查参考点,不要查模型。

5.3 现象:换数据集后形状报错

现象是换一份轨迹数据后,DataLoader 报stack expects each tensor to be equal size,或者模型前向报维度不匹配。原因通常是新数据的轨迹长度不一致,__getitem__返回的预测段长度不同;或者新数据是[T,3]带时间戳,代码仍按[T,2]取 xy。轨迹预测代码包在原始数据上能跑,不代表换数据能跑。

解决方式是在 Dataset 里做长度截断或补齐,并在collate_fn里统一。短于obs_len+pred_len的轨迹直接过滤,长轨迹按滑窗切;带时间戳的数据先选 xy 两列,时间戳只用于排序和重采样。若必须补齐,补零后加valid_mask,损失函数里屏蔽无效帧。不要指望模型自己学会忽略零值。

更稳妥的做法是在数据加载前加一道形状断言:assert seq.shape[1] == 2,并在切窗后打印x.shape和y.shape。换数据集时先跑 10 条样本,确认形状、数值范围和参考点都一致,再启动训练。形状问题早发现是几分钟,晚发现就是一轮训练白跑。

5.4 现象:多模态预测所有轨迹重合

现象是 K 条候选轨迹几乎完全重合,minADE 和单模态差不多,多模态头没有起作用。原因通常是多样性约束太弱,或者训练时只对最小损失候选回传,其他候选没有梯度。多模态头如果初始化在同一组参数上,又没有分离机制,很容易退化成单模态。

解决方式有两种:一是加多样性正则,惩罚候选之间的平均距离过小;二是用 NLL 或 winner-takes-all 的变体,让不同候选负责不同模式。先用 K=3 跑,不要一上来 K=5;K 越大,每条候选分到的监督越少。验证时除了 minADE,还要看候选之间的平均离散度,离散度接近零就说明多模态退化。

检查方法是取一条分叉明显的样本,把 K 条预测画出来。若所有候选都贴着均值走,先看损失函数里有没有多样性项,再看学习率是不是太大导致候选塌缩。多模态不是把输出维度乘 K 就完事,训练信号必须让不同候选看到不同的未来模式。

5.5 现象:可视化正常但指标异常

现象是画出来的轨迹和真值看着很接近,但脚本算出的 ADE/FDE 很大,或者出现负数。原因通常是指标计算前没有还原归一化,或者把像素坐标和米制坐标混在一起;也有可能是评估时把预测段和观测段错位,拿预测的第 0 帧对真值的最后一帧。可视化用的是还原后的数据,指标用的是归一化数据,两者自然对不上。

解决方式是评估函数只接受原尺度数据,归一化还原在进入评估前完成;打印一对预测和真值的数值,手工算一帧距离核对。若指标出现负数,检查是不是用了带符号的差值而没有取范数。评估脚本里加一句形状断言:预测和真值的[T,2]必须对齐,batch 维一致。

我一般会在评估前保存一条样本的预测、真值和还原参数,跑完指标后手工复算一次。这个“后悔药”不费事,但能避免把归一化问题误判成模型问题。轨迹预测的指标对尺度很敏感,可视化正常不代表数值正确,两个口径必须同时对齐。

6. 推理与验证:把轨迹预测代码变成可复现的软件/插件交付物

6.1 导出推理接口:单样本、批量、滑动更新

推理入口要独立于训练脚本,输入观测轨迹和归一化参数,输出原尺度候选轨迹。下面这个函数只做三件事:归一化、前向、还原。mean和std必须来自训练集保存的文件,不能现场重算。

@torch.no_grad() def predict_traj(model, obs_xy, mean, std, device="cpu"): model.eval() x = (obs_xy - mean) / std x = torch.from_numpy(x[None]).float().to(device) y = model(x).cpu().numpy()[0] # [K,T,2] 或 [T,2] return y * std + mean

obs_xy是[obs_len,2],[None]补 batch 维;device按部署环境切换;还原时std和mean对最后一维广播。批量推理把[None]换成[B],滑动更新则每次保留最近obs_len帧,丢掉最老的一帧。

6.2 验证清单:从数值到可视化的三重校验

交付前跑一张清单,别只看 loss。下表是我每次都会过的三项,缺一项都不算验证完成。

校验项检查内容失败信号
数值单条样本手工复算 ADE与脚本差一个量级
形状单样本/批量输出一致batch 维丢失或 K 维错位
可视化观测段与预测段首尾相接起点偏移或方向翻转

数值校验用一条固定样本,手工算预测末帧和真值末帧的欧氏距离,和脚本 FDE 对得上才算过;形状校验分别跑 batch=1 和 batch=8,输出维度必须只差第一维;可视化校验看起点是否重合,方向是否和观测段连续。

6.3 一个具体技巧:用回放式误差剖面定位漂移段

不要只看整段 ADE,把预测段按帧算误差,画一条误差剖面。横轴是预测帧,纵轴是该帧的平均位移误差。误差在第 5 帧后突然抬升,通常是模型只会短程外推;误差从第 1 帧就高,通常是参考点或归一化错了。这个技巧能把“模型不行”拆成“哪一段不行”。

从那以后我每次交付轨迹预测模型,都先跑回放式误差剖面,再谈调参;需要这份轨迹预测完整代码包的,按文末方式取用,先跑基线再改模型。希望帮到你。

本文还有配套的精品资源,点击获取