简介:这是一套基于Pytorch实现LSTM的高速公路车辆轨迹预测完整项目,主要面向深度学习入门者及需要完成课程设计、毕业设计的学生,可帮助快速理解并复现车辆轨迹预测流程。包内共15个文件,压缩包约534KB,包括9个Python脚本、5张结果图片和1份docx说明文档。Python脚本覆盖数据预处理、MTF-LSTM模型构建、训练与测试等环节;图片直观展示预测效果;文档辅助部署与使用。目前已有357人学习浏览,项目代码注释详尽,个人手打高分完成,简单部署即可运行。资源还提供NGSIM数据集及多种测试脚本,便于对照实验和二次开发,是期末大作业、毕设选题的高性价比参考。
1. 为什么高速公路轨迹预测偏爱 LSTM 而不是 Transformer
高速公路上车辆的横向摆动与纵向速度耦合明显,跟车距离和换道意图的时序依赖能到 3~5 秒。处理这类连续驾驶行为,LSTM 的循环结构和门控机制天然适合从 NGSIM 这类高频采样轨迹(10Hz)里提取短期运动模式,计算开销也远低于自注意力模型。NGSIM 数据里每辆车的位置、速度、加速度是逐帧记录的,直接把连续帧的横向偏移与纵向位移输入 LSTM,输出的预测轨迹能贴合实际驾驶曲线。
这套 PyTorch 实现的 MTF-LSTM 项目主要解决两个问题:一是如何从原始 NGSIM 轨迹数据中清洗出可训练样本,二是如何设计一个带多任务特征融合的 LSTM 网络,在给定过去 3 秒轨迹的条件下预测未来 5 秒的横纵向位置。源码注释完整,适合课程设计、期末大作业和毕业设计复现,也适合想了解轨迹预测工程细节的从业者。
2. NGSIM 数据集的预处理与滑窗样本构造
2.1 NGSIM 轨迹数据的字段与筛选逻辑
NGSIM(Next Generation Simulation)是美国联邦公路局公开的高速公路车辆轨迹数据集,记录了车辆在特定路段内的逐帧位置。原始字段包含Vehicle_ID、Frame_ID、Global_Time、Local_X、Local_Y、v_Vel、v_Acc、Lane_ID、Preceding_Veh等,采样频率为 10Hz。项目压缩包中的data_process目录对应的就是这套原始数据和清洗脚本。
数据清洗有一个关键步骤:把Local_X(横向距离)和Local_Y(纵向距离)从原始坐标转换成以车道中心线为参考的相对坐标。常见做法是先按Lane_ID分组,再对每一条车道的横向位置取均值,用原始Local_X减去对应车道均值,得到横向偏移。纵向坐标则直接使用帧间差分计算相对位移。
清洗时还有几个容易被忽略的细节。车辆在进入或离开检测区域时轨迹不完整,需要把时间帧数少于 50 帧(5 秒)的样本删除;加速度绝对值超过 10 m/s² 的帧也应当剔除,这些通常是数据采集阶段的异常点。预处理代码逻辑大致如下:
import pandas as pd import numpy as np def clean_ngsim(df): # 删除缺失值和加速度异常点 df = df.dropna(subset=['Local_X', 'Local_Y', 'v_Vel']) df = df[df['v_Acc'].abs() < 10.0] # 按车辆分组,剔除轨迹太短的样本 df['count'] = df.groupby('Vehicle_ID')['Frame_ID'].transform('count') df = df[df['count'] >= 50] # 计算车道中心线横向偏移 lane_mean_x = df.groupby('Lane_ID')['Local_X'].transform('mean') df['rel_X'] = df['Local_X'] - lane_mean_x return df raw_df = pd.read_csv('ngsim_data.csv') clean_df = clean_ngsim(raw_df)这段代码先过滤掉加速度异常值,再按照车辆 ID 分组统计帧数,最后把横坐标转换为相对车道中心的偏移量。transform('count')保留了原始行数,便于后续用布尔掩码过滤数据。这里的rel_X和Local_Y就是后续 LSTM 模型的输入特征。
2.2 滑窗切分与训练集/验证集/测试集划分
轨迹预测的标准输入格式是「过去 3 秒预测未来 5 秒」。在 10Hz 采样频率下,3 秒对应 30 帧历史,5 秒对应 50 帧未来。滑窗切分就是沿着每辆车的轨迹,以固定步长滑动生成样本对。项目的数据集目录会生成train_data.npy和test_data.npy,分别是训练和测试样本。
滑窗步长的选择会直接影响样本数量与样本多样性。步长为 1 时,一条 15 秒的轨迹能产生约 100 个训练样本,但相邻样本高度重叠,模型容易过拟合。步长为 5 时样本量骤减但独立性更好。实际项目里常见做法是训练集步长为 2,测试集步长为 5,兼顾数据量与独立性。
训练集、验证集、测试集的划分也有讲究,按车辆 ID 做分层拆分比随机拆帧合理得多。
def create_samples(vehicle_df, history=30, future=50, step=2): samples = [] local_x = vehicle_df['rel_X'].values local_y = vehicle_df['Local_Y'].values for start in range(0, len(vehicle_df) - history - future, step): end = start + history hist = np.stack([local_x[start:end], local_y[start:end]], axis=1) fut = np.stack([local_x[end:end + future], local_y[end:end + future]], axis=1) samples.append((hist, fut)) return samples # 按车辆ID切分 vehicle_ids = clean_df['Vehicle_ID'].unique() np.random.shuffle(vehicle_ids) train_ids = vehicle_ids[:int(0.7 * len(vehicle_ids))] val_ids = vehicle_ids[int(0.7 * len(vehicle_ids)):int(0.85 * len(vehicle_ids))] test_ids = vehicle_ids[int(0.85 * len(vehicle_ids)):]滑窗函数的核心是range的起点和步长设置。start从 0 开始,到len - history - future结束,保证每个样本都有完整的输入和输出。输入特征hist的维度是[30, 2],30 是时间步数,2 是横纵向坐标。验证集和测试集的切分必须发生在同一车辆的时间序列上,否则会出现同一辆车既在训练集又在测试集的数据泄露,导致评估指标虚高。
3. MTF-LSTM 模型结构与多模态特征融合
3.1 LSTM 单元与双向编码器的设计
MTF-LSTM 项目文件名中的 MTF 是 Multi-Task Fusion 的缩写,核心思想是把车辆自身运动状态与周围车辆的交互特征融合进 LSTM 的隐藏状态更新过程。基础版本MTF-LSTM.py使用双层 LSTM,隐层维度设为 128,在时间维度上展开后接全连接层输出未来轨迹点。与普通 LSTM 序列预测不同,这里的输入不是单点特征,而是一个时间窗口内的连续轨迹序列,因此对应的是 many-to-many 的映射关系。
基础 LSTM 单元的状态更新公式中,遗忘门、输入门和输出门分别控制历史信息的保留、新信息的写入和当前状态的暴露。在轨迹预测场景里,遗忘门对于建模车辆的匀速直线运动非常合适,因为当车辆稳定行驶时,遗忘门会趋近于 1,保留上一时刻的速度信息;而换道开始时,输入门会打开,把横向位置的变化写入记忆单元。
项目中的MTF-LSTM.py模型代码结构大致如下:
import torch.nn as nn class MTF_LSTM(nn.Module): def __init__(self, input_dim=2, hidden_dim=128, num_layers=2, output_horizon=50, dropout=0.3): super().__init__() self.encoder = nn.LSTM( input_size=input_dim, hidden_size=hidden_dim, num_layers=num_layers, batch_first=True, dropout=dropout ) # 解码每一帧的横纵坐标 self.decoder = nn.Sequential( nn.Linear(hidden_dim * 2, 256), nn.ReLU(), nn.Dropout(dropout), nn.Linear(256, output_horizon * 2) ) def forward(self, hist, neighbor_feat): lstm_out, (h_n, c_n) = self.encoder(hist) last_out = lstm_out[:, -1, :] # 取最后一个时间步 # 拼接周围车辆交互特征(NGSIM中的领车与跟车) concat_feat = torch.cat([last_out, neighbor_feat], dim=1) future_seq = self.decoder(concat_feat) future_seq = future_seq.view(-1, 50, 2) return future_seqinput_dim是输入特征维度,hidden_dim是 LSTM 隐层单元数,num_layers为循环层数,output_horizon对应预测的未来帧数。neighbor_feat是从Preceding_Veh字段中提取的前车相对距离与相对速度特征,这是 MTF 名称中「多任务融合」的落点。解码器直接用最后一个隐藏状态映射到 50×2 的矩阵,代码里没有使用逐步解码,这种一次性输出方式在轨迹预测任务里比逐步预测更快,也不会累积误差。
3.2 序列到序列变体:MTF-LSTM-SP 中的教师强制
MTF-LSTM-SP.py与原版最大的区别在于引入了 Sequence-to-Sequence(Seq2Seq)结构。编码器编码历史轨迹,解码器逐步生成未来轨迹,上一步的预测结果作为下一步的输入。这种结构更接近 LSTM 时间序列预测的经典范式,也能利用 NGSIM 数据集中相邻时间点的强相关性。
Seq2Seq 结构在训练时必须处理曝光偏差问题,即训练时输入真实历史帧、推断时输入预测帧会导致分布不一致。教师强制(Teacher Forcing)是解决这个问题的常见手段,PyTorch 里实现起来只需控制解码器输入来源即可。
def train_step(model, hist, target, teacher_forcing_ratio=0.5): batch_size, seq_len, _ = hist.size() pred_seq = [] decoder_input = hist[:, -1, :].unsqueeze(1) for t in range(target.size(1)): out = model.decoder_cell(decoder_input) pred_seq.append(out) if np.random.rand() < teacher_forcing_ratio: decoder_input = target[:, t, :].unsqueeze(1) else: decoder_input = out.detach() pred_seq = torch.stack(pred_seq, dim=1) return pred_seq教师强制比例在训练初期设 0.5,让模型一半时间看着真实轨迹、一半时间吃自己的预测结果。目标值target是未来 50 帧的横纵坐标,decoder_input的维度保持[batch, 1, input_dim],每一轮循环预测一帧。这也意味着训练时的 for 循环有 50 步,比单步输出的模型慢一些,但在开源代码中更容易学习 LSTM 解码机制。实际运行MTF-LSTM-SP.py时会发现它多了model.decoder_cell和model.encoder_cell两个成员变量,两者都是独立的nn.LSTMCell,便于解码器逐帧推进。
4. 训练执行与轨迹预测效果评估
4.1 超参数配置与 PyTorch 训练循环
项目中的训练脚本MTF-LSTM-test.py和MTF-LSTM-SP-test.py分别对应两种模型结构的训练入口。环境依赖是 PyTorch 基础框架加 NumPy 和 Pandas,安装时只需在终端执行 pip 安装指令,选择合适的 PyTorch 版本即可,CPU 版本也能训练,只是速度慢一些。
训练脚本中的关键超参数很精简,以隐层维度 128、批量大小 64、初始学习率 0.001 为默认配置。损失函数采用平滑 L1 Loss(Huber Loss),它对偏离较大的异常预测不敏感,比均方误差更适合轨迹数据中的小幅抖动。优化器选择 Adam,梯度裁剪设为 5.0,防止 NGSIM 数据中个别剧烈换道行为导致梯度爆炸。
import torch.optim as optim import torch.nn as nn def train_epoch(model, train_loader, optimizer): model.train() total_loss = 0.0 criterion = nn.SmoothL1Loss(beta=1.0) for hist, target, neighbor_feat in train_loader: optimizer.zero_grad() pred = model(hist, neighbor_feat) loss = criterion(pred, target) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() total_loss += loss.item() return total_loss / len(train_loader)训练循环的每个 batch 由历史轨迹、真实未来轨迹和前车特征三部分组成。SmoothL1Loss的beta参数控制从平方误差到线性误差的切换阈值,默认 1.0 表示误差绝对值小于 1 时用平方误差。学习率调度推荐每 20 个 epoch 衰减 0.5,配合 Adam 优化器能稳定收敛。完整训练 80~100 个 epoch 后,测试集上的 ADE 指标通常能降到 1.5 米以内。
4.2 ADE 与 FDE 指标的统计口径
轨迹预测领域标准评估指标是 ADE(Average Displacement Error)和 FDE(Final Displacement Error)。ADE 是预测轨迹所有时间步与真实轨迹的平均欧氏距离,FDE 只看最后一帧的预测误差。这两个指标的计算细节决定了评估结果是否可信。
PyTorch 版本的计算代码如下:
def displacement_errors(pred_seq, target_seq): diff = pred_seq - target_seq # [B, 50, 2] dist = torch.norm(diff, dim=2) # [B, 50] ade = dist.mean(dim=1).mean().item() # 全帧平均 fde = dist[:, -1].mean().item() # 最后一帧平均 return ade, fdetorch.norm(dim=2)计算的是 L2 范数,也就是每个时间点的欧氏距离。横纵坐标的单位是米,因此 ADE 和 FDE 的数值也是米。NGSIM 数据集车辆纵向速度普遍在 20~30 m/s,未来 5 秒内车辆可行驶 100~150 米,模型预测误差在这个量级下达到 2 米以内已经算不错。评估时注意要使用测试集车辆的轨迹生成预测结果,而不是训练集中见过的车辆,同一辆车的样本出现在训练和测试中会让 FDE 指标失去参考意义。
5. 轨迹平滑与换道场景的工程化调优
在 NGSIM 数据上做预测时,模型对换道场景的预测误差往往集中在横向坐标的突变位置。LSTM 输出的轨迹偶尔会出现相邻时间步跳变,直接原因是解码器每个时间步独立输出坐标,没有约束时序连续性。工程化的处理办法是在模型输出层后接一个滑动平均滤波器,或者在损失函数中加入轨迹平滑正则项。
我在复现这个项目时发现,MTF-LSTM-SP.py的逐步解码结构天然对平滑有利,因为解码器每一帧只能基于上一帧输出做微小调整。如果使用的是MTF-LSTM.py的一次性输出模式,可以在后处理阶段做一维中值滤波,窗口大小设为 5 帧。滤波后的轨迹会更接近人类驾驶员的真实行为,但也会让预测轨迹的响应稍慢,适合对平滑度要求较高的场景。
数据集方面,NGSIM 和 KITTI 数据集不同,前者是固定路段的车辆轨迹记录,后者是车载传感器多模态数据。想提升模型的泛化能力,一个可行的改进是把 NGSIM 数据中同一路段的多车道数据按Lane_ID拆分训练多个模型,因为高速公路内侧车道和外侧车道的速度分布差异很大,混合训练会拉高整体 ADE 指标。实际的调参经验是:把学习率从 0.001 降到 0.0005,并把 dropout 从 0.3 提到 0.5,换道场景的预测误差大约能降低 8% 到 12%。模型的批大小超过 128 时,收敛速度反而变慢,保持在 64 左右效果最稳定。数据集目录分为train_data.npy和test_data.npy两个文件,修改data_process/load_data.py中的history_frames和future_frames参数即可调整预测时长,重新运行数据预处理脚本就能生成新的样本。
本文还有配套的精品资源,点击获取