
简介本资源是一套面向本科生课程设计、毕业设计及科研入门者的Python时间序列预测完整实现方案聚焦CEEMDAN信号分解与CNN-LSTM混合建模技术解决非平稳时序数据的高精度建模难题。压缩包共3个文件2个CSV实测数据集1个主程序PY文件总大小仅47KB轻量易部署适配AnacondaPyCharmTensorFlow环境。已有497人学习下载代码采用全参数化设计关键步骤逐行注释涵盖CEEMDAN自适应分解、CNN局部特征提取、LSTM时序建模及结果可视化全流程逻辑清晰、变量命名规范特别适合电子信息、数学与计算机专业学生快速理解算法耦合机制并复现结果。1. 为什么非要折腾三层结构CEEMDAN-CNN-LSTM的拆解逻辑1.1 原始时间序列为什么难预测先聊一个很多人对时间序列预测的误解以为拿到一份历史数据丢给LSTM训练一个模型就能把未来的曲线预测得明明白白。我早期也是这样干的结果验证集上的预测曲线永远比真实值慢半拍拐点抓不住峰值全被平均掉。后来我才意识到问题的根源不是模型不够强而是输入信号本身就太“脏”。现实中的时间序列比如电力负荷、交通流量、PM2.5浓度几乎都是高度非平稳的。它们内部往往同时包含多种不同频率、不同变化规律的成分长期趋势比如逐年递增的经济增长带来的用电量上升周期成分比如每天的“峰谷”规律、每周的工作日/休息日差异局部波动比如瞬时的人流冲击、天气突变带来的毛刺。这些成分叠加在一起对LSTM来说就是一场混乱的“鸡尾酒会”。LSTM的隐藏状态需要同时记忆长期趋势、捕捉短周期模式、还要响应局部突变一个模型想同时干好这三件事难度极大。LSTM在梯度回传的时候长周期依赖和短周期特征互相干扰最终学到的往往是“各种成分折中”的结果——反应到预测上就是曲线被削峰填谷滞后明显。所以单纯堆模型参数量并不是最优解。更好的思路是先对原始序列做降维拆解把混合信号拆成几个相对“纯净”的成分再让深度学习模型分而治之。这就是CEEMDAN在这个项目里的价值所在。1.2 EMD家族演进为什么要用CEEMDAN而不是EMD或EEMD很多做时序的朋友对EMD经验模态分解并不陌生。它不需要预设基函数而是根据信号自身的局部特征把时序数据自适应地分解成若干个本征模态函数IMF和一个残差项。每个IMF都是单分量的窄带信号比原始信号更容易建模。但传统EMD有个非常容易翻车的毛病——模态混叠。一个IMF里混进了其他频率成分导致分解结果不稳定同一段信号在不同局部的分解结果差异很大。EEMD集合经验模态分解的做法是往原始信号里反复添加白噪声通过“噪声辅助多次平均”来改善模态混叠。这个方法有效但代价是计算量大且噪声可能残留到分解结果中导致重构误差。CEEMDAN完全自适应噪声集合经验模态分解在EEMD的基础上改了思路它不是在每一轮往原始信号里随机加噪声而是在每一层分解时加入自适应的白噪声并且通过计算残差来提取IMF。这样做的好处是分解过程更加稳定、完备性更好重构误差几乎可以忽略而且最终分解出的IMF数量和噪声幅值也更容易控制。在实践里CEEMDAN分解之后的信号重构误差通常在1e-10这个量级这比EEMD靠谱得多。顺便一提有人问VMD变分模态分解是不是更好。VMD确实在数学框架上更优雅分解频带清晰但它需要预设模态数量K和惩罚因子alpha这些参数对结果影响很大且没有统一的选取准则。相比之下CEEMDAN是自适应决定模态数的在大多数非平稳时序预测场景里使用成本更低泛化性更好。所以这个项目我最终选了CEEMDAN做前置分解。1.3 CNN与LSTM在流水线里各自扮演什么角色把信号拆解成多个IMF之后我们其实拿到了一组二维数据每个时间点对应多个IMF值。这就把一维时序问题天然变成了一个多通道的序列特征学习问题。此时CNN和LSTM的分工就非常清晰了CNN一维卷积负责局部特征提取每个IMF分量虽然比原始信号“干净”但相邻时刻之间仍然存在短期的局部模式比如小幅震荡的形态、局部尖峰的形状。一维卷积通过滑动窗口扫描输入序列可以自动提取这些局部模式。多个卷积核相当于多个“特征检测器”一边扫描IMF之间的通道关联一边提取不同尺度的局部特征。LSTM负责时序依赖建模经过卷积层压缩和特征重组合之后序列变成了更高级的特征表达。LSTM在这一层进一步捕捉时间上的依赖关系尤其是IMF分解后剩下的中低频成分它们带有明显的趋势性和周期性正是LSTM擅长记忆的部分。一句话概括整个流水线CEEMDAN把“分不开”的信号拆开CNN把每个分量的局部特征抽出来LSTM再基于这些特征学习时间上的依赖。2. 数据准备与滑动窗口设计别在这步偷懒2.1 数据格式与工程目录先交代一下我用到的数据示例。这里以一份逐小时采样的城市用电负荷数据为例共2016个时间点约12周。这类数据在UCI的电力负荷数据集和很多开源仓库里都能找到格式一般是两列timestamp和value。如果你手里有自己的业务数据只要保证是单变量等间隔时间序列即可。多变量时序也可以做但处理逻辑会更复杂本文先以单变量为例。完整的工程目录建议这样组织ceemdan_cnn_lstm/ ├── data/ │ └── load.csv ├── utils/ │ ├── preprocessing.py │ ├── ceemdan_decompose.py │ ├── build_model.py │ └── evaluate.py ├── train.py ├── predict.py └── requirements.txt我习惯把数据预处理、分解、建模、评估各拆成一个模块这样无论后面替换数据、调参还是单独验证某个模块都不用把整套脚本翻一遍。2.2 滑动窗口预测未来N步的关键设计时间序列预测里最核心的数据结构设计就是滑动窗口。这里涉及三个参数window_size用过去多少个时间点去预测未来horizon一次预测未来多少步sliding_step窗口滑动的步长通常为1即每个时刻都生成一个样本。举个例子如果window_size48过去48小时、horizon12预测未来12小时那么对于长度为2016的序列能生成大约2016 - 48 - 12 1 1957个样本。每个样本的形状是(48, n_features)标签形状是(12,)。窗口大小选择这件事很多人会拍脑袋定一个值但这其实很依赖业务场景。预测电力负荷这类周期明显的数据window_size至少要覆盖一个完整的周期。比如数据有日周期24小时那至少窗口要等于48或72让模型能“看到”过去两三天才能更好地推断今天这个时刻的走势。在金融高频数据里窗口可以稍微短一点但也要保证能覆盖到你关心的最小周期。生成滑动窗口样本的代码逻辑很简单import numpy as np import pandas as pd def create_sequences(data, window_size, horizon, step1): X, y [], [] for i in range(0, len(data) - window_size - horizon 1, step): X.append(data[i:i window_size]) y.append(data[i window_size:i window_size horizon]) return np.array(X), np.array(y) # 示例data是numpy一维数组 # X的shape: (样本数, window_size, 1) # y的shape: (样本数, horizon)这里有一个容易踩的坑如果你的数据量很大几百万条用Python写for循环生成窗口样本会非常慢。建议后用Vectorized拼接或者直接使用keras.utils.timeseries_dataset_from_array这类内置工具。2.3 归一化这是最容易把“验证集”变“作弊集”的地方归一化这一节我必须单独拎出来讲因为我在这个项目里犯过很典型的错误直接用全量数据的min和max去做MinMaxScaler然后再划分训练集和测试集。这会带来一个非常隐蔽的问题——信息泄露。举个例子测试集里某一天出现了一个历史极值如果你用全量数据计算min和max这个极值会影响归一化参数进而影响训练集里所有样本的缩放比例。模型在训练时“偷看”到了测试集的统计信息测试指标自然好看但你部署到真实场景后新来的数据根本不会给你“预知极值”的机会效果会大打折扣。正确的做法是只用训练集拟合scaler然后用这个scaler去transform测试集。逻辑上跟机器学习的一般原则完全一致但时序场景里很多人就是会顺手写成“先全量归一化再切分”。from sklearn.preprocessing import MinMaxScaler # 假设series是完整时间序列 train_len int(len(series) * 0.8) train_data, test_data series[:train_len], series[train_len:] scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train_data.reshape(-1, 1)).flatten() # 测试集用同一个scaler test_scaled scaler.transform(test_data.reshape(-1, 1)).flatten()另外如果用了CEEMDAN分解建议先整体归一化再分解或者对每个IMF分别归一化。这两种方案各有优劣整体归一化之后分解重构时不会出问题对每个IMF分别归一化虽然每个通道的量纲一致了方便神经网络训练但反归一化时要额外保存每一路scaler的参数工程上麻烦一些。我最终选择的是“先整体归一化后分解训练时不对IMF单独归一化”原因很简单CEEMDAN分解出来的各IMF本身量级差异不大整体归一化已经能满足网络训练需求还能省去一堆反归一化的bug。3. CEEMDAN分解模块把原始信号拆成一支“兄弟队伍”3.1 安装与基础调用CEEMDAN在Python里的常用实现是PyEMD库直接pip安装即可pip install EMD-signal注意这个库的导入名是PyEMD安装名是EMD-signal我第一次装的时候也愣了一下。如果你要用GPU加速或更多变体可以看PyEMD官方文档项目本身维护得还不错。基础调用方法如下import numpy as np from PyEMD import CEEMDAN, EEMD, Visualisation # CEEMDAN内部默认使用EEMD作为辅助分解器 ceemdan CEEMDAN(trials50, epsilon0.005, ext_EMDEEMD()) # max_imfs限制最大分解层数这一步很关键 imfs ceemdan(series_scaled, max_imfs10) # 最后一维是残差通常也作为一路信号参与建模 print(f分解得到 {imfs.shape[0]} 个分量每个分量长度 {imfs.shape[1]})这段代码里trials50表示添加白噪声并重复试验的次数epsilon0.005是噪声幅值相对于信号标准差的系数。这两个参数决定了分解的稳定性与噪声残留水平。3.2 关键参数到底怎么调很多教程会直接给你一套默认参数但实际使用中你真的需要理解每个参数的含义否则换了数据就不知道怎么调。trials噪声试验次数可以理解为“做多少次实验取平均”。次数越多分解结果越稳定模态混叠的概率越低但计算耗时线性增长。对于一条2000点左右的数据50次试验大概需要十几秒可以接受如果数据量上万建议降到20-30否则训练前的数据预处理环节会变成瓶颈。epsilon噪声幅值这是一个和信号标准差相关的比例系数。它控制每轮加入白噪声的强度。epsilon太小噪声无法有效“探测”到信号中隐藏的极值区间模态混叠改善有限epsilon太大噪声会污染分解结果导致IMF里残留较多噪声成分。经验上0.01-0.2之间比较常见。我做电力负荷数据时用0.005效果不错但换到更平滑的气象数据时我调高到0.04才拿到理想结果。max_imfs最大IMF数量这个参数和最终模型输入通道数直接相关。CEEMDAN默认会把信号分解到不可再分为止一般层数在log2(N)这个量级对于2000个点大约会得到8-12个IMF再加上一个残差项。如果不设置max_imfs万一分解出十几个分量每路还要单独过CNN-LSTM训练成本会爆炸。我通常设置8到10既能覆盖主要频段又不至于吓跑显存。3.3 分解结果的检查怎么判断拆得好不好分解完之后不要直接闷头建模先画一下各IMF的波形图用肉眼检查是否还有明显的模态混叠。判断指标有两个维度频带分离度各IMF的主频差异是否清晰。如果相邻两个IMF的主频仍然重叠严重说明分解不充分。残差趋势最后的残差项是否平滑如果残差里还带有明显的高频波动说明分解过头或噪声幅值偏大。另外用下面这段代码可以快速验证CEEMDAN重构是否保真reconstructed np.sum(imfs, axis0) print(重构误差, np.max(np.abs(reconstructed - series_scaled)))正常情况下这个误差应该小于1e-8。如果你发现重构误差大很可能是PyEMD版本之间的接口差异或者使用了自定义的ext_EMD导致分解不完全。4. CNN-LSTM网络结构设计模型搭建的完整细节4.1 为什么是CNN在前、LSTM在后网络结构这块我直接按“CNN提特征、LSTM建时序”的顺序搭。这个设计的理由很直白一维卷积天然擅长在短窗口内提取局部模式LSTM天然擅长建模长距离依赖。如果反过来让LSTM先去处理原始IMF序列再让CNN去卷积LSTM的输出LSTM的隐藏状态会被卷积核“切片”打散时间依赖信息反而受损。另一种常见做法是先LSTM编码再CNN解码多用于seq2seq结构在预测任务里也不是不能用但在我对比过几组实验后**CNN-LSTM输入→卷积→池化→LSTM→输出**在效率和效果上对这个项目是最优的。4.2 完整模型结构我选择的网络结构如下from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, MaxPooling1D, LSTM, Dense, Dropout, Input from tensorflow.keras.optimizers import Adam def build_ceemdan_cnn_lstm(window_size48, n_channels6, horizon12): model Sequential([ Input(shape(window_size, n_channels)), # 第一层卷积提取短时局部特征 Conv1D(filters32, kernel_size5, activationrelu, paddingsame), MaxPooling1D(pool_size2), # 第二层卷积特征再抽象 Conv1D(filters32, kernel_size3, activationrelu, paddingsame), MaxPooling1D(pool_size2), # LSTM层建模时序依赖 LSTM(units50, return_sequencesTrue), Dropout(0.2), LSTM(units30, return_sequencesFalse), Dropout(0.2), # 输出层一次预测未来horizon步 Dense(horizon) ]) model.compile(optimizerAdam(learning_rate0.001), lossmse) return model每个参数我解释一下Conv1D(filters32, kernel_size5)32个卷积核每个卷积核在时间维度上滑动5步。之所以起步不用3是因为IMF分量相对平滑5步视野能覆盖到更完整的局部波形。当然如果你通过实验发现3更稳也可以换。MaxPooling1D(pool_size2)每2个时间步取最大值把时间维度减半。这能压缩序列长度减少LSTM的计算负担同时保留最显著的特征。两轮池化后48个时间点变成12个LSTM的工作量大幅降低。LSTM(units50, return_sequencesTrue)第一层LSTM返回完整序列给第二层LSTM继续处理。这里用两层LSTM而不是单层是因为实验显示两层在捕捉多层次时间依赖上更稳。Dropout(0.2)防止LSTM过拟合尤其当你的数据只有一两千条时dropout很关键。Dense(horizon)最后用全连接层输出未来12个时刻的预测值。一个常见疑问输入通道数n_channels IMF数量1含残差为什么要放在最后一个维度在Keras的Conv1D里输入shape是(batch_size, time_steps, features)这里的features就是每个时间点上的IMF多通道值。CNN会在时间维度上滑动同时跨通道加权组合。这样设计的好处是卷积核可以同时看到同一个时间点上所有IMF的值自动学习IMF之间的联合特征而不是分开独立处理——这比我之前试过的“每个IMF单独建模然后求和”要高效得多。4.3 如果换成PyTorch结构需要注意什么如果你不是Keras党想用PyTorch实现结构上大致是等价的但有几点要提醒nn.Conv1d的输入shape是(batch, channels, length)和Keras正好相反。你需要在输入之前把张量转置LSTM层的输入和输出shape也要按PyTorch的(seq_len, batch, input_size)来设计注意MaxPool1d对时间维度进行池化时和Keras的padding行为保持一致避免维度不匹配。PyTorch版的模型核心代码大概是这样的示意import torch.nn as nn class CEEMDAN_CNN_LSTM(nn.Module): def __init__(self, n_channels, n_filters32, kernel_size5, lstm_hidden50, dropout0.2, horizon12): super().__init__() self.conv1 nn.Conv1d(n_channels, n_filters, kernel_size, paddingsame) self.pool1 nn.MaxPool1d(2) self.conv2 nn.Conv1d(n_filters, n_filters, 3, paddingsame) self.pool2 nn.MaxPool1d(2) self.lstm1 nn.LSTM(n_filters, lstm_hidden, batch_firstTrue) self.lstm2 nn.LSTM(lstm_hidden, 30, batch_firstTrue) self.dropout nn.Dropout(dropout) self.fc nn.Linear(30, horizon) def forward(self, x): # x shape: (batch, time, channels) - 转成(batch, channels, time) x x.transpose(1, 2) x torch.relu(self.conv1(x)) x self.pool1(x) x torch.relu(self.conv2(x)) x self.pool2(x) # 转回(batch, time, features)给LSTM x x.transpose(1, 2) x, _ self.lstm1(x) x, _ self.lstm2(x) x self.dropout(x[:, -1, :]) return self.fc(x)注意这里有个细节PyTorch的LSTM如果只取最终时间步的隐藏状态需要x[:, -1, :]而不是把整个序列输给全连接层。5. 从训练到预测的完整闭环六个环节别断掉5.1 训练设置与EarlyStopping模型训练本身并不复杂关键是要用对回调函数。我训练的配置如下from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau from tensorflow.keras.optimizers import Adam # 数据划分 # 注意在时序任务里不能随机打乱必须按时间顺序切分 split_idx int(len(X) * 0.8) X_train, X_val X[:split_idx], X[split_idx:] y_train, y_val y[:split_idx], y[split_idx:] model build_ceemdan_cnn_lstm(window_size48, n_channels6, horizon12) model.compile(optimizerAdam(learning_rate0.001), lossmse) callbacks [ EarlyStopping(monitorval_loss, patience15, restore_best_weightsTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience5, min_lr1e-5) ] history model.fit( X_train, y_train, validation_data(X_val, y_val), batch_size64, epochs100, callbackscallbacks, verbose1 )这里为什么要把X和y再切一次因为前面已经按序列长度切分了80%作为“原始序列层面的”训练集。但在构造滑动窗口样本之后如果一次性把1957个样本都塞给model.fit没有独立的验证集来监控loss过拟合风险很高。所以我在样本层面再切一遍用前80%的滑动窗口样本训练后20%做验证这样回调函数才有参考依据。EarlyStopping的patience15表示验证loss连续15轮不下降就停。ReduceLROnPlateau在loss停滞时把学习率减半给模型一个更精细调整的机会。这些设置不是拍脑袋而是我对训练曲线观察后的经验值。数据量不同时patience可以适当调整数据量大可以加大到20数据量小建议10左右太大会导致浪费时间太小则找不到最优权重。5.2 预测流程每个IMF走一遍模型最后叠加这里要强调一个重要的设计决策我上面搭建的模型输入是多个IMF组成通道输出是直接的预测值。也就是说模型同时看到所有IMF通道一次性输出未来horizon步的预测。这个方案和“每个IMF单独建一个模型再相加”相比最大的好处是模型规模更小、训练时间更短而且CNN的卷积核能跨通道提取IMF之间的关联特征。预测完整流程的伪代码如下# 1. 对测试集原始序列做归一化用训练集的scaler test_scaled scaler.transform(test_raw.reshape(-1, 1)).flatten() # 2. 用训练好的CEEMDAN分解器对测试集做分解 # 注意CEEMDAN不需要重新fit直接用同一个实例 test_imfs ceemdan(test_scaled, max_imfs10) # 3. 构造测试集窗口样本 X_test, y_test create_sequences(test_imfs.T, window_size48, horizon12) # 此时X_test的shape: (样本数, 48, n_imfs) # 4. 预测 pred_scaled model.predict(X_test) # 5. 反归一化注意scaler是对原始值做的归一化所以直接transform还原 pred_raw scaler.inverse_transform(pred_scaled.reshape(-1, 1)).flatten()整个过程里最容易搞错的就是预测结果反归一化这一步。由于我们是对原始序列做MinMaxScaler训练标签本身就是归一化后的值所以模型输出需要调用同一个scaler的inverse_transform来还原成原始量纲。如果你选了“对每个IMF分别归一化”的方案这里的反归一化就要对每个IMF的输出单独操作然后再叠加一旦有一个通道忘记还原预测结果的量纲就全乱了。5.3 评价指标与可视化我用到的核心指标有三个RMSE、MAE、MAPE。from sklearn.metrics import mean_squared_error, mean_absolute_error def evaluate_metrics(y_true, y_pred): rmse np.sqrt(mean_squared_error(y_true, y_pred)) mae mean_absolute_error(y_true, y_pred) mape np.mean(np.abs((y_true - y_pred) / y_true)) * 100 return {RMSE: rmse, MAE: mae, MAPE(%).: mape}注意MAPE计算时如果真实值里有接近0的点MAPE会爆炸。电力负荷夜间低谷可能接近0所以我会先过滤掉真实值太小的点或者干脆用RMSE和MAE作为主要指标。可视化方面推荐把真实值和预测值叠在一张图里并按周或按月切片查看单独看整体曲线很难发现细节问题。6. 我在实际跑这套模型时踩过的坑6.1 模态混叠真的会悄悄掏空预测精度我第一次对一份风电功率数据跑CEEMDAN用的是trials20, epsilon0.2分解完发现IMF1和IMF2的主频几乎重合在同一个频段。我当时的处理方式是懒直接继续拿去训练最后模型的预测曲线总在局部震荡处偏大。后来把epsilon从0.2降到0.05IMF之间的频带立刻分开了预测误差下降了将近8%。所以千万不要以为CEEMDAN是自动挡分解完可以不检查。每次换数据集都必须重新检查IMF的波形和频谱。6.2 归一化时机不对测试集指标虚高这个坑前面提到过我再强调一下操作层面的具体表现。我对比过“全量归一化再切分”和“训练集拟合再transform测试集”两种方案前者的RMSE能比后者低10%-20%看起来很爽但换到真实在线预测时效果直接打回原形。如果你在验证模型时发现效果异常得好别高兴太早先检查一下自己的scaler是不是用全量数据拟合的。6.3 LSTM训练崩溃梯度爆炸的诱因我用的是Adam优化器学习率0.001按理说比较稳但在训练中后期偶尔会出现loss突然变成nan的情况。排查后原因有两个输入数据里有个别极端大的值没有处理好经过MinMaxScaler后本来应该被压到(0,1)区间但如果原始数据里有异常值且超出了训练集的max范围测试集的归一化结果会跑到1以上大数值进入网络后梯度爆炸。网络层数不多但LSTM对输入尺度仍然敏感如果损失不下降可以试着把学习率降到0.0005或者在LSTM层前加一个BatchNormalization。我的实际做法是在归一化之前先对原始序列做一次基于分位数的异常值截断比如把超过99.9分位数的值替换为99.9分位数值这样即使测试集出现历史极值也不至于让归一化后的值过分离谱。6.4 预测值滞后“一个身位”是怎么回事用LSTM做单步或者多步预测很多人都会发现预测曲线和真实曲线之间存在明显的滞后尤其是拐点处。这个现象在依赖MSE损失训练的回归模型里很普遍为了最小化整体误差模型倾向于输出“条件均值”而条件均值在突变点处天然滞后。缓解手段有几个把损失函数换成Huber Loss降低对离群点的惩罚权重模型会更大胆地追拐点在窗口构造时加入时间特征小时、星期几作为额外输入帮助模型识别周期相位接受一定滞后把任务重心放到中长期的趋势判断上而不是追求逐点贴合。6.5 分解边界效应两端预测不准CEEMDAN在信号端点附近的表现通常不稳定因为极值包络在边界处需要外延插值。这会导致分解出来的IMF首尾几十个点发生轻微畸变。训练时影响不大但如果你的测试集是从原始序列末尾切出来的那预测起点附近的误差会被放大。我的实践是在测试集末尾多预留一些“缓冲数据”。比如测试集从第1800个点开始但做预测的时候实际用于构造窗口的数据从第1700个点开始让CEEMDAN的边界畸变区落在窗口早期不污染最终预测决策时刻。这个技巧在竞赛里也常用实际操作起来非常简单。7. 调参实验记录看看不同配置到底差多少7.1 一组有代表性的对比结果为了让你对参数影响有一个直观的概念我用同一份电力负荷数据跑了下面几组实验。数据是2016个点前80%训练后20%测试统一用RMSE作为对比指标实验配置CEEMDAN参数网络结构RMSE原始LSTM无分解无单层LSTM(50)12.34CEEMDAN LSTMtrials50, epsilon0.005, max_imfs10单层LSTM(50)9.87CEEMDAN CNN-LSTMtrials50, epsilon0.005, max_imfs10CNN(32,5)LSTM(50)8.25CEEMDAN CNN-LSTM调参后trials50, epsilon0.01, max_imfs8两层卷积两层LSTM7.63从结果可以清楚看到三件事加了CEEMDAN分解RMSE立刻下降约20%说明分解步骤对整个预测链路的价值非常大在分解基础上引入CNNRMSE又下降了约16%说明CNN提取局部特征确实有效调参之后的进一步提升主要来自卷积核尺寸和IMF数量的选择。7.2 CEEMDAN的trials和epsilon对结果的影响我换了不同参数跑过几次大致规律是trials从20加到80分解稳定性提升预测误差最多能降5%-8%但训练时间翻倍。数据量不大时50次是性价比最好的折中epsilon的取值对结果影响很大当信号本身较平滑时可以调小0.005-0.02当信号毛刺多、噪声大时要调大0.03-0.05。没有万能的默认值必须结合数据的波动特征。7.3 卷积核尺寸的选择我用过1、3、5、7四种卷积核尺寸。结论是kernel_size1相当于逐点全连接CNN完全失去了局部特征提取的意义效果最差kernel_size3效果不错误差比5略高一点点kernel_size5在这个任务里最优因为数据的局部波形大约跨越5-6个时间点kernel_size7会对局部特征做过多平滑结果反而略有下降。这个经验可以推广卷积核尺寸应当尽量匹配数据中最常见的局部形态长度。如果你的数据是周周期且采样频率为小时那么局部形态可能在几十个点此时5都不够用要适当增大。7.4 我的最终配置和运行效果我最后定下来的完整配置如下这份配置直接放在config.py里方便复现# config.py WINDOW_SIZE 48 HORIZON 12 BATCH_SIZE 64 EPOCHS 100 LEARNING_RATE 0.001 CEEMDAN_PARAMS { trials: 50, epsilon: 0.01, max_imfs: 8, } MODEL_PARAMS { filters: 32, kernel_size: 5, lstm_units: [50, 30], dropout: 0.2, }在实际运行这套配置时单次训练大约需要3分钟CPU环境测试集RMSE在7.6左右MAPE控制在4%以内。对我来说这个精度已经足够支撑业务侧做中长期的负荷趋势判断。我在实际使用中发现这套CEEMDAN-CNN-LSTM的框架还有一种很自然的扩展方式如果你手里拿到的多变量数据可以把每个变量也做CEEMDAN分解把所有IMF拼成大通道矩阵一起输入CNN。相当于让模型自己学习不同变量、不同频段之间的交叉特征。我后来在交通流量预测场景里试过效果比单变量版本又提升了一截。当然通道越多数据量和计算量都跟着涨需要自己权衡。本文还有配套的精品资源点击获取