简介这份资源给出基于CEEMDAN自适应噪声完备集合经验模态分解、WOA鲸鱼优化算法与LSTM长短期记忆网络相结合的时间序列预测完整Python实现包含可直接运行的完整源码与配套数据集。压缩包共3个文件包括2个CSV数据文件焦作与焦作全分别用于训练与测试和1个PY主程序整体仅48KB轻量紧凑可配合Anaconda、Pycharm及TensorFlow快速搭建实验环境。代码采用参数化编程关键超参数集中配置便于修改编程思路清晰几乎逐行注释适合初学者理解从CEEMDAN信号分解、WOA参数寻优到LSTM建模预测的完整流程链路。目前已有285人学习下载尤其适合计算机、电子信息、数学等专业学生用于课程设计、期末大作业或毕业设计也可作为时间序列预测算法仿真的参考模板便于算法对比与二次开发。1. CEEMDAN-WOA-LSTM 时间序列预测为什么单跑 LSTM 不够这套组合把我误差压下去一半直接用 LSTM 做时间序列预测是 Python 里最常见的起点但真实数据跑完你会发现网络结构没问题预测曲线总是滞后半拍。原因不是模型笨而是负荷、寿命、客流这类序列天然非平稳、噪声强LSTM 学进去的往往不是规律是噪声。CEEMDAN-WOA-LSTM 的组合思路是把问题拆开先用 CEEMDAN 把原始序列自适应分解成多个 IMF 和一个残差再用 WOA 鲸鱼优化算法自动搜 LSTM 的超参数最后对主要分量分别预测再叠加。适合做电力负荷、设备寿命、气象等中短期预测的 Python 从业者也能帮你在时间序列预测这条路上把完整链路跑通。下面从头到尾说清楚怎么落地以及我踩过的坑。2. 用 CEEMDAN 分解原始序列数据预处理与 IMF 重构2.1 为什么是 CEEMDAN 而不是 EMD/EEMD三种分解的适用边界EMD 是经验模态分解的起点它能把非线性非平稳序列按时间尺度拆成若干本征模态函数 IMF。问题是模态混叠严重同一频率成分可能分到两个 IMF 里低频和高频相互纠缠后续 LSTM 每个分量都学不干净。EEMD 通过往原始序列里加白噪声再做平均来缓解混叠代价是白噪声残留在重构序列里分解结果不容易完全重构回原序列。你拿 EEMD 结果去做预测叠加回的误差会先污染一部分精度。CEEMDAN 的改进是加了自适应白噪声每次迭代都对当前残差加噪声并重新做 EMD最终得到的 IMF 是多次分解的平均噪声残留被压低重构误差也小得多。在 Python 生态里直接能用的库是 PyEMD 中的 CEEMDAN 类没有太多额外依赖。选它的理由说白了比 EMD 稳定比 EEMD 干净。对小样本时间序列尤其值得用因为它不依赖随机初始化的局部收敛结果可复现。2.2 复制可跑的 CEEMDAN 分解代码参数与输出检查常见做法是直接用 PyEMD 的 CEEMDAN 类完成分解。下面这段代码是我每次开工前的模板包含读取序列、分解、重构误差检查import numpy as np import pandas as pd from PyEMD import CEEMDAN # 读取序列date 列为时间value 列为观测值 df pd.read_csv(series.csv, parse_dates[date]) series df[value].values.astype(float) # 注意原文序列里缺失值必须先处理否则分解会乱掉 series np.nan_to_num(series, nannp.nanmedian(series)) # 实例化 CEEMDAN ceemdan CEEMDAN(trials100, epsilon0.005) # 分解返回 2D 数组每行一个 IMF imfs ceemdan(series) # PyEMD 返回的 imfs 不含残差需要自己算 residue series - np.sum(imfs, axis0) # 重构误差检查正常应接近 0 recon_err np.max(np.abs(series - (np.sum(imfs, axis0) residue))) print(重构误差:, recon_err) # 保存各分量后面还要用 np.savez(ceemdan_result.npz, imfsimfs, residueresidue)参数里最值得调的是trials和epsilon。trials是加噪声的次数设太小模态不稳定设太大计算变慢我一般取 50 到 100epsilon是噪声幅值相对输入序列标准差的系数默认是 0.005数据噪声强才改成 0.01 到 0.02否则分解出的第一个 IMF 会吃掉过多高频成分。输出检查要看两点。第一重构误差应该在1e-8甚至更小如果误差大多半是imfs和residue的计算方式不对。第二把imfs按行画出来高频分量在顶部低频在底部如果看到某个 IMF 波形剧烈突变说明序列里有离群点没处理干净回去先做去噪。这一环节不必追求完美核心目标是让每个 IMF 成为相对平稳的分量好让 LSTM 学得动。2.3 把每个 IMF 和残差拼成监督学习数据集训练集/验证集/测试集划分分解完的数据不能直接丢进 LSTM要先把一维序列转成监督学习形式用过去lookback个点预测未来horizon个点。我一般写一个通用窗口函数def make_windows(series_in, lookback24, horizon6): x, y [], [] for i in range(len(series_in) - lookback - horizon 1): x.append(series_in[i:i lookback]) y.append(series_in[i lookback:i lookback horizon]) return np.array(x).reshape(-1, lookback, 1), np.array(y).reshape(-1, horizon)lookback设为 24 适合小时级数据等于把一整天当上下文horizon设为 6 表示一次预测未来 6 个点。如果你的数据是日粒度lookback可以放大到 30 或 60horizon视业务需要决定。窗口构造有个小细节reshape(-1, lookback, 1)的最后一维是特征数单变量序列就写 1后面如果加入温度、节假日等外生变量这里要多留维度。切分数据集要守住一条铁律不打乱时间顺序。对每个 IMF 和残差分别构造窗口后按时间顺序切 70% 训练、15% 验证、15% 测试。验证集用来给 WOA 当适应度评估测试集只做最终评测。有些人在整个序列上先归一化再切分这会在后面引入数据泄露测试集信息提前溜进训练过程第 5 章会细讲。3. 用 WOA 优化 LSTM 超参数鲸鱼算法怎样搜索最佳配置3.1 LSTM 超参难调在哪隐藏单元数、学习率、批大小、时间步之间的相互牵制LSTM 神经网络可调的旋钮比普通全连接多隐藏单元数、学习率、批大小、时间窗长度、层数、Dropout。最麻烦的是它们互相牵制学习率大了隐藏单元多容易发散小了又训不动批大小影响梯度稳定性时间窗再一变模型整体行为全变。手动调参通常就是玄学试几个组合看谁验证集误差小但每一组都要完整训练一轮人力和时间成本都很高。WOA 解决的是“超参数搜索”这件事。它把一组超参数当成解空间里的一个坐标用鲸鱼捕食的收缩包围、气泡网和随机搜索三种行为去更新坐标目标函数就是“按这组超参数训练一个 LSTM在验证集上的 RMSE”。你只需要定义好搜索边界和适应度函数剩下的交给算法迭代。相比网格搜索它在高维连续空间里不靠穷举对 LSTM 这种一次训练成本高的场景是常见做法。3.2 WOA 鲸鱼算法 Python 实现包围、气泡网、随机搜索下面是一个精简但能跑的 WOA 实现适应度函数由外部传入搜索维度支持自定义。核心逻辑在位置更新上import numpy as np def woa(objective, dim4, lbNone, ubNone, agents10, max_iter15): lb np.array(lb, dtypefloat) ub np.array(ub, dtypefloat) # 随机初始化种群 positions np.random.uniform(lb, ub, (agents, dim)) fitness np.array([objective(pos) for pos in positions]) best_idx np.argmin(fitness) best_pos positions[best_idx].copy() best_fit fitness[best_idx] for t in range(max_iter): # a 从 2 线性降到 0 a 2.0 - 2.0 * t / max_iter for i in range(agents): r1, r2 np.random.random(), np.random.random() A 2 * a * r1 - a C 2 * r2 p np.random.random() l np.random.uniform(-1, 1) if p 0.5: if abs(A) 1: # 收缩包围 D abs(C * best_pos - positions[i]) new_pos best_pos - A * D else: # 随机搜索 rand_idx np.random.randint(agents) D abs(C * positions[rand_idx] - positions[i]) new_pos positions[rand_idx] - A * D else: # 气泡网攻击对数螺旋更新 D abs(best_pos - positions[i]) new_pos D * np.exp(1.0 * l) * np.cos(2 * np.pi * l) best_pos # 边界处理 new_pos np.clip(new_pos, lb, ub) new_fit objective(new_pos) if new_fit fitness[i]: positions[i] new_pos fitness[i] new_fit # 更新全局最优 best_idx np.argmin(fitness) if fitness[best_idx] best_fit: best_fit fitness[best_idx] best_pos positions[best_idx].copy() return best_pos, best_fit参数含义agents是鲸鱼数量也就是超参数组合的候选个数取 8 到 15 之间max_iter是迭代代数我一般先设 10 到 15。A大于 1 或小于 -1 时算法倾向随机搜索避免陷入局部最优p 0.5走包围或随机否则走螺旋。这套更新没有复杂的算子和矩阵求逆直接可以用。注意边界处理用的是np.clip但超参数有不同类型隐藏单元数应是整数学习率应是对数尺度。建议在目标函数内部做转换隐藏单元int(round(param))学习率10 ** param这样搜索空间的数值范围更平滑WOA 收敛更快。3.3 设计适应度函数训练 LSTM 并返回验证集误差适应度函数是整套 WOA 的灵魂。它要做三件事把鲸鱼坐标翻译成 LSTM 参数训练一个最短生命周期模型返回验证集上的损失。示例def objective_function(params): hidden int(round(params[0])) lr 10 ** params[1] batch_size int(round(params[2])) lookback int(round(params[3])) # 用当前超参构建 LSTM 模型代码见第 4 章 model build_lstm(lookbacklookback, hiddenhidden, horizonhorizon) model.compile(lossmse, optimizerAdam(learning_ratelr)) # 训练轮数固定避免因早停导致超参对比不公平 model.fit(x_train, y_train, batch_sizebatch_size, epochs20, verbose0, validation_data(x_val, y_val)) y_pred model.predict(x_val, verbose0) return np.sqrt(np.mean((y_val - y_pred) ** 2))需要留意的点有三个。第一训练轮数固定且较小比如 20 到 30 轮WOA 关注的是超参相对优劣不需要让每个候选模型训到收敛否则一轮优化要跑几百次训练时间上受不了。第二验证集必须是同一份并且不能参与训练否则超参搜到的是“记住验证集”的模型。第三verbose0关掉输出否则屏幕会被刷屏。三维和四维哪个更推荐我一般设四维隐藏单元、学习率、批大小、时间窗。Layer 数和 Dropout 手动固定为常用值因为维度越多WOA 收敛越慢且每次评估越贵。想要快速跑通时只搜隐藏单元和学习率两个维度误差通常只比全搜索差一点但速度快几倍。4. 搭建 LSTM 预测模型并训练Keras 实现与预测结果重构4.1 每个 IMF 单独建模还是拼接两种模型结构怎么选有一种做法是把分解后的 IMF 当作多通道输入拼成一个 LSTM但我不推荐。LSTM 的多通道输入要求每个时间步上所有通道对齐而 CEEMDAN 分解出的分量频率差异大直接拼接会让网络把精力花在学习分量间的关系上反而丢失各自的时序模式。更稳的做法是“每个 IMF 训练一个 LSTM预测值相加”。这样每个模型只学一个相对平稳的分量LSTM 更容易捕捉到周期性。代价是模型数量多分解出 K 个 IMF 就有 K1 个模型残差也算一个。实际操作时我通常做一个小优化先看各 IMF 的方差贡献只对能量占比前 3 到 4 个分量单独建 LSTM其余分量和残差合并成一个“残差项”直接放进一个简单模型。这样模型总数从 7 个压到 4 个训练时间少一半测试集误差往往变化不大。等你把单分量的流程跑通再按这个思路扩展。4.2 LSTM 模型定义与训练可直接复制的 Python 代码这一节给出能直接用的 LSTM 模型代码适合单变量每个 IMF 的分量预测from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout, Input from tensorflow.keras.optimizers import Adam from tensorflow.keras.callbacks import EarlyStopping def build_lstm(lookback, hidden64, horizon6, n_features1, dropout0.2): model Sequential() model.add(Input(shape(lookback, n_features))) model.add(LSTM(unitshidden, return_sequencesTrue)) model.add(Dropout(dropout)) model.add(LSTM(unitshidden // 2, return_sequencesFalse)) model.add(Dense(horizon)) return model def train_model(model, x_train, y_train, x_val, y_val, lr0.001): model.compile(lossmse, optimizerAdam(learning_ratelr)) callbacks [ EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) ] history model.fit( x_train, y_train, epochs100, batch_size32, validation_data(x_val, y_val), callbackscallbacks, verbose1 ) return model, historyLSTM 第一层返回序列方便第二层继续提取时序特征第二层不返回序列直接进入输出层。hidden // 2让第二层容量减半减少过拟合。Input(shape(lookback, n_features))的lookback要和造窗口时的数值一致如果写错模型会直接报 shape 不匹配。训练时的早停patience10意味着验证损失连续 10 轮不下降就停止并回滚到最佳权重。这比固定跑 100 轮更稳因为不同超参组合的收敛速度差异很大早停能避免浪费。但要注意在第 3 章 WOA 的适应度函数里不要用 EarlyStopping原因前面说过每轮 epoch 数固定超参之间才有可比性。4.3 预测值重构与误差评估RMSE、MAE、MAPE 怎么算训练完所有分量模型后把每个分量的预测值相加就得到原始尺度上的预测。反归一化的顺序要放在相加之后统一做。我习惯把归一化也放进流程from sklearn.preprocessing import MinMaxScaler def predict_ensemble(imf_models, imfs_test, residue_model, residue_test, scalers): pred_sum np.zeros((len(imfs_test[0]), horizon)) for i, model in enumerate(imf_models): # imfs_test[i] 是该分量的监督测试集 x pred_part model.predict(imfs_test[i], verbose0) pred_sum scalers[i].inverse_transform(pred_part) pred_sum residue_model.predict(residue_test, verbose0) return pred_sum指标计算代码from sklearn.metrics import mean_squared_error, mean_absolute_error 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) # 真实值有 0 时 MAPE 会无穷大用 SMAPE 替代 smape np.mean(2 * np.abs(y_true - y_pred) / (np.abs(y_true) np.abs(y_pred))) return rmse, mae, smape评估时最容易忽略的问题是“相位对齐”多步预测中第h个预测点要和真实值中对应的第h个未来点对齐。如果你直接拿整段预测和原始序列对齐头尾会有偏移RMSE 虚高。建议只评估从lookback horizon到测试集末尾这一段舍弃最前面无法对齐的点。5. 避坑指南CEEMDAN-WOA-LSTM 的常见翻车现场5.1 数据泄露归一化在切分前还是切分后直接决定指标真实现象测试集 RMSE 好看得不像话曲线几乎贴着真实值但换一段未来数据就崩。原因你很可能在分解和切分之前对整条序列做了 MinMaxScaler。归一化时用了全序列的最大值和最小值测试集的分布信息提前渗进了训练数据。LSTM 实际是在“知道未来范围”的条件下做预测当然准。解决先把序列按时间切好训练、验证、测试三段再分别 fit 和 transform。对 CEEMDAN 分解尽量在原始尺度上分解再对每个分量的训练段单独做归一化测试段用训练段的 scaler 转换。这个顺序改动很小但指标从自欺欺人变成真实可用。5.2 CEEMDAN 边界效应端点漂移导致开头几个预测点跳变现象预测曲线的最后一段整体稳定但每个 IMF 的开头几十个点出现明显抖动叠加后预测值在测试起点处突然跳一下。原因CEEMDAN 的包络和均值计算在序列两端数据不足端点处边界效应放大。分解时两端分子被扭曲LSTM 学到的是一个歪掉的起点。解决在做窗口之前对分解后的每个 IMF 和残差丢弃前几个不稳定点比如丢掉前 1% 或固定 10 个点。如果数据量紧张可以使用信号延拓或镜像延拓后再分解分解完裁掉延拓部分。我通常选择直接丢点反正丢失的样本量很少对训练影响可忽略。5.3 WOA 收敛但 LSTM 误差没下降适应度函数设计有问题现象WOA 每代的最优适应度确实在下降但最后调出来的参数用在测试集上比随便手调的还差训练过程还极其漫长。原因适应度函数里没有限制训练成本每个候选超参都要训 100 轮一组优化跑下来几十小时。另一原因是搜索空间设置不合理比如学习率用线性范围 0 到 1实际有效区间是 0.0001 到 0.01算法在无效空间里空转。解决学习率改成对数尺度lr 10 ** param搜索边界设 -4 到 -1。训练轮数压低到 20 到 30并且在验证集上评估不要用训练集损失。WOA 的目标是找到相对好的区域不是绝对最优精度差距可以在后续精调中补。5.4 结果不可复现LSTM 每次都跑出不一样的结果现象同一个脚本跑两遍预测曲线完全两样评价指标相差 10% 以上你没法判断是模型变好还是运气。原因网络权重初始化、训练数据打乱、GPU 上的浮点运算都有随机性。WOA 本身也涉及随机种群初始化不固定种子整条链路都飘。解决在脚本最前面固定随机种子import random import numpy as np import tensorflow as tf np.random.seed(42) random.seed(42) tf.random.set_seed(42)这能保证常规 CPU 和 GPU 环境下结果可复现。需要注意不同 PyTorch/TensorFlow 版本的行为仍有差异跑对比实验时记录环境版本号否则别人复现不了数值也会对不上。5.5 分解后预测叠加反而比单个 LSTM 差相位差叠加问题现象每个 IMF 单独预测时 RMSE 都还行叠加起来却比直接用一个完整 LSTM 预测还要差。网上很多文章不提醒这一点实际跑的时候很容易翻车。原因每个分量的预测都有独立相位误差加法叠加时误差不会抵消反而可能同向叠加。尤其在 horizon 较大时高频 IMF 的微小偏差被放大低频分量又带来滞后两相叠加精度恶化。解决只对能量占比高、周期性明显的 IMF 单独建模型把高频低能量分量直接并入残差。另一个思路是分量化简把频率相近的 IMF 合并成一个分量再预测减少模型之间的相位误差累积。如果测试集误差仍然不降回到第 2 章重看分解参数优先检查第一个 IMF 是否吃掉太多噪声。6. 进阶验证与部署用滚动预测检验模型泛化能力模型在单个测试集上指标好不算数要模拟真实上线场景。常见做法是做滚动预测每次用已知数据预测未来一段等真实值到达后把真实值并入历史窗口再预测下一段。这样能暴露出模型在连续预测中的误差累积。代码思路history list(train_series) preds [] for i in range(len(test_series) - horizon 1): x np.array(history[-lookback:]).reshape(1, lookback, 1) pred best_model.predict(x, verbose0) preds.append(pred[0, 0]) # 把真实值推进历史而不是把预测值推进去 history.append(test_series[i])注意最后一行用的是真实值。如果改成把预测值推进历史误差会一路累积很快发散这能同时验证模型的稳定性和你对“在线更新”策略的理解。数据量大时可以考虑把 WOA 的迭代次数和种群数量减半只优化最重要的两个超参数其余沿用上次结果这样热更新一轮的成本可控。预测结果建议统一保存到 CSV 或 Excel方便后续对比分析import pandas as pd out_df pd.DataFrame({time: test_time, true: y_true_flat, pred: y_pred_flat}) out_df.to_csv(ceemdan_woa_lstm_predict.csv, indexFalse)我现在处理新的时间序列问题习惯先把 CEEMDAN-WOA-LSTM 作为默认 benchmark 跑一遍再用更复杂的模型去挑战它。这个组合虽然训练耗时但胜在稳定和直观误差高时你知道该看分解还是看超参不会一头扎进黑匣子里瞎试。希望这一套从分解、优化到预测的完整路径能帮到你。本文还有配套的精品资源点击获取