ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CEEMDAN-ISOS-VMD-GRU-ARIMA时间序列预测实战解析

CEEMDAN-ISOS-VMD-GRU-ARIMA时间序列预测实战解析 简介这是一份面向时间序列预测与毕业设计场景的完整且可直接运行的Python工程基于TensorFlow实现CEEMDAN、VMD、GRU与ARIMA的组合建模覆盖信号分解、特征提取、神经网络预测和误差修正的完整流程适合需要开展组合预测研究或课程设计的学生直接参考。压缩包共3个文件、大小仅52KB包含2个CSV格式的数据文件和1个Python源码可直接作为算法验证与预测实验的输入样例。目前已有387人学习下载是同类组合预测资源中较受关注的方案。源码采用参数化编程几乎一行一注释从环境配置到参数调整都有清晰指引整体结构按数据处理、分解、预测、评估分段组织初学者可逐模块学习也可以快速替换为自己的数据复现实验代码注释详细便于二次开发与改进。1. 为什么 CEEMDAN-ISOS-VMD-GRU-ARIMA 值得复现看到“Python实现CEEMDAN-ISOS-VMD-GRU-ARIMA时间序列预测”这个标题第一反应往往不是兴奋而是被三个信号分解算法和两个预测模型的长串组合吓到。但拆开看它解决的是单模型很难啃下的非平稳时序预测难题实际数据里趋势、周期、噪声和局部突变往往挤在一起单独用ARIMA会被非线性成分带偏单独用GRU又容易在长记忆上失效。做法是把原始信号先做CEEMDAN分解再用ISOS优化过的VMD做二次分离最后让GRU处理高频成分、ARIMA处理低频趋势再把预测结果叠加回最终序列。这套流水线在负荷预测、流量预测、水位预测里被反复证明有效适合已经会用Python跑sklearn和TensorFlow、但想突破时序精度瓶颈的工程师。本文不贴完整项目源码而是把这条管道的实现思路、关键代码和坑讲清楚。2. 把 CEEMDAN、ISOS、VMD、GRU、ARIMA 的流水线拆开2.1 CEEMDAN为什么不用 EMD 或 EEMD以EMD为代表的经验模态分解把原始序列按局部极值包络逐层拆成多个固有模态函数。它不预设基函数看起来适合非平稳数据但最大问题是模态混叠一个IMF里经常同时出现多个频带物理意义不干净。EEMD通过在每次分解前加入高斯白噪声让混叠在不同试验中被平均掉代价是重构结果里残留噪声。CEEMDAN的核心改进是在每一层分解时都加入自适应白噪声并且分解完成后再去除噪声分量最终得到的IMF既能有效分离频带又能几乎无损重构原序列。在Python里PyEMD库提供了现成的CEEMDAN实现。基本调用如下from PyEMD import CEEMDAN import numpy as np t np.linspace(0, 1, 1000) signal np.sin(2 * np.pi * 5 * t) 0.5 * np.sin(2 * np.pi * 30 * t) 0.2 * np.random.randn(1000) ceemdan CEEMDAN(trials50, noise_width0.05) imfs ceemdan(signal) print(imfs.shape) # 输出形状为 (分量数量, 样本数)trials表示添加白噪声后平均的次数noise_width是噪声幅值占信号标准差的比例。imfs里面最后一行是残差不是真正的IMF。判断分解是否合格比较简单的方法是检查重构误差reconstructed imfs.sum(axis0) rmse np.sqrt(np.mean((signal - reconstructed) ** 2))rmse越小说明分解的可靠性越高一般量级应低于信号标准差的百分之一。如果这个误差很大先检查trials是否太小再看noise_width是否过大。工程上我一般会把trials设为100起步因为单次CEEMDAN运行时间并不长多加几次平均能明显稳定IMF形状。2.2 VMD 与 ISOS 之间的关系VMD是另一种完全不同的分解思路。它通过构造变分问题把输入信号拆成指定个数的带限模态每个模态的中心频率和带宽在迭代中被自动分离。VMD的优点是对模态混叠的抑制能力强缺点是需要预设两个关键参数模态个数K和惩罚因子alpha。K设少了混合频带散不开K设多了会出现虚假模态。alpha控制模态的带宽约束过大会让模态过度光滑过小又会产生噪声泄露。工程上很难靠一次两次试验确定这两个参数于是就有了ISOS这类群体智能优化算法的用武之地。ISOS是在SOS基础上改进得到的SOS通常指共生生物搜索模拟生物之间互利共生、共栖和寄生三种关系。ISOS在不同论文里更新公式差别很大但共同点是种群个体携带一组待优化参数通过适应度函数评估好坏不断迭代逼近最优解。这里需要明确一个观点ISOS并不是负责预测的模型而是VMD的“参数搜索引擎”。它搜索的空间是[K, alpha]适应度函数则是某个衡量VMD分解质量的指标。许多人在复现这类标题时把重点放在GRU和ARIMA上却忽视了ISOS的适应度函数设计导致最终效果还不如手动选参数。2.3 为什么高频给 GRU、低频给 ARIMAGRU是LSTM的简化版本只有更新门和重置门参数更少、训练更快在处理高频、非线性、短记忆成分时表现比较稳定。ARIMA则是典型的线性统计模型对趋势项和平稳成分拟合效果很好且预测结果有可解释性。一个时间序列经过CEEMDAN和VMD分解后高频模态往往是非平稳的局部波动适合GRU去捕捉模式低频模态和残差则呈现缓慢变化用ARIMA拟合回归部分更合理。如果把这个分工调反局面会变得很难看。ARIMA处理高频成分时滞后阶数难以确定预测误差会被迅速放大GRU处理低频长期趋势时又容易因为样本量不足而发生过拟合。所以在这类混合模型的复现中先观察各IMF的频谱和方差贡献再决定哪些送给GRU、哪些送给ARIMA是很重要的一步。组件处理对象主要参数常用Python库CEEMDAN原始非平稳序列trials, noise_widthPyEMDISOSVMD参数寻优种群数、迭代次数、K范围、alpha范围自写VMD混频或宽频带IMFK, alpha, tau, initvmdpyGRU高频非线性成分hidden_size, dropout, time_stepTensorFlow/KerasARIMA低频趋势和残差p, d, qstatsmodels2.4 一个容易忽略的分量选择策略不是所有CEEMDAN分解出来的IMF都值得送到ISOS-VMD里进行二次分解。有些IMF本身频带已经很窄再跑VMD只会浪费时间并可能引入虚假模态。常见做法是依次计算每个IMF和原始信号的Pearson相关系数并观察IMF频谱的峰值个数。相关系数低的分量通常与主体信号关系不大可以直接送到残差组相关系数高且频谱主峰分散的分量才进入ISOS-VMD流程。我一般使用这样一个判断逻辑from scipy import signal as sp_signal def needs_second_decompose(imf, original, corr_threshold0.1): corr np.abs(np.corrcoef(imf, original)[0, 1]) if corr corr_threshold: return False freqs, powers sp_signal.welch(imf) peaks np.sum(powers np.median(powers)) return peaks 1corr_threshold控制进入VMD的IMF数量。如果设置过高许多有效信息被忽略设置过低又会增加VMD的无效计算。peaks 1说明频谱不是单峰存在混频风险。这个选择策略写起来很简单但它决定了整个管道的计算开销和最终精度值得在复现时单独写一页测试。3. 把 ISOS-VMD 跑通得到可预测的子序列3.1 vmdpy 的调用与参数VMD在Python里的常用实现是vmdpy库核心接口是一个VMD()函数。它输入原始信号和一组参数返回分解后的模态数组、频域表示和中心频率。调用代码非常精简from vmdpy import VMD def vmd_decompose(signal, K, alpha): tau 0 DC 0 init 1 tol 1e-7 u, u_hat, omega VMD(signal, alpha, tau, K, DC, init, tol) return u, omegaK是模态个数alpha是惩罚因子tau是噪声容忍度DC表示是否需要提取直流分量init控制中心频率初始化方式tol是迭代停止阈值。u的形状是(K, 样本数)也就是K个模态omega则是每个模态的中心频率轨迹。复现时先固定其余参数只让ISOS去接力搜索K和alpha这样搜索空间只有二维优化器不容易发散。3.2 ISOS 适应度函数怎么写要让ISOS驱动VMD必须定义适应度函数。一个经典方案是计算每个VMD模态的包络熵包络熵越小说明模态的稀疏性和正则性越好分解效果越接近理想。包络可以通过希尔伯特变换求瞬时幅值得到from scipy.signal import hilbert def envelope_entropy(modal): analytic hilbert(modal) amplitude np.abs(analytic) p amplitude / (amplitude.sum() 1e-12) p p[p 0] return -np.sum(p * np.log(p)) def fitness_individual(individual, original_signal): K int(round(individual[0])) alpha individual[1] u, _ vmd_decompose(original_signal, K, alpha) entropy_list [envelope_entropy(u[i]) for i in range(K)] corr_list [abs(np.corrcoef(u[i], original_signal)[0, 1]) for i in range(K)] return np.mean(entropy_list) - 0.01 * np.mean(corr_list)这里减去平均相关系数是为了避免优化器把“熵最小”理解为把所有能量拆到极窄频段、与原始信号失去关联。系数0.01是一个经验值当你发现优化结果出现大量无效模态时适当调大这个系数。注意alpha在搜索空间中一般不做对数缩放但在ISOS初始化时可以设置取值范围为[200, 5000]避免数量级差距让优化器走很多冤枉路。3.3 ISOS 优化器最小运行模板接下来用一个可以实际运行的最小SOS框架来模拟ISOS。ISOS与标准SOS的核心差别通常在第三阶段的寄生更新方式我会在代码注释里标出哪里是替换点class ISOSOptimizer: def __init__(self, original_signal, bounds, pop_size10, max_iter20): self.signal original_signal self.bounds bounds # [(K_min, K_max), (alpha_min, alpha_max)] self.pop_size pop_size self.max_iter max_iter self.pop np.array([ [ np.random.randint(bounds[0][0], bounds[0][1] 1), np.random.uniform(bounds[1][0], bounds[1][1]), ] for _ in range(pop_size) ], dtypefloat) def run(self): for _ in range(self.max_iter): fits np.array([ fitness_individual(ind, self.signal) for ind in self.pop ]) best_index np.argmin(fits) # 阶段一互利共生 for i in range(self.pop_size): j np.random.choice( [x for x in range(self.pop_size) if x ! i] ) mutual_vector (self.pop[i] self.pop[j]) / 2 r1, r2 np.random.rand(), np.random.rand() self.pop[i] r1 * (mutual_vector - r2 * mutual_vector) self.pop[j] r2 * (mutual_vector - r1 * mutual_vector) # 阶段二共栖 for i in range(self.pop_size): j np.random.choice( [x for x in range(self.pop_size) if x ! i] ) r3 np.random.rand() self.pop[i] r3 * (self.pop[best_index] - self.pop[j]) # 阶段三寄生ISOS 改进点通常在这里 parasite_host np.random.randint(0, self.pop_size) parasite_vector self.pop[parasite_host].copy() parasite_vector np.random.uniform(-0.1, 0.1, 2) if fitness_individual(parasite_vector, self.signal) fits[parasite_host]: self.pop[parasite_host] parasite_vector # 边界修正 self.pop[:, 0] np.clip(self.pop[:, 0], self.bounds[0][0], self.bounds[0][1]) self.pop[:, 1] np.clip(self.pop[:, 1], self.bounds[1][0], self.bounds[1][1]) best_index np.argmin([ fitness_individual(ind, self.signal) for ind in self.pop ]) K_best int(round(self.pop[best_index][0])) alpha_best self.pop[best_index][1] return K_best, alpha_best这份代码把生物启发算法简化成三段交互第一阶段的r1 * mutual_vector是让个体向种群公共信息靠近第二阶段是用最优个体引导其他个体移动第三阶段是随机扰动。寄生虫阶段每次只改一个宿主所以运行速度较快。使用时要根据你的alpha取值范围调整幅度如果alpha最大值是5000那么±0.1的扰动幅度会显得过小可以改成±200。3.4 VMD 结果和 ISOS 结果怎么验证ISOS每轮都会调用VMDVMD本身是一次迭代求解所以整体计算量并不小。为了验证优化结果可以拿ISOS给出的K和alpha再做一次VMD然后检查两点第一模态重构误差是否足够小第二omega的最后一步是否收敛到稳定的中心频率。如果相邻两个模态的中心频率相差过近说明K偏大下次搜索时应该把K的上界调低。另一个可视化技巧是画出模态的频谱。把每个VMD模态做FFT后叠加在同一张图里如果相邻模态频谱有显著重叠说明该组参数并不理想。这时候优先调大alpha而不是调小K因为带宽约束变严后重叠压力会降低。4. 用 GRU 和 ARIMA 完成预测与重组4.1 数据准备滑窗与归一化得到子序列后先决定哪些子序列进入GRU哪些进入ARIMA。高频成分通常需要做滑动窗口处理用过去一定步长预测下一点。这一步要注意归一化方式只能在训练集上做归一化然后把参数应用在测试集上否则会把未来的统计量泄漏进模型。from sklearn.preprocessing import MinMaxScaler def make_window_data(series, time_step12): X, y [], [] for i in range(len(series) - time_step): X.append(series[i:i time_step]) y.append(series[i time_step]) return np.array(X), np.array(y) scaler MinMaxScaler() train_scaled scaler.fit_transform(train.reshape(-1, 1)).flatten() test_scaled scaler.transform(test.reshape(-1, 1)).flatten() X_train, y_train make_window_data(train_scaled, time_step12) X_test, y_test make_window_data(test_scaled, time_step12)time_step是滑窗长度它决定了GRU能看到多久的历史。选太小会丢失模式选太大会引入噪声。从我的复现经验看12到24是比较稳妥的初始区间。4.2 Keras 中的 GRU 模型结构GRU模型的结构不宜过于复杂。高频成分往往只是原序列的一部分样本量有限网络太深反而容易过拟合。两层GRU加一个输出层是多数项目的起步模板from tensorflow.keras.models import Sequential from tensorflow.keras.layers import GRU, Dropout, Dense model Sequential([ GRU(32, return_sequencesTrue, input_shape(12, 1)), Dropout(0.2), GRU(16), Dropout(0.2), Dense(1) ]) model.compile(optimizeradam, lossmse) history model.fit( X_train.reshape(-1, 12, 1), y_train, epochs30, batch_size16, validation_split0.1, verbose0 )input_shape(12, 1)里的12对应time_step1是特征数。第一层GRU设置return_sequencesTrue是为了把完整的隐藏状态序列传给第二层。Dropout放在两层GRU之间用于降低小样本过拟合。epochs和batch_size需要按序列长度调整序列短就减小batch_size并放大epochs。4.3 ARIMA 拟合低频与残差部分低频成分和总残差送到ARIMA前先做ADF平稳性检验。如果不平稳做一阶差分后再拟合。这里容易犯的一个错误是差分后忘记反差分导致最后预测曲线整体偏移。from statsmodels.tsa.stattools import adfuller from statsmodels.tsa.arima.model import ARIMA def fit_arima(series, d_max2): d 0 diff_series series.copy() while adfuller(diff_series)[1] 0.05 and d d_max: diff_series np.diff(diff_series) d 1 model ARIMA(diff_series, order(2, d, 2)).fit() return model, dadfuller返回元组第二个元素是p值。order(2, d, 2)并不是固定最优实际项目中可以用pmdarima的auto_arima自动搜索。注意ARIMA的预测结果通常会返回置信区间这里只取预测均值即可。4.4 分量预测怎么叠加回最终结果GRU和ARIMA各自完成后所有分量需要相加得到最终的预测值。直接相加是最常见的操作但前提是所有分量的长度已经对齐。如果在构造滑窗时截掉了前12个点那么ARIMA的输出也要保持相同的时间偏移。gru_pred model.predict(X_test.reshape(-1, 12, 1)).flatten() arima_pred arima_model.forecast(len(y_test)) # 反归一化 gru_pred_original scaler.inverse_transform(gru_pred.reshape(-1, 1)).flatten() arima_pred_original scaler.inverse_transform(arima_pred.reshape(-1, 1)).flatten() final_pred gru_pred_original arima_pred_original如果你在训练GRU前分别对每个子序列做了归一化那么反归一化时也要使用对应子序列的scaler。这一点非常容易在代码迭代中搞混建议把每个分量的scaler存成字典方便后续回溯。若求稳可以先用训练集的误差计算各分量的组合权重再把这个权重应用到测试集。5. CEEMDAN-ISOS-VMD 的参数边界与验证方法5.1 三组关键参数的经验值CEEMDAN部分需要关心的参数有两个trials和noise_width。推荐trials100noise_width0.05起步。如果你的数据信噪比很低把noise_width提高到0.2并适当增加trials到200才能稳定分解结果。ISOS优化VMD时的K建议在3到10之间搜索alpha在200到5000之间。GRU的time_step可以从12开始如果预测目标是对未来24个点做预测那么time_step至少要与预测步长同量级。5.2 验证方法的两个关键点第一不要只用一步预测评估。实际预测往往会做滚动多步预测也就是把前一步的预测值重新作为输入喂回模型。误差会随步数累积这一步才是模型真实能力的写照。测试时至少用50个连续测试点做递归多步预测计算累积RMSE。第二分解算法的边界效应很容易被忽略。CEEMDAN和VMD在序列首尾都存在端点效应预测时如果直接使用最后一段序列的分解结果端点误差会直接引入预测。建议在训练集和测试集之间保留一段缓冲数据让分解算法在边界处有更多有效样本。5.3 复现时最容易被忽视的三件事第一ARIMA在预测低频成分前必须检查低频成分是否平稳。如果不平稳先差分预测后反差分。第二GRU随机性较大需要在代码顶层设置随机种子否则精度对比没有意义。第三不要对整个序列一次性做归一化而是先用训练集fit再transform测试集减少未来信息泄漏。import random import numpy as np import tensorflow as tf random.seed(42) np.random.seed(42) tf.random.set_seed(42)5.4 怎么判断整套方案是否真的有效最简单的做法是做一个消融对比一组只用CEEMDAN加GRU另一组完整跑CEEMDAN-ISOS-VMD-GRU-ARIMA。如果第二组在测试集上的RMSE下降不明显不要立刻怀疑ISOS先检查VMD分解出的模态是否真的比CEEMDAN结果更合理。画出模态频谱确认没有模态混叠后再看预测误差。如果某条IMF被ARIMA拟合后残差仍然很大这条IMF可能不适合进入ARIMA分支需要把它调整到GRU分支或直接做重构权重修正。通过这种逐个分支验证的方式才能让这个长标题名副其实地跑出可复现的效果。本文还有配套的精品资源点击获取
返回列表