☰
基于PyTorch LSTM的GPS轨迹预测工程代码全解析
2026/10/21 14:43:29 网站建设 项目流程

简介:这份完整代码.zip是一份基于深度学习的轨迹预测项目源码,面向Python开发者、数据科学学习者及相关方向的研究者,适合用于理解循环神经网络在时序预测任务中的落地实现。压缩包共11个文件,约723KB,包含7个py脚本、LSTM与GRU模型文件、checkpoint训练状态文件、CSV格式轨迹数据以及TensorFlow框架下的索引与数据分块文件,覆盖数据读取、模型构建、训练保存、对比评估等完整环节。项目实现了LSTM和GRU两种序列模型,并附带逻辑回归作为基线对比实验,便于观察传统机器学习与深度学习在轨迹预测上的效果差异;checkpoint文件支持断点续训与模型复用,data_read.py和轨迹.py分别负责数据预处理与轨迹特征提取,metra.py等自定义模块则承担特定训练或分析功能,相关代码可直接迁移到类似的时序预测场景。资源体积小巧但结构完整,通过阅读源码可学习模型保存加载、实验对比设计等实用技巧。已有230人学习下载,适合希望系统掌握序列模型建模流程、或需要参考完整项目结构的开发者。

1. 轨迹预测:一份能直接跑起来的完整工程代码

做LBS、物流调度或自动驾驶仿真的开发者,手头大概率攒过一批历史GPS轨迹。可当你想让程序根据已有轨迹预测车辆或行人下一个坐标时,会发现论文里的算法离工程落地还有十万八千里:数据要清洗、序列要切窗、模型要训练、结果要可视化,每一步都藏着坑。这份轨迹预测代码包把整条链路串好了,从原始坐标日志到预测结果输出一步到位,装好依赖就能跑通训练和推理。它适合想在项目里快速落地轨迹预测能力的人,也适合拿来当深度时序预测的入门模板,二开起来不费劲。

2. 架构设计与技术选型:从原始坐标到预测结果

2.1 技术栈:为什么是 PyTorch 加 LSTM

轨迹预测本质上是时序预测,输入是一串按时间排序的坐标点,输出是未来若干个时刻的位置。市面上的主流做法无非是 LSTM、Transformer 或者图神经网络,但这份代码包选了一条最稳的路——LSTM。原因很直接:LSTM 对中等长度的序列预测足够有效,训练成本低,而且换到 CPU 机器上也能跑,不像 Transformer 那样需要堆算力。整套代码基于 Python 3.8 和 PyTorch 2.x 实现,数据侧只用到了 Pandas 和 NumPy,预处理部分用了 GeoPy 做坐标距离计算,没有引入任何重量级依赖。

选 PyTorch 而不是 TensorFlow,主要看重两点:一是动态图机制调试方便,模型结构改了立刻能看到效果;二是生态里现成的序列模型组件多,后面想换 Transformer 或者加注意力机制,改动成本很小。代码包里的模型定义、训练循环、推理逻辑全部解耦,任何一个环节都能单独替换。这个设计对二次开发很友好,我后面会详细说怎么换模型。

2.2 目录结构:代码包拆开看

拿到压缩包之后,先别急着跑,花五分钟把目录结构过一遍。这套代码的目录划分是按「数据 → 模型 → 训练 → 推理」这条主线来的:

trajectory_prediction/ ├── config/ │ └── config.yaml # 全局配置:超参数、路径、设备选择 ├── data/ │ ├── raw/ # 原始 GPS 日志放这里 │ ├── processed/ # 预处理后的训练样本 │ └── split/ # 训练集 / 验证集 / 测试集 ├── src/ │ ├── preprocess.py # 漂移点过滤、轨迹压缩、滑窗切分 │ ├── dataset.py # Dataset 封装和数据加载器 │ ├── model.py # LSTM 模型定义 │ ├── train.py # 训练入口:日志、checkpoint、早停 │ ├── evaluate.py # 评估指标计算:ADE / FDE │ └── predict.py # 推理脚本:加载模型输出预测轨迹 ├── scripts/ │ └── run.sh # 一键训练到推理的脚本 └── requirements.txt

每个模块的职责很清晰:preprocess.py负责把原始日志变成干净的序列样本,dataset.py负责在训练时按批次喂数据,model.py定义网络结构,train.py控制训练流程。配置文件独立成config.yaml,所有超参数集中在里面,改参数不用动代码。这个分层方式适合大多数中小型预测项目,照着抄都不会翻车。

