
简介面向深度学习初学者与电力负荷预测从业者这份基于PyTorch实现的LSTM项目完整演示了时序预测流程从原始CSV负荷数据清洗、归一化处理到LSTM网络搭建、训练与评估最终以可视化形式呈现预测结果。压缩包共14个文件其中5个CSV提供实验数据、5个Python脚本分别承担数据预处理、模型定义、训练测试与绘图等模块另有MD说明文档和已训练模型权重文件整体仅2.1MB结构紧凑便于逐模块研读。目前已有156人学习下载。说明文档对LSTM门控机制、PyTorch动态图特性和关键代码做了细致解读并指导调整隐藏单元数、网络层数、学习率及优化器等超参数帮助读者在复现基准模型之上进一步改进预测效果。项目兼顾理论与实战是理解循环神经网络应用于电力系统负荷预测的实用参考。1. 电力负荷预测用 LSTM先想清楚数据形态再动手调度员盯着明天的负荷曲线需要提前安排机组出力售电公司要报次日电量偏差大了直接扣钱。这类短中期电力负荷预测这几年基本被 LSTM 这类循环神经网络接手了。用 PyTorch 搭一个 LSTM 做负荷预测门槛其实不高数据准备、滑窗切分、模型定义、训练评估四条线走完就能拿到一条能看的预测曲线。但真正动手的人会告诉你难点从来不是模型本身而是数据怎么切、标签怎么对齐、归一化在哪个环节做。这篇笔记把一个最小可运行的 PyTorch LSTM 负荷预测方案完整拆开讲新手能照着把代码跑通熟手可以对照检查自己项目里的坑。2. 为什么选 LSTM 而不是全连接或 ARIMA负荷序列的规律与模型结构是匹配的2.1 电力负荷数据的三个基本特征电力负荷数据是一组按时间顺序采集的数值比如每 15 分钟一个点、每小时一个点连续记录几天、几个月甚至几年。这类序列有三个特征直接影响模型选型周期性、趋势性和突变性。周期性最好理解居民用电有早上一个峰、晚上一个峰的日内双峰规律工厂用电有工作日和周末的差异北方冬天还有明显的季节趋势。趋势性来自经济增长和季节更替负荷整体逐年上涨冬天比夏天高或者反过来。突变性最麻烦极端天气、节假日、临时检修都会让负荷在短时间内跳变这种跳变没有固定模式任何模型都难以提前捕捉。这三个特征决定了模型至少要能记住昨天这个时候的负荷是多少因为预测明天 14:00 的负荷最有效的参考往往就是今天和昨天 14:00 的负荷。这种跨时间的依赖关系正是循环神经网络的设计初衷。2.2 LSTM 的门控机制为什么适合负荷序列RNN 处理时间序列时有一个致命问题序列太长梯度在反向传播过程中会指数级衰减或爆炸导致模型记不住几天前的信息。LSTM 在 RNN 的基础上加了三个门——遗忘门、输入门、输出门还有一个贯穿时间步的细胞状态。遗忘门决定上一时刻的细胞状态有多少被保留输入门决定当前时刻的新信息有多少写入细胞状态输出门决定细胞状态有多少输出到当前隐藏层。这套机制让 LSTM 在实践中可以记住 100 步甚至更长的依赖关系。对负荷预测来说昨天下午的峰值这种跨 24 小时甚至 48 小时的信息RNN 基本记不住LSTM 却能稳稳地留在细胞状态里。当然LSTM 不是唯一选择。GRU 是 LSTM 的简化版参数更少、训练更快效果在多数负荷预测场景下差距不大。Transformer 也可以做时间序列预测但对中小样本量的负荷数据来说容易过拟合训练成本也高。LSTM 在这个场景下依然是最稳妥的起点。2.3 全连接和 ARIMA 的边界在哪里全连接网络不是不能做负荷预测把过去 168 个小时的负荷拼成一个 168 维的向量塞进去也能得到一个预测值。但它的问题在于全连接网络不区分这 168 个维度的时间先后顺序第 1 小时和第 168 小时在输入层是完全平等的。负荷序列里离预测点越近的历史越重要这个先验知识全连接网络完全学不到需要自己通过特征工程去表达。ARIMA 这类传统统计模型对平稳序列效果很好但负荷序列有明显的周期性直接用 ARIMA 需要做差分、季节分解预处理流程相当繁琐。而且 ARIMA 本质上是线性模型对负荷和温度、湿度、节假日等外部因素的非线性耦合关系表达能力有限。LSTM 的优势在于原始负荷序列直接输入周期性让它自己学外部特征作为附加输入拼接进去也行特征工程的压力小很多。3. 用 PyTorch 实现 LSTM 负荷预测从滑窗切分到训练评估的最小可运行代码3.1 数据处理与滑窗构建把连续负荷切成样本LSTM 的输入是形如(batch, seq_len, input_size)的三维张量。batch是一批样本数seq_len是每个样本包含多少个历史时间步input_size是每个时间步的特征维度。原始负荷数据是一条长长的曲线第一步就是把它切成一个个滑窗样本。import numpy as np import pandas as pd import torch from torch.utils.data import Dataset, DataLoader def create_sequences(data, seq_len24): 把一维负荷序列切成 (样本数, seq_len) 的监督学习样本 data: 一维 numpy 数组按时间升序排列 seq_len: 用过去多少个时间步预测下一个时间步 xs, ys [], [] for i in range(len(data) - seq_len): x data[i:i seq_len] y data[i seq_len] xs.append(x) ys.append(y) return np.array(xs), np.array(ys) class LoadDataset(Dataset): def __init__(self, xs, ys): self.xs torch.tensor(xs, dtypetorch.float32).unsqueeze(-1) self.ys torch.tensor(ys, dtypetorch.float32) def __len__(self): return len(self.xs) def __getitem__(self, idx): return self.xs[idx], self.ys[idx]这里seq_len24表示用过去 24 个小时的负荷预测下一个小时。滑窗的步长是 1也就是说每个小时滑动一次数据利用率最高。unsqueeze(-1)把形状从(seq_len,)变成(seq_len, 1)因为 LSTM 要求最后一维是特征维度我们目前只用负荷本身这一个特征。切分数据时有一个关键细节必须按时间顺序切训练集和验证集绝对不能用随机打乱。负荷序列在时间上是连续的随机打乱会让模型看到未来。常见做法是按比例切分比如前 80% 的时间段做训练后 20% 做验证。# 以小时为单位的负荷数据假设已经读入 df列为 load data df[load].values.astype(np.float32) # 按时间顺序切分前 80% 训练后 20% 验证 train_size int(len(data) * 0.8) train_data data[:train_size] val_data data[train_size - seq_len:] # 预留 seq_len 给验证集构造滑窗 train_xs, train_ys create_sequences(train_data, seq_len) val_xs, val_ys create_sequences(val_data, seq_len) train_loader DataLoader(LoadDataset(train_xs, train_ys), batch_size64, shuffleTrue) val_loader DataLoader(LoadDataset(val_xs, val_ys), batch_size64, shuffleFalse)验证集从train_size - seq_len开始取是为了让验证集的每个样本也都有完整的seq_len个历史点。shuffleTrue只用于训练集验证集必须保持时间顺序否则评估结果没有意义。3.2 搭建 LSTM 模型nn.LSTM 的输入输出对齐PyTorch 的nn.LSTM是封装好的模块不需要自己实现门控逻辑。定义一个两层 LSTM 加一个全连接输出层的模型足够应付大多数负荷预测任务。import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers2, output_size1): super(LSTMPredictor, self).__init__() self.lstm nn.LSTM(input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): # x 形状: (batch, seq_len, input_size) out, _ self.lstm(x) # out 形状: (batch, seq_len, hidden_size)取最后一个时间步的输出 out out[:, -1, :] # out 形状: (batch, hidden_size) out self.fc(out) return outLSTM 的输出有两个out是每个时间步的隐藏状态序列(h_n, c_n)是最后一步的隐藏状态和细胞状态。负荷预测的常规做法是取out[:, -1, :]也就是最后一个时间步的隐藏状态再接全连接层输出预测值。batch_firstTrue让输入输出都变成(batch, seq_len, hidden_size)的顺序省去维度转置的麻烦。隐藏层大小hidden_size64是一个稳妥的起点序列长度 24 小时、单特征输入64 维的隐藏状态足够承载负荷的周期性信息。num_layers2加深模型对非线性关系的拟合能力但不要再往上加层数越多训练越慢负荷预测这种单特征任务三层以上基本没有收益。3.3 训练循环与评估指标MAPE 比 Loss 更说明问题训练循环的骨架是标准的 PyTorch 流程但有几个细节值得注意。损失函数用 MSE均方误差优化器用 Adam学习率从 0.001 开始。同时记录每个 epoch 的训练损失和验证损失验证集上算 MAPE平均绝对百分比误差因为 Loss 是绝对值不同数据量纲下没有直观意义MAPE 直接给出平均偏差百分之多少。import torch.optim as optim from sklearn.metrics import mean_absolute_percentage_error model LSTMPredictor() criterion nn.MSELoss() optimizer optim.Adam(model.parameters(), lr0.001) def evaluate(model, loader): model.eval() preds, trues [], [] with torch.no_grad(): for x, y in loader: out model(x) preds.append(out.numpy().flatten()) trues.append(y.numpy().flatten()) preds np.concatenate(preds) trues np.concatenate(trues) return mean_absolute_percentage_error(trues, preds) epochs 50 for epoch in range(epochs): model.train() train_loss 0.0 for x, y in train_loader: optimizer.zero_grad() out model(x) loss criterion(out.squeeze(), y) loss.backward() optimizer.step() train_loss loss.item() * x.size(0) train_loss / len(train_loader.dataset) val_mape evaluate(model, val_loader) if (epoch 1) % 10 0: print(fEpoch {epoch1}/{epochs}, Train Loss: {train_loss:.4f}, Val MAPE: {val_mape:.2%})loss.backward()之前必须optimizer.zero_grad()否则梯度会累加。criterion(out.squeeze(), y)里out形状是(batch, 1)y形状是(batch,)squeeze()去掉中间的维度才能对齐。验证时用torch.no_grad()关闭梯度计算节省内存也避免误更新参数。一个反直觉的经验是训练 Loss 降得很漂亮不代表验证集 MAPE 就好。LSTM 的拟合能力很强很容易把训练集的负荷曲线背下来但验证集一有天气突变就露馅。所以训练过程中要同时盯两个数验证集 MAPE 连续 5 个 epoch 不降就该考虑早停。3.4 从模型输出到负荷曲线别忘了反归一化训练时为了数值稳定通常把负荷数据归一化到 0~1 或标准正态分布。预测出来的值是在归一化空间里的画图或算真实误差之前必须还原回去否则预测曲线会压扁在某个区间里。def normalize(data): min_val, max_val data.min(), data.max() return (data - min_val) / (max_val - min_val), min_val, max_val def denormalize(norm_data, min_val, max_val): return norm_data * (max_val - min_val) min_val # 归一化只能在训练集上计算 min/max不能全量数据一起算 norm_train, min_val, max_val normalize(train_data)归一化的 min/max 必须只从训练集统计。如果拿全量数据一起归一化验证集的信息就提前泄露到了训练过程中预测结果会虚高。这个细节在后面的避坑章节还会再展开。# 用训练好的模型预测验证集 model.eval() val_preds, val_trues [], [] with torch.no_grad(): for x, y in val_loader: out model(x) val_preds.append(out.numpy().flatten()) val_trues.append(y.numpy().flatten()) val_preds denormalize(np.concatenate(val_preds), min_val, max_val) val_trues denormalize(np.concatenate(val_trues), min_val, max_val)把反归一化后的预测值和真实值画在同一条时间轴上肉眼看曲线重合度比任何指标都直观。这一步建议直接写到训练脚本末尾。3.5 项目说明里必须写清楚的三件事拿到一个源码项目说明.zip第一件事不是跑代码而是看说明文档里有没有写清楚三件事数据格式、运行环境、输出定义。数据格式要写明 CSV 的列名是什么、时间字段是哪种格式、负荷单位是 MW 还是 kW、采样间隔是 15 分钟还是 1 小时。运行环境要写 Python 版本、PyTorch 版本、依赖库清单。输出定义要说明模型预测的是下一个时间步的负荷还是未来 24 小时的负荷序列。如果这三件事没写清代码再漂亮也等于没有。很多开源项目跑不起来不是代码问题是数据格式对不上、版本不一致这些环境问题。4. 环境搭建与依赖清单让项目在别人机器上也能复现4.1 Python 与 PyTorch 的版本搭配PyTorch 的版本迭代很快不同版本之间的 API 变化虽然不大但 CUDA 版本不匹配会导致 GPU 不可用。一个经过验证的稳妥搭配是Python 3.9 或 3.10PyTorch 2.0 或 2.1CUDA 11.8 或 12.1。# conda 创建独立环境避免污染系统 Python conda create -n load_forecast python3.10 conda activate load_forecast # 安装 PyTorchCPU 版和 GPU 版二选一 pip install torch2.1.0 --index-url https://download.pytorch.org/whl/cpu # GPU 版用下面这条cuda 12.1 pip install torch2.1.0 --index-url https://download.pytorch.org/whl/cu121 # 其余依赖 pip install numpy pandas scikit-learn matplotlib用 conda 建独立环境是最省心的做法。负荷预测项目通常还有 pandas、numpy、scikit-learn 这些依赖装在系统 Python 里迟早会碰到版本冲突。conda 环境坏了直接删掉重来不需要跟依赖纠缠。GPU 版的安装命令里cu121表示 CUDA 12.1如果本机装的是 CUDA 11.8把后缀换成cu118。判断方法是命令行跑nvidia-smi看右上角的 CUDA Version。4.2 CPU 与 GPU 的选择小数据量 CPU 完全够用很多初学者一上来就配 GPU 环境其实负荷预测这种单特征、几千个样本的任务CPU 训练一个 epoch 通常只要几秒到几十秒。第一批次的 LSTM 参数量在几万到几十万之间CPU 完全可以胜任。GPU 的优势在大 batch、大模型、长序列场景。如果只是单特征负荷预测序列长度 24、隐藏层 64用 CPU 训练 50 个 epoch 一般不超过 10 分钟。真正需要 GPU 的是多特征场景比如把温度、湿度、风速、节假日标记都拼进输入特征维度上去了参数量变大训练时间才明显拉长。一个折中方案代码里自动检测 GPU 可用性有就用没有就退回 CPU。这样项目说明里不用写死必须 GPU。device torch.device(cuda if torch.cuda.is_available() else cpu) model LSTMPredictor().to(device) # 训练循环里把数据也搬过去 x, y x.to(device), y.to(device)4.3 项目说明文件怎么写让别人按三步跑起来写项目说明的目的是让一个完全没接触过这个项目的人能在 10 分钟内跑通。推荐按三步写第一步装环境第二步放数据第三步跑训练。每一步给一条命令不解释原理只保证能执行。环境部分直接贴requirements.txt不要只列包名要锁版本号。数据部分说明 CSV 放哪个目录、列名是什么。训练部分说明入口脚本是train.py输出模型权重和预测曲线到哪里。# requirements.txt 示例 torch2.1.0 numpy1.26.0 pandas2.1.0 scikit-learn1.3.0 matplotlib3.8.0版本号锁死的意义在于PyTorch 2.0 和 1.13 之间、pandas 2.0 和 1.5 之间API 差异足以让代码报出莫名其妙的错误。不锁版本号的 requirements 等于没写。5. LSTM 负荷预测避坑清单数据泄露、Loss 不降与 MAPE 爆表的五个翻车现场5.1 数据归一化时用了全量统计验证集信息提前泄露现象验证集 MAPE 只有 1%~2%但把模型部署到线上后误差飙升到 8% 以上。原因归一化时用(data - data.min()) / (data.max() - data.min())对全量数据做了计算验证集的 min/max 混进了训练集的归一化参数里相当于模型在训练阶段偷看了验证集的分布范围。解决归一化的 min/max 只从训练集统计验证集和测试集复用训练集的 min/max。这也是第 3 章代码里特意把normalize(train_data)单独拎出来的原因。5.2 时间索引错位预测值和真实值对不齐现象预测曲线看起来和真实曲线形状一致但整体平移了一截像错位的时间序列。原因滑窗切分时x data[i:iseq_len]y data[iseq_len]两个数组在时间上是对的。但 DataLoader 每个 batch 的返回顺序是打乱的训练集或者验证集里没有把i对应的真实时间戳保存下来画图时直接按 0,1,2,... 编号两条曲线就错开了。解决验证集预测时把每个样本对应的真实时间戳一起返回画图时按时间戳对齐。最简单的做法是在LoadDataset里额外返回idx预测时记录下来。5.3 Loss 一直在高位震荡完全不下降现象训练 20 个 epochLoss 在高位波动没有明显下降趋势。原因最常见的是学习率太大或太小。lr0.01在 LSTM 这类循环网络上经常导致损失震荡lr0.0001又会让收敛慢到看不见。另一个原因是输入数据的尺度没有归一化负荷值如果是几百 MW 的量级MSE Loss 的初始值会非常大梯度计算也不稳定。解决优先确认数据归一化然后从lr0.001开始试。如果前 5 个 epoch 损失基本没动把学习率调到0.01如果损失在震荡降回0.0005。还有一个技巧是给 LSTM 加 dropoutnn.LSTM(..., dropout0.2)可以防止模型在训练集上死记硬背。5.4 MAPE 突然爆表出现无穷大值现象验证集 MAPE 计算出来是 3000% 甚至 inf但看 Loss 并不高。原因MAPE 的公式是abs(真实值 - 预测值) / abs(真实值)深夜或节假日负荷接近 0 MW 的时候除以一个接近 0 的数单点误差直接趋近无穷大把整体指标拉爆。解决计算 MAPE 时过滤掉真实值过小的样本。常见做法有两种一是设定阈值真实值小于该阈值就跳过二是改用sMAPE对称平均绝对百分比误差分母变成abs(真实值 预测值) / 2避免除以接近零的数。5.5 改了一行代码结果完全复现不了现象同一个项目同一份数据在不同机器上跑出来的指标不一样甚至同一台机器第二次跑也不是第一次的结果。原因深度学习训练里有大量随机性来源——模型权重初始化的随机数、DataLoader 打乱顺序的随机数、GPU 上 cuDNN 的随机算法。不设随机种子结果每次都不一样。解决在训练脚本开头固定所有随机源。import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False set_seed(42)cudnn.deterministic True让 GPU 上的卷积和 LSTM 计算使用确定性算法代价是速度略慢。为了结果可复现这个代价值得付。6. 从单步到多步负荷预测三种进阶做法与结果验证单步预测解决的是下一个时间点负荷是多少但实际业务更常问未来 24 小时逐时的负荷曲线。多步预测有三种做法实现难度和效果递增。第一种是递推预测recursive把上一步的预测值当作下一步的输入循环 predict 24 次。优点是实现简单、模型不用改缺点是误差逐步累积预测时间越长越飘。第二种是直接多步输出direct multi-step修改模型输出维度从 1 变成n_steps让 LSTM 接一个输出 24 个值的全连接层一次性预测未来 24 小时。这种做法的误差不累积但模型要重新训练。第三种是序列到序列seq2seq编码器 LSTM 读历史序列解码器 LSTM 逐步生成未来序列效果最好但结构最复杂。验证多步预测有没有做对有一个简单但有效的检查方法把预测曲线画出来看 24 个预测点是否保留了日内双峰形态。如果预测曲线是一条水平直线说明模型只学到了用昨天的均值预测明天这是负荷预测最容易犯的默认错误。我的习惯是先把单步预测做到 MAPE 稳定在 3% 以内再上多步。单步都做不准多步递推只会更差。多步模型训练时损失函数可以用每一步预测误差的加权平均越靠前的步权重越大因为业务上近几个小时的准确率比 24 小时后的更重要。工具和代码只是基本面真正拉开差距的是对数据的理解。负荷预测做久了就会发现模型的调参空间有限数据质量、特征设计、训练验证切分这些数据侧的功夫才是决定预测精度的关键。希望这篇笔记能帮你沿着这个方向少走几步弯路。本文还有配套的精品资源点击获取