news 2026/10/14 16:23:37

EEMD-LSTM非平稳时间序列预测:Python实现与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
EEMD-LSTM非平稳时间序列预测:Python实现与避坑指南

简介:这是一套基于Python实现的EEMD-LSTM时间序列预测完整源码与配套数据,面向计算机、电子信息工程、数学等专业学生,可满足课程设计、期末大作业及毕业设计需求。项目在Anaconda、PyCharm环境下以Python与TensorFlow构建,采用集合经验模态分解(EEMD)对原始序列进行分解,再结合长短期记忆网络(LSTM)完成预测,适合希望学习信号分解与深度学习组合模型的入门者。压缩包共3个文件,包含2个csv数据文件与1个py主程序,整体仅47KB,结构紧凑,数据与代码分离,便于直接运行与替换。代码采用参数化编程,几乎一行一注释,思路清晰,既方便理解每个步骤,也便于调整参数开展对比实验。资源发布以来已有385人学习浏览,作者为资深算法工程师,长期从事神经网络预测与信号处理仿真,注释详尽、友好度高,适合作为毕业设计、竞赛项目或进阶学习的复现与扩展基础。

1. 用EEMD-LSTM做预测:先把非平稳信号拆开,再让LSTM逐项学习

做电力负荷预测时,我拿到过一段低频波动里叠着高频毛刺的信号。直接用LSTM建模,训练集上loss降得很漂亮,测试集上一到尖峰就翻车——模型的误差集中在趋势项上,而趋势项恰恰被噪声模态带偏了。EEMD-LSTM的思路是先用集合经验模态分解把信号拆成不同尺度的本征模态函数IMF,再让LSTM对每个模态单独预测,最后相加。这个标题讲的就是把这套流程用Python完整落地,从分解参数设置到LSTM训练再到结果重构。适合做负荷、风速、股价、振动这类非平稳时序预测的工程师和学生,新手能照着搭出一套可运行的工程,熟手可以对照参数边界和踩坑记录调整自己的方案。

2. EEMD分解:为什么先拆信号,以及怎样用PyEMD跑通第一步

2.1 为什么是EEMD而不是EMD或VMD:模态混叠问题的解法

直接对非平稳信号做LSTM预测,最大的麻烦是趋势项、周期项和噪声叠在一起,网络要同时拟合不同尺度的规律。EMD可以把信号自适应地拆成若干IMF,但它有个老毛病——模态混叠:相近频率的成分会在不同IMF之间串来串去,一个IMF里既有这段频率又有那段频率,LSTM学起来就会很混乱。

EEMD解决模态混叠的办法很直接:给原始信号加多次白噪声,每次加完噪声做一次EMD,再把所有分解结果的同阶IMF取平均。白噪声在不同尺度上均匀分布的特性会把极值点分布的密度拉均匀,模态混叠就被平均掉了。实际使用中,EEMD比EMD稳定得多,尤其是信号里含有间歇性高频成分时。

也有人用VMD做同样的事,但VMD需要预先指定模态个数K。K这个值是靠经验和实验试出来的,有一定的玄学成分;EEMD的自适应分解则不需要预设模态数量,给一套噪声参数就能跑,落地更省事。所以在这个项目里,我一般选EEMD。

2.2 用PyEMD完成EEMD分解:最小可运行代码

PyEMD是Python里最常用的EMD/EEMD实现库,安装方式很简单。下面的代码构造一段仿真信号,完成EEMD分解,并把结果保存成CSV,后续所有建模都从这个CSV读取数据:

import numpy as np import pandas as pd from PyEMD import EEMD # 构造一段待分解序列:真实项目中这里换成你的时序数据 t = np.linspace(0, 1, 1000) signal = (0.5 * np.sin(2 * np.pi * 5 * t) + 0.2 * np.sin(2 * np.pi * 30 * t) + 0.1 * np.random.randn(len(t))) # EEMD分解:集成100次,白噪声幅值设为信号标准差的0.2倍 eemd = EEMD(trials=100, noise_width=0.2) imfs = eemd.eemd(signal) # 保存分解结果:第0列是原始信号,后面依次是IMF1..IMFn和残差项 df = pd.DataFrame(imfs.T) df.insert(0, 'original', signal) df.to_csv('eemd_result.csv', index=False, header=False)

