news 2026/10/28 8:17:52

PyTorch LSTM高速公路车辆轨迹预测:源码、数据集与工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PyTorch LSTM高速公路车辆轨迹预测:源码、数据集与工程实践

简介:这份资源面向计算机相关专业正在做毕设、课程设计或期末大作业的学生,以及需要项目实战练习的开发者,提供一套基于PyTorch与LSTM的高速公路车辆轨迹预测完整方案,帮助解决时序建模与轨迹预测落地难的问题。压缩包共15个文件,约314KB,以9个Python源码文件为核心,涵盖模型定义、训练与测试脚本,另含5张png结果图与1个txt说明文档,便于对照理解实验流程与预测效果。资源围绕NGSIM高速公路数据集展开,包含数据处理模块与多组LSTM变体实现,可完整复现从数据预处理、模型搭建到轨迹预测评估的全过程。目前已有188人学习下载,适合希望快速掌握LSTM时序预测思路、积累深度学习项目经验的学习者参考使用。

1. 高速公路轨迹预测:为什么 LSTM 是绕不开的基线

高速公路上前车突然变道,你的 ACC 系统需要提前 1.5 秒知道它要往哪走——这类问题落到工程里,就是「基于历史轨迹预测未来 3~5 秒位置」。用 PyTorch 实现基于 LSTM 的高速公路车辆轨迹预测,配上源码、数据集和说明,本质是给你一套能跑通、能改、能换数据的基线工程。它解决的不是「预测得多准」,而是「从原始轨迹到可训练张量、再到能推理的模型」这条链路怎么搭。适合两类人:一是刚接触轨迹预测、想找一个能复现的 LSTM 起点;二是手里有 NGSIM、highD 这类高速数据集,想快速验证 LSTM 到底能到什么水平。热搜里 pytorch、LSTM、车辆轨迹预测、源码、数据集这几个词,恰好对应了这套东西的五个环节:环境、模型、任务、代码、数据。下面按这条链路拆开讲。

2. 轨迹预测任务拆解与 LSTM 选型理由

2.1 从「预测未来位置」到「序列到序列」的建模

车辆轨迹预测的输入通常是一段历史观测序列,输出是未来一段时间的坐标序列。设观测窗口为 T_obs,预测窗口为 T_pred,每条轨迹可以写成 (x, y) 坐标随时间变化的序列。高速场景下采样率常见 10Hz 或 25Hz,NGSIM 是 10Hz,highD 是 25Hz。如果按 10Hz 算,观测 3 秒就是 30 帧,预测 5 秒就是 50 帧。这个规模对 LSTM 来说刚好合适——太短了 LSTM 退化成全连接,太长了训练慢且容易过拟合。

建模方式有两种常见做法。第一种是直接回归未来坐标,输入历史 (x, y) 序列,输出未来 (x, y) 序列,损失用 MSE 或 Huber。第二种是预测未来位移增量 (Δx, Δy),再累加回绝对坐标。高速场景下车辆运动相对平滑,位移增量比绝对坐标更容易学,因为绝对坐标数值范围大且随路段变化,增量则集中在较小范围。我一般会先做增量预测,训练稳定后再看要不要切回绝对坐标。

LSTM 在这里的角色是捕捉时序依赖。相比 RNN,LSTM 的门控结构能缓解梯度消失,适合 30~50 帧的序列。相比 Transformer,LSTM 参数少、训练快、对小数据集更友好。高速轨迹预测的数据量通常不大,NGSIM 全量也就几百万条样本,highD 更少,Transformer 容易过拟合。所以 LSTM 不是「最先进」,而是「最稳的基线」。热搜里 lstm预测、lstm时间序列预测python 这些词,指向的正是这个基线定位。

2.2 数据格式与预处理:从 CSV 到张量

公开高速数据集常见格式是 CSV,每行一条记录,包含车辆 ID、帧号、全局时间、局部坐标、速度、加速度、车道等字段。NGSIM 的字段名是 Vehicle_ID、Frame_ID、Global_Time、Local_X、Local_Y、v_Vel、v_Acc、Lane_ID 等。highD 的字段略有不同,但核心是 ID、帧、x、y、速度。

预处理要做几件事。第一,按车辆 ID 分组,按帧号排序,保证时间连续。第二,过滤掉轨迹长度不足 T_obs + T_pred 的车辆。第三,处理缺失值和异常值,比如坐标跳变超过阈值就标记为无效。第四,归一化。坐标可以用路段长度归一化,速度用最大速度归一化,或者直接用 StandardScaler 按特征做零均值单位方差。归一化参数要保存,推理时用同一套。

