CNN-GRU-Attention组合模型在电力负荷预测中的实战解析
2026/9/23 15:03:32 网站建设 项目流程

简介:这个压缩包提供了一套基于CNN-GRU-Attention混合深度模型实现的电气预测完整代码,适合电气工程、数据科学及深度学习初学者与研究者使用。项目围绕电气时间序列数据(如负荷、电压、电流等)的处理与预测展开,涵盖了从数据预处理、模型构建、训练评估到结果可视化的标准流程。资源包共8个文件,主要包括2个Python程序(模型构建与训练脚本)、2个CSV数据文件(输入样本数据)以及4个说明性TXT文档(代码说明、依赖包版本等),压缩包大小仅1.24MB,轻量易部署。目前已有109人学习下载。通过这份代码,读者可以完整掌握CNN-GRU-Attention的组合建模思路:CNN负责提取局部特征,GRU捕捉序列长期依赖关系,注意力机制则强化关键时间步的影响。项目中还附有依赖包版本说明与运行提示,能帮助快速复现实验并迁移到电力需求预测、设备故障预警等类似场景。

1. 一张负荷曲线引发的“组合拳”:CNN-GRU-Attention 到底解决什么问题

做电气预测的人,手机里大概都存着几张深夜发来的曲线图:配电变压器一天 96 个点的负荷数据,一到早晚高峰就剧烈抖动,隔着屏幕都能感到现场工程师的无奈。单纯用 LSTM 或 GRU 去拟合这种序列,经常是“前面拟合得好好的,一进突变段就崩”;反过来只上卷积网络,又会把时序的先后顺序抹成一团。这个名为“051cnn-gru-attention(预测 Python程序)”的项目,核心就是用三种结构各管一段:CNN 抓局部波动形态,GRU 接力捕捉前后依赖,Attention 在输出前把历史步里真正关键的时刻挑出来加权。组合起来预测电力负荷、设备温度这类带明显周期又掺杂突变的序列,效果往往比单独用其中任何一种都稳。

这套方案适合两类人。一类是刚把 Python 环境装好、想找一份能直接跑的预测代码入门的初学者,跟着把数据换成自己的 CSV 就能出结果;另一类是已经用 LSTM 做过预测、正被“预测值滞后一拍”折磨的从业者,想看看注意力机制能不能把尖峰拉回来。下面从原理到复现,把每个环节讲透,包括那些不跑一遍绝对发现不了的坑。

2. 拆开模型看门道:CNN、GRU、Attention 各司其职,为什么非要搭在一起用

2.1 CNN 先上场:用卷积把曲线里的“拐点”抠出来

先想一个问题:一段 96 点的日负荷曲线,模型第一眼最该看什么?答案是局部形态——早高峰从 7 点开始爬升、午休回落、晚高峰再次拉起,这些“形状”在连续几个时间步里是有固定模式的。GRU 虽然能记东西,但它是按时间步逐个读入的,对“连续 3 个点急速上升”这种局部特征不敏感;卷积不一样,一个大小为 3 或 5 的卷积核滑过序列时,天然就在做“相邻几个点之间的关系”提取。CNN 在这一步扮演的角色,相当于先把原始序列里最有判别力的短片段筛出来,再交给 GRU 去编排顺序。

常见做法是用一维卷积(nn.Conv1d)对序列做特征提取。卷积核数量一般取 32 到 128 之间,核大小取 3 或 5,不要取太大,因为电气负荷序列的局部模式通常在 3 到 5 个点内就能体现;取 7 以上反而会模糊掉突变的边界。步长保持默认的 1,padding 设为“same”保证卷积前后长度一致,这样后面接 GRU 时不用重新对齐时间步。

2.2 GRU 接力:门控机制记住“昨天这时候在干嘛”

GRU 是 LSTM 的轻量改良版,只有更新门和重置门两个门,参数更少、训练更快,在序列长度几百的量级上精度几乎不输 LSTM。在这个组合里,它的输入是 CNN 提取出的特征序列,输出是每个时间步的隐藏状态。这些隐藏状态承载的不只是“当前长什么样”,还包括“相对于之前几步,当前处于什么阶段”——比如电网负荷的“上周同一时刻”效应,就是靠 GRU 的隐藏状态传递记住的。

