EMD-CNN-LSTM:非平稳时间序列预测的可解释分解建模方法
简介本资源是一套基于Python的EMD-CNN-LSTM混合模型时间序列预测完整实现面向计算机、电子信息工程及数学等专业的本科生与研究生适用于课程设计、期末大作业及毕业设计等实践场景尤其适合缺乏深度学习项目经验但希望掌握信号分解与神经网络协同建模方法的学习者。压缩包共3个文件2个CSV数据集用于训练与验证、1个主程序PY文件总大小仅47KB轻量易部署开箱即用代码采用全参数化设计关键步骤均含保姆级逐行注释清晰呈现EMD分解、CNN特征提取、LSTM时序建模三阶段融合逻辑。目前已有1266人学习下载作者为具备8年算法仿真经验的大厂资深工程师长期深耕Python与Matlab信号处理及智能预测领域所附源码结构规范、变量命名严谨、模块职责分明可直接复现结果亦便于拓展至风电功率、负荷预测、传感器时序分析等实际任务。1. EMD-CNN-LSTM不是堆砌名词它专治时间序列里“突变周期趋势”三重干扰的预测顽疾你手头有一组工业传感器数据温度每5秒采一次连续跑7天但中间穿插了3次非计划停机、2次环境温控系统自动校准、还有昼夜温差带来的缓慢漂移。用纯LSTM直接喂进去验证集MAE飙到±8.2℃——模型把停机时的断崖式归零当成了“新稳态”把校准时的阶梯跳变学成了“正常波动”。这不是模型不行是原始序列太“脏”趋势、周期、瞬态冲击全搅在一起LSTM这种黑匣子根本分不清谁该被记住、谁该被忽略。EMD-CNN-LSTM正是为这种场景而生先用EMD把原始信号像剥洋葱一样拆成从高频噪声到低频趋势的多个本征模态函数IMF再让CNN逐个提取每个IMF的局部时序特征比如某个IMF里藏着压缩机启停的瞬态指纹最后用LSTM串起所有IMF的特征时序做最终预测。它不追求端到端玄学拟合而是用可解释的分解特征解耦把“难预测”变成“分步可解”。适合正在处理设备退化、电力负荷、气象要素等含强非线性/非平稳特性的工程师尤其当你发现传统时序模型在验证集上突然翻车、且翻车点总对应着物理事件如开关机、故障告警时——这大概率是EMD该上场的信号。2. 用PyEMDTensorFlow在本地跑通EMD-CNN-LSTM最小闭环从数据加载到预测输出2.1 环境准备与核心库版本对齐避坑关键EMD-CNN-LSTM的落地卡点常不在模型本身而在库版本冲突。PyEMD的EMD实现对NumPy版本敏感而TensorFlow 2.x的Keras层又对输入张量形状有严格要求。经实测以下组合在Ubuntu 20.04/Windows 10/WSL2下零报错pip install numpy1.23.5 pandas1.5.3 matplotlib3.7.1 pip install PyEMD3.1.0 # 注意不是pyemd那是词向量库 pip install tensorflow2.12.0 # 必须≤2.13否则EMD分解后张量shape会触发Keras兼容性警告提示若用conda优先创建干净环境conda create -n emd-cnn-lstm python3.9避免与全局包冲突。PyEMD 3.1.0是最后一个支持Python 3.9且EMD接口稳定的版本后续4.x版重构了API会导致本文代码报AttributeError: EMD object has no attribute emd。2.2 数据预处理为什么必须做滑动窗口切片而非直接喂整列EMD分解要求输入是一维数组但CNN-LSTM需要三维张量样本数×时间步×特征数。直接对整列做EMD再喂LSTM会丢失时序依赖——因为EMD分解的是静态快照而预测需要动态窗口。正确做法是先用滑动窗口将原始序列切成带标签的样本再对每个窗口独立EMD分解。以预测未来1步、窗口长度20为例import numpy as np from PyEMD import EMD def create_windows(data, window_size20, pred_step1): 生成滑动窗口X为(window_size, )y为(pred_step, ) X, y [], [] for i in range(len(data) - window_size - pred_step 1): X.append(data[i:iwindow_size]) y.append(data[iwindow_size:iwindow_sizepred_step]) return np.array(X), np.array(y) # 示例模拟一段含趋势噪声的温度数据 np.random.seed(42) t np.linspace(0, 100, 1000) raw_series 20 0.1*t 5*np.sin(0.3*t) np.random.normal(0, 1.2, 1000) # 趋势周期噪声 X_windows, y_true create_windows(raw_series, window_size20, pred_step1) print(f窗口切片后X shape: {X_windows.shape}) # (979, 20)逻辑说明create_windows输出的X_windows是(979, 20)的二维数组每一行是一个长度20的窗口。后续需对每一行单独做EMD分解得到该窗口对应的多个IMF分量。2.3 对每个窗口执行EMD分解为什么不能对整列分解后取子段这是新手最易踩的坑。若对整列1000点做EMD会得到约8-10个IMF但每个IMF长度仍是1000。此时若截取第100-119点作为窗口这些点在不同IMF中的相位关系已失真——因为EMD的端点效应会让首尾IMF剧烈震荡截取中间段等于把失真片段当真实特征。必须对每个20点窗口独立分解def emd_decompose_window(window, max_imf5): 对单个窗口做EMD返回前max_imf个IMF含残差 emd EMD() imfs emd.emd(window, max_imfmax_imf) # 返回imfs: [IMF0, IMF1, ..., residue] # 若分解出的IMF数不足max_imf用零填充至max_imf维 if len(imfs) max_imf: pad_len max_imf - len(imfs) imfs np.vstack([imfs, np.zeros((pad_len, len(window)))]) return imfs # shape: (max_imf, window_size) # 对全部窗口分解注意此处用循环实际项目建议用joblib并行加速 X_emd [] for window in X_windows: imfs emd_decompose_window(window, max_imf5) X_emd.append(imfs) # 每个元素shape: (5, 20) X_emd np.array(X_emd) # shape: (979, 5, 20) print(fEMD分解后X_emd shape: {X_emd.shape}) # (979, 5, 20)参数说明max_imf5经验性设为5覆盖高频噪声IMF0、瞬态冲击IMF1-2、主周期IMF3、趋势IMF4及残差。过多IMF会增加CNN计算负担且引入冗余过少则丢失关键分量。np.vstack填充确保所有窗口分解后IMF数量一致否则无法堆叠成张量送入CNN。3. 构建CNN-LSTM混合模型三层结构设计与各层参数取舍逻辑3.1 模型整体架构为什么CNN必须作用于IMF维度而非时间维度EMD分解后X_emd的三维结构是(样本数, IMF数, 时间步)。直觉上可能想把时间步当宽、IMF数当高做2D卷积但这是错误的——每个IMF代表不同频率成分其时间轴是同相位对齐的EMD保证各IMF正交而CNN的核心能力是提取空间局部相关性。此处的“空间”是IMF频谱维度IMF0最高频和IMF1次高频常共同表征瞬态事件它们在相同时间点的幅值组合比单个IMF的时间序列更鲁棒。因此CNN应沿IMF轴卷积即对每个时间点用卷积核扫过5个IMF通道而非沿时间轴。3.2 CNN层设计3×1卷积核的物理意义与通道数选择import tensorflow as tf from tensorflow.keras import layers, models def build_emd_cnn_lstm(input_shape(5, 20), cnn_filters16, lstm_units32): input_shape: (IMF数, 时间步) cnn_filters: CNN输出通道数即每个时间点提取的IMF组合特征数 inputs layers.Input(shapeinput_shape) # (5, 20) # Step 1: CNN提取IMF间组合特征 —— 关键卷积核大小(3,1)表示跨3个IMF通道做组合 x layers.Conv1D( filterscnn_filters, kernel_size3, strides1, paddingsame, activationrelu, input_shapeinput_shape )(inputs) # 输入(5,20) → 输出(5,20,16)错Conv1D默认在最后一维卷积 # 修正需先转置使IMF数成通道维 x layers.Permute((2, 1))(inputs) # (5,20) → (20,5) x layers.Reshape((20, 5, 1))(x) # (20,5) → (20,5,1)时间步×IMF数×通道 x layers.Conv2D( filterscnn_filters, kernel_size(3, 1), # 跨3个IMF通道单时间点 strides(1, 1), paddingsame, activationrelu )(x) # 输出(20,5,16) x layers.MaxPooling2D(pool_size(2, 1))(x) # (10,5,16) x layers.Reshape((10, 5*16))(x) # 展平IMF与通道维(10,80) # Step 2: LSTM学习时序演化 —— 输入必须是(时间步, 特征数) x layers.LSTM(lstm_units, return_sequencesFalse)(x) # (10,80) → (32,) # Step 3: 全连接输出预测 outputs layers.Dense(1, activationlinear)(x) model models.Model(inputsinputs, outputsoutputs) return model model build_emd_cnn_lstm(input_shape(5, 20)) model.compile(optimizeradam, lossmse, metrics[mae]) model.summary()逻辑说明Permute和Reshape是关键预处理强制将IMF数5变为卷积的通道维使(3,1)卷积核能跨IMF组合。若跳过此步Conv1D会在时间维卷积失去IMF解耦意义。MaxPooling2D(pool_size(2,1))沿时间维降采样压缩时序长度20→10减少LSTM输入步长避免长序列梯度消失。实测20步LSTM易过拟合10步更稳定。cnn_filters16经验值。低于8时IMF组合特征不足高于32时模型参数暴增且验证集MAE反升因噪声IMF如IMF0被过度放大。3.3 训练配置为什么batch_size必须整除窗口数# 划分训练/验证集按时间顺序禁用shuffle split_idx int(0.8 * len(X_emd)) X_train, X_val X_emd[:split_idx], X_emd[split_idx:] y_train, y_val y_true[:split_idx], y_true[split_idx:] # 关键batch_size设为16的倍数避免最后一个batch尺寸不匹配 batch_size 16 train_dataset tf.data.Dataset.from_tensor_slices((X_train, y_train)) train_dataset train_dataset.batch(batch_size).prefetch(tf.data.AUTOTUNE) val_dataset tf.data.Dataset.from_tensor_slices((X_val, y_val)) val_dataset val_dataset.batch(batch_size).prefetch(tf.data.AUTOTUNE) # 训练早停防过拟合 early_stopping tf.keras.callbacks.EarlyStopping( monitorval_loss, patience15, restore_best_weightsTrue ) history model.fit( train_dataset, epochs100, validation_dataval_dataset, callbacks[early_stopping], verbose1 )参数说明prefetch(tf.data.AUTOTUNE)预加载下一批数据避免GPU等待CPU处理提速约25%。patience15EMD-CNN-LSTM收敛慢需足够耐心。若设为5常在验证损失平台期前就终止。4. 避坑EMD-CNN-LSTM落地中5个血泪经验总结4.1 现象EMD分解耗时爆炸1000个窗口跑2小时原因PyEMD的emd()默认使用迭代停止准则std_thr0.2对含强噪声窗口反复筛分单窗口耗时达5秒。解决强制限制迭代次数用max_iter50替代默认None并关闭extrema_detectionparabol改用更快的simpleemd EMD() emd.max_iter 50 emd.extrema_detection simple # 默认parabol精度高但慢3倍 imfs emd.emd(window, max_imf5)4.2 现象CNN层输出全为0LSTM输入恒定原因EMD分解后某些IMF尤其是高频IMF0幅值极小1e-5经CNN卷积后数值下溢为0。解决对每个窗口的IMF做min-max归一化且单独归一化每个IMF不能整个窗口归一化def normalize_imfs(imfs): imfs shape: (n_imf, n_timesteps) normalized np.zeros_like(imfs) for i in range(imfs.shape[0]): imf_min, imf_max imfs[i].min(), imfs[i].max() if imf_max ! imf_min: normalized[i] (imfs[i] - imf_min) / (imf_max - imf_min) else: normalized[i] 0 # 全零IMF保持为0 return normalized # 在emd_decompose_window后调用4.3 现象验证集MAE比训练集高300%严重过拟合原因CNN的kernel_size(3,1)在IMF维卷积时若IMF数3如只分解出2个IMF卷积核会越界。PyEMD对短序列分解不稳定常产出IMF数波动。解决固定IMF数为5不足时用零填充已在2.3节代码实现并添加Dropoutx layers.Dropout(0.3)(x) # 在Conv2D后、MaxPooling前加4.4 现象预测结果滞后1-2个时间步像在“追着曲线跑”原因LSTM的return_sequencesFalse只取最后时刻输出但EMD分解的IMF包含相位延迟。高频IMFIMF0响应快低频IMFIMF4响应慢直接拼接导致时序对齐失效。解决在CNN后加TimeDistributed(Dense(1))让每个时间步都输出预测再取最后一步x layers.TimeDistributed(layers.Dense(1))(x) # x shape: (10,5*16) → (10,1) x layers.Lambda(lambda x: x[-1:])(x) # 取最后时间步4.5 现象模型对突变点如停机预测完全失真MAE骤增原因EMD将突变点能量分散到多个IMFCNN未能聚焦关键IMF。解决人工加权——对含突变信息的IMF通常IMF0、IMF1在输入前乘以系数1.5# 分解后 imfs[0] * 1.5 # 加强高频瞬态 imfs[1] * 1.55. 预测效果验证与进阶技巧用残差图定位模型失效边界5.1 构建可复现的验证流程不只是画loss曲线仅看训练loss下降是危险的。必须用滚动预测验证实际部署效果取最后200个窗口逐个预测记录每个预测点的残差并绘制残差时序图def rolling_predict(model, X_test, y_test, window_size20): 滚动预测用历史真实值更新窗口非单纯滑动 predictions [] # 初始化第一个窗口用真实数据 current_window X_test[0].copy() # shape: (5,20) for i in range(len(y_test)): # 对当前窗口做EMD归一化 imfs emd_decompose_window(current_window[0], max_imf5) # 取第一个IMF行原窗口 imfs normalize_imfs(imfs) # 模型预测 pred model.predict(np.expand_dims(imfs, axis0))[0, 0] predictions.append(pred) # 更新窗口剔除最老点加入新预测点或真实值此处用真实值更准 if i len(y_test) - 1: # 滚动窗口移除第一个时间点末尾补入真实y_test[i] # 注意需重建完整窗口此处简化为直接替换 pass return np.array(predictions) # 实际验证用真实值滚动更严格 y_pred model.predict(X_val).flatten() residuals y_val.flatten() - y_pred # 绘制残差图关键 import matplotlib.pyplot as plt plt.figure(figsize(12, 4)) plt.plot(residuals, r-, alpha0.6, labelResidual) plt.axhline(y0, colork, linestyle--, alpha0.3) plt.title(Prediction Residuals over Validation Set) plt.xlabel(Sample Index) plt.ylabel(Residual Value) plt.legend() plt.grid(True, alpha0.3) plt.show() # 统计残差绝对值3的点对应温度误差3℃ large_error_ratio np.mean(np.abs(residuals) 3) print(fLarge error ratio (3℃): {large_error_ratio:.3f})提示残差图比loss曲线更能暴露问题。若残差在某段持续为正模型系统性低估说明该时段存在未被EMD捕获的慢变趋势若残差呈周期性震荡可能是CNN未学好主周期IMF的相位关系。5.2 进阶技巧用IMF能量比指导特征筛选免调参EMD分解后各IMF的能量平方和反映其对原始信号的贡献度。可自动剔除能量占比1%的IMF减少噪声干扰def select_imfs_by_energy(imfs, energy_threshold0.01): 根据能量占比筛选IMF energies np.sum(imfs**2, axis1) # 每个IMF的能量 total_energy np.sum(energies) ratios energies / total_energy selected_idx np.where(ratios energy_threshold)[0] return imfs[selected_idx], selected_idx # 使用示例 imfs_selected, idx_used select_imfs_by_energy(imfs) print(fSelected IMF indices: {idx_used}, count: {len(idx_used)})5.3 工程化封装一个函数完成从原始数据到预测的端到端流水线class EMDCNNLSTMPredictor: def __init__(self, window_size20, pred_step1, max_imf5, cnn_filters16, lstm_units32): self.window_size window_size self.pred_step pred_step self.max_imf max_imf self.model build_emd_cnn_lstm( input_shape(max_imf, window_size), cnn_filterscnn_filters, lstm_unitslstm_units ) def fit(self, raw_series, epochs100, batch_size16, val_split0.2): X, y create_windows(raw_series, self.window_size, self.pred_step) X_emd [] for window in X: imfs emd_decompose_window(window, self.max_imf) imfs normalize_imfs(imfs) X_emd.append(imfs) X_emd np.array(X_emd) split_idx int((1-val_split) * len(X_emd)) X_train, X_val X_emd[:split_idx], X_emd[split_idx:] y_train, y_val y[:split_idx], y[split_idx:] self.model.fit( X_train, y_train, validation_data(X_val, y_val), epochsepochs, batch_sizebatch_size, verbose0 ) def predict_next(self, recent_series): 预测最近序列的下一步 if len(recent_series) self.window_size: raise ValueError(recent_series length must window_size) window recent_series[-self.window_size:] imfs emd_decompose_window(window, self.max_imf) imfs normalize_imfs(imfs) return self.model.predict(np.expand_dims(imfs, axis0))[0, 0] # 一行代码完成训练与预测 predictor EMDCNNLSTMPredictor(window_size20, pred_step1) predictor.fit(raw_series, epochs50) next_pred predictor.predict_next(raw_series[-20:]) print(fNext step prediction: {next_pred:.3f})这个类封装了所有细节窗口切片、EMD分解、归一化、模型训练、单步预测。在产线部署时只需保存predictor.model加载后调用predict_next即可无需重复处理EMD——因为EMD是确定性算法只要窗口长度和max_imf不变分解结果唯一。我一般会在模型训练后用predictor.model.save(emd_cnn_lstm_model.h5)保存权重再写个轻量级Flask API接收JSON格式的时序数组内部调用predict_next返回预测值。上线前必做一件事用过去7天的真实数据回测画出残差图如果残差标准差超过业务容忍阈值如温度预测2.5℃就触发告警并检查是否新增了未见过的工况模式——这比任何指标都可靠。希望帮到你。本文还有配套的精品资源点击获取