简介:基于LSTM的电力负荷预测项目实践资源,面向电力系统分析人员、机器学习学习者以及需要解决时间序列预测问题的算法工程师。项目围绕负荷数据特性,完整梳理长短期记忆网络建模流程,将天气、季节、节假日等影响因子融入特征处理,同时体现LSTM通过门机制控制信息流动、缓解长序列梯度问题的设计思路,帮助读者理解从业务问题到深度学习方案的转化过程。压缩包共三百五十个文件,约十一点三四兆,以一百七十个CSV数据文件为核心,配套十四个PY脚本完成数据清洗、归一化、特征工程、模型构建、超参数调优与评估,并含PNG可视化、模型检查点及元数据文件,目录结构便于按步骤复现。资源已有二千四百二十四人浏览学习,训练记录保留三千步至一万步不同阶段对应的误差对照,可用于权衡训练成本与精度。读者可获得能够直接运行的LSTM预测工程框架,覆盖数据预处理、训练验证、结果可视化的完整链路,适合作为电力负荷预测竞赛、课程设计或实际项目落地的参考基线。
1. 基于LSTM的电力负荷预测:这个AI方向解决什么问题
电力负荷预测不是什么新鲜课题,但把它交给LSTM来做,近几年几乎成了人工智能落地能源领域的标准入门动作。原因很简单:负荷数据是典型时间序列,早晚高峰、工作日与周末、季节更替这些规律既长又杂,传统ARIMA需要人工抽特征,而LSTM能直接从历史序列里学。这篇文章不跟你讲泛泛的神经网络原理,只讲怎么从一份原始用电记录出发,完成数据清洗、滑窗切片、模型训练、指标评估,并绕过我在实际项目中踩过的坑。无论你是做毕业设计,还是刚进电力行业做AI算法,照着这套流程都能在本地跑通一个能用的负荷预测模型。
2. 负荷数据预处理:把原始用电记录变成LSTM能吃的样本
很多人拿到数据就急着搭LSTM,结果模型死活不收敛,最后发现是原始数据里全是坑。电力负荷数据来自SCADA系统或智能电表,经常有采集缺失、通信跳变、人工补录等情况。预处理不是可有可无的步骤,它直接决定LSTM看到的是“规律”还是“噪声”。
2.1 缺失值与异常值清洗:先解决“脏数据”再谈模型
先说缺失值。常见做法是用前后时刻的均值插值,但如果连续缺失好几个小时,线性插值比均值更稳。异常值更麻烦:负荷曲线在凌晨突然飙到三倍,多半是电表故障或数据上报错误,不是真实用电。我用一个简单阈值法:先算每个点与前后两天同一时刻的差值,超过该时刻均方差的3倍就判为异常,替换成中位数。
import pandas as pd import numpy as np df = pd.read_csv('load.csv', parse_dates=['timestamp'], index_col='timestamp') df = df.resample('15min').mean() # 统一到15分钟间隔 # 缺失值:连续缺失超过2个点用线性插值,否则用邻值填充 df['value'] = df['value'].interpolate(limit=8, limit_direction='both') df['value'] = df['value'].fillna(method='ffill') # 兜底 # 异常值:与前后两天同时刻作差 s = df['value'].copy() base = (s.shift(96) + s.shift(-96)) / 2 # 96个15分钟 = 1天 diff = (s - base).abs() threshold = 3 * (diff.rolling(96, min_periods=1).std()) df['value'] = s.where(diff < threshold, base)这段代码里有几个讲究。resample('15min')先把数据对齐到固定频率,后续滑窗才不会因为时间戳错位而出问题。interpolate(limit=8)表示最多连续插值8个点,也就是2小时,再多就说明采集系统有大故障,直接填充默认值更安全。异常值检测里shift(96)是硬编码的:因为一天的15分钟点数正好是96,如果你的数据是小时级,这里要改成24。
清洗完成后一定要画一遍曲线,肉眼扫一遍有没有“断崖”或“尖刺”。我见过有人用3σ标准去筛负荷,结果把春节假期真实的高负荷当成异常洗掉了。所以阈值不要拍脑袋,先看历史数据分布。
2.2 归一化方法选择:MinMax与Z-Score哪个适合负荷
LSTM用tanh和sigmoid做激活函数,输入数值范围过大或过小都会让梯度消失。负荷数据的特征是:有量纲、正数、有时出现瞬时尖峰。MinMax归一化能把数据压到[0,1],适合负荷这种有明确上下界的序列;但如果数据里有极端尖峰,MinMax会把正常段的数值压缩得很小。Z-Score对尖峰鲁棒性更好,但归一化后的值没有上下界,LSTM最后全连接层的输出可能会摆动。
我的默认选择是MinMax,但要对尖峰做截断:取99.9%分位数作为上限,超过的算异常。注意归一化必须只用训练集的统计量,测试集只能调用训练集已经算好的min和max,否则会引入未来信息,相当于作弊。
from sklearn.preprocessing import MinMaxScaler upper = np.percentile(train['value'], 99.9) lower = train['value'].min() train_clean = train['value'].clip(lower=lower, upper=upper) scaler = MinMaxScaler(feature_range=(0, 1)) scaler.fit(train_clean.values.reshape(-1, 1)) train_scaled = scaler.transform(train_clean.values.reshape(-1, 1)) test_scaled = scaler.transform(test['value'].values.reshape(-1, 1))clip(lower=lower, upper=upper)这一步极其重要。你不提前截断,MinMax会认为那个尖峰是正常最大值,把所有真实高负荷段压到0.5附近,模型训练时特征区分度肉眼可见地下降。scaler.fit只能喂训练集,测试集用同一个scaler直接transform,这是时间序列预测的底线。
2.3 滑窗切片与训练集划分:时间序列不能随机打乱
LSTM输入是固定长度的窗口。比如用过去48个小时的负荷预测未来15分钟,窗口长度就是192(如果数据是15分钟频率)。滑窗切片就是一个移动的截取动作:窗口从0到191,预测192;然后窗口从1到192,预测193。这里有个新手必翻车的点:训练集和测试集必须按时间顺序切,不能像图像分类那样随机打乱。随机打乱等于让模型看到了“未来”的统计数据,验证指标会虚高。
def create_sequences(data, seq_len=96, pred_len=1): xs, ys = [], [] for i in range(len(data) - seq_len - pred_len + 1): xs.append(data[i:i + seq_len]) ys.append(data[i + seq_len:i + seq_len + pred_len]) return np.array(xs), np.array(ys) # seq_len=96表示用过去一天(96个15分钟)预测下一时刻 X_train, y_train = create_sequences(train_scaled.squeeze(), seq_len=96) X_test, y_test = create_sequences(test_scaled.squeeze(), seq_len=96) # 保持时间顺序,不要shuffleseq_len不是拍脑袋定的。如果数据是15分钟粒度,96就是一天,可以考虑;如果是1小时粒度,24就是一天。做负荷预测至少要包含一个完整日周期,不然LSTM学不到“今天下午2点和昨天下午2点用电相似”这种规律。pred_len设为1就是单步预测,设成n就是n步预测,但多步预测的误差会随着步长指数放大,后面会专门讲这个问题。
训练集、验证集、测试集的比例我习惯用7:1:2,验证集放在训练集之后、测试集之前,三个集合不允许有重叠时间点。特别注意:测试集必须是最后一段连续时间,不能随机抽取,这样才能模拟“模型在当前时刻预测未来”的真实场景。
3. 用PyTorch实现LSTM负荷预测:模型结构与训练代码
这一章是核心动作。我直接给出能跑的PyTorch代码,并解释每个参数为什么这么设。你跑通之后,调整维度就能用到自己的数据上。
3.1 网络搭建:从nn.LSTM到全连接输出层
LSTM层负责提取时间依赖,但最后要输出一个具体负荷数值,所以后面必须接一个全连接层。常见做法是:输入(batch, seq_len, 1)经过LSTM后,取最后一个时刻的hidden state,再通过Linear输出预测值。为什么不取所有时刻的输出?因为我们要预测的是未来,不是序列标注,最后时刻的hidden已经编码了整个窗口的信息。
import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, output_size=1): super().__init__() self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): # x shape: (batch, seq_len, input_size) out, (h_n, c_n) = self.lstm(x) # out: (batch, seq_len, hidden_size) last_out = out[:, -1, :] # 取最后一个时刻的输出 return self.fc(last_out)hidden_size是LSTM记忆容量,负荷预测这种单变量序列,64或128足够。num_layers堆叠两层能捕捉不同时间尺度的模式,但更多层容易过拟合,而且训练慢一倍。batch_first=True是为了让输入形状更直观,尺寸是(batch, seq_len, features),不用去记输入转置有没有做对。输入特征数input_size=1是因为我们处理的是单变量负荷;后面进阶做多变量时改成2或3。
3.2 训练参数:batch_size、学习率与epoch如何设置
很多教程直接写optimizer.Adam(model.parameters(), lr=0.001),但在负荷预测任务上这个默认学习率经常不够稳。我的经验是:如果数据量小于几万条,Adam的默认0.001偏大,容易在损失曲线上看到“锯齿”震荡。先用0.0005,跑几十个epoch不降再调大。
batch_size直接影响梯度噪声。时间序列数据有连续性,batch喂太大,模型会偏向短期均值;喂太小,训练波动大。我一般用32或64,窗口长度96时,64个batch对显存要求很低。epoch不要一开始就设200,配合早停,通常跑50~100个epoch就能看到结果。
model = LSTMPredictor(input_size=1, hidden_size=64, num_layers=2, output_size=1) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.0005) X_train_t = torch.from_numpy(X_train).float().unsqueeze(-1) # (样本数, 96, 1) y_train_t = torch.from_numpy(y_train).float().unsqueeze(-1)unsqueeze(-1)是在最末尾加一维,把 (样本数, 96) 变成 (样本数, 96, 1),这样才能匹配input_size=1。如果你忘了这步,PyTorch会直接报三维输入不匹配的错。
3.3 训练循环与早停:用验证集防止过拟合
训练循环本身不复杂,但负荷预测里验证集的作用不只是看看指标,它决定你什么时候停。我见过很多人用固定epoch训练,结果验证集损失在第30个epoch就开始上升,硬是跑完100个epoch,最终模型把噪声都背下来了。早停的做法是:每个epoch算验证集损失,如果连续5个epoch没有改善,就恢复到历史最优的模型权重。
best_val_loss = float('inf') patience = 5 wait = 0 for epoch in range(100): model.train() train_loss = 0 for i in range(0, len(X_train_t), batch_size): X_batch = X_train_t[i:i+batch_size] y_batch = y_train_t[i:i+batch_size] optimizer.zero_grad() pred = model(X_batch) loss = criterion(pred, y_batch) loss.backward() optimizer.step() train_loss += loss.item() # 验证集 model.eval() with torch.no_grad(): val_pred = model(X_val_t) val_loss = criterion(val_pred, y_val_t).item() if val_loss < best_val_loss: best_val_loss = val_loss best_state = model.state_dict() wait = 0 else: wait += 1 if wait >= patience: model.load_state_dict(best_state) print(f'Early stop at epoch {epoch}') breakmodel.train()和model.eval()必须成对出现。LSTM层的dropout在train模式下才生效,eval模式下自动关闭,如果你忘记切换,验证集损失会被扰动得忽高忽低,早停跟着误判。patience=5是个保守值,如果你的数据噪声大,可以放到7;相反,如果验证集损失一直在降,只是降得慢,不要触发早停,让它跑完。
4. 从预测曲线到评估指标:单步预测的落地验证
模型训练完,你得到的是归一化范围内的数字,必须反归一化转回实际的兆瓦(MW)或千瓦(kW)量级,才能评估误差。这一章讲清楚评价指标怎么算、预测结果怎么画,以及单步预测和多步预测在业务上的区别。
4.1 反归一化与评价指标:MAE、RMSE、MAPE怎么算
反归一化很简单:pred_actual = pred_scaled * (upper - lower) + lower,注意这里upper和lower是训练集截断后的分位数,不是测试集的最大最小值。如果搞混,误差计算会凭空多出几个百分点。
from sklearn.metrics import mean_absolute_error, mean_squared_error pred_np = pred.cpu().numpy().squeeze() true_np = y_test.squeeze().squeeze() # 反归一化 pred_actual = pred_np * (upper - lower) + lower true_actual = true_np * (upper - lower) + lower mae = mean_absolute_error(true_actual, pred_actual) rmse = np.sqrt(mean_squared_error(true_actual, pred_actual)) mape = np.mean(np.abs((true_actual - pred_actual) / true_actual)) * 100MAPE在负荷预测里有个坑:凌晨低谷负荷接近0,分母很小,一点点绝对误差就会放大成巨大的百分比。我看指标时先看MAE和RMSE,MAPE只作为参考。如果测试集里包含夜间时段,MAPE能飙到几十,这不代表模型不行,而是评价指标本身对低基数不友好。这时候可以用MAE / 平均负荷作为替代百分比误差。
4.2 预测结果可视化:一眼看出模型是否“偷懒”
数值指标再好,也必须画图。负荷预测最常见的问题是“滞后性”:预测曲线比真实曲线晚一个采样周期。画图能直接看出这个现象。我会选取测试集里连续3天,把真实负荷和预测负荷画在一起,分工作日和周末对比。
import matplotlib.pyplot as plt plt.figure(figsize=(12, 5)) plt.plot(true_actual[:288], label='Actual', linewidth=1.5) plt.plot(pred_actual[:288], label='Predicted', linewidth=1.5, linestyle='--') plt.legend() plt.xlabel('15min points (3 days)') plt.ylabel('Load (MW)') plt.title('Single-step LSTM Load Forecast') plt.show()取前288个点代表3天(96×3),如果预测曲线整体向右偏移,就说明模型学成了“上一时刻的复制品”,而不是真正的预测。这种滞后现象在单步预测里极其常见,原因是LSTM在最小化MSE时发现,历史最后一个值本身就是对下一时刻的最好估计,于是模型偷懒了。解决思路在第5章。
4.3 单步预测与多步预测的差别:业务上该用哪种
单步预测是“用过去96个点预测下1个点”,每次预测完,把真实值滑进窗口再测下一天。这在评估模型能力时是公平的,但在实际业务里不实用。调度员想知道未来24小时,最多168小时的负荷曲线,他们需要多步预测。多步预测有两种做法:递推多步和直接多步。递推多步是把预测值当成下一步的输入,误差会累积;直接多步是让LSTM输出多个值,改最后一层Linear的output_size,但会丢失时间相关性。
我的建议是:项目验证阶段用单步,因为指标干净,便于模型对比;上线阶段如果非要多步,常用是Seq2Seq结构或Transformer,但那是后话。这篇基于LSTM的实践,先守住单步预测这个基线,再谈扩展。
5. LSTM电力负荷预测避坑指南:5个高频翻车现场
这一章写我实际调参和给别人review代码时反复见到的问题。每条都按“现象→原因→解决”来写,可以直接对照你的报错和预测曲线。
5.1 预测曲线滞后严重:模型学成了“上一个值”
现象:预测曲线整体比真实曲线晚一个点,峰值和谷值都错位,但误差却不大。 原因:LSTM在MSE损失下发现,输出历史最后一个值是最低成本的预测,尤其当数据有强自相关性时。这本质是模型退化,不是网络结构问题。 解决:第一,加入差分输入,把负荷增量作为额外特征,强制模型关注变化率;第二,调整滑窗,让窗口末尾不要紧贴预测点,比如留出几个点的间隔;第三,将单步预测的目标从“下一时刻值”改成“未来k步的值”,扩展时间跨度。
5.2 反归一化后预测值被压扁:整段曲线都偏小
现象:预测结果画出来是一条水平直线,数值大概在训练集的平均值附近。 原因:最常见是归一化时用了整个数据集(包括测试集)的min和max,导致测试集被压缩到很小的区间,模型输出的方差被缩小。另一种是训练时数据里有极端尖峰,MinMax被拉宽,正常段数值都接近0。 解决:严格只用训练集做fit,测试集只做transform。对尖峰做clip,设99.9%分位数为上限,防止归一化区间被异常值挟持。
5.3 验证集损失先降后涨:早停参数设得太晚
现象:前20个epoch验证损失稳定下降,之后突然掉头上升,训练损失还在降。 原因:模型开始过拟合训练噪声。如果patience设成10或更大,早停会犹豫很久,最终保留的是已经过拟合的权重。 解决:把patience设为5~7,并且每5个epoch打印一次训练/验证损失,先手动观察趋势再定阈值。我一般还会限制LSTM层数不超过3层,hidden_size不超过256,从结构上降低过拟合空间。
5.4 训练速度慢且不收敛:学习率与LSTM层数互相牵制
现象:损失曲线长时间不下降,或者一降就跳成NaN。 原因:学习率偏大,梯度在LSTM这种链式结构里容易爆炸。多层LSTM如果没有dropout,梯度传播不稳定。 解决:先用0.0001做测试,如果损失能降再调到0.0005;如果NaN,就降低学习率到0.00001。同时检查输入数据是否包含NaN或无穷值,归一化后如果还有,清洗步骤没做干净。
5.5 节假日与极端天气预测偏差大:单变量模型的天花板
现象:平时误差5%,春节假期误差飙到20%,台风天更惨。 原因:单变量LSTM只见过“历史负荷”,没见过日历和天气。节假日负荷模式和普通工作日完全不同,模型从历史序列里找不到相似样本。 解决:加入外部变量,比如星期几、是否节假日、温度、湿度。在输入端把input_size从1改成多维,比如负荷、温度、节假日编码三路输入。这一招能从根上缓解,但数据采集和特征对齐的活会多不少。这也是从“能跑通”走向“能用”的必经一步。
6. 进阶:让LSTM预测更实用的三个技巧
最后聊几个不增加太多代码量却能明显提升效果的技巧。第一个是多变量输入,把温度、湿度、星期几拼到特征里。做法是输入张量从 (batch, seq_len, 1) 变成 (batch, seq_len, 4),LSTM的input_size同步改成4。注意外部特征也要归一化,且必须和负荷用同一个时间索引对齐,否则模型学到的是错位因果。
第二个是双向LSTM。虽然负荷预测是因果任务,理论上不能用未来,但如果你做的是事后分析或填补历史数据,双向结构能利用前后文,误差通常比单向低10%~20%。如果是线上实时预测,别用双向。
第三个是模型融合。训练一个LSTM和一个LightGBM,LSTM擅长捕捉连续趋势,LightGBM擅长处理节假日、星期几这种离散特征,两者加权平均通常比单个模型稳。我试过最简单的做法:LSTM预测值乘0.7,GBM预测值乘0.3,RMSE比单模型降了8%左右。
我的习惯是:每次改完模型,先跑测试集第1天到第7天,画图看峰谷;再看MAE和RMSE;最后才盯着MAPE较真。做负荷预测久了就会明白,指标只是参考,真正考验你的是凌晨两点数据采集断了、第二天调度员拿着你的预测值去排机组——那种时候你就会感谢自己当初把预处理和避坑步骤做扎实了。希望这篇基于LSTM的电力负荷预测实践能帮到你,哪怕只是少踩一个坑,也值了。
本文还有配套的精品资源,点击获取