CNN-GRU-Attention负荷预测程序:原理与PyTorch实战
发布时间:2026/10/1 10:34:39来源:尧图网络
简介面向电气领域的深度学习预测任务这份Python代码包融合了CNN、GRU与Attention三种结构CNN用于提取信号局部特征GRU处理时间序列依赖注意力机制则聚焦关键输入适用于电力负荷预测、设备状态监测等场景。压缩包共8个文件、约1.24MB包含2个py脚本模型搭建与训练流程、2个csv数据样本及4个txt说明文档含依赖包版本与使用指南结构精简清晰便于快速对照学习。目前已有109人学习适合具备一定Python与深度学习基础、希望复现和调优电气预测模型的研究者或工程师。通过该代码包可掌握完整项目流程数据预处理、模型定义、训练调参到结果可视化说明文件对运行环境和依赖版本做了标注能帮助减少环境配置障碍还可参考注意力权重分析模型关注的重要特征。作为电气预测方向的实例这份代码兼具教学与应用价值。1. 电气代码里的 cnn-gru-attention一套能直接改的负荷预测程序电力负荷预测和新能源功率预测属于典型的“历史序列进、未来曲线出”的回归任务。传统 ARIMA 压不住日波动纯 LSTM 又很容易把长程依赖学过头而这个 051cnn-gru-attention 程序代表的方案是在工程上被反复验证过的一条折中路先用一维卷积抓局部波形再用 GRU 接力学时序依赖最后由 Attention 把注意力动态放到关键历史时点上。适合短期负荷预测、光伏或风电功率预测这类场景新手可以拿它做 python 数据分析与可视化的进阶练习熟手则可以直接复用它的数据预处理和训练骨架。下面把模型怎么搭、参数怎么设、哪些坑千万别踩一次讲透。2. 为什么是 CNNGRUAttention三个模块的分工与协同任何一个混合模型如果说不清每个模块负责什么、为什么要按这个顺序拼接落地时一定会变成黑匣子。这一章先把原理层面的分工讲透后面第 3 章再落代码。2.1 一维卷积在时序预测里的角色提局部波形不是提图像特征很多做 python 量化和电气预测的同事第一次看到 CNN 出现在时序模型里都会下意识往图像识别方向想这是理解上的第一个坎。序列预测里用的是一维卷积Conv1D它只在时间维度上滑动本质是做“局部波形提取”。具体到负荷预测假设输入是一段过去 96 个时间点比如每 15 分钟一个采样点一共 24 小时的负荷曲线一维卷积核覆盖其中连续的 k 个点做一次加权求和得到一个输出值。卷积核内部的权重就是可学习的“波形模板”。比如某些核学到了“连续 6 个小时负荷单调上升”的形态某些核学到了“夜间平缓波动”的形态当真实序列滑到相似的位置时对应卷积核的激活值就会变大。为什么要把卷积放在 GRU 前面而不是反过来因为 GRU 的循环结构对局部波形不敏感。让 GRU 直接吃原始序列它的记忆容量会浪费在逐点拟合上而卷积层先把原始波形压缩成更干净的特征序列GRU 就能把容量花在“今天中午的走势和昨天中午的走势如何关联”这种更高层的依赖上。还有一个现实收益卷积层 池化层把序列长度砍半之后GRU 展开的时间步变少训练明显变快。这类 python 程序在 CPU 上跑也能接受靠的主要就是这一步降维。设置卷积层时有三个参数值得关注卷积核大小 kernel_size、卷积核数量 filters、池化步长。kernel_size 一般取 3 或 5对应“连续 3 到 5 个采样点的短时形态”如果采样间隔是 15 分钟kernel 取 5 大致覆盖 1 小时出头的波动这个范围对负荷的毛刺比较敏感又不会把周期信息搅碎。池化层常用 MaxPooling步长取 2这会让时间长度减半代价是时间分辨率打折。如果你的预测目标本身很平滑这个代价可以接受如果预测目标是每分钟级的光伏功率池化步长我会建议先保持 1等后面出现明显过拟合再考虑加池化。2.2 GRU 门控单元为什么在这个场景比 LSTM 更划算GRU 是 LSTM 的简化版只保留重置门和更新门没有独立的遗忘门和记忆单元。在电力负荷这类数据量通常只有几千到几万条、特征维度不高的工程场景里GRU 的参数量比 LSTM 少约四分之一到三分之一收敛更快对噪声的耐受性也更好。我习惯在同一份电气负荷数据上对比过 GRU 和 LSTM 的实际差异验证集 MAPE 差距一般只有 0.2% 到 0.5%但 GRU 训练耗时能省下大概 30%。换句话说在大多数工程场景下GRU 和 LSTM 的精度差异没有论文里渲染的那么明显但 GRU 的性价比是实打实的。如果你的项目对响应时间有要求比如要在几分钟内完成多个站点同时重训GRU 就是更务实的选择。这个标题里选 GRU我认为是工程取舍不是玄学。有一个细节容易被忽略GRU 的 num_layers 超过 1 时层与层之间要传 hidden statePyTorch 里需要给前几层设置 dropout但这些 dropout 只在训练时生效。很多程序跑出来的训练集表现和验证集表现差距过大一部分原因就是没有区分“层间 dropout”和“最后一层到输出的正则”。如果数据量不大我建议直接 num_layers1把正则精力放到输入侧的 dropout 上效果更可控。2.3 Attention 注意力机制让模型自己记住该记住的时间点GRU 的输出在最后一个时间步会包含整段序列的信息但距离较远的早期影响会被“冲淡”。Attention 模块做的事是对 GRU 产生的每一步隐藏状态做加权求和权重由当前预测目标动态决定而不是只认最后一个隐藏状态。这个程序里用到的 Attention 大都是加性注意力过程可以写成三步第一步对每个时间步的隐藏状态 h_t用可学习的向量算一个能量分数 e_t v_a · tanh(W_a · h_t b_a)第二步对这些分数做 softmax得到归一化权重 α_t第三步用 α_t 对所有隐藏状态做加权求和得到上下文向量 c这个 c 就是 GRU 输出的“浓缩摘要”。从工程角度讲加性注意力有个很实际的优点不用像 Transformer 那样考虑位置编码和 mask训练极其稳定几行代码就能嵌入现有模型。而且它的收益能看得到——没有 Attention 的模型预测曲线在负荷尖峰处容易被抹平因为最后一步隐藏状态携带有“平均”倾向的信息加了 Attention 之后模型会主动把权重落在历史上与当前时段最相似的片段上峰值的回升速度明显变快。但也别把 Attention 神话。如果输入序列本身就不长比如 48 个点以下加不加 Attention 的差异会缩小到几乎看不见因为 GRU 的最后一个隐藏状态已经够用。什么时候必须加序列长度超过 96、或者输入里混入多变量负荷、温度、湿度、节假日特征时Attention 的加权意义才完全发挥。2.4 从滑窗输入到预测输出把三个模块串成完整数据流串联起来看数据流是一个清晰的四步过程。第一步原始 CSV 经过滑窗构造变成形状为 (样本数, 序列长度, 特征数) 的张量比如 (8000, 96, 5)5 个特征可以是负荷、温度、湿度、是否为工作日、当天小时数的正弦编码。第二步张量进入一维卷积层卷积在特征维上跨通道滑动输出的形状变成 (batch, 序列长度, 卷积核数量)如果加池化序列长度减半变成 (batch, 48, 64)。第三步GRU 接过这个压缩后的序列输出每一步的隐藏状态形状为 (batch, 48, 隐藏单元数)。第四步Attention 对 48 步隐藏状态做加权求和得到上下文向量把上下文向量和 GRU 最后一步的隐藏向量拼接起来接一个全连接层输出预测值。这个流程里有几个细节值得反复确认。第一GRU 的 return_sequences 必须打开这样输出才是每一步的隐藏状态如果只输出最后一步Attention 模块就失去输入了。第二PyTorch 里 GRU 默认 batch 在第一维的第二个维度seq 在前需要设置 batch_firstTrue让输入保持 (batch, seq, feature)否则后面取最后一步时要写出非常绕的切片。第三全连接层的输出维度就是预测目标维度如果做单步预测就是输出 1 个数如果想一次预测未来 24 个点输出维度就设成 24但这种情况更推荐在后面用序列生成的方式迭代直接多输出会摊薄模型对时序依赖的学习能力。这三个模块加起来参数总量远远小于一个同规模的 Transformer训练时对 GPU 的要求也不高。这块逻辑在后面代码实战中会原样体现。3. 从 zip 压缩包到可跑通的预测程序代码实战拿到名为“051cnn-gru-attention预测 Python程序.”的压缩包解压之后做的事情按顺序就是确认工程结构、搭环境、处理数据、定模型、跑训练、出预测。下面按这个顺序走一遍代码以 PyTorch 为例这套结构在大多数类似程序中是通用的。3.1 解压后的工程结构与运行环境这类预测程序的解压目录通常包含一个 CSV 格式的数据文件典型字段包括时间、负荷/功率、温度等一个负责读取和构造训练样本的数据预处理脚本一个定义模型结构的 Python 模块一个跑训练主循环的脚本以及一个加载模型做预测和可视化的脚本。有的工程还会带一个小的配置文件把序列长度、batch size、学习率等参数集中放在一起方便调参时不用反复改代码。建议先不要急着跑训练脚本第一步做环境核对。常见要求是 Python 3.8 以上需要 numpy、pandas、scikit-learn、PyTorch 和 matplotlib。如果是新环境可以这样检查依赖是否齐全缺哪个补哪个python -c import numpy, pandas, sklearn, torch, matplotlib; print(deps ok) python -c import torch; print(torch.__version__, torch.cuda.is_available())第一行确认常用库都装好了第二行确认 PyTorch 版本以及是否有可用 GPU。如果你之前只按 python 教程搭过基础环境多半会缺 sklearn 和 torch前者直接 pip install scikit-learn 即可后者需要按 CPU 或 CUDA 版本选安装命令。没有 GPU 也可以跑纯 CPU 训练这个规模的模型通常几十分钟到一个小时能收敛。提示如果工程里要求的 torch 版本和你本地已有版本差太多不要硬装最新版。先看代码里的 import 方式老代码如果用 torch.nn.init 等旧接口新版本可能已调整位置直接跑会报属性错误。3.2 数据预处理滑窗构造样本与归一化绝大多数预测程序的原始数据格式都是一行一个时间点按时间升序排列。预处理脚本做的第一件事是按时间排序并构造滑窗样本。下面这段代码对应常见的单步预测场景用过去seq_len个点预测未来第horizon个点。import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler df pd.read_csv(load_data.csv, parse_dates[time]) df df.sort_values(time).reset_index(dropTrue) # 特征列和目标列分开缩放避免目标信息提前混进特征 feature_cols [load, temp, humid, hour_sin, hour_cos] scaler_x MinMaxScaler() scaler_y MinMaxScaler() features scaler_x.fit_transform(df[feature_cols].values) target scaler_y.fit_transform(df[[load]].values) def make_windows(features, target, seq_len96, horizon1): X, y [], [] total len(features) - seq_len - horizon 1 for i in range(total): X.append(features[i: i seq_len]) y.append(target[i seq_len horizon - 1]) return np.array(X), np.array(y) X, y make_windows(features, target, seq_len96, horizon1) # 按时间顺序切分不打乱 cut int(len(X) * 0.8) X_train, X_val X[:cut], X[cut:] y_train, y_val y[:cut], y[cut:] print(X_train.shape, X_val.shape)这段代码有三处必须说清楚。第一scaler_x和scaler_y是分开拟合的scaler_y只对目标列做缩放预测完要把结果反向变换回真实量纲如果你把目标和特征放在同一个矩阵里 fit预测输出会带回不期望的数值偏移。第二滑窗切分时窗口之间存在重叠这本身不是问题因为单个窗口是独立样本重叠数据用于训练不同窗口的对应关系是合理的但切训练集和验证集时绝不能 random split必须按时间顺序否则验证集会“看到”未来。第三hour_sin和hour_cos建议由时间戳构造把 0 点到 24 点映射成周期特征这样模型不会把 23 点和 1 点当成毫无关系的两个点。3.3 构建 CNN-GRU-Attention 模型PyTorch 实现模型定义是整个程序的核心部分。下面给出一个可以直接跑通的实现关键位置都加了注释import torch import torch.nn as nn class CNNGRUAttention(nn.Module): def __init__(self, input_size, cnn_filters64, kernel_size3, gru_hidden32, output_size1, dropout0.1): super().__init__() # 一维卷积输入通道是特征维度输出通道是卷积核数量 self.conv1 nn.Conv1d(input_size, cnn_filters, kernel_size, padding(kernel_size - 1) // 2) self.relu nn.ReLU() # 池化步长2把时间长度减半降低GRU展开步数 self.pool nn.MaxPool1d(2) self.gru nn.GRU(input_sizecnn_filters, hidden_sizegru_hidden, num_layers1, batch_firstTrue, dropout0.0) # 加性注意力参数 self.W_a nn.Linear(gru_hidden, gru_hidden, biasFalse) self.v_a nn.Linear(gru_hidden, 1, biasFalse) self.dropout nn.Dropout(dropout) self.fc nn.Linear(gru_hidden * 2, output_size) def forward(self, x): # x: (batch, seq_len, input_size) x x.permute(0, 2, 1) # 转换到 (batch, channels, seq_len) x self.relu(self.conv1(x)) x self.pool(x) # (batch, cnn_filters, seq_len/2) x x.permute(0, 2, 1) # 回到 (batch, seq_len/2, cnn_filters) gru_out, _ self.gru(x) # (batch, seq_len/2, gru_hidden) # 加性注意力算每个时间步的能量分数 energy self.v_a(torch.tanh(self.W_a(gru_out))) # (batch, seq_len/2, 1) alpha torch.softmax(energy.squeeze(-1), dim1) # (batch, seq_len/2) # 加权求和得到上下文向量 context torch.bmm(alpha.unsqueeze(1), gru_out).squeeze(1) # (batch, gru_hidden) # 拼接上下文向量和最后一步隐藏状态 last_out gru_out[:, -1, :] # (batch, gru_hidden) out torch.cat([context, last_out], dim1) out self.dropout(out) return self.fc(out) # (batch, output_size)这段代码的维度变化需要逐行核对。conv1 收到的输入是 (batch, seq_len, input_size)但 Conv1d 期望通道维在第二维所以要先用permute把特征维换到 seq 前面。padding(kernel_size - 1) // 2的作用是让卷积前后序列长度不变kernel 为 3 时 padding 为 1kernel 为 5 时 padding 为 2。池化把 96 步压成 48 步GRU 的输入长度就是 48训练代价下降近一半。Attention 的W_a把每个时间步的隐藏状态映射到 gru_hidden 维度v_a再把它压成单个能量分数。softmax(dim1)是在时间步维度上求权重所以每个样本的权重之和为 1。bmm是批量矩阵乘法alpha 扩展成 (batch, 1, seq_len/2) 后和 gru_out 相乘得到 (batch, 1, gru_hidden)再 squeeze 掉中间维度。最后为什么要把上下文向量和最后一步隐藏状态拼接因为上下文向量是“全局摘要”最后一步隐藏状态是“近期状态”两者各有侧重拼接之后全连接层能同时参考全局和近期信息。这是一个在短期负荷预测里比较稳的组合方式比我一开始只拿 context 接输出效果好。3.4 训练主循环与模型保存训练主循环的模板也很固定关键在于学习率调度、梯度裁剪和早停。下面这段代码是训练脚本的核心部分import torch from torch.utils.data import DataLoader, TensorDataset X_train_t torch.tensor(X_train, dtypetorch.float32) y_train_t torch.tensor(y_train, dtypetorch.float32) dataset TensorDataset(X_train_t, y_train_t) loader DataLoader(dataset, batch_size64, shuffleTrue) model CNNGRUAttention(input_sizeX_train.shape[2]) optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5 ) loss_fn nn.MSELoss() best_val float(inf) bad_epochs 0 for epoch in range(200): model.train() for xb, yb in loader: optimizer.zero_grad() pred model(xb) loss loss_fn(pred, yb) loss.backward() # 梯度裁剪GRU在长序列上容易梯度爆炸需要加个上限 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() model.eval() with torch.no_grad(): X_val_t torch.tensor(X_val, dtypetorch.float32) y_val_t torch.tensor(y_val, dtypetorch.float32) val_pred model(X_val_t) val_loss loss_fn(val_pred, y_val_t).item() scheduler.step(val_loss) if val_loss best_val: best_val val_loss bad_epochs 0 torch.save(model.state_dict(), best_model.pt) else: bad_epochs 1 if bad_epochs 15: print(fearly stop at epoch {epoch}) break训练循环有四个值得单独说的地方。第一shuffleTrue在训练时没问题因为滑窗样本之间虽然有重叠但每个样本的输入输出对应关系独立随机顺序反而有利于优化器只要训练集和验证集按时间切分就不会引入信息泄露。第二clip_grad_norm_是保命配置GRU 在序列较长时很容易梯度爆炸尤其是负荷数据里偶尔有异常尖峰一个异常点带崩整批梯度加了这个上限后训练稳定很多。第三ReduceLROnPlateau会在验证损失连续 5 轮不下降时把学习率减半配合早停能避免最后一公里震荡。第四保存模型只保存状态字典不保存完整模型对象这样换结构时不用重新训练整个模型网络定义。这套代码跑通之后预测环节就是反向操作加载best_model.pt把测试集窗口输入得到标准化后的预测值再用scaler_y.inverse_transform还原成真实量纲。预测结果可以保存成 CSV 和真实值对比可视化后直接检查曲线形态。4. 参数怎么设从“能跑”到“跑得稳”代码本身好不好懂是一回事调到预测曲线能看是另一回事。这一章把最影响结果、也最容易被忽略的参数讲清楚。参数没有绝对最优但有合理区间和调节顺序。4.1 滑窗长度、预测步长与采样间隔的组合原则滑窗长度在时序预测里扮演的角色比很多人以为的更重要。滑窗太短模型看不到完整的周期结构滑窗太长噪声变大、样本量变少训练时间成倍增加。合理区间取决于采样间隔采样间隔一个完整日周期包含的点数推荐滑窗长度 seq_len15 分钟96 点96 到 19230 分钟48 点48 到 961 小时24 点24 到 721 天7 点周周期7 到 28这里的逻辑是滑窗至少要覆盖一个完整周期让模型有机会学到“周期段之间的相似性”。如果是 15 分钟采样seq_len 取 96 能覆盖过去一天取 192 能覆盖过去两天。我的习惯是先取一个完整周期跑通流程后再考虑要不要加长。预测步长 horizon 则和业务需求绑定做未来半小时预测horizon 取 1 到 2 个采样点做明天一天的负荷预测最好用多步滚动或序列生成而不是把 horizon 直接拉到 96 个点一次性输出。4.2 模型结构参数卷积核、隐藏单元与丢弃率的合理区间模型结构参数的调节顺序建议是先把 GRU 隐藏单元定下来再调卷积核数量最后调 dropout。理由很简单GRU 是信息吞吐的瓶颈卷积核数量只影响输入到 GRU 的特征维度调 GRU 对结果的影响最直接。参数推荐区间说明卷积核数量 cnn_filters32 到 128特征少取 32多变量场景取 64 以上卷积核大小 kernel_size3 到 7采样间隔短取 5间隔长取 3GRU 隐藏单元 gru_hidden16 到 64数据量小取 16 或 32数据上万条再考虑 64dropout0.1 到 0.3数据越多可以越小num_layers1 到 2优先 1 层效果不足再加深卷积核数量翻倍训练开销只增加线性比例作用是给 GRU 提供更多样的局部特征。但卷积核数量不是越多越好我曾经在负荷数据上把 cnn_filters 从 64 加到 256验证集 MAPE 反而上升了 0.3 个百分点原因是模型开始把噪声细节当成固定模式。GRU 隐藏单元数量同理数据量只有几千条时64 个隐藏单元经常就会出现多余的表示能力dropout 跟不上就过拟合。一个实用的判断方法训练集损失继续降、验证集损失开始回升的那一刻就是结构参数到了临界点不用追求两者同时最低。4.3 训练参数学习率、批次大小、早停与梯度裁剪训练参数里学习率是首要检查项。Adam 优化器的默认学习率 1e-3 对大多数时间序列任务适用但如果输入特征的量纲差距很大或者目标值经过标准化后方差很小1e-3 在后期会抖动明显。实际调参时我会先固定 batch size 为 32 或 64学习率从 1e-3 起步观察训练损失前 20 轮的变化如果前 20 轮损失完全不动学习率降到 3e-4 或 1e-4如果损失在某个值附近剧烈震荡学习率降到 5e-4 或 3e-4如果损失稳步下降但验证损失在后半程反弹先加 dropout而不是继续降学习率。batch size 的影响比很多人想的要小。在负荷预测这种数千到数万样本的数据集上batch 32 和 128 的最终验证误差差异通常在 0.1% 以内但 batch 越大每轮训练越快对学习率的敏感性越高。我的做法是batch 64 起步如果显存允许且训练损失下降平稳再尝试 128。早停参数建议 patience 取 10 到 15。patience 太短会在验证损失局部极小值时误停太长则白费训练时间。梯度裁剪 max_norm 设在 1.0 到 5.0 之间序列长度大于 96 时取 1.0 更稳妥序列短24 点以下时可以放宽到 5.0。4.4 损失函数MSE、MAE 与 MAPE 的取舍损失函数这个点很多 python 入门的教程不会着重讲但它在预测项目里决定了模型的“侧重点”。MSE 对离群点敏感会把训练重点放在误差大的样本上适合负荷曲线存在明显尖峰、尖峰误差不可接受的场景MAE 对所有样本误差一视同仁适合误差分布比较均匀的数据MAPE 则会把误差归一化到百分比但在负荷接近零点时MAPE 会被极小的实际值放大成巨大的百分比误差。如果在电气预测项目里只用一个损失函数我的建议是 MSE 为主用 MAPE 做验证指标。原因是 MAPE 在业务汇报时更直观但零值附近的负荷会产生误导性的大误差MSE 训练出的模型对峰值更敏感负荷预测业务中峰值时段恰恰是最需要准确性的。想两者兼顾的话可以把损失写成加权组合比如 0.7 倍的 MSE 加上 0.3 倍的加权 MAPE权重按业务对峰值的敏感度调整。这个设置在代码里就是一行损失函数定义但效果远比换模型结构明显。5. 避坑与常见问题最容易翻车的 5 个点这一章是我在这个方向上一路踩出来的记录每一条都对应一个“现象——原因——解决”的真实经历。模型跑不出来的问题八成不在模型本身而在数据环节。5.1 数据泄漏训练集 MAPE 0.8%验证集却正常得不真实现象训练集损失很低、MAPE 甚至低于 1%验证集表现也很好但模型部署到新数据上之后预测曲线明显失真。原因最常见的是数据预处理脚本在整体数据集上先调用了scaler.fit_transform再切分训练集和验证集。MinMaxScaler 的 fit 过程读了全量数据的最大值和最小值验证集的信息在训练阶段就已经被模型间接看到了。另一种泄漏是关机或节假日特征里包含未来信息比如用“未来是否放假”的标签来预测历史负荷。解决把 scaler 先对训练集单独fit再用训练集的缩放参数transform验证集和测试集特征构造时只允许使用 t 时刻之前的信息。改法就两行scaler_x.fit(X_train_raw)然后X_train scaler_x.transform(X_train_raw)、X_val scaler_x.transform(X_val_raw)。这个改动在代码级别非常简单但它是我见过影响最大的一个错误深度学习里的“后悔药”莫过于此。5.2 预测曲线“滞后”模型在抄上一时刻的值现象预测曲线整体形态和真实值一致但总是往右偏移一个或几个采样点峰值和谷值错位MAPE 却显示不高因为两条曲线的数值差距并不大。原因模型没有学到“未来走势如何形成”只学到了“下一时刻最接近当前时刻”。特别在采样间隔短15 分钟的数据里相邻点变化很小模型发现最省力的做法是输出近似当前值的预测就能拿到较低的损失。这叫“持久性陷阱”。解决在训练时加大 horizon 的权重比如训练目标改为预测未来第 3 或第 4 个点而不是第 1 个点或者评价时直接看多步滚动预测的曲线而不是只看单步误差。另一个常见做法是在特征里剔除掉“当前目标值”本身强制模型依赖外部特征温度、湿度、时段来建立预测关系。这两招方向不同但都能有效打断“抄上一时刻”的捷径。5.3 损失下降但预测值近似一条直线输出层的激活函数作祟现象训练损失正常下降但画出来的预测曲线几乎是一条水平的直线只在波动很小的范围内起伏真实曲线的尖峰完全预测不出来。原因模型最后一层被加了激活函数比如误在回归任务末尾加了 sigmoid 或 tanh。sigmoid 把输出限制在 0 到 1 之间如果标准化后的目标值分布范围本身不大模型只需要输出接近均值就能获得较低损失。另一个可能原因是池化过度序列长度从 96 压到 12时间分辨率不够细碎的波动被磨平了。解决回归任务输出层不加任何激活函数纯线性输出。检查代码里out self.fc(x)后面是不是多了torch.sigmoid之类的东西。如果输出层是对的再看池化层数量我把 MaxPool1d 从两层减到一层后尖峰预测能力立刻回来了。这类问题在代码里就是个一两行的小差异视觉影响却是模型“白训了”。5.4 验证集用随机切分线下评分高、线上准翻车现象验证集 MAPE 表现优秀但把模型放到真实业务中预测误差明显比验证结果差一个数量级。原因用train_test_split(shuffleTrue)对时间序列做划分。随机打乱把未来时间段的数据混进了训练集验证集“遇到”的时间段数据在训练时已经见过或部分见过评估结果虚高。这类错误在 python 里非常容易犯因为默认的 train_test_split 就是随机切分。解决时间序列必须按时间顺序切分先把数据集按时间排序再按 8:1:1 的比例切成训练、验证、测试。更进一步的做法是 walk-forward 验证把测试集切成多个连续时间段逐段滚动验证每一段只使用它之前的数据训练。这个方案虽然费时间但能真实反映模型在业务里的性能对负荷预测这种周期性强的数据尤其有价值。5.5 两次运行结果不一致随机种子与 CuDNN 的确定性现象同样的代码、同样的数据集两次训练出来的模型验证误差不同预测曲线也有肉眼可见的差别调参时无法判断是参数影响还是随机波动。原因框架里有多处随机源。PyTorch 初始化权重、DataLoader 打乱顺序、CuDNN 的卷积算法选择都会引入随机性。如果代码里没有任何固定种子的操作结果本来就应该不同。解决在训练脚本开头加一段固定的初始化代码。先torch.manual_seed(42)如果是 GPU 环境再设置torch.backends.cudnn.deterministic True和torch.backends.cudnn.benchmark False同时设置np.random.seed(42)。DataLoader 的shuffleTrue时可以给 DataLoader 也传一个generatortorch.Generator().manual_seed(42)。这样处理后两次运行结果基本一致这时候再调参看到的变化才能归因到参数本身。这是做实验前第一件该做的事我一开始没固定种子白白浪费了好多时间在“调参”上教训非常直接。6. 从“运行成功”到“预测可信”最后的验证方法6.1 用滚动多步验证替代单步验证很多人在做完训练、看验证集 MAPE 不错之后就认为模型达标了。实际上单步验证只评估了“用真实历史值预测下一步”而业务里往往是“用预测出的值继续预测再下一步”这两者的难度完全不同。决策路径是加载训练好的模型从测试集起点构造第一个滑窗预测下一时刻把预测值拼进滑窗末尾丢到最前面的一个点再预测再下一时刻滚动走完整段测试周期。def rolling_forecast(model, X_test, steps, scaler_y): model.eval() window X_test[0].copy() # (seq_len, features) preds [] with torch.no_grad(): for _ in range(steps): x torch.tensor(window, dtypetorch.float32).unsqueeze(0) y_norm model(x).item() preds.append(y_norm) # 用预测值替换窗口最后一个点的目标特征列 # 这里假设目标列是特征列的第0列 next_window np.roll(window, -1, axis0) next_window[-1, 0] y_norm window next_window return scaler_y.inverse_transform(np.array(preds).reshape(-1, 1))这段代码的逻辑是模拟真实业务中的持续预测而不仅仅做一步验证。np.roll把整个窗口前移一位末尾填入最新预测值下一次循环就拿到一个含预测值的完整窗口。滚动多步的误差通常比单步误差高不少但它是判断模型真实可用性的唯一可靠方式。这种滚动回测的思路和 python 量化交易策略代码里的 walk-forward 回测很相似本质都是“不用未来数据骗自己”。6.2 用电力行业习惯的指标验收模型业务汇报时预测误差一般按百分比误差 MAPE 来说话。日负荷预测的常见验收线大致是MAPE 在 3% 以内算良好5% 以内算合格超过 8% 基本不可用。实际判断时不要把单步 MAPE 当唯一依据要结合滚动多步结果和曲线形态指标计算方式验收建议MAPE预测误差绝对值除以真实值取平均短期负荷预测 3% 以下良好RMSE误差平方均值的平方根越接近 0 越好侧重峰值惩罚R2决定系数大于 0.95 说明拟合较好尖峰误差负荷最高 10% 时段内的 MAPE单独统计峰值预测持续关注我在项目里最后会做一个固定动作把真实负荷曲线和预测曲线叠加画在同一张图上专门放大看峰值时段和凌晨低谷时段。曲线形态相比数字更能暴露问题——比如滞后现象一眼就能看出来而 MAPE 反而可能被平均值掩盖。画图用的是 matplotlib代码和输出保存到本地后直接就能用于项目汇报材料。希望这些经验对你有用。这套程序我已经来回跑过很多遍最深的体会是序列预测翻车时先怀疑数据再看模型结构最后才轮到调参。数据切分顺序对了、特征不漏未来、种子固定住剩下的就是耐心调参祝顺利。本文还有配套的精品资源点击获取
网站建设高端定制企业官网