eemd.eemd()返回的imfs是一个二维数组,第0行是最高频的IMF,最后一行是残差项。因为EEMD集成了多次EMD结果,它天然地比EMD慢,1000个点的序列跑一百次集成大约需要十几秒。这里有两个关键参数:trials是集成次数,noise_width是白噪声幅值占信号标准差的比例。运行完建议画一下每个IMF的波形,确认高频模态在前、低频趋势在后的排列,再进入下一步。

2.3 噪声幅值与集成次数怎么设:两个参数的落地经验

噪声幅值和集成次数是EEMD最重要的两个参数。noise_width取值太小,白噪声起不到均匀化极值分布的作用,模态混叠压制不住;取值太大,会把真实信号淹没在噪声里,分解出的高频IMF基本就是噪声本身。常见范围是0.1到0.4,我一般从0.2起步,然后观察第一个IMF是否还有明显周期性。如果IMF1长得像纯随机序列,说明噪声幅值偏大,调小到0.1再试。

trials决定了集成平均的稳定性。50次以下分解结果抖动明显,同一个信号跑两次IMF形态会有差异;100次以上结果基本稳定,但耗时线性增加。我一般固定100次。另外,PyEMD允许设置最大IMF数量,如果分解出的模态过多,出现相邻两个IMF波形几乎相同的“孪生模态”,那就是过分解了,可以把max_imfs调小,或者换一个更小的noise_width重新分解。

提示:EEMD的分解结果受随机种子影响,工程化落地时一定要固定随机状态,否则每次训练前分解结果都不同,模型复现会变成一场灾难。后面第5章我会细说这个问题。

3. 把分解结果变成LSTM的输入:数据预处理与滑窗生成

3.1 归一化为什么一定要放在分解之后

EEMD分解完成之后,每个IMF都有自己的幅值尺度:高频IMF幅值可能只有0.01,而残差趋势项幅值是几百甚至几千。如果对这些IMF统一做归一化,高频成分会被直接压平,LSTM学不到有用信息。

正确的顺序是:先分解,再对每个IMF单独归一化。这里有一个常被忽略的细节——不要先归一化再分解。原因有两点:其一,EEMD的noise_width参数是相对信号标准差来设置的,先归一化会把信号幅值压缩到[0,1],同样倍数的白噪声在相对意义上完全不同;其二,分解后的每个IMF都保留自己的物理幅值尺度,后续做误差分析时,你能清楚看到预测误差主要来自哪个频段。

常见的做法是对每个IMF分别做MinMaxScaler或StandardScaler。MinMaxScaler适合幅值范围明确的模态,StandardScaler适合接近正态分布的残差项。我在实际项目中会把这两者混用:周期分量用MinMax,残差和高频噪声用StandardScaler。

3.2 用滑动窗口构造监督学习样本

LSTM不能直接吃一维时序数据,需要把序列改成“过去lookback步预测未来horizon步”的监督学习格式。下面的函数是把单个IMF转换成LSTM训练样本的核心工具:

import numpy as np def create_sequences(data, lookback=24, horizon=1): X, y = [], [] for i in range(len(data) - lookback - horizon + 1): X.append(data[i:i + lookback]) y.append(data[i + lookback:i + lookback + horizon]) return np.array(X), np.array(y)

lookback是回看窗口长度,horizon是预测步数。回看窗口的长度与时序数据的周期有关:日粒度数据有周周期就至少取7的倍数,我一般取周期的2倍左右;如果你做的是设备振动预测,采样率高,回看窗口可以按频谱分析的结果来定。horizon如果是多步预测,这个函数会直接输出多步的目标序列,LSTM的输出层维度也要相应调成horizon。

