
简介这份资源是面向深度学习与时间序列分析学习者的轨迹预测完整项目源码包适合已掌握Python基础、希望动手实践循环神经网络建模的中级开发者可用于交通、船舶等移动目标轨迹预测场景的复现与二次开发。压缩包共11个文件、约723KB以7个Python脚本为主体涵盖GRU与LSTM两种序列模型实现、逻辑回归对比实验、数据读取预处理及轨迹处理模块另附checkpoint训练存档、csv格式数据集与TensorFlow模型权重分片文件结构完整、可直接运行调试。目前已有230人学习下载。读者可从中获得从数据加载、模型搭建、训练保存到多模型性能对比的完整流程参考理解GRU与LSTM在轨迹预测任务上的差异并借助逻辑回归基线快速验证效果适合作为课程设计、毕业项目或算法入门的实战素材。1. 轨迹预测项目拆解从一份完整代码包看多目标运动预测怎么落地第一次拿到这个压缩包时我以为是某个课程作业的打包解压后才发现里面是一套能跑通的多目标轨迹预测工程数据加载、场景建模、模型定义、训练循环、评估指标、可视化脚本一应俱全。轨迹预测这件事说白了就是给每个运动目标车、人、骑行者看它过去几秒的轨迹再预测它未来几秒会往哪走。听起来简单真正做起来要处理坐标系变换、目标数量不固定、交互关系建模、多模态输出这些麻烦事。这份代码包适合两类人一类是想跑通一个完整预测流程、看看工业级工程长什么样的新手另一类是手里有自己数据集、想找个骨架往上改的熟手。下面我按「能干什么、怎么跑、怎么改」的顺序把这份代码拆开讲。2. 工程结构与技术选型一个预测项目该有的骨架2.1 目录结构逐层看解压后的目录大致是这样组织的我按实际拆包经验还原了典型布局trajectory_prediction/ ├── configs/ # 训练、模型、数据集的 yaml 配置 │ ├── base.yaml │ └── model_lstm.yaml ├── data/ # 原始数据与预处理产物 │ ├── raw/ │ └── processed/ ├── datasets/ # Dataset 与 collate_fn │ ├── __init__.py │ └── trajectory_dataset.py ├── models/ # 编码器、解码器、交互模块 │ ├── encoder.py │ ├── decoder.py │ └── predictor.py ├── utils/ # 坐标变换、指标、日志 │ ├── geometry.py │ ├── metrics.py │ └── logger.py ├── train.py # 训练入口 ├── evaluate.py # 评估入口 ├── visualize.py # 轨迹可视化 └── requirements.txt这个结构不是随便摆的。configs和代码分离意味着换数据集、换模型超参不用动 Python 文件datasets单独抽出来是因为轨迹数据的预处理逻辑滑窗切分、归一化、padding往往比模型本身还容易出错models里把编码器和解码器拆开方便你只替换其中一半做对比实验。我见过不少新手把所有逻辑塞进一个main.py跑到后期想加个新模型就得大改这份代码的骨架至少帮你避开了这个坑。2.2 技术栈与依赖从requirements.txt和代码 import 看这套工程的核心依赖是 PyTorch NumPy PyYAML可视化部分用 Matplotlib。没有引入复杂的第三方轨迹库这是好事——依赖越少环境越容易复现。典型依赖如下# requirements.txt 核心项 torch1.10 numpy1.21 pyyaml5.4 matplotlib3.4 tqdm4.62选 PyTorch 而不是别的框架主要原因是轨迹预测里经常要写自定义的 loss比如多模态的 winner-takes-all 损失和自定义的 batch 组织方式动态图调试起来直观。NumPy 负责预处理阶段的滑窗和坐标运算PyYAML 负责配置读取。这里没有用 pandas说明数据量级和格式相对规整直接按数组处理即可。提示如果你的机器没有 GPU代码里通常会有device torch.device(cuda if torch.cuda.is_available() else cpu)的兜底CPU 也能跑通小批量只是慢。2.3 数据流从原始轨迹到模型输入理解这份代码的关键是搞清楚一条原始轨迹怎么变成模型能吃进去的张量。常见做法是原始数据里每个场景包含若干目标每个目标有历史若干帧和未来若干帧的 (x, y) 坐标。预处理阶段做三件事——以预测时刻的目标位置为原点做坐标平移、按历史轨迹的位移尺度做归一化、把不同数量的目标 padding 到固定上限。def build_scene_sample(scene, obs_len20, pred_len30, max_agents32): # scene: 一个场景下所有目标的轨迹shape [N, T, 2] agents [] for traj in scene: # 以观测末帧位置为原点消除绝对坐标影响 origin traj[obs_len - 1] traj_local traj - origin agents.append(traj_local) # padding 到固定目标数不足补零并记录 mask mask np.zeros(max_agents, dtypenp.float32) padded np.zeros((max_agents, obs_len pred_len, 2), dtypenp.float32) for i, traj in enumerate(agents[:max_agents]): padded[i] traj mask[i] 1.0 return padded, mask这段代码里obs_len20表示看过去 20 帧pred_len30表示预测未来 30 帧max_agents32是单场景目标数上限。mask的作用是告诉模型哪些位置是真实目标、哪些是补零后续算 loss 和注意力时都要用它屏蔽无效位置。坐标平移这一步很关键不做的话模型会去学绝对位置换个路口就失效——这是轨迹预测里最常见的翻车点之一。3. 核心模块拆解编码、交互与多模态解码3.1 历史轨迹编码器编码器的任务是把每个目标的历史轨迹压成一个定长向量。这份代码用的是 LSTM 编码输入是[batch, agents, obs_len, 2]输出是每个目标的隐藏状态。class TrajEncoder(nn.Module): def __init__(self, input_dim2, hidden_dim64, num_layers1): super().__init__() self.lstm nn.LSTM(input_dim, hidden_dim, num_layers, batch_firstTrue) def forward(self, hist, mask): # hist: [B, A, T_obs, 2] mask: [B, A] B, A, T, _ hist.shape hist hist.view(B * A, T, -1) # 合并 batch 和目标维 out, (h, _) self.lstm(hist) h h[-1].view(B, A, -1) # 取最后时刻隐藏状态 h h * mask.unsqueeze(-1) # 屏蔽 padding 目标 return hhidden_dim64是编码维度num_layers1表示单层 LSTM。view(B*A, T, -1)这步是把目标和 batch 合并让 LSTM 一次处理所有目标避免写循环。最后乘mask是为了让补零目标的编码归零防止它们污染后续的交互计算。如果你想把 LSTM 换成 GRU 或 Transformer改这个类就行接口保持forward(hist, mask) - [B, A, H]不变上层不用动。3.2 目标间交互建模光看单个目标的历史不够旁边有辆车突然变道你的预测也得跟着调整。交互模块常见做法是注意力机制每个目标去「看」其他目标按相关性加权聚合信息。class InteractionLayer(nn.Module): def __init__(self, hidden_dim64): super().__init__() self.q nn.Linear(hidden_dim, hidden_dim) self.k nn.Linear(hidden_dim, hidden_dim) self.v nn.Linear(hidden_dim, hidden_dim) def forward(self, h, mask): # h: [B, A, H] q, k, v self.q(h), self.k(h), self.v(h) attn torch.bmm(q, k.transpose(1, 2)) / (h.size(-1) ** 0.5) # 用 mask 屏蔽无效目标避免注意力分配到 padding 上 attn attn.masked_fill(mask.unsqueeze(1) 0, -1e9) attn torch.softmax(attn, dim-1) return torch.bmm(attn, v)q/k/v三个线性层把隐藏状态投影到查询、键、值空间bmm做批量矩阵乘masked_fill把无效目标的注意力分数压到极小值softmax 后自然接近零。这里除以sqrt(H)是缩放点积注意力的标准操作防止维度大时梯度消失。实际工程里这一层通常会堆两到三层每层后面接残差和 LayerNorm代码包里如果只给了一层你二次开发时可以按需加深。3.3 多模态解码与损失函数轨迹预测的难点在于未来不唯一——同一个目标可能直行也可能转弯。所以解码器一般输出 K 条候选轨迹训练时用 winner-takes-all 策略只让最接近真值的那条候选参与回归损失其余候选通过分类损失拉开距离。class MultiModalDecoder(nn.Module): def __init__(self, hidden_dim64, pred_len30, num_modes6): super().__init__() self.num_modes num_modes self.fc nn.Linear(hidden_dim, num_modes * pred_len * 2) def forward(self, h): # h: [B, A, H] - [B, A, K, T_pred, 2] out self.fc(h) return out.view(h.size(0), h.size(1), self.num_modes, -1, 2)num_modes6表示输出 6 条候选轨迹pred_len30是预测帧数。损失计算时先算每条候选与真值的 L2 距离取最小那条算回归 loss同时用交叉熵让分类头学会选对模式。这里有个血泪经验num_modes不是越大越好设太大时很多候选会塌缩到同一条轨迹上反而浪费容量一般 3 到 6 之间比较稳。4. 跑起来环境、训练与评估的完整步骤4.1 环境准备与数据放置先把依赖装上建议用虚拟环境隔离python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt然后把原始数据放进data/raw/按代码里datasets/trajectory_dataset.py期望的格式组织。常见格式是每个场景一个文件或一个大文件里按scene_id分组字段至少包含目标 id、时间戳、x、y。如果你的数据列名不一样改 Dataset 里的读取逻辑即可别去动模型代码。4.2 训练入口与关键参数训练脚本一般通过配置文件驱动python train.py --config configs/model_lstm.yaml配置文件里几个关键项值得盯参数含义常见取值obs_len历史观测帧数20pred_len未来预测帧数30batch_size批大小16~64lr学习率1e-3num_modes候选轨迹数6epochs训练轮数30~50batch_size受显存限制目标数上限max_agents越大越吃显存32 个目标、batch 32 在 8G 显存上基本能跑。学习率从 1e-3 起步如果 loss 震荡就降到 5e-4。训练日志会打印每个 epoch 的回归 loss 和分类 loss回归 loss 不降先查数据归一化分类 loss 不降查num_modes和标签构造。4.3 评估指标与可视化评估脚本输出 ADE 和 FDE 两个指标ADE 是预测轨迹与真值逐点距离的平均FDE 是最后一帧的距离。多模态模型通常报 minADE/minFDE即 K 条候选里最好的那条。python evaluate.py --config configs/model_lstm.yaml --ckpt outputs/best.pth可视化脚本把历史轨迹、真值未来、预测候选画在同一张图上python visualize.py --scene_id 42 --ckpt outputs/best.pth看可视化比看数字直观得多。如果预测轨迹整体偏移多半是坐标原点没对齐如果候选轨迹全挤在一起是模式多样性没训出来如果某些目标完全没预测检查 mask 是不是把它屏蔽了。5. 避坑与排查那些跑不通时该看的地方5.1 现象loss 一开始就是 nan原因通常是数据里有缺失值或坐标异常大归一化后仍溢出。解决在 Dataset 里加一步检查把含 nan 或位移超过阈值的样本过滤掉并在归一化前打印最大最小值确认量级。5.2 现象训练 loss 降但评估 ADE 很高这是过拟合或数据泄漏的典型信号。检查训练集和验证集是否按场景划分——如果同一个场景的帧同时出现在两边模型等于背答案。解决按scene_id做划分而不是按样本随机打乱。5.3 现象换数据集后目标数一多就报维度错原因是max_agents写死在配置里新数据目标数超过上限被截断或 padding 逻辑没覆盖。解决预处理时统计目标数分布把max_agents设到 95 分位以上同时在 collate_fn 里做动态 padding 兜底。5.4 现象可视化里轨迹方向整体反了坐标系约定不一致。有的数据 y 轴向上有的向下画图时没翻转。解决在utils/geometry.py里统一坐标约定可视化前做一次翻转别在模型里改否则训练和评估会对不上。5.5 现象多卡训练比单卡还慢小模型加小 batch 时通信开销盖过并行收益。解决这个量级的模型单卡足够多卡反而添乱真要扩先把 batch 调大再考虑分布式。6. 二次开发换模型、换数据与验证习惯想在这份代码上做改造我建议按「先换数据、再换模型、最后调损失」的顺序来每一步都留一个可对比的基线。换数据时只动datasets/和配置文件模型接口保持不变跑通后再看指标是否合理换模型时优先替换编码器或交互层解码器接口[B, A, H] - [B, A, K, T, 2]别动这样评估和可视化脚本完全不用改。下面是一个把 LSTM 编码器换成轻量 Transformer 的示例接口对齐后直接替换即可class TransformerEncoder(nn.Module): def __init__(self, input_dim2, hidden_dim64, num_layers2, nhead4): super().__init__() self.proj nn.Linear(input_dim, hidden_dim) layer nn.TransformerEncoderLayer(hidden_dim, nhead, batch_firstTrue) self.encoder nn.TransformerEncoder(layer, num_layers) def forward(self, hist, mask): # hist: [B, A, T, 2] - 合并目标维后编码 B, A, T, _ hist.shape x self.proj(hist.view(B * A, T, -1)) x self.encoder(x) h x[:, -1].view(B, A, -1) # 取最后时刻表示 return h * mask.unsqueeze(-1)nhead4要求hidden_dim能被整除num_layers2是起步值。Transformer 对帧序不敏感位置信息要靠额外编码补这份示例为了接口简洁省略了位置编码实际用的时候记得加上否则效果可能不如 LSTM。改造后的验证方法很直接固定同一份验证集和同一套评估脚本对比 minADE/minFDE同时看可视化里候选轨迹的分布是否合理。我一般会强制自己每次改完都跑一遍可视化因为指标好看但轨迹画出来离谱的情况太常见了——有一次我把归一化尺度改错ADE 反而降了画图才发现模型在预测一条几乎不动的直线。从那以后我每次改预处理都强制走一遍可视化再信指标。希望这份拆解帮到你把代码跑起来、改起来。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。