简介:这份PDF文档围绕基于循环神经网络的航班延误预测模型展开,面向民航运行管理、空管数据分析及机器学习应用方向的学习者与研究人员,帮助解决航班延误趋势预判与地面保障资源调配中的建模问题。资源包内仅含1个PDF文件,整体约1MB,内容以学术论文形式呈现,涵盖RNN与LSTM混合模型的算法原理、公式推导、模型设计及民航空管历史真实数据的应用思路。文中重点讲解了循环神经网络利用隐藏层状态参数记录时序特性、LSTM通过输入门、遗忘门和输出门构成的细胞单元缓解梯度消失与长期依赖问题,并给出模型在延误预测、特征自动提取及与大数据平台并行计算集成方面的优势。目前已有194人学习,适合希望将深度学习落地到空管与航班延误预测场景的读者参考,可从中获取模型结构设计、算法选型依据与后续研究方向等具体内容。
1. 航班延误预测这件事,为什么 RNN 比传统模型更值得押注
做航班延误预测的人,大多经历过同一个尴尬:用逻辑回归或 XGBoost 把天气、航司、机场流量这些特征一股脑喂进去,离线 AUC 看着还行,一上线就翻车。原因不复杂——航班延误不是独立事件,它沿着时间轴和航线网络传染。今天下午广州的一场雷雨,会让晚上北京飞上海的航班跟着晚点,而传统机器学习模型把每条样本当成孤立点,天然看不见这种时序依赖。
循环神经网络(RNN)以及它的实用变体 LSTM,正是为「序列前后有关联」这类问题设计的。把某条航线过去若干小时的延误状态、天气、流量按时间排成序列喂进去,模型能学到「延误正在累积」还是「正在消化」这种动态趋势。这篇笔记就围绕「基于循环神经网络的航班延误预测模型」这个方向,把数据怎么组织、LSTM 怎么搭、参数怎么调、坑在哪,一步步讲清楚。适合已经会 Python 和基础机器学习、想把这个方向真正跑起来的从业者,也适合拿它做课程设计选题的同学。
2. 把航班数据整理成 RNN 能吃的序列:字段、滑窗与标签
RNN 和普通机器学习最大的区别在于输入形状。传统模型要的是一行一个样本、一列一个特征;RNN 要的是「样本 × 时间步 × 特征」的三维张量。所以做这个模型,一半工作量在数据工程上,而不是在搭网络上。这一章先把数据从原始表变成能喂进 LSTM 的序列,这是整个方案能不能成立的地基。
2.1 先想清楚一条序列代表什么
最常见的做法是「按航线 + 按天」切序列。比如固定「北京首都 → 上海虹桥」这条航线,把每天同一时段的航班聚合成一个时间步,一天一个点,连续取过去 T 天,预测第 T+1 天该时段的延误情况。这样一条序列就代表「这条航线最近一段时间的运行状态演变」。
另一种做法是「按机场」切,把某机场每小时的进出港延误率作为时间步。两种都行,区别在于你关心的是航线级还是机场级。我一般倾向航线级,因为延误的传播在具体航段上更可解释,特征也更好对齐。
时间步长(T)是第一个要定的参数。太短学不到趋势,太长会引入太多噪声且训练变慢。经验值:按天聚合取 7~14,按小时聚合取 12~24。可以先从 12 起步,后面用验证集调。
2.2 关键字段和它们的处理方式
原始航班数据通常包含计划起飞时间、实际起飞时间、延误分钟数、出发/到达机场、航司、机型、天气等。下面这张表是我实际会保留并处理的字段清单:
| 字段 | 类型 | 处理方式 | 说明 |
|---|---|---|---|
| 计划起飞时间 | 时间 | 解析后提取小时、星期 | 周期性特征用 sin/cos 编码 |
| 实际延误分钟 | 数值 | 作为核心特征和标签来源 | 需处理负值(提前起飞) |
| 出发/到达机场 | 类别 | 目标编码或嵌入 | 高基数别直接 one-hot |
| 航司 | 类别 | one-hot 或嵌入 | 基数低,one-hot 够用 |
| 天气类别 | 类别 | one-hot | 晴/雨/雪/雾等 |
| 历史延误率 | 数值 | 滚动统计 | 注意只能用预测点之前的数据 |
标签的定义要提前定死。常见两种:一是回归任务,直接预测延误分钟数;二是分类任务,把「延误超过 15 分钟」标为 1,否则 0。分类任务更稳,评估也直观,新手建议先做分类。
2.3 滑动窗口构造序列的代码
下面这段是把按天聚合后的表转成三维序列的核心逻辑。假设df已经按航线和日期排好序,每行是一天一条记录。
import numpy as np import pandas as pd def build_sequences(df, feature_cols, target_col, time_steps=12): """ 把按时间排好序的 DataFrame 转成 RNN 需要的三维序列 df: 已按 [route, date] 排序 feature_cols: 特征列名列表 target_col: 标签列名 time_steps: 回看的时间步数 """ X, y = [], [] # 按航线分组,避免不同航线的序列被拼在一起 for route, group in df.groupby('route'): group = group.sort_values('date').reset_index(drop=True) feat = group[feature_cols].values label = group[target_col].values # 从第 time_steps 个点开始,每个点取前 time_steps 步做输入 for i in range(time_steps, len(group)): X.append(feat[i - time_steps:i]) y.append(label[i]) return np.array(X), np.array(y) # 调用示例 feature_cols = ['delay_min', 'hour_sin', 'hour_cos', 'is_weekend', 'weather_rain', 'weather_snow', 'hist_delay_rate'] X, y = build_sequences(df, feature_cols, 'is_delayed', time_steps=12) print(X.shape) # (样本数, 12, 7)逻辑说明:外层按route分组是关键,如果不分组,序列会跨越不同航线,模型学到的就是噪声。内层用i - time_steps:i取窗口,i位置的标签作为预测目标,保证输入永远在标签之前,不会数据穿越。
参数说明:time_steps控制回看长度,先用 12;feature_cols里delay_min是连续值,其余是编码后的类别或周期特征。注意hist_delay_rate这类滚动统计特征,计算时必须用shift(1)之类的操作,确保只用到当前时刻之前的信息,否则离线指标会虚高,上线直接崩。
提示:序列构造完先打印几个样本肉眼看一下,确认时间顺序没乱、没有把未来数据混进来。这一步省下的时间,比后面调参省的多。
3. 搭一个能跑通的 LSTM 预测模型:结构、损失与训练循环
数据准备好之后,模型本身其实不复杂。这一章给出一个最小可用的 LSTM 结构,并解释每一层为什么这么设。很多人一上来就堆深层网络,结果在小数据集上过拟合得厉害,反而不如一个单层 LSTM 稳。
3.1 为什么选 LSTM 而不是朴素 RNN
朴素 RNN 在反向传播时会有梯度消失问题,序列一长,早期时间步的信息就传不到后面,学不到长距离依赖。航班延误恰恰需要看过去一两周的趋势,所以朴素 RNN 基本不够用。LSTM 通过输入门、遗忘门、输出门控制信息流动,能记住更长的模式,是这类任务的主流选择。GRU 是更轻量的替代,参数少、训练快,数据量不大时可以优先试 GRU。
结构上,我一般用「一层 LSTM + 一层全连接」就够。LSTM 输出取最后一个时间步的隐藏状态,接一个 Dense 做二分类。层数越多越容易过拟合,除非你有几十万条以上序列。
3.2 用 PyTorch 定义模型
import torch import torch.nn as nn class DelayLSTM(nn.Module): def __init__(self, input_size, hidden_size=64, num_layers=1, dropout=0.3): super().__init__() # batch_first=True 让输入形状为 (batch, seq, feature) self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout if num_layers > 1 else 0 ) self.dropout = nn.Dropout(dropout) self.fc = nn.Linear(hidden_size, 1) # 二分类输出一个 logit def forward(self, x): # out: (batch, seq, hidden), (h_n, c_n) 是最后时间步的状态 out, (h_n, c_n) = self.lstm(x) last = out[:, -1, :] # 取最后一个时间步 last = self.dropout(last) logit = self.fc(last) return logit.squeeze(-1)逻辑说明:batch_first=True让输入维度顺序符合直觉,避免转置搞错。取out[:, -1, :]是因为我们要用整段序列的最终状态做预测。num_layers=1时 dropout 不生效,这是 PyTorch 的行为,别被坑。
参数说明:input_size等于特征列数(上面例子是 7);hidden_size从 64 起步,数据多可以加到 128;dropout0.2~0.5 之间调,过拟合就加大。num_layers建议先 1,效果不够再加到 2。
3.3 训练循环和类别不平衡处理
航班延误数据通常正负样本不平衡,延误的是少数。直接训练模型会偏向预测「不延误」。两个常用手段:损失函数用带权重的BCEWithLogitsLoss,或者对少数类过采样。
from torch.utils.data import DataLoader, TensorDataset # 转成张量 X_tensor = torch.tensor(X, dtype=torch.float32) y_tensor = torch.tensor(y, dtype=torch.float32) dataset = TensorDataset(X_tensor, y_tensor) loader = DataLoader(dataset, batch_size=64, shuffle=True) model = DelayLSTM(input_size=X.shape[2]) # pos_weight 设为 负样本数/正样本数,缓解不平衡 pos_weight = torch.tensor([(y == 0).sum() / max((y == 1).sum(), 1)]) criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) for epoch in range(30): model.train() total_loss = 0 for xb, yb in loader: optimizer.zero_grad() logit = model(xb) loss = criterion(logit, yb) loss.backward() # 梯度裁剪,防止 LSTM 梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() total_loss += loss.item() print(f"epoch {epoch}, loss {total_loss / len(loader):.4f}")逻辑说明:pos_weight让正类(延误)的损失被放大,模型不敢忽略少数类。梯度裁剪是训练 RNN 类模型的标准操作,max_norm=1.0是常用值,能有效防止 loss 突然变 NaN。
参数说明:batch_size64 是稳妥起点,显存够可以加大;lr=1e-3配 Adam 是常规组合,loss 震荡就降到 5e-4;epoch看验证集早停,别死磕 30 轮。
注意:训练时一定要划出验证集,并且按时间切分而不是随机切分。随机切分会让未来数据混进训练集,指标虚高,这是时序任务最常见的翻车点。
4. 评估与调参:别被虚高的准确率骗了
模型能跑通只是第一步,能不能用要看评估。航班延误预测里,准确率(accuracy)几乎没意义,因为不延误样本占多数,全猜「不延误」也能有七八十的准确率。真正要看的是召回率、F1 和 AUC,尤其是对延误类的召回。
4.1 该盯哪几个指标
| 指标 | 含义 | 为什么重要 |
|---|---|---|
| 召回率(延误类) | 真实延误中被抓出来的比例 | 漏报延误代价高 |
| 精确率(延误类) | 预测延误中真的延误的比例 | 误报太多没人信 |
| F1 | 精确率和召回率的调和 | 综合权衡 |
| AUC | 排序能力 | 不依赖阈值,看整体区分度 |
实际业务里,如果模型是给调度做预警,召回率优先,宁可多报也别漏报;如果是给旅客推送,精确率更重要,误报会引发投诉。先想清楚用途,再定阈值。
4.2 阈值不是 0.5,要按业务调
模型输出的是概率,0.5 只是默认切分点。把验证集上的预测概率排序,画一条精确率-召回率曲线,找到满足业务要求的点。比如要求召回率不低于 0.8,就在曲线上找对应阈值。
from sklearn.metrics import precision_recall_curve, f1_score import numpy as np model.eval() with torch.no_grad(): probs = torch.sigmoid(model(X_val_tensor)).numpy() precision, recall, thresholds = precision_recall_curve(y_val, probs) # 找召回率 >= 0.8 时 F1 最高的阈值 best_thr, best_f1 = 0.5, 0 for p, r, t in zip(precision[:-1], recall[:-1], thresholds): if r >= 0.8: f1 = 2 * p * r / (p + r + 1e-8) if f1 > best_f1: best_f1, best_thr = f1, t print(f"最佳阈值 {best_thr:.3f}, F1 {best_f1:.3f}")逻辑说明:precision_recall_curve返回不同阈值下的精确率和召回率,遍历找到满足召回约束且 F1 最高的点。注意precision和recall比thresholds多一个元素,切片时要去掉最后一个。
参数说明:召回约束 0.8 是示例,按业务改。阈值定下来后要固化到推理代码里,别每次重新算。
4.3 超参数怎么调才不浪费时间
要调的其实就几个:time_steps、hidden_size、dropout、lr。我的顺序是先固定网络结构,调time_steps(8/12/16 各试一遍),再调hidden_size(32/64/128),最后微调dropout和lr。每次只动一个,记录验证集 F1。别用网格搜索一把梭,时序数据训练慢,网格搜索性价比极低。
5. 航班延误预测模型避坑清单:五个真实踩过的坑
这一章是我自己在这个方向上翻车过的地方,按「现象 → 原因 → 解决」写,能帮你少走弯路。
5.1 离线指标漂亮,上线一塌糊涂
现象:验证集 AUC 0.9,部署后实际预警准确率惨不忍睹。 原因:数据穿越。滚动统计特征(如历史延误率)在构造时用了包含当前时刻的数据,或者随机切分了训练/验证集,导致未来信息泄漏。 解决:所有统计类特征统一用shift(1)之后再滚动;训练/验证/测试严格按时间切分,比如前 70% 时间训练、中间 15% 验证、最后 15% 测试。
5.2 模型永远预测「不延误」
现象:训练 loss 一直降,但召回率接近 0,混淆矩阵里正类全错。 原因:类别不平衡,正样本太少,模型学到「全猜负类」就能把 loss 压下去。 解决:损失函数加pos_weight,或对正类过采样;同时把评估指标从准确率换成召回率和 F1,否则你根本发现不了这个问题。
5.3 loss 突然变成 NaN
现象:训练到一半 loss 变 NaN,参数全废。 原因:LSTM 梯度爆炸,学习率偏大时尤其容易发生。 解决:加梯度裁剪clip_grad_norm_(max_norm=1.0);把学习率降到 5e-4 或 1e-4;检查输入特征有没有异常大值,做标准化。
5.4 序列跨越了不同航线
现象:模型学不动,验证集指标和随机猜差不多。 原因:构造序列时没按航线分组,一条序列里混了不同航线的数据,时间步之间没有真实关联。 解决:groupby('route')之后再滑窗,确保每条序列来自同一条航线。机场级任务同理,按机场分组。
5.5 天气特征全是缺失值
现象:天气字段大量为空,模型对天气几乎不敏感。 原因:原始数据里天气是按机场和时段记录的,和航班表没对齐,直接 merge 后大量 NaN。 解决:先按「机场 + 小时」聚合天气,再和航班表对齐;缺失值用前向填充或单独设一个「未知」类别,别直接填 0,0 在天气编码里可能代表「晴」,会误导模型。
6. 让模型真正可用:多步预测与在线更新的落地技巧
单步预测(预测下一个时间点)跑通之后,真正有价值的是多步预测——提前几小时甚至一天给出延误预警,业务才有反应时间。多步预测有两种做法:直接多输出(一次预测未来 N 步)和滚动预测(把预测值喂回输入,一步步推)。直接多输出更稳,误差不会累积;滚动预测实现简单但误差会滚雪球。我一般用直接多输出,把最后一层改成输出 N 个值。
class MultiStepLSTM(nn.Module): def __init__(self, input_size, hidden_size=64, horizon=6): super().__init__() self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True) self.fc = nn.Linear(hidden_size, horizon) # 一次输出未来 horizon 步 def forward(self, x): out, _ = self.lstm(x) return self.fc(out[:, -1, :]) # (batch, horizon)horizon就是你要提前预测几个时间步,比如按小时聚合时horizon=6表示提前 6 小时。损失函数换成BCEWithLogitsLoss的多标签版本,或者对每一步分别算 loss 再平均。
在线更新这块,别指望模型一劳永逸。航班运行有强季节性,冬夏航班时刻表不同,模型隔一段时间就会漂移。我的习惯是每周用最近几个月的数据增量微调一次,学习率调小(1e-4),只训练几轮,既跟得上变化又不会把旧知识冲掉。同时监控线上召回率,一旦连续几天低于阈值就触发重训。
还有一个容易被忽略的点:把预测结果和实际结果做闭环记录。每次预测都存下输入特征、预测概率、实际结果,攒够了就是下一轮训练的高质量数据,也能用来分析模型在哪些场景下系统性偏差。这个习惯坚持下来,模型迭代会越来越顺。
最后说个我自己的教训:早期我总想一步到位搞个复杂模型,结果调了两周还不如同事一个单层 LSTM 加好特征。这个方向真正拉开差距的是数据质量和特征工程,不是网络有多深。先把序列构造、防穿越、类别不平衡这三件事做扎实,再谈模型结构。希望帮到你。
本文还有配套的精品资源,点击获取