简介:这份PDF论文面向电力系统调度、电力市场交易及新能源并网领域的研究人员与工程技术人员,聚焦短期电力负荷预测这一关键问题。针对传统统计学方法对负荷平稳性要求高、普通机器学习难以兼顾负荷时序依赖与多因素非线性影响的局限,论文提出基于长短期记忆单元(LSTM)构建深度循环神经网络的预测方法,可灵活定义历史窗口期、灵活添加负荷影响因素,并借助遗忘门、输入门、输出门三个门控单元实现对长距离时序依赖的建模。资源包共1个PDF文件,大小约1.45MB,内容涵盖RNN与LSTM结构原理、负荷预测模型基本思路、不同历史时间窗口与网络架构的性能验证及与其他算法的对比实验。目前已有479人学习,适合希望掌握深度学习时序建模方法、提升负荷预测准确性与稳定性的读者参考,可从中获取完整的建模思路、实验设计与算法对比分析。
1. 短期电力负荷预测为什么总在晚高峰翻车:LSTM 能补上哪块短板
做过电网侧负荷预测的同行大多有过类似经历:白天曲线拟合得漂漂亮亮,一到 18 点到 21 点的晚高峰,预测值就明显偏低,第二天调度计划被迫临时调整。问题往往不在模型不够深,而在于传统方法把负荷序列当成若干独立点的组合,忽略了「前一小时的状态会持续影响后一小时」这种时间依赖。短期电力负荷预测要解决的,正是未来 1 小时到 1 天这个尺度上,如何用历史负荷、温度、星期属性等输入,把下一段曲线尽量贴近真实值。LSTM(长短期记忆网络)作为循环神经网络的一种改进结构,靠输入门、遗忘门、输出门三个机制,把「该记多久、该忘什么」变成可学习参数,天然适合处理这种带周期性和突变性的时间序列。这篇笔记面向已经会写 Python、想把这套方案真正跑起来的人:从数据构造、模型搭建、训练调参到上线前验证,一步步拆开讲,顺带把那些让我返工好几次的坑标出来。
2. 从原始负荷表到 LSTM 可吃的张量:数据管道怎么搭
2.1 先想清楚 LSTM 到底在学什么
LSTM 单元在每个时间步接收两个东西:当前时刻的输入向量 x_t,以及上一时刻传下来的隐藏状态 h_{t-1} 和细胞状态 C_{t-1}。它内部做四件事——遗忘门决定 C_{t-1} 里哪些信息丢掉,输入门决定当前输入里哪些写进细胞状态,然后更新细胞状态,输出门决定这一步对外吐出什么隐藏状态。放到负荷预测场景里,这意味着模型不是孤立地看「今天 19 点多少度、星期几」,而是会结合「过去 24 小时负荷怎么爬升、昨天同一时段什么水平」来推断。这也是它比单纯的全连接网络或 ARIMA 更适合短期负荷的原因:ARIMA 对线性趋势和固定周期还行,遇到气温骤降导致的负荷跳变就容易失准,而 LSTM 的门控结构对非线性突变更宽容。
选型上还有几个现实考量。如果只做单步预测(预测下一小时),单层 LSTM 加一个全连接输出层通常够用;如果要做未来 24 小时的多步预测,要么用 Seq2Seq 结构,要么把输出维度直接设成 24,让模型一次性吐出整段曲线。我一般倾向后者,因为实现简单、训练稳定,代价是输出之间缺少显式依赖约束,后面可以用平滑后处理补。
2.2 数据清洗与特征工程的具体做法
拿到手的负荷数据通常是 15 分钟或 1 小时粒度的 CSV,列里可能有时间戳、有功负荷、温度、湿度、节假日标记。第一步不是急着喂模型,而是处理三类脏数据:缺失值、异常值、时间对齐。
缺失值常见于采集设备掉线,连续缺几个点可以用线性插值,缺一大段(比如超过 6 小时)建议直接标记为无效样本,别硬填。异常值用滑动窗口的 3σ 或 IQR 判断,负荷突然从 800MW 跳到 2000MW 又跳回来,基本是采集故障,按前后均值修正。
特征方面,除了原始负荷,我一般会构造这几类:小时、星期、是否节假日这类时间特征做 one-hot 或 sin/cos 周期编码;温度、体感温度直接归一化;过去 24 小时和过去 7 天同一时刻的负荷作为滞后特征。注意滞后特征在训练集和测试集之间不能穿越,否则会引入未来信息。
import pandas as pd import numpy as np def build_features(df, load_col='load', temp_col='temp'): df = df.copy() df['hour'] = df.index.hour df['weekday'] = df.index.weekday # 周期编码,避免 23 点和 0 点被当成距离很远 df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24) df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24) df['weekday_sin'] = np.sin(2 * np.pi * df['weekday'] / 7) df['weekday_cos'] = np.cos(2 * np.pi * df['weekday'] / 7) # 滞后特征:过去 24 小时、过去 7 天同一时刻 df['lag_24'] = df[load_col].shift(24) df['lag_168'] = df[load_col].shift(168) # 滑动统计 df['roll_mean_24'] = df[load_col].rolling(24).mean() df['roll_std_24'] = df[load_col].rolling(24).std() df = df.dropna() return df这段代码的关键点在于:周期编码用 sin/cos 而不是直接给 0-23 的整数,是因为整数会让模型误以为 23 点和 0 点相隔很远,而实际上它们是相邻的。lag_24 和 lag_168 分别捕捉日周期和周周期,roll_mean_24 给模型一个近期基线参考。dropna 会丢掉前 168 行,这是构造滞后特征的必然代价,数据量少的时候要权衡是否保留 lag_168。
2.3 归一化与滑动窗口切样本
负荷值动辄几百上千 MW,直接喂网络会导致梯度爆炸,必须归一化。我一般用 MinMax 缩放到 [0,1],注意 scaler 只能在训练集上 fit,然后 transform 测试集,否则就是数据泄漏。
滑动窗口是把长序列切成 (输入长度, 特征数) 的样本。假设用过去 48 小时预测未来 1 小时,窗口就是 48,步长为 1。
from sklearn.preprocessing import MinMaxScaler def make_windows(data, target_col_idx, window=48, horizon=1): X, y = [], [] for i in range(len(data) - window - horizon + 1): X.append(data[i:i+window]) y.append(data[i+window:i+window+horizon, target_col_idx]) return np.array(X), np.array(y) scaler = MinMaxScaler() train_scaled = scaler.fit_transform(train_df.values) test_scaled = scaler.transform(test_df.values) target_idx = train_df.columns.get_loc('load') X_train, y_train = make_windows(train_scaled, target_idx, window=48, horizon=1) X_test, y_test = make_windows(test_scaled, target_idx, window=48, horizon=1)window=48 表示回看两天,horizon=1 表示预测下一小时。如果做 24 小时预测,把 horizon 改成 24,y 的形状会变成 (样本数, 24)。这里有个容易忽略的点:归一化是对所有列一起做的,包括温度、滞后负荷,所以 scaler 的维度要和特征数一致,预测时反归一化只取负荷那一列。
3. PyTorch 搭 LSTM 预测模型:层数、隐藏维度和训练循环怎么定
3.1 网络结构设计与参数含义
PyTorch 的 nn.LSTM 几个核心参数:input_size 是每个时间步的特征数,hidden_size 是隐藏状态维度,num_layers 是堆叠层数,batch_first=True 让输入形状变成 (batch, seq, feature),符合直觉。输出有两个,output 是每个时间步的隐藏状态,h_n 和 c_n 是最后一步的,做单步预测时通常取 output 的最后一个时间步。
隐藏维度怎么定?我试过 32、64、128、256,在几千到几万条样本的量级上,64 到 128 通常性价比最高。再大容易过拟合,尤其数据只有一两年的时候。层数上,单层够用,两层能捕捉更复杂的时序模式,但训练时间翻倍,且需要更多数据支撑。dropout 加在层间,0.1 到 0.3 之间。
import torch import torch.nn as nn class LoadLSTM(nn.Module): def __init__(self, input_size, hidden_size=64, num_layers=1, output_size=1, dropout=0.1): super().__init__() self.hidden_size = hidden_size self.num_layers = num_layers 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 ) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): # x: (batch, seq_len, input_size) out, (h_n, c_n) = self.lstm(x) # 取最后一个时间步的输出 last = out[:, -1, :] return self.fc(last)注意 dropout 只在 num_layers > 1 时生效,单层加 dropout 在 PyTorch 里会报警告。fc 层把 hidden_size 映射到 output_size,单步预测就是 1,多步就是 24。forward 里取 out[:, -1, :] 是标准做法,因为最后一步的隐藏状态已经聚合了整个窗口的信息。
3.2 训练循环与损失函数选择
损失函数用 MSELoss 最常见,但电力负荷里晚高峰的绝对误差往往比凌晨大,如果业务更关心高峰段精度,可以考虑对高峰样本加权,或者用 MAE 降低对异常值的敏感度。优化器 Adam 起步,学习率 1e-3,配合 ReduceLROnPlateau 在验证损失不降时减半。
from torch.utils.data import DataLoader, TensorDataset device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = LoadLSTM(input_size=X_train.shape[2], hidden_size=64, num_layers=1).to(device) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=5, factor=0.5) train_loader = DataLoader( TensorDataset(torch.FloatTensor(X_train), torch.FloatTensor(y_train)), batch_size=64, shuffle=True ) for epoch in range(100): model.train() total_loss = 0 for xb, yb in train_loader: xb, yb = xb.to(device), yb.to(device) optimizer.zero_grad() pred = model(xb) loss = criterion(pred, yb) loss.backward() # 梯度裁剪,防止 LSTM 梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() total_loss += loss.item() scheduler.step(total_loss)batch_size=64 是常见起点,样本少就降到 32。梯度裁剪 max_norm=1.0 对 LSTM 几乎是必备的,因为循环结构反向传播时梯度容易累积爆炸,不加这个经常训练几个 epoch 后 loss 变 NaN。shuffle=True 打乱样本顺序,避免模型学到样本排列的伪规律。
3.3 验证集划分与早停
时间序列不能随机划分验证集,否则未来信息会泄漏到训练里。正确做法是按时间顺序切:前 70% 训练,中间 15% 验证,最后 15% 测试。早停策略是验证损失连续 10 个 epoch 不降就停,保存验证损失最低的模型权重。
best_val_loss = float('inf') patience_counter = 0 patience = 10 for epoch in range(100): # ... 训练代码 ... model.eval() with torch.no_grad(): val_pred = model(torch.FloatTensor(X_val).to(device)) val_loss = criterion(val_pred, torch.FloatTensor(y_val).to(device)).item() if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), 'best_lstm.pth') patience_counter = 0 else: patience_counter += 1 if patience_counter >= patience: break这套流程跑下来,在典型的地市级负荷数据上,单步预测的 MAPE 通常能压到 2% 到 4%,比 ARIMA 的 5% 到 8% 有明显改善。但别只看 MAPE,晚高峰时段的误差单独统计一下,那才是调度真正关心的。
4. 预测结果不达标时先查这几处:LSTM 负荷预测的避坑清单
4.1 现象:训练 loss 正常下降,验证 loss 却一路飙升
原因:典型过拟合,常见于数据量少但 hidden_size 开太大,或者 num_layers 堆到 3 层以上。另一个隐蔽原因是特征里混入了未来信息,比如归一化时用了全量数据的 min/max。
解决:先把 hidden_size 降到 32 或 64,num_layers 回到 1,加 dropout 0.2。然后检查 scaler 是否只在训练集 fit。如果还不行,减少特征数量,把相关性低的温度、湿度先去掉,只留负荷滞后和时间特征。
4.2 现象:预测曲线整体滞后真实值一个到两个小时
原因:模型倾向于输出「上一时刻的值」作为预测,尤其在负荷变化平缓时,MSE 损失会奖励这种保守策略。窗口太长也会加剧这个问题,因为模型被太多历史信息淹没。
解决:缩短输入窗口,从 48 降到 24 试试。损失函数换成 MAE 或 Huber,对偏差更敏感。还可以在特征里加入负荷的一阶差分,让模型直接看到变化趋势。
4.3 现象:晚高峰预测值系统性偏低
原因:训练样本里高峰时段占比小,模型没学够。另外温度特征如果只用了当前时刻,模型无法感知「持续高温导致负荷累积」这种效应。
解决:对高峰时段样本加权,比如 18 点到 21 点的样本 loss 乘以 2。温度特征加入过去 3 小时的滑动平均。如果数据里有节假日标记,确保它被正确编码,因为节假日高峰形态和工作日完全不同。
4.4 现象:多步预测时后面几步误差急剧放大
原因:直接多输出(output_size=24)时,各步之间没有依赖约束,模型对远期预测趋于保守或发散。Seq2Seq 结构如果解码器训练不充分也会这样。
解决:改用滚动预测,每次只预测一步,把预测值填回输入窗口再预测下一步。代价是误差会累积,但短期(6 小时内)通常比直接多输出稳。或者用 Seq2Seq 加 teacher forcing,训练时按一定概率喂真实值。
4.5 现象:换一批新数据后模型完全失效
原因:负荷模式随季节、经济结构变化而漂移,年初训练的模型到夏天可能就不准了。归一化参数如果固定不变,新数据的分布偏移会让输入超出训练时的范围。
解决:建立定期重训机制,比如每月用最近 12 个月数据重新训练。归一化参数用滚动窗口更新,或者改用对分布偏移更鲁棒的标准化方法。上线后监控预测误差,超过阈值就触发告警。
5. 把 MAPE 再压一个点:几个我反复验证过的调优技巧
模型能跑通只是起点,真正拉开差距的是细节。第一个技巧是残差建模:先用 LSTM 预测一个基线,然后把残差(真实值减预测值)拿出来,看它是否还有规律。如果残差在特定时段呈现周期性,说明模型漏掉了某个特征,把它补进去往往能直接降 0.5 到 1 个百分点。我一般会画残差的 ACF 图,如果滞后 24 小时还有显著相关,就说明日周期没学干净。
第二个技巧是多模型融合。LSTM 对突变敏感,但对平稳段有时不如简单的持久性模型(直接用昨天同一时刻的值)。把 LSTM 输出和持久性模型的输出按验证集误差加权平均,权重用逆误差法,通常比单模型稳。具体做法是:在验证集上分别算两个模型的 MAPE,权重 w = (1/MAPE_lstm) / (1/MAPE_lstm + 1/MAPE_persist),然后加权求和。
第三个技巧是输入窗口的精细选择。别默认用 24 或 48,用验证集做网格搜索,试 12、24、36、48、72,看哪个窗口的验证 MAPE 最低。不同地区、不同季节的最优窗口可能不一样,我见过夏季最优是 36、冬季最优是 24 的情况,因为冬季负荷对近期温度更敏感。
第四个技巧是学习率预热。LSTM 训练初期梯度不稳定,前 5 个 epoch 用线性预热,从 1e-5 升到 1e-3,之后再正常衰减。这个改动让我的模型收敛更稳,最终 MAPE 平均降了 0.3 个点。
# 学习率预热示例 def warmup_lr(epoch, warmup_epochs=5, base_lr=1e-3): if epoch < warmup_epochs: return base_lr * (epoch + 1) / warmup_epochs return base_lr for epoch in range(100): lr = warmup_lr(epoch) for param_group in optimizer.param_groups: param_group['lr'] = lr # ... 训练 ...验证方法上,除了 MAPE,我建议加一个高峰时段命中率指标:预测值落在真实值 ±5% 范围内的样本占比,单独统计 18 点到 21 点。这个指标比整体 MAPE 更能反映调度可用性。上线前用最近三个月的数据做滚动回测,每周重训一次,看指标是否稳定。
最后说个血泪教训:别在特征工程没做干净的时候就去调模型超参,我曾经花了两天调 hidden_size 和层数,最后发现是归一化时把测试集数据混进去 fit 了 scaler,修掉之后模型直接好了。先保证数据管道正确,再谈调优。希望帮到你。
本文还有配套的精品资源,点击获取