电网负荷预测实战:BP与LSTM的对比与选型
2026/9/17 10:43:20 网站建设 项目流程

简介:面向电网负荷预测任务,这套资料以 MATLAB 工程形式提供反向传播神经网络与长短期记忆网络两种模型的完整实现,适合电力调度、负荷预测研究与算法对比学习。包体共 4 个文件,包含两个 MATLAB 脚本、一个已训练模型文件和一个澳大利亚电力负荷与价格预测数据表,分别用于 BP 模型训练、LSTM 主流程运行、模型结果保存与外部数据导入。数据表涵盖日期、时间、负荷值等字段,可直接切分时间序列并构造训练样本。整体大小约 9.73MB,已有 653 人学习下载。通过运行代码,读者能直观对比两类网络在负荷时间序列上的拟合差异,掌握网络结构搭建、训练参数设置以及均方误差、平均绝对误差、决定系数等评价指标的计算方法,并借助现成数据快速复现预测流程,迁移到相似负荷场景,适合作为课设复现与算法选型参考。

1. 电网负荷预测不是"跑个模型"那么简单

电网负荷曲线看着有规律——早晚高峰、季节性起伏、工作日和周末差异——但真正落地的预测系统里,很少有人直接拿原始负荷去拟合。原因在于负荷数据是非平稳的:既有以天、周为周期的强周期性,又有气温、湿度、节假日带来的突变。气象耦合越强,非线性越明显,传统线性回归和ARIMA在拐点处往往追不上。这两年行业内最常见的技术路线,是从传统BP神经网络切到LSTM,原因也很直白:BP擅长做高维非线性映射,但没有时序记忆能力;LSTM在结构上自带"记忆门控",能把前几小时甚至前几天的负荷状态传递下来。对做电力调度、售电交易、微网能量管理系统的工程师来说,这两者不是替代关系,而是不同数据条件和业务需求下的两个答案。本文从数据构造、模型实现、参数调优讲到验证技巧,把两条路线各自怎么落地、互相怎么取舍,完整拆开讲一遍。

2. BP神经网络做电网负荷预测的最小实现

2.1 为什么BP在这个任务里还没退休

BP神经网络(反向传播网络)在电网负荷预测里已经用了二十多年,至今在中短期预测场景仍有工程价值。它的理论基础是通用逼近定理:一个包含足够多隐藏层神经元的BP网络,能以任意精度逼近紧致集上的连续函数。电网负荷预测本质上就是一个回归问题——给定历史负荷、气象、日期等特征,拟合出一个连续的负荷值。BP在处理这类问题时收敛稳定、工程实现简单、推理速度快,尤其适合特征工程做得比较扎实的场景。

那为什么都说BP"不够用"?关键不在结构,而在输入构造方式。BP没有时间维度上的隐状态,你要把时间信息塞给网络,只能通过滑动窗口构造特征。窗口开得太大,特征维度升高、数据稀疏;开得太小,又抓不住负荷的日内周期。这是BP做时序预测的天然短板,但换个角度看,只要窗口构造合理,BP在新数据上往往比复杂模型更抗过拟合。

2.2 特征工程:决定BP上限的不是网络层数

负荷预测的特征设计,行业内有个基本共识——特征是天花板,模型是逼近手段。以预测次日96点负荷(15分钟一个采样点)为例,我会按下面几个维度构造输入特征:

def build_features(df, lag_hours=[1, 2, 24, 48, 168]): """构造BP网络的输入特征 df: DataFrame, 必须包含load列和timestamp列 lag_hours: 滞后阶数, 按小时计算 """ for h in lag_hours: df[f'lag_{h}h'] = df['load'].shift(h * 4) # 15分钟一个点,4个点为1小时 # 时间特征 df['hour'] = df['timestamp'].dt.hour + df['timestamp'].dt.minute / 60 df['dayofweek'] = df['timestamp'].dt.dayofweek df['is_weekend'] = (df['dayofweek'] >= 5).astype(int) # 气象特征 df['temp'] = df['temperature'] # 气温是负荷最强相关变量 df['humidity'] = df['humidity'] # 去掉NaN行(滞后构造产生的空值) df = df.dropna().reset_index(drop=True) feature_cols = [f'lag_{h}h' for h in lag_hours] + ['hour', 'dayofweek', 'is_weekend', 'temp', 'humidity'] return df[feature_cols].values, df['load'].values

