ARTICLE DETAIL

资讯详情

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

基于BP神经网络的城市电网负荷预测:从数据清洗到在线部署的完整实战

基于BP神经网络的城市电网负荷预测:从数据清洗到在线部署的完整实战 简介这份资源面向电气工程、自动化及相关专业的本科生与研究生以及从事电网调度、负荷分析的技术人员提供一套基于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(methodtime, limit3, limit_directionboth) # 第三步剩余长缺失段用同日同时刻的周均值填充城市负荷周周期性极强 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的limit3和limit_directionboth保证短缺口两端都能补不会在序列头部留空。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), axis1) # 周期性编码小时和星期用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:iHORIZON, 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网络全连接只接受一维输入如果你用一维卷积做前端可以保留二维结构。LOOKBACK96对应24小时HORIZON4对应未来1小时这两个值要根据你的数据粒度调整15分钟粒度下96是合理的1小时粒度下24就够。提示归一化的范围要保存下来预测阶段对新数据做同样的缩放否则输入分布偏移会让模型输出完全失真。3. BP神经网络结构设计与训练从结构图到能收敛的代码BP神经网络的原理不复杂——前向传播算输出反向传播算梯度链式法则更新权重。但落到城市电网负荷预测上结构怎么定、激活函数选什么、学习率设多少这些才是决定你能不能在一周内交付的关键。下面按结构图、代码实现、训练循环三块讲。3.1 网络结构图对应的层数与神经元数怎么定BP神经网络结构图通常画成输入层、若干隐藏层、输出层。城市电网负荷预测的输入维度是LOOKBACK × 特征数上面例子里是96 × 9 864维输出是4维。隐藏层怎么设我的经验是两层足够第一层宽、第二层窄。层神经元数激活函数说明输入层864无96个时间步 × 9个特征展平隐藏层1128ReLU提取非线性组合特征隐藏层264ReLU压缩表示降维输出层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(), lr1e-3, weight_decay1e-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_size64, shuffleTrue) test_loader DataLoader(test_ds, batch_size64, shuffleFalse)Dropout(0.2)放在第一隐藏层后是因为864维输入到128维的映射参数最多最容易记住训练集噪声。第二层后Dropout(0.1)轻一些。优化器用Adam而不是SGD是因为负荷预测的损失曲面在不同特征方向上尺度差异大Adam的自适应学习率能省掉大量调参时间。weight_decay1e-5是L2正则配合Dropout双保险。batch_size64是经验值样本量过万时可以提到128。3.3 训练循环、早停与学习率调度训练循环里要加早停否则BP网络在负荷预测任务上通常在第30到50轮开始过拟合。from torch.optim.lr_scheduler import ReduceLROnPlateau scheduler ReduceLROnPlateau(optimizer, modemin, factor0.5, patience5) 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(fEarly stop at epoch {epoch}, best val loss {best_loss:.6f}) break if epoch % 10 0: print(fEpoch {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))], axis1) )[:, :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))], axis1) )[:, :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→6464→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_norm1.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:iHORIZON_LONG, 0]) y_long np.array(y_long) # 模型输出层改为96维其余结构不变 model_long LoadBPNet(input_dimX_train.shape[1], output_dimHORIZON_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(), lr1e-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_samples50): 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(axis0) std preds.std(axis0) 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高的样本毫无规律说明模型还没学好得回去查数据。这个习惯帮我省过好几次线上事故。希望帮到你。本文还有配套的精品资源点击获取
返回列表