
简介面向航空领域轨迹预测研究与Python开发者此压缩包包含一套基于注意力机制的双分支LSTM-Transformer混合网络的飞机轨迹预测实现。文档详细阐述了融合LSTM时序建模与Transformer全局依赖捕捉的模型结构并给出针对盘旋、螺旋上升、巡航、爬升、俯冲等飞行场景的预测代码同时包含双向GRU变体对比与轨迹可视化脚本便于研究者分析模型表现并直观理解预测结果。压缩包内共三十三个文件主体为二十七个CSV格式的轨迹数据集另有四个Python算法文件、一个训练好的best_model.h5权重文件及一个DOCX技术文档整体大小约十点五三兆字节目录结构清晰可直接用于模型训练、测试与结果可视化。已有115人学习下载对希望快速复现混合网络研究或投入轨迹预测工程应用的开发者这套资源能有效降低从数据处理到模型调用的门槛兼顾了科研探索与实际落地需求。1. 轨迹预测不是模型问题先搞懂这套代码在解决什么无论你手里拿到的是 ADS-B 广播数据、二次雷达点迹还是仿真平台生成的航迹飞机轨迹预测的核心从来不是“选一个看起来很厉害的模型”而是先把坐标、时间、速度这些最基础的东西对齐。这套代码轨迹预测飞机轨迹预测资源包解决的正是从原始轨迹点到可训练样本、再到模型输出未来轨迹点的完整链路。适合刚接触轨迹预测、想直接跑通一套 Python 实现的读者也适合已经在用传统滤波方法、想对比深度模型效果的人。拿到它之后你的第一步不是打开模型文件而是先看数据长什么样——这个顺序错了后面全是白费功夫。接下来我会按拆包顺序把这套代码的数据处理、模型选型、训练验证和典型坑位逐个过一遍。2. 拆解代码包结构数据、特征、模型各司其职2.1 压缩包内的文件构成与职责一个典型的轨迹预测代码包解压后通常不是一个大而全的脚本而是按职责拆成几个模块。这套资源的结构大致如下文件/目录职责说明data/原始轨迹数据通常是 CSV 或 Parquet字段含时间戳、经度、纬度、高度、速度、航向preprocess/数据清洗与特征工程坐标系转换、缺失值处理、时间序列重采样models/模型定义LSTM、Transformer 或卡尔曼滤波的封装类train.py训练入口读取数据、构造样本、训练并保存模型权重predict.py推理入口加载模型输入历史轨迹输出未来 N 个轨迹点eval.py评估指标RMSE、终点误差、航迹偏差计算config.yaml超参数配置滑动窗口长度、预测步长、学习率、批次大小我一般会先打开config.yaml和predict.py因为这两个文件直接决定了你能不能跑通。config.yaml里如果滑动窗口是 30、预测步长是 10意味着用过去 30 个轨迹点预测未来 10 个点这两个数字直接关系到模型输入输出的形状改错了连 tensor 维度都对不上。predict.py里通常有一个load_model的过程它读的是训练阶段保存的权重文件。如果包里没有附带训练好的权重你就得老老实实把train.py跑一遍。这一步没有捷径模型结构再花哨没有对应权重的模型文件是没法直接拿来推理的。2.2 为什么轨迹预测用 LSTM/Transformer 而不是传统外推经典的轨迹预测基线是卡尔曼滤波或多项式外推它们的好处是计算量小、可解释性强在短时预测3~5 个点以内表现不差。但飞机轨迹有一个特点转弯段和水平段的行为模式差异极大线性外推在转弯处会直接穿透航迹误差呈指数级放大。深度学习模型能根据历史序列学到转弯半径、速度变化率和高度变化率的组合模式本质上是把轨迹预测从“位置外推”变成了“模式匹配”。LSTM 适合中等长度序列Transformer 适合超长上下文但在航迹数据量不够大时Transformer 容易过拟合LSTM 反而是更稳妥的起点。这套代码包如果默认用的是 LSTM不用急着换先把曲线拟合和损失函数调好效果通常已经够用。数据量没有上万条完整航迹之前换 Transformer 的收益大概率赶不上它带来的调参成本。3. 数据预处理是第一道坎坐标系、时间戳与特征工程3.1 坐标系转换经纬度不能直接当平面坐标用ADS-B 数据里的经纬度是 WGS84 球面坐标直接把经度当 x、纬度当 y 丢进模型中高纬度地区的距离会被严重压缩导致速度、加速度特征全面失真。常见做法是先转成 UTM 投影坐标或者在局部区域内做等距圆柱投影。UTM 按 6 度带划分跨带数据要先做带号判断否则相邻航迹点可能落到两个不同的坐标系里。import numpy as np import utm def wgs84_to_local(easting_list, northing_list, lat_ref, lon_ref): 将 WGS84 经纬度转为以参考点为原点的局部平面坐标。 这里用 UTM 投影 去中心化避免经度/纬度直接作为平面坐标导致的距离畸变。 lon np.array(easting_list, dtypenp.float64) # 经度注意命名上的小误会 lat np.array(northing_list, dtypenp.float64) # 纬度 easting, northing, zone_num, zone_lett utm.from_latlon(lat[0], lon[0]) x_all, y_all, _, _ utm.from_latlon(lat, lon) # 以起点为原点统一单位到米 x x_all - easting y y_all - northing return x, y这段代码的关键在于先取第一个点作为参考原点后续所有点都相对它偏移。这样模型输入的特征数值不会是大几百万的 UTM 坐标而是以米为单位的相对位移神经网络训练起来稳定得多。注意zone_num和zone_lett要保留下来如果一条航迹跨了两个带utm.from_latlon返回的坐标会出现跳变这时候要按参考点统一重投影而不是直接拼接。如果不想引入utm依赖也可以用简化的等距圆柱投影在数据覆盖范围小于 100 公里时误差可接受# 简化版仅适合小范围数据R 取地球平均半径 6371000 米 R 6371000.0 x R * np.radians(lon - lon_ref) * np.cos(np.radians(lat_ref)) y R * np.radians(lat - lat_ref)这个简化版的限制很明确数据覆盖范围一大地球曲率的影响就出来了。我会在预处理脚本里加一个范围检查超过阈值就自动切成 UTM 模式避免用户拿到新数据时踩同样的坑。3.2 时间戳对齐与轨迹点清洗原始轨迹数据里最隐蔽的问题是时间戳。ADS-B 地面站接收到的报文延迟不稳定有的点相隔 1 秒有的点相隔 12 秒如果直接按原始时间间隔丢给模型模型学到的“速度”含义就乱了。必须重采样成固定时间间隔的序列。import pandas as pd def resample_trajectory(df, interval_sec5.0): 按固定时间间隔重采样航迹。 df 必须包含列timestamp, x, y, altitude, velocity, heading interval_sec: 目标采样间隔单位秒。ADS-B 数据一般 5 秒或 10 秒比较合适。 df df.sort_values(timestamp).reset_index(dropTrue) t_min, t_max df[timestamp].min(), df[timestamp].max() new_index pd.DataFrame({timestamp: np.arange(t_min, t_max interval_sec, interval_sec)}) merged pd.merge_asof(new_index, df, ontimestamp, directionnearest) # 时间差特征相邻重采样点之间的实际秒数用于校正速度 merged[dt] merged[timestamp].diff().dt.total_seconds().fillna(interval_sec) return mergedpd.merge_asof是处理采样时刻没有精确匹配时最顺手的工具它会取离目标时间最近的一个观测点作为该时刻的值。这里有一个参数值得留意directionnearest会同时允许向后或向前找点如果数据本身比较稀疏可以改成backward只允许用更早的数据点避免未来信息泄漏。时间差特征dt必须保留模型在计算速度变化率时要用它做归一化否则重采样后时间间隔不一致的问题会残留到特征里。轨迹点本身的清洗也不难但很烦。常见问题包括经纬度突然跳变到另一个机场附近、速度瞬间从 800 km/h 掉到 0、高度出现负值。我的处理原则是单点异常直接剔除并前后插值连续异常超过 5 个点就整段截断不强行缝合两个不相邻的可疑轨迹段。3.3 特征工程航向角的 sin/cos 编码与速度归一化模型需要的特征不是原始经纬度而是能刻画运动学状态的量。速度、高度变化率、航向角变化率是最常用的三个。其中航向角有个数学上的坑0 度和 360 度其实是一个方向但如果直接作为数值特征模型会认为 0 和 360 差了很大导致转弯处的预测剧烈抖动。def add_motion_features(df): 根据 x/y/时间差计算速度、加速率、航向角及其变化率。 航向角用 sin/cos 两列表示避免 0/360 度跳变问题。 dx df[x].diff().fillna(0.0) dy df[y].diff().fillna(0.0) dt df[dt].values df[speed] np.sqrt(dx**2 dy**2) / dt df[heading_raw] np.arctan2(dy, dx) # 关键用 sin/cos 编码角度而不是直接用角度值 df[heading_sin] np.sin(df[heading_raw]) df[heading_cos] np.cos(df[heading_raw]) # 高度变化率 df[climb_rate] df[altitude].diff().fillna(0.0) / dt return df.drop(columns[heading_raw])这里最关键的参数是fillna(0.0)。序列第一行没有前值diff 结果是 NaN直接用 0 填充意味着假设初始速度为 0实际中这会让模型对航迹起点的预测偏差偏大。更稳妥的办法是保留 NaN在构造训练样本时把包含 NaN 的前缀段截掉。训练数据多的时候这点影响不大但如果你想做“从起飞开始预测”就得分段处理起点不能一刀切填 0。特征列里heading_sin和heading_cos是成对使用的只保留其中一个等于丢失了角度的部分信息模型无法区分朝向正北和正东的区别。拼特征时务必两列同时加入。4. 模型搭建与训练从滑动窗口到轨迹输出4.1 滑动窗口样本构造输入输出怎么对齐模型训练前要先把连续轨迹切成固定长度的样本。假设原始轨迹有 300 个点滑动窗口长度为 30预测步长为 10那么第一个样本用第 0~29 个点预测第 30~39 个点第二个样本用第 1~30 个点预测第 31~40 个点以此类推。这里的步长决定了样本之间的重叠程度步长越小样本越多但相邻样本高度相关训练出来的模型泛化性会打折。def make_samples(features, lookback30, horizon10, stride5): 构造监督学习样本。 features: N x D 的特征矩阵列顺序固定 lookback: 历史窗口长度 horizon: 预测未来点数 stride: 滑动步长越大样本量越小样本间相关性越低 samples_x, samples_y [], [] for i in range(0, len(features) - lookback - horizon 1, stride): x features[i : i lookback] y features[i lookback : i lookback horizon, :2] # 只预测 x/y 坐标 samples_x.append(x) samples_y.append(y) return np.array(samples_x), np.array(samples_y)stride5是一个平衡选择。它对一条 300 点的航迹能切出约 50 组样本既保持足够的训练数据量又不至于让相邻样本几乎完全相同。预测目标y只取了 x/y 两个坐标这是个可以按需改的决策如果你关心高度预测可以把俯仰相关的特征列也加进去如果你想同时预测速度和航向就把目标改成多输出回归。但目标维度增加会直接放大训练难度前期建议只保留 x/y先把位置预测做准再扩展。4.2 LSTM 模型与前向传播结构LSTM 做轨迹预测标准套路是用一个多层 LSTM 编码历史序列再接一个全连接层输出未来点坐标。有一个容易忽略的点LSTM 的输出步长和预测步长不是一回事。编码器只输出最后一个时间步的隐状态然后让全连接层一次性输出未来 10 个点的坐标而不是让 LSTM 逐步自回归生成。一次性输出的优点是训练稳定缺点是转弯段容易输出过度平滑的轨迹。二者各有利弊第一次跑建议用一次性输出调通后再试自回归。import torch import torch.nn as nn class TrajLSTM(nn.Module): def __init__(self, input_dim, hidden_dim64, num_layers2, horizon10): input_dim: 特征维度即每个轨迹点的特征数量 hidden_dim: LSTM 隐层维度64 是常用起点 num_layers: LSTM 层数2 层是性价比比较高的选择 horizon: 预测未来点数 super().__init__() self.lstm nn.LSTM( input_sizeinput_dim, hidden_sizehidden_dim, num_layersnum_layers, batch_firstTrue ) self.fc nn.Linear(hidden_dim, horizon * 2) # 2 表示 x/y 两个坐标 def forward(self, x): # x: (batch, lookback, input_dim) lstm_out, (h, c) self.lstm(x) last_hidden h[-1] # 取最后一层的隐状态 out self.fc(last_hidden) return out.view(-1, self.horizon, 2)h[-1]取的是顶层 LSTM 最后一个时间步的隐状态它聚合了整个输入序列的信息。horizon * 2这个维度要仔细核对如果预测目标改成 4 维这里就要改成horizon * 4同时view的最后一个维度也要同步改。batch_firstTrue让输入张量形状是 (batch, seq_len, features)这是 PyTorch 惯例也方便你直接调试中间张量形状。训练时损失函数选HuberLoss而不是MSELoss。轨迹数据里偶尔会有离群点MSE 会给这些点极高的梯度权重导致模型为了拟合一个异常转弯牺牲全局精度。Huber 损失在误差小于delta时表现为平方损失大于delta时表现为线性损失天然对离群点有抵抗性。criterion nn.HuberLoss(delta10.0) # delta 取 10 米坐标单位是米 optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-5)delta10.0意味着误差小于 10 米时按平方处理超过 10 米后梯度不再随误差线性增长这个值应该结合业务需求调整——如果你只需要百米级精度delta可以放宽到 50。weight_decay是 L2 正则防止过拟合轨迹预测数据量通常不大这个参数基本都要开。学习率从1e-3起步loss 不降时再按 0.1 倍衰减而不是一开始就调小。4.3 训练/验证/测试划分的隐藏问题轨迹预测里最忌讳随机打乱后划分数据集。同一条航迹的相邻样本高度相似随机划分会导致训练集和验证集包含几乎相同的轨迹段验证 loss 虚低模型实际泛化能力被高估。正确做法是按航迹划分整条航迹放进训练集或验证集不允许同一条航迹的数据横跨两个集合。trajectory_ids df[flight_id].unique() np.random.shuffle(trajectory_ids) train_ids trajectory_ids[: int(0.8 * len(trajectory_ids))] eval_ids trajectory_ids[int(0.8 * len(trajectory_ids)) :]这个划分策略直接决定了模型能不能“记住”航迹。如果你发现训练 loss 很低、验证 loss 也低但实飞数据预测效果很差先检查是不是同一条航迹被切进了训练和验证两边。我习惯在划分后打印一下训练集和验证集里重叠的flight_id数量为零才继续往下走。5. 避坑指南五个让轨迹预测翻车的常见错误5.1 经纬度当平面坐标距离直接失真现象模型训练收敛很快loss 降到很低但画出来的预测轨迹和真实轨迹明显偏离误差在纬度高的区域尤其严重。原因预处理阶段直接把经度、纬度作为 x、y 特征喂给模型没做任何投影转换。在中纬度地区1 度经度对应的地面距离大约是 1 度纬度的一半模型学到的“距离”概念完全是畸形的速度特征也跟着错。解决所有经纬度数据先统一转成 UTM 或局部等距投影坐标以米为单位作为模型输入。如果你用的是简化版等距圆柱投影务必限制数据覆盖范围不超过 100 公里跨越更大范围就切换到 UTM。5.2 时间戳乱序导致时序倒置现象模型训练过程中 loss 忽高忽低完全无法收敛到稳定区间。原因原始数据里同一航迹的报文按接收时间排序但地面站可能有多个通道数据落盘顺序和实际点位时间不一致。直接按行序号当时间轴样本里的轨迹点时序是乱的。解决每个轨迹样本构造前先按timestamp排序。排序后再做一次单调性检查遇到个别乱序点直接删除而不是靠模型去适应噪声排序。5.3 速度特征里混入跳变点现象预测轨迹在速度突变处出现明显的抖动位置误差集中在转弯切弯或起降阶段。原因尽管做了轨迹清洗少数点仍然存在异常定位跳变。这些点在计算速度时表现为瞬时速度突然翻倍或减半模型把这种异常模式当成了正常特征。解决速度特征计算后加一道中值滤波窗口。窗口长度取 5 个采样点对速度序列做中值平滑剔除单点异常。注意滤波操作只在训练前做预测阶段用同一条滤波管道处理新数据否则训练和推理分布不一致。5.4 航向 0/360 度跳跃导致转弯处发散现象模型在水平直线段表现不错一到转弯段预测轨迹出现“Z”字形抖动严重时直接逆转向心。原因航向角直接作为数值特征360 度转到 0 度时数值发生断崖式跳变模型无法理解这种“平滑的突变”。解决航向角做 sin/cos 编码后再入模。如果不想改特征工程也可以在模型输出后用姿态估计替代航向角变化率但这样会引入额外误差源不如在特征层直接解决。5.5 起飞/降落段被当作异常直接删除现象模型对巡航段预测精度尚可但对爬升和进近段基本失效。原因数据清洗阶段把高度快速变化、速度跨度大的轨迹段当成异常点删除导致训练集里几乎没有起降段样本。解决起降段和巡航段分开处理。计算特征时保留完整轨迹训练时按阶段分别评估如果你的业务场景就是巡航段预测可以删掉起降段但如果要覆盖全飞行阶段务必保留这些样本并按阶段做数据增强。6. 验证与进阶用点迹回放和逐点误差定位模型死角模型训练完不要只看整体 RMSE轨迹预测的误差在空间上分布极不均匀。我会把验证集里每条航迹的预测结果和真实轨迹展开成逐点对比找出误差显著偏大的区域再回到数据里确认这些区域发生了什么。具体做法是写一个评估脚本输出每个预测点的误差、该点对应的真实位置和当前航向角变化率然后按误差分位数排序。def pointwise_eval(truth, pred, headings): truth/pred: 都是 (num_samples, horizon, 2) 的坐标数组 headings: 每个样本最后一个真实点的航向变化率用于区分转弯/直线段 err np.linalg.norm(truth - pred, axis-1) # 逐点距离误差 # 按航向变化率绝对值分组 turn_mask np.abs(headings) np.radians(3.0) straight_mask ~turn_mask turn_rmse np.sqrt(np.mean(err[turn_mask]**2)) straight_rmse np.sqrt(np.mean(err[straight_mask]**2)) return turn_rmse, straight_rmseturn_rmse和straight_rmse的差距是模型健康度的核心指标。如果转弯段误差是直线段的 3 倍以上问题大概率出在特征层面而非模型结构如果两者都高优先检查数据预处理是否有系统性偏差。点迹回放也很直接把真实轨迹和预测轨迹画在同一张图上按时间顺序逐点播放你能直观看到模型在转弯入口是提前切弯还是延后进弯这两个现象分别对应特征中航向变化率权重过高和过低。从经验来看轨迹预测的提升空间通常不在模型结构而在于你花了多少功夫在数据清洗和特征细节上。坐标投影、时间重采样、航向编码这三件事做扎实LSTM 就能出一个相当可用的结果反过来模型再复杂也救不回来一个脏数据基础。有一件事我每次训练新数据集都会强制做一遍把验证集里误差最大的 10 条航迹单独拎出来画图看它们失败的原因。其中绝大部分是数据问题——某一段轨迹点缺失、某一时刻加速度异常、或者某两个特征之间出现了数据泄漏。从那以后我训练任何轨迹预测模型都会在训练前先打印一份各航迹的点数分布和时间跨度点数少于 50 的航迹直接剔除跨度超过 2 小时的航迹分段处理避免让极端长度干扰模型的时间尺度感知。这套代码包能不能发挥最大价值取决于你愿意在数据检查上花多少耐心模型本身反而是最不需要担心的部分。希望这篇拆解能帮你少走几次弯路。本文还有配套的精品资源点击获取