news 2026/10/1 10:02:28

LSTM电力负荷预测实战:基于PyTorch从预处理到滚动预测

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LSTM电力负荷预测实战:基于PyTorch从预处理到滚动预测

简介:基于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和load,load 单位可能是 MW,也可能是电网侧采集的归一化功率。拿到后第一件事不是建模,而是把曲线画出来。你会看到三条规律:

第一,强周期性。一天内通常有两个峰,上午和晚间,低谷在凌晨;周末的峰形和工作日明显不同,一周的相似性又会重复。第二,尖峰多。高温天中央空调、寒潮来电采暖,都会让负荷在几小时内爬升一大截,这类样本占比不高,但对预测误差影响极大。第三,温度对负荷的影响不是同步的,建筑热惯性会让负荷比气温滞后两三小时,这就是所谓的“温度后置效应”,单变量LSTM只能靠历史负荷间接捕捉它。

数据清洗这一步我会做两件事:把缺失值补齐,把小时编码成周期特征。缺失值用前后时刻的线性插值,但负荷断点经常持续几小时,所以用method="time"比普通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_col="date") df["load"] = df["load"].interpolate(method="time", limit_direction="both") 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_len=168,也就是过去一周的负荷。理由很直接:让模型在每个样本里都见过完整的周周期,它才有机会学到“今天是周三和上周三更像,而不是和昨天更像”这类规律。如果你的数据是15分钟一条,那168个点只覆盖42小时,这时要把seq_len提升到7 * 96 = 672才对应一周。预测步长horizon=1是单步预测,horizon=24就是预测未来一天。

def make_sequences(data, seq_len=168, horizon=1): 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_len=168, horizon=1) 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参数到可复现的forward

3.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_size=3, hidden_size=64, num_layers=2, output_size=1, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout 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_size=3对应load_scaled、hour_sin、hour_cos三个特征;hidden_size=64是隐状态维度,它决定了LSTM“记忆容量”,数据量不大时64足够,加大到128通常不会带来质的提升,只会明显拖慢训练;num_layers=2是层数,两层LSTM能建模稍复杂的非线性,三层以上在几千个样本的小数据集上容易过拟合。batch_first=True是必须的,它让输入张量形状变成(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_size=1时输出单步负荷,output_size=24时是一次性多步预测,这个选择在后面的多步预测章节会展开。

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, dtype=torch.float32), torch.tensor(y_train, dtype=torch.float32)) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) model = LoadLSTM(input_size=3, hidden_size=64, num_layers=2, output_size=1) criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=1e-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_norm=5.0) optimizer.step()

训练数据我用shuffle=True,因为滑窗样本之间本来就存在时间重叠,打乱顺序并不会引入未来信息,反而能让每个batch的分布更均匀。验证集和测试集的 DataLoader 必须shuffle=False,否则评估结果会受到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(delta=1.0)

delta的取值和归一化后的数据尺度强相关,如果数据被缩放到了[0,1],delta=1.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(f"early stop at epoch {epoch}") break

这套逻辑里,patience是“容忍轮数”,10表示连续10轮验证集没有刷新最优就停。配合学习率衰减更省心:用ReduceLROnPlateau让学习率在验证loss连续不动时自动减半。固定随机种子也是必须做的,否则每次跑出来的结果都不一样,没法判断参数改动是否有效。

def seed_everything(seed=42): 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,并检查最后一层线性层是否有bias,nn.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, steps=24): 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], dim=1) 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_location="cpu") model.load_state_dict(ckpt["model_state"])

6.3 快速回测一套方案值不值得投入

判断这套方案有没有价值,我的习惯是拿最近三个月的真实数据做滚动回测:从第1天起,每天预测次日24小时负荷,每周校准一次,算整个回测周期的MAPE和峰值误差。在跑这套流程之前,我会顺手用线性回归和朴素“上周同时刻”预测做两个对比模型,如果LSTM的优势不足10%,那就别上复杂度了。这是我吃了多次亏以后留下来的习惯——先定baseline,再谈神经网络。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 10:02:23

Keil MDK下载安装全指南:版本选择与Pack包管理实战

1. 下载Keil MDK之前&#xff0c;先搞清楚你要的到底是哪个Keil很多人一上来就搜“keil下载”&#xff0c;搜完之后反而懵了——页面上有好几个下载入口&#xff0c;什么MDK-ARM、C51、C251、Vision&#xff0c;点进去又发现有不同的版本号&#xff0c;到底该下哪个&#xff1f…

作者头像 李华
网站建设 2026/10/1 10:01:21

状态空间模型SSM实战指南:从Mamba选型到长文本工程落地

最近在做长文档问答项目时&#xff0c;被一份百万token级的合同文本折磨得焦头烂额。用常规Transformer模型跑一次全量注意力&#xff0c;显存直接OOM&#xff1b;用滑窗吧&#xff0c;跨章节的关联信息又抓不住&#xff1b;后来试着把主干换成状态空间模型&#xff08;SSM&…

作者头像 李华
网站建设 2026/10/1 10:01:12

WorkBuddy 实验记录:照片、参数、结论自动归档,复现实验终于不靠回忆

WorkBuddy 实验记录:照片、参数、结论自动归档,复现实验终于不靠回忆 [!NOTE] 实验做完才补记录,常会丢失样本编号、仪器设置和异常过程。WorkBuddy 可以协助整理,但原始数据与时间戳必须保持不可篡改。 本课不会用“AI 一键完成”制造错觉,而是把 WorkBuddy、Python 3.11…

作者头像 李华
网站建设 2026/10/1 10:00:11

自建Asterisk VoIP服务器:从云PBX账单到开源电话系统的完整落地指南

说实话&#xff0c;我第一次认真考虑自建VoIP服务器Asterisk&#xff0c;不是出于折腾的兴趣&#xff0c;而是被一张云PBX账单刺激到了。公司一百多号人&#xff0c;分机总数不过五十&#xff0c;每月给托管电话系统交的费用够买好几台正经服务器了。后来我一算&#xff0c;用一…

作者头像 李华
网站建设 2026/10/1 9:59:50

运动想象脑电0预处理分析:Matlab实操与ERD/ERS特征提取

我最早接触运动想象脑电的时候&#xff0c;踩过一个很实在的坑&#xff1a;拿到原始EEG数据&#xff0c;第一反应就是赶紧上带通滤波、ICA去伪迹、坏导插值&#xff0c;结果预处理管线还没跑通&#xff0c;一天就过去了。后来我把问题重新想了一遍&#xff0c;才发现很多时候我…

作者头像 李华
网站建设 2026/10/1 9:57:41

Voicebox:2秒克隆任何人的声音,Meta语音生成模型技术全解析

给它一段 2 秒钟的人声——哪怕只是一句"你好"——它就能用这个人的声音&#xff0c;念出任何你给的文字&#xff0c;连语气、音色、口音都几乎一致。这不是科幻片&#xff0c;而是 Meta 在 2023 年 6 月开源论文中展示的语音生成模型 Voicebox。更反直觉的是&#x…

作者头像 李华