这里有几个参数值得细说。lag_hours里为什么24、48、168是必选项:24小时对应日周期同相位点,48小时捕捉跨日趋势,168小时对应周周期同星期几的负荷水平。短期滞后1到2小时,则让模型能跟上负荷的快速波动。这个组合能覆盖负荷序列的主要时间尺度。

用滑动窗口构造特征后,切记对特征和标签分别做归一化。最常见的做法是Z-score标准化,但注意一个关键细节:标准化参数只能从训练集统计,不能放到全量数据上算,否则会把未来信息泄漏进训练过程。

2.3 用PyTorch手写BP训练脚本

用PyTorch写BP不需要太复杂的封装,核心就是nn.Module定义网络和训练循环两个部分。下面给一个可以直接改用的最小实现:

import torch import torch.nn as nn import numpy as np from torch.utils.data import TensorDataset, DataLoader class BPForecaster(nn.Module): """三层BP网络:输入层 -> 32节点隐藏层 -> 32节点隐藏层 -> 输出层""" def __init__(self, input_dim, hidden_dim=32, output_dim=1): super().__init__() self.fc1 = nn.Linear(input_dim, hidden_dim) self.fc2 = nn.Linear(hidden_dim, hidden_dim) self.fc3 = nn.Linear(hidden_dim, output_dim) self.act = nn.Tanh() # Tanh比ReLU收敛更平稳, 适合回归任务 def forward(self, x): x = self.act(self.fc1(x)) x = self.act(self.fc2(x)) return self.fc3(x).squeeze(-1) def train_bp(model, X_train, y_train, epochs=50, batch_size=256, lr=1e-3): """训练BP模型 X_train/y_train: 标准化后的numpy数组 """ dataset = TensorDataset(torch.FloatTensor(X_train), torch.FloatTensor(y_train)) loader = DataLoader(dataset, batch_size=batch_size, shuffle=True) optimizer = torch.optim.Adam(model.parameters(), lr=lr) criterion = nn.MSELoss() for epoch in range(epochs): model.train() epoch_loss = 0.0 for xb, yb in loader: optimizer.zero_grad() pred = model(xb) loss = criterion(pred, yb) loss.backward() optimizer.step() epoch_loss += loss.item() * len(xb) if (epoch + 1) % 10 == 0: print(f"Epoch {epoch+1}/{epochs}, Loss: {epoch_loss/len(X_train):.6f}") return model

代码里有几个对初学者不友好的点需要解释。batch_first=True在LSTM里会出现,BP网络的输入维度是(batch, features)Dense层自动处理。shuffle=True对BP回归有利有弊——随机打乱能让梯度估计更稳定,但如果数据的时间顺序本身携带有效信息(比如负荷有连续上升趋势),打乱会破坏分布一致性。我训练BP做负荷预测时,会把shuffle关掉,保持时间顺序让每个batch内的数据分布更接近真实滚动预测时的样子。

2.4 三个必调的BP参数:学习率、隐藏层宽度、早停

BP神经网络的训练稳定性几乎全部取决于三个参数的配合。学习率先固定1e-3,用Adam优化器跑前10个epoch观察loss曲线,如果震荡剧烈降到1e-4,如果收敛过慢升到3e-3。隐藏层宽度方面,输入特征在8到12个维度时,32个神经元的隐藏层是合理的起点——太宽容易把训练集背下来,太窄拟合不动多峰值负荷曲线。早停至关重要,见过太多人训练50个epoch后模型明显过拟合还继续跑。

# 早停逻辑: 验证集loss连续5个epoch不下降就停止 best_loss = float('inf') patience = 5 trigger = 0 for epoch in range(epochs): train_loss = train_one_epoch(model, train_loader) val_loss = evaluate(model, val_loader) if val_loss < best_loss: best_loss = val_loss trigger = 0 torch.save(model.state_dict(), 'best_bp.pt') else: trigger += 1 if trigger >= patience: print(f"Early stop at epoch {epoch}") model.load_state_dict(torch.load('best_bp.pt')) break