滑窗构造的样本量大约为总长度 - lookback - horizon + 1,如果你的原始数据只有几百个点,滑窗后样本量会很小,LSTM很容易过拟合,这时要把lookback调短一些。

3.3 训练集/验证集/测试集切分:时间序列不能用随机切分

很多初学者在构造完滑窗样本后,直接调用train_test_split随机切分,这是时间序列预测里最典型的数据泄露错误。随机切分会让测试集里混入训练集前后时间段的信息,LSTM学到的是“跳跃式”组合规律而不是连续演化的规律,测试集loss会虚低,一上真实环境就露馅。

时间序列必须按时间顺序切分。切分顺序要和滑窗顺序严格对应,前70%做训练,中间15%做验证,最后15%做测试。代码逻辑如下:

n_train = int(len(X) * 0.7) n_val = int(len(X) * 0.15) X_train, X_val, X_test = X[:n_train], X[n_train:n_train+n_val], X[n_train+n_val:] y_train, y_val, y_test = y[:n_train], y[n_train:n_train+n_val], y[n_train+n_val:]

还有一个和切分配套的细节:每个IMF的归一化器必须在训练集上fit,再用同样的参数对验证集和测试集做transform。如果对整个序列先求min/max再切分,相当于把未来区间的极值信息提前泄露给了训练过程,这就是“用未来信息训练过去”的典型坑。等五个模态全部处理完,你会得到一套干净的、逐模态的监督学习样本,下一步就可以进入LSTM建模阶段。现在也有一些基于Agent的自动化时间序列预测框架在尝试把这类流程封装成编排任务,但底层模型核心仍然是LSTM这类循环神经网络,数据预处理这一步绕不开。

4. 构造并训练EEMD-LSTM:完整模型代码与参数说明

4.1 对每个IMF分别建模还是统一建模:两种结构的取舍

EEMD分解出的IMF频率尺度差异很大,这里存在两种建模路线。第一种是“分解-预测-重构”路线,简称additive模式:对每个IMF单独训练一个LSTM子模型,各自预测后把结果相加。第二种是把所有IMF作为多变量输入,统一送给一个LSTM模型,让网络自己学模态之间的交互。

我一般选additive模式,理由很直接:高频IMF和低频趋势项的尺度差太远,统一建模时网络会把高频分量当作噪声忽略掉,导致预测结果只拟合了趋势,丢失了细节。对每个模态单独建模,每个子模型只需要学一个频率范围内的时间演化规律,难度大大降低。它的代价是要训练N个模型,训练时间线性增加——如果你分解出8个IMF,就要训练8个LSTM。

不过additive模式也有例外情况:当某个IMF的方差贡献极小时,它本质上就是噪声,没必要为它单独建模,直接丢弃或者合并进相邻模态,这部分策略我会在第5章展开讲。

4.2 Keras实现LSTM预测的最小模型

