简介面向电力负荷预测与毕业设计场景的 Python 实现方案围绕 VMD-CNN-BiLSTM-Attention 混合模型涵盖数据分解、特征提取、时序建模与注意力机制融合等关键环节。压缩包共15个文件大小2.86MB包含多个 Python 模型脚本、Jupyter Notebook 调试记录、备份文件、电力负荷数据集及说明文档兼顾可运行代码与过程笔记便于对照学习和二次开发。已有119人学习下载。资源提供 VMD 分解数据保存脚本、四折交叉验证 Notebook、多种对比模型如 VMD-CNN-LSTM、VMD-CNN-BiGRU等既可直接用于负荷预测实验也能帮助理解模型组合与参数调优思路。资源来源于网络分享仅供学习交流使用前可先阅读说明文档确认数据格式与运行依赖对正在完成毕业设计或开展短期预测研究的读者具有较高参考价值。1. 负荷预测为什么绕不开 VMD-CNN-BiLSTM-Attention 这个组合电力负荷数据天生不稳定有日周期、周周期还有节假日、温度突变带来的非平稳波动。单靠 LSTM 硬训模型容易把周期信息记住却在尖峰时段彻底失准。VMD-CNN-BiLSTM-Attention 这个组合本质是把分解、提特征、建模时序、聚焦重点四件事拆给四个模块做VMD 先把非平稳负荷拆成若干个相对平稳的分量CNN 抓局部形态BiLSTM 从两个方向读时序依赖Attention 再从长序列里挑出真正影响预测结果的关键时步。组合模型在短中期负荷预测里通常比单一 LSTM 在 MAPE 上再降 3~8 个点但它最大的问题不是原理而是在 Python 里把四个模块拼起来时数据和形状的坑远比模型本身多。这篇文章就把全流程拆开讲清楚适合正在做负荷预测论文、平台原型或者想用它当基线模型的工程师。2. 动手前先定好数据协议切分、归一化与样本构造2.1 数据清洗先做三件事不管用哪个公开负荷数据集还是自己从现场取数第一件事永远是先把时间序列整理干净。我一般按三个步骤过一遍先按时间戳升序排列并去重重复的时间戳按记录取均值或直接删掉再处理缺失值短缺失用线性插值长缺失不要瞎补直接截掉整段最后处理异常值比如负荷突然跌到 0 又跳回正常值这种多数是采集端问题用超过中位数 3 倍绝对偏差的规则替换成前后均值。清洗时有个分寸宁缺毋滥。不要用平滑滤波把负荷曲线磨得太光VMD 后面还要做分解过度平滑会把真正的尖峰信息抹掉模型学到的就是一个低通滤波后的世界。清洗的目的是让数据可用不是让数据好看。2.2 切分和归一化时间序列的泄漏红线时间序列数据切分不能随机打乱。正确做法是按时间顺序切成训练集、验证集、测试集三段比例建议 70%/15%/15%。注意验证集一定要从训练集之后切测试集必须是时间上最靠后的那段。如果你随机抽样切分模型等于提前看到了未来的统计分布测试指标会好看得离谱上线后马上翻车。归一化是个重灾区。很多人在整个数据集上 fit 一个 MinMaxScaler再切分训练集测试集这种做法有严重的数据泄漏。测试集的最大值最小值已经参与训练模型相当于提前知道了未来量纲验证集和测试集的指标都会偏乐观。正确做法只有一种from sklearn.preprocessing import MinMaxScaler # 只允许在训练段上 fit scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train[[load]].values) # 验证集、测试集用同一套参数 transform val_scaled scaler.transform(val[[load]].values) test_scaled scaler.transform(test[[load]].values)这里fit_transform只出现在训练集上transform对验证集、测试集生效保证三段数据用完全相同的缩放参数。这样做还有一个额外好处上线时你只需要保存这一个 scaler 对象新来的实时数据用 joblib 或者 pickle 加载后 transform 即可。2.3 滑动窗口构造样本序列切成模型能吃的形状负荷预测的标准做法是把连续时间序列切成输入窗口 预测目标的样本对。假设你的数据是 15 分钟一条那么一天就是 96 个点。输入窗口取 96预测未来 1 个点这就是一个典型样本。窗口滑动步长不要设成 1否则相邻样本高度重合训练集冗余很大我一般取 step6也就是每 6 个时间点生成一个样本。import torch from torch.utils.data import Dataset class LoadDataset(Dataset): def __init__(self, data, seq_len96, horizon1, step6): self.seq_len seq_len self.horizon horizon self.data data # 所有合法窗口的起点索引 self.indices list(range(0, len(data) - seq_len - horizon 1, step)) def __len__(self): return len(self.indices) def __getitem__(self, idx): i self.indices[idx] x self.data[i : i self.seq_len] # 输入特征形状 [seq_len, feat] y self.data[i self.seq_len : i self.seq_len self.horizon, 0] # 目标负荷 return torch.tensor(x, dtypetorch.float32), torch.tensor(y, dtypetorch.float32)这段代码的核心是indices列表。step6控制样本密度seq_len96控制模型能看到多长的历史horizon1先做单步预测。注意y取的是第 0 列也就是负荷本身因为后续要做 VMD 分解输入特征会从单列变成多列但预测目标始终只有一个负荷维度。3. VMD 分解把非平稳负荷拆成 K 个可预测的分量3.1 VMD 的原理与 K、alpha 两个关键参数变分模态分解VMD做的事情是把一段原始信号分解成 K 个带限模态分量。它的目标函数同时做了两件事每个模态围绕自己的中心频率保持窄带同时所有模态加起来要能重构回原始信号。这比 EMD 的递归筛选稳定得多不会因为一个小的噪声就导致模态完全变形。两个参数需要重点理解。K 是模态个数设小了会欠分解多个频率成分挤在一个模态里模型还是在学非平稳序列设大了会过分解出现两个中心频率几乎重合的模态模型被迫去拟合两条高度相关的曲线白白浪费参数。alpha 是带宽惩罚因子alpha 越大各模态带宽越窄中心频率分得越开。对电力负荷这种有日周期和周周期的信号我一般从 K5、alpha2000 起步再看中心频率分布微调。3.2 用 vmdpy 在 Python 里跑分解Python 生态里 VMD 的可用实现不多最常见的是 vmdpy 这个库它封装的是原作者释放的 MATLAB 算法逻辑接口很稳定。import numpy as np from vmdpy import VMD # load_scaled 是归一化后的一维负荷序列形状 [N] K 5 alpha 2000 tau 0 # 对偶上升步长0 表示让算法自动决定 DC 0 # 是否保留直流分量负荷数据一般设为 0 init 1 # 中心频率初始化方式1 表示均匀初始化 tol 1e-7 # 收敛容差 u, u_hat, omega VMD(load_scaled, alpha, tau, K, DC, init, tol) print(u.shape:, u.shape) # 期望输出 (K, N) print(最后一个迭代步的中心频率:\n, omega[-1])u是分解出的模态分量每一行是一条长度为 N 的 IMF 曲线u_hat是对应的频谱omega是每轮迭代的中心频率轨迹omega[-1]就是最终收敛的中心频率。分解完成后第一件事不是接模型而是先验证重构误差recon u.sum(axis0) recon_error np.max(np.abs(recon - load_scaled)) print(最大重构误差:, recon_error)正常量级应该在 1e-6 以下。如果重构误差偏大先检查输入序列是否有 NaN再看 tau 是否被手动设成了奇怪的值。重构没问题说明分解环节是可信的数据已经变成了 K 条更平稳的分量接下来才轮到模型出场。3.3 怎么判断 K 选没选对只看重构误差远远不够因为 K1 也能重构得很好。核心判断方法是看中心频率的分布。VMD 迭代完成后打印omega[-1]如果是一个单调递增的序列比如 [0.03, 0.08, 0.15, 0.28, 0.62]说明 K5 是合理的各模态在频域上分得开。如果发现其中两个中心频率的差小于 0.02基本可以判定发生过分解把 K 减一再跑一轮。还有一个比较省事的方法把每个模态的曲线画出来如果某个模态的波形看起来和另一个几乎一样只是幅值差个倍数那就是 K 设大了。负荷数据本身具有强周期性一般 K 在 4~8 之间就能覆盖。3.4 分解结果怎么接进 CNN-BiLSTM-Attention很多实现是每个模态单独用一个模型预测再把结果加起来这种做法准确率确实可以但训练开销直接变成 K 倍。工程上更常用的方案是把 K 条模态分量堆叠成多通道特征喂给一个模型让模型自己学会融合这些分量。具体到数据形状原序列是[N, 1]VMD 分解后转置成[N, K]即每个时间步上有 K 个特征。这样滑动窗口切出来的样本 x 形状变成[seq_len, K]CNN 的输入通道数也变成 K。也就是说第 2 章的 Dataset 不需要改逻辑只需要把喂进去的data从单列变成 K 列即可。4. 模型主体CNN、BiLSTM 与 Attention 的搭建和参数选择4.1 CNN 卷积层用局部感受野抓负荷突变为什么前置一层 CNN因为 BiLSTM 对逐点的时序变化不敏感而负荷序列里有很多短时局部特征比如晚高峰突然爬升、空调负荷集中启动。一维卷积通过滑动窗口能提取这些局部形态参数量又小不会给整个模型增加多少训练压力。对负荷这种 96 点窗口卷积核大小取 7 比较合适padding3 可以让卷积输出长度和输入一致。输出通道一般取 64太少了特征表达不够太多了后面 BiLSTM 的计算量会明显上浮。4.2 BiLSTM双向读取时序依赖CNN 提取完局部特征后每个时间步变成一个 64 维向量整个序列变成了[batch, seq_len, 64]。BiLSTM 在这个序列上做时序建模。双向的含义是第 t 个时间步的输出同时由 t 之前的正向状态和 t 之后的反向状态拼接而成。对负荷预测来说某个时刻的负荷不仅依赖过去几个小时的变化趋势也依赖未来一段时间的变化斜率比如晚高峰是从哪个点开始拐头的单向 LSTM 只能看到过去双向能同时利用两侧信息。hidden_size 我一般取 64层数取 2。再加到 128 或 3 层收益通常不明显但训练时间会涨一截。这里有个容易踩的维度坑BiLSTM 的最终输出特征维度是hidden_size * 2因为双向两个方向各输出一份后续 Attention 和全连接层都必须拿这个维度去设计。4.3 Attention 加权把注意力放在关键时刻BiLSTM 输出了seq_len个时间步的状态但并非每个时间步对最终预测同等重要。Attention 的作用是给每个时间步学一个权重最后把所有时间步的状态做加权求和得到一个浓缩了关键信息的向量。这里用的是最简洁的加性注意力把每个时间步的2*hidden维状态过一层线性映射打出一个分数再对seq_len维做 softmax得到归一化权重。比起直接取最后一个时间步的输出Attention 的好处是让梯度从输出端直接流向那些关键时步不会因为序列过长而把信息冲淡到后面的时间步里。4.4 组合模型的完整 PyTorch 实现把上面几块拼起来就是一个可运行的模型定义import torch.nn as nn import torch.nn.functional as F class VMD_CNN_BiLSTM_Attention(nn.Module): def __init__(self, in_channels, seq_len, lstm_hidden64, lstm_layers2, horizon1): super().__init__() self.conv nn.Conv1d(in_channels, 64, kernel_size7, padding3) self.bilstm nn.LSTM( input_size64, hidden_sizelstm_hidden, num_layerslstm_layers, batch_firstTrue, bidirectionalTrue ) self.attn nn.Linear(lstm_hidden * 2, 1) self.fc nn.Linear(lstm_hidden * 2, horizon) def forward(self, x): # x: [batch, seq_len, in_channels] x x.permute(0, 2, 1) # [batch, in_channels, seq_len] x F.relu(self.conv(x)) x x.permute(0, 2, 1) # [batch, seq_len, 64] out, _ self.bilstm(x) # [batch, seq_len, 2*hidden] attn_w torch.softmax(self.attn(out), dim1) # [batch, seq_len, 1] ctx (out * attn_w).sum(dim1) # [batch, 2*hidden] return self.fc(ctx)几个关键点说明第一Conv1d默认在最后一维做卷积所以输入要先permute成[batch, channels, seq_len]第二padding3配合kernel_size7能保证卷积后序列长度不变否则后面 BiLSTM 的时间步数会对不上第三Attention 权重在dim1上 softmax也就是每个样本内部、所有时间步之间做归一化而不是跨样本归一第四ctx的维度是2*hidden最后用一层全连接映射到horizon。整个模型参数量在 20 万到 30 万之间CPU 上也能跑。5. 训练、调参与五个翻车现场5.1 训练循环与梯度裁剪模型搭建完训练循环本身不长但有两个细节必须写进去梯度裁剪和验证集早停。BiLSTM 在长序列上很容易梯度爆炸典型表现是 loss 突然变成 NaN。梯度裁剪能直接掐掉这种风险import torch.optim as optim model VMD_CNN_BiLSTM_Attention(in_channels5, seq_len96, horizon1) criterion nn.L1Loss() # MAE 作为训练目标对尖峰不那么敏感 optimizer optim.Adam(model.parameters(), lr3e-4) for epoch in range(100): model.train() train_loss 0.0 for x, y in train_loader: optimizer.zero_grad() pred model(x).squeeze(-1) loss criterion(pred, y) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() train_loss loss.item() model.eval() val_loss 0.0 with torch.no_grad(): for x, y in val_loader: pred model(x).squeeze(-1) val_loss criterion(pred, y).item() print(fepoch {epoch:02d} | train_mae {train_loss / len(train_loader):.4f} | val_mae {val_loss / len(val_loader):.4f})clip_grad_norm_的max_norm1.0表示把整个参数梯度向量的 L2 范数限制在 1.0 以内超过就等比缩放。这个值不需要精细调设在 0.5 到 5.0 之间都能起到保护作用。训练时盯验证集 loss连续 10 个 epoch 不降就停保存验证集 loss 最低的那份权重。不要等训练结束才选模型那是事后诸葛亮。5.2 三个必调参数学习率、batch_size 与 seq_len第一个是学习率。用 Adam 的话默认 1e-3 偏激进我一般从 3e-4 起步。如果训练 loss 前几个 epoch 下降很猛但验证集抖动厉害马上把学习率降到 1e-4如果 loss 几乎不动先别调结构把学习率提到 1e-3 试试看学习率对收敛速度的影响比模型宽度大得多。第二个是 batch_size。负荷数据训练集通常就几万条样本batch_size 取 64 或 128 都行。数据量少于一万条时batch_size 尽量别超过 64否则每个 batch 的梯度方向太平均模型收敛得慢且容易陷入局部平缓区。第三个是 seq_len。这个参数必须和数据的采样周期对齐。15 分钟粒度取 96一天小时粒度取 24一天或 168一周。如果数据有明确的周周期性直接拉长到一周的跨度通常比强行堆层数更有效。5.3 五个翻车现场与排查路径翻车 1测试集最后一天的前几个点预测误差突然放大。现象是训练集、验证集表现都不错滚动预测一到最后一段就开始飘。原因是 VMD 分解基于频谱计算在序列两端会产生边界效应模态分量在首尾出现小幅摆动。解决方法是分解前对序列两端做对称延拓预测完再裁掉延拓段如果只是做离线实验更省事的办法是直接丢弃序列开头和结尾各 1~2 个采样点别让边界脏数据进测试集。翻车 2K 取大了两个中心频率几乎重合。现象是打印omega[-1]发现有两个值非常接近模型训练速度明显变慢。原因是过分解把同一频带拆成了两个模态模型花了大量参数拟合两条几乎一样的曲线。解决方法是把 K 减一重新分解直到中心频率之间有明显间隔。翻车 3训练到一半 loss 变成 NaN。现象是 loss 曲线先正常下降突然某一步直接变成 NaN之后所有参数更新都失效。原因是 BiLSTM 这种循环结构对梯度幅度很敏感多层叠加后梯度呈指数级放大。解决方法是先确认已经加了梯度裁剪如果还出 NaN把学习率降到 1e-4再把归一化检查一遍确保输入里没有 NaN 或 inf。翻车 4测试集指标离谱地好一到别的时段数据就废。现象是同一份数据上验证集 MAPE 很低换一段新数据预测结果一塌糊涂。绝大多数情况不是模型好而是数据泄漏。检查一下归一化 scaler 是不是在整个数据集上 fit 过或者切分时是否混入了重叠窗口。时间序列切分没有随机性一旦出现验证集异常好的错觉先怀疑数据流程别急着怀疑模型。翻车 5预测值几乎是一条平线等于训练集均值。现象是预测曲线的波动幅度很小整体贴着均值走。原因有两个方向一是目标直接回归绝对负荷模型发现输出均值这个策略在 MAE 上损失已经够小就懒得学细节了二是 VMD 分解后低频分量占主导高频分量被模型忽略。解决方法是把预测目标改成差分序列比如预测t时刻与t-1时刻的差值叠加上一步真实值作为最终输出模型学起来会容易很多。6. 向前一步多步预测、基线与可复现的验证习惯6.1 从单步滚动走向直接多步预测单步模型滚动预测未来 24 个点误差会随步数累积越往后越不可信。工程上更推荐直接多步预测把输出维度改成 horizon一次预测未来一段序列。改动极小把模型定义里的horizon设为 24fc输出维度自动对应调整训练时y也换成 24 个点的序列。代价是输出层的拟合难度变大但避免了误差累积带来的预测越来越平滑问题。如果训练后长时间步的预测值偏保守可以把损失函数换成 MAEMSE 加权或者单独提高后几步损失的权重。6.2 验证时把工作日和休息日分开统计负荷预测里工作日和休息日的曲线形态差异很大混合统计会把指标平均得看起来还行。我一般的做法是写一个按星期几分组的评估脚本分别算工作日、周末、节假日的 MAPE 和峰值时段 MAPE。如果模型在工作日表现正常但周末误差显著放大问题通常出在特征侧也就是模型没有有效感知今天是星期几。解决办法是在输入特征里加上 one-hot 的星期特征和是否节假日特征这些特征作为额外通道拼到 VMD 分量后面CNN 会自己学会区分。6.3 固定随机种子、先跑基线再上组合给模型做复现时第一件事是固定随机种子。PyTorch 里需要同时固定 Python、NumPy、PyTorch 三处import random import numpy as np import torch random.seed(42) np.random.seed(42) torch.manual_seed(42) torch.cuda.manual_seed_all(42) torch.backends.cudnn.deterministic Truecudnn.deterministic True这行容易被漏掉不设的话卷积在 GPU 上的实现仍然有随机性每次推理结果都会略有差异。除了复现还要养成先跑基线的习惯用同一个数据流程先训一个纯 LSTM 模型再逐步加 CNN、BiLSTM、Attention每一步单独记录 MAPE 的增量。这既能看到每个模块的真实贡献也能在论文或项目汇报里给出可信的对比依据不然你没法解释为什么这个组合比单个 LSTM 好。我自己的习惯是每换一个数据集先把基线跑通再把 VMD 单独替换成 EMD 比较一次确认分解方式的收益真实存在后再接入完整模型。这样一轮下来代码和结论都不会心虚。希望帮到你。本文还有配套的精品资源点击获取