一个容易忽略但很影响预测结果的参数是损失函数权重。电网负荷预测中,峰时段的绝对误差通常被评价体系加权得更高——调度最关注的是高峰负荷是否可以拉闸。如果不想在损失函数层面做改动,至少要在验证集上按峰/谷时段分组计算误差,不要只看全天的MAPE。

3. 把LSTM接进负荷预测:从网络结构到训练细节

3.1 BP到LSTM的建模思维转变

从BP切到LSTM,核心变化不是把nn.Linear换成nn.LSTM就完成任务——而是样本的组织方式从根本上变了。BP把每个时间点的数据看成独立样本,LSTM把(t-k, ..., t-1, t)的序列看成一条样本,预测t+1时刻的负荷。这意味着LSTM的输入张量多出一个时间步维度,shape从(batch, features)变成(batch, seq_len, features)

这个改变带来的直接影响是:模型不必再依赖手工构造的滞后特征来感知时间顺序,LSTM内部的隐状态会自动编码过去几个时间步的负荷变化趋势。工程上一个直接的收益是,你不必再花大量时间搜索滞后阶数的组合了。代价是需要更多训练数据——LSTM时序模式的学习需要大量序列样本,一般来说,至少要有一整年的15分钟级负荷数据才谈得上训练一个靠得住的LSTM。

3.2 LSTM的门控机制在负荷预测里怎么理解

LSTM的核心设计是门控——遗忘门、输入门、输出门。很多教程爱用解剖图讲解,这里换个角度,站在负荷预测的语义上去理解。

遗忘门决定"上一时刻的负荷状态有多少要保留下来"。如果昨天下午2点负荷是900MW,今天下午2点的负荷大概率也在900MW附近波动,遗忘门会保留这个状态;但如果今天是节假日,负荷模式完全变了,遗忘门应该把昨天的信息清掉,让模型从最近的输入重新积累上下文。这就是LSTM比BP更能应对节假日负荷突变的机制原因。

输入门决定"当前时刻的新信息有多少写入细胞状态"。负荷曲线中的尖峰突变(比如气温骤降导致取暖负荷猛增),需要通过输入门快速引入到长期状态中。

输出门决定"当前细胞状态暴露给输出层多少"。这部分控制模型输出与隐状态的解耦程度,让训练过程更稳定。

三者在训练中自适应调整,不需要工程师手工指定哪些日期特征更重要,这是LSTM在时间序列任务上最强的地方。

3.3 PyTorch实现LSTM负荷预测:数据构造和模型定义

先明确一下LSTM样本的构造方式。以15分钟粒度数据为例,用seq_len=96做输入序列——刚好是一天的采样点数,预测未来第1个点的负荷。这是"多对一"的预测模式,也是最常用的单点滚动预测结构:

def build_sequences(data, seq_len=96, pred_len=1): """构造LSTM训练序列 data: 一维负荷数组(已标准化) seq_len: 回看窗口长度, 96 = 24小时 pred_len: 预测步长, 1 = 预测下一个15分钟点 """ X, y = [], [] for i in range(len(data) - seq_len - pred_len + 1): X.append(data[i:i+seq_len]) y.append(data[i+seq_len:i+seq_len+pred_len]) return np.array(X), np.array(y)

模型定义注意两个关键参数——num_layersbatch_first:

class LSTMForecaster(nn.Module): """LSTM负荷预测模型 input_size: 输入特征数, 如果只用负荷就是1 hidden_size: 隐状态维度 num_layers: LSTM堆叠层数 """ def __init__(self, input_size=1, hidden_size=64, num_layers=2, output_size=1): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True ) self.reg = nn.Linear(hidden_size, output_size) self.tanh = nn.Tanh() def forward(self, x): # x: (batch, seq_len, input_size) out, (h_n, c_n) = self.lstm(x) # 取最后一层最后一个时间步的隐状态 last_hidden = out[:, -1, :] # (batch, hidden_size) output = self.reg(self.tanh(last_hidden)) return output.squeeze(-1)

batch_first=True让输入输出都遵循(batch, seq_len, features)的结构,可读性更高,代码里不容易搞混维度。out[:, -1, :]取的是序列最后一个时间步的隐状态,这是"多对一"预测的标准取法。h_n, c_n在单步预测场景用不上,但在做多步预测时,可以把它们作为下一段序列的初始状态传入,维持上下文连续性,这一步是LSTM提升多步预测稳定性的常用技巧。