下面是一个把 NGSIM 风格 CSV 转成滑动窗口样本的脚本。假设 CSV 有 Vehicle_ID、Frame_ID、Local_X、Local_Y 四列,观测 30 帧,预测 50 帧。

import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler OBS_LEN = 30 PRED_LEN = 50 STRIDE = 5 def load_and_group(csv_path): df = pd.read_csv(csv_path) df = df.sort_values(["Vehicle_ID", "Frame_ID"]) groups = [] for vid, g in df.groupby("Vehicle_ID"): if len(g) < OBS_LEN + PRED_LEN: continue xy = g[["Local_X", "Local_Y"]].values.astype(np.float32) groups.append(xy) return groups def make_windows(tracks, obs_len=OBS_LEN, pred_len=PRED_LEN, stride=STRIDE): xs, ys = [], [] for track in tracks: total = obs_len + pred_len for start in range(0, len(track) - total + 1, stride): obs = track[start:start + obs_len] fut = track[start + obs_len:start + total] # 用观测段最后一个点做相对坐标,减少绝对位置影响 origin = obs[-1] obs_rel = obs - origin fut_rel = fut - origin xs.append(obs_rel) ys.append(fut_rel) return np.stack(xs), np.stack(ys) tracks = load_and_group("ngsim_sample.csv") X, Y = make_windows(tracks) print("X shape:", X.shape, "Y shape:", Y.shape) # 对输入输出做标准化,保存 scaler 供推理使用 scaler = StandardScaler() X_flat = X.reshape(-1, 2) scaler.fit(X_flat) X_norm = scaler.transform(X_flat).reshape(X.shape) Y_norm = scaler.transform(Y.reshape(-1, 2)).reshape(Y.shape) np.save("scaler_mean.npy", scaler.mean_) np.save("scaler_scale.npy", scaler.scale_)

这段代码的逻辑是:先按车辆分组,过滤短轨迹,再用滑动窗口切样本。关键参数是 OBS_LEN、PRED_LEN、STRIDE。OBS_LEN 决定模型能看到多少历史,PRED_LEN 决定预测多远,STRIDE 控制样本重叠程度。STRIDE 越小样本越多但冗余越大,我一般设 5,既增加样本量又不至于让相邻窗口几乎一样。相对坐标那一步很重要,直接用绝对坐标会让模型花容量去记路段位置,而不是学运动模式。标准化用观测段拟合,推理时用保存的 mean 和 scale,避免数据泄漏。

2.3 LSTM 模型结构:几层、多少隐藏单元、要不要双向

一个能用的 LSTM 轨迹预测模型,结构通常是:输入维度 2(x, y),LSTM 层,取最后时间步的隐藏状态,接全连接层输出 PRED_LEN * 2,再 reshape 成 (PRED_LEN, 2)。层数一般 1~2 层,隐藏单元 64~256。双向 LSTM 在观测段上可以用,因为观测段是完整已知的,但预测段不能用双向,否则会看到未来。常见做法是编码器用双向 LSTM,解码器用单向 LSTM 或直接全连接。

下面是一个编码器-解码器结构的 PyTorch 实现。编码器读观测序列,解码器逐步生成预测序列。

import torch import torch.nn as nn class TrajLSTM(nn.Module): def __init__(self, input_dim=2, hidden_dim=128, pred_len=50, num_layers=2): super().__init__() self.pred_len = pred_len self.hidden_dim = hidden_dim self.encoder = nn.LSTM(input_dim, hidden_dim, num_layers, batch_first=True, bidirectional=True) self.decoder = nn.LSTM(input_dim, hidden_dim * 2, num_layers, batch_first=True) self.fc = nn.Linear(hidden_dim * 2, 2) def forward(self, obs): # obs: (B, OBS_LEN, 2) enc_out, (h, c) = self.encoder(obs) # 取编码器最后时间步,拼接双向 h_last = enc_out[:, -1, :] # (B, hidden*2) # 用编码器输出作为解码器初始输入,逐步预测 dec_input = obs[:, -1:, :] # (B, 1, 2) dec_h = h_last.unsqueeze(0).repeat(self.decoder.num_layers, 1, 1) dec_c = torch.zeros_like(dec_h) outputs = [] for _ in range(self.pred_len): out, (dec_h, dec_c) = self.decoder(dec_input, (dec_h, dec_c)) pred = self.fc(out[:, -1, :]) # (B, 2) outputs.append(pred.unsqueeze(1)) dec_input = pred.unsqueeze(1) return torch.cat(outputs, dim=1) # (B, pred_len, 2)