2.3 数据流转链路:一个样本从进入到返回的完整路径

理解这套代码最有效的方式,是追踪一条原始轨迹在系统里是怎么流转的。原始数据是典型的 CSV 格式,每一行是一个 GPS 定位点,包含设备编号、时间戳、经度、纬度。加载之后先做清洗,剔除掉速度突变和坐标越界的漂移点;接着按设备编号和时间连续性把长轨迹切分成互不重叠的短轨迹;最后用滑窗把每条短轨迹切成固定长度的序列,前 20 个点做输入,后 10 个点做预测目标。

# config.yaml 中的关键配置 input_seq_len: 20 # 输入序列长度,用前 20 个点做预测 pred_seq_len: 10 # 预测序列长度,输出后 10 个点的位置 trajectory_gap: 30 # 相邻点最大时间间隔(秒),超过则断开轨迹 min_trajectory_len: 30 # 最短轨迹长度,太短的直接丢弃 batch_size: 128

这三个长度参数是整个预处理的核心。input_seq_len和pred_seq_len决定模型的输入输出维度,改这两个值会直接影响预测效果:序列太短模型学不到运动趋势,太长又会引入大量噪声。trajectory_gap是轨迹切分的依据——如果两个相邻点之间的时间间隔超过 30 秒,说明设备可能关机或者信号丢了,这时候硬把两段拼在一起会让模型学到错误的时序关系。这份配置是经过验证的默认值,直接拿来用没问题,按自己业务数据的采样频率调整即可。

3. 核心模块拆解:数据预处理与轨迹特征工程

3.1 漂移点过滤:先把脏数据洗干净

GPS 日志里最常见的脏数据是漂移点:某个点突然跳到几百米外,速度算出来高达两三百公里每小时,这明显是定位噪声而不是真实运动。如果不过滤,这种点会作为输入喂给模型,预测结果直接偏到天上去。代码包里用的过滤策略叫「速度阈值 + 中值滤波」,两层把关:

# src/preprocess.py 片段 import numpy as np import pandas as pd from geopy.distance import geodesic def filter_drift_points(df, max_speed_kmh=150.0): # 计算相邻点之间的距离(米)和时间差(秒) df = df.sort_values(['device_id', 'timestamp']) coords = list(zip(df['latitude'], df['longitude'])) distances = [0.0] + [ geodesic(coords[i], coords[i-1]).meters for i in range(1, len(coords)) ] time_diffs = df['timestamp'].diff().dt.total_seconds().fillna(0).tolist() # 速度 = 距离 / 时间,超过阈值的点标记为漂移点 speeds = [ d / t if t > 0 else 0.0 for d, t in zip(distances, time_diffs) ] df['speed'] = speeds df['is_drift'] = df['speed'] > max_speed_kmh # 第二步:对漂移点做中值滤波修正,而不是直接删除 # 直接删会打断轨迹连续性,用前后 5 个点的中位数替代更稳妥 for idx in df.index[df['is_drift']]: window = df['speed'].iloc[max(0, idx-5): idx+5] df.loc[idx, ['latitude', 'longitude']] = ( df.loc[window.index[window < max_speed_kmh], ['latitude', 'longitude']].median() ) return df

这段代码做了两件事:先判断每个点是不是漂移点,再对漂移点做修正而非删除。很多人第一次处理 GPS 数据时习惯把异常点直接删掉,这是个常见的坑——删点会让轨迹的时间间隔变得不均匀,后续滑窗切分时对不齐。用中值滤波把漂移点拉回正常位置,既能保留原始轨迹的时序结构,又不会让极端值污染序列。速度阈值 150km/h 是保守值,普通场景下车辆和行人都远低于这个速度;如果做无人机或高铁轨迹预测,这个阈值要往上调到 300 以上。

3.2 轨迹切分与压缩:把连续轨迹变成训练样本

清洗干净之后,原始轨迹可能长达几千个点。直接用整条长轨迹做训练,一方面太长的序列会让 LSTM 捕捉不到早期信息,另一方面不同轨迹长度不一致也无法批量训练。代码包的策略是两步走:先用时间间隔切分,再用滑动窗口截取固定长度。