下面是一个标准的LSTM回归模型,输入形状是(样本数, lookback, 1),输出是预测值。这个结构足以应对大多数单步预测场景:

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping def build_lstm(lookback, units=64): model = Sequential([ LSTM(units, return_sequences=True, input_shape=(lookback, 1)), Dropout(0.1), LSTM(units // 2), Dense(1) ]) model.compile(optimizer='adam', loss='mse') return model

第一个LSTM层设置return_sequences=True,是为了把完整的时间步输出给第二层LSTM,让网络能捕捉更长时间跨度的依赖;第二层不设return_sequences,只输出最后一个时间步的隐状态。units的取法和lookback相关,经验值是lookback的一半到等量之间,我习惯从64起步。Dropout(0.1)是给第一层到第二层之间加一点正则化,防止在样本量不大时过拟合。

全部IMF的LSTM结构可以共用同一个build_lstm函数,但要注意每个IMF的归一化范围不同,所以每个模态要单独配一个scaler对象。训练时的EarlyStopping回调按验证集loss触发,超过10个epoch没有改善就停止并恢复最佳权重。

4.3 预测结果重构:反归一化与各模态相加

训练和预测循环的完整逻辑如下。核心是把归一化的预测结果还原成原始幅值,再把所有IMF的预测叠加:

from sklearn.preprocessing import MinMaxScaler predictions = [] lookback, horizon = 24, 1 for i in range(imfs.shape[0]): imf = imfs[i] # 每个IMF单独归一化,fit时用训练区间 scaler = MinMaxScaler(feature_range=(-1, 1)) imf_train = imf[:n_train].reshape(-1, 1) scaler.fit(imf_train) # 构造滑窗样本,注意顺序:先fit归一化器,再transform imf_scaled = scaler.transform(imf.reshape(-1, 1)).flatten() X, y = create_sequences(imf_scaled, lookback, horizon) # 按时间顺序切分 X_train, y_train = X[:n_train], y[:n_train] X_val, y_val = X[n_train:n_train+n_val], y[n_train:n_train+n_val] X_test, y_test = X[n_train+n_val:], y[n_train+n_val:] # 调整形状:LSTM输入是 (样本数, lookback, 特征数) X_train = X_train.reshape((X_train.shape[0], lookback, 1)) X_val = X_val.reshape((X_val.shape[0], lookback, 1)) X_test = X_test.reshape((X_test.shape[0], lookback, 1)) model = build_lstm(lookback) model.fit(X_train, y_train, epochs=80, batch_size=32, validation_data=(X_val, y_val), callbacks=[EarlyStopping(patience=10, restore_best_weights=True)], verbose=0) pred = model.predict(X_test) pred_inv = scaler.inverse_transform(pred.reshape(-1, 1)).flatten() predictions.append(pred_inv) # 所有模态预测相加得到最终预测 final_pred = np.sum(predictions, axis=0)

这段代码有两个值得注意的点。第一,scaler.fit用的是训练区间的数据,transform才作用于全序列,这保证了min/max不包含未来信息。第二,每个IMF的预测结果在相加之前必须对齐索引,因为滑窗会让预测起点整体后移,X_test对应的真实值应从n_train + n_val + lookback处开始取。final_pred和y_test的对应关系是:第k个预测值对应原始序列第n_train + n_val + lookback + k个点,画对比图时一定要按这个索引对齐,否则你会看到预测曲线整体“超前/滞后”,这是建模结果重构时最容易翻车的地方。

提示:如果IMF数量多,建议把每个模态的模型保存到磁盘,例如model_imf0.h5,下次直接加载预测,不需要重新训练。工程上这是必须的,生产环境不可能每次都训练一遍。

5. EEMD-LSTM落地避坑:5个高频翻车点与排查思路

5.1 分解结果在序列两端发飘:边界效应的压制方法

现象:EEMD分解后,每个IMF在序列的开始和结束几十个点处出现明显摆动,幅度比中间段大得多,训练出的模型在预测末尾段时误差陡增。

原因:EMD系列的筛分过程依赖极值点进行包络插值,序列两端没有极值点,算法只能用边界附近的极值外推,外推本身就不稳定,叠加多次集成后两端误差被进一步放大。短序列受这个影响尤其严重。

解决:在分解前对序列做镜像延拓,左右各延长20到50个点,分解完成后把延拓部分裁剪掉。镜像延拓的代码很轻量,用np.pad配合mode='reflect'即可。如果做的是在线预测,每次只预测未来一步,EEMD分解窗口也要包含足够的“预测前缓冲”,让端点效应落在你不需要的那一段。

5.2 归一化范围不一致导致重构后的预测值偏移

现象:每个IMF的反归一化预测结果单独看形态正常,相加之后整体幅值偏小,尤其是峰谷处被明显削平。

原因:如果归一化用MinMaxScaler(feature_range=(0,1)),LSTM输出层默认是线性激活,但训练时目标值被压缩到[0,1]区间,网络输出被“惯性”拉向区间中部,预测值很难触达0和1附近的极值。反归一化后,峰谷值就被系统性低估了。

解决:把feature_range改为(-1, 1),让目标值分布在对称区间,网络对极值的预测压力更小。如果削顶现象仍然存在,检查最后一个Dense层的激活函数是否是linear(Keras默认就是)。残差项如果偏态明显,就不要用MinMax,改用StandardScaler,它的输出没有固定上下界,对极值更宽容。重构后做一次残差散点图,看误差是否集中在峰谷,能快速定位是不是归一化引起的。

5.3 训练时用到了未来信息:最隐蔽的数据泄露

现象:验证集loss稳步下降,测试集loss也好看,但部署到新数据上效果急转直下,预测曲线比真实曲线滞后一拍。

原因:除了第3章讲的先归一化再切分问题,还有一个更隐蔽的泄露点——EEMD分解本身是全局操作。EEMD对整段序列做分解,每个时间点的IMF值都受到整段序列极值分布的影响,相当于分解过程已经把测试区间的统计特性“泄漏”给了训练样本。这不是PyEMD的bug,而是信号分解类方法固有的全局性。

解决:在工程上做滚动分解,每次只用截止到预测点之前的一段窗口做EEMD,得到最后一个IMF值作为当前时刻的分解结果。训练阶段可以全序列分解,但验证和测试阶段要模拟滚动预测,每预测一步都重新分解一次。这样做的代价是训练慢不少,但结果可信。这条是我踩过最深的一个坑,也是EEMD-LSTM在学术paper里效果好、生产落地难的最常见原因。

5.4 IMF数量过分解:高频噪声模态该丢就丢

现象:分解出的前两三个IMF几乎没有周期特征,波形完全像白噪声,模型在它们上训练出的预测结果基本是随机数,最终叠加后把主信号的预测精度拖垮。

原因:EEMD在信号长度不足、噪声幅值设置偏大时,会把白噪声单独拆成高频IMF。这些IMF在方差占比上可能很小,但对LSTM来说,它们是一段无规律可学的序列,模型只能硬拟合,结果就是预测时输出噪声。

解决:添加一个模态筛选步骤,用方差贡献率决定哪些IMF值得建模:

var_ratio = np.array([np.var(imf) for imf in imfs]) / np.var(signal) keep_idx = np.where(var_ratio > 0.01)[0]

方差贡献低于1%的模态,即使保留,也建议把它的预测结果设为0(即用均值代替),不让噪声进入最终叠加结果。筛选之后,可以进一步把几个相邻的高频噪声模态求和成一个高频噪声项,再决定是丢弃还是加入。这个“先看方差再决定建不建模”的习惯,能帮你省下大量无效调参时间。

5.5 PyEMD版本差异导致结果不可复现

现象:同一个脚本、同一份数据,隔一天跑出来的分解结果不同,LSTM训练精度也有细微差异。

原因:PyEMD内部有随机数生成逻辑,部分版本EEMD的噪声生成没有固定随机种子,每次调用eemd()都会产生不同的分解结果。另外,不同版本PyEMD的参数名不完全一致,有些版本用noise_width,有的版本用width,混用时会直接报TypeError。

解决:固定随机种子,并且把分解结果落盘。在创建EEMD对象时,如果当前版本支持seed或random_state参数就显式传入,不支持的话在最顶部调用np.random.seed(42),同时对每个import版本做好记录。最稳妥的做法是像我第2章代码里那样,把分解结果保存成CSV,训练时直接读CSV,整个过程不再重新分解。这样既保证复现,又省去了每次训练都要重新跑一次EEMD的时间成本。

6. 把预测误差再压低一档:模态合并策略、验证指标与我的检查习惯

最后这一层,说的是在基础流程跑通之后怎么精进。我常用的一个手段是模态合并:把方差贡献极小、频率相近的相邻IMF合并成一个模态,降低LSTM子模型的数量。比如风速数据分解出9个IMF,前3个高频模态方差贡献合计只有2%,单独建模纯属给网络送噪声;把这三个模态相加成一个“高频噪声项”,直接丢弃或用简单指数平滑处理,保留剩下6个模态建模,误差反而下降。

# 模态合并示例:把方差贡献小于1%的前几个高频IMF相加 merge_idx = [i for i in range(3) if var_ratio[i] < 0.01] if merge_idx: noise_imf = np.sum([imfs[i] for i in merge_idx], axis=0) # 用零均值替代该噪声项,或对噪声做简单阈值截断 noise_imf[np.abs(noise_imf) < np.std(noise_imf)] = 0

验证阶段不要只看训练集的loss,要同时看三样东西。第一是RMSE和MAE,RMSE对大误差敏感,能暴露预测是否在某些尖峰处严重偏移;MAE反映平均偏移水平,两个指标一起看。第二是预测曲线和真实曲线的叠图,重点看峰谷处的相位对齐,如果预测曲线总是慢半拍,问题多半出在滑窗长度不足或数据未平稳化处理。第三是预测残差的白噪声检验,用Ljung-Box检验看残差是否还有显著自相关;如果残差呈现出明显的周期性,说明某个IMF没有被充分建模,需要回头调整对应模态的网络结构。

我做风速预测时曾经为了多保留一个高频IMF把模型调了三天,换来换去各种超参数,最后发现丢掉它误差反而小了。踩过这个坑之后,我再也不追求把每一个IMF都送进LSTM,而是先看方差贡献再决定哪些模态值得建模。这个思路可以在你的项目里直接复用:分解跑通后先打印一张各模态方差贡献表,开会讨论模型结构时,这张表比任何参数曲线都更有说服力。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 5:09:00

2026企业级AI编程工具深度测评:TaoToken统一Key接入与选型指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:06:04

LeetCode 70 爬楼梯全解:从递归到滚动数组的机考实战指南

机考刷题到第 70 题爬楼梯&#xff0c;很多人会觉得这题太简单了&#xff0c;一个 easy&#xff0c;递归几行就写完。但真到了机考或者面试手写代码的环节&#xff0c;这道题往往能筛掉一批人。LeetCode 70 爬楼梯是 LeetCode 热门 100 题里的钉子户&#xff0c;也是动态规划入…

作者头像 李华
网站建设 2026/10/14 17:53:46

红黑树与哈希表深度对比:从C++ map/unordered_map底层原理到工程实践

1. 容器选型背后的设计逻辑&#xff1a;先从使用感受说起很多人在初学阶段就背过一个结论&#xff1a;map底层是红黑树&#xff0c;unordered_map底层是哈希表。但真正被问到“为什么set和map可以共用同一套红黑树实现”“为什么unordered_map的元素不是有序的”这类问题时&…

作者头像 李华
网站建设 2026/10/14 22:57:21

Java IO流全解析:字节流、字符流、NIO与实战陷阱

1. 读不完的文件&#xff0c;写不完的坑&#xff1a;Java IO流到底在解决什么问题很多人学Java IO时会有一个直观感受&#xff1a;类太多、继承关系太乱、记不住。FileInputStream、FileReader、BufferedReader、InputStreamReader……光看名字已经头大。但如果你真正在项目里写…

作者头像 李华
网站建设 2026/10/12 4:54:31

Codex连更28天:日更节奏下的功能演进与工程落地实践

1. 这场“连更28天”到底在卷什么1.1 从标题拆出来的三个核心信号先把标题拆开看。“Codex 连更 28 天”说的是一个明确的节奏承诺&#xff1a;连续 28 天&#xff0c;每天都有新东西落地&#xff0c;不是修修补补的补丁&#xff0c;而是能被用户直接感知的功能更新。“每天不上…

作者头像 李华