简介:这份资源面向电气工程、自动化及相关专业的本科生与研究生,以及从事电网调度、负荷分析的技术人员,提供一套基于MATLAB实现的BP神经网络城市电网负荷预测完整方案。包内共11个文件,包含4个m脚本文件、2个mat数据文件、1个xlsx数据表以及4张png结果图,压缩包约585KB,脚本覆盖主程序、网络训练与MSE、RMSE、MBE、MAE、R²等误差评价指标计算,数据文件与表格可直接用于训练和验证,图片则展示预测曲线与误差对比结果。代码注释较为完整,便于理解BP网络建模流程,也方便替换数据或调整网络结构进行扩展应用。目前已有174人学习下载,适合作为课程设计、毕业设计或科研入门阶段的参考案例,帮助读者快速跑通从数据加载、网络构建、训练到精度评估的全流程,并在此基础上开展创新改进。
1. 城市电网负荷预测:为什么BP神经网络依然是绕不开的基线
做城市电网负荷预测的人,迟早会撞上同一个尴尬:LSTM、Transformer、时序基础模型轮番上阵,论文指标刷得飞起,可一到调度侧的真实数据集上,最稳的往往还是那个被嫌弃"太老"的BP神经网络。这不是情怀,是数据形态决定的——城市电网负荷受温度、湿度、节假日、工作日类型、历史惯性共同驱动,特征维度不高、样本量中等、非线性强但时序依赖没那么长,这种场景下BP神经网络的拟合能力刚好够用,训练成本又低到可以在变电站边缘盒子上跑。
这篇笔记讲的就是基于BP神经网络的城市电网负荷预测,从数据长什么样、网络怎么搭、参数怎么调,到训练完怎么验证、上线后怎么排错,全部落到能直接抄的代码和参数上。适合两类人:一类是刚接手负荷预测任务、需要一周内跑出可用基线的工程师;另一类是已经上了复杂模型、想回头补一个可靠对照组的熟手。代码完整、数据齐全不是口号,下面每一段都能直接进你的工程目录。
2. 数据准备与特征工程:把城市电网负荷喂给BP之前要做的四件事
BP神经网络对输入尺度极其敏感,喂进去什么,它就学什么。城市电网负荷数据常见的形态是15分钟或1小时粒度的有功功率序列,附带气象和日历字段。直接把这些列拼成矩阵丢进网络,十有八九收敛慢、精度飘。下面按我实际项目的顺序拆开讲。
2.1 负荷数据的清洗与缺失值处理
城市电网负荷序列的缺失通常不是随机的:采集终端掉线、通信中断、检修停电都会造成成片缺失。用均值填充会把停电时段的零负荷抹平,用前向填充会在负荷爬坡段引入滞后。我一般分三步走。
import pandas as pd import numpy as np # 读取原始负荷数据,假设列名为 timestamp, load_mw, temp, humidity, is_holiday df = pd.read_csv("city_grid_load.csv", parse_dates=["timestamp"]) df = df.sort_values("timestamp").set_index("timestamp") # 第一步:标记连续缺失段,超过4个点(1小时)的段不插值,直接置NaN留待后续处理 df["is_missing"] = df["load_mw"].isna() df["miss_group"] = (df["is_missing"] != df["is_missing"].shift()).cumsum() miss_len = df.groupby("miss_group")["is_missing"].transform("sum") df.loc[(df["is_missing"]) & (miss_len > 4), "load_mw"] = np.nan # 第二步:短缺失用时间线性插值,限制最多插3个点 df["load_mw"] = df["load_mw"].interpolate(method="time", limit=3, limit_direction="both") # 第三步:剩余长缺失段用同日同时刻的周均值填充(城市负荷周周期性极强) df["hour"] = df.index.hour df["weekday"] = df.index.weekday weekly_mean = df.groupby(["weekday", "hour"])["load_mw"].transform("mean") df["load_mw"] = df["load_mw"].fillna(weekly_mean) # 兜底:仍有缺失则用全局中位数 df["load_mw"] = df["load_mw"].fillna(df["load_mw"].median())这段逻辑的关键在第一步的分组标记:miss_group用布尔值变化点做累积求和,把每一段连续缺失归到同一个组,再用transform("sum")算出段长。超过4个点的段不插值,是因为城市电网超过1小时的缺失往往对应真实停电或终端故障,插出来的值是假的,不如用周同期均值这种统计量兜底。interpolate的limit=3和limit_direction="both"保证短缺口两端都能补,不会在序列头部留空。
2.2 气象与日历特征的构造方式
城市电网负荷和温度的关系不是线性的:夏天高温制冷、冬天低温取暖,中间段温度舒适负荷平稳。直接把温度一列喂进去,BP网络要花大量容量去拟合这个U型关系。我一般显式构造几个派生特征。
# 温度分段特征:捕捉制冷/取暖两个非线性区间 df["temp_cool"] = np.maximum(df["temp"] - 26, 0) # 高于26度部分视为制冷负荷驱动 df["temp_heat"] = np.maximum(8 - df["temp"], 0) # 低于8度部分视为取暖负荷驱动 # 体感温度:温湿组合,夏季湿热地区尤其重要 df["humidex"] = df["temp"] + 0.5555 * (6.11 * np.exp(5417.7530 * (1/273.16 - 1/(273.15 + df["temp"]))) * df["humidity"]/100 - 10) # 日历特征:工作日类型、是否节假日、周末标记 df["is_weekend"] = (df["weekday"] >= 5).astype(int) df["is_holiday"] = df["is_holiday"].astype(int) df["day_type"] = df.apply(lambda r: 2 if r["is_holiday"] else (1 if r["is_weekend"] else 0), axis=1) # 周期性编码:小时和星期用sin/cos,避免BP把23点和0点当成距离很远 df["hour_sin"] = np.sin(2 * np.pi * df["hour"] / 24) df["hour_cos"] = np.cos(2 * np.pi * df["hour"] / 24) df["weekday_sin"] = np.sin(2 * np.pi * df["weekday"] / 7) df["weekday_cos"] = np.cos(2 * np.pi * df["weekday"] / 7)temp_cool和temp_heat是分段线性化的思路,把U型拆成两段单调关系,网络学起来快得多。humidex是加拿大环境部常用的体感温度公式,湿热地区夏季负荷对湿度敏感,这一列能明显降误差。周期编码用sin/cos是时序特征工程的标准操作,不这么做的话,BP网络会把23点和0点的数值差当成23个单位,而实际上它们只差1小时。
2.3 滑动窗口构造监督样本与归一化
BP神经网络是逐样本训练的,要把时间序列转成(输入特征,目标负荷)的监督格式。窗口长度的选择直接影响精度:太短抓不到日周期,太长引入冗余维度。
from sklearn.preprocessing import MinMaxScaler feature_cols = ["load_mw", "temp_cool", "temp_heat", "humidex", "day_type", "hour_sin", "hour_cos", "weekday_sin", "weekday_cos"] # 用过去24小时(96个15分钟点)预测未来1小时(4个点) LOOKBACK = 96 HORIZON = 4 data = df[feature_cols].values scaler = MinMaxScaler(feature_range=(0, 1)) data_scaled = scaler.fit_transform(data) X, y = [], [] for i in range(LOOKBACK, len(data_scaled) - HORIZON + 1): X.append(data_scaled[i-LOOKBACK:i, :].flatten()) # 展平成一维输入 y.append(data_scaled[i:i+HORIZON, 0]) # 目标只取负荷列 X = np.array(X) y = np.array(y) # 按时间顺序切分,禁止随机打乱 split = int(len(X) * 0.8) X_train, X_test = X[:split], X[split:] y_train, y_test = y[:split], y[split:]这里有两个容易翻车的点。第一,MinMaxScaler必须只在训练集上fit,然后transform测试集,否则测试集信息泄漏,指标虚高。上面为了简洁用了全量fit,实际项目里要改成先切分再fit。第二,X.append里把窗口展平成一维,是因为标准BP网络(全连接)只接受一维输入;如果你用一维卷积做前端,可以保留二维结构。LOOKBACK=96对应24小时,HORIZON=4对应未来1小时,这两个值要根据你的数据粒度调整:15分钟粒度下96是合理的,1小时粒度下24就够。
提示:归一化的范围要保存下来,预测阶段对新数据做同样的缩放,否则输入分布偏移会让模型输出完全失真。
3. BP神经网络结构设计与训练:从结构图到能收敛的代码
BP神经网络的原理不复杂——前向传播算输出,反向传播算梯度,链式法则更新权重。但落到城市电网负荷预测上,结构怎么定、激活函数选什么、学习率设多少,这些才是决定你能不能在一周内交付的关键。下面按结构图、代码实现、训练循环三块讲。
3.1 网络结构图对应的层数与神经元数怎么定
BP神经网络结构图通常画成输入层、若干隐藏层、输出层。城市电网负荷预测的输入维度是LOOKBACK × 特征数,上面例子里是96 × 9 = 864维,输出是4维。隐藏层怎么设?我的经验是两层足够,第一层宽、第二层窄。
| 层 | 神经元数 | 激活函数 | 说明 |
|---|---|---|---|
| 输入层 | 864 | 无 | 96个时间步 × 9个特征展平 |
| 隐藏层1 | 128 | ReLU | 提取非线性组合特征 |
| 隐藏层2 | 64 | ReLU | 压缩表示,降维 |
| 输出层 | 4 | 线性 | 回归任务,不加激活 |
隐藏层用ReLU而不是Sigmoid,是因为864维输入下Sigmoid容易梯度消失,训练几轮就卡住。输出层必须线性,负荷预测是回归,加Sigmoid会把输出压到(0,1),反归一化后量纲全错。神经元数128和64不是拍脑袋:输入864维,第一层压到128大约是7:1的压缩比,第二层再压到64,最后输出4,逐层递减符合信息瓶颈的思路。如果你数据量小(少于1万样本),把128降到64、64降到32,否则过拟合。
3.2 用PyTorch搭一个可训练的城市电网负荷预测网络
下面这段代码是完整可跑的,包含模型定义、损失函数、优化器。
import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset class LoadBPNet(nn.Module): def __init__(self, input_dim, output_dim): super().__init__() self.net = nn.Sequential( nn.Linear(input_dim, 128), nn.ReLU(), nn.Dropout(0.2), # 防过拟合,训练时随机丢20%神经元 nn.Linear(128, 64), nn.ReLU(), nn.Dropout(0.1), nn.Linear(64, output_dim) # 输出层线性 ) def forward(self, x): return self.net(x) input_dim = X_train.shape[1] output_dim = y_train.shape[1] model = LoadBPNet(input_dim, output_dim) criterion = nn.MSELoss() # 回归任务用均方误差 optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5) # 转成Tensor并建DataLoader train_ds = TensorDataset(torch.FloatTensor(X_train), torch.FloatTensor(y_train)) test_ds = TensorDataset(torch.FloatTensor(X_test), torch.FloatTensor(y_test)) train_loader = DataLoader(train_ds, batch_size=64, shuffle=True) test_loader = DataLoader(test_ds, batch_size=64, shuffle=False)Dropout(0.2)放在第一隐藏层后,是因为864维输入到128维的映射参数最多,最容易记住训练集噪声。第二层后Dropout(0.1)轻一些。优化器用Adam而不是SGD,是因为负荷预测的损失曲面在不同特征方向上尺度差异大,Adam的自适应学习率能省掉大量调参时间。weight_decay=1e-5是L2正则,配合Dropout双保险。batch_size=64是经验值,样本量过万时可以提到128。
3.3 训练循环、早停与学习率调度
训练循环里要加早停,否则BP网络在负荷预测任务上通常在第30到50轮开始过拟合。
from torch.optim.lr_scheduler import ReduceLROnPlateau scheduler = ReduceLROnPlateau(optimizer, mode="min", factor=0.5, patience=5) best_loss = float("inf") patience_counter = 0 EARLY_STOP_PATIENCE = 15 for epoch in range(200): model.train() train_loss = 0.0 for xb, yb in train_loader: optimizer.zero_grad() pred = model(xb) loss = criterion(pred, yb) loss.backward() optimizer.step() train_loss += loss.item() * xb.size(0) train_loss /= len(train_ds) # 验证 model.eval() val_loss = 0.0 with torch.no_grad(): for xb, yb in test_loader: pred = model(xb) val_loss += criterion(pred, yb).item() * xb.size(0) val_loss /= len(test_ds) scheduler.step(val_loss) if val_loss < best_loss: best_loss = val_loss patience_counter = 0 torch.save(model.state_dict(), "best_load_bp.pt") else: patience_counter += 1 if patience_counter >= EARLY_STOP_PATIENCE: print(f"Early stop at epoch {epoch}, best val loss {best_loss:.6f}") break if epoch % 10 == 0: print(f"Epoch {epoch}: train {train_loss:.6f}, val {val_loss:.6f}")ReduceLROnPlateau在验证损失连续5轮不降时把学习率砍半,这是处理损失平台期的标准手段。早停耐心设15轮,是因为负荷预测的验证损失有时会先降后升再降,耐心太短会错过第二个下降段。保存best_load_bp.pt而不是最后一轮的权重,是因为最后一轮往往已经过拟合。训练完加载最佳权重做预测,这是工程上的后悔药。
注意:
model.eval()和torch.no_grad()在验证阶段必须同时用,前者关Dropout,后者省显存,缺一个都会让验证损失偏大。
4. 预测结果评估与误差排查:MAPE之外还要看什么
模型训练完,输出一条预测曲线,接下来是判断它到底能不能用。很多人只看MAPE,结果上线后被调度员骂。下面讲评估指标怎么选、误差怎么拆、以及一个独立的排查章节。
4.1 负荷预测该看哪几个指标
MAPE(平均绝对百分比误差)是最常报的,但它有个致命缺陷:负荷接近零时百分比爆炸。城市电网负荷很少接近零,所以MAPE还能用,但要配合其他指标。
def evaluate(y_true, y_pred): y_true = y_true.reshape(-1) y_pred = y_pred.reshape(-1) mae = np.mean(np.abs(y_true - y_pred)) rmse = np.sqrt(np.mean((y_true - y_pred) ** 2)) mape = np.mean(np.abs((y_true - y_pred) / (y_true + 1e-6))) * 100 # 峰值误差:只取真实负荷最高的10%样本 threshold = np.percentile(y_true, 90) peak_mask = y_true >= threshold 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_test_inv = scaler.inverse_transform( np.concatenate([y_test, np.zeros((len(y_test), len(feature_cols)-1))], axis=1) )[:, :y_test.shape[1]] model.eval() with torch.no_grad(): y_pred = model(torch.FloatTensor(X_test)).numpy() y_pred_inv = scaler.inverse_transform( np.concatenate([y_pred, np.zeros((len(y_pred), len(feature_cols)-1))], axis=1) )[:, :y_pred.shape[1]] print(evaluate(y_test_inv, y_pred_inv))Peak_MAPE是我必看的指标。城市电网负荷预测的误差在峰值时段代价最高——预测偏低会导致备用容量不足,预测偏高会导致机组浪费。整体MAPE 3%但峰值MAPE 8%的模型,在调度侧是不可接受的。反归一化那段代码有点绕,是因为MinMaxScaler当初是对所有特征一起fit的,逆变换时要补齐其他列的占位,再取负荷列。
4.2 误差的时段分布与日类型分布
整体指标合格不代表每个时段都合格。把误差按小时和日类型拆开看,往往能发现系统性问题。
import matplotlib.pyplot as plt # 假设 y_test_inv 和 y_pred_inv 形状为 (样本数, 4),对应未来4个15分钟点 errors = np.abs(y_test_inv - y_pred_inv) # 取第一个预测点(未来15分钟)做分析 err_15min = errors[:, 0] # 按预测时刻的小时分组 test_timestamps = df.index[LOOKBACK + split : LOOKBACK + split + len(y_test)] err_df = pd.DataFrame({"error": err_15min, "hour": test_timestamps.hour, "day_type": df.loc[test_timestamps, "day_type"].values}) hourly_err = err_df.groupby("hour")["error"].mean() daytype_err = err_df.groupby("day_type")["error"].mean() print("按时段平均误差:\n", hourly_err) print("按日类型平均误差:\n", daytype_err)如果发现早高峰(7-9点)和晚高峰(18-20点)误差明显高于平段,说明模型对负荷爬坡段的动态响应不足,解决办法是加一阶差分特征(当前负荷减上一时刻负荷)。如果节假日误差是工作日的两倍以上,说明节假日样本太少,要么补充历史节假日数据,要么对节假日单独训练一个模型。
4.3 城市电网负荷预测的五个常见翻车点
现象一:训练损失降到很低,验证损失从第10轮开始持续上升。原因:模型容量过大或训练轮数过多,BP网络把训练集的噪声也记住了。 解决:先加Dropout和weight_decay,如果还不行就减隐藏层神经元数(128→64,64→32),再不行就减输入窗口(96→48)。
现象二:预测曲线整体比真实曲线低一截,形状对但幅值偏。原因:归一化时用了全量数据的min/max,测试集里出现了训练集没见过的更高负荷,反归一化后系统性偏低。 解决:改用训练集的min/max做归一化,或者用RobustScaler(基于中位数和四分位距),对极端值不敏感。
现象三:MAPE只有2%,但调度员说预测不可用。原因:MAPE被大量平稳时段的低误差拉低了,峰值时段误差其实很大。 解决:看Peak_MAPE,如果超过5%,在损失函数里给峰值样本加权,比如loss = MSE * (1 + 2 * (y_true > threshold))。
现象四:换了一个城市的数据,模型完全不收敛。原因:不同城市的负荷量纲差异大(有的城市峰值5000MW,有的500MW),归一化范围不匹配。 解决:每个城市单独fit scaler,或者用Z-score标准化(减均值除标准差),量纲无关。
现象五:训练时loss正常下降,但预测输出全是同一个值。原因:学习率太大,权重更新步长过大,网络输出饱和到ReLU的零区或线性层的常数区。 解决:学习率从1e-3降到1e-4,或者加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)。
5. 从离线模型到在线预测:滚动预测与模型更新的实操技巧
模型在测试集上跑通了,下一步是让它每天自动出预测。城市电网负荷预测的在线场景有两个特点:一是要滚动预测未来24小时甚至72小时,二是新数据每天都有,模型不能一劳永逸。下面讲滚动预测的实现和一个我踩过坑的模型更新策略。
5.1 多步滚动预测的两种模式
上面训练的模型是直接多步输出(一次输出未来4个点)。如果要预测未来24小时(96个点),有两种做法。
第一种是递归预测:用模型预测未来1个点,把这个预测值当作历史输入的一部分,再预测下一个点。代码简单但误差会累积,预测到第96个点时可能已经偏得离谱。
第二种是直接多步:训练时输出维度就设成96,一次输出全天。这要求模型容量更大,但误差不累积。我一般用直接多步,因为城市电网负荷的日周期强,模型能学到全天形状。
# 直接多步:修改输出维度为96(未来24小时,15分钟粒度) HORIZON_LONG = 96 y_long = [] for i in range(LOOKBACK, len(data_scaled) - HORIZON_LONG + 1): y_long.append(data_scaled[i:i+HORIZON_LONG, 0]) y_long = np.array(y_long) # 模型输出层改为96维,其余结构不变 model_long = LoadBPNet(input_dim=X_train.shape[1], output_dim=HORIZON_LONG)输出维度从4变96,参数量增加不多(最后一层从64×4变成64×96),但训练样本数会减少(因为需要更长的未来窗口),要确保样本量足够。如果样本少于5000,建议还是用递归预测,但每预测12个点就用真实值校正一次输入。
5.2 模型更新的触发条件与增量训练
城市电网负荷有季节漂移:夏天制冷负荷占比高,冬天取暖负荷占比高,一个在夏天数据上训练的模型,到冬天MAPE可能翻倍。我的做法是设两个触发条件。
| 触发条件 | 阈值 | 动作 |
|---|---|---|
| 滚动7天MAPE | 超过基线1.5倍 | 触发增量训练 |
| 季节切换 | 连续3天平均温度跨过15度 | 触发全量重训 |
| 新增重要负荷 | 变电站扩容或大用户接入 | 人工确认后重训 |
增量训练不是从零开始,而是加载已有权重,用最近30天数据以更小的学习率(1e-4)微调5到10轮。这样既适应新数据,又不会把之前学到的通用模式忘掉。
# 增量训练:加载已有模型,小学习率微调 model.load_state_dict(torch.load("best_load_bp.pt")) optimizer_ft = torch.optim.Adam(model.parameters(), lr=1e-4) # 用最近30天数据构造训练集(代码同2.3,略) for epoch in range(10): model.train() for xb, yb in recent_loader: optimizer_ft.zero_grad() loss = criterion(model(xb), yb) loss.backward() optimizer_ft.step() torch.save(model.state_dict(), "best_load_bp_updated.pt")学习率从1e-3降到1e-4,是因为模型已经在旧数据上收敛,大步长会破坏已有权重。微调轮数控制在10轮以内,多了会过拟合到最近30天的局部模式。
5.3 一个被忽略的技巧:预测区间的估计
调度侧不仅想知道预测值,还想知道这个预测有多不确定。BP神经网络本身不给置信区间,但可以用Dropout在推理时打开(Monte Carlo Dropout)来近似。
def predict_with_uncertainty(model, x, n_samples=50): model.train() # 保持Dropout开启 preds = [] with torch.no_grad(): for _ in range(n_samples): preds.append(model(x).numpy()) preds = np.stack(preds) # (n_samples, batch, horizon) mean = preds.mean(axis=0) std = preds.std(axis=0) return mean, std mean, std = predict_with_uncertainty(model, torch.FloatTensor(X_test[:100])) # std 越大表示模型对该样本越不确定,可作为调度备用的参考model.train()在推理时保持Dropout开启,每次前向传播随机丢不同神经元,50次采样得到预测分布。std大的时段通常是负荷拐点或节假日,调度侧可以对这些时段多留备用。这个技巧不增加训练成本,只在推理时多跑几十次前向,对城市电网负荷预测这种实时性要求不极端的场景完全可行。
我自己的习惯是:每次模型上线前,先用MC Dropout跑一遍测试集,把std最高的50个样本挑出来人工看一眼。如果这些样本集中在节假日或极端天气,说明模型知道自己不确定,可以放心用;如果std高的样本毫无规律,说明模型还没学好,得回去查数据。这个习惯帮我省过好几次线上事故。希望帮到你。
本文还有配套的精品资源,点击获取