选 GRU 而不是 LSTM 还有一个现实原因:训练更稳。LSTM 有三个门,在数据量不大、又没做精细调参的情况下,更容易出现梯度问题;GRU 结构简单,对学习率不那么敏感,对“数据拿来就跑”的工程场景更友好。隐藏层大小建议取 32 到 64 之间,层数一层就够。电气预测任务输入特征维度通常不高(负荷值、温度、湿度、星期几等),两层 GRU 带来的收益很小,却会让参数量翻倍、训练时间明显变长。

2.3 Attention 最后拍板:凭什么预测值要参考第 37 步而不是第 12 步

GRU 的最后一个隐藏状态确实编码了整个序列的信息,但当序列长度超过 30 步时,早先的信息在传递中会有损耗。Attention 机制不依赖这种“挤压式”的记忆,它让模型在输出前回看每一个时间步的隐藏状态,算出一个权重:权重大的时刻被重点参考,权重小的时刻基本忽略。这解决了一个非常实际的问题——预测 18 点的负荷时,模型应该重点参考 17 点和昨天 18 点的状态,而不是平均看待过去 24 小时的每一个点。

一个容易误解的点是:这里的 Attention 不是 transformer 里的自注意力(self-attention),而是加性注意力或乘性注意力,专门对 GRU 隐藏状态序列做加权求和。它不需要位置编码,也不用做多头,结构非常轻。计算过程不复杂:先把每个时间步的隐藏状态通过一个线性层映射成一个标量分数,再用 softmax 归一化成权重,最后把隐藏状态按权重加权求和,得到一个上下文向量,拼上最后一拍的隐藏状态一起送进全连接层输出预测值。

2.4 组合模型的参数规模与训练成本预估

模块典型参数输出形状变化
Conv1d 输入in_channels=特征数, out_channels=64, kernel=3(batch, 特征数, 时间步) → (batch, 64, 时间步)
GRU 输入hidden_size=64, num_layers=1(时间步, batch, 64)
Attention64→1 线性层(时间步, batch, 64) → (batch, 64)
全连接输出64+64 → 1(batch, 128) → (batch, 1)

参数量粗略估算在几十万这个量级,单张普通显卡或者 CPU 都能在几分钟内完成一轮训练。这正是这个组合的工程优势:比纯 Transformer 轻得多,但比纯 LSTM 能抓住更多局部特征和关键历史时刻。

3. 从零跑通预测程序:数据预处理、模型定义与训练参数怎么设

3.1 数据和环境准备:先看清输入输出再动手

动手前建议先建一个干净的 Python 3.8 以上环境,用 venv 或 conda 都行,避免把系统 Python 搞乱。需要安装的核心库就四个:torchnumpypandasmatplotlib。如果机器没有 N 卡,直接装 CPU 版 PyTorch,这个规模的模型 CPU 训练完全能接受;装完顺手验证一下python -c "import torch; print(torch.__version__)",最快能发现环境问题。

数据格式上,多数预测程序的原始输入是 CSV 文件,至少包含两列:时间戳和预测目标值。常见做法是额外拼上几个手工特征——小时数、星期几、是否节假日,这些对电力负荷预测特别管用。模型输入通常是一个滑动窗口:用过去seq_len个时刻的特征预测未来pred_len个时刻的值。seq_len 常见取 24 或 48(对应 24 小时或两天),pred_len 取 1 或 24 都可以,看你要做单步预测还是多步预测。

下面是一个标准的滑窗数据集构造代码,输入的原始数据假设是 pandas DataFrame:

import numpy as np import pandas as pd def create_sequences(data, seq_len=24, pred_len=1): """ 把长序列切成 (输入窗口, 预测目标) 的样本对 data: 二维 numpy 数组, 每行是一个时刻的特征 """ X, y = [], [] for i in range(len(data) - seq_len - pred_len + 1): # 取过去 seq_len 个时刻做输入 X.append(data[i : i + seq_len]) # 取未来 pred_len 个时刻的某一列做预测目标 y.append(data[i + seq_len : i + seq_len + pred_len, 0]) return np.array(X), np.array(y) # 示例: 加载 CSV, 取特征列 df = pd.read_csv("load_data.csv") # 假设特征列: load, hour, is_weekend, temp features = df[["load", "hour", "is_weekend", "temp"]].values X, y = create_sequences(features, seq_len=24, pred_len=1)

