简介:面向计算机相关专业学生与从业者的采用长短期记忆网络的高速公路车辆轨迹预测项目,基于PyTorch框架实现,经过严格调试可直接运行。资源完整覆盖数据预处理、模型训练、测试评估与可视化流程,既可作为期末课程设计与课程大作业,也能支撑毕业设计中的轨迹预测任务。压缩包共15个文件,包含9个Python脚本、5张流程示意图及1份教程说明,整体仅311KB,轻量易用。脚本涵盖MTF-LSTM核心算法及其多种测试变体,配套NGSIM数据集处理模块,教程文本详细说明了数据划分与模型搭建步骤,图示则直观展示了各阶段运行效果;资源包内目录结构清晰,算法模块与数据模块分离,便于二次开发与对比实验。已有5214人学习下载,适合需要快速上手深度学习轨迹预测项目的学生与开发者参考。
1. LSTM高速公路车辆轨迹预测:拿过来就能跑的PyTorch源码项目
做自动驾驶方向课程设计的人,多少都会碰到轨迹预测这道坎。第一次接触的人以为它很高深,实际落地之后你会发现,它就是一个典型的时间序列预测问题:把车辆过去几秒的位置、速度喂给模型,让模型把未来几秒的轨迹猜出来。LSTM在这个场景里几乎是最稳的选择,不用像Transformer那样堆大量数据,也不像卡尔曼滤波那样对运动模型有强假设。这套资源就是我把整个流程打包好的一份完整版本:PyTorch写的LSTM轨迹预测源码、完整的训练与推理脚本、整理过的高速路车辆轨迹数据集,训练、验证、测试环节都能直接跑通。拿来交期末大作业、做毕业设计,或者当课程设计交付物,都够用。代码结构不复杂,一条数据管线跑完,就能看到Loss下降曲线和最终的预测轨迹图。
2. 轨迹数据预处理:从原始CSV到LSTM输入序列
动手写模型之前,先把数据这个问题掰扯清楚。轨迹预测项目最后翻车,一大半原因不是模型不行,而是数据预处理出了岔子。这里的预处理不只是读CSV、做归一化这么简单,关键在怎么把一条条连续轨迹切成长度一致的样本,以及切分时如何防止信息泄漏。顺序搞错,后面所有环节都会跟着错。
2.1 数据集字段与读取检查
这份资源自带的数据集是典型的表格结构,每一行记录某一帧时刻某辆车的状态。高速公路轨迹数据的常见字段如下:
| 字段名 | 含义 | 示例值 |
|---|---|---|
| vehicle_id | 车辆唯一编号 | 3 |
| frame_id | 帧号,按时间递增 | 120 |
| x | 横向位置 | 15.2 |
| y | 纵向位置 | 312.7 |
| v | 瞬时速度(m/s) | 23.6 |
| lane_id | 所在车道编号 | 2 |
字段名称可能略有不同,但核心信息就这些。CSV里的行不是按车按时间排好的,读取之后第一件事是排序和分组,否则后面滑窗切出来的根本就不是连续轨迹。
import pandas as pd df = pd.read_csv("data/tracks.csv") df = df.sort_values(["vehicle_id", "frame_id"]) print(df.head(10)) print("车辆总数:", df["vehicle_id"].nunique()) print("帧范围:", df["frame_id"].min(), "-", df["frame_id"].max())这段代码做了两件事:按车辆ID和帧号排序,保证后续按组遍历时轨迹是时间连续的;然后输出车辆总数和帧范围,确认数据规模。跑完这一步,你应该对数据集有个直观概念:多少辆车、多少帧、采样间隔是否均匀。
注意帧间隔。很多高速场景数据集是0.1秒一帧,也就是10Hz采样,相邻帧差1代表0.1秒;如果frame_id跨度更大,要先换算成实际时间。这个间隔决定后面obs_len和pred_len的物理含义,也会直接影响训练效果,先确认清楚再往下走。
2.2 滑窗采样与DataLoader构造
LSTM的输入是定长序列,一条轨迹几百帧不能整段塞进模型。正确做法是滑动窗口切块:取前obs_len帧作为观测输入,紧接着的pred_len帧作为预测目标,窗口每次向后挪一帧。这样一条长轨迹能切出大量短样本,相当于把数据集放大了。
import numpy as np def sliding_window(df, obs_len=8, pred_len=6): X, Y = [], [] for veh_id, group in df.groupby("vehicle_id"): group = group.sort_values("frame_id") coords = group[["x", "y"]].values for i in range(len(coords) - obs_len - pred_len + 1): X.append(coords[i:i + obs_len]) Y.append(coords[i + obs_len:i + obs_len + pred_len]) return np.asarray(X), np.asarray(Y)函数返回两个数组:X的形状是(N, obs_len, 2),Y的形状是(N, pred_len, 2),N是滑窗切出来的样本总数。代码重点在range那一行,i从0走到len(coords) - obs_len - pred_len,保证每个窗口有前8帧和后6帧,不多不少。一条200帧的轨迹大约切出186个样本,数据量直接被放大一个数量级。
obs_len和pred_len是后续最常调的两个参数。obs_len=8代表观测8个时间步,如果帧间隔0.1秒就是0.8秒历史;pred_len=6则是预测0.6秒。课程设计题目如果要求预测未来1秒,把pred_len改成10。obs_len没必要太长,LSTM在短序列上的表现往往比长序列更稳,训练也更快。
切出来的样本要通过DataLoader组织成batch。注意shuffle的位置,训练集要shuffle,验证和测试不要。
from torch.utils.data import TensorDataset, DataLoader import torch X_tensor = torch.FloatTensor(X) Y_tensor = torch.FloatTensor(Y) dataset = TensorDataset(X_tensor, Y_tensor) loader = DataLoader(dataset, batch_size=64, shuffle=True)shuffle=True让每个epoch的batch组成不同,帮助模型收敛。batch_size=64在轨迹预测这种小样本任务里比较稳妥,样本少的时候batch设太大,一个epoch的梯度更新次数太少,模型容易欠拟合。
2.3 归一化与集合切分:时序任务的信息泄漏红线
轨迹坐标是绝对位置,数值范围可能到几百米,直接喂给LSTM会让梯度计算吃力,所以必须归一化。但时序任务和普通分类有一个本质区别:训练集、验证集、测试集必须按车辆ID切,绝不能随机按行打乱再切。同一辆车的前后帧如果被分到两个集合,测试时的"未来"其实已经被模型在训练时见过,指标会虚高到离谱。
vehicle_ids = df["vehicle_id"].unique() train_ids = vehicle_ids[: int(len(vehicle_ids) * 0.7)] val_ids = vehicle_ids[int(len(vehicle_ids) * 0.7):int(len(vehicle_ids) * 0.85)] test_ids = vehicle_ids[int(len(vehicle_ids) * 0.85):] from sklearn.preprocessing import MinMaxScaler scaler_x = MinMaxScaler() scaler_y = MinMaxScaler() train_df = df[df["vehicle_id"].isin(train_ids)] scaler_x.fit(train_df[["x"]]) scaler_y.fit(train_df[["y"]])这段代码把车辆ID顺序切出70%训练、15%验证、15%测试,然后只在训练集上fit归一化器。scaler的fit只能看到训练集,验证集和测试集的极值不参与计算,否则等于让模型在训练阶段就"看到"了未来数据的分布。
切分比例不是死的,数据量大可以放宽到80/10/10。但70%是底线,再低模型学不到足够多的运动模式。另外注意切分前把vehicle_ids随机打乱一次,避免数据集本身按车道或时间段排序导致三个集合分布不均衡。
注意:如果你后续换用NGSIM这类公开数据,先确认坐标单位。NGSIM原始坐标是英尺,要乘0.3048换算成米再归一化。单位不一致时,后续ADE/FDE指标会成倍偏移,和论文结果完全没法比,这是轨迹预测项目里最隐蔽的单位坑。
3. LSTM轨迹预测模型:网络结构、训练与推理配置
数据准备到位后,核心就是模型部分。这套资源用的是两层LSTM加一个全连接输出层,输入历史8帧坐标,输出未来6帧坐标。结构不算复杂,但作为课程设计或期末作业,已经能拿到很扎实的指标,而且代码量小、答辩时容易讲清楚每个模块的作用。
3.1 模型定义与维度设计
LSTM神经网络处理轨迹预测的标准做法:观测序列走LSTM编码,取最后一个时间步的隐藏状态作为整段历史轨迹的浓缩特征,再接全连接层一次性输出未来所有坐标点。
import torch import torch.nn as nn class TrajectoryLSTM(nn.Module): def __init__(self, input_dim=2, hidden_dim=128, num_layers=2, pred_len=6): super().__init__() self.pred_len = pred_len self.lstm = nn.LSTM( input_dim, hidden_dim, num_layers, batch_first=True, dropout=0.2 ) self.fc = nn.Linear(hidden_dim, pred_len * 2) def forward(self, x): # x: (batch, obs_len, 2) out, (h_n, c_n) = self.lstm(x) last_hidden = h_n[-1] # last_hidden: (batch, hidden_dim) pred = self.fc(last_hidden) # pred: (batch, pred_len * 2) pred = pred.view(-1, self.pred_len, 2) return predforward里的逻辑可以拆成三步看。第一步,把观测序列x传入LSTM,PyTorch返回的out是每个时间步的隐藏状态序列,形状(batch, obs_len, hidden_dim),h_n是最后一层在最后一个时间步的隐藏状态,形状(num_layers, batch, hidden_dim)。第二步,取h_n[-1]也就是最后一层LSTM的最终输出,这一步是整个模型的关键假设:LSTM的隐藏状态已经足够浓缩车辆在观测窗口内的运动模式。第三步,全连接层把hidden_dim维向量映射成pred_len*2个值,对应未来每个时间步的x和y坐标,view再拆成(batch, pred_len, 2)。
input_dim=2表示输入只有x、y坐标。如果数据集里有速度、加速度字段,把input_dim改成4,喂入的每个时间步向量拼上对应字段,模型通常会收敛更快,预测高速变道这种工况时优势更明显。hidden_dim=128适合几千条样本的中小数据集,样本量上万可以加到256。num_layers=2是经过验证的折中,层数再多反而容易在小数据集上过拟合。
3.2 损失函数与优化器配置
轨迹预测最常用的损失函数是MSE,也就是所有预测时间步上坐标差的平方均值。MSE对误差大的点更敏感,模型会优先纠正偏差最大的那一两个时间步。
criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)MSE有个缺点:对离群点过于敏感。如果数据集里存在坐标跳变的异常轨迹,训练loss会被少数几个离群点主导。遇到这种数据,把损失函数换成SmoothL1Loss更稳:
criterion = nn.SmoothL1Loss(beta=1.0)beta=1.0表示误差小于1时使用平方误差,大于1时退化为线性误差,对离群点更温和。切换只需要改一行,验证集指标往往比MSE更平滑。优化器选Adam,lr=1e-3是稳妥起点。训练集样本只有几千条时,这个学习率跑30到50个epoch足够收敛。如果loss震荡不降,先把lr降到5e-4做对比实验。
3.3 训练循环与模型保存策略
训练环境上,PyTorch的GPU版和CPU版都能跑这份代码。数据集不大,CPU训练也就几分钟到十几分钟,GPU只是更快,不影响结果。关键在训练循环本身有几个细节值得注意。
def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, total_num = 0.0, 0 for x, y in loader: x, y = x.to(device), y.to(device) optimizer.zero_grad() pred = model(x) loss = criterion(pred, y) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() total_loss += loss.item() * x.size(0) total_num += x.size(0) return total_loss / total_num每个batch先清零梯度,前向传播算预测值,MSE算loss,反向传播后做一次梯度裁剪再更新参数。total_loss累计时乘上batch大小,最后除以总样本数,得到按样本加权的平均loss。梯度裁剪这行很多人会漏,但对LSTM这类序列模型来说,反向传播路径长,梯度范数容易突然暴增,表现为loss曲线跳崖式升高。clip_grad_norm_把梯度范数限制在1.0以内,就是防这一手。loss稳定时去掉也行,留着一版对结果影响很小。
主循环与模型保存:
best_val = float("inf") for epoch in range(50): train_loss = train_one_epoch(model, train_loader, optimizer, criterion, device) val_loss = evaluate(model, val_loader, criterion, device) print(f"epoch={epoch+1:03d} train_loss={train_loss:.5f} val_loss={val_loss:.5f}") if val_loss < best_val: best_val = val_loss torch.save(model.state_dict(), "best_model.pth")保存策略是看验证集loss,不是训练集loss。验证loss最低的那一轮参数通常泛化性最好。训练完再用测试集跑一次评估,拿测试集结果作为最终指标,不要拿验证集结果顶替。加载测试时有个高频错误容易犯:
model = TrajectoryLSTM(input_dim=2, hidden_dim=128, num_layers=2, pred_len=6) model.load_state_dict(torch.load("best_model.pth")) model.eval()eval()会把模型切到推理模式,dropout和batchnorm的行为随之变化。不写这一行,推理结果每跑一遍都可能不一样,这是很多人复现结果对不上的原因之一。
3.4 推理与反归一化
训练完成后的推理流程,数据路径必须和训练时完全一致,包括归一化、滑窗、维度排列。
with torch.no_grad(): obs_tensor = torch.FloatTensor(obs[:8]).unsqueeze(0) # obs_tensor: (1, 8, 2) pred_norm = model(obs_tensor).squeeze(0).numpy() # pred_norm: (6, 2) pred_real = np.zeros_like(pred_norm) pred_real[:, 0] = scaler_x.inverse_transform(pred_norm[:, 0].reshape(-1, 1)).ravel() pred_real[:, 1] = scaler_y.inverse_transform(pred_norm[:, 1].reshape(-1, 1)).ravel()模型输出的是归一化坐标,画图、算误差指标之前必须用训练时那套scaler反变换回真实坐标。这里最坑的地方是:scaler必须是训练时fit的同一个对象或持久化保存的对象,不能在推理脚本里重新fit一次,否则统计量不同,反归一化结果整体偏移。
4. 避坑手册:轨迹预测实战最容易翻车的五个场景
这一章写的是我在做轨迹预测项目时真实踩过的坑,每一个都让结果出过问题。按照现象、原因、解决的思路整理,你在复现时如果遇到类似情况,直接对照排查。
4.1 顺序没排序,滑窗切出"假连续"序列
现象:训练loss能正常下降,验证集指标也还能看,但可视化预测轨迹时,画出来的点序列在空间里乱跳,完全不像一辆车的行驶路径。
原因:CSV原始数据不按时间排序,滑窗前没有对每个车辆ID单独排序。groupby取出来的坐标序列是乱序的,滑窗切出来的"连续帧"实际混杂了不同时刻甚至不同车道的点。
解决:排序必须写在滑窗函数内部,每次调用都执行,不给遗漏机会。我在2.2节代码里就在groupby之后先sort_values("frame_id")再取坐标。这条看似简单,实际是轨迹预测项目里最隐蔽的翻车点。
4.2 数据泄漏:测试集的未来被训练集提前看到
现象:训练集和验证集指标都不错,但进一步分析发现模型预测轨迹基本是复读最后观测位置,没有体现出车辆的速度趋势和变道意图。指标虚高,模型实际没学到东西。
原因:切分训练/测试时用了随机按行打乱,同一辆车的轨迹前后帧被分到两个集合。模型在训练时已经见过测试样本的历史和未来,测试指标自然好看,但模型并没有真正的泛化能力。
解决:切分必须以vehicle_id为粒度,同一辆车的所有帧只能归属一个集合。用70/15/15比例切好之后,再开始归一化和滑窗。这条是时序任务的红线,不守住,后面所有评估都没有意义。
4.3 反归一化不一致,预测结果整体漂移
现象:模型输出的坐标范围在0到1之间,inverse_transform之后某个轴还是0到1,另一个轴变成了几百,预测轨迹和真实轨迹错位明显。
原因:反归一化时把x和y的scaler搞反了,或者加载了一个重新fit过的scaler,或者只对部分坐标做了反变换。训练和推理环节的统计量不一致,结果必然错位。
解决:把scaler和模型一起持久化保存。用python的pickle模块在训练结束时把scaler_x、scaler_y存成一个pkl文件,推理脚本里load这个文件,绝不重新fit。从那以后我每次都强制走这条流程,再没出过反归一化错位。
4.4 异常轨迹没过滤,loss出现NaN
现象:训练到某个epoch后loss突然变成nan,之后再也回不来,重启训练也没用。
原因:数据集中存在坐标跳变或重复帧的异常轨迹。归一化时某些坐标接近0方差,产生极大数值,LSTM的反向传播梯度在数值上溢出,权重被破坏。
解决:滑窗前加一道过滤逻辑。轨迹长度不足obs_len + pred_len的车辆直接丢弃;再用diff检测帧间坐标跳变,超过阈值就剔除整条轨迹。
for veh_id, group in df.groupby("vehicle_id"): if len(group) < obs_len + pred_len: df = df[df["vehicle_id"] != veh_id] continue diff_x = group["x"].diff().abs().max() diff_y = group["y"].diff().abs().max() if diff_x > 5.0 or diff_y > 5.0: df = df[df["vehicle_id"] != veh_id]阈值5.0按坐标单位调整,0.1秒帧间隔下车辆帧间位移超过5米基本就是异常点。这步过滤放预处理最前面,能省掉后面大量排查时间。
4.5 ADE/FDE指标算错,结果对比全乱套
现象:模型跑通后输出的ADE/FDE数值和论文里同量级的数字差好几倍,或者FDE的曲线变化和训练loss趋势对不上。
原因:坐标单位不一致,或者FDE的定义理解有偏差。FDE只取预测终点那一个点与真实终点的距离,被当成所有预测点的平均误差是常见错误。
解决:先确认坐标单位,统一成米。再按标准定义计算:ADE是预测轨迹所有点的平均位移误差,FDE是最后一帧的位移误差。具体实现放在5.1节,两个指标共用一套函数,避免重复代码造成偏差。
5. 评估与调优:从能跑通到预测得更准
模型能跑只是第一步。课程设计和毕业设计的评分差距,往往体现在指标是否规范、有没有对比实验、能不能解释清调参过程。这一章把评估和调优串起来讲。
5.1 ADE与FDE:轨迹预测的两个核心指标
轨迹预测领域最常用的两个指标是ADE和FDE。ADE是Average Displacement Error,预测轨迹上所有时间步与真实轨迹的平均欧氏距离;FDE是Final Displacement Error,只取最后一个预测点与真实终点的欧氏距离。两者配合使用,ADE衡量整体贴合度,FDE衡量终点准确性。
def compute_ade_fde(y_true, y_pred): # y_true, y_pred: (batch, pred_len, 2) dist = torch.sqrt(((y_true - y_pred) ** 2).sum(dim=-1)) ade = dist.mean().item() fde = dist[:, -1].mean().item() return ade, fdedist的形状是(batch, pred_len),每个样本每个预测时刻的位移误差。ade对dist全部元素求平均,fde只取最后一列再求平均。注意输入的单位必须已经是米,且是反归一化后的真实坐标。这套计算逻辑在所有实验里保持一致,对比才有意义。
测试集评估的完整流程:加载saved模型,遍历测试DataLoader,每个batch算pred,和y_true一起传进compute_ade_fde,最后汇总所有batch的均值和标准差。标准差也值得报出来,答辩时如果老师问"为什么有的轨迹预测好有的差",标准差能帮你解释误差分布。
5.2 超参调优与消融对比
调参要有方向,不能靠玄学。下面这张表是我在类似规模数据集上比较常用的参数起点和调整方向:
| 参数 | 初始值 | 调优方向 | 说明 |
|---|---|---|---|
| obs_len | 8 | 6/10/12 | 观测窗口长度,过短缺上下文,过长引入噪声 |
| pred_len | 6 | 5/8/12 | 由题目要求的预测时长决定 |
| hidden_dim | 128 | 64/256 | 样本量小时用小维度防过拟合 |
| num_layers | 2 | 1/3 | 1层快但表达能力弱,3层易过拟合 |
| lr | 1e-3 | 5e-4/1e-4 | loss震荡时优先降lr |
| batch_size | 64 | 32/128 | 小数据集用32更稳 |
| dropout | 0.2 | 0.1/0.3 | 过拟合时加大,欠拟合时减小 |
消融实验是答辩加分项。课程设计不要求做完整消融,但至少做一个baseline对比:恒速模型加上LSTM。恒速模型用最后两个观测点算速度,匀速外推未来位置,完全不依赖神经网络。对比它和LSTM的ADE/FDE,能直观说明LSTM学到了什么。
def constant_velocity_baseline(obs_coords, pred_len): # obs_coords: (obs_len, 2) v = obs_coords[-1] - obs_coords[-2] start = obs_coords[-1] future = [start + v * (i + 1) for i in range(pred_len)] return np.asarray(future)把这条基线跑在同一个测试集上,通常恒速模型的FDE不会太差,但ADE会被LSTM明显拉开,尤其在有变道和加减速的片段里。这个对比结果放进答辩PPT,比任何文字解释都有力。
5.3 把训练过程记录下来:科学调参的基建
调参最忌讳凭记忆。每个实验跑完,记录一组参数和两个指标,坚持几轮之后你才能看出规律。我一般用CSV记录,结构简单,Python直接写:
import csv with open("experiments.csv", "a", newline="") as f: writer = csv.writer(f) writer.writerow([epoch, hidden_dim, num_layers, lr, batch_size, ade, fde])每次实验追加一行,累积20条之后扫一眼表格,哪些参数组合在ADE上有优势一目了然。这也避免了同一个参数组合重复跑的问题。答辩时如果被问到"怎么确定的最终参数",直接把CSV记录展示出来,说是做了这些对比实验后选的,比空口说"我试了试觉得这个好"要可信得多。
6. 轨迹可视化与答辩演示:让结果自己说话
模型调完,最后一步是把效果可视化。一张预测轨迹对比图,比任何指标数字都直观。用matplotlib画三条线:历史轨迹、真实未来轨迹、LSTM预测未来轨迹。
import matplotlib.pyplot as plt obs = np.array(history_coords) # (8, 2) true = np.array(true_coords) # (6, 2) pred = np.array(pred_coords) # (6, 2) plt.figure(figsize=(8, 6)) plt.plot(obs[:, 0], obs[:, 1], "b-o", label="历史轨迹") plt.plot(true[:, 0], true[:, 1], "g--s", label="真实未来轨迹") plt.plot(pred[:, 0], pred[:, 1], "r-^", label="LSTM预测轨迹") plt.legend() plt.xlabel("x / m") plt.ylabel("y / m") plt.title("LSTM轨迹预测效果") plt.savefig("prediction_result.png", dpi=150)画图前确认坐标是反归一化后的真实坐标,否则图里数值范围对不上。随机抽3条测试集轨迹,一条直线行驶、一条变道、一条加减速,有代表性,答辩时讲起来更有说服力。答辩PPT我建议放三张核心图:第一张是训练loss曲线,证明收敛过程干净;第二张是3条轨迹对比图,展示预测效果;第三张是ADE/FDE对比表,放恒速基线和LSTM的数据。三张图配一个简短的文字说明,项目的完整度和可信度就立起来了。
这套源码和数据集我整理在同一个压缩包里,解压后按第2章的预处理顺序走一遍,再到第3章跑训练,最后的可视化和指标输出都是现成脚本。我自己做轨迹预测项目的习惯是每次跑完训练,强制把模型权重、scaler、实验结果图和CSV记录统一归档,命名带日期。答辩前再完整跑一遍测试流程,确认环境没变、结果可复现。这些血泪经验总结成的流程,建议你也直接沿用。希望帮到你。
本文还有配套的精品资源,点击获取