
简介这是一份基于深度学习的区域电力负荷预测模型完整项目面向机器学习或电力系统方向的课程设计、期末大作业场景适合具备一定Python基础、希望直接获取可运行高分方案的学习者。项目以Python实现涵盖数据预处理、模型构建、训练评估与可视化等关键环节压缩包内共62个文件其中39个py源码文件按功能模块组织覆盖数据加载、网络定义、训练测试等完整流程17个jpg与1个png为结果图表和结构示意图便于直观理解另有md说明文档和pyc缓存文件整体大小仅3.72MB目录结构清晰可快速定位所需内容。目前已有150人学习使用项目经导师指导获97分的高分评价下载后无需修改即可直接运行。除完整源码外还附带项目说明文档和性能展示图表能帮助使用者快速掌握设计思路与实验效果非常适合作为课程设计或期末大作业的可靠参考模板。1. 区域电力负荷预测为什么这项深度学习应用值得认真做「基于深度学习的区域电力负荷预测模型」听起来像课程作业但放到真实场景里它是电网调度、分时电价和需求响应都要依赖的前置环节。你需要在未来 15 分钟到未来几天的时间尺度上估计某个区域的用电负荷误差直接决定调度员是少开一台机组还是多储备一份备用容量。传统统计方法擅长处理平稳线性序列可负荷曲线里有天气突变、节假日、工厂排产还有早晚高峰的尖刺这些都是深度学习模型相对擅长的模式。这篇笔记就围绕怎么用 Python 把这套模型从数据清洗、特征工程一路做到多步预测和误差诊断适合想把课程项目或开源代码真正跑通、并迁移到自己数据上的工程师。2. 拿到负荷数据后先做什么清洗、特征工程与时间切分的关键步骤2.1 原始负荷数据常见的三个脏点缺测、异常尖峰与节假日标记拿到压缩包后先别急着找 model.py先把 data 目录下的 CSV 打开看前 20 行确认三件事时间戳列是不是标准格式、采样频率是 15 分钟还是 1 小时、有没有可用的温度列。国内公开负荷数据集大多是 15 分钟一个点一天 96 个点有些电网内部数据是 5 分钟或 1 小时这会直接影响后面所有滑动窗口参数。我一般会把数据频率写死在项目说明里并写一个函数校验相邻时间戳的间隔防止后面切窗口时出现重复或空洞。原始表从来不会干净。先说缺测采集终端掉线会让某些时间点变成 NaN。我一般不会直接删除因为固定频率采样的序列一旦删除数据就会留下断点滑动窗口切到断点附近时模型会取到错位的时间上下文。常见做法是前后线性插值连续缺测超过一定数量时用上周同一天同时刻的值替换。再说异常尖峰负荷曲线偶尔会出现比正常值高 30% 的毛刺这多半是上报错误而不是真实用电。我会用滚动中位数加绝对中位差识别超过阈值就替换成中位数而不是把所有高值当异常删掉。最后是节假日标记很多模型在节假日这天预测翻车是因为日历特征里只有星期几没把法定节假日和调休补班日标出来。import pandas as pd import numpy as np df pd.read_csv(regional_load.csv, parse_dates[timestamp]) df df.set_index(timestamp).sort_index() # 1) 缺测修复 df[load] df[load].interpolate(limit_directionboth) # 2) 基于滚动中位数的异常尖峰过滤 median df[load].rolling(96, centerTrue, min_periods1).median() mad (df[load] - median).abs().rolling(96, centerTrue, min_periods1).median() thresh 5 * (1.4826 * mad 1e-6) df.loc[(df[load] - median).abs() thresh, load] median # 3) 节假日标记holiday_dates 从项目说明维护的日历表读取 df[is_holiday] df.index.normalize().isin(holiday_dates).astype(int)rolling(96, centerTrue) 里的 96 对应 15 分钟一个点、一天 96 个点窗口跨一整天能识别持续较长的真实高峰又不会把一整段早高峰误判成毛刺。limit_directionboth 表示首尾缺测也插值填补避免样本被削掉。为什么用中位数而不是均值均值容易被极端值带偏中位数对单点毛刺更稳。2.2 把时间戳构建成模型特征周期编码、温度与滑动窗口模型不认识时间戳它只认识数值。第一步是把时间戳拆成小时、星期几但直接把这些整数喂给神经网络会引入一个隐藏问题23 点和 0 点之间数值差 1语义上只隔一个小时周一和周日也是同样。更好的做法是把「小时 星期几」折算成一周内的总小时数再做 sin/cos 周期编码。温度是区域负荷预测里最有效的外生变量夏季空调负荷几乎和温度呈指数关系拿到温度序列后我会对缺测做前向填充同时保留原始温度和滞后温度两个版本。def build_features(df, temp_colNone): df df.copy() week_hour df.index.dayofweek * 24 df.index.hour df[week_hour_sin] np.sin(2 * np.pi * week_hour / 168) df[week_hour_cos] np.cos(2 * np.pi * week_hour / 168) if temp_col and temp_col in df.columns: df[temp] df[temp_col].ffill() df[temp_lag1] df[temp].shift(96) # 前一天同一时刻温度 return df周期编码的意义在于让模型学到「0 点和 24 点是连续的」这个常识sin/cos 两个维度一起给模型才能同时知道小时和星期。temp_lag1 为什么取前一天同一时刻因为负荷本身有很强的 24 小时周期性加上滞后 24 小时版本的温度模型可以对比今天与昨天同时刻的温度变化捕捉天气系统过境带来的负荷爬升。做完这一步特征列包括周期编码、节假日标记、温度等原始负荷列作为预测目标单独保存不要混进特征矩阵。特征不是越多越好。温度、湿度、光照在部分区域强相关全塞进去会让模型容量浪费在冗余信息上训练时间变长泛化能力反而下降。我常用的策略是先保留周期编码、节假日、温度这三组基础特征跑一版基线再逐步加入湿度和负荷一阶差分作为候选特征用验证集 MAPE 的变化决定留不留。要注意负荷的一阶差分属于目标变换不能和目标同时进入同一根训练管线否则泄漏问题会更难排查。2.3 训练/验证/测试划分按时间顺序切分并留出一个步长间隙时序预测最忌讳随机打乱。随机切分会让模型在训练时看到未来信息验证集上分数虚高到线上预测就原形毕露。正确做法是按时间顺序切三段训练集、验证集、测试集比例大概 7:1:2 或 8:1:1。我通常在训练集和验证集之间留出一个步长间隙避免「训练窗口末尾贴着验证窗口开头」的共享状态。train_end 2023-03-31 23:45:00 val_end 2023-05-31 23:45:00 train_df df.loc[:train_end] # 注意从 train_end 后一个采样点开始留出 15 分钟间隙 val_df df.loc[train_end pd.Timedelta(minutes15):val_end] test_df df.loc[val_end pd.Timedelta(minutes15):] print(train_df.shape, val_df.shape, test_df.shape)这里每个集合都按时间顺序保留验证集起点故意和训练集终点错开 15 分钟相当于多一个 margin。如果后续把数据频率改成 60 分钟一个点这个 gap 也要跟着改成 60 分钟。切分完成后要做归一化但归一化的均值、标准差只能在训练集上计算这一点放到避坑章节展开属于最容易翻车的位置。划分完以后一个常见疑问是样本量到底够不够。假设你有三年 15 分钟数据总计约 105120 个点seq_len 取 96那么可用样本数是 105120 - 96 105024 个约 7 成给训练集大约 73000 条。对 LSTM 这种参数规模在几十万量级的模型来说这个样本量足够如果只有三个月数据样本数不到两万五此时优先考虑把 seq_len 降到 48或者干脆换成参数更少的 GRU。3. 模型选型LSTM、GRU 还是轻量 Transformer参数如何起步3.1 为什么传统预测方法在区域负荷上效果变差区域负荷序列有三个让传统方法头疼的特征强周期性、非平稳性、对天气敏感。ARIMA 这类方法假设序列经过差分后平稳但负荷会跟随气温整体爬升夏季尖峰和冬季采暖负荷让方差一直变化节假日和突发事件的脉冲又让残差显著偏离正态。机器学习方法如 XGBoost 能做非线性拟合但它默认每个样本独立要自己构造滞后特征来隐式表达时间顺序滞后阶数一多特征维度爆炸。循环神经网络的价值在于把「过去 96 个时刻的序列」整体作为输入在参数共享的前提下自动提取长期依赖。换句直白的话传统方法是在用手工特征猜明天LSTM 是在用过去一周的波形推算明天的波形。3.2 基线 LSTM 的 PyTorch 实现先从一个能跑出不错结果的基线开始。负荷预测里的 LSTM 不需要花哨最常见配置是两层 LSTM 加一个全连接输出头。hidden_size 从 128 起步num_layers 取 2dropout 取 0.2这个组合在大多数区域负荷数据上都能在一小时内看到可靠结果。import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, n_features, hidden_size128, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizen_features, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout, ) self.regressor nn.Sequential( nn.Linear(hidden_size, 64), nn.ReLU(), nn.Linear(64, 1), ) def forward(self, x): out, _ self.lstm(x) # out: (batch, seq_len, hidden) return self.regressor(out[:, -1, :]) # 取最后一个时间步为什么只取最后一个时间步的输出因为预测目标是未来一个点整个窗口的信息经过 LSTM 已被压缩到最后一步的 hidden state 里out[:, -1, :] 等价于取最后一个时刻的短期记忆再接两层全连接做非线性映射。batch_firstTrue 让输入形状变成 (batch, seq_len, n_features)对刚接手的人更直观不容易把维度搞反。dropout 只作用于层与层之间不会作用于最后一层输出所以 regressor 里不需要再加 dropout。3.3 升级方向GRU 和自注意力模块如果数据量不大或者想让训练更快我会把 LSTM 换成 GRU改动只有一行nn.LSTM 换成 nn.GRU。GRU 少了一个门控单元参数约少四分之一在负荷这种中等规模序列上精度几乎不会下降。另一个升级方向是在 LSTM 顶部加一个自注意力层让模型自己学习「窗口里的哪几个时刻对明天同一时刻最重要」比如捕捉温度突升前 12 小时的特殊模式。轻量 Transformer 也可以做但区域负荷序列长度通常只有 96自注意力对 96 步的建模优势并不明显训练配平反而更麻烦。我的建议是把 Transformer 放在第二版再试第一版用 LSTM 或 GRU 把基线和数据管线跑通。3.4 选型对比精度、训练速度与内存模型验证集 MAPE 通常范围训练速度内存占用适用场景LSTM2% – 4%中等中等数据充足追求精度GRU2% – 4.5%较快较低数据量在 2 万条以内快速验证LSTM Attention1.8% – 3.5%较慢较高特征多、需要解释关键时间步轻量 Transformer1.8% – 3.5%慢高有充足 GPU 预算的进阶实验表格里的 MAPE 是以 15 分钟为粒度的区域负荷预测日累计负荷预测的 MAPE 通常会低一半。如果你的验证集 MAPE 落在 5% 以上先别怀疑模型结构大概率是数据或特征的问题。训练速度按单张消费级 GPU 估算LSTM 在 7 万样本、seq_len96 场景下每个 epoch 大约 20 秒配上早停整体在 15 分钟内能跑完。轻量 Transformer 慢的原因主要不是参数量而是注意力矩阵计算和更小的 batch size 导致的显存瓶颈。4. 最小可复现方案项目目录、数据管线与训练循环4.1 项目目录怎么组织才能让「项目说明」读得快一份项目说明能不能让人十分钟进入状态取决于目录是否把数据、配置、代码、输出分开。我见过不少把模型定义、训练脚本和图片全堆在一个文件里的项目跑是能跑但改参数和复现结果都靠猜。常见做法是这样组织load_forecast/ ├── data/ # 原始 CSV、节假日字典 ├── configs/ # 训练参数 JSON ├── src/ │ ├── data.py # 清洗与特征工程 │ ├── dataset.py # Dataset 与 DataLoader │ ├── model.py # LSTM/GRU 定义 │ └── train.py # 训练与评估脚本 ├── checkpoints/ # 权重文件 └── outputs/ # 预测结果和图表源码与输出分离的最大好处是跑实验时不需要反复改动训练脚本改配置文件就行也不会因为误改模型代码污染上一次的结果。项目说明里如果没有目录结构拿到压缩包后先按这个思路重新整理。4.2 构造 LoadDataset 与 DataLoader窗口边界别搞错Dataset 的核心是「用连续 seq_len 个时刻的特征预测下一个时刻的负荷」。最容易犯的错是让预测目标等于窗口内最后一个值那模型等于在做「预测现在」验证集上自欺欺人。from torch.utils.data import Dataset, DataLoader class LoadDataset(Dataset): def __init__(self, features, target, seq_len96): self.features features self.target target self.seq_len seq_len def __len__(self): return len(self.features) - self.seq_len def __getitem__(self, i): x self.features[i:i self.seq_len] # 96 个历史时刻 y self.target[i self.seq_len] # 未来 1 个时刻 return x, ylen 返回的是 len(features) - seq_len而不是 len(features)。因为要保证从起点 i 取 96 个特征后i96 这个索引仍然在 target 范围内。如果把 len 写成 len(features)最后一个样本会越界训练在最后一个 epoch 报 IndexError。这里 y 是一维还是二维取决于 target 的初始形状通常我会把 target 先 reshape 成 (n_samples, 1)再在训练循环里 squeeze。4.3 训练循环与验证逻辑分开看 Train Loss 和 Val MAPE训练时监控 loss 用的是 MSE 或 Huber评估时用的是 MAPE。两者分开的原因是loss 只告诉模型梯度往哪走MAPE 才是业务关心的「平均偏差百分之几」。如果只看 train loss模型可能在绝对值大的夜间低谷上表现很好但在高峰上误差很大。import torch import torch.nn as nn def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0.0 for x, y in loader: x, y x.to(device), y.to(device) optimizer.zero_grad() pred model(x).squeeze(-1) loss criterion(pred, y) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() total_loss loss.item() * len(x) return total_loss / len(loader.dataset)clip_grad_norm_ 是循环神经网络训练的必要保护。负荷序列里偶尔会有极端样本梯度一冲hidden state 的数值范围就容易爆炸把梯度范数截断到 1.0 后训练会更平顺。criterion 我通常用 nn.SmoothL1Loss(beta1.0)也就是 Huber loss它对小误差用平方、大误差用线性不像 MSE 那样被高峰决堤式的大误差带着跑。验证逻辑不在这里展开但要记住验证时用 torch.no_grad() 包裹前向计算并在每个 epoch 结束时计算验证集 MAPE早停的触发条件是验证 MAPE 连续 10 个 epoch 不再下降。4.4 训练超参数速查表与建议起点超参数建议值调整方向seq_len96数据是 1 小时粒度时应降到 24hidden_size128数据量小降到 64数据量大升到 256num_layers2超过 3 层收益极小dropout0.2验证集 MAPE 高于 5% 可降到 0.1batch_size256OOM 时降到 64learning_rate0.001AdamW 配 cosine 退火epochs60配合 early stopping不必跑满clip_grad_norm1.0梯度爆炸时再降到 0.5这些参数的起点来自大多数区域负荷项目的共性数据量几万到十几万条特征 5 到 10 个。seq_len 取 96 是因为一天 96 个 15 分钟点模型能看到完整的一天波形如果改成 48模型只能看到 12 小时夜间和日间的衔接会差。learning_rate 用 0.001 起步训练到中段用 cosine 退火慢慢降避免后期在最优解附近震荡。5. 避坑与排查负荷预测翻车的 6 个典型问题5.1 时间泄漏验证集被训练集的归一化信息污染现象验证集 MAPE 做得很好测试集或线上预测一塌糊涂。原因很多人在整个 df 上做归一化用全局 mean/std 缩放后再切分数据集。验证集和测试集的归一化参数包含了它们自身的均值信息等于模型在验证时间接看到了未来的统计量。这是时序预测里最隐蔽的泄漏。解决只拿训练集计算 mean/std验证集和测试集复用这组参数。代码上养成习惯把归一化器单独 fit 在 train_df 上再 transform 到 val_df 和 test_df。5.2 预测曲线滞后模型只是搬了昨天的负荷现象把预测值和真实值画在一起预测曲线整体向右平移了几个点像「慢半拍」。原因负荷序列自相关极高模型发现最简单省力的做法是复制前一个时刻的观测值。如果目标构造错了比如 y 取成了窗口内最后一个值或者特征里没有足够的外生变量模型就会走这条捷径。解决先检查目标索引确认 y target[i seq_len] 而不是 target[i seq_len - 1]再检查特征里有没有温度、节假日这类能打破自相关的外生变量最后把训练目标从原始负荷改成负荷的一阶差分预测完再累加回去滞后现象通常会明显缓解。5.3 训练 Loss 很低验证 MAPE 却很难看现象train loss 一路下降到接近 0验证 MAPE 卡在 8% 不往下走。原因两个方向。一是过拟合模型把训练集的噪声也背下来了二是 MAPE 的尺度陷阱夜间低谷负荷很小绝对误差 0.1 MW 在低谷上算出的百分比误差很大模型如果牺牲低谷去保高峰MAPE 就不好看。解决先把 dropout 从 0.2 提到 0.3 看验证集变化再把验证指标改成「分段 MAPE」分别计算 0-6 点、7-9 点、18-21 点的误差定位到底哪一段在拖后腿。5.4 显存 OOMbatch_size、seq_len 与梯度累积现象训练刚开始就报 CUDA out of memory或者跑到一半被系统 kill。原因batch_size 256 乘上 seq_len 96再乘 6 个特征每批数据本身不大占显存的大头是 LSTM 的反向传播需要保留的中间状态。hidden_size 越大、num_layers 越多OOM 概率越高。解决第一个操作是把 batch_size 降到 64还不行就把 hidden_size 从 128 降到 64把 num_layers 从 2 降到 1最后的手段是梯度累积每 4 个 batch 更新一次梯度模拟 batch_size256 的统计效果。5.5 高峰总是被低估损失函数与样本权重现象MAPE 控制住了但早高峰和晚高峰的峰值预测普遍偏低 5% 左右。原因MAPE 对低负荷敏感模型如果优先压低低谷误差整体 MAPE 会下降得很快代价就是高峰期被牺牲。另一个原因是 MSE 训练出来的模型天然趋于均值回归极端峰值会被拉平。解决给损失函数加峰值权重w 1 alpha * (load 的归一化值)高峰样本的梯度更大或者改用 pinball loss 做分位数预测输出 p0.5 的中位数作为点预测。我一般先用前一种改动最小。5.6 拿到项目说明先看哪三处数据、模型定义、训练输出现象压缩包解压后文件很多不知道从哪读起跑脚本还报错。原因项目说明没有按数据、模型、输出组织或者作者默认你懂他的目录结构。解决先读 data 目录的 README 或 CSV 列头确认时间戳和特征列名再打开 model.py 看 forward 返回的是什么形状是 (batch, 1) 还是 (batch, horizon)最后看 train.py 里加载数据的方式是直接读全量 CSV 还是已经做了切分。大部分报错都出在列名不一致、时间戳没解析、归一化器没有保存这三个点上。6. 让预测真正被调度采纳多步预测策略与误差诊断6.1 直接多步预测的模型改动如果业务要的是未来 4 小时或者 24 小时的负荷曲线只输出未来一个点的模型不够用。常见做法有两种递归预测是把预测值当成下一个时刻的输入喂回模型误差会随时间累积预测到第 8 步时曲线通常会变得平滑到失真直接多步预测是把输出维度直接改成预测步长 h让模型一次输出未来 h 个时刻的值。class DirectMultiStepLSTM(nn.Module): def __init__(self, n_features, horizon24, hidden_size128, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizen_features, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout, ) self.regressor nn.Linear(hidden_size, horizon) def forward(self, x): out, _ self.lstm(x) return self.regressor(out[:, -1, :]) # (batch, horizon)训练时 target 从单个标量变成未来 24 个值。损失函数可以逐时刻独立计算 MSE也可以对不同时刻用不同权重比如越近的时刻权重越高。直接多步的好处是每一步都有独立的输出头不会累积误差缺点是 horizon 增大后模型复杂度上升预测远期曲线往往偏保守。我个人的习惯是预测未来 12 个点以内用直接多步超过 24 个点就分两段做先预测未来 24 小时再用第一段的末端值作为下一段序列窗口的输入。6.2 三个可上手的验证技巧第一把验证集的预测曲线与真实曲线画在同一张图里叠加 7 天人眼比 MAPE 更能发现问题。预测曲线如果和真实曲线形状一致但整体偏低说明模型偏差稳定可以做简单的偏差修正如果曲线明显滞后问题多半在特征或目标构造。第二按时刻统计 MAPE。把验证集所有样本按小时分组画出一天 24 个小时的 MAPE 柱状图。通常凌晨 2-5 点的 MAPE 最高早高峰 7-9 点和晚高峰 18-21 点次之午后相对平顺。如果某个高峰时段的误差异常突出就该检查这个时段是否叠加了温度突变或特殊排产。第三做误差的自相关分析。如果残差在时间上仍然呈现出明显的周期性或自相关说明模型没有把序列中的决定性信息抽干净还有可用特征没加进来。这一步不用写复杂代码pandas 的 autocorr 函数可以直接看滞后阶数。6.3 一段我踩过的坑收尾我最早做负荷预测时第一版模型直接套用了成熟的 Transformer结果验证集 MAPE 比 LSTM 还高前三个 epoch 训练 loss 甚至不下降。后来发现不是模型不行是温度特征做完归一化之后没有保留原始尺度注意力机制对特征尺度非常敏感特征缩放不一致直接影响了注意力权重的计算。之后我都先把基线 LSTM 和手工特征管线跑通再考虑上复杂结构这套顺序让很多项目少走了弯路。把数据、模型、评估三条线理顺后你会觉得负荷预测真正难的地方不在网络结构而在每一个平凡细节是否做对。希望帮到你。本文还有配套的精品资源点击获取