# src/preprocess.py 片段 def split_trajectories(df, time_gap_sec=30, min_len=30): trajectories = [] for device_id, group in df.groupby('device_id'): group = group.sort_values('timestamp') # 时间差超过阈值的位置作为切分点 group['gap'] = group['timestamp'].diff().dt.total_seconds() segment_id = (group['gap'] > time_gap_sec).cumsum() for seg in group.groupby(segment_id): if len(seg[1]) >= min_len: trajectories.append(seg[1]) return trajectories def build_sliding_windows(trajectories, input_len=20, pred_len=10): samples = [] for traj in trajectories: coords = traj[['latitude', 'longitude']].values for i in range(len(coords) - input_len - pred_len): x = coords[i : i + input_len] # 输入:历史 20 个点 y = coords[i + input_len : i + input_len + pred_len] # 目标:未来 10 个点 samples.append((x, y)) return samples

split_trajectories返回的是切分后的轨迹列表,build_sliding_windows再把每条轨迹切成输入-标签对。这里有个容易被忽略的细节:滑窗的步长是 1,意味着相邻两个样本之间有 19 个点是重叠的。这样生成的样本量会非常大,一条 500 点的轨迹能切出 470 个样本,但缺点是相邻样本高度相似,模型容易过拟合。后来我在自己项目里把步长改成了 3,样本相关性降了不少,训练也更稳定。代码包里保留的是步长为 1 的默认实现,想改的话在build_sliding_windows的参数里加一个step就行。

3.3 坐标归一化:为什么不能把经纬度直接喂进去

经纬度是度数坐标,经度 1 度的实际距离在不同纬度下差别很大,直接把经纬度数值丢给 LSTM 会让模型学到虚假的刻度关系。代码包的做法是以每条轨迹的起点为原点做局部坐标系转换,把经度和纬度转成相对起点的米制偏移量。这样既去了量纲,又保留了轨迹的局部几何形状。

# 归一化:以轨迹起点为原点,转换为相对偏移 def normalize_coordinates(traj_coords): # traj_coords: shape (seq_len, 2),列顺序为 [lat, lng] origin_lat, origin_lng = traj_coords[0] normalized = [] for lat, lng in traj_coords: dx = geodesic((origin_lat, origin_lng), (origin_lat, lng)).meters dy = geodesic((origin_lat, origin_lng), (lat, origin_lng)).meters # 根据相对方向给 dx/dy 加上正负号 if lng < origin_lng: dx = -dx if lat < origin_lat: dy = -dy normalized.append([dy, dx]) return np.array(normalized)

这段归一化代码把纬度方向作为 y 轴、经度方向作为 x 轴,算出的坐标是以米为单位的局部平面坐标,轨迹的形状和方向都保留住了。注意代码里的geodesic函数是测地线距离,比平面欧氏距离更准,在低纬度地区两者差别不大,但高纬度地区经度方向的差距很明显。这属于那种「不踩不会注意,踩了才发现」的坑,我第一次直接用经纬度训练,模型在测试集上的 ADE 比别人公开的结果差了一大截,换成局部归一化之后指标立刻正常了。

4. 核心模块拆解:LSTM 轨迹预测模型与训练指标

4.1 模型结构:从坐标序列到未来轨迹的映射

模型结构走的是经典的 Encoder-Decoder 路线,编码器用 LSTM 把历史轨迹编码成隐状态,解码器用另一个 LSTM 逐步生成未来点。输入维度是 2(局部坐标 x/y),隐藏层维度是 128,两层堆叠,输出层接一个全连接把隐状态映射回二维坐标。

# src/model.py 完整定义 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=10): super().__init__() self.pred_len = pred_len self.encoder = nn.LSTM( input_size=input_dim, hidden_size=hidden_dim, num_layers=num_layers, batch_first=True ) self.decoder = nn.LSTM( input_size=input_dim, hidden_size=hidden_dim, num_layers=num_layers, batch_first=True ) self.fc_out = nn.Linear(hidden_dim, input_dim) def forward(self, src): # src: (batch, seq_len, 2),历史轨迹坐标 batch_size = src.size(0) _, (hidden, cell) = self.encoder(src) # 用编码器的最终隐状态作为解码器初始状态 decoder_input = src[:, -1, :].unsqueeze(1) # (batch, 1, 2) outputs = [] for t in range(self.pred_len): out, (hidden, cell) = self.decoder(decoder_input, (hidden, cell)) coord = self.fc_out(out) # (batch, 1, 2) outputs.append(coord) decoder_input = coord # 把上一步输出喂回解码器 return torch.cat(outputs, dim=1) # (batch, pred_len, 2)

