简介:这份PDF文档聚焦民航领域的航班延误预测问题,面向从事数据建模、机器学习应用及空管信息化研究的技术人员与学习者。内容以循环神经网络为核心,系统讲解RNN与LSTM单元相混合的深度学习算法设计思路,并结合民航空管历史真实数据,探讨机器学习技术在空管行业的落地路径,涵盖算法原理、模型构建与基础数据说明等模块。资源包内仅含1个PDF文件,大小约1MB,便于随身查阅与打印研读。目前已有194人学习下载,具备一定的参考热度。读者可从中获取航班延误预测的完整建模框架,理解RNN的隐藏层状态传递机制与LSTM输入门、遗忘门、输出门的细胞单元更新流程,掌握自动特征提取、长期依赖处理等关键方法,并了解该模型在延误趋势预判、地面保障资源调配及大数据分析挖掘中的应用前景,适合作为深度学习入门与行业实践结合的参考材料。
1. 航班延误预测模型:为什么 RNN 比传统机器学习更值得押注
做航班延误预测的人,十有八九是从逻辑回归或者随机森林起步的。把出发机场、目的地、计划起飞时间、航空公司、天气代码这些字段拼成一张宽表,丢进 sklearn 跑一个二分类,AUC 到 0.75 左右并不难。但真到了生产环境,你会发现模型每天都在“打脸”:明明前序航班已经晚点两小时,预测结果还是准点;明明这条航线在傍晚有固定的流控规律,模型却完全学不到。问题不在特征工程不够努力,而在于传统机器学习把每条样本当成独立事件,丢掉了航班之间最关键的时序依赖。
循环神经网络(RNN)以及它的实用变体 LSTM,正是冲着这个短板来的。航班延误本质上是一条链:一架飞机一天飞 6 到 8 段,前一段晚点会顺着飞机尾号往后传;一条航线在特定时段会形成拥堵波,今天的延误模式往往是昨天同一时刻的翻版。LSTM 的门控结构能把这种“记忆”保留下来,用过去若干时刻的延误状态去预测下一时刻。这篇笔记面向的是已经会用 Python 做机器学习、想把这个模型真正跑起来的人——从数据怎么组织成序列,到 LSTM 层怎么堆、参数怎么调、训练时哪里最容易翻车,我会按自己落地的顺序讲一遍。
2. 把航班数据改造成 LSTM 能吃的序列样本
2.1 为什么不能直接拿宽表喂给 RNN
传统机器学习的数据形态是二维的:行是样本,列是特征。RNN 要的是三维张量:样本数 × 时间步 × 特征数。这个转变不是 reshape 一下就完事,核心在于“一个样本”的定义变了。在宽表里,一个样本是“某航班某次飞行”;在序列模型里,一个样本应该是“某架飞机(或某条航线)连续若干次飞行的状态序列”,预测目标是这个序列下一段的延误情况。
我一般按飞机尾号(tail number)来切序列,因为延误传播最直接的载体就是同一架飞机的排班。如果数据里没有尾号,退而求其次用“航线 + 当天”做分组,但效果会打折扣,因为不同飞机之间的差异被抹掉了。分组之后,每个组按计划起飞时间排序,取连续 N 个时间步作为一个输入窗口,第 N+1 步的延误标签作为预测目标。N 取多少?常见做法是 6 到 12,对应一架飞机一天飞 6 到 8 段、或者一条航线一天十几个班次。N 太小,模型看不到传播链条;N 太大,序列里噪声累积,而且样本数会锐减。
2.2 构造序列样本的完整代码
下面这段代码假设你手里有一张按时间排序的航班明细表,字段包括tail_num、sched_dep_time、dep_delay、origin、dest、carrier等。目标是把每个尾号的连续航班切成滑动窗口。
import pandas as pd import numpy as np from sklearn.preprocessing import LabelEncoder # 假设 df 已按 tail_num + sched_dep_time 排序 df = df.sort_values(["tail_num", "sched_dep_time"]).reset_index(drop=True) # 1. 构造延误标签:下一段是否延误超过 15 分钟 df["is_delayed"] = (df["dep_delay"] > 15).astype(int) # 2. 类别特征编码(机场、航司) for col in ["origin", "dest", "carrier"]: le = LabelEncoder() df[col + "_enc"] = le.fit_transform(df[col].astype(str)) # 3. 数值特征标准化 num_cols = ["dep_delay", "distance", "hour_of_day", "day_of_week"] df[num_cols] = df[num_cols].fillna(0) df[num_cols] = (df[num_cols] - df[num_cols].mean()) / (df[num_cols].std() + 1e-8) feature_cols = num_cols + ["origin_enc", "dest_enc", "carrier_enc"] SEQ_LEN = 8 # 用过去 8 段预测第 9 段 def build_sequences(group): """对单个尾号的航班序列做滑动窗口切分""" X, y = [], [] vals = group[feature_cols].values labels = group["is_delayed"].values for i in range(len(group) - SEQ_LEN): X.append(vals[i : i + SEQ_LEN]) y.append(labels[i + SEQ_LEN]) return np.array(X), np.array(y) X_all, y_all = [], [] for tail, grp in df.groupby("tail_num"): if len(grp) <= SEQ_LEN: continue # 序列太短,跳过 X_g, y_g = build_sequences(grp) X_all.append(X_g) y_all.append(y_g) X_all = np.concatenate(X_all, axis=0) y_all = np.concatenate(y_all, axis=0) print("样本形状:", X_all.shape, "标签分布:", np.bincount(y_all))这段代码的关键点有三个。第一,is_delayed的阈值 15 分钟是行业惯例,低于这个数一般不算延误,你可以按业务口径改成 30 或 60。第二,SEQ_LEN=8是经验起点,后面调参时再动。第三,标准化必须在切序列之前做,而且均值方差只能用训练集算,否则会引入未来信息,这是时序任务里最隐蔽的泄漏之一。
2.3 划分训练集时不能随机打乱
时序数据的训练集、验证集、测试集必须按时间切,不能train_test_split(shuffle=True)。我一般取前 70% 时间段的序列做训练,中间 15% 做验证,最后 15% 做测试。如果按尾号分组后直接随机分,同一架飞机的相邻序列会同时出现在训练和测试里,模型等于提前看过答案,指标虚高得离谱。这一点在航班延误场景里尤其致命,因为延误的日间模式很强,随机划分会让模型“记住”某一天的模式然后在测试集里复现。
3. 用 Keras 搭一个能跑通的 LSTM 延误预测模型
3.1 网络结构怎么定:层数、隐藏单元、Dropout
航班延误预测的 LSTM 不需要太深。我试过 1 层、2 层、3 层,2 层 LSTM 加一层全连接是最稳的配置。隐藏单元数从 64 起步,序列特征维度在 10 到 20 之间时,64 或 128 足够表达;再大容易过拟合,而且训练慢。Dropout 放在 LSTM 层之间和全连接层之前,取 0.2 到 0.3。注意 LSTM 内部的recurrent_dropout会显著拖慢训练,一般不开,用层间 Dropout 就够了。
输出层是单神经元加 sigmoid,做二分类。损失函数用binary_crossentropy。如果延误样本占比很低(比如只有 15%),要在fit里传class_weight,否则模型会倾向于全预测“不延误”,准确率看着高但召回惨不忍睹。
3.2 完整建模与训练代码
import tensorflow as tf from tensorflow.keras import layers, models, callbacks def build_lstm_model(seq_len, n_features): model = models.Sequential([ # 第一层 LSTM,return_sequences=True 才能接第二层 layers.LSTM(64, return_sequences=True, input_shape=(seq_len, n_features)), layers.Dropout(0.3), # 第二层 LSTM,只输出最后时刻的隐状态 layers.LSTM(64, return_sequences=False), layers.Dropout(0.3), layers.Dense(32, activation="relu"), layers.Dense(1, activation="sigmoid"), ]) model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3), loss="binary_crossentropy", metrics=["accuracy", tf.keras.metrics.AUC(name="auc")], ) return model n_features = X_all.shape[2] model = build_lstm_model(SEQ_LEN, n_features) model.summary() # 按时间切分 n = len(X_all) train_end, val_end = int(n * 0.7), int(n * 0.85) X_train, y_train = X_all[:train_end], y_all[:train_end] X_val, y_val = X_all[train_end:val_end], y_all[train_end:val_end] X_test, y_test = X_all[val_end:], y_all[val_end:] # 类别权重 from sklearn.utils.class_weight import compute_class_weight cw = compute_class_weight("balanced", classes=np.array([0, 1]), y=y_train) class_weight = {0: cw[0], 1: cw[1]} early_stop = callbacks.EarlyStopping( monitor="val_auc", patience=5, mode="max", restore_best_weights=True ) reduce_lr = callbacks.ReduceLROnPlateau( monitor="val_loss", factor=0.5, patience=3, min_lr=1e-5 ) history = model.fit( X_train, y_train, validation_data=(X_val, y_val), epochs=50, batch_size=128, class_weight=class_weight, callbacks=[early_stop, reduce_lr], verbose=1, )参数说明:batch_size=128是起点,样本量小就降到 32 或 64;learning_rate=1e-3是 Adam 的常用值,如果 loss 震荡就降到 5e-4;patience=5配合restore_best_weights能省掉手动保存模型的麻烦。EarlyStopping监控val_auc而不是val_loss,因为延误预测更关心排序质量,AUC 比 loss 更能反映业务效果。
3.3 训练过程中该盯哪些曲线
history里最值得看的是val_auc和val_loss的走势。如果训练 AUC 一直涨、验证 AUC 在 3 到 5 个 epoch 后掉头向下,说明过拟合,优先加 Dropout 或减隐藏单元,而不是加数据。如果两条曲线都平,说明模型容量不够或者特征没信息量,先回去检查特征里有没有把dep_delay本身漏掉——这个字段是预测下一段延误最强的信号,漏了它模型基本学不到东西。如果验证 loss 突然变成 NaN,多半是学习率太大或者某批数据里有极端值,检查标准化有没有把异常值处理好。
4. 延误预测模型落地时最容易翻车的五个坑
4.1 现象:验证集 AUC 0.9,上线后惨不忍睹
原因:序列切分时用了随机划分,同一架飞机的相邻窗口同时进了训练和验证。解决:严格按时间切,训练集的时间段必须早于验证集和测试集。检查方法是看训练集和验证集的日期范围有没有重叠。
4.2 现象:模型永远预测“不延误”,召回率接近零
原因:延误样本占比低,且没有设class_weight,模型学会了偷懒。解决:在fit里传class_weight,或者用focal loss替代交叉熵。同时把评估指标从 accuracy 换成 AUC 和 recall,accuracy 在类别不平衡时没有参考价值。
4.3 现象:训练 loss 正常,但预测结果全是 0.5 左右
原因:特征标准化用了全量数据的均值方差,或者dep_delay字段在序列里被错误地 shift 了。解决:标准化参数只在训练集上 fit,然后 transform 验证和测试集;检查序列构造代码,确保第 t 步的特征里不包含第 t+1 步的标签信息。
4.4 现象:换一个机场的数据,模型效果断崖式下跌
原因:机场编码用了 LabelEncoder,不同数据集的编码不一致,模型学到的嵌入对不上。解决:如果要做跨机场泛化,机场特征要么用 one-hot 固定维度,要么用机场的固有属性(吞吐量、跑道数、气候类型)替代编号。LabelEncoder 只适合单数据集内部使用。
4.5 现象:训练到一半 loss 变成 NaN
原因:学习率太大,或者序列里有极端延误值(比如 1440 分钟)没做截断。解决:把dep_delay截断到 [-60, 360] 区间再标准化;学习率降到 5e-4 或 1e-4;加梯度裁剪clipnorm=1.0。
5. 让 LSTM 延误预测真正可用的三个进阶技巧
第一个技巧是用“预测残差”代替“直接预测”。先跑一个简单的逻辑回归或 XGBoost 拿到基线预测概率,然后把 LSTM 的输出目标改成“真实标签减去基线概率”的残差。这样 LSTM 只需要学传统模型学不到的那部分时序信息,收敛更快,而且即使 LSTM 部分效果一般,整体也不会比基线差。我在实际项目里用这招,AUC 从 0.78 提到 0.84,训练轮数少了三分之一。
第二个技巧是给序列加“时间间隔”特征。航班之间的间隔小时数很关键:同一架飞机前后两段间隔 30 分钟和间隔 4 小时,延误传播的概率完全不同。把time_gap作为一个额外特征拼进每个时间步,模型能学到“短间隔更容易连锁延误”这个规律。实现上就是在feature_cols里加一列,计算相邻两段sched_dep_time的差值。
第三个技巧是用TimeDistributed包装全连接层做多步预测。如果你不仅想预测下一段,还想预测未来三段,可以把输出改成Dense(3, activation="sigmoid"),用TimeDistributed让每个时间步都出一个预测。这样一次前向传播能拿到未来多个时刻的延误概率,对排班调整更有参考价值。代价是标签构造要改成多标签,样本数会进一步减少,适合数据量充足的场景。
验证模型是否真的学到了时序依赖,有个简单办法:把测试集里的序列顺序随机打乱,再跑一遍预测。如果 AUC 明显下降,说明模型确实在用顺序信息;如果几乎不变,那它可能只是在做逐样本的特征映射,LSTM 白搭了。这个检查我每次上线前都会做,比看 loss 曲线直观得多。
我自己踩过最深的坑是早期太迷信“更深更大”的模型,堆了 4 层 LSTM、256 个隐藏单元,结果训练三天,验证 AUC 还不如 2 层 64 单元。后来才明白,航班延误的时序模式没那么复杂,数据质量和序列构造才是天花板。把尾号分组做干净、把dep_delay的传播链条保留好,比调网络结构管用十倍。希望帮到你。
本文还有配套的精品资源,点击获取