3.4 LSTM训练中的参数表和调整策略

LSTM的超参数比BP多一个数量级,下面这个参数表是可以直接照抄的起点配置,已经去掉了一些失效的默认值:

参数推荐初始值调节方向
seq_len96(24小时)天气敏感负荷可加时长到192
hidden_size64数据量大到数百万点可升128
num_layers2层数超过3在中小数据上必过拟合
learning_rate1e-3训练集过大时尝试1e-4配合更多epoch
batch_size64序列越长batch要越小,防显存溢出
clip_grad_norm0.5防止梯度爆炸,LSTM必加
dropout0.2(仅2层以上生效)负荷数据噪声大时提高至0.3

梯度裁剪是LSTM训练里最容易漏掉的一步。RNN的误差反向传播是按时间步展开的,梯度范数会随时间步数增长,典型表现是loss在某个epoch突然跳到NaN。加一行torch.nn.utils.clip_grad_norm_(model.parameters(), 0.5)能挡掉大部分训练发散。

# LSTM训练中的关键配置: 梯度裁剪 optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=15, gamma=0.5) for epoch in range(epochs): for xb, yb in train_loader: optimizer.zero_grad() pred = model(xb) loss = criterion(pred, yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 0.5) # 关键 optimizer.step() scheduler.step()

学习率调度器StepLR每15个epoch把学习率减半,这样模型先大步搜索,后小步逼近最优区域。对于负荷预测这类数据量大但特征相对简单的问题,这个策略比固定学习率更容易在测试集上拿到更低的误差。

4. 两个模型怎么选:数据量与预测步长的权衡

4.1 从训练成本到推理延迟

BP和LSTM的选择不是"哪个准就上哪个",而是数据条件、算力条件与业务实时性共同决定的。从训练成本看,BP训练速度大约比LSTM快一个数量级——没有时间步展开,梯度传播路径短,收敛快。在CPU上训练BP做负荷预测,百万级样本大约几分钟到十几分钟,LSTM同样数据量则要按小时计。

从推理延迟看,单次前向推理BP在微秒级,LSTM在毫秒级。对实时调度场景,这个差异无所谓;对需要做几千个场景的蒙特卡洛模拟(比如光伏渗透率不确定性的概率负荷预测),推理速度就会成为瓶颈。我的经验是:预测点数少于1000,且不需要滚动多步预测时,BP完全够用;需要预测未来24小时甚至更久,且每步以历史预测值为输入滚动下去时,LSTM因为内部状态能保持连续性,通常压过BP。

4.2 数据量分界:多少样本才值得用LSTM

根据我自己在不同项目上的经验,可以画一条粗略的分界线:

数据规模推荐模型理由
不足3个月的15分钟数据优先BPLSTM序列数据量不足,容易过拟合
3个月-1年的15分钟数据两个都跑用验证集对比,LSTM未必赢
1年以上15分钟数据优先LSTMLSTM的时序建模优势被数据量激活
大量历史但特征单一两个都跑特征信息量决定上限,模型差异变小

这个分界线的底层逻辑是:LSTM的参数空间比BP大很多(隐状态、门控权重、输出层),数据量小时容易记下训练样本的时序噪声,泛化能力反而不如结构简单的BP。数据量足够多时,LSTM才能学到"负荷变化模式"这种抽象规律,而不是死背序列。

4.3 损失函数和评估指标得按电网业务的逻辑选

负荷预测行业内常用MAPE(平均绝对百分比误差)作为主指标,但MAPE有个天然缺陷——负荷接近零点时误差会无限放大。我见过多个项目因MAPE在夜间时段偏高,把整个模型的评估拖到不合格线以下,但考核方实际关心的是峰时段的预测精度。更稳妥的做法是多指标并行评估:RMSE衡量大误差的惩罚力度,MAE衡量平均误差水平,再单独算峰时段(比如9:00-11:00和18:00-20:00)的MAPE。