这里有几个参数需要较真。seq_len=24对应一天 24 个点,如果数据是 15 分钟一个点,一天就是 96 个点,seq_len就该取 96。pred_len=1表示只预测下一刻;预测未来 24 个点可以用pred_len=24,但训练难度会明显上升,建议先单步跑通再加预测步长。预测目标取第 0 列(load),因为建模目标就是负荷值,其余列对模型来说都是辅助特征。

数据切分完毕之后,有一个非常关键、又特别容易被忽略的步骤:归一化。推荐用sklearn.preprocessing.MinMaxScaler,把每一列特征压到 0 到 1 区间。注意要用训练集的数据去拟合 scaler,而不是对全量数据做归一化,否则测试集的信息在训练时就已经“泄漏”给模型了。

from sklearn.preprocessing import MinMaxScaler # train_df 是训练集部分, 不要混入测试集 scaler = MinMaxScaler(feature_range=(0, 1)) # 用训练集拟合 scaler train_scaled = scaler.fit_transform(train_df[["load", "hour", "is_weekend", "temp"]]) # 用同一个 scaler 转换测试集 test_scaled = scaler.transform(test_df[["load", "hour", "is_weekend", "temp"]])

fit_transform只用在训练集上,测试集只能用transform。如果先对全量数据做fit_transform,测试集里未来时刻的信息会参与归一化参数的估计,模型在训练时就“偷看”了未来数据的统计特征,测试结果会虚高,部署后真实表现立刻下滑。

3.2 模型定义与训练参数:核心实现逐行说明

这个组合模型的 PyTorch 实现不复杂,核心是把三个模块串起来。下面这段代码是一个可以直接跑通的最小实现:

import torch import torch.nn as nn import torch.nn.functional as F class CNNGRUAttention(nn.Module): def __init__(self, n_features, seq_len, hidden_size=64): super().__init__() # CNN 层: 1D卷积提取局部特征, kernel=3 看相邻3个点 self.conv1 = nn.Conv1d(n_features, 64, kernel_size=3, padding=1) self.conv2 = nn.Conv1d(64, 64, kernel_size=3, padding=1) # GRU: 把 CNN 特征按时间步读入 self.gru = nn.GRU(64, hidden_size, batch_first=True) # Attention 打分层: 把每个时间步的隐状态映射成标量 self.attn = nn.Linear(hidden_size, 1) # 输出层: 拼接注意力加权向量和最后时刻隐状态 self.fc = nn.Linear(hidden_size * 2, 1) def forward(self, x): # x: (batch, seq_len, n_features) # Conv1d 要求 (batch, channels, length), 所以要转置 x = x.permute(0, 2, 1) x = F.relu(self.conv1(x)) x = F.relu(self.conv2(x)) # 转回 (batch, seq_len, channels) 给 GRU x = x.permute(0, 2, 1) gru_out, _ = self.gru(x) # gru_out: (batch, seq_len, hidden_size) # Attention: 打分 -> softmax -> 加权求和 attn_scores = self.attn(gru_out).squeeze(-1) # (batch, seq_len) attn_weights = F.softmax(attn_scores, dim=1) context = torch.bmm(attn_weights.unsqueeze(1), gru_out).squeeze(1) # (batch, hidden_size) # 拼接最后时刻的隐状态 last_hidden = gru_out[:, -1, :] # (batch, hidden_size) combined = torch.cat([context, last_hidden], dim=1) return self.fc(combined)

