简介这一Python源码包提供CEEMDAN-WOA-LSTM组合模型的时间序列预测完整实现适合需要完成课程设计、期末大作业或毕业设计的计算机、电子信息、数学等专业学生也适合刚接触深度学习预测的初学者。项目以TensorFlow为后端在Anaconda与PyCharm环境下可直接运行代码经过参数化设计并配有保姆级逐行注释便于理解信号分解、鲸鱼优化算法与LSTM网络结合的完整流程。压缩包共含3个文件包括2个csv数据文件与1个Python主程序整体大小仅48KB小巧完整能快速用于焦作地区相关数据的预测实验。目前已有285人学习浏览内容覆盖从数据加载、CEEMDAN分解、WOA超参数寻优到LSTM建模预测与结果评估的完整链路。作者为资深算法工程师资源后续也可按需扩展定制适合作为入门复现和二次开发的基础。1. 三个算法叠一起是炫技还是真能提精度做时间序列预测的工程师手里通常有两类工具一类是信号分解把乱七八糟的非平稳序列拆成相对规矩的分量一类是神经网络让LSTM去学这些分量的时序规律。但单用LSTM拟合原始序列遇到强非平稳、强噪声的数据经常翻车——突变点被平滑掉趋势和波动揉在一起互相干扰。CEEMDAN-WOA-LSTM这套组合就是先把序列用CEEMDAN分解成若干本征模态函数IMF和残差再用WOA鲸鱼算法去搜LSTM的隐藏层神经元数、学习率、正则化系数这些超参数最后对每个分量分别建模预测、叠加输出。它解决的不只是“换个网络”的问题而是把“数据预处理”和“模型选参”两个环节同时纳入优化这在设备寿命预测、电力负荷预测、量化交易这类长序列场景里往往比单纯调LSTM省下大量试错时间。这套组合的代价也很直观训练链路长、中间产物多、调参维度翻倍。如果你只是拿平稳的、周期性明显的序列做预测直接上LSTM甚至线性回归就够了没必要叠这三层。但如果你的数据带明显趋势项、季节项和随机噪声的混合且历史规律不直观这套“分解-优化-预测”的流程就值得动手复现。下文我会讲清楚每一步在做什么、参数怎么定、中间有哪些坑并给出一套可直接跑的完整代码结构。2. CEEMDAN分解为什么是它以及分解出来之后怎么办2.1 EMD、EEMD到CEEMDAN的演进逻辑要理解CEEMDAN得先从EMD说起。EMD经验模态分解能把任意信号分解成一组从高频到低频的IMF每个IMF都要满足极值点数和过零点数相等或至多差一个、且上下包络均值局部为零这两个条件。它不需要预设基函数这是它最大的吸引力——你不需要知道数据是周期性的还是趋势性的分解过程自己适应数据。但EMD有个著名毛病模态混叠。一个IMF里可能同时出现不同时间尺度的成分或者同一个尺度被拆到相邻两个IMF里。EEMD的解决方案是往原始信号里加白噪声做多次EMD再平均利用噪声的统计特性来“均匀化”极值点分布。代价是计算量大、且每次加噪声产生的IMF数量可能不一致对齐困难。CEEMDAN在这里做了一个关键改进它不是在原始信号上反复加噪声而是在每一阶IMF的分解残差上自适应地加噪声并且一旦某个IMF被提取出来全局只提取一次不再参与后续迭代。这意味着分解结果具有完备性和一致性——同样的参数下不同批次分解出的IMF数量和形态是稳定的这一点对后续的LSTM建模至关重要你的训练集和测试集如果分解形态不一致模型输入分布就不稳定预测结果根本没有可解释性。2.2 Nstd和MaxIter这两个参数决定了你分解出什么CEEMDAN在PyEMD库中的核心参数是Nstd噪声标准差与原始信号标准差的比值和MaxIter每个IMF提取的最大迭代次数另有EnsembleSize表示集成次数。这三个参数直接影响IMF的数量和形态是第一个值得花时间调的点。常见的参数设定是Nstd0.005, EnsembleSize100, MaxIter500但根据我的经验Nstd设得太小如0.001分解结果接近普通EMD模态混叠问题没解决干净设得太大如0.05噪声成分会渗入IMF出现“拆过头”的伪分量。对一般的设备振动信号、油温数据、股价序列Nstd在0.0020.01之间是比较稳的区间。MaxIter不是越大越好超过1000之后IMF形态变化极小但耗时线性增长500800是性价比合理的区间。from PyEMD import CEEMDAN # 部分版本库导入方式不同若上面导入失败可尝试 # from PyEMD import EEMD 后使用 EEMD().ceemdan() import numpy as np # 生成示例信号趋势项 周期项 噪声 t np.linspace(0, 1, 1000) signal 2 * t np.sin(2 * np.pi * 5 * t) 0.5 * np.random.randn(len(t)) ceemdan CEEMDAN() ceemdan.Nstd 0.005 # 噪声系数一般 0.001~0.01 ceemdan.MaxIter 500 # 最大筛迭代次数 ceemdan.EnsembleSize 100 # 集成次数越大越稳定但越慢 imfs ceemdan(signal) # imfs[-1] 为残差项 print(f分解出 {len(imfs)} 个分量分别对应从高频到低频的IMF与残差)在这段代码里imfs是一个二维数组每一行是一个IMF分量。首要检查项是每个IMF是否大致从高频到低频排列残差是否是单调趋势。如果中间某个IMF的均值明显不为零或频率分布和相邻分量重叠严重说明Nstd偏大或集成次数不足。事件发生后可以把该序列的频谱画出来对比IMF中心频率确认没有混叠后再进入下一步。2.3 分解结果不是全都要喂给LSTM——重构思路这是整套方案里最容易走弯路的地方。许多刚接触CEEMDAN的工程师会把每个IMF都单独训练一个LSTM再叠加输出结果模型数量爆炸训练耗时翻了数倍精度却没有显著提升。原因在于CEEMDAN会把高频噪声拆成若干个零均值、类随机过程的IMF这类分量本身没有强时序相关性LSTM对它们做预测几乎等同于在拟合噪声收益极低。我采用的策略是先做分量筛选和重构。具体步骤是对每个IMF计算其与原始序列的相关系数并观察其频谱特征。相关系数高且Lomb-Scargle周期图上有显著峰值的IMF保留相关系数极低且类似白噪声的IMF直接剔除。接下来把保留的IMF按频率范围合并成24个重构分量例如高频组细节波动、低频组主周期、趋势项残差再分别建模。这样可以把模型数量控制在35个又能保留原始信号的绝大部分有效信息。至于合并方式有人用样本熵做聚类有人用皮尔逊相关系数筛选实操中都不复杂。一个比较直接的经验是分解出的前几个IMF往往是噪声主导直接看方差占比就能识别——若某个IMF的方差不到原始序列方差的1%它对最终预测的贡献几乎可以忽略丢掉它还能减少过拟合风险。3. WOA鲸鱼算法LSTM的超参数搜索不再是玄学3.1 WOA优化LSTM到底在优化什么LSTM需要人为设定的超参数很多隐藏层神经元数、层数、学习率、批大小batch size、训练轮数epochs等。常规做法是网格搜索或随机搜索但这两者要么维度爆炸要么效率低下。WOA的核心是模拟座头鲸的泡泡网捕食行为将候选解看作鲸鱼个体通过包围猎物、螺旋气泡网攻击和随机搜索三种机制不断迭代更新解的位置。在CEEMDAN-WOA-LSTM这个框架中WOA的优化目标和搜索空间需要提前定义。常见做法是把每个待优化超参数映射为鲸鱼位置向量的一个维度然后以验证集上的均方误差MSE或平均绝对误差MAE为适应度函数让WOA去最小化这个指标。需要说明的是WOA并不是深度学习圈子里最“先进”的优化器但它没有需要梯度信息、没有复杂的协方差矩阵估计对离散和连续混合的超参数空间处理起来非常直接在工程上比贝叶斯优化更容易实现和控制。当前这套流程中我一般优化的参数是LSTM隐藏层神经元数范围32256、学习率范围0.00010.01取对数坐标、dropout比例0.10.5、批大小1664。层数通常固定为2因为对CEEMDAN分解后的分量而言单层或双层LSTM已经足够捕捉时序依赖层数过多反而容易过拟合分解后的低频分量。3.2 适应度函数怎么定义最合理适应度函数的定义决定了WOA在朝哪个方向优化。最朴素的写法是直接返回验证集的MSE但这样有一个隐患——WOA可能在训练过程中“记住”了验证集的噪声得到一组泛化能力差的参数。我的做法是将一次训练的最终验证集MSE、训练集MSE的差值也纳入适应度防止过拟合参数胜出。import numpy as np from math import inf def fitness_function(params, build_model_func, X_train, y_train, X_val, y_val): lstm_units, lr, dropout, batch_size params # 每次评估都新建模型防止上一轮权重残留 model build_model_func(lstm_unitsint(lstm_units), lr10**lr, dropoutdropout) # 训练轮数固定不做早停保证评估公平 history model.fit(X_train, y_train, batch_sizeint(batch_size), epochs30, validation_data(X_val, y_val), verbose0) val_loss min(history.history[val_loss]) train_loss min(history.history[loss]) # 如果验证损失很低但训练损失也异常低说明可能过拟合给予惩罚 return float(val_loss 0.3 * abs(val_loss - train_loss))这段代码需要注意三个点。第一lr10**lr表示WOA搜索到的数值是一个对数刻度这是处理学习率这种跨数量级参数的常用手段能防止鲸鱼在0.01到0.001之间的微小区间内无效跳动。第二每次评估都重置模型并固定训练轮数否则WOA会分不清精度的提升来自参数还是来自随机初始化。第三0.3 * abs(val_loss - train_loss)是泛化惩罚项当模型在训练集和验证集上差距过大时整体适应度变差避免选到纯记忆型参数组合。3.3 WOA主循环包围、螺旋与随机搜索的平衡WOA的标准实现不长核心在于位置更新公式的选择。当|A| 1时鲸鱼向当前最优个体收缩包围当p 0.5时鲸鱼沿螺旋路径向最优个体靠近当|A| 1时鲸鱼随机选择一个个体进行全局探索。这个“探索-开发”的平衡在超参数搜索中非常关键——搜索前期希望鲸鱼广泛试探不同的超参数组合后期则收缩到最优区域精细搜索。def woa_optimize(fitness_func, dim4, pop_size8, max_iter20): # 参数边界: [units, log10(lr), dropout, batch_size] lb np.array([32, -4, 0.1, 16]) ub np.array([256, -2, 0.5, 64]) # 初始化种群 positions np.random.rand(pop_size, dim) * (ub - lb) lb scores np.array([fitness_func(p) for p in positions]) best_idx np.argmin(scores) best_pos positions[best_idx].copy() for t in range(max_iter): a 2 - 2 * t / max_iter # a 从 2 线性衰减到 0 for i in range(pop_size): r1, r2 np.random.rand(2) A 2 * a * r1 - a C 2 * r2 p np.random.rand() 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(pop_size) rand_pos positions[rand_idx] D abs(C * rand_pos - positions[i]) new_pos rand_pos - A * D else: D abs(best_pos - positions[i]) new_pos D * np.exp(1) * np.cos(2 * np.pi * 1) best_pos positions[i] np.clip(new_pos, lb, ub) scores np.array([fitness_func(p) for p in positions]) if scores.min() fitness_func(best_pos): best_pos positions[scores.argmin()].copy() return best_pos这段WOA代码是教材级的简化实现在实际使用中你需要把max_iter20这种规模往上调。以我跑设备温度预测数据的经验种群数量812、迭代次数3050是起步档位。注意每个个体都要完整训练一个LSTM模型所以一次优化实验的时间成本是“个体数×迭代次数×单次模型训练时间”动辄几个小时很正常。要缩短耗时可以把训练轮数从30降到15作为预筛选选出少量候选后再用全量轮数精跑这样总时间可减少40%以上。4. 完整训练与预测流程从数据切分到结果叠加4.1 数据格式与滑窗切分LSTM输入的三维结构LSTM的输入要求是[样本数, 时间步长, 特征数]三维结构这是初学者最容易卡住的地方。一个常见误区是想当然地把二维表格数据直接塞进模型——如果你报错说维度不对先检查数据形状。以单变量预测为例假设你有1000个时间点的样本使用60个历史时间点预测未来第1个点单步预测那么按滑窗方式切分后训练数据的形状应为[941, 60, 1]标签形状为[941]。def create_sequences(data, seq_len60): X, y [], [] for i in range(len(data) - seq_len): X.append(data[i:i seq_len]) y.append(data[i seq_len]) return np.array(X), np.array(y) # 假设 data 是经过CEEMDAN分解并重构后的某个分量序列 # 每个分量单独调用该函数得到各自的训练样本 X, y create_sequences(data, seq_len60) split_idx int(len(X) * 0.8) X_train, X_test X[:split_idx], X[split_idx:] y_train, y_test y[:split_idx], y[split_idx:] # 归一化要分别拟合训练集的均值与标准差禁止用全局统计量 mean X_train.mean(axis(0, 1), keepdimsTrue) std X_train.std(axis(0, 1), keepdimsTrue) 1e-6 X_train (X_train - mean) / std y_train (y_train - mean[0, 0, 0]) / std[0, 0, 0] # 标签用同一组均值标准差这里有一个容易踩的细节归一化使用的均值和标准差必须全部来自训练集如果先对全序列归一化再切分验证集和测试集的信息会通过统计量泄露到训练过程中导致验证指标虚高。此类问题在时间序列预测中尤其隐蔽因为序列存在自相关性相邻时间点的数据分布高度相似泄露后的指标好看得让人误以为模型有效实则上线即失效。滑窗长度seq_len的选择也有讲究。LSTM捕捉的是窗口内的时序依赖窗口太短学不到长周期模式窗口太长则训练难度陡增、梯度传播路径变长。通常做法是先做自相关分析看序列的自相关系数在多少阶之后衰减到接近零以这个滞后阶数作为seq_len的参考下限。比如油温数据的自相关在滞后80步时显著下降我会设seq_len为96对于每日股价数据常用2030。4.2 分量建模与预测叠加重构的逆过程在2.3节中我们把分解后的分量重构为若干组现在对每一组分别训练LSTM模型并预测。注意CEEMDAN是在全序列上一次性分解的但你只能在训练集上做分解、把分解器“记住”然后对测试集做同样参数下的分解吗现实是残酷的——CEEMDAN分解并不像FFT那样是纯数学变换它依赖极值点分布新增的测试点会改变包络线形态导致全序列分解和滑动分解结果不一致。工程上常用两种妥协方案。第一种是全序列先做CEEMDAN分解再把分解后的分量按时间点切分成训练集和测试集。也就是说先分解后切分而不是先切分后分解。这种方式保证了每个分量都是完整的序列训练测试切分只是按时间截断。第二种是滚动预测时每次把最新数据纳入后再重新分解一次成本高但更严谨。对大多数离线预测任务第一种就够用。各分量预测结果按其所属的分解重构关系直接相加得到最终预测值。需要注意预测阶段所有分量都必须使用相同的滑窗方式、相同的训练测试切分比例否则时间点对齐不上相加时会出现“错位”现象——就像三个人合唱各唱各的拍子合起来反而是噪音。实际代码里可以在每个分量的预测结果末尾补零或截断到公共长度保证索引对齐。# 假设 preds 是字典key 为分量编号value 为该分量的预测序列 # 所有分量预测长度需保持一致 final_pred np.zeros_like(preds[0]) for key in preds: final_pred preds[key] # 反归一化 final_pred final_pred * std[0, 0, 0] mean[0, 0, 0] # 计算整体误差 from sklearn.metrics import mean_squared_error, mean_absolute_error mse mean_squared_error(y_test, final_pred) mae mean_absolute_error(y_test, final_pred) print(fMSE: {mse:.4f}, MAE: {mae:.4f})这一段看起来简单但“对齐”问题在实操中最容易出现。有一个血泪经验如果不同分量使用不同的滑窗长度有人为了让某些分量“看更长的历史”而单独调窗预测起始点和长度都会错开。我见过一个翻车案例其中4个分量有3个预测序列长度是N、1个是N-1直接相加报错后有人用循环补齐结果对不齐的索引导致预测曲线整体偏移了几个时间点最终评价指标惨不忍睹。所以所有分量的seq_len必须保持一致这比每分量单独优化窗口更重要。4.3 模型结构从单层到双层的取舍CEEMDAN分解后的分量复杂性从高频到低频递减。高频分量往往波动剧烈、规律性弱不需要复杂的网络结构低频分量和趋势项平滑序列规律强适当增加网络容量反而有效。我常见的做法是高频重构分量使用单层LSTM加Dropout3264个隐藏单元低频和趋势项使用双层LSTM单元数量由WOA在32256范围内搜索。使用Keras TensorFlow 2.x搭建模型的代码如下from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout def build_model(units64, lr0.001, dropout0.2): model Sequential([ LSTM(unitsunits, return_sequencesTrue, input_shape(seq_len, 1)), Dropout(dropout), LSTM(unitsunits // 2, return_sequencesFalse), Dropout(dropout), Dense(units1) ]) model.compile(optimizertf.keras.optimizers.Adam(learning_ratelr), lossmse) return modelLSTM(unitsunits, return_sequencesTrue)表示该层输出所有时间步的隐藏状态给下一层LSTM使用最后一层return_sequencesFalse只输出最后一个时间步的隐藏状态接一个全连接层输出预测值。units // 2是经验做法让第二层比第一层神经元少一半起到降维和抽象特征的作用。这段结构几乎不用改真正需要由WOA决定的只有units、lr、dropout三个值。5. 避坑指南CEEMDAN-WOA-LSTM的5个典型踩坑现场5.1 分解与切分的顺序错误导致数据泄露现象模型在验证集和测试集上表现极好但部署到新数据上效果断崖式下跌。原因先对全序列做了归一化和CEEMDAN分解再切分训练测试集。分解过程中测试段的包络线参与了IMF的提取等于模型在训练时“偷看”了测试段的形态信息。这是时间序列预测中最高级的隐性泄露方式之一——你很难察觉因为指标不会报错只会给你虚假的信心。解决严格遵循“先切分、后分解”或“全序列分解、但切分时保证每个IMF的边界效应不跨训练测试交界”的原则。在离线实验里我一般先切分训练测试段再分别做CEEMDAN分解。但这样训练段和测试段分解出的IMF数量可能不同为此可以让分解器的MaxIter保持一致、且都使用固定随机种子尽量让两端分解结果形态对齐。另一种做法是只对训练段分解并用包络外插的方式延拓到测试段工程上实现略复杂但对高频分量的预测精度有明显帮助。5.2 高频IMF预测无用功现象分解出8个IMFLSTM对前3个IMF的预测曲线基本是平线但MSE还不小。原因高频IMF接近随机噪声LSTM没有足够强的时序模式可学。网络退化为输出历史均值反而因为噪声方差大导致MSE偏高拉低了整体精度。解决引入相关系数阈值筛选。计算每个IMF与原始序列的皮尔逊相关系数低于0.1的直接剔除对于相关系数在0.10.3之间的高频分量可以尝试用简单基准如上一时刻值预测不用LSTM。这套方案做下来模型数量减少约30%总误差不一定显著降低但训练耗时大幅下降。如果只追求预测精度把高频分量作为一个整体回归到原始序列的一个残差项上比分别预测更省力且精度更稳。5.3 过拟合的优化目标WOA把验证集“背”下来了现象WOA找到的参数在验证集上MSE极低换成测试集后误差反而明显上升甚至比默认参数还差。原因WOA的适应度函数直接使用验证集MSE优化过程等价于在验证集上做隐式的“梯度下降”迭代次数多了参数迟早会过拟合到验证集的噪声模式上。解决把验证集改成K折交叉验证中的单折且每几轮WOA迭代就换一次验证折或者在适应度函数里加入训练集与验证集MSE之差作为惩罚项如前面的0.3系数。另外WOA迭代次数不宜过多2030轮足够找到合理区域继续迭代收益极低并开始曲线恶化。用早停机制判断WOA何时收敛而不是让它跑满预设迭代上限。5.4 LST-M训练不稳定不同次运行结果浮动大现象同样的参数和数据集两次运行的预测结果相差5%甚至更多。原因LSTM的权重初始化是随机的且训练过程中的batch shuffle顺序也带随机性。WOA每次评价适应度时如果模型训练不固定随机种子同一组超参数可能因运气好坏被高评或低评优化方向被噪声主导。解决在模型构建和训练前固定随机种子。TensorFlow需要在代码开头设置tf.random.set_seed(42)、np.random.seed(42)和random.seed(42)三处。即便如此GPU上的某些算子仍然有非确定性行为稳妥做法是对同一组参数重复训练3次取平均适应度虽然耗时变为3倍但WOA搜索方向会稳定很多。5.5 分解边界效应预测段末尾的“甩尾”现象现象每个分量单独预测表现正常叠加后的最终预测在序列末尾出现明显偏离像曲线“甩了一下尾巴”。原因CEEMDAN分解在序列两端存在包络拟合误差IMF在边界处不稳定而预测的目标点恰好位于序列末端受边界效应影响最大。比较典型的场景是用过去200个点预测未来10个点最后几个点的分解结果已经和真正成分有偏差。解决预测前在序列末尾补一段镜像延拓或线性预测延拓的数据等分解完成后再裁掉延拓部分只保留目标区间的结果。这个技巧被很多信号处理工程师使用但在时间序列预测的论文里很少被明说。实践下来对低频分量效果明显遇到趋势突变时作用有限——如果在延拓段预测的方向本身就是错的那补出来的IMF同样不可信。6. 验证模型效果从误差分解到消融对比模型跑通之后最容易被忽视的是验证环节。只用MSE、MAE来判断够不够不够。这类分解-优化-预测模型有三个层次的问题需要验证。第一层整体预测曲线和真实曲线的拟合程度。除了MSE、MAE还需要看R2和最大绝对误差MaxAE。R2反映的是模型相对“直接用均值预测”的改进程度MaxAE则暴露最差时间点的误差有多大——在设备寿命预测中你更关心的是峰值处的误差而不是平均误差。建议把误差指标做成表格对比测试集上的MSE、MAE、R2、MaxAE四项缺一不可。第二层每个重构分量对最终误差的贡献。把每个分量的预测误差单独打印出来能看到高频分量的误差占比是否过高若高频分量的误差贡献超过40%合理的做法是放弃对该分量的LSTM建模改用更简单的平滑预测或直接置零。这一步往往比继续优化WOA参数更能提升整体精度。第三层消融对比。这是决定“CEEMDAN-WOA-LSTM这套组合值不值得用”的关键实验。至少要做四组对比纯LSTM不做分解、不做优化CEEMDAN-LSTM做分解、固定默认参数WOA-LSTM不分解、用WOA搜参完整版CEEMDAN-WOA-LSTM。四组使用相同的训练测试切分和评估指标。根据我的实操经验在信噪比低的数据上CEEMDAN的加入能显著拉低MSE在信噪比高的平稳序列上分解带来的提升微乎其微有时反而因为分解误差导致预测精度下降。WOA的贡献也类似——它能在默认参数基础上再提升5%10%但对本身已经调参合理的LSTM提升空间有限。说到消融对比还有一个工程上的习惯值得分享每次跑这类组合模型我都会把每次实验的随机种子、参数组合、训练耗时、验证集指标和测试集指标同时记录到一个CSV文件里不放过任何一个实验条件。这个习惯帮我发现过不少问题例如某次分解后测试集指标暴涨回看记录才发现那次训练和测试的切分比例与之前不一致——是代码里一个变量被意外覆盖导致的。没有这些记录这种错误几乎不可能被发现。希望这套文档化的实验管理方式和上文逐层避坑的方法能帮你在复现CEEMDAN-WOA-LSTM时少走几个来回把精力真正花在数据分析和模型改进上。本文还有配套的精品资源点击获取