☰
轨迹预测基线复现实战:等速、单车与Kalman模型
2026/10/9 12:22:35 网站建设 项目流程

简介:面向车辆轨迹预测研究,该代码包提供恒定速度与运动学单车模型的可复现基线实现,并已在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_dir='data/ngsim/', dataset='us101', # 或 'i80' time_span=15.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], axis=1))

NLL 的计算则要看你的模型是否输出不确定性。如果预测结果是一个多元高斯分布,那么 NLL 是负的对数概率密度;如果预测结果只是确定性坐标,那你需要手动为每个预测点加上一个协方差。这里的代码里,NLL计算中会把协方差矩阵限制在对角阵上,并且给对角线加了 1e-6 的 eps 防止除零。这和某些论文用全协方差矩阵的做法不同,对比时一定要仔细核对公式。

MR(Miss Rate)的定义是预测误差超过某个阈值(通常 2 米)的样本比例:

def miss_rate(pred, true, threshold=2.0): distance = np.linalg.norm(pred[:, -1] - true[:, -1], axis=1) 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 python=3.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.0,measurement_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 适合导出交互式 HTML,OpenCV 适合处理实时视频流。调试时推荐用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_dim=4, hidden_dim=64, output_dim=2, dropout=0.2): super().__init__() self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers=2, batch_first=True) 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_len=20是最均衡的,future_len=30保持和基线一致。另一个关键是hidden_dim,64 和 128 的性能差距不大,但 128 会让训练时间翻倍。如果你用GRU_kalman.py,因为 GRU 参数比 LSTM 少,hidden_dim可以提高到 96 而不会显著变慢。

训练过程中,建议每 5 个 epoch 打印一次验证集 RMSE。如果发现训练 RMSE 下降但验证 RMSE 上升,说明过拟合了,先降低dropout到 0.1,不要直接减小模型尺寸。我在调参时发现dropout=0.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 小,这说明采样频率也是一个不可忽视的基线上界因素。从那以后,我每次跑新数据集都会强制走一遍这个流程:先加载数据 → 可视化检查坐标方向 → 跑一步等速基线 → 对比协方差一致性 → 再考虑上复杂模型。希望帮到你。

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

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

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

立即咨询