我第一次拿到真实版本的NGAFID航空数据集时,最直观的反应就是“这不就是一堆传感器时间戳加一个标签列吗”,可真正把它当成时序分类任务跑起来以后,才意识到这类数据有多磨人。噪声、采样率不稳、正负样本极度不均,哪一项单拿出来都够折腾几天。当时拿统计特征组合加传统分类器试了一轮,效果始终上不去;换成InceptionTime做端到端的时序分类之后,指标提升非常明显。这篇博文把整个研究过程里的核心思路、数据管线、模型原理、实验对比和踩坑记录都整理出来,给准备碰同类航空时序数据或者想用InceptionTime做分类的同学一条可以照着走的路径。
这项研究的背景很简单:NGAFID航空数据集记录了大量飞行阶段的多维时间序列,我要做的是根据一段连续记录判定该航段是否包含非正常状态。你不一定也做航空数据,但只要你的任务属于“多变量时间序列二分类/多分类”,这篇文章里的绝大多数内容都能平移过去。
1. 飞行数据分类难在哪:远不止拿训练集跑一个模型
很多刚接触这类项目的同学会把注意力全放在模型上,但真实数据一打开就发现,光是搞清楚字段含义、采样粒度和标签分布,可能就要耗掉整个项目三分之一的时间。
1.1 数据集底层长什么样
NGAFID航空数据集按我的理解,本质上是把一架飞机的飞行记录按时间轴展开,每个时间戳对应一组同步采样的传感器读数。常见的有水平速度、垂直速度、姿态角、升降率、油门杆位置、襟翼角度这类通道,也可能有发动机参数。采样率不一定固定,有的通道是几赫兹,有的能到几十赫兹,所以第一步不是直接丢进网络,而是先做时间对齐和重采样。
另一个关键点是数据是按航段组织的,不是一长条无限流。每个航段有起止时间,长度从几分钟到几小时不等。对于分类任务,我们通常不会把整段直接丢进去,而是切成固定长度的窗口。标签一般落在航段级别,比如“该航段有没有触发非正常状态”,这种标签粒度决定了你在构建样本时要非常小心,不能让一个窗口同时出现在训练集和测试集里。
在实际操作时,我会先把数据整理成三列结构:航段编号、时间戳、多通道数值。画几条通道的时间曲线快速看一眼,能比任何统计数字更快发现异常。比如某条通道的数值在某个时间段变成恒定不变的常数,基本可以判断是传感器断连或填入默认值。
1.2 我把研究任务收敛成异常状态识别
原始场景可能包罗万象,但要做可控的时序分类实验,必须先把任务边界划清楚。我这边的设定是二分类:给定一个固定长度的窗口,判断该窗口是否落在非正常状态时间段内。
所谓非正常状态,也不是随便拍脑袋定义的,而是参考数据集中标签列做了归类。比如陡降、大速率变向、超限参数等等,凡是触发了事件标记的航段都归为正样本,其余为负样本。
把任务收敛成二分类有几个好处。第一,评估标准清晰,后面选指标时不容易绕晕;第二,正负样本不均衡的问题会暴露得很早,逼着你从一开始就处理它而不是最后才补救;第三,后续如果要扩展成多分类,比如区分具体的告警类型,网络结构基本不用大改,只要换输出层就能继续训练。
1.3 为什么手工特征加浅层分类器总是不稳定
很多人面对这类数据的第一反应是提取统计特征,均值、方差、峰值、过零率,然后丢给逻辑回归或者随机森林。我也这么试过,效果嘛,勉强能用,但很不稳定。
问题出在飞行数据的局部模式上。非正常状态往往不是体现在整个窗口的均值抬升或方差变化,而是在某个时间段内出现特定的变化形态,比如一段快速俯冲,接着改平,再出现振荡。这种多阶段、时间相关的模式很难用几个全局统计量刻画。
另一个问题是特征工程的主观性太强。同一段数据,你认为是窗口第3000个采样点很重要,别人认为是第5000个,谁都没把握。手工特征的设计成本高,换一种数据分布就要重新折腾。比较下来,端到端的深度时序分类模型能把“特征提取”这件事交给卷积层自动完成,适应性明显更好。
2. 为什么选择InceptionTime:多尺度卷积和集成设计的底层逻辑
InceptionTime在时序分类社区里不是什么新模型,但在实际项目里见过它的人反而没有想象中多。它最打动我的地方在于,设计者把“从多尺度提取局部时序特征”这个思路执行得非常干净,同时在工程上特别好稳定复现。
2.1 Inception模块:多个卷积核覆盖不同长度的局部时间模式
Inception模块的核心是并行跑不同尺寸的卷积核。对时间序列来说,小卷积核比如5或10个采样点,只能看到很局部的急剧变化;大卷积核比如40或80个采样点,能捕捉稍长一点的趋势;如果序列很长,还可以用更大的核。多个尺度的感受野叠在一起,比单一卷积核更不容易漏掉某种时间尺度的模式。
具体实现上,我会把一维卷积核尺寸设置成[10, 20, 40, 80],这组数值基本覆盖了从短促脉冲到持续数秒变化的范围。当然核尺寸到底取多少,需要结合你的采样率来定。比如你每秒只有1个采样点,窗口长度才128点,那80的卷积核就太大了,需要缩小。
还要注意每个分支的padding策略。一维卷积默认会缩短序列长度,如果不做padding,四个分支拼接时长度就对不上。项目里统一使用same padding,让每个分支输出的时间维度和输入保持一致。
2.2 Bottleneck卷积与残差连接:不是为了炫技
Inception模块里通常会先加一个1×1卷积做bottleneck,作用是把输入通道数压下去,再进大卷积核。这一步的意义非常实际:航空数据通常有十几个通道,如果每个尺寸的卷积核直接作用于原始通道数,参数量会膨胀得很快,训练速度变慢,也容易过拟合。
我先用1×1卷积把通道数压到比如32维,再用不同尺寸的卷积核去提取特征,最后把所有分支拼回去。这样既保存了多尺度信息,又控制了计算量。
残差连接则是把模块输入直接加到输出上。别小看这个操作,它相当于给梯度开了一条高速公路。堆了个四五层卷积之后,没有残差连接的网络很容易在训练中途梯度消失,尤其是你的输出层又接了若干层全连接。加了残差之后,整个网络对学习率的敏感度大为降低,收敛稳定很多。
2.3 和LSTM、Transformer、普通ResNet相比选谁
我在这个项目里,一开始也纠结过用循环网络还是Transformer。搞了组小范围对比,最后留了三个模型做正式实验。
| 模型 | 对局部模式捕捉 | 训练速度 | 长序列支持 | 调参难度 |
|---|---|---|---|---|
| LSTM | 弱,容易记住噪声 | 慢 | 弱,容易遗忘 | 中 |
| Transformer | 强,但数据量少时易过拟合 | 较慢 | 强,但复杂度高 | 较高 |
| 普通1D ResNet | 中 | 快 | 中 | 低 |
| InceptionTime | 强 | 快 | 中 | 低 |
Transformer虽然理论能力强,但在NGAFID这种样本量不算巨大的数据集上,很容易学过头,在验证集上指标抖动很大。而且训练时间比InceptionTime长不少。LSTM对时间顺序敏感,但对前文的依赖在窗口较大时明显不足,实测下来反而比不过卷积。
InceptionTime给我的感觉像“一组有针对性的卷积核叠成的ResNet”,它把多尺度特征提取做到了极致,却不需要像Transformer那样靠海量数据喂养。单从“稳定出结果”这一点讲,它在工业级时序分类项目里的性价比非常高。
2.4 集成机制:为什么要拿五个模型取平均
InceptionTime原始设计中有一个很实用的细节:不是只训练一个模型,而是用不同初始化种子训练多个InceptionTime,最后把每个模型的输出概率取平均。
刚开始我觉得这只是锦上添花,直到我看了单个模型和集成模型在验证集上的表现差距。单个模型会时不时在某几个窗口上给出非常极端的预测,集成之后,这种偶然性的尖刺被压平了。原因是不同初始化下,网络在训练中陷入的局部最优不同,对噪声样本的“敏感点”也不同,平均之后相当于做了一个模型层面的投票。
实际实现时,不需要真去维护五个独立的复杂工程,只要循环训练五次,把每个模型的预测概率存下来,最后按航段ID做聚合平均就行。我一般保留五组随机种子,效果已经足够稳定,再往上加模型数量收益会递减。
3. 数据预处理与样本切分:真正决定成败的部分
模型部分很快就能讲清楚,反倒是数据处理这个环节需要反复打磨。在时序分类里,窗口怎么切、训练集和测试集怎么划,稍有不慎就会得到虚高的分数,上线后立刻现出原形。
3.1 缺失值、离群点与传感器噪声的处理方法
先处理缺失值。常见的选择是线性插值。如果缺失段太长,比如连续几十秒都没有有效值,线性插值会造出一堆缓慢变化的假数据,反而干扰模型。这时候我的做法是直接把该段剔除,宁可少一点训练样本,也不要往模型里喂合成的噪声。
离群点更隐蔽。我遇到过某条通道在个别时间戳数值直接跳到最大量程,接着又恢复正常,看起来像真实突变,实际上只是信号毛刺。简单的绝对阈值能挡掉一部分,但对飞行数据来说,某些通道的合法数值范围本来就很大,最省事的方案是用每个通道的移动中位数做平滑,再用原始值减平滑值的残差去识别突变。
对模型输入,我不建议做太重的滤波,因为过度平滑会把正常状态和异常状态之间尖锐的过渡区抹掉,反倒抹掉了关键特征。一个轻量的中值滤波或者不去管它,效果可能更好。
3.2 滑动窗口切片时的重叠策略
窗口长度我设置成固定的80秒,按100Hz采样就是8000个点。对一个航段来说,直接按不重叠滑动,样本量可能不够;但重叠过多,又会让相近窗口高度相关,训练集和测试集之间容易混入重复信息。
我用的策略是50%重叠。也就是每40秒切一个新窗口。这个比例在绝大多数场景下都够用,既不牺牲样本量,又不会让两个相邻窗口之间的差异过小。如果后面发现训练样本还是太少,才会把重叠率逐步提到75%,但一定要重新检查验证集指标,防止因为数据重复导致分数虚高。
窗口切出来后,还需要给每个窗口记录它来自哪个航段。别把这个信息丢掉,后面划分数据的时候全靠它。
3.3 按航段分组切分,而不是随机切窗口
这是整个预处理里最重要的一条经验:训练集、验证集、测试集的划分必须按航段进行,不能按窗口随机切。
原因很好理解。同一个航段里的相邻窗口本来就高度相似,如果某一个航段的窗口同时出现在训练集和测试集里,模型相当于见过这个航段的“邻居”,短期记忆就能帮它作弊。这种泄漏在随机切分时不一定会触发,但一旦触发,你拿到的测试分数就毫无参考价值。
实际操作中,我会先把所有航段编号做一次乱序,然后按比例分给训练集、验证集和测试集,再把属于每个航段的所有窗口归入对应集合。这样即便窗口重叠,泄漏也被限制在了同一份数据内部,测试集永远不会看到来自训练集航段的窗口。
3.4 归一化统计量必须在训练集上计算
时间序列分类的归一化最容易犯的错是:直接在整个数据集上算均值方差,然后把所有数据都减去这个均值。这等于让测试集的信息参与了训练过程。
正确做法是按通道分别计算训练集窗口的均值和标准差,然后把训练集、验证集、测试集都用同一组统计量去归一化。判断有没有做错,只需要看归一化后的测试集均值是不是大约等于零,如果明显偏离,说明你把测试集泄进去了。
还有一个小细节:如果不同航段之间的传感器基准漂移比较明显,全局按通道归一化不一定够。此时可以改成按航段内z-score,也就是每个航段用自己的均值方差做标准化。两种方案我都试过,哪个效果更好得看数据分布,但在NGAFID这种记录库里,按航段标准化通常更鲁棒。
4. 从baseline到InceptionTime:三组实验和一组参数分析
有了预处理好的窗口数据,接下来就是正式的对照实验。我不建议上来就直接调InceptionTime,先跑几个简单模型当参照系,否则你根本说不清楚最后的效果提升来自模型结构还是来自数据管线。
4.1 评价指标该怎么选
二分类最自然的指标是准确率,但这类数据正负样本严重不均衡,准确率没有参考价值。比如负样本占比80%,模型什么都不学,全预测为正常,准确率就已经有80%,看起来还不错,实际上一个异常都抓不住。
我采用三个指标组合:
- F1分数,看正样本的精确率和召回率的平衡情况。
- ROC-AUC,看模型把好坏样本分开的综合能力。
- 虚警率,也就是正常样本被抓成异常的占比。对航空告警场景,这个指标比准确率更能决定系统是否能被现场接受。
实际实验中,F1和ROC-AUC用来挑模型,虚警率用来做最终阈值选择的依据。
4.2 三个对比模型的具体配置
第一个模型是“统计特征 + 逻辑回归”。从每个窗口里提取均值、方差、最大值、最小值、过峰次数等20多个统计特征,喂给带L2正则的逻辑回归。这个模型训练秒级完成,但表达能力有限,主要用来当整个任务的下限参考。
第二个模型是单层LSTM。隐层128维,后面接全局平均池化和全连接输出。这个模型能真正看到时序内部的先后关系,但训练速度慢,而且对局部瞬态特征的捕捉不如卷积。
第三个模型就是InceptionTime。我采用了两层Inception模块叠加,bottleneck卷积设32维,卷积核尺寸[10, 20, 40, 80],输出端用全局平均池化加一个全连接层。训练时用Adam优化器,初始学习率1e-4,batch size 32,最多训练50轮,带早停策略。
4.3 三组模型的实测结果对比
下面这张表来自我这次研究里自行分配的一次完整实验,结果只代表当前这套预处理配置下的表现,不等于整个数据集的官方结论。
| 模型 | 输入 | F1 | ROC-AUC | 虚警率 |
|---|---|---|---|---|
| 统计特征 + 逻辑回归 | 手工特征 | 0.67 | 0.73 | 0.34 |
| 单层LSTM | 原始多通道窗口 | 0.73 | 0.80 | 0.27 |
| InceptionTime(单个模型) | 原始多通道窗口 | 0.80 | 0.87 | 0.18 |
| InceptionTime(5模型集成) | 原始多通道窗口 | 0.84 | 0.90 | 0.14 |
逻辑回归的下限作用很大,它告诉我这个任务至少做到什么程度;LSTM的微弱提升说明光靠序列先后性还不够,需要更丰富的局部模式;InceptionTime的明显优势则证实了多尺度卷积对这段数据是更匹配的归纳偏置。
集成之后F1从0.80涨到0.84,虚警率从0.18降到0.14,提升幅度非常稳定。代价只是训练次数变成五倍,但在推理阶段这五个模型可以并行计算,实际耗时增长很少。
4.4 超参数变化带来的直观感受
我在调参过程中发现几个规律。第一,学习率是最敏感的参数。初始学习率从1e-3改成1e-4,训练Loss比之前稳定很多。InceptionTime的多分支结构让梯度在拼接层特别容易波动,学习率稍大就会在这个位置出现不规则的震荡。
第二,batch size对结果的影响没有想象中大,但对显存影响非常直接。窗口是8000点,多通道输入下,batch size 64会吃掉不少显存,很多同学就是在这里被迫改成batch size 16。我最后稳定在32,F1基本不受影响。
第三,早停的耐心轮数设在8左右比较合适。这个任务的验证集指标不会逐轮单调上升,偶尔会有小回落,耐心太少会停在局部较差位置,耐心太大又容易过拟合。
5. 复现InceptionTime时的关键避坑记录:维度、收敛和虚警
这个模型看着结构简单,真正复现时还是会遇到很多细节问题。我把印象最深的几个坑列出来,每个都配上了我自己的排查过程和最终解法。
5.1 多分支拼接时的维度不匹配
第一次在PyTorch里实现Inception模块时,我顺理成章地对每个分支都做了padding,心想长度肯定没问题。结果一跑,报错信息显示拼接时长度对不上。
排查过程很折磨,最后发现是某个分支里顺手加了一个步长为2的最大池化,它把时间维减半了。同一个模块里,如果某个分支改变了时序长度,而其他分支没改,拼接必然炸。
解决办法是统一每个分支的输出长度。最大池化如果确实需要,就必须在池化前补回相应长度的padding,或者干脆只用全局平均池化在模型末端做降维,不在Inception模块内部做任何下采样。
这个教训放到整个网络里也一样:InceptionTime的核心结构要求所有并行分支长度一致,任何改一个分支不改其他分支的操作,都会引起维度灾难。
5.2 训练Loss震荡:学习率、调度器与梯度裁剪
训练到第20轮左右,我发现验证集的ROC-AUC在几个区间反复横跳,训练Loss也一直无法稳定下降。一开始还以为是模型结构写错了,逐层打印输出形状也没发现问题。
后来把优化器从Adam换成SGD才意识到,跟优化器没关系,就是学习率太大了。Adam在初始阶段自适应的学习率更新非常激进,对多分支网络容易产生明显的震荡。降到1e-4之后,Loss曲线立刻平滑下来。
另外我还加了梯度裁剪,max norm设置为1.0。这个操作在进入全局平均池化前的卷积层特别有效,能防止某个极短区间内的强信号把整个梯度带偏。正常情况下大家用InceptionTime不会加梯度裁剪,但一旦学习率调不低,或者数据里存在极端脉冲,它比任何调度器都更直接。
5.3 类别不平衡导致的虚警失控
这个数据集正样本占比不到两成,最开始训练时模型完全偏向了负样本。我当时用F1做指标,在某个阈值下试了好几次,F1一直卡在0.7左右,正样本召回率特别低。
试过加权损失函数,给正样本的cross entropy乘上一个权重,比如4到6之间的数。一开始效果立竿见影,召回率很快上来了,但紧接着虚警率也飙了上去,控制不住。
后来我换了一个思路:不再改损失权重,而是保留模型输出的原始概率,在阈值选择阶段单独调。比如原本默认阈值是0.5,我根据验证集把阈值降到0.2或者0.15,正样本召回率上去的同时,用虚警率卡住不要放得太开。这种方式比改损失权重更可控,因为你能直观看到阈值移动时召回率和虚警率之间的权衡曲线。
5.4 固定随机种子不等于完全复现
很多人跑深度学习实验时,都会在整个训练过程前设一个随机种子。但InceptionTime的集成机制包含多次训练,即使每个训练过程都固定种子,多个模型之间的随机性叠加之后,最终集成结果依然会有细微波动。
最直接的体现是:同一个数据集,分两次完整训练,得到的集成F1可能差0.01到0.02。这不是代码写错了,而是深度学习中常见的训练随机性。
我建议在报告结果时,不要只跑一次,而是把整个“训练五个模型再集成”的流程重复三遍,报告这三遍结果的均值和标准差。这样你的结论才经得起复现,而不是只有一个单次分数。
6. 方法边界与进一步优化方向
InceptionTime在这个数据集上表现不错,但不能把它当成所有时序分类问题的万能解。有必要把它的适用边界和扩展方向说清楚,省得你盲目套用到别的项目上。
6.1 长序列和低频慢变化场景会失效
InceptionTime的输入长度如果超过几万个点,模型的计算量会急剧增长,而且大卷积核的感受野依然有限。对整段几十分钟的飞行数据直接建模,效果反而远不如先切成短窗口。
另外,如果数据的差异体现在缓慢的长期趋势上,比如几十分钟内的漂移,InceptionTime这种固定卷积核尺寸的模型很难直接在单个窗口内捕捉完整过程。这时候,不如用窗口级特征再套一层时序模型,或者用更专门的长序列架构。
如果你要继续用InceptionTime,比较务实的策略是保持短窗口切分,并把窗口间预测结果按航段聚合。这不是模型的妥协,而是一种符合工业实践的降复杂度方式。
6.2 从异常状态分类延伸到异常评分
在这类航空数据任务里,很多实际场景并不需要最终输出一个硬分类,而是想得到一个平滑的异常分数,供调度系统或者人工复核参考。
InceptionTime最后输出的是概率值,不需要强制转换成正负标签。你可以直接把概率作为异常评分,再用一个滑动平均做平滑。实测下来,这种连续评分比硬分类更抗干扰,因为单个窗口的误判不会导致整条航段被盖章。
把这个评分和人工规则做拼接,效果通常更好。比如先做规则过滤,把明显的传感器失效排除掉,再让InceptionTime只负责规则覆盖不到的细微模式。这种方法算是把统计学习与深度学习结合在一起的落地技巧,比纯端到端更容易被现场接受。
6.3 后续可以尝试的注意力增强方案
InceptionTime对局部模式很敏感,但它使用全局平均池化输出时,相当于对整条时间轴的所有特征做了一视同仁的汇总。对航空数据来说,不同时间段对分类的贡献其实极不均匀,异常往往发生在一个很窄的时间窗内。
比较自然的升级方向是在InceptionTime的卷积输出后加一个轻量级注意力层,给每个时间步计算一个权重,再把时间轴加权求和。这样既能保留多尺度卷积的特征提取能力,又能让模型更关注真正包含异常模式的那段时间。如果样本量允许,甚至可以尝试把InceptionTime作为特征提取器,在它后面接一个小型Transformer编码器,专门建模卷积特征之间的时间依赖关系。
不过要注意的是,注意力机制会引入新的超参数,调参成本会增加。考虑性价比的话,还是要把基础的InceptionTime完整跑通,确实验证了问题之后,再决定是否值得往上加复杂度。
6.4 最后一点实操建议
整套流程跑下来,我最深的体感是:时序分类项目里,模型选择和调参占据的时间可能只有两成,剩下八成都在数据清洗、窗口划分、防泄漏、指标设计和结果解释上。InceptionTime是一个容错率相当高的模型,只要数据管线不出问题,它很少会让你失望,真正的坑往往埋在数据里而不是代码里。
如果你现在正准备拿NGAFID或者类似的多通道时间序列做分类实验,我建议你从InceptionTime的最简版本开始,先把数据管线完整跑通,再逐步替换模块和调整超参数。过早堆复杂度没有意义,先把一条干净的数据流建立起来,比分出胜负更值钱。