编码器用双向 LSTM,隐藏单元 128,两层。解码器用单向 LSTM,隐藏单元 256(因为编码器双向输出是 256)。全连接层把隐藏状态映射到 2 维坐标。训练时用 teacher forcing 还是自回归?我一般先用自回归,因为推理时就是自回归,训练和推理一致。但自回归训练慢,可以先用 teacher forcing 预热,再切自回归微调。损失用 MSE 或 Huber,Huber 对异常值更稳。

参数选择上,hidden_dim 128 是起点,数据量大可以加到 256,数据少就降到 64。num_layers 2 层足够,再深容易过拟合。pred_len 50 对应 5 秒,如果只要预测 3 秒就设 30。学习率 1e-3 配 Adam,batch size 64 或 128。这些不是金科玉律,但能让你第一次就跑出一个不崩的结果。

3. 训练、验证与推理:把模型跑起来

3.1 训练循环与损失曲线怎么看

训练循环本身不复杂,但有几个点容易翻车。第一,数据要按车辆 ID 划分训练集和验证集,不能随机划分窗口,否则同一辆车的相邻窗口会同时出现在训练和验证里,验证损失虚低。第二,损失用 MSE 时,坐标单位是米还是归一化后的值要清楚,归一化后的 MSE 数值很小,看起来「收敛了」但实际误差可能很大。第三,早停 patience 设 10~20 轮,别等到过拟合。