重点讲三个设计点。第一,permute的原因:Conv1d默认输入是(batch, channels, length),而序列数据通常是(batch, length, features),所以必须先交换后两个维度。第二,两层卷积之间加 ReLU 激活,否则卷积的线性组合还是线性。第三,Attention 的bmm操作是批量矩阵乘法:attn_weights的形状是(batch, seq_len),先unsqueeze(1)变成(batch, 1, seq_len)才能与(batch, seq_len, hidden_size)做乘法,结果是(batch, 1, hidden_size)squeeze(1)后正好是加权求和向量。拼接时选gru_out[:, -1, :]作为补充,因为最后一个时间步的隐状态本身就带着最新的信息,和 Attention 抓重点历史形成了互补。

训练参数方面,batch size 常用 256,学习率 0.001 配 Adam 优化器是多数人的起点。如果用 GPU 训练,可以在模型对象创建后立刻执行.cuda()或把模型放进device,但要注意输入数据也必须在同一个设备上,常见的报错就是模型在 GPU 上而数据还在 CPU。Loss 函数用nn.MSELoss(),这是回归任务的标准选择,对异常值比较敏感——如果数据里有明显的坏数据(比如负的负荷值),建议先清洗掉,不然后期 loss 会一直下不来。

3.3 训练循环与验证:跑通最小 demo 的三个关键动作

训练循环的骨架是固定的,但有几个动作决定结果能不能用。第一,每次迭代要把优化器的梯度清零,否则 PyTorch 会默认积累梯度。第二,model.train()model.eval()必须切换,虽然这个模型没有 dropout 和 batch norm 影响不是致命的,但从一开始养成习惯可以避免以后换更复杂模型时踩坑。第三,验证阶段必须用torch.no_grad()包裹,否则验证过程会构建计算图、白白消耗显存,甚至把验证数据当成训练样本来更新梯度。

def train_model(model, train_loader, val_loader, epochs=50, lr=0.001): optimizer = torch.optim.Adam(model.parameters(), lr=lr) criterion = nn.MSELoss() for epoch in range(epochs): model.train() train_loss = 0.0 for x_batch, y_batch in train_loader: optimizer.zero_grad() pred = model(x_batch) loss = criterion(pred, y_batch) loss.backward() optimizer.step() train_loss += loss.item() * len(x_batch) val_loss = evaluate(model, val_loader, criterion) print(f"Epoch {epoch+1}: train_loss={train_loss/len(train_loader.dataset):.6f}, " f"val_loss={val_loss:.6f}") def evaluate(model, val_loader, criterion): model.eval() total_loss = 0.0 with torch.no_grad(): for x_batch, y_batch in val_loader: pred = model(x_batch) loss = criterion(pred, y_batch) total_loss += loss.item() * len(x_batch) return total_loss / len(val_loader.dataset)

如果跑完 50 轮,训练 loss 还在 0.01 以上,不用急着调模型结构,优先检查三个地方。第一个是归一化是否生效——拿原始数据直接喂给网络,数值范围大了几个数量级,梯度自然会爆。第二个是学习率是否过大——把lr从 0.001 降到 0.0003 试试,通常会有惊喜。第三个是seq_len和预测目标是否对齐——y_batch的形状可能是(batch, pred_len)而不是(batch, 1),这时候需要把模型输出形状和标签形状统一,常见做法是在计算 loss 前加一行pred = pred.view(-1, pred_len)

模型结构本身的调参空间主要在两个方向:CNN 的卷积核数量决定局部特征的丰富度,GRU 的hidden_size决定时序记忆的容量。数据量大的,可以把这两个参数加倍(从 64 加到 128),感受一下训练时间和精度的权衡;数据量只有几千条,保持 64 就好,参数太多容易过拟合。

4. 训练避坑清单:Loss 不降、数据泄漏、多步预测失真,这 5 个坑我全踩过

4.1 归一化泄漏:测试集提前“偷看”了未来信息

现象:训练时验证集 loss 一路降到接近 0,模型看起来完美,但换一批新数据预测,误差立刻涨了一倍还不止。原因:对全量数据一起做MinMaxScaler.fit_transform,测试集的均值和最大值混进了归一化参数里,相当于模型在训练阶段就知道了未来数据的大致分布。解决:严格用训练集fit出 scaler,再对测试集只做transform。这个坑最隐蔽,因为训练曲线完全正常,甚至比正常模型还要漂亮,但它纯属“作弊”出来的幻觉。