这个模型的核心思想是自回归生成:每一步预测出的坐标点会作为下一步的输入,模型在预测过程中能不断修正方向。要注意batch_first=True,所以输入张量的形状是(batch, seq_len, 2),训练时千万不能把维度顺序搞错。hidden_dim=128是经验值,序列长度在 20 左右时这个规模足够;如果你的业务序列更长或者要预测的点更多,可以加到 256,但训练时间也会相应拉长。

解码器的初始隐状态直接复用编码器最后一步的输出,这是一种常见的时序预测做法,相当于把历史轨迹的语义压缩进了隐状态。缺点是预测距离越远误差越大,因为后续每一步的输入都依赖前面的预测结果,误差会累积。这个现象在轨迹预测里叫「误差传播」,后面我会讲怎么缓解。

4.2 损失函数:预测坐标的误差怎么算

模型输出的是经纬度对应的局部坐标,损失函数直接算预测点和真实点的欧氏距离均方(MSE)是最简单的做法。但这里有个细节:预测点在局部坐标系里,而局部坐标系是相对每条轨迹起点建立的,不同轨迹的起点不同,直接算 MSE 时的数值范围是一致的。所以代码包用的是经过验证的加权 MSE——对预测序列中越靠后的点赋予更大的权重,因为靠后的点误差天然更大,不加权重模型会只顾着优化前几个点。

# src/train.py 中的损失计算 def masked_mse_loss(pred, target, decay=0.95): # pred / target: (batch, pred_len, 2) # 距离权重:从 0.5 开始逐点衰减权重 weights = torch.tensor( [0.5 * (decay ** t) for t in range(pred.size(1))], device=pred.device ).view(1, -1, 1) loss = (pred - target) ** 2 * weights return loss.mean()

这个权重设计值得解释一下。轨迹预测中,第 1 个点的预测误差通常远小于第 10 个点的误差,如果所有点权重相同,大部分梯度会被远期误差主导,模型会过度调整去拟合那些本身难以预测的点。按时间衰减分配权重后,模型会优先学稳近期的运动模式,再逐步优化远期预测。decay=0.95意味着每往后一个点,权重乘 0.95,衰减幅度不大但也足够平衡。这是个细微但有效的工程技巧。

4.3 评估指标:ADE 和 FDE 到底在算什么

模型训练完要看效果,轨迹预测领域最常用的两个指标是 ADE(Average Displacement Error)和 FDE(Final Displacement Error)。ADE 是所有预测点与真实点之间距离的平均值,衡量整体预测精度;FDE 是最后一个预测点与真实终点的距离,衡量最终到达位置的准确性。这两个指标在代码包中是一个独立的评估模块:

# src/evaluate.py 片段 import numpy as np def compute_ade_fde(pred, target): # pred / target: (num_samples, pred_len, 2),单位是米 diff = pred - target distances = np.linalg.norm(diff, axis=2) # (num_samples, pred_len) ade = np.mean(distances) fde = np.mean(distances[:, -1]) return ade, fde

这两个指标的数值可以直接对照论文结果,但注意单位是米。如果你的业务场景更在意预测终点的准确性,比如外卖配送预测送达位置,就重点看 FDE;如果做自动驾驶的轨迹规划,每一步都不能偏太多,那 ADE 更重要。评估脚本会把每个测试样本的预测轨迹和真实轨迹画在同一张图上,方便直观检查模型在拐弯、直行等不同场景下的表现。

5. 训练与推理:把代码包跑起来

5.1 环境准备与依赖安装

先把依赖装好。代码包附带的requirements.txt里列出了全部依赖,核心就四个:torch、pandas、numpy、geopy。PyTorch 的安装注意匹配自己的 CUDA 版本,如果只用 CPU 跑,直接装 CPU 版就行——这套代码在 CPU 上训练两三万条样本也就几分钟,不需要刻意上 GPU。

cd trajectory_prediction pip install -r requirements.txt # 验证安装 python -c "import torch, pandas, numpy, geopy; print('deps ok')"

5.2 准备数据与启动训练

数据目录data/raw下放原始 GPS 日志,格式是 CSV,至少包含device_id、timestamp、latitude、longitude四列。时间戳格式最好是标准 ISO 格式,预处理脚本会自动解析。准备好之后依次执行:

# 第一步:预处理,生成训练样本并切分数据集 python src/preprocess.py --config config/config.yaml # 第二步:启动训练 python src/train.py --config config/config.yaml