def evaluate_forecast(y_true, y_pred, peak_hours=[9, 10, 18, 19]): """按电网负荷的业务特征分时段评估 """ from sklearn.metrics import mean_absolute_error, mean_squared_error mae = mean_absolute_error(y_true, y_pred) rmse = np.sqrt(mean_squared_error(y_true, y_pred)) mape = np.mean(np.abs((y_true - y_pred) / (y_true + 1e-6))) * 100 # 按小时拆分, 只算峰时段 # y_true.index = 时间戳, 按hour筛选 peak_mask = y_true.index.hour.isin(peak_hours) peak_mape = np.mean(np.abs((y_true[peak_mask] - y_pred[peak_mask]) / (y_true[peak_mask] + 1e-6))) * 100 return {'MAE': mae, 'RMSE': rmse, 'MAPE': mape, 'Peak_MAPE': peak_mape}

y_true + 1e-6防止除零是工程标配,但更重要的是peak_mask这个思路——把评价口径拆开,才能知道模型到底是在峰时段拉胯还是谷时段拉胯。很多调参方向性错误,就是因为只看一个综合MAPE,模型在夜间拉低平均值掩盖了白天峰值预测的偏差。

5. 事后验证模型靠不靠谱的三个技巧

5.1 时序交叉验证,别用K-Fold

电网负荷数据有严格的时间顺序。很多刚上手LSTM的工程师习惯顺手打train_test_split(random_state=42),这在负荷预测里是致命的——随机拆分会让训练集里出现"未来"的数据点,模型实际上偷看到了答案,验证集误差失真。正确的做法是时序交叉验证:按时间顺序切分,训练集只包含验证集之前的数据。

def time_series_split(data, n_splits=5): """按时间顺序切分训练/验证集 data: 按时间升序排列的完整数据 n_splits: 切分段数 """ size = len(data) // n_splits for i in range(n_splits - 1): train_end = (i + 1) * size train, val = data[:train_end], data[train_end:train_end + size] yield train, val

这种切分方式能让模型每轮都只用"过去"预测"未来",更贴近实际部署时的工作状态。最后一折的结果最有说服力,因为它用的训练数据量最大,模拟了模型持续累积数据后的表现。

5.2 残差自相关分析——发现LSTM没有学到的东西

做负荷预测的模型评估,除了误差数值还要看残差结构。残差应该像是纯随机噪声,如果残差还有明显的时间相关(比如凌晨时段系统性偏高),说明模型遗漏了某些周期性因素。计算残差的自相关系数(lag=24小时)是一个快速检查手段。

# 检查残差在24小时滞后上是否仍有显著自相关 residuals = y_true - y_pred lag24_corr = np.corrcoef(residuals[96:], residuals[:-96])[0, 1] # |corr| > 0.1 说明模型没有学到完整的日周期

如果lag24_corr明显偏离零,常见处理是在特征或序列长度上加强日周期信息。对BP,检查lag_24h特征是否意外没进入模型;对LSTM,尝试把seq_len从96拉长到192或288,让网络看到至少两个完整日周期。这个技巧通常能把MAPE再压低0.5到1个百分点。

5.3 多步滚动预测的误差累积测试

单步预测的评估与实际业务存在一个落差:真正运行时,往往需要预测未来若干个15分钟点。滚动预测的本质是把上一步的预测值作为下一步的输入,误差会逐点累积。用第一步预测的MAPE来评价系统性能是乐观的,要测试模型在滚动模式下的真实表现:

def rolling_predict(model, initial_seq, steps=96): """从initial_seq开始滚动预测96个点(24小时) model: 已训练好的LSTM模型 initial_seq: 长度为seq_len的初始序列, 已标准化 """ model.eval() seq = initial_seq.copy() preds = [] with torch.no_grad(): for _ in range(steps): # 每次预测一个点, 然后拼接到序列尾部,丢弃最早的1个点 x = torch.FloatTensor(seq[-96:]).reshape(1, 96, 1) p = model(x).item() preds.append(p) seq = np.append(seq, p) # 预测值回流 return np.array(preds)

这段代码里最关键的是seq = np.append(seq, p)——预测值回流导致的误差累积是滚动预测的常态,也是和"一步预测"最大的差异。实测中滚动96步的MAPE通常比单步高2到3倍,如果模型跟不上这个增长速度,问题大概率出在训练数据覆盖的负荷模式不够多样,而不是调参没到位。用这个方法对不同季节的数据分别测试,就能判断模型在极端天气下是否还有可靠性。

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

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

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

立即咨询