
简介基于PyTorch实现一个简单LSTM模型进行电力负荷预测适合深度学习初学者、电力系统分析人员及时间序列建模爱好者参考。压缩包共14个文件含5个csv电力负荷数据集、5个Python脚本、1个训练好的pt权重文件、1份Markdown项目说明及少量缓存文件整体仅2.1MB。目前已有156人学习下载。源码按数据预处理、模型搭建、训练、测试和结果可视化等模块清晰划分开发者可快速掌握数据清洗与归一化、LSTM网络构建、损失函数和优化器选择等关键环节并通过生成的对比图直观评估预测效果。项目说明文档逐段解读代码实现帮助理解门控机制、序列建模流程及PyTorch训练细节在此基础上调整超参数或扩展隐藏层结构即可进一步优化预测精度为深入探索深度学习在电力系统中的应用打下基础。1. LSTM电力负荷预测源码包它能不能真的预测出明天零点抢负荷拿到一份“基于pytorch实现一个简单的LSTM模型来进行电力负荷预测源码项目说明.zip”先别急着import torch。电力负荷预测是时间序列里最有代表性的练手场景负荷数据有24小时周期、168小时周周期还有节假日和气温带来的突发尖峰线性模型很难同时吃下这些规律。LSTM用隐状态把过去一段窗口压进记忆直接预测下一点或未来24小时这正是短期负荷预测最常用的套路。这个源码包适合两类人刚学完PyTorch循环神经网络、想找一个完整项目练手的人以及要做能源管理、需求响应预研的工程师。看懂这份代码换一份数据也能用。2. 数据预处理把负荷序列切成LSTM能吃的滑窗样本2.1 负荷数据的三个特征周期、尖峰和“温度后置效应”先看数据长什么样。最常见的负荷数据是逐小时一条记录字段就两个date和loadload 单位可能是 MW也可能是电网侧采集的归一化功率。拿到后第一件事不是建模而是把曲线画出来。你会看到三条规律第一强周期性。一天内通常有两个峰上午和晚间低谷在凌晨周末的峰形和工作日明显不同一周的相似性又会重复。第二尖峰多。高温天中央空调、寒潮来电采暖都会让负荷在几小时内爬升一大截这类样本占比不高但对预测误差影响极大。第三温度对负荷的影响不是同步的建筑热惯性会让负荷比气温滞后两三小时这就是所谓的“温度后置效应”单变量LSTM只能靠历史负荷间接捕捉它。数据清洗这一步我会做两件事把缺失值补齐把小时编码成周期特征。缺失值用前后时刻的线性插值但负荷断点经常持续几小时所以用methodtime比普通interpolate()更合理。小时特征不能直接喂0,1,2,...,23因为23点和0点之间数值上差了23模型会以为这两个时刻差异巨大用正弦余弦编码可以消除这个假边界。import pandas as pd import numpy as np df pd.read_csv(load.csv, parse_dates[date], index_coldate) df[load] df[load].interpolate(methodtime, limit_directionboth) hours df.index.hour.values df[hour_sin] np.sin(2 * np.pi * hours / 24) df[hour_cos] np.cos(2 * np.pi * hours / 24) print(df.head()) print(df[load].isna().sum())这组代码的逻辑是先用时间索引读入数据interpolate填补缺失值再用三角函数把小时映射到单位圆上。hour_sin和hour_cos是给模型的两个弱特征它们能让LSTM区分凌晨2点和下午2点但不强制模型依赖它们。参数说明如果数据是半小时一条周期改成2 * np.pi * minutes / (24 * 60)如果数据带多列特征后续归一化时要把特征列一起处理。一份合格的项目说明在数据这节至少该写清四件事数据来源和采样间隔、缺失比例和处理方式、字段含义和单位、训练验证测试的时间切分点。缺了任何一项三个月后你自己都看不懂这份源码。2.2 滑窗构造168小时输入1步或多步输出LSTM不接收一维时间序列它接收的是“样本”。一个样本就是一段连续的历史窗口和它对应的目标值。滑窗就是把原始序列切成(样本数, 窗口长度, 特征数)和(样本数, 预测步长)两个矩阵的过程。窗口长度seq_len的选择比模型结构更影响效果。做负荷预测我习惯默认seq_len168也就是过去一周的负荷。理由很直接让模型在每个样本里都见过完整的周周期它才有机会学到“今天是周三和上周三更像而不是和昨天更像”这类规律。如果你的数据是15分钟一条那168个点只覆盖42小时这时要把seq_len提升到7 * 96 672才对应一周。预测步长horizon1是单步预测horizon24就是预测未来一天。def make_sequences(data, seq_len168, horizon1): X, y [], [] for i in range(len(data) - seq_len - horizon 1): X.append(data[i : i seq_len]) y.append(data[i seq_len : i seq_len horizon]) return np.array(X), np.array(y) feature_cols [load, hour_sin, hour_cos] X, y make_sequences(df[feature_cols].values, seq_len168, horizon1) print(X.shape, y.shape) # 例如 (N, 168, 3) 和 (N, 1)这段代码的核心边界条件在range上最后一段可用数据的起始位置是len(data) - seq_len - horizon保证窗口结束位置i seq_len严格早于目标开始位置i seq_len。很多初写滑窗的人会在这里出错索引差一个位置等于把未来数据放进了输入训练时loss会很低上线后立刻露馅。划分训练集、验证集、测试集时注意永远按时间顺序切不要随机打乱。比如前70%训练、中间15%验证、最后15%测试。随机切分会让模型在训练时见过验证时间段附近的负荷形态相当于泄露。2.3 归一化与反归一化训练集统计量是唯一合法的统计量负荷数据动辄几百上千MW小时编码在[-1,1]之间如果不归一化LSTM的输入门和遗忘门会被大数值特征主导训练很难收敛。常见做法是用MinMaxScaler把特征压到[0,1]但这个归一化操作里藏着一个经典的坑只能用训练段的统计量去fit验证集和测试集只能transform。from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler() train_values df[load][:train_end].values.reshape(-1, 1) scaler.fit(train_values) df[load_scaled] scaler.transform(df[load].values.reshape(-1, 1))这段代码的关键在于fit的参数是train_values而不是全量数据。如果用全量数据算min和max测试集的信息在训练前就参与了缩放验证集的误差会被系统性低估这个误差在论文里看不出来但真实部署时模型会原形毕露。transform输出的load_scaled会直接作为滑窗的输入特征。反归一化只发生在两个地方计算评估指标前以及最后输出预测结果时。把模型的预测值乘回去pred_real scaler.inverse_transform(pred_scaled.reshape(-1, 1))。注意scaler只对负荷列做小时特征不需要反归一化。3. 用PyTorch搭建LSTM模型从nn.LSTM参数到可复现的forward3.1 环境与模型骨架input_size、hidden_size、num_layers怎么选搭建环境这一步建议用conda单独建一个项目环境装好与显卡驱动匹配的pytorch版本再装pandas、numpy、scikit-learn。装完后先跑一句print(torch.__version__)验证。环境不固定后面想复现实验结果会非常被动项目说明里也应该写清这一步包括操作系统、CUDA版本、Python版本和各库的版本号。模型定义是整个源码包里最核心的一块PyTorch自带的nn.LSTM把循环神经网络的底层计算封装好了你只需要决定它的参数import torch import torch.nn as nn class LoadLSTM(nn.Module): def __init__(self, input_size3, hidden_size64, num_layers2, output_size1, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0.0 ) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): out, _ self.lstm(x) last out[:, -1, :] return self.fc(last)参数选择的逻辑input_size3对应load_scaled、hour_sin、hour_cos三个特征hidden_size64是隐状态维度它决定了LSTM“记忆容量”数据量不大时64足够加大到128通常不会带来质的提升只会明显拖慢训练num_layers2是层数两层LSTM能建模稍复杂的非线性三层以上在几千个样本的小数据集上容易过拟合。batch_firstTrue是必须的它让输入张量形状变成(batch_size, seq_len, input_size)否则默认形状是(seq_len, batch_size, input_size)新手在这里反复踩索引错位的坑。dropout参数只对num_layers 1时生效它作用在层与层之间不是输入和输出。还有一批网络权重初始化的小技巧比如对LSTM的隐层权重做正交初始化、对fc层做均匀分布初始化能减少训练初期的震荡但不必过度追求先用默认初始化跑通再调。3.2 前向计算为什么取 out[:, -1, :] 而不是碰运气nn.LSTM的返回值有两个out和(h_n, c_n)。out的形状是(batch_size, seq_len, hidden_size)它保存的是每个时间步的输出h_n是最后一层、最后一个时间步的隐状态。负荷预测这个场景我们的目标是用整段历史窗口预测窗口之后的负荷所以要取窗口末尾的信息也就是out[:, -1, :]。这里有个容易混淆的点为什么不直接取h_n[-1]h_n的形状是(num_layers, batch_size, hidden_size)取最后一层h_n[-1]和out[:, -1, :]在单层LSTM下结果一致但多层时h_n[-1]只代表最后一层而out[:, -1, :]已经包含了所有层的处理结果语义更直观。从可读性出发我建议统一用out[:, -1, :]。最后一个线性层self.fc把隐状态从hidden_size维映射到output_size维。output_size1时输出单步负荷output_size24时是一次性多步预测这个选择在后面的多步预测章节会展开。3.3 训练循环一个epoch里发生的四件事训练PyTorch模型的每个epoch都在重复四件事前向计算损失、反向传播梯度、更新权重、清空梯度。看起来简单但顺序错了或漏了zero_grad()梯度就会跨batch累积loss曲线会变得异常。import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset train_dataset TensorDataset(torch.tensor(X_train, dtypetorch.float32), torch.tensor(y_train, dtypetorch.float32)) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) model LoadLSTM(input_size3, hidden_size64, num_layers2, output_size1) criterion nn.MSELoss() optimizer optim.Adam(model.parameters(), lr1e-3) for epoch in range(30): model.train() for xb, yb in train_loader: optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step()训练数据我用shuffleTrue因为滑窗样本之间本来就存在时间重叠打乱顺序并不会引入未来信息反而能让每个batch的分布更均匀。验证集和测试集的 DataLoader 必须shuffleFalse否则评估结果会受到batch顺序干扰。clip_grad_norm_是很多人忽略的一步LSTM在长序列上容易梯度爆炸把梯度范数截断到5能让训练更稳。学习率1e-3是Adam的默认值通常不用调就能收敛如果loss下降太慢再考虑1e-2或3e-4。4. 训练配置与评估指标让LSTM网络不是“背答案”4.1 损失函数MSE训练、Huber兜底、MAPE汇报lstm神经网络训练时损失函数的选择直接影响模型对误差的敏感度。最常见的是MSELoss它对大误差样本给出平方级惩罚这既是优点也是缺点负荷尖峰日的预测误差会被放大模型会花大量容量去拟合那几个极端日子而牺牲普通日子的精度。如果训练集里尖峰样本不多我不会急着换损失函数先用MSE跑通整个流程。当发现loss一直降不下去、且预测曲线在尖峰日明显“不敢冲高”时换HuberLoss试试。Huber在误差小于delta时按MSE计算大于delta时按MAE计算相当于对大误差的惩罚从平方退化为线性训练更稳。criterion nn.HuberLoss(delta1.0)delta的取值和归一化后的数据尺度强相关如果数据被缩放到了[0,1]delta1.0意味着一倍标准差之外都算离群点如果数据范围更大要相应调大delta。我一般会先看验证集预测误差的分布把80%分位误差作为delta的参考值。4.2 三条评估指标MAE、RMSE、MAPE的读法训练时看loss评估时看另外三个指标MAE、RMSE、MAPE。为什么要多算这几个数而不是直接用loss因为loss是归一化空间里的数值单位模糊而这三个指标能反归一化回真实负荷的物理单位直接告诉业务方“平均误差是多少MW、百分之多少”。指标计算公式关注点适用场景MAEmean(|y_pred - y_true|)平均绝对误差单位与负荷一致绝大多数负荷预测汇报RMSEsqrt(mean((y_pred - y_true)^2))对极大误差更敏感需要控制峰值风险的场景MAPEmean(|y_pred - y_true| / y_true) * 100%相对误差百分比跨数据集横向比较def evaluate(y_true, y_pred): y_true scaler.inverse_transform(y_true.reshape(-1, 1)).flatten() y_pred scaler.inverse_transform(y_pred.reshape(-1, 1)).flatten() mae np.mean(np.abs(y_pred - y_true)) rmse np.sqrt(np.mean((y_pred - y_true) ** 2)) mape np.mean(np.abs((y_pred - y_true) / (y_true 1e-6))) * 100 return mae, rmse, mape注意代码里先反归一化再算误差。y_true 1e-6是为了防止负荷为零时分母爆炸。MAPE的读法单步预测在3%到8%之间都算合理低于3%要么数据本身非常平稳要么你泄露了未来信息高于10%则说明模型基本没学到周期性该回看滑窗或特征。4.3 早停与学习率衰减省掉一半无效训练pytorch实战里最常见的资源浪费是让模型跑满固定的epoch数。明明第15轮就已经过拟合了训练循环还在继续。早停的逻辑很简单监控验证集指标连续多轮不下降就停止并保留历史最优的模型参数。best_loss float(inf) patience 0 for epoch in range(100): train_one_epoch() val_loss evaluate(model, val_loader) if val_loss best_loss: best_loss val_loss torch.save(model.state_dict(), best_model.pt) patience 0 else: patience 1 if patience 10: print(fearly stop at epoch {epoch}) break这套逻辑里patience是“容忍轮数”10表示连续10轮验证集没有刷新最优就停。配合学习率衰减更省心用ReduceLROnPlateau让学习率在验证loss连续不动时自动减半。固定随机种子也是必须做的否则每次跑出来的结果都不一样没法判断参数改动是否有效。def seed_everything(seed42): torch.manual_seed(seed) np.random.seed(seed) torch.cuda.manual_seed_all(seed)5. LSTM负荷预测的5个翻车点与排查清单5.1 预测曲线整体滞后一步模型在学“昨天等于今天”现象把预测值和真实值画在一起曲线高度重合但预测的波峰总比真实波峰晚几小时MAPE看起来不高可峰值永远“慢半拍”。原因负荷序列自相关极强t时刻的负荷最接近t-1时刻。当模型没有额外特征可用时最省力的策略就是模仿上一时刻的负荷这样的loss很低但预测结果没有实用价值因为你需要的是提前预测而不是复读昨天。解决先把这个“恒值预测”作为baseline算一遍指标任何模型的误差都应该显著低于它。模型层面把seq_len从24提升到168让模型看到完整周周期特征层面加入气温和节假日标志打破“昨天等于今天”的路径依赖评估层面额外计算峰值时刻误差比如把预测日的每日最大负荷发生时间和真实值对比滞后超过2小时就判为不合格。5.2 归一化泄露验证集指标好得可疑现象验证集MAPE只有1%左右训练集MAPE却有5%模型在验证集上“神预测”换了新数据就崩。原因最常见的是scaler.fit()用了全量数据或者切分数据集之前就做了归一化验证集的统计信息被模型间接看到了。解决把切分和归一化的顺序在项目说明里写死——先切出train、val、test三个索引区间再对train段做fit然后依次transform。检查代码时重点看两行scaler.fit的入参是不是只有训练段滑窗构建是否发生在归一化之后。5.3 验证集loss在下滑预测值却是一条平线现象训练过程loss稳步下降但反归一化后预测值几乎是一条水平直线数值接近训练集负荷均值。原因这是输出层与损失函数共同作用的结果。当特征对目标值的解释力不足时预测均值是最小化MSE的保守选择。另一个常见原因是归一化后目标值集中在0.5附近模型输出被线性层压平了。解决先画一下训练集负荷的分布如果大部分值集中在窄区间说明模型不是没学会而是目标本身缺乏方差这时预测均值也算合理需要引入外部特征扩大区分度如果分布正常把损失函数换成Huber并检查最后一层线性层是否有biasnn.Linear(hidden_size, 1)默认带bias不要去掉。5.4 单步预测挺好多步预测越滚越差现象单步预测MAPE 4%改成递归滚动预测未来24小时前6小时还行后面误差越来越大第20小时MAPE能到15%。原因递归滚动时每一步的预测值会被当作下一步的输入误差按指数级累积。负荷数据有周期性但并非严格确定一步错步步错。解决不要用递归滚动到24步改用直接多输出把output_size设为24让模型一次输出未来24个时刻的预测。代价是24步输出的最后几步精度也有限但至少不会误差爆炸。另一个实用方案是滚动修正预测到第6小时时用最近的真实负荷重新构造输入窗口再做一次6步预测俗称“每6小时校准一次”。5.5 换机器就跑不出同样的结果seed与环境不一致现象同一份源码在A机器上训练MAPE 4%到B机器上变成6%模型结构、数据都没变。原因模型训练里有三处不确定性PyTorch和CUDA的随机数生成器、DataLoader的多进程数据顺序、GPU上的非确定性算法。只设了torch.manual_seed只解决了一部分。解决固定环境在项目说明里记录操作系统、Python版本、pytorch版本、CUDA版本代码里同时固定numpy、python内置random、torch.cuda.manual_seed_all。想要更严格地复现开启torch.use_deterministic_algorithms(True)但要注意某些操作会因此报错性能也会下降适合测试而不适合日常训练。6. 把训练好的LSTM变成实用预测工具滚动预测、模型固化与回测6.1 递归滚动24小时预测的两种写法单步模型已经训练好但业务要的是未来24小时曲线。最简单的滚动写法是每预测出1个新值就把它拼到输入序列尾部丢掉最早的1个值再预测下一点循环24次。归一化空间里操作预测完再统一反归一化。def rolling_predict(model, X_last, steps24): model.eval() seq X_last.clone() preds [] with torch.no_grad(): for _ in range(steps): y_hat model(seq).item() preds.append(y_hat) new_step torch.tensor([[[y_hat, seq[0, -1, 1], seq[0, -1, 2]]]]) seq torch.cat([seq[:, 1:, :], new_step], dim1) return np.array(preds)这段代码里要注意的是new_step的构造负荷部分用刚预测出的y_hat小时特征部分直接用上一时刻的hour_sin和hour_cos因为未来24小时的小时编码是已知的。如果你在模型里加了温度特征滚动预测时要额外准备一段未来温度序列。6.2 保存和加载把scaler和seq_len一起存进去训练结束只保存state_dict是不够的下次加载时你还需要scaler的统计量、seq_len、特征列表。把这几样打包成一个字典torch.save({ model_state: model.state_dict(), scaler: scaler, seq_len: 168, feature_cols: feature_cols, }, lstm_load_model.pth) ckpt torch.load(lstm_load_model.pth, map_locationcpu) model.load_state_dict(ckpt[model_state])6.3 快速回测一套方案值不值得投入判断这套方案有没有价值我的习惯是拿最近三个月的真实数据做滚动回测从第1天起每天预测次日24小时负荷每周校准一次算整个回测周期的MAPE和峰值误差。在跑这套流程之前我会顺手用线性回归和朴素“上周同时刻”预测做两个对比模型如果LSTM的优势不足10%那就别上复杂度了。这是我吃了多次亏以后留下来的习惯——先定baseline再谈神经网络。希望帮到你。本文还有配套的精品资源点击获取