ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

省间现货购电需求预测:CNN-BiGRU-Attention超短期实战

省间现货购电需求预测:CNN-BiGRU-Attention超短期实战 简介这份资源是发表于《电力系统及其自动化学报》的网络首发论文面向电力市场分析师、电力行业研究人员及购电策略制定人员聚焦省间现货市场超短期购电需求预测这一实际问题。论文提出融合卷积神经网络、双向门控循环单元与注意力机制的组合预测模型先以Lasso系数法筛选购电需求影响因素再由CNN提取时间序列局部特征BiGRU捕捉长期依赖关系注意力机制聚焦关键时间步以提升精度并基于实测数据验证其准确性优于单一模型及其他组合模型。资源包为1个PDF文件约930KB完整收录论文正文涵盖模型构建过程、数据处理方法与仿真实验结果可直接用于研读技术路线、复现建模思路或作为电力需求预测研究的参考文献。目前已有65人学习适合希望了解深度学习在电力现货市场预测中应用的中高级读者。1. 省间现货购电需求预测为什么超短期窗口让传统时序模型集体翻车省间现货市场的购电需求预测跟省内中长期交易完全是两码事。省内中长期按日、按周滚动曲线平滑趋势项占主导随便一个ARIMA或者简单LSTM都能把MAPE压到可接受范围。但省间现货的结算颗粒度是15分钟交易窗口短、价格信号驱动强、跨区联络线约束频繁触发需求曲线在超短期尺度上呈现出极强的非平稳性和突变特征——上一刻还在爬坡下一刻因为对端省份新能源大发导致通道阻塞购电需求直接跳水。这就是「超短期」三个字的杀伤力。预测步长一旦压到未来1到4小时即4到16个15分钟点传统时序模型的两个致命短板就暴露了第一RNN类模型在长序列上的梯度消失问题导致它记不住几小时前的关键拐点第二单向结构只能利用历史信息无法捕捉需求序列中「未来上下文对当前决策的修正」——比如日前计划已经排定的购电曲线对实时需求有明确的前向约束。CNN-BiGRU加注意力机制这套组合恰好是冲着这两个短板去的。CNN做局部特征提取把15分钟粒度的原始负荷序列里的高频波动先卷成特征图BiGRU双向跑前向抓历史趋势、后向抓未来约束注意力机制在BiGRU输出上做加权让模型自己决定哪些时间步对当前预测更重要。这套架构在超短期光伏功率预测、共享单车需求预测等场景已经被反复验证过迁移到省间现货购电需求上逻辑是通的。这篇文章面向的是已经有一定深度学习基础、想把这套架构落到实际购电预测业务里的算法工程师和电力交易从业者。我会从数据构造讲到模型实现再到训练调参和上线排查把能复现的细节都摊开。新手可以跟着代码走一遍熟手可以直接看参数设置和避坑部分。2. 从15分钟粒度原始数据到模型输入CNN-BiGRU的工程化拆解2.1 为什么是CNN-BiGRU而不是Transformer或纯LSTM先说选型逻辑。省间现货购电需求序列的典型长度是96点/天超短期预测通常取过去24到48小时的数据作为输入窗口也就是96到192个时间步。这个长度区间很尴尬纯LSTM在超过100步后门控机制对早期信息的保留能力急剧下降你调参调到死它该忘的还是忘。Transformer倒是能全局建模但自注意力机制在几百步序列上计算量是O(n²)而且电力负荷序列的局部连续性很强全局注意力反而容易过拟合噪声。CNN-BiGRU的折中方案是这样的CNN先做局部感受野的特征压缩把192步的原始序列卷成48步的特征序列序列长度直接砍到四分之一BiGRU再在这个压缩后的序列上跑双向建模计算量可控信息保留也够。注意力机制加在BiGRU输出层本质是一个软对齐操作让模型在解码每个预测点时动态决定回看哪些历史时刻。多头注意力机制在这里不是必须的。我试过4头和8头在省间现货数据上跟单头比MAPE差异在0.3%以内但训练时间翻了近一倍。除非你的输入特征维度很高比如同时融合了气象、新能源出力、联络线计划等几十维特征否则单头注意力够用。2.2 数据预处理从原始购电曲线到归一化张量省间现货的原始数据一般来自交易系统的出清结果字段包括时间戳、购电需求值MW、成交价格、联络线计划值等。第一步是构造监督学习样本用过去N步预测未来M步。import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler def create_sequences(data, input_len, pred_len): 构造监督学习序列 data: 归一化后的二维数组 (总时间步, 特征数) input_len: 输入窗口长度如19248小时 pred_len: 预测窗口长度如164小时 X, y [], [] for i in range(len(data) - input_len - pred_len 1): X.append(data[i : i input_len, :]) # 只预测购电需求这一列假设是第0列 y.append(data[i input_len : i input_len pred_len, 0]) return np.array(X), np.array(y) # 读取原始数据 df pd.read_csv(province_spot_demand.csv, parse_dates[timestamp]) df df.sort_values(timestamp).reset_index(dropTrue) # 缺失值处理省间现货数据常见通道阻塞导致的短时缺失 df[demand] df[demand].interpolate(methodlinear, limit8) # 最多补2小时 df df.dropna() # 归一化对购电需求单独做MinMax其他特征按列处理 scaler MinMaxScaler(feature_range(0, 1)) features [demand, price, tie_line_plan] df[features] scaler.fit_transform(df[features]) input_len 192 # 过去48小时 pred_len 16 # 未来4小时 X, y create_sequences(df[features].values, input_len, pred_len) print(f样本数: {X.shape[0]}, 输入形状: {X.shape[1:]}, 输出形状: {y.shape[1:]})这段代码的关键参数是input_len和pred_len。我一般会先用相关性分析确定输入窗口计算历史各滞后阶数与预测目标的最大互信息取互信息衰减到峰值10%以内的滞后步数作为input_len的下界。省间现货场景下这个值通常在144到240之间对应36到60小时。pred_len取决于你的交易申报周期如果是4小时滚动申报就设16。缺失值处理有个坑省间现货的缺失往往不是随机的而是通道阻塞或市场暂停导致的。线性插值最多补8个点2小时超过这个长度必须标记为异常段并考虑剔除否则模型会学到错误的连续性假设。2.3 CNN-BiGRU-Attention模型实现模型结构分三层CNN特征提取层、BiGRU时序建模层、注意力加权输出层。import torch import torch.nn as nn import torch.nn.functional as F class CNNBiGRUAttention(nn.Module): def __init__(self, input_dim, cnn_channels64, kernel_size3, gru_hidden128, num_layers2, pred_len16, dropout0.2): super().__init__() # CNN层一维卷积提取局部特征 self.conv1 nn.Conv1d(in_channelsinput_dim, out_channelscnn_channels, kernel_sizekernel_size, paddingkernel_size // 2) self.bn1 nn.BatchNorm1d(cnn_channels) self.pool nn.MaxPool1d(kernel_size2) # 序列长度减半 # BiGRU层 self.gru nn.GRU(input_sizecnn_channels, hidden_sizegru_hidden, num_layersnum_layers, batch_firstTrue, bidirectionalTrue, dropoutdropout if num_layers 1 else 0) # 注意力层对BiGRU输出做加权 self.attn_weights nn.Linear(gru_hidden * 2, 1) # 输出层 self.fc nn.Sequential( nn.Linear(gru_hidden * 2, 64), nn.ReLU(), nn.Dropout(dropout), nn.Linear(64, pred_len) ) def forward(self, x): # x: (batch, input_len, input_dim) x x.permute(0, 2, 1) # - (batch, input_dim, input_len) x F.relu(self.bn1(self.conv1(x))) x self.pool(x) # 序列长度减半 x x.permute(0, 2, 1) # - (batch, seq_len//2, cnn_channels) # BiGRU gru_out, _ self.gru(x) # (batch, seq_len//2, gru_hidden*2) # 注意力加权 attn_score self.attn_weights(gru_out) # (batch, seq_len//2, 1) attn_weight F.softmax(attn_score, dim1) context torch.sum(gru_out * attn_weight, dim1) # (batch, gru_hidden*2) # 输出预测 out self.fc(context) # (batch, pred_len) return out逐层说明。CNN部分kernel_size3对应45分钟的局部感受野15分钟×3padding1保持序列长度不变MaxPool1d(2)把192步压到96步。这里有个细节——池化会丢失相位信息如果你的购电需求曲线对时间偏移敏感比如峰谷切换时刻可以把池化改成步长为2的卷积。BiGRU部分gru_hidden128双向拼接后输出维度256。num_layers2时加dropout防止过拟合。注意batch_firstTrue输入格式是(batch, seq, feature)。注意力部分用一个线性层把256维映射到1维打分softmax归一化后对BiGRU输出做加权求和。这就是时序注意力机制的核心——让模型自己学出哪些时间步重要。我试过加多头效果不明显前面说过了。输出层两层全连接中间加ReLU和Dropout最后输出16个预测点。2.4 训练配置与损失函数选择from torch.utils.data import DataLoader, TensorDataset from sklearn.model_selection import train_test_split # 划分数据集按时间顺序切不能随机打乱 X_train, X_val, y_train, y_val train_test_split(X, y, test_size0.2, shuffleFalse) train_loader DataLoader(TensorDataset(torch.FloatTensor(X_train), torch.FloatTensor(y_train)), batch_size64, shuffleTrue) val_loader DataLoader(TensorDataset(torch.FloatTensor(X_val), torch.FloatTensor(y_val)), batch_size64, shuffleFalse) device torch.device(cuda if torch.cuda.is_available() else cpu) model CNNBiGRUAttention(input_dimlen(features), pred_lenpred_len).to(device) # 损失函数MSE 峰谷加权 class WeightedMSELoss(nn.Module): def __init__(self, peak_weight2.0): super().__init__() self.peak_weight peak_weight def forward(self, pred, target): # 对高需求时段加权峰谷差大的场景下防止模型偏向均值 weights torch.where(target target.mean(), self.peak_weight, 1.0) loss weights * (pred - target) ** 2 return loss.mean() criterion WeightedMSELoss(peak_weight2.0) optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-5) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience5, factor0.5) # 训练循环 best_val_loss float(inf) for epoch in range(100): model.train() train_loss 0 for batch_x, batch_y in train_loader: batch_x, batch_y batch_x.to(device), batch_y.to(device) optimizer.zero_grad() pred model(batch_x) loss criterion(pred, batch_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 with torch.no_grad(): for batch_x, batch_y in val_loader: batch_x, batch_y batch_x.to(device), batch_y.to(device) pred model(batch_x) val_loss criterion(pred, batch_y).item() scheduler.step(val_loss) if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), best_model.pth) print(fEpoch {epoch1}, Train Loss: {train_loss/len(train_loader):.6f}, Val Loss: {val_loss/len(val_loader):.6f})几个关键决策。损失函数用加权MSE而不是纯MSE省间现货购电需求的峰谷差可能达到3到5倍纯MSE会让模型偏向预测均值高峰时段欠预测严重。peak_weight2.0是我在多个省份数据上试出来的经验值太高会导致低谷时段预测偏高。梯度裁剪max_norm1.0是必须的。BiGRU在序列较长时容易出现梯度爆炸不加裁剪训练loss会突然飞掉。学习率用ReduceLROnPlateaupatience5连续5个epoch验证loss不降就砍半。数据划分必须按时间顺序切不能随机打乱。随机打乱会导致验证集里混入训练集未来时段的信息评估结果虚高上线后直接翻车。3. 注意力权重的可视化验证与特征工程补强3.1 注意力权重到底学到了什么模型训完之后第一件事不是看MAPE而是把注意力权重抽出来看。注意力机制如果学不到有意义的时间模式那它就是个摆设。def extract_attention_weights(model, sample_x): 提取注意力权重用于可视化 model.eval() with torch.no_grad(): x sample_x.unsqueeze(0).to(device) x x.permute(0, 2, 1) x F.relu(model.bn1(model.conv1(x))) x model.pool(x) x x.permute(0, 2, 1) gru_out, _ model.gru(x) attn_score model.attn_weights(gru_out) attn_weight F.softmax(attn_score, dim1) return attn_weight.squeeze().cpu().numpy() # 取一个样本看权重分布 weights extract_attention_weights(model, torch.FloatTensor(X_val[0])) # weights形状: (96,) 对应池化后的96个时间步正常的注意力权重应该呈现「近高远低」的衰减模式但在峰谷切换点附近会有局部峰值。如果权重是一条平线说明注意力层没学到东西可能是学习率太低或者注意力层初始化有问题。如果权重全部集中在最后几个时间步说明模型退化成了简单的「取最近值」BiGRU的双向建模能力没发挥出来。我一般会把注意力权重和原始购电曲线叠在一起画图。如果权重峰值对应的是历史同时段的峰谷切换点说明模型确实学到了日周期模式。如果峰值对应的是价格突变点说明模型捕捉到了价格对购电需求的驱动关系。3.2 特征工程除了购电需求本身还要喂什么只用历史购电需求做预测MAPE大概在3%到5%。加上以下几类特征后可以压到2%以内特征类别具体字段作用注意事项价格信号日前出清价、实时出清价价格驱动购电行为做差分处理消除趋势联络线计划跨区联络线计划值物理约束注意计划值与实际值的偏差新能源出力风电、光伏预测出力影响供需平衡用预测值而非实际值避免未来信息泄露时间编码小时sin/cos、星期one-hot周期模式sin/cos编码比one-hot更紧凑滞后特征昨日同时段需求、上周同时段需求日周期和周周期注意节假日偏移时间编码用sin/cos而不是one-hot是因为96个时间步的one-hot太稀疏CNN的卷积核在稀疏输入上提取不到有效局部模式。sin/cos编码把时间映射到连续空间卷积核能捕捉到时间的平滑过渡。滞后特征要小心。昨日同时段需求在正常工作日的相关性很高但遇到节假日就完全失效。我的做法是加一个「是否节假日」的布尔特征让模型自己学节假日和正常工作日的差异。3.3 多步预测策略直接多步 vs 滚动单步超短期预测要输出未来16个点有两种策略。直接多步是模型一次性输出16个值滚动单步是每次预测1个点然后喂回去。直接多步的误差不会累积但模型要同时学16个不同步长的映射关系难度大。滚动单步的误差会逐步累积16步之后可能偏得离谱。我选的是直接多步但在输出层做了分段处理前4步用一个全连接头后12步用另一个。前4步1小时对精度要求最高单独优化后12步允许更大的误差。这个技巧在省间现货场景下能把1小时内的MAPE再降0.5个百分点。4. 避坑与排查省间现货预测里那些血泪教训4.1 验证集MAPE很低但上线后偏差巨大现象离线验证MAPE 1.8%上线跑了一周实际偏差超过8%。原因数据泄露。最常见的是归一化时用了全量数据的min/max验证集的信息泄露到了训练阶段。另一个隐蔽原因是特征里混入了未来信息比如用了实际新能源出力而不是预测出力。解决归一化的scaler只能在训练集上fit然后transform验证集和测试集。所有特征必须确认在预测时刻是已知的。我一般会做一个「时间穿越检查」把每个特征的时间戳和预测目标的时间戳对比确保特征时间戳严格早于预测目标时间戳。4.2 注意力权重全部塌缩到最后一个时间步现象训练loss正常下降但注意力权重可视化后发现softmax输出几乎全部分配给了最后一个时间步。原因BiGRU的最后一层隐藏状态已经包含了全序列信息注意力层如果初始化不好会直接「偷懒」只关注最后一步。另一个原因是学习率太大注意力层的参数在初期就被推到了极端值。解决把注意力层的初始化改成小方差初始化学习率单独设小一点比如主网络的十分之一。或者在注意力打分前加一个温度系数训练初期温度高softmax更平滑后期逐步降低。4.3 峰谷切换时段预测严重滞后现象在早高峰和晚高峰的起始点模型预测值比实际值滞后2到3个时间步30到45分钟。原因CNN的池化操作引入了相位延迟MaxPool1d在压缩序列时会把峰值位置往后推。另外MSE损失对滞后误差的惩罚不够敏感。解决把MaxPool1d换成步长为2的平均池化或者直接用步长为2的卷积做下采样。损失函数里加一个一阶差分惩罚项让模型对变化率敏感def diff_penalty_loss(pred, target, alpha0.1): mse F.mse_loss(pred, target) # 一阶差分惩罚 pred_diff pred[:, 1:] - pred[:, :-1] target_diff target[:, 1:] - target[:, :-1] diff_loss F.mse_loss(pred_diff, target_diff) return mse alpha * diff_lossalpha0.1是经验值太大会导致预测曲线过于抖动。4.4 省间通道阻塞导致的需求突变学不到现象通道阻塞发生时购电需求会在15分钟内突变20%以上模型完全跟不上。原因训练数据里通道阻塞的样本太少模型没见过这种模式。另外通道阻塞是离散事件连续模型天然不擅长处理。解决把通道阻塞状态作为一个离散特征喂进去0正常/1阻塞并且在损失函数里对阻塞时段的样本加权。如果阻塞样本实在太少可以用SMOTE做少数类过采样但要注意时序数据的过采样不能简单插值得用时间序列特定的增强方法。4.5 GPU显存溢出但batch size已经调到很小现象batch size降到16还是OOM但模型参数量看起来不大。原因BiGRU的中间状态占用显存跟序列长度成正比。192步输入经过CNN池化后是96步BiGRU隐藏层128维双向中间状态是96×256×2双向×batch size。如果num_layers2还要再乘2。解决把输入序列长度从192降到144或者把CNN的池化窗口从2改成3序列压到64步。另一个办法是用梯度累积batch size设8累积4次等效于32。5. 让模型在省间现货场景真正能用的三个进阶技巧5.1 用在线学习对抗概念漂移省间现货市场的规则和参与者行为在持续变化模型上线三个月后精度通常会下降1到2个百分点。定期全量重训成本太高我一般用在线学习做增量更新。具体做法是维护一个滑动窗口的训练缓冲区每天把新来的实际数据加进去同时丢弃最旧的数据。每周末用缓冲区数据做一次微调学习率设得很小1e-5只更新输出层和注意力层的参数CNN和BiGRU层冻结。这样既能适应新模式又不会把之前学到的通用特征忘掉。# 在线微调只更新注意力和输出层 for name, param in model.named_parameters(): if attn in name or fc in name: param.requires_grad True else: param.requires_grad False optimizer torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr1e-5) # 用最近7天数据微调10个epoch5.2 预测区间估计而不只是点预测交易员做申报决策时不只需要一个预测值还需要知道这个预测的不确定性有多大。我在模型输出层加了一个分位数回归头同时输出P10、P50、P90三个分位数。class QuantileHead(nn.Module): def __init__(self, input_dim, pred_len, quantiles[0.1, 0.5, 0.9]): super().__init__() self.quantiles quantiles self.heads nn.ModuleList([ nn.Linear(input_dim, pred_len) for _ in quantiles ]) def forward(self, x): return [head(x) for head in self.heads] # 分位数损失 def quantile_loss(preds, target, quantiles): loss 0 for pred, q in zip(preds, quantiles): error target - pred loss torch.max((q - 1) * error, q * error).mean() return lossP10和P90之间的区间宽度就是模型对当前预测的置信度。区间宽的时候交易员可以保守申报区间窄的时候可以激进一些。5.3 注意力权重作为异常检测信号注意力权重除了做可视化验证还能当异常检测器用。正常运行时注意力权重的分布是稳定的当市场出现异常事件比如某条通道突然阻塞、某省新能源出力骤降注意力权重的分布会发生显著偏移。我一般会计算每天注意力权重的均值和方差跟过去30天的基线做对比。如果KL散度超过阈值就触发告警提示交易员当前市场状态可能偏离模型训练分布预测结果需要谨慎使用。这个技巧帮我避免过好几次因为市场规则调整导致的预测翻车。这套方案从数据预处理到模型上线我前后迭代了大概四个月。最大的教训是不要一上来就堆模型复杂度先把数据质量和特征工程做扎实。注意力机制不是万能药它只能在你喂进去的特征确实包含有效信息时才能发挥作用。如果历史购电需求本身就是噪声主导再复杂的模型也学不出规律。希望帮到你。本文还有配套的精品资源点击获取
返回列表