简介:一套基于深度学习LSTM的多特征电力负荷预测Python项目源码集成包,内含项目说明文档与实验数据集。适合计算机、电气工程及自动化等专业学生用于毕业设计、期末大作业或课程设计,解决电力负荷时间序列预测建模与代码落地的问题。压缩包仅830KB,共8个文件,涵盖3个Python脚本、2个Markdown说明文档、2个CSV数据集及1个Excel数据表。Python脚本分别对应数据预处理、LSTM模型训练预测与结果可视化展示,Markdown文档提供项目说明与使用指引,数据文件则包含多特征负荷数据,便于直接替换为自身场景数据。代码附有详细注释,整体结构清晰,下载后简单部署即可运行,无需复杂配置。无论是需要快速搭建预测模型,还是想学习LSTM在电力负荷场景下的完整实现流程,都可以参照本资源进行二次开发或直接提交。已有239人下载学习,作为导师认可的高分项目,具备较强的参考价值。
1. 数据决定了LSTM的上限:多特征电力负荷预测从源码到落地的完整路径
拿到一份"Python基于深度学习LSTM的多特征电力负荷预测程序源码(附有项目说明+数据集)"时,多数人第一反应是直接跑通看效果,但这样大概率会翻车。电力负荷预测是典型的时间序列预测场景,LSTM的多特征输入能捕捉负荷与气温、日期类型、湿度等多因素的耦合关系,让预测误差从10%以上压到3%以内。这篇笔记围绕这套程序源码展开,适合电力行业数据分析师、能源系统方向的研究生,以及刚接触LSTM时间序列预测的Python工程师。我会把数据的滑窗构造、模型训练的完整代码、评估指标和部署技巧都拆开讲,你跟着做完,就能把这份源码吃透,换到自己的数据上跑通。
2. 多特征电力负荷数据怎么处理:清洗、滑窗与归一化的三个关键步骤
2.1 原始数据字段构成:负荷之外还有哪些特征能用
电力负荷预测里"多特征"指的不只是历史负荷这一条序列,而是把影响负荷的多种因素一起喂给模型。常见的数据集字段包括:时间戳、有功负荷(MW)、干球温度、相对湿度、节假日标记、星期几。部分公开数据集还会带上电价、风速、光照强度,但用于负荷预测,温度、湿度、日期类型是性价比最高的三个外生特征。
拿到源码里的数据集后,第一步是检查数据的时间粒度和缺失情况。多数项目说明里标注的是15分钟或1小时间隔,你需要在代码中确认。数据清洗时,我会先做三件事:检查时间戳是否连续、处理缺失值、剔除异常突变点。缺失值用前后线性插值,异常突变点用滚动窗口的中位数替换——电力负荷的瞬跳通常是数据采集故障,直接删掉会让时间序列断裂。
import pandas as pd import numpy as np # 读取CSV数据,time列作为索引 df = pd.read_csv('load_data.csv', parse_dates=['time'], index_col='time') # 检查缺失值:电力负荷数据常见的坑是节假日前后整段缺失 print("缺失值统计:\n", df.isnull().sum()) # 用线性插值填充缺失值 df = df.interpolate(method='linear', limit_direction='both') # 用滚动窗口中位数识别并处理异常突变点 window = 12 # 1小时窗口(15分钟粒度) df['load_median'] = df['load'].rolling(window, center=True).median() # 超过3倍标准差的点视为异常,用中位数替换 df['load'] = np.where((df['load'] - df['load_median']).abs() > 3 * df['load'].std(), df['load_median'], df['load']) df = df.drop(columns=['load_median'])这里有个关键参数:滚动窗口window的大小。15分钟粒度的数据取12个点(3小时)比较合适,因为电力负荷的短时波动周期一般在小时级。窗口太小会把正常的负荷毛刺误判为异常,窗口太大则对真实的负荷突变失去响应。你在这个程序源码上调试时,从这份数据集自带的说明里看一看到底是15分钟还是1小时粒度,再调整这个参数。
2.2 滑窗构造:用过去N小时预测未来M小时的完整实现
LSTM不能直接吃一整条时间序列,它需要你手工构造"样本对"。每一组样本包括一个seq_len长度的历史窗口和一个pred_len长度的目标窗口。对电力负荷预测来说,最常见的是用过去72小时的数据预测未来24小时。滑窗构造是这套源码里最核心的数据预处理逻辑,也是新手最容易写错的地方——滑窗的步长、样本之间的重叠度、是否打乱顺序都会直接影响模型效果。
def create_sequences(data, feature_cols, target_col, seq_len=72, pred_len=24, stride=1): """ 构造LSTM训练样本对。 data: 完整DataFrame(已填充缺失值) feature_cols: 参与预测的特征列名列表 target_col: 要预测的负荷列名 seq_len: 历史窗口长度(小时数) pred_len: 预测窗口长度(小时数) stride: 滑窗步长 """ X, y = [], [] feature_data = data[feature_cols].values target_data = data[target_col].values for i in range(0, len(data) - seq_len - pred_len + 1, stride): X.append(feature_data[i:i + seq_len]) # 历史 seq_len 步的所有特征 y.append(target_data[i + seq_len:i + seq_len + pred_len]) # 未来 pred_len 步的负荷 return np.array(X), np.array(y) # 示例调用 feature_cols = ['load', 'temperature', 'humidity', 'is_holiday'] X, y = create_sequences(df, feature_cols, target_col='load', seq_len=72, pred_len=24, stride=3) print("样本形状:", X.shape, y.shape) # (样本数, 72, 4) / (样本数, 24)stride参数容易被忽略。工程上我一般设置stride=3(每3小时采样一个样本),这样既能覆盖足够多的训练样本,又避免了相邻样本过于相似导致的过拟合倾向。如果数据集足够大,stride=1也可以,但训练时间会大幅增加。滑窗构造后的X三维结构(样本数, seq_len, 特征数)就是 LSTM 需要的输入格式,这一步做完,模型部分的代码才能跑通。
2.3 归一化与数据集划分:时间序列不能随机打乱
LSTM 对输入数据的尺度非常敏感。多特征里负载的量级在几十到几千MW,气温在零下到40度,如果不归一化,模型会天然偏向量级大的特征,学到错误的关系。常规做法是MinMaxScaler把所有特征压缩到 [0,1] 区间。这里有一个不能省的操作:scaler必须只用训练集的数据来fit,再对验证集和测试集做transform。
from sklearn.preprocessing import MinMaxScaler # 按时间顺序划分:前80%训练,中间10%验证,最后10%测试 train_size = int(len(df) * 0.8) val_size = int(len(df) * 0.1) train_df = df.iloc[:train_size] val_df = df.iloc[train_size:train_size + val_size] test_df = df.iloc[train_size + val_size:] feature_cols = ['load', 'temperature', 'humidity', 'is_holiday'] scaler = MinMaxScaler() # 用训练集 fit,再分别 transform scaler.fit(train_df[feature_cols]) train_arr = scaler.transform(train_df[feature_cols]) val_arr = scaler.transform(val_df[feature_cols]) test_arr = scaler.transform(test_df[feature_cols])这里为什么强调不能对整个数据集做fit?因为scaler在fit时看到了未来的最大值和最小值,等于把测试集的信息泄露到了训练阶段,会让验证指标虚高。这是这套源码里最容易翻车的地方。另外,数据集划分绝对不能用train_test_split(random_state=42)这种随机切分——电力负荷是强自相关的时序数据,随机切分会把连续的时间段打散,模型相当于"偷看"了未来临近时段的数据,测试结果毫无参考价值。
3. 用PyTorch搭建LSTM多特征预测模型:核心代码与参数含义
3.1 为什么多特征输入要reshape成三维张量
PyTorch 的nn.LSTM要求的输入形状是(seq_len, batch_size, input_size),如果设置了batch_first=True,则形状为(batch_size, seq_len, input_size)。input_size就是特征数量,在上一步构造的X里就是feature_cols的长度(4)。有时你会看到源码里出现X = X.reshape(-1, seq_len, input_size)这样的代码,它的作用就是把二维或扁平的输入整理成这个三维结构,这一步漏掉会直接报维度错误。
常见误区:把特征拼接成(batch_size, seq_len * input_size)的二维输入丢给 LSTM,这是不对的。LSTM 期望的是"时间步"这一维度独立存在,它才能逐步迭代计算。如果已经是X.shape = (样本数, 72, 4)的形状,就无需再 reshape,直接转成 PyTorch 张量即可。
import torch import torch.nn as nn X_tensor = torch.tensor(X, dtype=torch.float32) # (样本数, seq_len, 特征数) y_tensor = torch.tensor(y, dtype=torch.float32) # (样本数, pred_len)3.2 网络结构定义:input_size、hidden_size、num_layers怎么定
LSTM 模型的结构并不复杂,但参数选择直接决定拟合能力。hidden_size是 LSTM 细胞状态的维度,可以理解成"记忆容量";num_layers是堆叠的 LSTM 层数,堆两层通常能捕捉更高层的时序模式,但超过三层容易过拟合且训练极慢。下面这个定义是这套源码的标准结构,我补充了一些注释和参数建议。
class LSTMPredictor(nn.Module): def __init__(self, input_size=4, hidden_size=64, num_layers=2, pred_len=24, 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 # num_layers>1时生效 ) # 从LSTM的hidden_state映射到pred_len个预测值 self.fc = nn.Linear(hidden_size, pred_len) def forward(self, x): # x: (batch, seq_len, input_size) out, (h_n, c_n) = self.lstm(x) # 取最后一层的最后时间步输出: out[:, -1, :] -> (batch, hidden_size) last_out = out[:, -1, :] preds = self.fc(last_out) # (batch, pred_len) return predsout[:, -1, :]是只取最后一个时间步的隐藏状态作为整个序列的浓缩表示。理论依据是:LSTM 的最后一个状态已经编码了整个历史窗口的信息,再往后接一个全连接层做回归输出即可。有时你会看到有人return self.fc(out),这是把每个时间步都做了预测,只有在做序列到序列(seq2seq)预测时才需要。对单步多步预测,取最后状态是更常见的做法。dropout参数只在num_layers>1时才会生效,如果你只堆了一层却加了dropout=0.5,代码不会报错,但模型实际上没有使用 dropout——这是 PyTorch 里的一个隐藏细节。
3.3 训练循环与损失函数:MSE之外还要关注MAPE
训练循环本身是标准的 PyTorch 流程,但有几个针对时间序列预测的调整。损失函数用nn.MSELoss()没问题,因为它对大误差的惩罚更重;但电力负荷预测的业务方往往更关心百分比误差,所以我会在每一个 epoch 结束后额外计算MAPE,用来判断模型是否真的学得准,而不只是看 loss 数字在降。
import torch.optim as optim model = LSTMPredictor(input_size=4, hidden_size=64, num_layers=2, pred_len=24) optimizer = optim.Adam(model.parameters(), lr=0.001) criterion = nn.MSELoss() batch_size = 64 epochs = 50 train_dataset = torch.utils.data.TensorDataset(X_tensor_train, y_tensor_train) train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True) def mape_loss(y_true, y_pred): return torch.mean(torch.abs((y_true - y_pred) / (y_true + 1e-8))) * 100 for epoch in range(epochs): model.train() epoch_loss = 0.0 for X_batch, y_batch in train_loader: optimizer.zero_grad() y_pred = model(X_batch) loss = criterion(y_pred, y_batch) loss.backward() optimizer.step() epoch_loss += loss.item() * X_batch.size(0) # 每个epoch后计算验证集MAPE,不需要梯度 model.eval() with torch.no_grad(): val_pred = model(X_tensor_val) val_mape = mape_loss(y_tensor_val, val_pred).item() print(f"Epoch {epoch+1}/{epochs} | Train Loss: {epoch_loss/len(train_dataset):.6f} | Val MAPE: {val_mape:.2f}%")学习率的设置是玄学,但我给一个相对稳定的起步经验:lr=0.001+Adam是大多数序列预测项目的默认配置。如果训练 loss 震荡剧烈或出现 NaN,把学习率降到0.0003;如果 loss 下降太慢,提升到0.003但需要配合早停。batch_size在负荷预测这种样本量几千到几万的任务里取64或128比较稳,过小会让梯度更新太频繁,过大则每个 epoch 时间太长且容易陷入局部极值。shuffle=True在这里是合理的,因为样本在滑窗构造时已经天然按时间排列,打乱批次顺序只影响梯度更新顺序,不影响时间序列的时间依赖关系——注意这与数据集划分时的"不能打乱"不是一个概念。
4. LSTM电力负荷预测最常见的5个坑:现象、原因与解决方案
4.1 预测结果是一条水平直线
现象:测试集上的预测曲线几乎是一条水平线,数值接近训练集的均值。
原因:我第一次跑类似的模型时也遇到过。水平线往往意味着模型没有学到时序特征,退化为"对每个输入都输出平均负荷"。常见诱因有两个:一是数据没有归一化,LSTM的激活函数(默认tanh)对原始尺度差异大的输入直接饱和;二是网络过于简单,hidden_size太小或没有隐藏层,模型容量不足。
解决:先确认特征都做了MinMaxScaler;再把hidden_size从 16 往上加到 64 或 128 试一轮;如果还不行,检查是不是seq_len太短(低于24小时),LSTM看不到足够长的历史模式。
4.2 训练损失下降但验证损失升高
现象:训练集 loss 稳定下降,但验证集的 MAPE 在第 10 个 epoch 后不降反升。
原因:这是典型的过拟合。时间序列模型因为样本之间存在强自相关,过拟合比图像分类更容易发生——模型把训练集里某些时段的特殊模式背下来了,而不是学到通用的负荷变化规律。
解决:在nn.LSTM中把dropout从 0.2 提到 0.5;把num_layers从 2 降到 1;或者直接加早停(当验证 MAPE 连续 5 个 epoch 不下降就停止训练并回滚到最优权重)。这套源码里的训练脚本如果没有早停逻辑,我建议一定自己补上,它是防止过拟合的后悔药。
4.3 滑窗构造后样本顺序随机化导致预测结果失真
现象:验证集和测试集的 MAPE 都很好看,但部署到线上后效果一塌糊涂。
原因:你在源码里用了train_test_split(X, y, random_state=42)或DataLoader中shuffle=True作用于整个数据集——这两个操作都会把时间序列打乱。模型在训练时"见过"未来相邻时间点的信息,测试时相当于开卷考试。
解决:按时间顺序划分数据(参考 2.3 的代码),并且DataLoader只在训练集上设置shuffle=True,验证集和测试集加载时保持shuffle=False,保证评估时严格按时间推进模拟真实预测。
4.4 loss 变成 NaN
现象:训练到某一步,loss 直接变成nan。
原因:通常有三个:学习率过大导致梯度爆炸、输入数据里含有 NaN(滑窗构造时把缺失值窗口带进来了)、LSTM 的梯度值本身在长序列反向传播时容易出现梯度爆炸。
解决:先检查X_tensor和y_tensor是否包含 NaN(torch.isnan(X_tensor).any());再把学习率从 0.001 降到 0.0003;最后可以考虑对梯度做裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0),这个操作在时间序列训练里是常见的保底手段。
4.5 预测曲线滞后于真实负荷一天
现象:预测的负荷曲线形状和真实曲线一致,但整体向右平移了一天。
原因:这是多特征负荷预测里最经典的问题——模型学到的其实是"今天的负荷接近昨天同一时刻的负荷",而不是真正利用气温、节假日等信息给出前瞻性预测。深层原因是特征里load占了绝对主导,LSTM 直接把上一个周期的负荷值复制给了输出,而额外特征(气温、湿度、节假日)的权重被摊薄了。
解决:尝试在训练时把load这一个特征和其他特征分别归一化(或增大模型中其他特征的嵌入权重),或者在损失函数里对预测误差中"滞后部分"加大惩罚——具体做法是把预测序列的一阶差分也加入 loss 计算,迫使模型捕捉变化趋势而不是平移复制。也可以考虑把seq_len从 72 小时缩短到 24 小时,减少模型对"昨天数据"的依赖。这个坑不解决,你的多特征模型实际效果和单特征模型没有任何区别。
5. 评估与调优:用三个指标看清模型真实水平
5.1 MAPE、RMSE、R2 的PyTorch实现与业务含义
写完训练流程,接下来最关键的是评估。电力负荷预测在业务侧看的是"偏差了多少",MAPE 是行业里最通用的指标——它把误差换算成百分比,调度员和领导都能看懂。RMSE 则放大了大误差的惩罚,适合你判断模型是否在高峰负荷时段出现严重失守。R2 是统计学拟合优度,但在负荷预测上我一般作为辅助参考,不过度依赖。
def evaluate_model(model, X_tensor, y_tensor): model.eval() with torch.no_grad(): y_pred = model(X_tensor) # 如果做了归一化,需要反归一化才能计算业务指标 y_pred_inv = scaler.inverse_transform( np.concatenate([y_pred.numpy(), np.zeros((len(y_pred), 3))], axis=1) )[:, 0] y_true_inv = scaler.inverse_transform( np.concatenate([y_tensor.numpy(), np.zeros((len(y_tensor), 3))], axis=1) )[:, 0] # MAPE: 平均绝对百分比误差 mape = np.mean(np.abs((y_true_inv - y_pred_inv) / (y_true_inv + 1e-6))) * 100 # RMSE: 均方根误差 rmse = np.sqrt(np.mean((y_true_inv - y_pred_inv) ** 2)) # R2: 决定系数 ss_res = np.sum((y_true_inv - y_pred_inv) ** 2) ss_tot = np.sum((y_true_inv - np.mean(y_true_inv)) ** 2) r2 = 1 - ss_res / ss_tot print(f"MAPE: {mape:.2f}% | RMSE: {rmse:.2f} MW | R2: {r2:.4f}") return mape, rmse, r2注意scaler.inverse_transform这一步,因为我在第 2 章把整个特征矩阵一起归一化了,反归一化时要把预测的负荷列和零填充的其他列拼回去,才能还原到真实量纲。这个细节漏掉的话,你打印出来的 MAPE 是 0.002%,会让你误以为自己已经封神,实际上只是拿归一化数字算了个自嗨指标。MAPE在电力负荷上的合理区间:短期(未来1-24小时)负荷预测MAPE在2%到5%之间算良好,超过8%需要认真调参。
5.2 超参数的影响范围:一张表说清楚调参方向
调参是深度学习中看起来最像玄学的环节,但 LSTM 负荷预测的超参数影响有迹可循。我整理了这套源码里最值得调整的几个参数,按重要性排序:
| 参数 | 推荐范围 | 效果与调整方向 |
|---|---|---|
| seq_len | 24-168(小时) | 太短学不到负荷的日周期与周周期,太长引入噪声且训练变慢。先设为24或72 |
| hidden_size | 32-128 | 决定LSTM记忆容量。验证MAPE高且训练loss收敛慢(欠拟合)就加大 |
| num_layers | 1-3 | 2层是性价比之选。3层以上训练时间翻倍,易过拟合 |
| dropout | 0-0.5 | 验证集MAPE反弹时从0.2往上调,但超过0.5会让模型欠拟合 |
| lr | 0.0003-0.003 | 训练loss震荡时调低,loss下降极慢时调高,配合CosineAnnealing更稳 |
| batch_size | 32-128 | 数据集在万级样本时用64或128,更大数据集可上256 |
有一个易被忽略的联动:seq_len增大时,hidden_size也可以适当增大,因为更长的历史窗口需要更大的记忆容量来编码。反过来seq_len不变只加大hidden_size,收益很快会饱和。我一般先固定seq_len=72、hidden_size=64、num_layers=2跑一个基线,再单独动一个参数看验证集 MAPE 的变化,不要同时调多个参数,否则你无法判断是谁起了作用。
5.3 单特征 vs 多特征的对比实验:多出来的两个点MAPE从哪来
很多读者拿到源码后关心一个问题:所谓"多特征"到底比"只用负荷"强多少?我建议你在自己的数据集上做一次对照实验,只改feature_cols,其他训练配置完全不变。用同一个测试集评估两条路径:
- 单特征:
feature_cols = ['load'],input_size=1 - 多特征:
feature_cols = ['load', 'temperature', 'humidity', 'is_holiday'],input_size=4
我跑过多个电力数据集后得到的典型差距:多特征方案在平稳日 MAPE 只比单特征好 0.5 到 1 个百分点,但在气温骤变日(比如夏季高温预警或冬季寒潮)差距会拉到 3 到 5 个百分点。原因是单特征模型本质上只能做"历史相似日"的外推,而多特征模型能感知到气温的跳升预先拉升预测值。如果你的数据所在地区气候稳定、负荷季节性弱,多特征带来的提升不明显,这时候把精力放在调seq_len和hidden_size上更划算。做这个对比实验的另一个好处是,你能直观看到源码里每个模块的作用,而不是黑匣子式地跑完就扔。
6. 模型存档与向前一步:把最优权重固定下来
训练完模型后,最要紧的是把权重和scaler完整保存下来。只保存模型不保存scaler是新手最常见的失误——部署时你拿到的线上数据是新尺度的,没有scaler就无法把输入转换到训练时的分布。我会在同一目录下保存两个文件:
# 最优模型与scaler的保存 torch.save(model.state_dict(), 'lstm_load_forecast.pth') import joblib joblib.dump(scaler, 'scaler.save')加载时要注意:必须先恢复scaler,再用同样的参数重建模型结构,最后load_state_dict。社区里有同学把scaler.save丢了,只能从训练集重新估算归一化范围,预测结果偏差明显。更稳妥的做法是用torch.save({'model_state': model.state_dict(), 'config': {'input_size': 4, 'hidden_size': 64, ...}}, 'checkpoint.pth')把配置和权重打成一个包,加载时从config里重建模型,省去手动维护模型结构代码。
最后一个习惯:正式训练前固定随机种子。LSTM 的初始化权重和DataLoader的批次顺序都带随机性,不固定种子的话,同一次调参跑出的 MAPE 会在小数点后两位浮动,干扰你判断参数调整的效果。在训练脚本开头加三行:
torch.manual_seed(42) np.random.seed(42) random.seed(42)这套源码在电力负荷预测这个方向上算是性价比很高的起点,它的边界在于:模型本质上是统计模型,学不到"电网检修导致某区域短暂停电"这类突发零负荷事件;也没有用到 Transformer 这类更强的序列模型,如果你手里的数据量超过 10 万样本,可以换成 PyTorch 的TransformerEncoder做对照。把滑窗、归一化、反归一化和评估这四个环节摸透,换到任何时间序列预测任务(光伏功率、交通流量)你都能快速迁移。希望这份笔记帮你把坑填平,跑出靠谱的预测曲线。
本文还有配套的精品资源,点击获取