4.2 形状不匹配:Conv1d 的通道维度总报错

现象:一跑模型就报Expected 3D input (batch, channels, length)或者mat1 and mat2 shapes cannot be multiplied。原因:Conv1d期望输入是三维,且中间的通道维必须等于in_channels。数据原始形状是(batch, seq_len, features),直接喂给卷积层后,它把seq_len当成了通道数,维度全错位了。解决:在进入Conv1d前先x.permute(0, 2, 1),把特征维换到通道位置;用完卷积再permute回来给 GRU。遇到维度报错,第一步先打印每一层前后的形状,判断是哪一步的维度悄悄变了。

4.3 预测值严重滞后:像把昨天的值抄了一遍

现象:预测曲线和真实曲线几乎平行,但整体向右平移了一个时间步,专业术语叫“滞后效应”。原因:序列本身就存在强烈的自相关性,当前时刻的值和上一时刻的值高度相近,模型学着学着发现“直接把上一个点抄过来”的 loss 最低,于是偷懒了。解决:给输入数据增加外部特征(温度、湿度、星期几),迫使模型去学习真正的影响因素,而不只是时序自回归。第二个办法是加大seq_len,让窗口从 24 扩到 48,让模型有更多上下文去判断“这个上升是开始还是快结束了”。第三个是在 loss 上叠加一个一阶差分损失,惩罚预测值在相邻时刻的跳变不合理。

4.4 多步预测误差随步长急剧膨胀

现象:pred_len=1没问题,改成预测未来 24 个点之后,越往后预测越不准,最后变成一条水平线。原因:多步预测有两种策略——递归预测(把上一步的预测值喂回输入)和直接预测(一次性输出多个值)。递归预测的误差会逐级累积,而且模型在训练时用的是真实值做输入,推理时用的是自己的预测值,这个分布不一致导致误差越滚越大。解决:改用直接预测,让模型的输出层输出pred_len个值,训练时用对应的未来一段真实序列做标签。代价是输出层参数变多,但避免了误差累积。更好的做法是引入课程学习或者计划采样(scheduled sampling),训练中也有很多讲究,不建议入门时就碰,先把直接预测跑通。

4.5 训练很快收敛但测试集崩溃:典型的过拟合信号

现象:训练 loss 掉到 0.0001,测试 loss 却在某个 epoch 之后开始回升。原因:模型参数量相对数据量来说太大了,把训练集的噪声都背了下来。电气预测的数据通常只有几个月到一两年,几千个样本很常见。解决:按优先级做三件事。第一,在 GRU 后面加nn.Dropout(p=0.3),注意只加在训练时。第二,把训练 epoch 数减半并配合早停(patience=10),验证集 loss 连续 10 轮不降就停。第三,缩小hidden_size和卷积核数量,从根上减少参数容量。在调这些参数的时候顺便说一句:用验证集反复调参存在“对验证集过拟合”的风险,数据量允许的话,最好切出一段末期的时间段做独立的测试集,只在中间的验证集上调参。

4.6 数据顺序被打乱:时间序列切分切出了“未来”,切片千万别 shuffle 整个数据集

现象:训练 loss 低,验证 loss 也不算差,但模型上线第二天就开始“发疯”,预测值明显离谱。原因:时间序列必须按时间顺序切分。如果像普通分类任务那样对整个数据集做随机打乱,再把前 80% 当训练、后 20% 当测试,那么测试集里混着比训练集更早的数据,模型在训练时已经见过了测试样本的信息。解决:切分数据集时只用位置索引切片,先切后洗。即便在训练集内部要用 shuffle,也必须在切片之后按样本顺序做 shuffle 打包,而不能把整个数据集洗乱再切。

5. 把模型用出工程味:可复现设置、K 折验证与导出部署的细节

5.1 先定随机种子,再谈结果好坏

模型训练跑通以后,第一件事不是调参,而是做可复现性设置。PyTorch 的训练结果受三个随机源影响:模型权重初始化、数据加载器的打乱顺序、GPU 上的某些算子。如果不固定,同一套代码跑两次结果线性都不完全相同,你很难判断改动参数到底是真有效还是随机波动。一份能锁死的代码需要同时设置三个随机源:

def set_seed(seed=42): import random random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) # 部分算子会改用确定性算法, 速度略慢但结果可复现 torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False

cudnn.deterministic保证卷积算子选择确定性的实现,代价是可能慢一点;benchmark=False禁用运行时自动寻找最优算法的逻辑。这两行在调试阶段必须开启,等完全定稿后再考虑关掉提速。还有一个细节容易被忽略:torch.utils.data.DataLoader里的shuffle=True依赖全局随机状态,固定了上面的种子之后它也一并确定了。

5.2 时间序列的 K 折验证:滚动切分而不是随机切分

常规 K 折交叉验证在序列预测里不适用——随机把数据切成 5 份,每份里都散落着不同时间段的数据,训练集中某些片段比验证集更靠后,等于又泄漏了。适合序列预测的是滚动前向验证:第一次用第 1 到第 100 天训练、第 101 到 110 天测试;第二次用第 1 到 110 天训练、第 111 到 120 天测试;依此类推。这样每一折的测试数据都严格晚于训练数据,反映的是真实部署时的情形。如果数据量不够做多折,至少保证测试集是一段连续的、时间上晚于训练集的数据,而不是随意抽出来的散点。

5.3 导出与落地:从训练脚本到可调用组件

模型训练完成只是第一步,实际使用时要把它变成一个可以接收新数据、返回预测结果的函数。常见做法是保存模型参数,然后在需要预测时加载:

# 训练结束后保存 torch.save(model.state_dict(), "cnn_gru_attention.pt") # 预测时加载 model = CNNGRUAttention(n_features=4, seq_len=24, hidden_size=64) model.load_state_dict(torch.load("cnn_gru_attention.pt")) model.eval() def predict_next(model, recent_window, scaler): """ recent_window: 最近 seq_len 个时刻的原始特征 (二维 numpy) 返回: 反归一化后的预测值 """ x = scaler.transform(recent_window) # 用训练时保存的 scaler x_tensor = torch.FloatTensor(x).unsqueeze(0) # 加 batch 维 with torch.no_grad(): pred_scaled = model(x_tensor).item() # 注意: 反归一化需要把预测值还原到原始量纲 # 由于 scaler 是 4 列的, 构造一个虚拟行再取第 0 列 dummy = np.zeros((1, 4)) dummy[0, 0] = pred_scaled return scaler.inverse_transform(dummy)[0, 0]

load_state_dict要求模型的类定义结构和保存时的完全一致,改过hidden_sizen_features都会导致加载失败。一个非常有用的习惯是:保存模型时把seq_lenhidden_sizen_features这些超参数同时存成一份 JSON,下次加载时先读参数再建模型,避免“模型文件还在但结构定义忘了”的局面。把加载操作包成一个类或者函数,是为了让模型只暴露predict接口,内部用torch.no_grad()保证推理阶段不构建计算图,节省内存。

5.4 后处理与评估:别忘了把结果还原成真实数值

模型输出的是归一化后的值,如果不还原,预测的“负荷”没有任何物理意义。评估指标同样要在真实量纲上计算,常用的有 RMSE(均方根误差)和 MAPE(平均绝对百分比误差)。MAPE 对接近 0 的真实值特别敏感,如果负荷序列有接近零的时刻,MAPE 会被个别点拉得特别大,这时候改用 MAE 或 RMSE 更稳妥。绘制预测 vs 真实的曲线时,也建议把两条曲线都还原到原始量纲,肉眼看偏移量比看归一化数字直观得多。

我在做了几个预测项目之后养成的习惯是:每次训练结束,立刻把当次的随机种子、超参数、训练/验证 loss 曲线、测试集指标一起归档成一个文本文件。这个动作不值钱,但回看时很有用——否则三个月后回头,面对一份跑出好结果的代码,你会完全想不起当时的参数是怎么设的,那种白折腾的感觉最磨人。这套 CNN-GRU-Attention 组合方向经过多个电气场景验证,值得你投入时间把它吃透;从单步预测做起,跑通后再逐步加大步长、丰富特征,每一步都有可见的收益。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询