from torch.utils.data import DataLoader, TensorDataset def split_by_vehicle(tracks, obs_len, pred_len, stride, val_ratio=0.2): n = len(tracks) idx = np.random.permutation(n) val_n = int(n * val_ratio) val_idx = set(idx[:val_n]) train_tracks = [t for i, t in enumerate(tracks) if i not in val_idx] val_tracks = [t for i, t in enumerate(tracks) if i in val_idx] X_tr, Y_tr = make_windows(train_tracks, obs_len, pred_len, stride) X_va, Y_va = make_windows(val_tracks, obs_len, pred_len, stride) return X_tr, Y_tr, X_va, Y_va X_tr, Y_tr, X_va, Y_va = split_by_vehicle(tracks, OBS_LEN, PRED_LEN, STRIDE) train_ds = TensorDataset(torch.tensor(X_tr), torch.tensor(Y_tr)) val_ds = TensorDataset(torch.tensor(X_va), torch.tensor(Y_va)) train_loader = DataLoader(train_ds, batch_size=128, shuffle=True) val_loader = DataLoader(val_ds, batch_size=256, shuffle=False) model = TrajLSTM(pred_len=PRED_LEN) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) criterion = nn.HuberLoss(delta=1.0) best_val = float("inf") patience = 15 wait = 0 for epoch in range(100): model.train() train_loss = 0.0 for xb, yb in train_loader: optimizer.zero_grad() pred = model(xb) loss = criterion(pred, yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() train_loss += loss.item() * xb.size(0) train_loss /= len(train_ds) model.eval() val_loss = 0.0 with torch.no_grad(): for xb, yb in val_loader: pred = model(xb) val_loss += criterion(pred, yb).item() * xb.size(0) val_loss /= len(val_ds) print(f"epoch {epoch} train {train_loss:.6f} val {val_loss:.6f}") if val_loss < best_val: best_val = val_loss torch.save(model.state_dict(), "best_lstm.pth") wait = 0 else: wait += 1 if wait >= patience: print("early stop") break

梯度裁剪那行别省,LSTM 自回归训练时梯度容易炸。HuberLoss 的 delta 设 1.0,对归一化后的数据合适。验证损失用同一个 criterion,但注意它和实际米制误差不是线性关系,最终评估要反归一化后算 ADE 和 FDE。

3.2 评估指标:ADE、FDE 和高速场景的容忍度

轨迹预测常用两个指标:ADE(Average Displacement Error)和 FDE(Final Displacement Error)。ADE 是预测序列和真实序列每个时间步距离的平均,FDE 是最后一个时间步的距离。高速场景下,预测 5 秒,FDE 在 1.5~2.5 米算不错,ADE 在 0.8~1.5 米算合理。但要注意,这些数值和数据集、采样率、预测时长强相关,不能跨数据集直接比。

def compute_ade_fde(pred, gt): # pred, gt: (N, pred_len, 2) 反归一化后的米制坐标 dist = torch.norm(pred - gt, dim=-1) # (N, pred_len) ade = dist.mean().item() fde = dist[:, -1].mean().item() return ade, fde model.load_state_dict(torch.load("best_lstm.pth")) model.eval() with torch.no_grad(): pred_val = model(torch.tensor(X_va)) # 反归一化 mean = torch.tensor(np.load("scaler_mean.npy"), dtype=torch.float32) scale = torch.tensor(np.load("scaler_scale.npy"), dtype=torch.float32) pred_denorm = pred_val * scale + mean gt_denorm = torch.tensor(Y_va) * scale + mean ade, fde = compute_ade_fde(pred_denorm, gt_denorm) print(f"ADE {ade:.3f} m, FDE {fde:.3f} m")

反归一化时注意 scaler 是按 (x, y) 两列拟合的,mean 和 scale 各两个值,广播时要对齐最后一维。如果训练时用的是相对坐标,反归一化后还要加回原点才是绝对坐标,评估时如果只关心相对位移,可以不加。

3.3 推理部署:从 PyTorch 到 ONNX 的注意点

训练完的模型要落地,常见做法是导出 ONNX。PyTorch 转 ONNX 时,自回归解码器里的循环会被展开成固定长度的图,pred_len 多大就展开多少步。导出时要用 torch.onnx.export,提供 dummy input,opset 建议 11 或以上。

dummy = torch.randn(1, OBS_LEN, 2) torch.onnx.export( model, dummy, "traj_lstm.onnx", input_names=["obs"], output_names=["pred"], dynamic_axes={"obs": {0: "batch"}, "pred": {0: "batch"}}, opset_version=11 )

dynamic_axes 只对 batch 维开启动态,时间维固定。如果推理时 batch 会变,这样够了。导出后建议用 onnxruntime 跑一遍,对比 PyTorch 输出,误差在 1e-4 以内算正常。如果差太多,检查是不是有算子不支持,或者自回归循环里的状态没对齐。

4. 避坑与排查:轨迹预测里最容易翻车的几件事

4.1 验证损失很低但实际预测一塌糊涂

现象:训练时 val loss 降到 0.001 以下,但可视化预测轨迹发现车「原地不动」或「直线飞出去」。原因通常是数据泄漏或归一化问题。如果按窗口随机划分,同一辆车的相邻窗口同时进训练和验证,模型记住了这辆车的轨迹,验证损失虚低。另外,如果归一化用了全量数据的 mean 和 scale,验证集信息泄漏到训练里。解决:按车辆 ID 划分,归一化只用训练集拟合,验证和测试用训练集的参数。

4.2 预测轨迹平滑但滞后于真实轨迹

现象:预测的轨迹形状对,但整体比真实轨迹晚 0.5~1 秒。原因通常是模型倾向于输出「平均运动」,因为 MSE 损失下,预测均值能最小化误差,但均值不等于真实。高速场景下车辆匀速时滞后不明显,加减速时滞后严重。解决:改用位移增量预测,或者在损失里加一项对速度的约束。另一个办法是输入里加入速度、加速度特征,让模型不用从坐标里隐式推断。

4.3 训练到一半 loss 变成 NaN

现象:前几十轮正常,突然 loss 变 NaN。原因多半是梯度爆炸,自回归解码器逐步累积误差,反向传播时梯度连乘。解决:梯度裁剪设 5.0 或 1.0,学习率降到 1e-4,或者用 teacher forcing 预热几轮再切自回归。如果还不行,检查数据里有没有异常大的坐标值,归一化前先做异常值过滤。

4.4 换数据集后模型完全不工作

现象:在 NGSIM 上训好的模型,换到 highD 上 ADE 翻倍。原因通常是采样率不同、坐标单位不同、车道宽度不同。NGSIM 是 10Hz,highD 是 25Hz,同样的 OBS_LEN=30 对应的时间窗口不一样。解决:按时间窗口而不是帧数来切数据,比如统一观测 3 秒、预测 5 秒,然后根据采样率换算帧数。坐标单位如果是英尺和米混用,统一转成米。

4.5 推理时 batch 变了就报错

现象:训练时 batch size 128,推理时单条输入报维度错误。原因可能是模型里某处硬编码了 batch size,或者 ONNX 导出时没开 dynamic_axes。解决:PyTorch 模型里避免硬编码 batch,ONNX 导出时对 batch 维开 dynamic_axes。如果用了 DataParallel 或 DistributedDataParallel,导出前先包一层去掉。

5. 进阶技巧:用交互特征和课程学习把 FDE 再压 15%

LSTM 基线跑通后,想再往上走,有两个方向性价比最高。第一个是加交互特征。高速上车辆不是孤立的,前车速度、车头时距、相邻车道车辆位置都会影响本车轨迹。做法是把目标车周围一定范围内的车辆轨迹也取出来,按相对距离加权,拼到输入里。输入维度从 2 变成 2 + 2*K,K 是邻居数。邻居数一般取 3~5,太多会引入噪声。这个改动不需要换模型,LSTM 输入维度改一下就行,但数据预处理要重写,工作量在数据侧。

第二个是课程学习。先用简单样本训,比如匀速直线段,再用困难样本微调,比如变道、加减速段。判断样本难度可以用观测段的速度方差或加速度绝对值。训练时按难度排序,分阶段喂数据。我试过在 NGSIM 上把 FDE 从 2.1 米压到 1.8 米左右,大概 15% 的提升,代价是训练流程复杂了一倍。

还有一个技巧是预测多模态。LSTM 输出单一轨迹,但真实未来有不确定性。可以改成输出 K 条候选轨迹加概率,损失用 winner-takes-all 或 EM 风格。这个改动大,适合已经跑通基线、想发论文或做产品原型的场景。如果只是做课程设计或内部验证,单模态 LSTM 加交互特征已经够用。

验证方法上,除了 ADE/FDE,我习惯再画两类图:一是按预测时长分段的误差曲线,看误差是线性增长还是爆炸;二是按速度分段的误差,看低速和高速哪个更差。高速场景下低速段误差通常更大,因为低速时车辆行为更随机。如果低速段误差明显偏高,可以考虑对低速样本加权。

最后说个习惯。我每次换数据集或改模型结构,都会先跑一个「最小闭环」:取 100 辆车的数据,训 10 轮,看 loss 能不能降、预测图是不是人形。这个闭环跑通了再上全量。很多翻车都是因为一上来就全量训练,等了两小时才发现数据格式错了。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 8:16:54

AI浏览器扩展安全风险:权限过载、Prompt注入与防护策略

1. AI浏览器扩展&#xff1a;从提效神器到风险盲区1.1 我在哪一刻意识到问题不对劲先说个真实经历。上个月帮一家做跨境电商的团队做安全巡检&#xff0c;他们全员装了某款AI浏览器扩展&#xff0c;用来自动抓取竞品网页、总结邮件、翻译社媒评论。团队负责人挺得意地跟我说&am…

作者头像 李华
网站建设 2026/10/28 8:16:54

智能交通中的地图匹配与车速预测技术实践

1. 项目背景与核心价值车速预测一直是智能交通系统中的关键技术痛点。传统基于GPS轨迹的速度计算存在采样率低、信号漂移等问题&#xff0c;导致瞬时速度估算误差经常超过30%。我们在某网约车平台的实际数据测试中发现&#xff0c;单纯依赖GPS原始坐标计算速度时&#xff0c;高…

作者头像 李华
网站建设 2026/10/28 8:16:30

从热词到趋势:如何高效追踪AI动态并建立信息筛选框架

1. 今日AI动态&#xff1a;这周社区真正在讨论的几个方向本来想按老规矩逐条报新闻&#xff0c;但翻了上百条信息之后我发现&#xff0c;今天真正值得聊的并不是某个单点突破&#xff0c;而是一批方向同时在“从演示走向干活”。这一周的讨论热点可以归成七类&#xff0c;每类背…

作者头像 李华
网站建设 2026/10/28 8:16:18

微软三款语音模型深度解析:实时转写与多语言合成落地指南

语音识别和语音合成这两个方向&#xff0c;过去几年一直是"能用但不够好用"的状态。做会议记录的人知道&#xff0c;实时转写最怕延迟高、断句乱&#xff1b;做多语言内容分发的团队更头疼&#xff0c;一个语音模型能覆盖五六种语言就算不错了&#xff0c;剩下的语种…

作者头像 李华
网站建设 2026/10/28 8:16:09

5G NR定时提前TA与距离换算:从原理到现网覆盖优化实战

简介&#xff1a;5G(NR)网络优化中&#xff0c;时间对齐&#xff08;TA&#xff09;机制直接关系上行同步与信号质量。资源聚焦这一主题&#xff0c;面向无线网络优化工程师、接入网维护及5G技术学习者&#xff0c;系统梳理TA从原理到实际应用的关键环节。资源包为单个docx文件…

作者头像 李华
网站建设 2026/10/28 8:15:56

计算机视觉在UI自动化测试中的应用与实践

1. 计算机视觉与UI自动化的跨界融合在传统UI自动化测试领域&#xff0c;我们通常依赖DOM元素定位和XPath选择器来识别界面元素。这种方法虽然直接&#xff0c;但存在明显的局限性——当UI结构发生变化或元素属性修改时&#xff0c;测试脚本就会大面积失效。我在2018年参与的一个…

作者头像 李华