简介:EEMD-LSTM时间序列预测完整项目,基于Python与TensorFlow实现,提供可直接运行的源码和配套数据集。资源将经验模态分解(EEMD)与长短期记忆网络(LSTM)相结合,适合需要完成课程设计、期末大作业或毕业设计的高校学生,也适合刚接触预测建模的开发者对照学习。压缩包共3个文件,包含2个csv数据文件和1个py主程序,整体仅47KB,结构简洁。已有383人学习下载。代码几乎每行都有注释,采用参数化编程,参数可方便修改,便于理解EEMD分解、LSTM建模及预测输出的完整流程。通过阅读源码,读者可以快速搭建类似的时间序列预测实验,并在此基础上调整数据与网络结构,拓展到其他预测场景。
1. 非平稳序列预测的实用解法:EEMD-LSTM 完整源码包能帮你省掉什么
非平稳时间序列直接丢给 LSTM,效果经常像盲人摸象:趋势扛不住、噪声又被放大。EEMD 先做分解,把原始序列拆成若干个本征模态函数(IMF)和一个残差项,LSTM 再对每个子序列建模预测,最后叠加还原,这套组合处理气象、水文、负荷这类数据很实用。这份资源就是围绕这个思路给的:EEMD-LSTM.zip 里有一个 EEMD-LSTM.py 和焦作.csv、焦作全.csv 两份数据,注释几乎一行一句,很适合课程设计、期末大作业和毕业设计拿去改。它不是讲 EEMD 数学原理的教材,而是一条能跑的完整链路,从数据读取、分解、建模到预测出图都齐。适合两类人:一是导师给了时间序列方向但还没跑通组合模型的在校生;二是日常工作里要快速验证“分解+神经网络”思路的工程师,拿过来改参数就能做基线。
2. 原理先立住:EEMD 分解与 LSTM 的分工,为什么一定要组合
2.1 非平稳序列为什么难预测
先看数据形态。像焦作.csv 这种按时间排列的观测值,往往同时包含长期趋势、周期波动和局部突变。直接用 LSTM 建模时,梯度在长序列上容易衰减,网络要把趋势、周期、噪声一次性塞进隐状态里,拟合出来的结果经常是“平均化”的——高峰被削平,转折点滞后。很多人跑完第一次训练,看到预测曲线就是一条比原始序列平滑很多的曲线,这就是 LSTM 在非平稳数据上的典型表现。
EMD 的思路是把序列拆成本征模态函数(IMF),每个 IMF 代表一个尺度上的振荡,残差代表整体趋势。拆完之后,原本一条复杂的曲线变成多条相对简单的曲线,每条曲线各自有着更清晰的规律。EEMD 则在 EMD 基础上往原序列里加白噪声,利用噪声的统计特性消除模态混叠,避免相邻尺度的振荡纠缠在一起。换句话说,EEMD 先替 LSTM 做了一次信号预处理,把复杂度从“一条不平稳曲线”降成“几条相对平稳的子序列”。
2.2 LSTM 在每个子序列上更好学什么
拆出来的 IMF 各包含不同的频率成分。比如低频分量变化缓慢,LSTM 的遗忘门可以长时间维持状态,适合学趋势;高频分量幅值随机性大,LSTM 不需要记忆太久,只需抓住短时波动规律。相比在原始序列里“既要记趋势又要跟噪声”,每个子序列的学习目标明确得多,收敛速度也会快一些。
还有一个工程层面的理由:单条序列长度往往只有几百到几千个点,LSTM 堆太多层反而容易过拟合。分解之后,序列长度不变,但目标变得更简单,用一个两层的 LSTM 就能跑出不错的效果,训练时间也短。这也是这份资源里代码结构不太复杂但仍能出结果的原因——复杂度被 EEMD 前置消化掉了。
2.3 组合链路和资源里的实现路径
常见做法是三步:先对原始序列做 EEMD 分解,得到 IMF 序列集;然后针对每个 IMF 分别构造滑动窗口样本并训练 LSTM;最后把所有子预测结果相加,还原成最终预测。你也可以把全部 IMF 拼成一个多维输入,丢给同一个 LSTM 做多输出,但这样模型要同时学习不同频率的映射,可解释性差一些,调参也更难。
我拆这份源码时看到的主链路是:读入 csv → 数据归一化 → EEMD 分解 → 构造监督学习数据集 → 训练 LSTM → 逐项预测并叠加。资源里把参数全部提到文件顶部,换数据集时只需要改窗口大小、训练轮数这些数,不用翻函数体。需要注意,EEMD 分解不是越多 IMF 越好,分解层数受数据长度和停止条件影响,程序里一般用默认停止条件,也就是残差不再满足 IMF 条件就停。
3. 环境与数据准备:Anaconda + PyCharm + TensorFlow 的最小可运行配置
3.1 创建虚拟环境与依赖安装
资源标注的运行组合是 anaconda + pycharm + python + tensorflow,这里建议把 Python 锁在 3.8 到 3.9。TensorFlow 2.10 及以下版本对 Python 3.8 支持最稳,后续要用到的一些老代码片段也基本不用改。PyEMD 是常见的 EEMD 实现库,安装量小,没有额外编译负担。
conda create -n eemd python=3.8 -y conda activate eemd pip install numpy pandas matplotlib scikit-learn pip install tensorflow==2.10.0 pip install PyEMD这段命令创建了一个名为 eemd 的独立虚拟环境,后面所有依赖都装在这个环境里,不会污染系统基础 Python。tensorflow==2.10.0是刻意锁版本,因为 2.11 之后 Windows 上的 GPU 支持方式有变化,2.10 对课程设计和毕设场景来说足够稳定。scikit-learn用来算 RMSE、MAPE 这类评估指标,PyEMD提供 EEMD 分解函数。
3.2 数据文件与工作目录
资源里两个 csv:焦作.csv 和焦作全.csv。焦作全.csv 一般是完整字段或更长时间跨度的数据,焦作.csv 可能是清洗后供主程序直接运行的版本。拿到资源后先别急着跑,用 pandas 看一眼两份数据的结构。
import pandas as pd df = pd.read_csv("焦作.csv", encoding="gbk") print(df.columns.tolist()) print(df.shape) print(df.head())这里指定encoding="gbk"是因为部分来源的 csv 用 GBK 编码导出,不指定会直接报 UnicodeDecodeError。打印列名和 shape 是为了确认数据是单列还是多列,如果读出来列名乱码,就把编码换成utf-8再读一次。数据里有时间列的话,先转成 datetime 类型并按时间升序排列,EEMD 分解对序列顺序很敏感。
3.3 先做一次冒烟测试
在跑完整训练之前,我习惯先用一个很短的程序确认 EEMD 能分解、TensorFlow 能正常调用,避免把时间浪费在环境问题上。
import numpy as np from PyEMD import EEMD signal = np.sin(np.linspace(0, 20, 200)) + np.random.randn(200) * 0.1 eemd = EEMD() imfs = eemd.eemd(signal) print("IMF shape:", imfs.shape) import tensorflow as tf print("TensorFlow version:", tf.__version__)这段代码用 200 个点的合成信号做分解测试,imfs.shape应该是(分量数, 200)。如果这里能正常输出,说明 PyEMD 安装没问题;TensorFlow 能打印版本号,说明环境基本可用。冒烟测试不追求精度,只求链路通。
3.4 参数化编程的入口
这份源码的亮点之一是参数集中管理。打开 EEMD-LSTM.py,文件顶部一般会有一组类似下面的全局参数,换数据集时主要改这里。
| 参数名 | 常见取值 | 作用 |
|---|---|---|
| LOOK_BACK | 12 或 24 | 滑动窗口长度,决定用过去多少个点预测下一个点 |
| EPOCHS | 50 ~ 200 | 每个 IMF 子模型的训练轮数 |
| BATCH_SIZE | 16 或 32 | 批大小,影响收敛速度和显存占用 |
| TRIALS | 100 | EEMD 中加入白噪声的试验次数,越大越稳定但越慢 |
| NOISE_WIDTH | 0.05 | 白噪声幅值比例,一般取 0.01 ~ 0.1 |
| TEST_SPLIT | 0.2 | 训练集与测试集划分比例 |
参数化编程最大的好处是:做课程设计答辩时,老师问“你调过哪些参数”,你可以直接说改过哪几个值、效果怎么变,而不是含糊地说“跑了一下”。这几个参数里,TRIALS 和 NOISE_WIDTH 影响分解质量,LOOK_BACK 影响模型记忆长度,优先级高于 EPOCHS。
4. 源码拆解:从 EEMD 分解到 LSTM 预测的完整通读
4.1 读取数据与 EEMD 分解
主程序的第一步是读取目标列并做归一化,然后调用 EEMD 分解。归一化推荐用 MinMaxScaler,把数值压到 0 到 1 之间,LSTM 的 tanh 激活函数在这个区间内梯度特性最好。
from sklearn.preprocessing import MinMaxScaler from PyEMD import EEMD data = df["value"].values.astype(float) scaler = MinMaxScaler(feature_range=(0, 1)) data_norm = scaler.fit_transform(data.reshape(-1, 1)).ravel() eemd = EEMD(trials=100, noise_width=0.05, parallel=True) imfs = eemd.eemd(data_norm) print("分解得到分量数:", imfs.shape[0], "每个分量长度:", imfs.shape[1])这里trials=100表示做 100 次带噪声的 EMD 试验后取平均,试验次数越多模态混叠抑制越好,但耗时线性增长。noise_width=0.05是白噪声标准差相对原始信号的幅值比例,太小起不到消除混叠的作用,太大会把真实信号淹没。parallel=True在多核 CPU 上能明显缩短分解时间,但如果你在 Windows 下遇到进程启动报错,就把它改回 False。
EEMD 分解后,imfs是一个二维数组,第一维是分量个数,最后一维通常是残差项,对应整体趋势。分量个数由算法根据数据复杂度自动决定,一般在 6 到 10 个之间。
4.2 滑动窗口样本构造
LSTM 不能直接吃一维序列,需要把序列变成“特征 + 标签”的监督学习格式。这份资源里会有一个类似下面的函数,这是整个程序里最容易写错的地方。
import numpy as np def create_dataset(series, look_back=12): X, y = [], [] for i in range(len(series) - look_back): X.append(series[i:i + look_back]) y.append(series[i + look_back]) return np.array(X), np.array(y)代码里series[i:i+look_back]取的是从 i 开始的连续 look_back 个点作为输入,series[i+look_back]是紧跟在窗口后面的那个点作为标签。注意循环范围是len(series) - look_back,不是len(series) - look_back - 1,否则标签会超出索引。构造完成后,把 X reshape 成(样本数, look_back, 1)才能送入 LSTM。
LOOK_BACK 的取值要结合数据本身的周期。如果数据有明显季节性,比如以 24 小时为周期,就取 24;没有明显周期就取 12 到 30 之间做对比实验。窗口越大,模型能看到的上下文越长,但训练样本数会减少。
4.3 LSTM 建模与训练
每个 IMF 分量会训练一个独立的 LSTM 模型,模型结构保持一致,方便批量循环训练。典型结构是两层 LSTM 加一个全连接输出层。
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout def build_model(look_back=12): model = Sequential() model.add(LSTM(units=64, return_sequences=True, input_shape=(look_back, 1))) model.add(Dropout(0.2)) model.add(LSTM(units=32, return_sequences=False)) model.add(Dropout(0.2)) model.add(Dense(units=1)) model.compile(optimizer="adam", loss="mse") return model第一层 LSTM 设return_sequences=True,目的是让输出保留完整的时间步,供第二层 LSTM 继续处理;第二层设return_sequences=False,只输出最后一个时间步的隐状态,然后接 Dense 层映射到预测值。units=64和units=32是隐状态维度,数据量大可以加大到 128,数据量小就维持 64 以下。Dropout 放在 LSTM 层之间,用来抑制过拟合,对高频 IMF 尤其有效。
训练时把每个 IMF 的数据切分成训练集和测试集,然后循环训练:
from sklearn.model_selection import train_test_split train_loss = [] for i, imf in enumerate(imfs): X, y = create_dataset(imf, look_back=12) X = X.reshape((X.shape[0], X.shape[1], 1)) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, shuffle=False) model = build_model(look_back=12) history = model.fit( X_train, y_train, epochs=80, batch_size=32, validation_data=(X_test, y_test), verbose=1) train_loss.append(history.history["loss"][-1])这里刻意用shuffle=False,时间序列的样本顺序不能随机打乱,否则未来信息会混进训练集,造成数据泄漏。每个 IMF 单独训练、单独保存模型对象,后续预测时再一一对应。如果机器性能一般,把epochs降到 50,训练时间会明显缩短。
4.4 预测、反归一化与还原
训练完成后,对每个 IMF 的测试集做预测,把所有预测结果相加还原成最终预测值。叠加前必须做逆归一化,否则数值范围不对,画出来的图会很难看。
pred_parts = [] for i, imf in enumerate(imfs): X, y = create_dataset(imf, look_back=12) X = X.reshape((X.shape[0], X.shape[1], 1)) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, shuffle=False) pred = model.predict(X_test) pred_parts.append(pred.ravel()) pred_sum = np.sum(pred_parts, axis=0) pred_inv = scaler.inverse_transform(pred_sum.reshape(-1, 1)).ravel()每个 IMF 都在同一套归一化参数下建模,叠加后的预测再做一次全局逆变换。这里有个细节:如果训练时是对每个 IMF 单独做归一化,那预测时也要各自逆变换后再叠加;如果训练时是对原始序列统一归一化再分解,那就最后做一次逆变换。资源源码里采用的是后一种方式,代码结构上更简洁。
4.5 出图与保存结果
最后把原始测试集和预测值画到一张图上,肉眼判断趋势拟合情况。建议把每个 IMF 的预测误差也打印出来,方便定位哪个分量拖了后腿。
import matplotlib.pyplot as plt plt.figure(figsize=(12, 4)) plt.plot(y_test_original, label="true") plt.plot(pred_inv, label="pred") plt.legend() plt.title("EEMD-LSTM Prediction") plt.savefig("result.png", dpi=200)如果整体预测曲线贴合得不错,但局部尖峰没跟上,说明高频 IMF 训练不足,可以单独提高高频分量的训练轮数或加大模型容量。这一步虽然简单,但很多时候能直接看出问题所在。
5. 避坑指南:数据泄漏、维度错位与版本冲突的四个典型翻车现场
5.1 验证集指标好到离谱,先查数据泄漏
现象上,测试集 RMSE 只有训练集的一半不到,预测曲线几乎和真实值重合,你会以为模型训得很好。实际上很可能是归一化时用了整段序列的均值和最大值,或者滑动窗口划分时shuffle=True把未来样本混进了训练集。
原因在于,MinMaxScaler 在整段数据上 fit 再切分,相当于模型看到了未来数据的统计信息;窗口乱序则直接把未来点的信息泄露给过去点,LSTM 在测试集上等于开卷考试。
正确做法是先切分再归一化:先用训练集的均值和极值 fit scaler,再用同一个 scaler 去 transform 测试集。代码上直接改成在 train_test_split 之后再做归一化即可。滑动窗口划分一律shuffle=False,这是时间序列任务的铁律。
5.2 分解结果每次运行都不一样,特征拼接时维度对不上
现象上,第一次跑分解出 7 个分量,第二次变成 8 个,第三次又变回 7 个。你把所有 IMF 拼成多维特征时,维度对不上直接报错。
原因是 EEMD 引入随机白噪声,分解结果天然带有随机性;加上停止条件受数值精度影响,边缘样本会在不同试验次数下多拆或少拆一个分量。
解决方法是固定随机种子,或者在拆分后统一分量数。固定种子可以在 EEMD 调用前加一句np.random.seed(42),保证每次分解一致。如果换数据后分量数确实变了,可以在拼接特征时只取前 N 个 IMF 加残差,或者在循环里判断分量数并用线性插值补齐到目标维度。课程设计场景下,固定种子最简单也最稳。
5.3 TensorFlow 报 numpy 兼容错误,训练中途线程崩溃
现象上,import tensorflow 时提示_ARRAY_API not found,或者 EEMD 的parallel=True训练到一半进程直接崩溃。原因通常是 numpy 版本过新,TensorFlow 2.10 对应的 numpy 上限是 1.23 左右;多进程并行在 Windows 的 spawn 模式下也容易出问题。
解决方法是安装匹配版本:pip install "numpy<1.24",TensorFlow 就不会抱怨_ARRAY_API了。EEMD 那边把parallel=True改成parallel=False,牺牲一点分解速度换稳定性,对几百个点的数据来说差别不大。如果 GPU 版本有 cudnn 报错,检查一下 CUDA 和 cuDNN 的版本对应关系,资源用的 CPU 版 TensorFlow 基本不会遇到这个问题。
5.4 预测曲线整体右移或左移一个点
现象上,预测值和真实值的波形高度相似,但明显错开了一个采样周期。看起来模型“学到了规律但没对齐时间”,其实问题出在窗口构造时的标签偏移。
原因是在 create_dataset 里,如果y = series[i + look_back],预测的是“窗口结束后第一个点”;如果误写成y = series[i + look_back - 1],那标签就比真实下一时刻提前了一个点,画图时自然错位。
解决方法是检查数据集构造的标签索引,然后打印一组 X 和 y 人工核对:X 的最后一位是否等于 y 的前一位。这个小验证只要做一次,就能排除大量时间对齐问题。
5.5 高频 IMF 预测不动,误差全集中在高频分量
现象上,叠加还原后整体误差不大,但逐分量看,高频 IMF 的预测几乎是平的或严重滞后。原因是高频分量随机性强,LSTM 很难从中学到稳定映射,加上 Dropout 和归一化把原本就弱的振幅进一步压缩。
解决方法是把高频 IMF 的训练轮数单独调高,或者干脆不用 LSTM,改用简单移动平均或 AR 模型去拟合。组合模型的价值在低频和趋势分量,高频部分贡献的往往是噪声而非信号,强行用 LSTM 拟合反而引入额外误差。我在实际项目里会把最后一个高频 IMF 从 LSTM 预测链路里摘出去,直接沿用上一时刻值,整体误差反而下降。
6. 进阶验证:用误差分解和随机种子把模型调到可交付状态
模型跑通只是第一步,交作业或做项目前,我还会做三轮验证。第一轮是整体指标,计算测试集上的 RMSE 和 MAPE;第二轮是逐分量误差,确认误差到底来自趋势分量还是高频分量;第三轮是固定随机种子复跑两次,确认结果不是偶然。
from sklearn.metrics import mean_squared_error, mean_absolute_percentage_error rmse = mean_squared_error(y_test_original, pred_inv, squared=False) mape = mean_absolute_percentage_error(y_test_original, pred_inv) print(f"RMSE: {rmse:.4f}, MAPE: {mape:.4f}")如果 MAPE 超过 20%,先不要急着调 LSTM 参数,回头检查 EEMD 分解是否合理。比较稳健的做法是把每个 IMF 的预测误差单独打印出来,误差占比最大的分量通常是高频 IMF,这时可以降低它的权重,或者用滚动均值替代 LSTM 预测。整体框架不变,但预测结果会稳定不少。
进阶调参时,优先动 LOOK_BACK 和 TRIALS,而不是堆 LSTM 层数。把 LOOK_BACK 从 12 改成 24,观察 MAPE 变化;把 TRIALS 从 100 提高到 200,观察分解是否更稳定。这两组参数对结果的边际影响远超 units 和 Dropout。若要做多步预测,可以把 Dense 输出层改成Dense(horizon),标签从单点改成未来多个时间点,实现一步到位预测未来 7 天或 24 小时。
我现在的习惯是:拿到任何时间序列数据,先固定np.random.seed(42),再跑一遍 EEMD 看分量数,然后才敢动模型。这个流程看起来笨,但能避免大量“这次能跑下次不能跑”的玄学问题。希望帮到你。
本文还有配套的精品资源,点击获取