简介:这份基于长短期记忆网络的路径跟踪与路径预测实现包,面向自动驾驶、GIS、物流规划等领域的研究与学习者,提供一套可直接运行的MATLAB代码与完整数据。资源共14个文件,以8个.m脚本为主,涵盖LSTM模型构建、训练与评估流程,并附带2个.mat数据文件、2个CSV结果文件及自动保存的.asv备份;整体压缩包仅114KB,轻量但流程完整。项目入口脚本负责整体调度,核心脚本完成序列建模与路径预测,滤波相关脚本引入扩展卡尔曼滤波以融合辅助信息,评估脚本用于计算均方误差、均方根误差、平均绝对误差等预测性能指标,便于量化效果。结果CSV中同时给出预测路径与实际轨迹的对比数据,可直观检验模型性能。已有186人学习参考,适合希望同时了解深度学习时序建模与经典滤波融合方法的读者。
1. 长短期神经网络做路径跟踪与路径预测:为什么同一套模型有两种用法
“基于长短期神经网络的路径跟踪与路径预测”不是两个独立项目缝在一起,而是同一套时序建模能力被用在两种不同的控制任务上。路径预测是拿历史轨迹推未来若干步的位置,属于序列到序列的回归问题;路径跟踪则要把预测误差反馈回输入端,让模型输出的不再是位置,而是控制量。很多人把预测代码原封不动拿去跑跟踪,效果不好就怪数据集,实际上两者的损失函数、输入结构和训练策略都有本质区别。下面直接把这条路径拆开讲,从数据组织、模型结构、训练参数到五个高频翻车现场,照着就能跑通并验证。
2. 路径跟踪与路径预测的任务拆解:先搞清楚你要预测还是控制
2.1 路径跟踪和路径预测的本质区别:预测是“看”,跟踪是“动”
路径预测的输入输出关系比较简单:给定过去一段长度的轨迹点,比如 x/y 坐标、速度、航向角,预测接下来若干时刻的位置。这是一个纯粹的监督回归任务,常用损失函数是均方误差,评估指标是 RMSE 或平均位移误差。LSTM 在这里扮演的角色是一个时序外推器,它把隐状态当作对运动规律的一种内部表达,每一步都根据新输入更新这个表达,最终用窗口最末时刻的隐状态解码出未来轨迹。你不需要关心模型输出的数值会被谁消费,只需要关心“预测值和真实值之间差多少”。
路径跟踪的输入输出关系则完全不同:它需要在每一个控制周期里,根据当前的实际位置、航向角、与参考路径之间的偏差,输出转向角或速度指令,让执行机构把偏差收敛到零附近。这里面有一个闭环概念——模型的输出会作用于车辆,车辆的状态因此发生改变,下一个时刻的误差又会被重新喂回模型。如果模型只看当前误差,不看误差的历史变化过程,就像一个只看当前车速而不看加速度的驾驶员,永远无法平顺地切入参考线。
把问题拆开之后再回头看“长短期神经网络”这个标题会发现,两者不是继承关系,而是两种数据组织方式。预测数据是“过去窗口 → 未来窗口”,跟踪数据是“误差时间序列 → 控制量”。我在实际项目里见过太多人拿着预测模型去做跟踪,最后的典型症状是轨迹发飘、过弯提前减速,原因就是控制律缺失了反馈回路,而不是 LSTM 本身不行。
2.2 数据组织:窗口大小、坐标系选择与采样频率
路径预测的第一步是构造样本。假设有一条航迹数据集,每行记录包含时间戳、x 坐标、y 坐标、速度 v 和航向角 theta。要做成监督学习的输入输出对,需要选定两个数字:历史窗口长度 seq_len 和预测步数 pred_len。常见做法是 seq_len 取 10 到 30 个采样点,pred_len 取 5 到 20 个采样点,具体要看采样频率和应用场景。比如在 10 Hz 采样下,seq_len=20 意味着模型看过去 2 秒的运动,pred_len=10 意味着预测未来 1 秒的轨迹。这个比例不是随便拍的,移动机器人项目里常用 1:1 到 2:1 的历史/未来比例,既能保留足够的运动趋势,又不至于让预测误差在长时域里失控。
坐标系选择对结果影响很大。如果做全局路径预测,直接在绝对坐标上训练可以,但要注意先做去均值处理,否则 LSTM 的数值更新在坐标值达到几万的时候收敛极慢。如果做跟踪,强烈建议把输入定义在误差空间,也就是横向误差、航向误差与参考曲率,坐标系原点跟随参考线最近点移动。这样模型学到的是普适的纠偏规律,不会因为参考线起点的位置不同而失效。
采样频率要保持一致,这是 LSTM 和训练数据的硬约束。训练数据里不允许混入不同采样率的轨迹,否则每个时间步在语义上不是等间隔的,模型学到的时序关系就是错的。我一般会在预处理阶段对全部轨迹做一次重采样,统一到固定频率,并用线性插值补掉丢帧的坑。样本构造的代码很短,但这一步决定了后续模型的生死:
import numpy as np def make_samples(traj, seq_len=20, pred_len=10, stride=1): samples, targets = [], [] # 滑动窗口构造输入输出对,stride 控制相邻样本的重叠程度 for i in range(0, len(traj) - seq_len - pred_len, stride): x = traj[i : i + seq_len] # 历史窗口 y = traj[i + seq_len : i + seq_len + pred_len, :2] # 未来窗口,只取 xy samples.append(x) targets.append(y) return np.stack(samples), np.stack(targets)这段代码的逻辑很简单:从轨迹起始点开始,每次向后挪动 stride 个采样点,切出一个长度为 seq_len 的历史窗口和一个长度为 pred_len 的未来窗口。参数说明里最需要注意的是 stride。步长等于 1 时数据量最大,但相邻样本重叠过多,会让训练集极其冗余;步长等于 seq_len 时样本完全不重叠,训练集规模骤减,适合轨迹特别长的情况。我一般取 stride 在 1 到 10 之间,先看样本总量,控制在几千到几万条即可。还要注意数据分割必须在构造样本之前完成,而不是样本生成之后再随机划分,否则同一条轨迹的相邻窗口会同时出现在训练集和测试集,导致评估指标虚高得离谱。
归一化方面,预测任务常用零均值单位方差标准化,跟踪任务则把误差先做 min-max 缩放到 [-1, 1] 区间。这个差异来自输出空间的物理意义:预测位置的量级是米,而跟踪控制量的量级通常是有界的,tanh 输出天然匹配 [-1, 1]。如果你硬把两个任务的归一化方式互换,你会看到 LSTM 的损失函数在训练初期异常振荡,这就是数值尺度在作祟。
3. 用 LSTM 做路径预测:最小实现与三个决定性参数
3.1 最小可跑的 LSTM 路径预测模型:PyTorch 版本
路径预测是时间序列回归问题,LSTM 的结构选择相对固定。下面这个模型是我在移动机器人项目里最常用的一种简化版本,输入是历史窗口内的坐标序列,输出直接是一整段未来轨迹:
import torch import torch.nn as nn class LSTMPathPredictor(nn.Module): def __init__(self, input_dim=4, hidden_dim=128, num_layers=2, pred_len=10): super().__init__() self.pred_len = pred_len # batch_first=True 让输入形状变成 (batch, 时间步, 特征维) self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers, batch_first=True) self.head = nn.Sequential( nn.Linear(hidden_dim, 64), nn.ReLU(), nn.Linear(64, pred_len * 2) # 每个未来时刻输出一组 (x, y) ) def forward(self, x): out, _ = self.lstm(x) # out: (batch, seq_len, hidden_dim) h_last = out[:, -1, :] # 取最后一个时间步的隐状态 coords = self.head(h_last) # (batch, pred_len * 2) return coords.view(-1, self.pred_len, 2)代码逻辑分三步:先把整段历史窗口送进 LSTM,得到每个时间步的隐状态输出,然后只取最后一步的隐状态作为整个窗口的摘要,最后过一个线性层直接映射到未来 pred_len 个点的坐标。这里没有做自回归展开,属于 one-shot 型预测,好处是训练稳定、梯度路径短,坏处是时间相关性的建模完全依赖 LSTM 内部状态,所以最后一步隐状态的质量是整个模型的命门。
实际训练时,这个模型配合一个数据加载器就能跑。训练循环里有一个关键参数值得单独拿出来说:梯度裁剪。LSTM 在长序列上很容易出现梯度爆炸,哪怕数据归一化做得再好也躲不开。我会在每次反向传播之后加一行clip_grad_norm_,把梯度的 L2 范数压在 1.0 附近:
def train_one_epoch(model, loader, optimizer, criterion, clip=1.0): model.train() total_loss = 0.0 for x, y in loader: pred = model(x) # 前向:预测未来轨迹 loss = criterion(pred, y) # MSE 损失 optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(model.parameters(), clip) optimizer.step() total_loss += loss.item() * x.size(0) return total_loss / len(loader.dataset)参数说明里最值得留意的是 clip 值。1.0 是一个比较保守的起点,适合大多数传感器轨迹数据。如果训练 Loss 曲线出现突然跳高然后变 NaN,先降到 0.5 试试,再考虑减小学习率;如果 Loss 曲线一直平缓但预测结果很差,说明这个裁剪上限没有触发,问题不在梯度爆炸,而在模型容量或数据质量。
3.2 三个决定性参数:序列长度、隐层维度与学习率
路径预测 LSTM 的调参里,有三个参数几乎决定了下限和上限,其他细节都是在它们的基础上微调。我把它们列成了一张参数速查表,适合作为第一版模型的起点配置:
| 参数 | 推荐起点 | 调参方向 | 影响 |
|---|---|---|---|
| 历史窗口 seq_len | 20(10 Hz 下为 2 秒) | 小窗不适合转弯多的路径,大窗增加训练成本 | 决定模型能“记住”多久之前的运动趋势 |
| 预测步数 pred_len | 10(未来 1 秒) | 超过 2 秒后误差快速增大 | 决定任务难度和评估指标 |
| 隐层维度 hidden_dim | 128 | 数据量大且轨迹复杂时提至 256 | 过小拟合不足,过大在样本少时过拟合 |
| LSTM 层数 | 2 | 一般不超过 3 层 | 层数过深时梯度衰减加剧 |
| 学习率 | 0.001(Adam) | 从 0.0005 到 0.003 之间网格搜索 | 过大导致 Loss 震荡,过小收敛极慢 |
seq_len 对预测效果的影响是长短期记忆最直接的体现。seq_len 太短时,模型只看到最近一小段直线运动,无法判断曲率变化趋势,转弯处会“切弯”;seq_len 太长时,模型被过多的历史噪声干扰,而且训练样本数量会因为窗口变大而减少。一个直观的经验法则是:将 seq_len 设置为车辆完成一次典型换道或转弯所需采样点数的 1 到 2 倍。
hidden_dim 的选择和轨迹复杂度相关。对于园区内低速 AGV 的路径预测,128 个隐层单元绰绰有余;对于高速公路场景下的车辆轨迹预测,因为要考虑多模态交互和速度变化,256 个单元更稳妥。hidden_dim 再往上加就会开始频繁出现过拟合,表现为训练 Loss 很低但验证集预测轨迹抖动剧烈。
学习率是老生常谈,但在 LSTM 上尤其敏感。我见过不少人直接用默认 0.001,结果 Loss 在 0.5 到 5 之间来回弹跳。原因是 LSTM 的循环权重梯度幅度不一致,同一个学习率对不同参数的步长效果差异很大。如果你用 Adam,就把初始学习率放在 0.0005 到 0.001 之间,配合梯度裁剪跑 50 到 100 个 epoch,基本能收敛到一个可以用的水平。
4. 从预测到跟踪:闭环误差反馈是怎么接进去的
4.1 为什么开环预测不能直接当跟踪控制律
很多人以为路径跟踪就是“预测未来的位置,然后朝那个位置走”,这个直觉在低速直行时勉强成立,一旦遇到连续弯道就会出问题。原因在于开环预测模型没有感知自己“当前偏了多少”。它只是基于历史轨迹外推未来位置,完全没有把车辆当前的横向误差和航向误差作为修正依据。当车辆因为地面打滑或执行机构延迟偏离参考线时,预测模型会沿着已经偏离的轨迹继续预测下去,误差只会越来越大,这就是典型的开环发散。
我把这个场景叫作“离线模型直接在线用”的陷阱。路径预测模型在测试集上效果再好,也只是证明了它擅长复现已有运动模式,不等于它能在反馈回路里稳定工作。路径跟踪必须把“我当前偏了多远”这个信息放入模型输入。更准确地说,LSTM 在跟踪任务里不再是一个预测器,而是一个函数逼近器:它拟合的是从误差序列到控制量的映射关系。
4.2 把误差历史喂回 LSTM:闭环跟踪的实现与训练数据来源
闭环跟踪模型的关键是把上一时刻的控制量也作为当前输入的一部分。这样做有一个物理意义:车辆动力学是一阶或二阶系统,当前转向角会在下一时刻影响车辆状态,所以控制量的历史本身就是一条重要的时序信息。下面是一个最小闭环跟踪模型:
class LSTMTrackingController(nn.Module): def __init__(self, input_dim=5, hidden_dim=64, output_dim=2): super().__init__() # 输入的 5 个维度:横向误差、航向误差、参考曲率、速度误差、上一时刻控制量 self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers=1, batch_first=True) self.fc = nn.Linear(hidden_dim, output_dim) def forward(self, err_seq): # err_seq: (batch, time, 5) 误差时间序列,长度等于控制指令的时延窗口 out, _ = self.lstm(err_seq) last = out[:, -1, :] # 输出转向角增量与速度增量,tanh 限制在 [-1, 1] return torch.tanh(self.fc(last))这个模型的输入不能从单一时刻截取,必须按时间滑窗构造。比如每个训练样本是一段长度为 10 的误差序列,每条样本的标签是序列末尾时刻之后实际执行的控制量。训练数据从哪里来?常见做法有三种:一是从仿真环境跑闭环控制采集,二是用 PID 或纯跟踪算法作为专家策略,记录误差序列与控制量对应关系,三是真车上装有下位机控制日志时直接提取。
无论哪种来源,都要保证数据里包含足够的偏差场景。如果训练数据全部来自“误差很小、车辆基本贴线”的轨迹,模型只学会在完美条件下保持现状,一旦遇到较大的初始偏差就会手足无措。我一般会在数据构造阶段人为叠加随机偏移,让横向误差覆盖从 -0.5 米到 0.5 米的区间,航向误差覆盖 -10 度到 10 度,这样模型才能学到“从大偏差拉回来”的能力。
闭环跟踪模型训练时还要注意一个细节:损失函数应该定义在控制量空间而不是位置空间。如果用“模型输出控制量后的预测位置与参考轨迹的偏差”作为损失,梯度需要穿过车辆动力学模型,这在仿真里可行,但要处理多步时间展开,训练复杂度成倍增加。作为落地项目的第一步,直接在控制量空间计算 MSE 就够了,即使模型输出的控制量和专家策略不完全一致,只要跟随误差持续收敛,效果就已经达标。
5. 路径 LSTM 避坑指南:五个高频翻车现场与排查路线
5.1 Loss 直接变 NaN:数据归一化与学习率的叠加问题
现象:模型训练到第几个 epoch 时 Loss 突然跳到几千,然后永远回不来,打印出来的权重全是 NaN。
原因:两个因素叠加。一是输入数据没有做标准化,坐标值范围从几十到上万,LSTM 在时间步内的加权求和数值溢出;二是学习率偏大,让权重更新跨过稳定区域。
解决:先把输入和输出都做标准化,预测任务用 z-score,跟踪任务用 min-max,然后再训练。如果已经做了标准化还是 NaN,就把学习率从 0.001 降到 0.0003,同时加上clip_grad_norm_。我排查的顺序是:先看数据是否有 NaN 或无穷值,再看归一化是否真正生效(打印一批样本统计量),最后才是砍学习率。前两步能解决九成问题。
5.2 多步预测轨迹漂移:训练模式和推理模式不一致
现象:单步预测在测试集上误差很小,但把预测结果接回输入做多步递推时,预测轨迹越来越偏,最后飘到完全不是原来的形状。
原因:训练时每个样本的输入都来自真实历史轨迹,而推理时输入的前几个预测点本身就带误差,模型在“吃自己的输出”时误差被不断放大,这种分布偏移在文献里叫 exposure bias。
解决:在训练阶段引入多步自回归。方法不复杂:让模型在训练时以一定概率把上一时刻的预测输出当作下一时刻输入的一部分,概率从 1.0 随训练轮次逐步衰减到 0.5。这个技巧跟老师强迫学习是一对,一个强调用真实值校验,一个强调用预测值抗噪,两者配合使用效果最好。落地时最省事的方案是先用纯老师在训练集上预训练,再切换成自回归模式微调 20 个 epoch。
5.3 跟踪模型原地打转:在绝对坐标空间算损失
现象:把跟踪任务的数据做成“当前位置 + 参考点 → 转向角”,模型训练 Loss 一直降不下来,或者车辆在直线路径上左右摆动。
原因:绝对坐标系下,同样的几何误差在不同位置有不同的数值表达,模型需要同时记住空间位置和纠偏策略,这对 LSTM 来说是双重负担。更本质的问题是绝对坐标无法直接反映车辆与参考线的相对关系。
解决:把模型输入全部改成误差空间,包括横向误差、航向误差和参考曲率。横向误差用车辆当前位置到参考线最近点的距离带符号表示,航向误差用车辆航向减去参考线切线方向。这样输入输出都变成了“跟坐标系无关的偏差信号”,模型的泛化能力立刻上一个台阶。这也是项目里最容易翻车的地方——数据格式看着像预测,结构却藏着控制的几何关系。
5.4 评估结果虚高:相邻轨迹片段同时进了训练集和测试集
现象:测试集 RMSE 只有 0.1 米,看起来非常漂亮,但放到新地图的新轨迹上效果惨不忍睹。
原因:数据分割在样本构造之后做,相邻样本窗口共享大量时间步,导致训练集和测试集之间存在直接重叠。LSTM 学到的是“背出训练集里的那段轨迹”,而不是泛化出运动规律。
解决:先按轨迹文件或连续运行段分割,再把每一段完整分配给训练集或测试集。也就是说,样本构造发生在数据划分之后。一个可执行的检验方法是:训练完成后随机抽一条测试集轨迹,把模型预测结果和真实轨迹画在一起,如果某段长得跟训练集里某段几乎一致,就要怀疑数据泄露了。
5.5 只看一条轨迹的预测效果:单次评估没有统计意义
现象:项目汇报时只贴了一张效果图,某条轨迹的预测线和真实线几乎重合,但另一个场景下模型表现很差,结论互相矛盾。
原因:单条轨迹评估天然带有选择性。预测模型在直线段误差小,在急弯段误差大,单一轨迹的 RMSE 会因为这组数据的弯道比例不同而差异巨大。
解决:把测试指标分场景统计。至少按直线段、缓弯段、急弯段三类场景分开计算 RMSE,同时报告预测步长为 1 秒、2 秒、3 秒时的误差增长曲线。这样能一眼看出模型是整体退化,还是在某种特定工况下崩溃。
6. 验证 LSTM 路径预测效果的一个实用方法:多点前视误差与样本外轨迹测试
6.1 多点前视误差:一个指标看透多步预测质量
传统的 RMSE 把所有预测步长混在一起算,掩盖了“短期准、长期飘”的问题。我的习惯是计算每个预测时刻的单独误差,做成一条误差随预测步长变化的曲线。下面这段代码可以接在任意预测模型的测试流程后面:
def per_step_rmse(preds, targets): # preds / targets: (num_samples, pred_len, 2) diff = preds - targets l2 = torch.sqrt((diff ** 2).sum(dim=-1)) # 每个样本每个步长的欧氏距离 return l2.mean(dim=0).cpu().numpy() # 每个预测步长的平均误差 errors = per_step_rmse(model_pred, model_target) for i, err in enumerate(errors): print(f"step {i+1}: {err:.3f} m")这段代码的作用是把前面模型输出的 (batch, pred_len, 2) 预测结果拆成单步误差序列。如果 errors 在第 1 步就超过 0.3 米,说明模型的短期惯性都没有学好;如果第 1 步误差稳定但第 5 步误差突然翻倍,说明模型缺乏对转弯趋势的长期建模能力。这个曲线也是调节 seq_len 的重要依据:曲线在你设定的预测范围里保持平缓增长,说明窗口长度合适;如果曲线在中段出现拐点,就该把历史窗口加长或检查数据里是否有变速场景。
6.2 样本外轨迹测试:把“没见过的路”当作验收标准
样本外测试指用完全不参与训练的轨迹段来评估模型,包括新场景、新路径形状甚至不同驾驶风格的数据。我给自己定的一个验收标准是:训练集表现再漂亮,都不算数,至少拿三条全新轨迹做测试,而且这三条轨迹要覆盖直线、弯道和连续 S 弯。如果模型在新轨迹上的误差曲线和测试集大体一致,才能说明 LSTM 学到了运动规律而不是记住了数据。
这个方案的落地成本很低:准备数据时就把轨迹按路段分组,每组单独编号,切分时保证同组全部落在训练集或全部落在测试集。后面不管怎么调参数,都拿这三条轨迹当基准,每次改动只允许让样本外误差下降,不允许反过来迁就训练集。多年实践下来,这个方法帮我挡掉过至少两次“测试集看起来无敌、上车就翻”的事故。路径跟踪和预测这两个方向,数据组织和验证方式往往比网络结构更决定成败——把数据切分和评价指标先立住,剩下的 LSTM 调参只是时间问题。希望这些经验对你也有用。
本文还有配套的精品资源,点击获取