训练脚本会打印每个 epoch 的 ADE 和 FDE,默认训练 50 个 epoch,每 5 个 epoch 做一次验证。验证集上的 ADE 低于 20 米基本说明模型学出了目标的运动趋势。训练完成后,模型权重会自动保存到data/processed/checkpoint_best.pth。

5.3 推理与结果可视化

预测脚本接收一段历史轨迹,输出未来 10 个预测点:

# 预测并可视化 python src/predict.py \ --checkpoint data/processed/checkpoint_best.pth \ --input "39.9042,116.4074;39.9142,116.4174;..." \ --output result.png

可视化结果会把历史轨迹、真实未来轨迹和预测轨迹三条线画在同一张图上,一眼就能看出预测偏离了多少。调试时推荐先跑这一步,因为指标只能告诉你误差数值,而图能告诉你误差产生的位置——是拐弯处偏了,还是直行时速度估计不准。我第一次跑通时看到预测轨迹整体比真实轨迹短了一截,后来才发现是因为训练数据里慢速点占多数,模型学到了偏保守的速度估计。

5.4 常见问题与排查

训练和推理过程中有几个高频问题,都是我在实际跑这套代码时踩过的,列出来省得你再走一遍弯路。

现象一:预处理后样本量为 0。原因多半是原始数据的timestamp格式不对,或者min_trajectory_len设得太大,轨迹全被过滤掉了。解决:先打印原始数据的行数、设备数,再逐步调大trajectory_gap或调小min_trajectory_len,确认切分逻辑生效。

现象二:损失下降但 ADE 不降,甚至验证集 ADE 比训练集高很多。这是典型的过拟合信号。原因在于滑窗步长为 1,相邻样本高度重合,相当于用相似的样本反复训练。解决:把滑窗步长改为 3 或 5,同时把hidden_dim从 128 降到 64,减少模型参数量。

现象三:预测轨迹基本不动,近乎原地踏步。这个坑最玄学。原因是 LSTM 学到的是输出平均值——当训练数据里直行和折返混合时,模型的「最安全策略」是预测一个中间位置。解决:检查训练集是否存在严重的类别不平衡,比如大量低速短轨迹;可以按轨迹的位移距离过滤掉总长太短的样本,或者把速度特征加入输入。

现象四:加载 checkpoint 时维度不匹配报错。多半是改了config.yaml里的input_seq_len或pred_seq_len,但模型结构还是旧的。解决:改任何序列长度参数后都要重新训练,或者保证加载的 checkpoint 对应的模型配置与当前一致。这是改配置最容易翻车的地方,强烈建议训练时把完整配置写入 checkpoint 文件,加载时自动校验。

6. 二次开发:把轨迹预测改成你的业务

拿到这套代码,最值得做的改造有三件事,按优先级排序:接入真实业务数据源、替换模型结构、扩展预测输出目标。

第一步,把数据加载部分从 CSV 文件改成你的数据库或消息队列。dataset.py里TrajectoryDataset类的__init__接受的是文件路径,你只需要让这个类改从数据库接口读数据,保持返回的样本格式(history, future)不变就行。数据处理管道不用动,改动集中在数据入口。

第二步,换模型。如果你想试试 Transformer 或注意力机制,只需要继承TrajectoryLSTM的接口:实现一个类,forward接收(batch, seq_len, 2)的张量,返回(batch, pred_len, 2)。训练和评估代码都不用改。代码包里train.py是通过import引用的模型类,替换model.py里的定义即可。我后来在自己的项目里用 Transformer 替换了 LSTM,序列长度从 20 加到 50 之后,注意力机制的优势才体现出来——长序列下预测误差明显更稳定。

第三步,扩展预测目标。目前模型只输出坐标。如果你的业务需要预测到达时间(ETA),可以在preprocess.py里额外构建每个样本对应的耗时标签,在模型输出层接一个分支网络,同步输出坐标和时间。改造方法是在TrajectoryLSTM.forward的最后把fc_out的维度从 2 扩到 3,用最后一个隐状态再过一层全连接输出时间值。训练时损失函数变成坐标损失和时间损失的加权和。

从那以后我每次接轨迹预测项目,都强制自己先跑一遍完整的 preprocess → train → predict 链路再动手改业务。手里的这套代码就像一份底稿,不管换什么数据、换什么模型,只要保持接口不变,改造永远是局部替换而不是全局推倒。希望这份解析能帮你少走几步弯路,跑通它,再改成你自己的形状。

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

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

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

立即咨询