ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

EMD-SSA-BiLSTM时间序列预测实战:分解去噪与双向LSTM完整指南

EMD-SSA-BiLSTM时间序列预测实战:分解去噪与双向LSTM完整指南 这两年凡是碰过时间序列预测的同行应该都绕不开这个组合词EMD-SSA-BiLSTM预测程序。第一次看到这个名字我以为是三个模型随便堆在一起先拿EMD把序列拆开拿SSA过滤一遍再丢给BiLSTM去学。真在数据上跑完一圈才发现每个环节都有自己的脾气组合顺序和参数稍微拧一下结果天差地别。这篇文章就把我实际搭这套预测程序的完整过程拆给你看包括代码、参数、踩坑以及哪些地方最容易翻车。适合正在做非平稳时间序列预测、想把准确率再往上提一提的朋友。这套流程我主要用在一个模拟的工业设备温度趋势预测项目上后来又在某风电场的功率数据上验证过一版。数据特点非常典型有趋势、有周期、还夹杂大量随机噪声单靠经典统计模型或者直接上神经网络都很难得到干净的预测曲线。如果你也遇到类似的数据这篇文章应该能帮你少走不少弯路。1. 整体思路拆解三个模型各管一段1.1 为什么不能直接拿BiLSTM硬上很多初学者会问既然BiLSTM这么强直接把原始序列丢进去不行吗我的回答是非平稳、非线性数据里信号和噪声在同一个时间轴里纠缠得很厉害模型很难分清哪些是应该学的规律、哪些是应该忽略的扰动。举个例子。你听一段混音很差的录音人声、伴奏、现场噪声全叠在一起直接让人去听写歌词很难听准但如果先把不同频段分开人声单独拿出来识别准确率会高得多。EMD做的事情就是把混在一起的“声轨”拆开SSA负责把每条“声轨”里的底噪再去掉一遍最后BiLSTM只需要专注学习相对干净的分量之间的时间依赖。这样每个模型只负责自己最擅长的一段整体效果远好于单模型硬扛。如果拿原始序列直接训练BiLSTM模型往往会把噪声也当成规律记住尤其是训练集和测试集噪声分布不一致时误差会非常明显。所以前置分解和去噪不是锦上添花而是这套流程能跑出效果的关键。1.2 整条流水线是怎么串起来的这套预测程序的完整链路是数据归一化 → EMD分解 → SSA去噪重构 → 序列叠加 → 滑窗构造样本 → BiLSTM训练预测 → 反归一化输出。第一步先对原始序列做整体归一化避免不同量纲的分量影响分解质量。接着用EMD把原始序列分解成若干个本征模态函数IMF和一个残差项。此时高频IMF里通常还有明显的随机噪声需要对这些高频分量做SSA降噪。低频IMF和残差基本保留原样因为SSA很可能会把本应有的趋势信息也“洗”掉一部分。最后把所有处理过的分量线性叠加得到一条相对干净的序列再交给BiLSTM。我踩过的一个坑是顺序问题。有人喜欢先对每个IMF分别归一化再送进模型这样做会把分量之间的能量比例关系破坏掉重构之后预测结果会很怪。正确做法是先整体归一化分解、去噪、重构之后计算误差指标时再做反归一化。1.3 SSA的两种身份先分清再动手这里必须单独说一句因为网上关于SSA的说法非常混乱。在这类组合模型里SSA可能指两种完全不同的东西。一种是奇异谱分析英文是Singular Spectrum Analysis用于时间序列的趋势提取和去噪位置一般在EMD之后、BiLSTM之前。另一种是麻雀搜索算法英文是Sparrow Search Algorithm属于群体智能优化算法通常用来搜索BiLSTM的隐藏层节点数、学习率、正则化系数这些超参数。本文的主线以奇异谱分析为准因为在“EMD - SSA - BiLSTM”这个顺序结构里放在EMD和BiLSTM之间最自然的动作是降噪或重构。但我也建议你根据实际需求区分SSA身份全称核心作用出现位置典型用法奇异谱分析Singular Spectrum Analysis降噪、趋势分解、信号重构EMD之后、BiLSTM之前对高频IMF做低秩重构麻雀搜索算法Sparrow Search Algorithm超参数寻优BiLSTM训练过程外围搜索学习率、隐藏单元数如果标题里的SSA指的是麻雀搜索算法那整个流程会变成EMD分解得到分量后直接重构或挑选主要IMF然后用麻雀搜索算法去寻优BiLSTM超参数。这个路线的重点在优化器设计不在信号重构。我这里先讲奇异谱分析路线后面也会在参数调优章节补充麻雀算法思路方便你切换。2. EMD分解把复杂序列拆成可解释的分量2.1 EMD到底在做什么经验模态分解最吸引人的一点是自适应。它不像小波分解那样需要提前选基函数和分解层数而是根据数据本身的局部时间尺度把序列一层一层筛出来。整个过程不需要预设太多人为参数这对非平稳数据非常友好。每个分解出来的IMF需要满足两个条件一是极值点数量和过零点数量相等或最多差一个二是上下包络的均值在局部始终接近于零。你用一句话理解就是每一层IMF都是数据在不同振荡尺度上的“干净成分”残差是最后的整体趋势。数学上可以写成x(t) Σ IMFi(t) r(t)。所以最终重构序列时把所有IMF加上残差就能还原原始信号。这里要强调的是EMD不是纯数学公式套娃它的“筛分”过程依赖极值点包络拟合所以数据端点、采样密度、噪声水平都会明显影响分解结果。2.2 用Python把序列拆开我目前常用的是第三方库PyEMD底层是Python实现数据量不大时足够用。安装命令很简单pip install PyEMD如果你在新版本环境中遇到依赖问题也可以安装新的维护版本pip install EMD-signal不同库的调用方式略有差异我用PyEMD举例import numpy as np from PyEMD import EMD # data是已经预处理并归一化的一维序列shape(N,) values data[value].values emd EMD() imfs emd.emd(values) print(imfs.shape) # 返回 (n_imfs, N)最后一行通常是残差跑完之后你会发现imfs的第一行通常振荡频率最高越往后频率越低最后一行是单调趋势或低频残差。这时候不要急着全部丢进模型先画一下每个IMF的频谱或者算一下相关系数判断哪些是噪声主导分量。我处理工业温度数据时一般会计算每个IMF与原始序列的皮尔逊相关系数。相关系数低但频率很高的IMF基本可以判定为噪声主导需要送到SSA去降噪相关系数高且能看出明显周期或趋势的直接保留。2.3 分解参数怎么定PyEMD的EMD类本身提供一些参数选项但很多情况下用默认值就能跑。值得关注的一个是sift_iters也就是筛分迭代次数。默认值通常够用但如果遇到分解不充分或者出现奇怪的包络可以适当增大。另一个是extrema_detection默认用一阶差分找极值点数据噪声大时可能出现很多伪极值。我个人的习惯是先跑一遍默认参数观察IMF数量和末行残差形态如果发现相邻两个IMF轮廓高度相似明显是模态混叠再考虑改用集合经验模态分解EEMD或完全自适应噪声集合经验模态分解CEEMDAN。这些变体通过加入辅助噪声来抑制模态混叠代价是计算量更大对业务场景来说数据量不大时完全可以接受。2.4 EMD阶段的坑端点效应是第一个躲不开的问题。分解时两端极值点不完整包络拟合容易“翘起来”导致IMF首尾出现发散的大幅摆动。我的处理方法是在分解前用一段镜像延拓或者直接用原始序列末尾若干点做预测延伸分解后只取中间有效段参与后续建模。第二个坑是分解层数不稳定。有时候同一份数据两次运行得到的IMF数量不一样这和极值点检测的数值敏感性有关。解决方案是固定库版本、固定输入顺序并尽量保证输入序列没有NaN或异常尖峰。数据里的一个极端值很容易让EMD多拆出一层高频分量。第三个坑是我在很多项目里都见过的有人把每个IMF分别归一化再训练。这等于把各分量的物理尺度打乱了重构预测结果后误差会扩大到难以接受。记住一条铁律预处理只对原始序列做一次整体归一化后续所有操作都在这个尺度下进行。3. SSA降噪把残余噪声从分量里“拧”出来3.1 奇异谱分析的直观理解SSA的英文全称是Singular Spectrum Analysis它要做的事情是把一维时间序列重排成高维矩阵然后做奇异值分解再把主要成分找出来重构。通俗地讲它把序列从“一条线”变成“一叠照片”从中找出最主要的几层纹理丢掉那些杂乱的细碎纹理。具体来说先选一个窗口长度L把长度为N的序列切成N-L1个长度为L的滞后片段组成轨迹矩阵。对这个矩阵做奇异值分解得到按贡献大小排序的奇异值。大的奇异值对应数据里的主要趋势和周期小的对应噪声和随机扰动。选前r个奇异值对应的成分做重构就能得到一条相对干净的序列。你可以把奇异值理解成班里合影时每个人的“重要程度”。前几个同学占据画面核心后面的同学只是背景。只看前r个人合影的主体内容还在背景噪声被丢掉了。3.2 窗口长度L怎么选L是SSA最重要的参数。它决定了能捕捉到的最大周期尺度也直接影响计算开销。L太小时轨迹矩阵信息太碎趋势和周期分不开L太大时矩阵规模变大奇异值分解会明显变慢而且边界效应更严重。工程上常用经验是L取序列长度的10%到20%并且不要超过N/2。如果数据有明显的周期比如24小时周期或7天周期L至少要比一个周期的长度大最好能覆盖两个周期。我在风速数据上用N1200L取120效果就比较理想在电力负荷数据上N2016L取168正好对应7天的小时数。3.3 重构阶数r怎么选选r的核心原则是看奇异值贡献率。把所有奇异值平方求和计算前r个奇异值贡献占比一般取到85%到95%。但这里有一个容易理解错的地方去噪并不是r越小越好。r取得太小会连真实信号的尖峰也一起抹平r取得太大又等于没去噪。我的做法是画出奇异值下降曲线观察“拐点”。拐点之前是主要成分拐点之后是噪声平台。然后从贡献率90%开始试看重构序列和原始序列的残差是否接近白噪声。如果残差还有明显周期说明r取小了需要增加如果残差全是高频毛刺说明当前r已经能有效去噪。3.4 SSA去噪代码与组合细则下面给一个可直接用的SSA重构函数注释我写得比较详细import numpy as np def ssa_reconstruct(series, L, r): N len(series) if L N / 2: L int(N * 0.2) K N - L 1 # 构造轨迹矩阵shape(K, L) X np.stack([series[i:iL] for i in range(K)]) # 奇异值分解 U, s, Vt np.linalg.svd(X, full_matricesFalse) # 取前r个成分重构轨迹矩阵 X_rec U[:, :r] np.diag(s[:r]) Vt[:r, :] # 对角平均把矩阵还原成一维序列 y np.zeros(N) count np.zeros(N) for i in range(K): for j in range(L): y[i j] X_rec[i, j] count[i j] 1 y / count return y, s使用的时候对参与降噪的IMF逐个处理clean_imfs [] for idx, imf in enumerate(imfs): # 自定义判断高频且与原始序列相关较弱的IMF才做SSA if idx high_freq_idx: cleaned, _ ssa_reconstruct(imf, Llen(imf)//5, r10) clean_imfs.append(cleaned) else: clean_imfs.append(imf) clean_series np.sum(clean_imfs, axis0)需要提醒一句SSA的L和r不是一成不变的。如果某个IMF本身能量很低L可以取短一点如果IMF有明显的周期振荡r要稍微多取几个避免把波形压扁。3.5 SSA的坑第一个坑是边界权重问题。对角平均时序列中间每个点被矩阵覆盖的次数多两端覆盖次数少所以重构后两端容易出现幅度失真。解决方法是分解前做短段延拓重构后裁掉两端或者在计算权重时注意边界点的实际覆盖次数。第二个坑是L选得过大导致内存和耗时爆炸。如果N有几千L取到N/2构造出来的矩阵是几千乘几千奇异值分解会非常慢。建议先用小N的测试数据调通流程再上全量数据。第三个坑是对残差趋势项做SSA。趋势项本身就是低秩的SSA做不做区别不大反而可能把末端趋势拉弯。我通常把残差原样保留。4. BiLSTM模型构建前看一段后看一段4.1 为什么这里是双向LSTMLSTM单向传播时每个时间步只能看到当前时刻之前的信息。BiLSTM则把序列从头到尾和从尾到头分别过一遍然后把两个方向的隐状态拼接起来让每个时间步都能同时获得前后文信息。不过在时间序列预测里要特别注意“双向”不等于“作弊”。如果我们的任务是拿过去7天的数据预测明天的值那么在构造训练样本时输入窗口本身就是已知的历史数据BiLSTM在这个窗口内部做双向编码是合理的因为它并没有偷看窗口之外的未来。但如果要做严格的在线滚动预测每一步只用截止到当前时刻的数据那BiLSTM就要谨慎使用因为它会用到窗口内后面时间点的信息实际部署时可能造成信息泄漏。我做预测实验时通常是离线训练加滚动验证窗口内双向编码没有引入未来信息效果比单向LSTM更扎实。4.2 样本构造与数据划分先用滑窗把一维序列变成监督学习需要的(X, y)格式def build_samples(data, seq_len24, pred_len1): X, y [], [] for i in range(len(data) - seq_len - pred_len 1): X.append(data[i:i seq_len]) y.append(data[i seq_len:i seq_len pred_len]) return np.array(X).reshape(-1, seq_len, 1), np.array(y)这里seq_len是输入窗口长度pred_len是预测步数。要注意时间序列切分训练集和验证集时不能随机打乱否则会破坏时间依赖关系。我一般按顺序切分前80%训练中间10%验证最后10%测试。归一化时只能用训练集的均值和标准差去转换验证集和测试集不能整个数据集一起做归一化否则验证集信息会渗入训练过程。4.3 模型结构与超参我常用的Baseline结构是两层双向LSTM加Dropoutfrom tensorflow.keras.models import Sequential from tensorflow.keras.layers import Bidirectional, LSTM, Dropout, Dense model Sequential() model.add(Bidirectional(LSTM(units64, return_sequencesTrue), input_shape(seq_len, 1))) model.add(Dropout(0.2)) model.add(Bidirectional(LSTM(units32))) model.add(Dropout(0.2)) model.add(Dense(pred_len)) model.compile(optimizeradam, lossmse)第一层设置return_sequencesTrue是为了让第二层能继续处理完整的时间步。units先取64和32后续根据验证集表现调整。输入维度是(seq_len, 1)只用了单变量序列如果你有多个相关变量可以把最后一个维度改成特征数量。4.4 训练策略与评估指标训练时我一般会挂几个回调函数from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau callbacks [ EarlyStopping(monitorval_loss, patience15, restore_best_weightsTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience5, min_lr1e-6) ] history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs200, batch_size32, callbackscallbacks, verbose1 )EarlyStopping的patience我习惯设为15到20太小容易在loss还没降到底时提前停掉。学习率用ReduceLROnPlateau在验证loss变平后降为原来一半让训练后期更精细。评估时不要只看损失函数我通常输出RMSE、MAE、MAPE和R2from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score def evaluate_pred(y_true, y_pred): rmse np.sqrt(mean_squared_error(y_true, y_pred)) mae mean_absolute_error(y_true, y_pred) mape np.mean(np.abs((y_true - y_pred) / (y_true 1e-8))) * 100 r2 r2_score(y_true, y_pred) return rmse, mae, mape, r2注意一定要先反归一化再算指标。很多人在归一化后的数值上直接算RMSE得到的结果看起来很小但实际上没有意义。5. 端到端整合与参数调优实录5.1 完整串联代码骨架把前面所有环节拼成一个可运行的流程大致长这样import numpy as np from PyEMD import EMD from sklearn.preprocessing import MinMaxScaler # 1. 读数据 raw data[value].values.reshape(-1, 1) scaler MinMaxScaler(feature_range(0, 1)) scaled scaler.fit_transform(raw).flatten() # 2. EMD分解 emd EMD() imfs emd.emd(scaled) # 3. 对高频IMF做SSA其他保留 clean_imfs [] for i, imf in enumerate(imfs): if i 2: cleaned, _ ssa_reconstruct(imf, Llen(imf)//5, r8) clean_imfs.append(cleaned) else: clean_imfs.append(imf) clean np.sum(clean_imfs, axis0) # 4. 构造样本 seq_len 48 X, y build_samples(clean, seq_lenseq_len, pred_len1) split int(len(X) * 0.8) X_train, X_val, X_test X[:split], X[split:splitint(len(X)*0.1)], X[splitint(len(X)*0.1):] y_train, y_val, y_test y[:split], y[split:splitint(len(X)*0.1)], y[splitint(len(X)*0.1):] # 5. 训练BiLSTM model build_bilstm(seq_len, pred_len1) model.fit(X_train, y_train, validation_data(X_val, y_val), epochs100, batch_size32, callbackscallbacks) # 6. 预测与反归一化 pred model.predict(X_test) pred_inv scaler.inverse_transform(pred) y_test_inv scaler.inverse_transform(y_test)这个骨架是能跑的实际项目里你要根据数据频率、样本量、噪声强度去调整L、r、seq_len、units这些参数。5.2 参数调优经验我整理过一张常用的调参表格可以当参考参数常见范围我的经验值说明seq_len24 ~ 16848或96日数据可取7到30天分钟数据取一天或两天的周期倍数LSSA窗口0.1N ~ 0.2N0.15N能覆盖两个主要周期即可r重构阶数看奇异值贡献率贡献率90%左右低频IMF不用SSAunits32 ~ 12864 32两层结构时逐层减半batch_size16 ~ 6432数据量小用16大用64learning_rate1e-4 ~ 1e-21e-3起配合ReduceLROnPlateau关于超参数搜索如果SSA指麻雀搜索算法你可以把units、dropout、learning_rate作为麻雀个体的位置维度用验证集MAPE作为适应度迭代搜索最优组合。这么做在数据量大、训练时间长时非常耗时我的建议是先用网格大概圈定合理区间再用麻雀算法加密搜索不要一开始就全局随机搜。5.3 模型效果怎么看只看RMSE低很容易被误导。有一次我的模型RMSE降得挺漂亮但把预测曲线和真实曲线叠到一起后发现预测值整体比真实值滞后了一个周期。这种情况常见于序列高度自相关时模型学到的是“拿上一个值预测下一个值”本质是在复读。所以我会额外看残差序列的自相关图。如果残差在滞后1阶甚至多阶上仍然显著相关说明模型没有把时间依赖学干净需要增大seq_len或者增加LSTM层容量。如果残差近似白噪声说明信息已经提取得比较充分。另外多步预测时要格外小心误差累积。迭代预测中第一步的小误差会被模型当作输入继续传下去越往后偏得越厉害。如果业务上需要预测未来多个时刻建议直接让模型输出多步向量而不是反复滚动调用单步预测。6. 常见问题与排查我替你踩过的坑6.1 EMD分解结果不对怎么办先检查输入是不是有NaN或无穷值这会让包络拟合直接失控。其次如果发现IMF数量频繁变化先升级或固定PyEMD版本。最后如果高频IMF明显出现畸形的尖峰可以尝试用EEMD或CEEMDAN替代原始EMD。EEMD会往原始序列里加白噪声做多次分解再平均能够有效抑制模态混叠代价是计算时间成倍增加。对数据量在几千以内的序列完全能接受。6.2 SSA重构后首尾变形这基本是端点权重问题。你可以把序列在分解前用镜像方式延拓20个点重构后裁掉对应位置也可以接受两端各丢弃一小段只对中间部分做预测建模。不要让SSA去噪后的首尾异常值直接进入训练样本。6.3 BiLSTM损失不降或过拟合损失不降第一反应是检查数据归一化范围。如果数据量级差异过大梯度会震荡。第二是学习率太高试着从3e-4到1e-3之间调整。第三是输入窗口太长但数据量太少模型欠拟合。过拟合的表现是训练loss持续下降、验证loss开始反弹。这时候先加大Dropout再检查训练数据是不是太少。我遇到过数据量不足两千条就硬上两层大宽度LSTM的情况过拟合几乎躲不掉。比较有效的做法是缩小units把网络压到能拟合目标规律的最低复杂度。6.4 快速排障速查表现象可能原因解决方案EMD末行残差剧烈摆动端点效应明显延拓两端、只取中间段相邻IMF轮廓高度相似模态混叠换EEMD或CEEMDANSSA重构后曲线被压平r太小增大重构阶数SSA计算很慢L过大把L控制在0.1N到0.2N验证loss不降学习率过高或数据未归一化调整学习率、检查归一化预测曲线明显滞后序列自相关太强增大seq_len、检查是否在“复读”多步预测后期全偏迭代误差累积直接多步输出或用seq2seq结构6.5 一个容易被忽略的工程细节在线滚动预测时要把真实值滑入窗口还是把预测值滑入窗口直接影响误差曲线。如果只能拿到历史真实值那每次都用真实值更新窗口预测会准很多。如果真实值延迟获取只能把预测值当输入继续滚这时必须在评估里模拟这种带误差的滚动否则实验结果会和线上表现差很多。我在第一个模拟项目里就是没注意这一点离线回测RMSE很漂亮到实时验证时发现每隔几步就明显偏移。后来改成“预测值滚入窗口”的方式重跑对照才能比较真实地反映实际性能。最后分享一个我反复踩坑后总结的经验EMD-SSA-BiLSTM不是万能的。如果数据本身的信噪比已经很高趋势也相对平稳直接上个简化LSTM反而更省事但如果数据是强非平稳、噪声明显、业务上又需要解释趋势变化这套组合确实很值。别一上来就追求复杂的参数组合先把每一步的分量图、重构图和误差曲线打印出来看一遍再动手调参。可视化能帮你发现百分之八十的问题。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进