news 2026/10/1 14:52:44

CEEMDAN-WOA-LSTM时间序列预测:分解、寻优与建模全流程解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CEEMDAN-WOA-LSTM时间序列预测:分解、寻优与建模全流程解析

简介:这份资源给出基于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, nan=np.nanmedian(series)) # 实例化 CEEMDAN ceemdan = CEEMDAN(trials=100, epsilon=0.005) # 分解,返回 2D 数组,每行一个 IMF imfs = ceemdan(series) # PyEMD 返回的 imfs 不含残差,需要自己算 residue = series - np.sum(imfs, axis=0) # 重构误差检查:正常应接近 0 recon_err = np.max(np.abs(series - (np.sum(imfs, axis=0) + residue))) print("重构误差:", recon_err) # 保存各分量,后面还要用 np.savez("ceemdan_result.npz", imfs=imfs, residue=residue)

参数里最值得调的是trials和epsilon。trials是加噪声的次数,设太小模态不稳定,设太大计算变慢,我一般取 50 到 100;epsilon是噪声幅值相对输入序列标准差的系数,默认是 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, lookback=24, horizon=6): 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 或 60,horizon视业务需要决定。窗口构造有个小细节: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, dim=4, lb=None, ub=None, agents=10, max_iter=15): lb = np.array(lb, dtype=float) ub = np.array(ub, dtype=float) # 随机初始化种群 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(lookback=lookback, hidden=hidden, horizon=horizon) model.compile(loss="mse", optimizer=Adam(learning_rate=lr)) # 训练轮数固定,避免因早停导致超参对比不公平 model.fit(x_train, y_train, batch_size=batch_size, epochs=20, verbose=0, validation_data=(x_val, y_val)) y_pred = model.predict(x_val, verbose=0) return np.sqrt(np.mean((y_val - y_pred) ** 2))

需要留意的点有三个。第一,训练轮数固定且较小,比如 20 到 30 轮,WOA 关注的是超参相对优劣,不需要让每个候选模型训到收敛,否则一轮优化要跑几百次训练,时间上受不了。第二,验证集必须是同一份,并且不能参与训练,否则超参搜到的是“记住验证集”的模型。第三,verbose=0关掉输出,否则屏幕会被刷屏。

三维和四维哪个更推荐?我一般设四维:隐藏单元、学习率、批大小、时间窗。Layer 数和 Dropout 手动固定为常用值,因为维度越多,WOA 收敛越慢且每次评估越贵。想要快速跑通时只搜隐藏单元和学习率两个维度,误差通常只比全搜索差一点,但速度快几倍。

4. 搭建 LSTM 预测模型并训练:Keras 实现与预测结果重构

4.1 每个 IMF 单独建模还是拼接:两种模型结构怎么选

有一种做法是把分解后的 IMF 当作多通道输入拼成一个 LSTM,但我不推荐。LSTM 的多通道输入要求每个时间步上所有通道对齐,而 CEEMDAN 分解出的分量频率差异大,直接拼接会让网络把精力花在学习分量间的关系上,反而丢失各自的时序模式。

更稳的做法是“每个 IMF 训练一个 LSTM,预测值相加”。这样每个模型只学一个相对平稳的分量,LSTM 更容易捕捉到周期性。代价是模型数量多:分解出 K 个 IMF 就有 K+1 个模型(残差也算一个)。

实际操作时我通常做一个小优化:先看各 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, hidden=64, horizon=6, n_features=1, dropout=0.2): model = Sequential() model.add(Input(shape=(lookback, n_features))) model.add(LSTM(units=hidden, return_sequences=True)) model.add(Dropout(dropout)) model.add(LSTM(units=hidden // 2, return_sequences=False)) model.add(Dense(horizon)) return model def train_model(model, x_train, y_train, x_val, y_val, lr=0.001): model.compile(loss="mse", optimizer=Adam(learning_rate=lr)) callbacks = [ EarlyStopping(monitor="val_loss", patience=10, restore_best_weights=True) ] history = model.fit( x_train, y_train, epochs=100, batch_size=32, validation_data=(x_val, y_val), callbacks=callbacks, verbose=1 ) return model, history

LSTM 第一层返回序列,方便第二层继续提取时序特征;第二层不返回序列,直接进入输出层。hidden // 2让第二层容量减半,减少过拟合。Input(shape=(lookback, n_features))的lookback要和造窗口时的数值一致,如果写错模型会直接报 shape 不匹配。

训练时的早停patience=10意味着验证损失连续 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], verbose=0) pred_sum += scalers[i].inverse_transform(pred_part) pred_sum += residue_model.predict(residue_test, verbose=0) 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, verbose=0) 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", index=False)

我现在处理新的时间序列问题,习惯先把 CEEMDAN-WOA-LSTM 作为默认 benchmark 跑一遍,再用更复杂的模型去挑战它。这个组合虽然训练耗时,但胜在稳定和直观,误差高时你知道该看分解还是看超参,不会一头扎进黑匣子里瞎试。希望这一套从分解、优化到预测的完整路径能帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 14:51:55

经济统计学专业想做数据分析:考证与项目准备实用指南

经济统计学专业想做数据分析&#xff0c;优先准备1个面向业务的实战项目&#xff0c;熟练掌握SQL和Excel核心操作&#xff0c;搭配1个低时间成本的能力证明类证书&#xff0c;即可覆盖大部分应届数据分析岗的入门要求&#xff0c;适用条件是面向本科阶段、意向从事互联网金融零…

作者头像 李华
网站建设 2026/10/1 14:51:25

WPF MVVM中ModbusTCP通信类库封装:协议细节、轮询与断线重连实战

接到这个系列的第20篇&#xff0c;前面我们聊了不少WPF和MVVM Toolkit的玩法&#xff0c;但通信这块其实一直被问得最多&#xff1a;到底怎么把ModbusTCP封装成一个像样的类库&#xff0c;而不是在ViewModel里堆一堆Socket裸代码&#xff1f;我见过太多项目&#xff0c;刚开始图…

作者头像 李华
网站建设 2026/10/1 14:51:24

24小时自助健身房系统软件开发实战:架构设计与部署指南

24小时自助健身房系统软件开发实战&#xff1a;架构设计与部署指南 引言&#xff1a;24小时自助健身房系统软件开发的整体思路 在当前体育消费智能化的大背景下&#xff0c;24小时自助健身房系统软件开发已成为传统健身行业转型升级的核心方案。这类系统旨在完全脱离人工值守&a…

作者头像 李华