基于LSTM的飞机轨迹预测:ADS-B数据预处理与工程实践
简介飞机轨迹预测是保障飞行安全、优化航线规划的关键技术也是当前序列建模研究的热点应用。这份资源以结合注意力机制的双分支LSTM-Transformer混合网络为核心面向航空数据研究人员、深度学习开发者及高校相关专业学生可用于盘旋、爬升、俯冲、巡航等典型机动动作的高精度轨迹预测帮助理解并应用两种主流序列模型的融合策略。压缩包共33个文件整体约10.53MB包含27个csv格式的飞行轨迹数据集、4个Python算法脚本、1个训练完成的h5最优模型和1份docx研究文档覆盖从数据处理、模型训练到结果可视化的完整链路。Python脚本分别实现基础预测、Transformer融合、双向GRU等方案便于横向对比不同网络在时序依赖建模上的差异可视化脚本可直接绘制预测轨迹训练好的模型文件免去重复训练csv数据也方便二次分析与扩展实验。文档还详细阐述了双分支结构与注意力机制的设计思路可帮助复现实验。目前已有115人学习下载适合需要快速搭建轨迹预测模型、开展对比实验或直接应用现成模型的开发者与研究人员。1. 飞机轨迹预测不是玄学这份代码到底在解决什么如果你手头拿到的是一个名为“代码轨迹预测飞机轨迹预测.rar”的压缩包大概率是你正在处理空管、通航监控或者无人机冲突探测一类的问题。说白了它要干的事情很具体拿到飞机过去 20 秒甚至 2 分钟的真实轨迹点预测出未来 30 秒、60 秒它会在哪。这个问题在序列预测里属于典型的“短时预测易、长时预测难”——预测两秒后的位置是线性外推的事预测两分钟后的位置就完全变成了一个需要模型捕捉运动模式的问题。搜“lstm模型代码”找到的大多是股票和量化交易场景的示例而飞机轨迹的特点在于方向变化有惯性、速度受飞行性能约束这一层物理约束是纯时间序列模型不容易天然学到的。这份代码包能解决的是三类人的诉求一是做通航监控的工程师需要给管制界面画一条未来轨迹线二是做无人机冲突探测的开发者需要判断两架飞机是否会在未来某个时间点距离过近三是研究多目标跟踪的学生需要一个能落地的基线模型。它不是雷达信号处理也不是气象预报它就是把坐标序列和速度序列喂给神经网络然后输出未来坐标。难点不在模型结构而在数据清洗、坐标变换和评估口径——这三件事做不好再花哨的网络结构都会在真实数据上翻车。2. 先把数据喂对ADS-B 轨迹怎么切成训练样本2.1 轨迹数据长什么样从原始 CSV 到归一化坐标不管是空管雷达还是 ADS-B 地面站导出的轨迹数据最常见的格式就是 CSV每一行是一条状态向量。关键的字段一般是这几列航班标识icao24 或 callsign、时间戳Unix 秒或 ISO 时间、纬度、经度、高度米或英尺、地速、航向。拿到手的第一步不是建模而是把这些字段整理成一条条连续的轨迹序列。常见坑先说一个原始数据的采样频率不固定。ADS-B 报文在飞机平飞时可能 5 秒一条在盘旋机动时可能 1 秒一条。所以要先按航班分组组内按时间排序再做时间重采样。我一般会先看看时间间隔的分布如果大部分间隔集中在 1 秒附近就先按 1 秒插值如果间隔乱到没法看就把数据集按“每个航班的绝对时间”对齐到同一个网格再切样本。坐标处理上有个原则训练模型用的坐标必须是局部直角坐标而不是经纬度。直接用经纬度喂进模型在赤道附近还能凑合在中纬度地区 1 度经度对应的物理距离和 1 度纬度差很多模型会被这个几何畸变带偏。常见做法是选一个参考点把经纬度转成东北天坐标系ENU单位是米。import pandas as pd import numpy as np def wgs84_to_enu(lat, lon, alt, ref_lat, ref_lon, ref_alt0.0): # 先把 WGS84 经纬度转成 ECEF再转到以 ref 点为原点的 ENU a 6378137.0 e2 6.69437999014e-3 lat_r np.radians(lat) lon_r np.radians(lon) ref_lat_r np.radians(ref_lat) ref_lon_r np.radians(ref_lon) def lla_to_ecef(la, lo, al): n a / np.sqrt(1 - e2 * np.sin(la) ** 2) x (n al) * np.cos(la) * np.cos(lo) y (n al) * np.cos(la) * np.sin(lo) z (n * (1 - e2) al) * np.sin(la) return np.array([x, y, z]) p lla_to_ecef(lat_r, lon_r, alt) ref lla_to_ecef(ref_lat_r, ref_lon_r, ref_alt) d p - ref # ENU 旋转矩阵 x -np.sin(lon_r) * d[0] np.cos(lon_r) * d[1] y -np.sin(lat_r) * np.cos(lon_r) * d[0] - np.sin(lat_r) * np.sin(lon_r) * d[1] np.cos(lat_r) * d[2] z np.cos(lat_r) * np.cos(lon_r) * d[0] np.cos(lat_r) * np.sin(lon_r) * d[1] np.sin(lat_r) * d[2] return x, y, z这段代码把经纬高转成以参考点为原点的东北天坐标单位是米。值得说明的是参考点怎么选我一般取整个数据集里出现次数最多的机场位置或者取轨迹数据集的经纬度中位数。转换完的 x、y 就是模型真正看到的坐标z 轴高度是否一起喂进模型要看你预测的目标——如果只预测水平轨迹高度列可以先留着但不进特征如果预测完整的三维轨迹那就把 x、y、z 和速度一起拼成特征向量。2.2 时间片切分与样本构造pred_len 和 hist_len 怎么定数据清洗完之后下一步就是把长轨迹切成长度为 hist_len pred_len 的滑动窗口。hist_len 是模型看到的历史帧数pred_len 是要求模型预测的未来帧数。这两个参数直接决定了问题难度和训练样本量。先说最常见的选择如果数据是 1 秒一条hist_len 取 20 到 30pred_len 取 30 到 60。20 秒左右的上下文足够让模型看到一次转弯的起始段30 秒到 60 秒的预测窗口则是空管关心的“提前一分钟预警”区间。如果只做短时冲突检测可以把 pred_len 缩到 10这样误差会小一个量级但工程价值也下降。切样本时有个关键动作必须剔除那些历史窗口或预测窗口跨越两次飞行阶段切换的样本。什么叫阶段切换飞机从爬升转平飞从平飞转下降从直飞转盘旋。这些过渡段不是模型应该背下来的模式混进去会让训练集里充满相位不齐的“脏样本”。我的做法是先对每条轨迹做一次分段按速度变化率和航向变化率把轨迹切成若干段切窗口时确保窗口完整落在某一段内。def sliding_window(seq, hist_len20, pred_len30, stride5): total hist_len pred_len samples [] for i in range(0, len(seq) - total 1, stride): win seq[i : i total] hist win[:hist_len] pred win[hist_len:] samples.append((hist, pred)) return samplesstride是滑窗步长。为什么不用 1因为相邻窗口的重合度太高样本之间几乎完全相关训练时模型会反复看到同一段轨迹的不同偏移版本等于变相放大了这部分数据的权重。步长取 5 到 10 能让训练集在保持多样性的同时压缩体积。注意切出来的 hist 和 pred 是相对坐标还是绝对坐标我建议 hist 使用原始 ENU 坐标pred 也使用原始 ENU 坐标但 在训练时把预测目标从“绝对坐标”改成“相对位移”。也就是让模型预测的是未来每个时刻相对当前时刻的位置增量而不是直接预测绝对位置。这样模型更容易学到运动趋势而不是去记忆每个机场附近的地理位置。2.3 解耦数据管线用缓存换迭代速度数据处理这块容易被低估的一点是如果每次训练都在主进程里现算坐标转换和滑窗切分IO 和 CPU 的开销会占到总训练时间的一半以上。我见到的很多“lstm模型代码”跑不动或者迭代慢原因不在模型而在数据管线没做好缓存。常规做法是三步第一步清洗后的轨迹按航班存成中间格式比如 parquet每个航班一个文件第二步把所有航班切成滑窗样本打包成一个 npy 文件或者多个 shard第三步训练时直接读预处理好的样本不再做任何坐标变换。这样做的代价是预处理脚本要单独跑一遍换来的是后面几十次训练实验的迭代速度。# 预处理阶段跑一次生成训练缓存 python preprocess.py --input adsb_2024.csv --output ./cache/train_data.npz --hist_len 20 --pred_len 30 --stride 5preprocess.py内部做的事情就是上面两节讲的按航班分组、时间排序、重采样、转 ENU、滑窗切片。生成npz后训练脚本里用np.load直接加载然后在内存里做 shuffle 和 batch 切分。这套管线的另一个好处是当你需要调大 hist_len 或者更换参考点时只需要重跑预处理脚本不需要动训练代码数据管线和模型之间的耦合被解开了。3. 从代码包到跑通LSTM 基线模型的落地骨架3.1 rar 解压后的目录结构判断与数据流核对拿到rar包第一步是解压然后把目录结构摸清楚。基于这个标题的命名方式里面的内容大概率是两类东西的混合一份轨迹预测代码一份飞机轨迹预测的说明或数据集。解压后如果看到train.py、model.py、data/、config.yaml这样的结构那是一个标准的深度学习工程项目如果只有几个.py文件和一个 Excel那可能只是某个算法复现的草稿。无论哪种你都要自己重新梳理一遍它的数据流原始数据从哪来、进什么函数、变成什么形状、喂给什么模型。我拿到别人代码后的习惯是先别急着跑先画一条数据流线——文件路径 → 读取函数 → 预处理函数 → 模型输入 → 模型输出 → 损失函数。在代码里搜np.load、pd.read_csv、torch.utils.data.DataLoader这几个入口把输入输出的 shape 打出来核对一遍。这一步能帮你省掉后面至少两小时的排查时间尤其是当代码用了多个脚本互相调用时数据形状的约定不一致是常态。3.2 LSTM 基线PyTorch 实现的核心代码块不管压缩包里原本是什么模型我都建议你先拿一个 LSTM 基线跑通全流程然后再考虑换成注意力机制或者其他结构。原因是LSTM 在轨迹预测这种低维度序列特征维度通常不到 10上表现稳定训练速度快而且对超参数不敏感。下面这个是我常用的基线模型骨架可以直接放进model.py里。import torch import torch.nn as nn class TrajLSTM(nn.Module): def __init__(self, input_dim5, hidden_size128, num_layers2, pred_len30, output_dim2): super().__init__() self.lstm nn.LSTM(input_dim, hidden_size, num_layers, batch_firstTrue) self.head nn.Sequential( nn.Linear(hidden_size, 128), nn.ReLU(), nn.Linear(128, pred_len * output_dim) ) def forward(self, x): # x: (batch, hist_len, input_dim) out, _ self.lstm(x) last_feat out[:, -1, :] pred self.head(last_feat) batch x.size(0) return pred.view(batch, self.pred_len, self.output_dim)几个参数要说清楚。input_dim是每条轨迹点的特征维度常见组合是 [x, y, 速度, 航向, 高度] 共 5 维如果你没有航向和速度的测量值可以只喂 [x, y, 高度] 共 3 维但模型预测效果会明显差一截。hidden_size取 128对轨迹数据一般够用再大会导致过拟合且训练变慢。num_layers取 2 是因为单层 LSTM 对长时间依赖的捕捉能力有限两层能在不大幅增加参数量的情况下提升记忆能力。输出层的设计是把最后一步的隐藏状态经过全连接层一次性输出pred_len * 2个数值也就是未来 30 个时刻的 x、y 坐标。这种方式叫“一次性预测”比逐点递归预测稳定得多。3.3 输入输出的维度约定batch、seq_len、feature 的对齐PyTorch 的 LSTM 默认输入是(seq_len, batch, input_dim)但上面代码里设置了batch_firstTrue所以输入变成(batch, seq_len, input_dim)。这个batch_first是个高频翻车点很多人从网上复制 LSTM 代码忘了自己的数据是(seq_len, batch, feature)的形状结果要么运行时报维度错误要么模型学到的是乱七八糟的跨样本关联。还有一个维度约定特别容易踩历史轨迹的排序方向。RNN 读序列是从左往右读的所以x[:, 0, :]必须是时间上最早的那个点x[:, -1, :]必须是当前时刻。有些代码为了训练时“预测未来”把整个序列倒过来喂给模型输出再倒回来这种做法偶尔能提升短期记忆但对于轨迹预测这种有明确物理时序的场景保持正向输入是更可靠的选择。数据进模型前还有一个必须做的操作特征归一化。x、y 坐标的单位是米可能跑到几千甚至几万速度和航向的取值范围又不一样。如果不做归一化LSTM 的训练会非常不稳定。我之前踩过的坑是只对 x、y 做了归一化速度和航向原样输入结果 loss 一直在震荡。后来把所有特征都单独做标准化减均值除标准差训练立刻就稳了。4. 训练与评估别只看 loss要盯住 ADE 和 FDE4.1 训练循环里最容易忽略的三个参数训练一个轨迹预测模型loss 用什么、优化器参数怎么设、随机种子定多少这三件事直接决定你复现别人代码时能不能得到同样的曲线。先说 loss轨迹预测最常用的不是纯 MSE而是 Huber Loss也叫 Smooth L1。原因是 ADS-B 数据里偶尔会混入离群点MSE 对离群点的惩罚是平方级的会导致模型为了少数几个异常点扭曲整个预测Huber 在误差小的时候表现为平方损失误差大的时候转为线性损失对离群点更宽容。优化器选择上我一般用 AdamW 而不是 Adam。原因不是 AdamW 一定更好而是轨迹预测模型的输入特征是连续值权重衰减能有效抑制过拟合。学习率初始值设在 1e-3配合余弦退火调度器在 30 个 epoch 内衰减到 1e-5。很多人复现失败的原因都在于学习率设成 1e-2 甚至更高导致训练一开始就发散。criterion nn.SmoothL1Loss() optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-5) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) for epoch in range(30): for batch_x, batch_y in train_loader: optimizer.zero_grad() pred model(batch_x) loss criterion(pred, batch_y) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() scheduler.step()clip_grad_norm_是第三个容易忽略的参数。LSTM 在长序列上容易梯度爆炸特别是预测窗口拉长到 60 步之后。设置max_norm5.0的意思是如果所有参数的梯度范数超过 5就等比缩放梯度到范数为 5。这个操作几乎不改变模型收敛方向但能避免训练中期 loss 突然跳到 NaN。4.2 ADE/FDE 指标计算与损失函数彻底解耦训练时的 loss 和评估时的指标在轨迹预测里是两套东西。loss 是模型优化的目标指标是你向业务方交代的数字。空管场景下最常看的两个指标是 ADEAverage Displacement Error和 FDEFinal Displacement Error。ADE 是预测的全部时间点上预测位置和真实位置的平均欧氏距离FDE 只算最后一个预测点与真实终点之间的距离。这两个指标的意义完全不同ADE 衡量的是整条预测轨迹的贴合度FDE 衡量的是“最终会落到哪”的决策能力。对于冲突探测FDE 比 ADE 重要得多因为你想知道的是未来 30 秒后两架飞机隔多远对于态势显示ADE 更重要因为你要画的是一条连贯的线条。def compute_ade_fde(pred, target): # pred: (batch, pred_len, 2), target: (batch, pred_len, 2) displacement torch.norm(pred - target, dim-1) # (batch, pred_len) ade displacement.mean(dim-1).mean().item() fde displacement[:, -1].mean().item() return ade, fde注意一个细节如果你在训练时把预测目标从“绝对坐标”改成了“相对位移”那么评估时必须把相对位移累加成绝对坐标再和真实绝对坐标计算误差。不能直接把模型输出的相对位移和真实相对位移拿去算 ADE/FDE那样算出来的是“位移误差”不是“位置误差”两个指标在数值上会差一个量级且没有业务解释力。4.3 验证集的时间切分防止数据泄漏的边界轨迹预测项目里最隐蔽的坑是数据集切分方式。如果直接把所有滑窗样本随机打乱然后按 8:2 切训练集和验证集那么同一个航班的不同时间段会同时出现在训练集和验证集里。模型在训练时已经见过这个航班的飞行风格验证集上的 ADE 会好看得离谱但上线后面对新航班立刻原形毕露。正确做法是按时间切分把航班按日期排序比如 1 月到 10 月的数据做训练11 月到 12 月的数据做验证。这样验证集里的航班和训练集里的航班没有任何时间重叠模型面对的是真正没见过的数据。按时间切分通常会让 ADE 比随机切分差 30% 到 50%这不是模型不行而是随机切分本身就低估了真实难度。如果你在复现别人代码时发现他的验证集指标好到不对劲先问一句他的数据是怎么切分的5. 飞机轨迹预测的 5 个常见问题排查现象、原因、解决5.1 训练 loss 收敛但预测全是直线现象训练曲线正常下降验证集 ADE 也不差但把预测结果画出来看模型输出的轨迹就是一条从当前点出发的直线完全没有转弯趋势。原因这个现象在数据中转弯样本占比太少时几乎必然出现。如果 90% 的飞行数据都是巡航阶段的平飞模型学到的最优策略就是“按当前速度方向直飞”因为在大部分样本上这样做的误差已经足够小。模型没有动力去学习转弯模式。解决最直接的办法是重采训练数据把转弯段样本做复制或加权。我一般会在滑窗切分前先统计每个窗口的航向变化率航向变化累计超过 30 度的窗口标记为“机动样本”在构造 DataLoader 时让机动样本的抽样权重是平飞样本的 3 到 5 倍。如果重采样后还是直线再检查输入特征里有没有航向角——如果没有航向角模型只能从坐标序列间接推断方向变化能力上限有限。5.2 坐标系混乱导致预测点漂移到离谱位置现象训练时指标正常但用训练好的模型做推理输出的坐标点飞到几百公里外完全不符合物理规律。原因训练和推理时的坐标转换不一致。常见的情况是训练用的是 ENU 坐标但推理脚本里没有做转换直接把经纬度喂给了模型或者是参考点选得不一致训练时以 A 机场为原点推理时却用 B 机场为原点。解决把坐标转换函数单独抽出来和模型文件放在一起。无论训练还是推理统一走同一个wgs84_to_enu函数并且把参考点的经纬度写死在配置文件里。推理脚本里在喂给模型之前打一行日志打印输入坐标的均值和标准差和训练时的统计值对比就能快速发现坐标体系是否错乱。5.3 时间戳不对齐导致滑窗样本错乱现象模型训练 loss 一直在高位抖动无法下降。检查数据发现同一窗口内轨迹点的顺序是乱的。原因ADS-B 数据存在报文乱序问题特别是多地面站接收时同一个航班的数据可能在不同时间点到达时间戳小的报文可能晚于时间戳大的报文被写入文件。如果按文件名或按接收顺序切窗口就会把时间上颠倒的轨迹点当作正常顺序喂给模型。解决在预处理脚本里按航班分组后必须先做sort_values(timestamp)然后校验时间戳是否严格递增。对于时间戳相同重复报文的行保留第一条。做完排序后再检查时间间隔如果相邻两条间隔超过 10 秒说明中间有数据缺失这段轨迹应该在拼接时断开而不是跨越缺失段滑窗。5.4 预测轨迹发散越预测越快最后速度失去物理意义现象一次性预测模式下模型输出的轨迹点间距在时间上逐渐增大到了第 20 秒的预测位置已经比第 1 秒的位置远了 5 倍距离明显违反飞行性能约束。原因模型只优化了位置误差没有显式约束速度。训练数据里飞机速度变化是平缓的但模型在长时预测时可能找到一个“把不确定性转化为大步长”的局部最优解。解决在两个位置加约束。第一在输入特征里显式加入速度列让模型能看到当前速度第二在 loss 里加一项速度平滑损失计算预测轨迹相邻点之间的距离和参考速度做 Huber 损失。这样模型被强制保持合理的速度范围。还有一种做法是把输出从绝对坐标改为相对位移因为位移的大小本身就受限于训练集中速度的分布不容易发散。5.5 归一化统计量泄漏导致指标虚高现象验证集 ADE 只有几十米看起来效果极好但部署到现场后误差大了十倍。原因归一化时用了全量数据训练 验证的均值和标准差。如果某个机场的坐标范围在验证集和训练集里不相同但归一化使用的是整体统计量等于验证集的信息在预处理阶段就被模型间接看到了。解决归一化的统计量只在训练集上计算保存成norm_stats.json推理时加载这个文件用相同的参数做标准化。这是数据管线上一个很小的改动但能避免“实验室指标虚高、线上翻车”的尴尬。验证集的意义是模拟未知数据任何从验证集泄露到训练过程的信息都会让评估失真。6. 最后一步把单条预测变成多条候选提升轨迹预测工程可用性前面几节讲的都是预测一条轨迹。但在真实空管和无人机冲突探测里只给一条轨迹是不够的——未来 60 秒存在多种可能的走法飞机可能直飞、可能左转 30 度避让、可能增速爬升。单条轨迹预测面对这种多模态分布时模型只能输出一个平均结果而这个平均值在业务上往往不像任何一条真实轨迹。这就是为什么现在轨迹预测工程里开始用“多模态预测”或者“候选轨迹生成”的方案。具体做法是把模型输出的最后一层从pred_len * 2改成k * pred_len * 2其中 k 是候选轨迹的条数。训练时不再让所有候选都靠近真实轨迹而是让每条候选轨迹各自预测一个可能的结果loss 取 k 条候选中离真实轨迹最近的那条的误差。这就把模型从“猜一个最好的答案”变成了“猜 k 个可能答案”真实轨迹只需要落在其中一个候选附近即可。评估指标也从 ADE/FDE 变成 minADE_k 和 minFDE_k意思是 k 条候选里的最优误差。这样做之后业务系统可以同时显示 3 条未来的可能路径让管制员看到“这架飞机未来一分钟大概率在这个扇形范围内活动”。验证这个方法是否真的有效可以用一个简单的统计测试随机抽取 1000 个测试样本对比单模态模型和多模态模型在 k3 时的 minADE 值。如果多模态模型的 minADE_3 比单模态 ADE 下降了 40% 以上说明数据集里确实存在明显的多走向模式多模态输出拿到了这部分收益如果两个指标几乎一样说明你的应用场景里飞机轨迹的确定性很高多模态只会增加计算量不值得做。回想我第一次把自己训练的单模态模型拿给一个做空管的朋友看他看完只说了一句你这个预测线太“孤注一掷”了我看不到它的不确定性没法信它。从那以后我养成了一个习惯任何轨迹预测模型交付之前先画一张预测结果的散点图把 50 个测试样本的真实轨迹和预测轨迹叠在一起。如果看不出模型的“犹豫”就说明它还没有把不确定性表达出来。这个习惯让我少走了很多弯路希望帮到你。本文还有配套的精品资源点击获取