简介:这是一份面向Python开发者的EEMD-LSTM时间序列预测完整源码与数据集,项目将集成经验模态分解和长短时记忆网络相结合,可有效处理非平稳、非线性时间序列数据,适用于风速、负荷、交通流量等预测场景。压缩包内共3个文件,包括1个主程序文件以及2个csv格式的数据文件,整体大小仅47KB,结构精简但完整。代码采用参数化编程,几乎每一行都配有注释,清晰展示数据读取、EEMD分解、LSTM模型构建、训练与预测全过程,方便小白对照学习;环境基于Anaconda、PyCharm和TensorFlow搭建,修改参数或替换数据即可迁移到其他实际任务。该资源适合计算机、电子信息、数学等专业学生用于课程设计、期末大作业或毕业设计,也适合入门深度学习时序建模的读者,目前已有383人学习下载,轻量而实用,具有很好的参考与扩展价值。
1. 为什么 EEMD-LSTM 会比单一 LSTM 更能打
拿焦作.csv 这种以小时为粒度的监测序列直接喂给 LSTM,收敛慢,预测到突变点时基本是跟着上一时刻的值平移,峰值明显滞后。原因是趋势、周期、随机扰动三种成分叠在一起,门控单元难以同时拟合多尺度信息,梯度反而被高频抖动干扰。
EEMD 先把序列拆成从高频到低频的多个 IMF 和一个残差趋势项,每个分量尺度相对单一,再交给 LSTM 逐个建模,最后把各分量预测叠加还原。这套"分解-预测-重构"结构在时间序列预测里很通用,适合课程设计、期末大作业和毕业设计,负荷预测、流量预测都能复用同一套代码骨架。
源码包里除了 EEMD-LSTM.py 还有两份 CSV 数据。入门的人看逐行注释就能跑通,有经验的人重点盯分解参数、窗口构造和重构对齐三处,这套方案最容易被改坏的地方全在这。
2. EEMD 分解原理与 PyEMD 实战:先把非平稳序列拆干净
2.1 模态混叠是怎么来的,EEMD 为什么有效
EMD 的核心操作是迭代地寻找上下包络、取包络均值、从原信号中筛出本征模态函数 IMF,分解结果天然按频率从高到低排列。但原始 EMD 有一个被反复吐槽的缺陷:模态混叠。一个 IMF 里同时出现时间尺度相差很大的成分,或者同一个尺度被切碎散落在多个 IMF 里,根源是原始信号里存在间歇性扰动,包络拟合被局部突变带偏。
EEMD 的思路很直接:单次分解对扰动敏感,那就人为加白噪声重复分解再平均。白噪声频谱在整个频带均匀分布,相当于给间歇性成分一个均匀的"底座",多次试验后噪声在平均中相互抵消,剩下的是稳定的模态。正是这一步让 EEMD 在非平稳时间序列预测中大量替代了普通 EMD,也是这个源码把 EEMD 放在 LSTM 前面的原因。
两个核心参数决定分解质量。trials 是集成次数,也就是加噪后重复做 EMD 的次数,常见取值 50~200;次数太低噪声消不干净,每次运行得到的 IMF 都不一样。noise_width 是白噪声标准差与原始信号标准差的比值,取 0.05~0.2 比较常见;太小起不到抗混叠作用,太大会把白噪声残留在分解结果里。具体影响看下面这张表:
| 参数 | 常见范围 | 偏小的后果 | 偏大的后果 |
|---|---|---|---|
| trials | 50~200 | 模态不稳定,结果不可复现 | 训练耗时成倍增长,收益递减 |
| noise_width | 0.05~0.2 | 抗混叠效果不足,IMF 纯度变差 | 白噪声残留在 IMF 中,重构误差变大 |
2.2 PyEMD 安装与核心分解代码
环境是 Anaconda + PyCharm + Python + TensorFlow。装 PyEMD 时有个坑:pip install pyemd装的是另一个完全不同的库,正确包名是EMD-signal,引入路径才是PyEMD。安装命令:
pip install EMD-signal下面这段就是把焦作.csv 拆成 IMF 的完整逻辑:
# -*- coding: utf-8 -*- import numpy as np import pandas as pd from PyEMD import EEMD # 读取数据:焦作.csv 第一列是时间,第二列是要预测的监测值 df = pd.read_csv('焦作.csv', encoding='utf-8') series = df.iloc[:, 1].values.astype(float) # 构造 EEMD 分解器 eemd = EEMD(trials=100, noise_width=0.08) # trials=100:做 100 次加噪 EMD 再平均,模态更稳定 # noise_width=0.08:白噪声幅度取原信号标准差的 8% imfs = eemd.eemd(series) # 返回二维矩阵,形状为 (n_imf, len(series)) # 最后一行是残差趋势项,不是严格意义上的 IMF np.save('imfs.npy', imfs) print('IMFs shape:', imfs.shape)读数据时建议先把 DataFrame 的列名打出来确认目标变量在第几列,有些 CSV 的列顺序和预期不一致会直接导致分解结果变成垃圾。eemd()返回的二维数组第一维是模态数量,第二维和原始序列等长,最后一行是残差。把残差也当 IMF 送进 LSTM 不是不行,但残差接近单调趋势,用 LSTM 建模收益很低,后面会专门说怎么处理。
2.3 IMF 筛选:不是每个分量都值得建模
分解完先别急着训练。把每个 IMF 的方差贡献率算一遍,能量占比极低的高频分量大多是噪声主导,预测它们既增加训练时间,又把抖动误差叠加进最终重构结果。我一般先筛掉贡献率低于 1% 的分量:
# 计算每个 IMF 的方差贡献率 energy = np.var(imfs, axis=1) ratio = energy / energy.sum() keep_idx = [k for k, r in enumerate(ratio) if r > 0.01] imfs_used = imfs[keep_idx] print('保留的 IMF 序号:', keep_idx)筛选之后残差单独处理。常见做法是残差用一阶差分加线性外推,或者也走一遍 LSTM;如果训练集只有几百条,直接忽略残差只保留能量高的几个 IMF 也能接受,代价是趋势段的预测会略微偏移。先算方差贡献率再决定保留哪些分量,比凭眼睛看波形靠谱得多。
3. LSTM 预测器设计与 TensorFlow 参数细节
3.1 为什么分解之后仍然选 LSTM,而不是 ARIMA
分解把多尺度混叠拆开了,但每个 IMF 仍然是非平稳的调幅振荡。高频 IMF 的方差随时间变化,低频 IMF 的周期也在缓慢漂移,ARIMA 这类线性模型对调幅振荡的适应性很差。LSTM 的遗忘门在每个时间步决定保留多少历史信息,对"幅值在变、局部有规律"的信号拟合效率比纯线性模型高。
在 EEMD-LSTM 源码里,每个保留的 IMF 对应一个结构完全相同的 LSTM 模型,只是输入输出的尺度不同。这种"同构模型、独立训练"的设计优点是训练快、可以顺序跑不抢显存,也不存在多任务学习里常见的梯度相互干扰。每个模型学到的只是自己那个频段的时间依赖,最后相加时互不污染。
3.2 滑动窗口构造与归一化的顺序
LSTM 的输入是一个固定长度的窗口,窗口长度 lookback 决定模型能看到多长的历史。小时级数据取 12 或 24,能覆盖半天到一天的周期;如果是低频 IMF,可以适当加大。构造窗口的代码:
def make_windows(data, lookback): X, y = [], [] for i in range(lookback, len(data)): X.append(data[i - lookback:i]) # 前 lookback 个点作为特征 y.append(data[i]) # 当前点作为标签 return np.array(X), np.array(y)归一化放在建窗之前,并且分 IMF 单独做。每个 IMF 的幅值差异很大,共用一个 MinMaxScaler 会让高频分量在数值上被压制。更关键的是fit只能用训练段数据:
from sklearn.preprocessing import MinMaxScaler split = 2400 # 训练/测试切分点,按行索引 scaler = MinMaxScaler() scaler.fit(imf[:split].reshape(-1, 1)) # 只用训练段算 min/max scaled = scaler.transform(imf.reshape(-1, 1)) # 再变换整段序列 X, y = make_windows(scaled.flatten(), lookback)注意fit和transform的对象不能反过来。先在全序列上 fit 再切训练测试,等于把测试集的数值范围提前泄露给了 scaler,测试误差会被系统性低估。这种顺序问题在时间序列预测的复现里是最常见的失分点,代码能跑、损失在降,但实验结论不成立。
3.3 模型结构与参数速查表
每个 IMF 的模型用两层 LSTM 加 dropout,第一层返回完整序列给第二层,第二层只输出最后一个时间步的特征,最后接 Dense(1) 输出预测值:
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout def build_lstm(lookback): model = Sequential([ LSTM(64, activation='tanh', return_sequences=True, input_shape=(lookback, 1)), Dropout(0.2), LSTM(32, activation='tanh', return_sequences=False), Dropout(0.2), Dense(1) ]) model.compile(optimizer='adam', loss='mse', metrics=['mae']) return model第一层return_sequences=True是为了把每个时间步的隐状态都传给第二层;第二层return_sequences=False只保留最后一步,正好对应预测目标。激活函数保持 tanh 默认,LSTM 的循环计算对 tanh 的输出范围依赖很强,改成 relu 容易出现数值爆炸。
| 参数 | 推荐值 | 调整方向 |
|---|---|---|
| lookback | 12 | 数据周期明显时取 24 或 48 |
| 第一层单元数 | 64 | 训练量小于 500 条时降到 32 |
| 第二层单元数 | 32 | 跟随第一层同比例缩放 |
| dropout | 0.2 | 过拟合时提到 0.3~0.5 |
| batch_size | 16 | 序列长、显存够时提到 32 |
| epochs | 30~50 | 配早停,不要硬编码固定值 |
| 优化器学习率 | adam 默认 1e-3 | 不收敛时降到 5e-4 |
注意:每个 IMF 训练前都要重新初始化权重,不能复用上一个模态训练好的模型。不同 IMF 的统计特性差异很大,复用权重会把低频分量学到的时间依赖错误地带到高频分量上。
4. 焦作数据集的完整训练流程:分解、预测、重构
4.1 两份 CSV 的分工与数据预处理
压缩包里给了焦作.csv 和焦作全.csv 两份数据。常见分工是全量文件用于 EEMD 分解,短文件用于训练和测试。EEMD 存在端点效应,序列越短两端越不稳定,用全量序列做分解能明显减轻这个问题;分解完成后,按训练集的行范围截取对应的 IMF 片段再训练。
预处理先做缺失值和时间对齐。CSV 数据里偶尔有空值和重复时间戳,稳妥的做法是按时间列排序,再用前后两个有效值的线性插值填充空缺:
df = df.sort_values('time').reset_index(drop=True) df.iloc[:, 1] = df.iloc[:, 1].interpolate(method='linear')interpolate默认就是线性插值,对监测类数据足够。不要用填充常数的方式补缺失,那会引入人为的阶梯,LSTM 会把阶梯当成真实的趋势去学习,预测段会出现台阶状误差。
4.2 分解—训练—预测主循环
把前面的逻辑串起来就是 EEMD-LSTM.py 的主循环。对每个保留的 IMF 独立完成归一化、建窗、训练、预测,最后把预测结果叠加:
lookback = 12 pred_parts = [] for k, imf in enumerate(imfs_used): # 每个 IMF 单独归一化,避免幅值差异互相压制 scaler = MinMaxScaler() scaler.fit(imf[:split].reshape(-1, 1)) scaled = scaler.transform(imf.reshape(-1, 1)).flatten() X, y = make_windows(scaled, lookback) # X[i] 用区间 [i-lookback, i-1] 预测第 i 个时刻 X_train, y_train = X[:split - lookback], y[:split - lookback] X_test, y_test = X[split - lookback:], y[split - lookback:] model = build_lstm(lookback) model.fit(X_train, y_train, epochs=40, batch_size=16, validation_split=0.1, verbose=0) model.save(f'lstm_imf_{k}.h5') # 每个 IMF 的模型单独存盘 pred_scaled = model.predict(X_test) pred = scaler.inverse_transform(pred_scaled).flatten() pred_parts.append(pred) print(f'IMF {k}: 训练完成') # 重构:各分量预测值直接相加 final_pred = np.sum(pred_parts, axis=0) y_true = y[split - lookback:]注意切分索引。make_windows返回的 X[i] 对应的标签是 y[i],也就是第 i 个时刻的真实值。训练段取到split - lookback之前,保证标签全部落在训练区间内;测试段从split - lookback开始,第一个测试窗口恰好使用训练段末尾的 lookback 个点,这是一步预测的正常用法,不算数据泄露。
4.3 重构结果的误差评估
叠加完成后用三个指标衡量,RMSE 对大幅误差敏感,MAE 反映平均偏差,R² 看整体拟合度:
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score rmse = float(np.sqrt(mean_squared_error(y_true, final_pred))) mae = float(mean_absolute_error(y_true, final_pred)) r2 = float(r2_score(y_true, final_pred)) print(f'RMSE={rmse:.4f} MAE={mae:.4f} R2={r2:.4f}')在焦作这类监测数据上,EEMD-LSTM 相对单一 LSTM 的改进主要体现在 RMSE 上,因为大幅度的趋势项被单独建模,峰值位置的误差比单一模型小。常见结果大致落在下面这个区间,具体数值随数据长度和划分方式浮动:
| 方案 | RMSE 相对变化 | MAE 相对变化 | 说明 |
|---|---|---|---|
| 单一 LSTM | 基准 | 基准 | 峰值滞后约 1~2 步 |
| EEMD-LSTM 保留全部 IMF | 下降 15%~30% | 下降 10%~25% | 模型数量多,训练时间长 |
| EEMD-LSTM 筛掉低能量 IMF | 与上一行接近 | 略降 | 训练时间明显减少 |
如果跑出来的改进不明显,先检查是不是跳过了 IMF 筛选。残差项的预测误差如果很大,叠加时会污染所有高频分量的成果,这是复现 EEMD-LSTM 效果不佳最常见的原因。
提示:分解步骤耗时且带随机性,
imfs_used存成 .npy 后在调模型参数阶段就不用重新分解了。
5. 调参技巧与三种验证方法
调参时先动分解参数还是模型参数?我的答案很明确:先动分解。下面三个验证手段能帮你在动手训练之前就把问题定位到具体环节。
5.1 分解参数的两个坑
noise_width 取到 0.2 以上时,白噪声会残留在 IMF 里,分解结果看起来平滑,但重构回去和原序列的误差变大。我判断噪声是否残留的方法是:把全部 IMF 加回来看重构误差,如果和原序列的 RMSE 超过原序列标准差的 2%,说明噪声占比偏高,把 noise_width 往 0.05~0.1 调。trials 低于 50 时,两次运行的 IMF 数量可能不一致,结果不可复现;正式实验用 100 起步,时间允许就上 200。
5.2 验证一:重构残差检查分解是否彻底
分解完先做一次重构验证,再谈模型:
reconstruct = np.sum(imfs_used, axis=0) residual = series[:len(reconstruct)] - reconstruct print('重构 RMSE:', np.sqrt(np.mean(residual ** 2)))如果重构误差已经很大,后面所有预测分析都没有意义,问题一定出在分解阶段,而不是 LSTM。这一步 10 秒跑完,能省下大量排查模型的时间。
5.3 验证二:单步预测的逐点稳定性
模型跑通后,把每个 IMF 的预测按时间画子图,观察高频 IMF 的预测线是围绕真实线小幅抖动,还是整体偏移。如果某个 IMF 的误差远大于其他 IMF,单独对这个分量加大 lookback 或降低学习率重训,不要动全局参数。高频分量对 lookback 更敏感,低频分量对学习率更敏感,这个规律在分解粒度越细时越明显。
5.4 验证三:固定随机种子做可复现对比
对比 EEMD-LSTM 和单一 LSTM 时,先用tf.random.set_seed(42)固定种子,再跑三次取平均。LSTM 初始化是随机的,不加种子得到的对比结论很可能是噪声。另外把每个分量的预测结果存成单独一列 CSV,最后对比单个分量的 RMSE,能快速定位误差集中在哪个频率段,再针对性调整那个分量对应的模型参数。
EEMD-LSTM 的调参优先级是:分解参数先于模型参数,IMF 筛选先于层数堆叠。多数项目的问题不是 LSTM 不够深,而是分解质量不够干净。
本文还有配套的精品资源,点击获取