
简介面向电力系统研究、调度及数据分析人员提供一份可直接运行的LSTM电力负荷预测Python源码。压缩包共350个文件大小11.36MB包含170个csv数据文件、47个模型meta与48个checkpoint文件、14个Python脚本及多个训练过程图表可用于完成从历史负荷数据清洗、归一化、训练集/测试集划分到LSTM模型搭建、训练和MAPE评估的完整流程。源码覆盖TensorFlow/Keras模型构建、断点保存与迭代调参并输出不同训练步数对应的MAPE结果便于对比精度与收敛情况。已有83人学习下载适合具备一定Python和深度学习基础、希望快速搭建时序预测模型的读者参考。1. 为什么电力负荷预测偏偏用LSTM先回答一个反直觉结论拿到一批历史负荷数据人人都想用 LSTM 一步登天把预测准确率怼到 99%。但真正做过这个项目的人都知道翻车往往不在模型而在数据被喂进模型之前、以及预测结果被解读之后。电力负荷预测的核心难点不是「深度学习」四个字而是把一个有强周期、强噪声、且会被节假日与极端天气突然打断的时间序列变成 LSTM 能稳定吃透的样本序列。这篇源码级笔记围绕「基于 LSTM 的电力负荷预测 python 源码」这个选题把从 CSV 原始表到 MAPE 评估的完整链路拆开适合两类人一类是刚接触时间序列预测、想用 python 跑通一个能交付的 LSTM 模型的工程师另一类是电力行业的数据分析手上有历史负荷但不确定 LSTM 到底该怎么接、参数设多少、为什么测试集好看、上线就崩。2. 选型逻辑为什么电力负荷这类序列数据归 LSTM 管2.1 负荷数据的三条硬约束决定了 MLP 和 ARIMA 谁来都不好使电力负荷数据不是一张普通的营销表它有三条硬约束。第一条是强周期一天之内有早高峰和晚高峰一周之内工作日和周末完全是两种曲线一年之内还有冬夏两个负荷峰值带。第二条是强时序依赖今天上午十点的负荷和昨天上午十点的负荷高度相关和三个月前随机某个上午十点的负荷几乎无关这意味着「顺序」本身就是信息。第三条是突变一场寒潮、一次全省范围的强对流天气负荷曲线会在两三个小时内整体平移一个台阶。这三条约束直接淘汰了两类常见选手。普通 MLP 把每个时间点当成独立样本丢掉了顺序关系它学不到「昨天同刻的负荷是今天最好的参考系」这件事。ARIMA 这类线性模型对单周期还能对付一旦周期叠加、同时还要处理周末效应和节假日毛刺人工差分和季节分解的工作量比调模型大得多而且线性假设在负荷尖峰时段经常失真。LSTM 的门控结构天然为这种「带记忆的序列建模」设计这也是为什么 lstm 时间序列预测 python 生态里负荷预测几乎成了 LSTM 的默认演示场景。2.2 LSTM 的门控机制梯度能走「高速公路」才是关键很多人把 LSTM 当黑匣子用以为它只是「比 RNN 强一点的循环网络」。真正决定它适合负荷预测的是门控机制解决了一个非常具体的工程问题梯度消失。普通 RNN 在反向传播时梯度要沿着时间步连乘同一个权重矩阵序列稍微长一点梯度就指数级缩小到 0模型根本学不到三十步之前的信息。而 LSTM 的细胞状态cell state是一条允许梯度直通的「高速公路」遗忘门只做乘法门控梯度可以在几十甚至上百个时间步里接近无损地传回去。落到负荷场景里看三个门的直觉。遗忘门决定「昨天的负荷、昨天的温度该忘掉多少」如果今天是突然降温模型需要弱化昨天高温对应的状态输入门决定「刚刚发生的突变要不要写进记忆」比如一次设备跳闸导致的小时级负荷骤降输出门决定「当前记忆有多少暴露给输出层去凑出预测值」。这三件事对应到数据上就是日周期记忆、突变响应、当前时刻的修正恰好是负荷预测的三个核心动作。2.3 LSTM 的适用边界数据量、序列长度、预测步数LSTM 不是万能钥匙「lstm 模型代码」也不是贴上去就能跑出好结果的。根据我做过负荷预测项目的经验有三类情况不建议硬上 LSTM。第一类是数据量少于 5000 条这个量级下带滞后特征的线性回归或 XGBoost 往往更快更稳LSTM 的参数空间太大容易过拟合到噪声上。第二类是预测步数特别长比如要做未来 720 步15 分钟粒度下是 7 天半的预测单 LSTM 递归预测误差会滚雪球这时应该考虑序列到序列结构。第三类是你只有纯负荷序列、没有温度节假日等外部特征LSTM 的收益会明显打折因为负荷的突变部分单靠历史序列是猜不出来的。反过来说当你手上有一年以上、分钟级或小时级的历史负荷数据预测目标是未来几小时到次日全天LSTM 就是这个区间里性价比最高的方案。PyTorch 的实现足够灵活改输入维度、加深网络、接多输出头都不需要重写框架这也是相比 Prophet 这类封装好的工具我仍然愿意用 LSTM 的原因——边界可控出了问题你知道去哪改。3. 数据准备从 CSV 到 LSTM 能吃的样本顺序错一步全盘皆输3.1 预处理三步走重采样、缺失值、毛刺剔除负荷数据的原始 CSV 通常长得不太规整常见问题有时间戳不连续、凌晨数据缺失、偶发传感器毛刺。第一步先把时间列解析成索引并按固定频率重采样。电力行业最常见的是 15 分钟一个点一天 96 个点这个口径在调度侧是标准格式我建议直接按这个粒度对齐。import pandas as pd import numpy as np df pd.read_csv(load.csv, parse_dates[time], index_coltime) # 15分钟粒度是电力负荷预测的常见口径一天96个点 df df[load].resample(15min).mean() # 缺失值短于2小时8个点用线性插值更长的缺口用前向填充兜底 df df.interpolate(limit8, methodlinear).fillna(methodffill) # 毛刺剔除与滚动中位数偏离超过3倍滚动标准差的位置替换为中位数 rolling_median df.rolling(window96, centerTrue).median() rolling_std df.rolling(window96, centerTrue).std() mask (df - rolling_median).abs() 3 * rolling_std df[mask] rolling_median[mask]这段代码里有两个参数值得说。interpolate(limit8)的 limit 限制的是最多连续填补几个缺失点8 个对应两小时超过两小时的缺口说明数据质量已经不可信用ffill兜底只是保证序列不中断后续要在评估时把这几个区域标出来单独观察。毛刺剔除用中位数而不是均值是因为均值对极端值本身敏感中位数在负荷曲线上更稳。窗口window96取的是「以当前点为中心、前后各 48 个点」正好覆盖一整天的周期这样判断毛刺时参考的是同一天前后几个小时的正常形态。3.2 归一化与先切分为什么必须用训练集的统计量LSTM 对输入尺度敏感负荷值跨度从凌晨的几百 MW 到高峰的几千 MW不归一化会让 loss 被大数值样本主导训练震荡。MinMax 归一化把数据压到 [0,1] 区间是负荷预测里最常见的做法。from sklearn.preprocessing import MinMaxScaler # 顺序先按时间切分再fit训练集的scaler测试集和验证集只transform train_size int(len(df) * 0.7) val_size int(len(df) * 0.15) train df[:train_size] val df[train_size:train_size val_size] test df[train_size val_size:] scaler MinMaxScaler() train_scaled scaler.fit_transform(train.values.reshape(-1, 1)) val_scaled scaler.transform(val.values.reshape(-1, 1)) test_scaled scaler.transform(test.values.reshape(-1, 1))注意这里必须是先切分、再 fit。如果先对全量数据算 min 和 max再切分测试集的统计信息就已经混进了 scaler 里训练时模型相当于提前「知道」了测试集的数值范围。这种泄漏不会立刻报错但会让测试集指标好到离谱上线之后原形毕露。判断自己有没有踩这个坑有一个简单的自检训练、验证、测试三段的 min 值和 max 值如果几乎一致说明你多半用了全局归一化数据的范围上下限已经被未来信息污染了。提示scaler 的 fit 永远只放在训练集上执行。验证集和测试集只能被动调用 transform没有第二个合法写法。3.3 滑动窗口构造数据集lookback、horizon 怎么设归一化之后要把一维序列切成「窗口 标签」的监督学习样本。窗口长度 lookback 决定模型每次看多长的历史horizon 决定预测未来几个点。这两个参数直接由数据粒度和业务需求决定不是拍脑袋。def make_sequences(data, lookback96, horizon1): X, y [], [] for i in range(len(data) - lookback - horizon 1): X.append(data[i:i lookback, 0]) # 历史窗口长度lookback y.append(data[i lookback:i lookback horizon, 0]) # 未来horizon个点 return np.array(X), np.array(y) X_train, y_train make_sequences(train_scaled, lookback96, horizon1) X_val, y_val make_sequences(val_scaled, lookback96, horizon1) X_test, y_test make_sequences(test_scaled, lookback96, horizon1) print(X_train.shape, y_train.shape) # 比如 (批次, 96, 1) 和 (批次, 1)lookback96 在 15 分钟粒度下就是整整 24 小时这是负荷预测最常用的窗口长度理由很直接昨天同一时刻的负荷是今天最重要的参考。如果数据是小时粒度一天的周期只需要 lookback24照搬 96 会让每个样本覆盖四天训练数据量反而被稀释。horizon1 是单步预测适合先跑通链路、确认模型没问题真正业务上要预测未来几小时或全天建议先读完第 6 章再决定 horizon 的取值。滑窗构造有一个副作用相邻样本之间高度重叠。比如第 i 个样本和第 i1 个样本共享 95/96 的时间点。因此在训练时随机打乱顺序没问题但验证集和测试集绝不能和训练集混在一起打乱必须按时间顺序切分。否则验证集里会出现「和训练集样本共享同一段历史窗口」的情况评估成绩相当于开卷考试。4. PyTorch 实现LSTM 模型源码与训练参数一手调优4.1 模型定义nn.LSTM 各参数在负荷场景的真实含义PyTorch 里搭一个 LSTM 负荷预测模型核心只有两段代码一个nn.LSTM层加一个全连接输出层。工程上通常把工程组织成model.py、train.py、evaluate.py三个文件模型定义独立放在一个文件里方便复用和改参。import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size, hidden_size, num_layers, horizon, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, # dropout只在多层LSTM之间生效单层时传dropout会告警 dropoutdropout if num_layers 1 else 0 ) self.fc nn.Linear(hidden_size, horizon) def forward(self, x): out, _ self.lstm(x) # out形状: (batch, seq_len, hidden_size) return self.fc(out[:, -1, :]) # 取最后一个时间步的隐藏状态逐参数说含义。input_size是每个时间步的特征数如果只喂纯负荷序列就是 1加了温度、节假日标记就改成对应维度。hidden_size是隐状态维度64 起步数据量上五万样本时可以试 128但收益递减这个参数是负荷预测里典型的「调大了不一定好、调小了一定差」的玄学区间。num_layers2是实用区间一层表达能力不够三层以上在中小数据集上几乎必过拟合。horizon是预测步数单步预测填 1多步预测填目标步数全连接层直接输出这么多个点。batch_firstTrue这个参数很容易忽略。PyTorch 的 LSTM 默认输入形状是(seq_len, batch, features)如果不设 batch_first后面构造数据、写训练循环时每处都要跟着调转轴容易出 bug。建议统一设成 True让输入形状和 numpy 构造出来的(batch, seq_len, features)保持一致。out[:, -1, :]取出最后一个时间步的隐状态作为整个序列的汇总表示再过一个线性层映射到预测值这是单步预测最标准的接法。4.2 训练循环lr、batch、梯度裁剪、ReduceLROnPlateau 的配合模型定义好之后训练环节是踩坑重灾区。LSTM 的 loss 曲线不会像教科书里画得那么平滑lr 设大了直接 NaN设小了跑一百个 epoch 还在原地踏步。我一般用 Adam 初始 lr1e-3 验证集监控早停的组合。from torch.utils.data import TensorDataset, DataLoader model LSTMPredictor(input_size1, hidden_size64, num_layers2, horizon1) optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.MSELoss() # 验证loss连续5轮不降学习率减半连续15轮不降停止训练 scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor0.5, patience5) dataset TensorDataset(torch.FloatTensor(X_train), torch.FloatTensor(y_train)) loader DataLoader(dataset, batch_size64, shuffleTrue) # shuffle只在训练集上做 best_val_loss float(inf) patience 0 for epoch in range(100): model.train() epoch_loss 0 for X_batch, y_batch in loader: optimizer.zero_grad() pred model(X_batch) loss criterion(pred, y_batch) loss.backward() # 梯度裁剪是LSTM训练的必要保护防止梯度爆炸 nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() epoch_loss loss.item() model.eval() with torch.no_grad(): val_pred model(torch.FloatTensor(X_val)) val_loss criterion(val_pred, torch.FloatTensor(y_val)).item() scheduler.step(val_loss) if val_loss best_val_loss: best_val_loss val_loss patience 0 # checkpoint里不只是模型权重scaler和lookback也要一起存 torch.save({ model: model.state_dict(), scaler: scaler, lookback: 96, hidden_size: 64, num_layers: 2 }, best.pt) else: patience 1 if patience 15: print(fearly stop at epoch {epoch}) break这里几个参数配合起来才有意义。clip_grad_norm_(max_norm1.0)把梯度的全局范数裁到 1.0防止梯度爆炸这是 LSTM 训练里最后悔没早点加的一行代码——不加它你可能在某个 epoch 突然看到 loss 变成 NaN然后花一下午调 lr。ReduceLROnPlateau的 patience5 表示连续 5 轮验证 loss 不降就减半学习率早停的 patience15 表示减半后仍不回升才停两个 patience 不能设成一样大否则模型还没等来 lr 生效就被停了。batch_size64在 96 长度的序列下是比较稳妥的选择显存不够就降到 32太小小于 16会让梯度估计噪声过大loss 曲线像心电图。4.3 加载 checkpoint 做预测模型、scaler、lookback 一个都不能少训练完的模型不是拿到生产环境就能用的因为预测时输入必须走和训练时完全相同的预处理链路。很多工程里模型部署后预测结果全偏原因就是加载模型时丢了 scaler 或者 lookback 记错了。ckpt torch.load(best.pt) model LSTMPredictor( input_size1, hidden_sizeckpt[hidden_size], num_layersckpt[num_layers], horizon1 ) model.load_state_dict(ckpt[model]) model.eval() # 取测试集最后一段窗口做一次完整预测 last_window torch.FloatTensor(X_test[-1:]) # 形状 (1, 96, 1) with torch.no_grad(): pred_scaled model(last_window).item() # 反归一化时注意pred_scaled是单个数值要reshape成(1,1)再还原 pred_value ckpt[scaler].inverse_transform([[pred_scaled]])[0][0]torch.save时把 scaler、lookback、超参一起塞进 dict是省钱的做法。否则你训练完三个月再回来加载模型很可能忘了当初用的 lookback 是 96 还是 72这会直接导致预测结果对不上号。反归一化时inverse_transform要求输入形状是(样本数, 特征数)单个预测值必须包成[[pred_scaled]]这是新手最容易报错的地方报错信息往往还是隐晦的形状不匹配。5. 评估与避坑测试集 MAPE 3%为什么上线第一周就翻车5.1 指标怎么算才不算骗自己反归一化后再算 MAPE评估指标必须在反归一化之后的原始负荷尺度上计算而不是在 [0,1] 的归一化尺度上算完再报数。归一化尺度的误差是相对量和业务理解的 MW 对不上号而且 MinMax 之后小负荷值对应的误差会被放大造成指标看起来很差、实际还行或者反过来。from sklearn.metrics import mean_absolute_error, mean_squared_error def inverse_scale(arr, scaler): orig_shape arr.shape # 统一reshape成(-1,1)逐元素还原再reshape回原形状 inv scaler.inverse_transform(arr.reshape(-1, 1)) return inv.reshape(orig_shape) pred_inverse inverse_scale(pred.numpy(), scaler) y_test_inverse inverse_scale(y_test, scaler) mae mean_absolute_error(y_test_inverse, pred_inverse) rmse np.sqrt(mean_squared_error(y_test_inverse, pred_inverse)) mape np.mean(np.abs((y_test_inverse - pred_inverse) / y_test_inverse)) * 100 print(fMAE{mae:.2f} MW, RMSE{rmse:.2f} MW, MAPE{mape:.2f}%)三个指标各有偏向。MAE 是平均绝对误差单位一致最直观RMSE 对大的偏差取平方会放大极端时段的失败适合用来盯「有没有某几个点错得离谱」MAPE 是百分比误差方便跨项目对比但它在低负荷时段会爆炸——凌晨两点负荷只有几百 MW同样 30 MW 的偏差在高峰时段是 1%在凌晨就是 10%。看 MAPE 时要按峰、平、谷三个时段分开统计或者干脆报告「高峰时段 MAPE」和「全天 MAPE」两个数。5.2 避坑一归一化泄漏测试集指标好到离谱的元凶现象训练完后测试集 MAPE 只有 2%你兴冲冲拿去汇报结果用同样模型做未来一周的滚动预测误差直接翻三倍。原因这是整条链路里最阴险的坑——归一化泄漏。如果对全量数据一起 fit MinMaxScaler再切分训练测试集scaler 的 min 和 max 里就包含了测试集的信息。模型训练时虽然没直接见过测试样本但 loss 的数值范围已经被「校准」过了相当于考试前知道了分数区间。解决严格按 3.2 的写法先切分、再 fit、再 transform。自检方法是分别打印训练集和测试集归一化前的 min 和 max如果两者高度接近甚至完全相等说明大概率用了全量归一化。另一个快速排查方式是拿测试集的 MAPE 和验证集的 MAPE 对比如果测试集远好于验证集优先怀疑数据泄漏而不是模型变强了。5.3 避坑二递归多步预测误差一步步滚成雪球现象单步预测的测试集 MAPE 只有 3%但用模型迭代预测未来 24 小时前两小时还行到第 12 小时之后预测曲线开始严重偏移甚至出现「预测值越来越平」的形态。原因这是递归预测的误差累积。预测第 t1 步时用的是真实历史误差小预测第 t2 步时输入窗口里混入了第 t1 步的预测值误差开始传递每往后一步输入里预测值的占比就多一格误差指数级累积。LSTM 训练时用的是真实历史teacher forcing推理时却面对的是自己生成的输入分布不一致这是结构性问题不是你调参能救的。解决三个思路。第一改成多输出结构模型一次输出未来 24 个点误差各自独立不互相传染代价是损失了步间连续性第二训练时随机用预测值替代部分真实历史scheduled sampling让模型见过自己的错误第三数据量足够的话上 seq2seq teacher forcing。对多数工程场景方案一性价比最高具体改法在第 6 章给出。5.4 避坑三节假日和极端天气模型没见过就乱报现象春节期间负荷曲线骤降模型按照工作日规律预测误差直接冲上 15%。台风天更夸张负荷在两小时内掉了 20%LSTM 输出的曲线还是平稳的日周期形态。原因模型学的是历史分布里的「常规模式」。春节、台风这些事件在训练集里出现的次数可能只有几次甚至没有LSTM 不会凭空学会它们。这属于数据分布外的问题再调 hidden_size 也没用这是用血泪经验换来的结论。解决第一步是给模型加特征至少加星期几用正弦余弦编码和节假日标记让模型有条件区分工作日和假期第二步是对特殊日单独建模或做残差修正比如训练一个「正常日模型」再对节假日训练一个差值修正器第三步是极端天气场景下明确告诉业务方 LSTM 的输出只作为参考需要人工干预修正。诚实标注模型的适用边界比硬扛一个 15% 的误差值更重要。5.5 避坑四NaN loss 与梯度爆炸源头往往是一个 lr现象训练到第 40 个 epochloss 突然从 0.008 跳到 NaN之后再也回不来。或者 loss 在某个数值附近反复震荡怎么看都降不下去曲线像锯齿。原因NaN 几乎都是梯度爆炸导致权重更新过大把数值推出浮点数表达范围。常见诱因是 lr 太大、batch 太小导致梯度估计方差大、或者序列太长梯度在时间维累积爆炸。loss 锯齿状震荡则通常是 lr 和 batch 不匹配更新步长过大模型在损失曲面两侧反复横跳。解决给训练循环加clip_grad_norm_(max_norm1.0)这是最直接的后悔药。然后把 lr 从 1e-3 降到 1e-4 看 loss 是否稳定。如果还震荡把 batch 从 64 提到 128扩大 batch 会让梯度估计更平滑。还有一个排查顺序先看训练集 loss 能不能降下来训练集 loss 都不降问题在模型容量或数据预处理而不是调参训练集降而验证集不降才是过拟合需要加 dropout 或早停。6. 把单步模型升级成多步预测三个方案与一个自检习惯6.1 三种多步预测方案怎么选业务上很少有只要预测下一个 15 分钟的场景多数需求是「预测未来 4 小时」或「明天全天 96 点」。在现有单步模型基础上三个方案按工程成本排序。方案一是递归预测把每一步的输出拼回输入窗口循环预测下去实现最省但误差累积最严重只适合预测未来不超过 2 小时的应用。方案二是直接多输出把模型的horizon从 1 改成目标步数全连接层一次吐出一串点这是最常见的落地选择。# 构造多输出样本lookback96, horizon16 表示预测未来4小时15分钟粒度 X_train, y_train make_sequences(train_scaled, lookback96, horizon16) model LSTMPredictor(input_size1, hidden_size64, num_layers2, horizon16)改这两行就够。多输出结构各步误差独立不会滚雪球代价是模型没有显式建模步与步之间的相关性曲线可能不够平滑。方案三是 seq2seq teacher forcing编码器读窗口、解码器逐步生成预测值训练时以一定概率用真实值替代上一步输出效果最好但代码量翻倍数据量少于十万条时收益不明显我不建议一上来就上 seq2seq。6.2 一个自检习惯先看训练集再谈调参最后分享一个我踩过无数次坑后养成的习惯任何新数据集上跑 LSTM先看训练集的 MAPE再看验证集最后才轮到测试集。训练集 MAPE 都下不来说明问题在数据预处理、窗口构造或模型容量这时候调 lr、加 dropout 都是在错误的方向上自我感动。训练集好而测试集差才是过拟合或数据泄漏此时再谈正则化和检查归一化链路。顺序反了你会花一周时间调参调到一个不可能解决问题的方向上。这套从数据预处理到 PyTorch 源码实现再到评估避坑的链路是我做负荷预测项目时的固定动作。LSTM 的代码本身不难难的是知道每个参数在负荷场景下意味着什么、每个坑长什么样。希望今天这篇笔记能帮你把链路一次跑通少走几段我走过的弯路。本文还有配套的精品资源点击获取