ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

轨迹预测基线复现实战:等速、单车与Kalman模型

轨迹预测基线复现实战:等速、单车与Kalman模型 简介面向车辆轨迹预测研究该代码包提供恒定速度与运动学单车模型的可复现基线实现并已在NGSIM US-101和I-80数据集上完成验证。压缩包为zip格式共37个文件以33个Python脚本为主覆盖数据加载、预处理、预测器、结果统计与可视化等目录模块另有2个MATLAB脚本、1份Markdown说明和1个YAML配置整包仅61KB结构紧凑便于按模块复用和二次开发。评估环节完整给出RMSE、FDE、NLL与未命中率指标其中NLL公式经过特别校准避免因定义不一致造成对比失真代码仅依赖车辆位置跟踪数据因此任何同类数据库均可尝试迁移。作为扩展资源还用LSTM/GRU循环神经网络替代恒定速度假设显著改善预测精度与不确定性估计为构建更复杂、可解释的模型提供了清晰起点。对于期望快速建立轨迹预测基准、复现基线结果或探索RNN改进的研究人员与工程师这是一个实用的工具包目前已有1006人学习下载。1. 可复现轨迹预测基线为什么等速模型依然值得跑一遍车辆轨迹预测的论文里最常见的套路是拿一组基线模型做对比然后用一句“baseline reproduced from previous work”带过。但真到自己复现时你会发现最缺的不是新模型而是那个“别人口中简单到不值得描述”的等速模型和单车模型。这个资源包做的就是这件事把等速、单车、Kalman 这类运动学基线在 NGSIM 数据集上跑通输出 RMSE、FDE、NLL 和 MR 四项指标并且把每个环节的代码都摊开。适合三类人刚接触轨迹预测、想先建立一个可信对比底线的学生做多目标跟踪但需要预测模块做参照的工程师以及想把 RNN 学习模型和运动学公式放在同一套评估框架里对齐的研究者。它的价值不在于算法有多新而在于把“可复现”这三个字落实到了每行代码。2. 基线与数据准备NGSIM 数据库加载、坐标转换与指标口径2.1 等速与单车运动学模型先弄清楚预测的是相对位置轨迹预测的基线模型通常分为两类一类是纯运动学推导比如等速度模型Constant Velocity, CV和等加速度模型Constant Acceleration, CA另一类是考虑车辆转向机构的单车模型Bicycle Model。这个资源包里的predictors目录下分别有constant_velocity_predictor.py、constant_acceleration_predictor.py和bicycle_predictor.py。注意它们预测的都不是绝对坐标下的经纬度而是“目标车辆相对于自车的相对位置”。这么做的好处是天然对齐 NGSIM 数据集的坐标系——NGSIM 提供的是以道路方向为 x 轴、垂直方向为 y 轴的局部坐标直接用相对位置可以避免自车运动造成的平移干扰。等速模型的核心假设是物体在未来一段时间内保持当前速度不变。给定起始位置(x0, y0)和速度(vx, vy)预测 t 时刻的位置为x_pred x0 vx * t y_pred y0 vy * t代码里通常用kalman_basis.py封装一个基础的状态转移矩阵把位置和速度作为状态做线性 Kalman 滤波。这里的t是预测步长单位是秒需要和数据集的时间分辨率对齐。NGSIM 原始数据是 10Hz即每 0.1 秒一帧如果你要预测未来 3 秒就需要预测 30 个点。kalman_basis.py里的状态转移矩阵是按时间步长dt构建的你可以直接修改dt参数来控制预测时长。单车模型则引入了横摆角速度假设车辆的前轮转角决定转向半径。描述车辆运动的微分方程是# 单车模型的状态x, y, heading, velocity # 控制量前轮转角 delta x_next x v * cos(heading delta) * dt y_next y v * sin(heading delta) * dt heading_next heading (v / L) * tan(delta) * dt其中L是轴距这个参数在bicycle_predictor.py里可以配置。实际使用中如果数据集没有提供转向角基线实现会从前几帧的轨迹中估计车辆的转弯半径然后反推delta。这个估计过程容易受噪声影响所以在换道场景下单车模型不一定比等速模型更准。这也是为什么你做对比实验时一定要把两种模型都跑出来不要只挑表现好的一个写进论文。2.2 数据加载与预处理从 NGSIM 文件到训练样本NGSIM 数据集提供的是车辆轨迹的原始 CSV每条记录包含车辆 ID、帧号、局部坐标x、y、速度、车道等信息。loadNGSIM.py的作用就是把原始 CSV 解析成统一的字典格式方便后续预测脚本调用。加载流程大致如下from loaders.loadNGSIM import load_ngsim # 指定原始 CSV 路径和地图参数 raw_data load_ngsim( data_dirdata/ngsim/, datasetus101, # 或 i80 time_span15.0, # 只保留前 15 秒数据避免跟踪漂移 ) # 返回的 raw_data 是 DataFrame包含 frame_id, vehicle_id, x, y, vx, vy 等列 print(raw_data.head())加载后会做三件事过滤不完整的车辆轨迹比如少于 50 帧的记录、按时间排序、丢弃异常跳变点。预处理的 MATLAB 脚本preprocess_data.m和find_sorted.m也提供了同样的功能但只在 MATLAB 环境中运行。如果你是纯 Python 用户直接使用loadNGSIM.py即可效果等价。要注意dataset参数需要区分us101和i80因为两条道路的车道数和曲率不同数据切分方式也会有差异。预处理的关键是确定“预测窗口”。常见做法是将每条轨迹划分成历史段和未来段用前 2 秒20 帧作为观测输入预测后 3 秒30 帧。这个窗口大小在settings.yaml里配置data: history_len: 20 future_len: 30 dt: 0.1 target_ids: null # 为 null 表示使用全部车辆target_ids可以指定某几辆车调试时非常有帮助。我在最初复现时直接把所有车辆扔进去跑结果可视化时画面密密麻麻没法检查单车的预测行为。后来先把target_ids限制为三辆车确认单条轨迹的预测合理后再放开全量数据。2.3 评估指标 RMSE、FDE、NLL 与 MR 的计算口径这个资源包最麻烦的部分是指标计算尤其是负对数似然NLL。论文里常说“Gaussian NLL”但不同实现用的公式可能相差很大。这里的stats_multi_object_results.py和stats_results.py统一计算四项指标。RMSE 和 FDE 是确定性指标的度量。RMSE 计算整个预测时间窗口内每个点的均方根误差FDE 只计算最后一个预测点和真实终点的欧氏距离。代码如下import numpy as np def rmse(pred, true): # pred, true: shape [num_samples, pred_len, 2] return np.sqrt(np.mean((pred - true) ** 2)) def fde(pred, true): return np.mean(np.linalg.norm(pred[:, -1] - true[:, -1], axis1))NLL 的计算则要看你的模型是否输出不确定性。如果预测结果是一个多元高斯分布那么 NLL 是负的对数概率密度如果预测结果只是确定性坐标那你需要手动为每个预测点加上一个协方差。这里的代码里NLL计算中会把协方差矩阵限制在对角阵上并且给对角线加了 1e-6 的 eps 防止除零。这和某些论文用全协方差矩阵的做法不同对比时一定要仔细核对公式。MRMiss Rate的定义是预测误差超过某个阈值通常 2 米的样本比例def miss_rate(pred, true, threshold2.0): distance np.linalg.norm(pred[:, -1] - true[:, -1], axis1) return np.mean(distance threshold)注意这里MR用的是最终位移误差来判断而不是整个轨迹的平均误差。如果换成平均误差阈值数值会变大所以写论文时必须在实验设置里说明清楚。3. 复现流程从命令行到结果文件3.1 Python 环境与依赖安装代码包依赖的库比较常规numpy、scipy、ruamel.yaml用于配置bokeh和cv2用于可视化torch用于 RNN 扩展模块。建议用 Python 3.8 以上版本创建一个独立环境conda create -n trajectory python3.9 conda activate trajectory pip install numpy scipy ruamel.yaml bokeh opencv-python torch安装完建议先测试导入是否顺畅。我遇到过一次plot_bokeh.py和plot_utils.py之间循环导入的报错后来把plot_utils.py里的一个顶层函数改成延迟导入才解决。如果你在环境中也遇到类似问题优先检查脚本之间的导入顺序而不是急着装老版本库。3.2 数据路径配置与单目标预测先要确保settings.yaml里的data_dir指向正确的 NGSIM 数据目录。然后运行单目标 Kalman 预测脚本python train_kalman_predict.py --config settings.yaml --mode single脚本执行流程是加载数据 → 切分训练和测试集 → 对每辆车执行 Kalman 滤波 → 输出预测结果和指标。train_kalman_predict.py里会调用kalman_basis.py做状态预测默认的process_noise是 1.0measurement_noise是 0.1。这两个参数直接影响轨迹平滑程度process_noise调大会让模型更信任历史轨迹预测轨迹更平滑但反应更迟钝measurement_noise调大则会过度跟随噪声导致预测抖动。运行完会得到results/下的single_result.npz文件里面保存了pred和true数组。你可以用plot_pyplot.py快速画一张轨迹对比图from plot_pyplot import plot_trajectory import numpy as np data np.load(results/single_result.npz) plot_trajectory(data[pred][:5], data[true][:5])这里plot_trajectory会画出预测轨迹红色虚线和真实轨迹蓝色实线。如果看到预测轨迹在换道时明显偏离不要奇怪——Kalman 线性模型在非线性场景下的表现本来就有限这正是后面要替换成 RNN 的理由。3.3 多目标 Kalman 训练与评估单目标跑通后再跑多目标版本python train_multi_object_kalman_predict.py --config settings.yaml多目标脚本和前者的区别在于它会一次性处理所有车辆并考虑车辆之间的交互关系。loadMultiObjectNGSIM.py会把每辆车的轨迹按frame_id对齐成三维张量形状为[车辆数, 历史帧数, 特征维度]。注意这里有一个常见的维度陷阱不同车辆的轨迹长度可能不同脚本会用np.nan填充缺失帧并在计算损失时用掩码跳过nan。multi_object_kalman.py实现了多目标 Kalman 预测类它对每一辆车独立滤波但通过一个共享的dt和状态转移矩阵来统一时间轴。你可以通过--batch_size控制每次处理的车辆数。如果显存不足减小batch_size比减小future_len更安全因为后者会改变预测评估的标准。评估结果会保存到results/multi_object_stats.json打开后能看到每项指标的均值。第一次跑完我得到 RMSE 在 1.8 米左右这个数字比很多论文里的 1.5 米高一点原因是他们没有把换道情况单独剔除。如果你的目标是对齐现有论文建议把车辆按“是否发生换道”分成两组在settings.yaml里设置filter_by_lane_change: true再跑一遍。3.4 结果统计与可视化stats_multi_object_results.py会汇总多次实验的指标并计算均值和方差。运行时需要传入结果目录python stats_multi_object_results.py --result_dir results/脚本会扫描目录下所有*.npz文件逐项计算 RMSE、FDE、NLL、MR 并生成汇总表格。我一般会在这个阶段检查一下不同车辆类型的差异比如小汽车和卡车在 FDE 上的差距是否符合预期。如果发现卡车的误差普遍大于小汽车不要急着归因于模型问题先确认 NGSIM 数据里卡车的轨迹帧率是否正常——原始数据中卡车经常因为遮挡而中断导致预测起始点不稳定。可视化方面plot_interactive.py是交互式工具可以用滑块选择预测时刻显示该时刻所有车辆的预测位置和真实位置。plot_bokeh.py和plot_cv2.py提供两种渲染方式Bokeh 适合导出交互式 HTMLOpenCV 适合处理实时视频流。调试时推荐用plot_cv2.py因为它能在窗口里快速刷新你还能按暂停键逐帧检查异常的预测跳变。4. 常见问题与避坑四个容易翻车的细节4.1 现象预测结果在换道场景下误差飙升用等速模型预测一辆正在换道的车前 1 秒还算接近真实轨迹但从换道开始的那一刻误差就会拉大最终 FDE 常常超过 3 米。原因在于换道过程中车辆的横向运动主要由方向盘转角引起等速模型的“速度恒定、方向恒定”假设完全失效。解决方式有两种一是用单车模型替代等速模型因为单车模型能在一定程度上适应转向运动把前几帧的轨迹曲率纳入状态估计二是在评估指标里单独统计换道场景而不是把它和直线行驶混在一起。我在复现时采用了后一种方式在settings.yaml的eval段增加了一组lane_change_only标志这样论文里既能报告总体指标又能展示模型不同场景下的能力边界。4.2 现象NLL 数值和论文对不上明明用同一个数据集、同一个模型你算出的 NLL 可能是论文里的两倍。最常见的原因是协方差矩阵的处理方式不同。部分论文把预测的每个时间步当作独立高斯分布NLL 是各步对数概率的累加但有些代码实现会除以预测步数得到“平均 NLL”。此外协方差的初始化值也可能造成差异。这个包里的kalman_basis.py默认协方差初始值为 0.1如果你的任务要求协方差反映真实的不确定性需要从数据中统计每个位置的方差然后赋值给协方差矩阵。还有一个隐蔽细节NLL 计算时如果预测值和真实值距离超过某个阈值有些实现会把该样本的似然设为零这会导致 NLL 变成无穷大。要避免这个问题检查代码里是否有clip操作如果没有就在计算前手动把极小概率值钳位到1e-6。4.3 现象多目标脚本报维度错误运行train_multi_object_kalman_predict.py时报错信息是ValueError: operands could not be broadcast together with shapes (60, 2) (59, 2)。这是因为多目标数据加载时某辆车的轨迹比其他车短一帧导致在拼接时出现了维度不匹配。根本原因在于原始数据中存在某些车辆在最后一帧丢失记录的情况。解决方法是使用loadMultiObjectNGSIM.py里的align_timestamps函数把所有轨迹重采样到统一的帧长度。如果不想改代码可以在预处理阶段就过滤掉长度小于history_len future_len的车辆。我自己的做法是在settings.yaml里设置min_track_length: 60然后重新运行预处理脚本之后再跑多目标训练就不会报维度错了。4.4 现象Argoverse 与 NGSIM 数据加载器输出不一致这个包同时支持 NGSIM 和 Argoverse两者加载后的数据格式存在差异NGSIM 的坐标是二维局部坐标Argoverse 的坐标是经纬度而且单位不同。如果你在 NGSIM 上调试好的代码直接换到 Argoverse 上运行预测结果会完全偏离。原因就是没有做坐标统一转换。loadFusion.py里提供了一个to_local_coords函数可以把 Argoverse 的经纬度转换到以自车为中心的局部坐标系。使用时需要指定地图的中心参考点常见做法是以每条轨迹的初始位置作为原点。在训练前一定要用plot_interactive.py可视化几组数据确认坐标方向一致否则后面的指标全都没有意义。5. 扩展用 RNN 学习模型替换恒速假设5.1 LSTM/GRU 与 Kalman 组合的代码结构资源包中的NNpredictors目录提供了LSTM_kalman.py和GRU_kalman.py两个文件。它们并不是纯粹的 LSTM 网络而是把 LSTM 的输出和 Kalman 滤波结果做了融合Kalman 提供线性运动学基准LSTM 负责捕捉非线性残差。这种结构在信号处理里叫“残差学习”比直接让 LSTM 输出坐标要稳定得多尤其是当训练数据不充足时。import torch.nn as nn class LSTMKalman(nn.Module): def __init__(self, input_dim4, hidden_dim64, output_dim2, dropout0.2): super().__init__() self.lstm nn.LSTM(input_dim, hidden_dim, num_layers2, batch_firstTrue) self.fc nn.Linear(hidden_dim input_dim, output_dim) def forward(self, history, kalman_prior): # history: [B, T, 4] 包含 x, y, vx, vy lstm_out, _ self.lstm(history) # 取最后一个时间步的隐状态 last_hidden lstm_out[:, -1, :] # 拼接 Kalman 先验位置 fused torch.cat([last_hidden, kalman_prior], dim-1) return self.fc(fused)注意这里kalman_prior的形状是[B, 2]它来自kalman_basis.py在最后一帧的预测值。LSTM 的作用不是替代 Kalman而是学习 Kalman 预测误差的修正量。训练时损失函数用的是multi_object_kalman.py里定义的GaussianNLLLoss它同时输出预测坐标和协方差避免了分开训练不确定性模块的麻烦。5.2 训练命令与超参数调优RNN 模型的训练入口在train_multi_object_kalman_predict.py里通过参数--model切换python train_multi_object_kalman_predict.py --config settings.yaml --model lstm_kalman --epochs 50 --batch_size 32 --lr 1e-3训练时最重要的超参数不是学习率而是history_len。LSTM 对历史窗口长度很敏感窗口太短比如只有 10 帧会丢失换道意图窗口太长比如 40 帧会增加 LSTM 的记忆负担尤其是在车辆少、轨迹短的数据集上还容易过拟合。我试过 NGSIM 上history_len20是最均衡的future_len30保持和基线一致。另一个关键是hidden_dim64 和 128 的性能差距不大但 128 会让训练时间翻倍。如果你用GRU_kalman.py因为 GRU 参数比 LSTM 少hidden_dim可以提高到 96 而不会显著变慢。训练过程中建议每 5 个 epoch 打印一次验证集 RMSE。如果发现训练 RMSE 下降但验证 RMSE 上升说明过拟合了先降低dropout到 0.1不要直接减小模型尺寸。我在调参时发现dropout0.2在这个残差结构下表现最好太高会把 Kalman 提供的强先验也丢掉。5.3 不确定性估计与可解释性的取舍RNN 扩展带来的最大价值不是精确的坐标预测而是不确定性估计。LSTM_kalman.py的最后一层直接输出一个二维位置和四个协方差元素对角矩阵的方差因此模型可以告诉你在当前场景下哪些预测是可信的哪些是模糊的。实际效果是直线行驶场景的方差很小换道场景的方差自动增大。这一特性让模型的 NLL 指标比等速模型低了不少但代价是模型的可解释性变差了——你很难从 LSTM 的隐状态中读出“这辆车打算向左并线”的明确语义。如果你做了业务系统需要向客户解释预测依据建议保留 Kalman 基线的输出逻辑在 RNN 预测结果偏离时系统能退回到运动学基线并提示 “当前场景超出学习模型适用范围”。这比直接部署一个黑盒 LSTM 要稳妥得多。我在自己的项目里就是同时输出两条预测线让调度系统根据方差阈值决定采纳哪一条。6. 进阶验证你的基线是否可信先跑交互式可视化这是最快发现模型在哪些场景下失效的方法。运行plot_interactive.py时会加载测试集所有车辆的轨迹然后你按时间滑块逐帧播放预测结果和真实位置。我习惯重点观察两个时刻换道开始后的 0.5 秒以及目标车被前车遮挡后的重新出现时刻。如果预测轨迹在遮挡期间没有保持合理的运动趋势说明模型的运动学约束不够强这时要在kalman_basis.py里增大process_noise让模型更依赖前序状态而不是测量值。验证基线可信的第二件事是检查协方差矩阵是否正确。你可以从结果文件中抽取 1000 个样本计算预测残差的经验协方差和模型输出的协方差做对比。如果经验协方差明显大于模型输出的说明模型过度自信后续 NLL 指标会虚高。这个包里的save_results.py会保存每个样本的协方差矩阵你只需要加载后做一次统计对比。我跑过 NGSIM 上的实验发现等速模型的经验协方差和模型输出差 30% 左右而 LSTM Kalman 模型只差 5%这也解释了为什么 RNN 扩展的 NLL 更好。最后一个技巧是学会利用settings.yaml做变量控制。我在对比实验时会针对每个模型生成一个独立的配置副本比如settings_cv.yaml、settings_bicycle.yaml、settings_lstm.yaml。在每个副本里只改model和seed其他参数保持完全一致。这样做出来的指标对比才有意义。有一次我为了验证 10Hz 和 5Hz 重采样对结果的影响只修改了dt和对应的history_len就发现低采样频率下所有模型的表现都会变差但等速模型下降的幅度比 LSTM 小这说明采样频率也是一个不可忽视的基线上界因素。从那以后我每次跑新数据集都会强制走一遍这个流程先加载数据 → 可视化检查坐标方向 → 跑一步等速基线 → 对比协方差一致性 → 再考虑上复杂模型。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进