简介:面向Python时间序列预测学习者的完整实现方案,提供CEEMDAN信号分解、WOA鲸鱼优化与LSTM预测模型一体化源码,针对非平稳、非线性数据预测难题,可直接应用于课程设计、期末大作业或毕业设计。代码采用参数化编程,关键参数可灵活修改,算法流程与TensorFlow训练细节配有接近逐行的保姆级注释,极大降低小白入门门槛。资源包共3个文件,包含1个主程序py和2个csv数据文件,分别存放原始序列与测试样本,压缩包整体仅48KB,轻量易用。代码从数据读取、CEEMDAN分解、WOA寻优到LSTM训练与预测均有清晰模块划分,使用者可替换自有数据、调整超参数,快速迁移到电力负荷、交通流量、气象等场景。目前已有285人学习下载,适合希望借助完整源码理解分解-优化-预测链路,并在此基础上完成实验扩展与二次开发的研究者。
1. 用 CEEMDAN-WOA-LSTM 做时间序列预测:先把数据拆明白,再让鲸鱼替 LSTM 找参数
“焦作.csv”到手,典型的气象或水文时间序列。想预测未来一段时间的变化,第一反应是把序列直接丢给 LSTM。LSTM 确实擅长捕捉长短期依赖,但这类原始序列往往噪声和趋势混叠,直接训练要么收敛慢,要么预测结果比真实曲线滞后一拍——这是纯 LSTM 做单变量预测最常见的翻车点。
压缩包里的方案是:先用 CEEMDAN 把序列自适应分解成多个本征模态函数 IMF 和残差,再用 WOA 鲸鱼算法自动寻优 LSTM 超参数,最后逐项预测并叠加重构。适合正在做课程设计、毕业设计和论文复现的 Python 用户,环境是 Anaconda 加 TensorFlow,源码近乎一行一注释,数据是规范 CSV,下载后改几行路径就能跑通。
2. CEEMDAN 分解:把混叠序列拆成规则分量,PyEMD 实操与 IMF 筛选
2.1 为什么先用 CEEMDAN 而不是直接上 LSTM
大多数时间序列预测教程会让新手直接构造监督样本喂给 LSTM,模型确实能学到一些模式,但遇到带尖峰、趋势突变和多周期叠加的数据时,单一网络很难同时拟合低频趋势和高频扰动。EMD 类方法的价值在于把原始序列按频率自适应拆开,每个 IMF 在某个尺度上更平稳,LSTM 只需要针对单个 IMF 做相对单一的映射,最后叠加,单步预测难度会明显下降。
EEMD 是对 EMD 的改进,通过加入白噪声来避免模态混叠,但它分解出的 IMF 数量和噪声水平不稳定,同一段数据跑两次可能得到不同结果。CEEMDAN 在 EEMD 的基础上引入自适应噪声,每一轮都根据残差重新计算添加的噪声量,IMF 分量更干净,重构误差也更小,所以工程上更常用。另一个常被拿来对比的方法是 VMD,但 VMD 需要预设模态数 K 和惩罚因子 alpha,序列特征未知时这两个参数本身就是优化问题;CEEMDAN 不需要预设 K,分解完自然得到一组 IMF 加一个残差。对于“尽可能少人工干预”的自动寻优流程,CEEMDAN 更匹配。
2.2 PyEMD 安装与分解代码
先装依赖。PyEMD 在 PyPI 上的包名很容易踩坑,安装命令和导入命令不一样:
pip install EMD-signal这里的包名是 EMD-signal,导入时却用 PyEMD。如果你用的是 Anaconda,也可以在 conda-forge 里搜 emd-signal,效果一样。装完以后在终端验证一下导入是否成功:
from PyEMD import CEEMDAN print(CEEMDAN)能打印出类对象,说明环境正常。接着写分解代码:
import pandas as pd import numpy as np from PyEMD import CEEMDAN, Visualisation # 1. 读取CSV,中文表头优先用gbk df = pd.read_csv("焦作.csv", encoding="gbk") print(df.columns) # 先看一眼列名,下面按实际列名取 series = df.iloc[:, 1].values.astype(float) # 取第2列,按需调整 # 2. 初始化CEEMDAN ceemdan = CEEMDAN(trials=100, epsilon=0.005) imfs = ceemdan(series) # shape: (n_imfs, len(series)) # 3. 画IMF图,确认每个分量形态 vis = Visualisation() vis.plot_imfs(imfs, series)几个参数按数据长度和噪声程度调整。trials 是集合平均次数,控制每次加噪重复分解的次数,数据短可以减到 50,长序列建议提到 200;调小省时间,但 IMF 可能带残留噪声。epsilon 是噪声幅值系数,0.001 到 0.01 之间取值,调高会让低频分量更平滑,但也可能把一些细节抹掉;这套数据用 0.005 表现正常。注意 astype(float) 这步不能省,CSV 里哪怕有一个缺失值变成 NaN,后面 CEEMDAN 会直接报警或产出全 NaN 的 IMF。
提示:CSV 里一旦出现缺失值,CEEMDAN 会把 NaN 传染给所有 IMF,先 fillna 再分解。
2.3 IMF 筛选:哪些留下预测,哪些当噪声剔除
不是所有 IMF 都值得喂给 LSTM。高频 IMF 通常对应噪声或瞬时扰动,强行预测这部分会让叠加结果抖动明显,还会拖慢整体训练。我一般用两个指标做筛选:
- 过零率:单位长度内符号正负交替的次数,高频噪声明显高于趋势分量。
- 相关系数:每个 IMF 与原始序列的 Pearson 相关系数,低于阈值的视为弱相关。
# 过零率计算:相邻符号发生变化即为一次过零 zero_cross = np.sum(np.diff(np.sign(imfs), axis=1) != 0, axis=1) / imfs.shape[1] # 每个IMF与原始序列的相关系数 corr = [np.corrcoef(imfs[i], series)[0, 1] for i in range(len(imfs))] print("过零率:", zero_cross) print("相关系数:", corr) # 假设前两个高频IMF被判定为噪声,其余叠加 denoised = imfs[2:].sum(axis=0)判定时别把相关系数当成绝对标准。有的 IMF 相关系数不高,但过零率也低,说明它是低频缓变信号,仍值得参与预测;有的 IMF 相关系数中等却过零率极高,多半是噪声。我习惯把过零率超过所有 IMF 均值三倍以上的判为噪声,然后结合相关系数做二次确认。预处理结束后,denoised 就是下一步喂给 WOA-LSTM 的输入序列。顺手保存一份:
pd.DataFrame({"denoised": denoised}).to_csv("denoised.csv", index=False)这样不用每次重跑分解。
3. WOA 寻优 LSTM 超参数:units、学习率与时间步长的三参数优化
3.1 三个参数凭什么值得优化
LSTM 调参空间很大,但单变量时间序列预测这个任务里,真正影响结果的是三个参数:隐藏层神经元数 units、优化器学习率 lr、监督样本构造的时间步长 look_back。
- units 决定网络容量。太小拟合不了复杂映射,太大不仅慢,还容易把噪声也背下来。
- lr 控制梯度下降步长。Adam 默认 0.001 是通用值,但不同数据和网络深度下,最优值经常在 0.0001 到 0.01 之间漂移。
- look_back 决定用过去多少个点预测下一个点。它直接决定输入矩阵形状,选小了模型看不到足够上下文,选大了引入大量冗余历史。
三个参数互相耦合。手动调参每次改一个参数就要重训练一遍,网格搜索更是几十种组合起跳。WOA 的价值在于把寻优建模成鲸鱼找猎物,用较少的评估次数在三维空间收敛到一组可用参数。实践中通常两三百次评估内就能看到结果,相比网格搜索动辄上千次训练,省下的时间很明显。
有个常见误区是顺手把 LSTM 的层数、Dropout、batch_size 也丢进 WOA 一起优化。不是不行,而是在硬件有限的情况下,搜索维度越高收敛越慢,维度灾难在这里同样成立。先用三个主要参数拿到基线,再手动调一两个次要参数,性价比最高,这比我以前瞎试参数那种玄学路子要靠谱得多。
3.2 WOA 核心机制与位置更新实现
完整概念这里不展开,只说代码里必须理解的三件事:
- 包围猎物:鲸鱼向当前最优位置收缩,对应代码里的 A 系数。
- 气泡网攻击:以螺旋轨迹接近猎物,兼顾局部精细搜索。
- 随机搜索:A 的绝对值大于等于 1 时,鲸鱼向随机个体移动,避免过早收敛到局部最优。
import numpy as np def woa_lstm(pop, dim, lb, ub, max_iter, obj_func): # 初始化鲸鱼种群 positions = np.random.uniform(lb, ub, (pop, dim)) fitness = np.array([obj_func(p) for p in positions]) best_idx = np.argmin(fitness) best_pos, best_fit = positions[best_idx].copy(), fitness[best_idx] for t in range(max_iter): a = 2 - 2 * t / max_iter # a从2线性降到0 for i in range(pop): r1, r2 = np.random.rand(), np.random.rand() A = 2 * a * r1 - a C = 2 * r2 p = np.random.rand() if p < 0.5: if abs(A) < 1: D = np.abs(C * best_pos - positions[i]) positions[i] = best_pos - A * D # 包围猎物 else: rand_idx = np.random.randint(pop) rand_pos = positions[rand_idx] D = np.abs(C * rand_pos - positions[i]) positions[i] = rand_pos - A * D # 随机搜索 else: D = np.abs(best_pos - positions[i]) l = np.random.uniform(-1, 1) positions[i] = D * np.exp(2 * l) * np.cos(2 * np.pi * l) + best_pos # 螺旋更新 positions[i] = np.clip(positions[i], lb, ub) # 边界约束 fitness = np.array([obj_func(p) for p in positions]) if fitness.min() < best_fit: best_fit = fitness.min() best_pos = positions[np.argmin(fitness)].copy() return best_pos, best_fit代码逻辑不复杂:a 从 2 线性下降到 0,前期 A 绝对值大,偏向全局探索,后期偏局部开发。p 是 0 到 1 的随机数,决定走螺旋还是包围,两种策略随机切换。np.clip 把越界鲸鱼拉回搜索空间,这一步必须做,否则可能出现负学习率导致模型编译失败。obj_func 接收一个三维参数向量,返回验证集 MSE,适应度越小代表参数组合越好。
这类基础 WOA 代码网上有很多版本,区别主要在细节:有的用 Levy 飞行增强探索,有的加入惯性权重。项目源码里用的是标准 WOA 结构,对新手更好读,也更容易改成你自己的优化器。
3.3 适应度函数与 LSTM 训练封装:归一化顺序别搞错
适应度函数是整个 WOA 和 LSTM 的桥梁。常见做法是构造完监督样本后,训练一个短 epoch 的 LSTM,用验证集 MSE 作为适应度。这里归一化的顺序很讲究,如果先 fit_transform 全部数据再切分,验证集信息提前泄漏进训练集,选出来的参数在真实场景会水土不服。
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense from tensorflow.keras.optimizers import Adam from sklearn.preprocessing import MinMaxScaler from sklearn.metrics import mean_squared_error def obj_func(params): units, lr, look_back = int(params[0]), float(params[1]), int(params[2]) X, y = create_samples(denoised, look_back) # 构造监督样本 split = int(len(X) * 0.8) X_train, X_val = X[:split], X[split:] y_train, y_val = y[:split], y[split:] model = Sequential() model.add(LSTM(units, input_shape=(look_back, 1))) model.add(Dense(1)) model.compile(optimizer=Adam(learning_rate=lr), loss="mse") model.fit(X_train, y_train, epochs=30, batch_size=32, verbose=0) pred = model.predict(X_val).flatten() # 反归一化到原始尺度再算MSE,避免量纲误导 pred_real = scaler.inverse_transform(pred.reshape(-1, 1)).flatten() y_val_real = scaler.inverse_transform(y_val.reshape(-1, 1)).flatten() return mean_squared_error(y_val_real, pred_real)这里有个细节容易被忽略:int(params[0]) 和 int(params[2]) 必须做,否则 LSTM 层收到浮点神经元数会直接抛异常。学习率用 float(params[1]) 接收,后续 Adam 才能正常解析。create_samples 函数负责把一维序列转成监督矩阵,look_back 为几,就用前几个时间点预测下一个点。epochs 在寻优阶段控制在 30 以内,数据量大时甚至 20 就够,先让 WOA 收敛;拿到最优参数后,再用 100 epoch 精训练一轮做最终预测。
选 MSE 还是 MAPE 当适应度取决于业务口径。MSE 对大幅值误差敏感,MAPE 对量纲变化不敏感但遇到接近零的真实值会爆炸。我建议默认 MSE,输出结果时再算 MAPE 给业务看,别让两个指标混着当寻优目标。
4. 避坑排错:PyEMD 安装失败、预测滞后与 WOA 不收敛的排查记录
4.1 问题一:pip 装完 EMD-signal 后 import 仍然失败
现象:pip install EMD-signal 显示 Successfully installed,进 PyCharm 写 from PyEMD import CEEMDAN 却报 ModuleNotFoundError: No module named 'PyEMD'。
原因:pip 包名和导入模块名不一致是最大的坑,PyPI 上搜 PyEMD 会指向别的包,正确包名是 EMD-signal。另一个常见原因是 PyCharm 的解释器与终端解释器不是同一个环境,pip 装进了 base 环境,项目里用的却是另一个虚拟环境。这条坑我自己踩过最多次,几乎每个新手都中招。
解决:先用终端确认当前解释器路径,再对照 PyCharm 设置:
python -c "import sys; print(sys.executable)" pip show EMD-signal如果路径不一致,在 PyCharm 的 Project Interpreter 里切换到同一个解释器,或者直接在项目终端重装。装完跑一句验证:
from PyEMD import CEEMDAN print(CEEMDAN)打印出类对象才算通。Python 3.10 以上的机器还可能遇到 pyhht 编译失败,报一堆 gcc error,这时优先用 conda 从 conda-forge 安装,或者退回 Python 3.8 环境,这是目前最省力的两条路。
4.2 问题二:预测曲线比真实曲线滞后一拍
现象:训练损失很低,预测曲线与真实曲线形状几乎重合,但明显向右平移了一两个点,像一条“延迟复制版”。
原因:时间序列预测最容易误判成成功的失败模式。两种来源最常见:一是监督样本构造时 look_back 和预测步长不一致,模型学会把上一时刻值搬运到下一时刻;二是 CEEMDAN 分解后没做 IMF 筛选,模型在低频趋势部分发现“复制上一个点”误差已经很小,就没有再学复杂映射。
解决:先用一个简单诊断确认是不是滞后:
# 最佳滞后诊断,结果接近0正常 lag = np.argmax(np.correlate(pred_real, y_val_real, "full")) - len(pred_real) + 1 print("最佳滞后:", lag)lag 明显大于 0 时,优先剔除高频噪声 IMF 再做重构,其次考虑把预测目标从一步改成多步滚动输出,让模型必须依赖长程上下文而不是相邻复制。还有一个验证技巧:打印预测值与真实值前十项的差分,如果差分基本是原序列的滞后差分,基本可以断定模型在“背答案”。
4.3 问题三:WOA 迭代几十轮,适应度一动不动
现象:best_fit 打印了两三百轮几乎没变化,甚至比前面迭代还差一点。
原因:最常见的是学习率搜索范围太宽,Adam 在 lr=0.01 量级反复震荡,每个组合训练出的模型都没收敛,适应度自然被拉平。其次是鲸鱼种群太小,30 只个体在三维空间里探索能力有限,很快聚到同一个局部最优。
解决:把 lr 的搜索边界从 [0.0001, 0.01] 缩到 [0.0005, 0.005],并做对数尺度采样:
lr = 10 ** np.random.uniform(-4, -2)同时把种群 pop 从 30 提到 50,max_iter 不小于 30。如果适应度是精确不变的常数,比如一直等于 0.3182,先别怀疑算法,单独把 obj_func 用一组手写参数跑一遍,多半是 int(params[0]) 传成了整个数组,参数类型转换写错了。
4.4 问题四:预测结果反归一化后数值整体偏小
现象:模型输出看着正常,反归一化后预测曲线比真实曲线低一大截,甚至整体偏移。
原因:MinMaxScaler 在切分之前就用 fit_transform 把全量数据拟合了,训练集混入未来信息,属于数据泄漏。虽然训练集上指标好看,验证集却失真,反归一化时幅值对不上。另一个隐蔽原因是 CEEMDAN 分解出的 IMF 有正有负,有人为了归一化提前取绝对值,导致重构后相位被破坏。
解决:先切分再归一化,scaler 只从训练集学习 min 和 max:
scaler = MinMaxScaler(feature_range=(0, 1)) train_scaled = scaler.fit_transform(train_real.reshape(-1, 1)) val_scaled = scaler.transform(val_real.reshape(-1, 1))预测输出用同一个 scaler.inverse_transform 还原。IMF 的负值是正常物理含义,不要取绝对值,否则后面叠加结果会漂。
5. 落地验证:损失曲线、残差自相关与纯 LSTM 对比,一个都不能少
5.1 两个图判断模型是否真的学到了规律
训练结束不要只看预测曲线。先看损失曲线:训练和验证损失同步下降并趋缓,是正常;验证损失降到低位后回升,是过拟合,应减少 epochs 或加 Dropout。再看残差的自相关:把预测值减真实值得到残差,画 autocorrelation_plot,如果残差自相关在滞后几步后掉进置信区间,说明模型已经把主要规律学走,残余部分基本是白噪声;如果残差还带着明显的周期性,说明有 IMF 没被模型利用好,或 look_back 太短。
import matplotlib.pyplot as plt from pandas.plotting import autocorrelation_plot plt.figure(figsize=(10, 3)) plt.subplot(1, 2, 1) plt.plot(history.history["loss"], label="train") plt.plot(history.history["val_loss"], label="val") plt.legend() plt.subplot(1, 2, 2) autocorrelation_plot(residual) plt.show()5.2 轻量对比实验:纯 LSTM 与 CEEMDAN-WOA-LSTM 的同一验证集测试
验证这套组合拳是否值得,最直接的办法是跑一组对比。纯 LSTM 固定 look_back=12、lr=0.001、units=50,不分解、不寻优;另一边走完整流程,两者用同一段验证集记录 RMSE 和 MAPE:
| 模型 | RMSE | MAPE |
|---|---|---|
| 纯 LSTM | 记录值A | 记录值B |
| CEEMDAN-WOA-LSTM | 记录值C | 记录值D |
不必预设分解一定赢。数据本身干净、周期单一的情况下,分解反而可能丢掉残差信息。这套组合真正擅长的是带噪声、多周期叠加的序列,先分解再寻优,才能体现出“单尺度映射”的优势。
从那以后我每次拿到新数据,都会强制按“分解 → 筛 IMF → 归一化 → 预测 → 反归一化 → 残差自相关”的顺序走一遍,确认没踩滞后陷阱才敢把结果交出去。这套流程里最费时间的往往不是训练,而是中间每一步都能向自己解释清楚“为什么这么做”。希望帮到你。
本文还有配套的精品资源,点击获取