简介:这份PDF文档面向电力系统从业者、深度学习入门者及科研人员,系统讲解如何用PyTorch实现LSTM-GRU混合模型完成电力负荷预测。内容从电力负荷预测的基本概念与分类讲起,逐步深入RNN、LSTM、GRU的原理与对比,再展开混合模型的提出动机、结构设计与工作流程,并完整覆盖PyTorch环境搭建、数据收集与预处理、模型定义、训练调优、评估指标计算及结果可视化等环节,最后给出实际应用案例与效益分析。资源包为1个PDF文件,大小约2.22MB,共48页,支持目录章节跳转与阅读器左侧大纲快速定位,图表、目录等元素显示正常。目前已有64人学习下载。读者可借此掌握从数据清洗、归一化到模型保存部署的全流程实现思路,理解MSE、RMSE、MAE、MAPE等评估指标的应用,并参考梯度消失、过拟合等常见问题的解决策略,适合作为电力负荷预测项目的实践参考。
1. 为什么电力负荷预测开始流行 LSTM-GRU 级联结构
很多做时序预测的团队都遇到过同一个尴尬:单层 LSTM 在负荷平稳段拟合得很好,一到节假日切换、气温骤降那几天就开始飘;换成 GRU 训练是快了,可遇到长周期的周内规律又抓不住。电力负荷本身是"长期趋势 + 短期波动"叠加的信号,单一门控结构很难同时兼顾这两头。这份 48 页的文档给出的思路是把 LSTM 和 GRU 级联起来:先让 LSTM 层提取长期依赖,再把它的隐藏状态喂给 GRU 层去细化短期动态,最后接全连接层输出预测值。它适合已经会写 PyTorch 训练循环、但模型精度卡在 MAPE 3% 上下下不去的从业者,也适合想找一个完整可复现案例来练手混合模型的新手。下面按原理、数据、实现、调优、排错的顺序把它拆开讲。
2. LSTM 与 GRU 的门控差异和级联动机
2.1 从 RNN 的梯度问题说起
普通 RNN 的隐藏状态递推是 $h_t = \sigma(W_{hh}h_{t-1} + W_{xh}x_t + b_h)$,反向传播时梯度要沿着时间步连乘,序列一长就指数衰减或爆炸。LSTM 用三个门加一个记忆单元解决这个问题:遗忘门 $f_t$ 决定旧记忆保留多少,输入门 $i_t$ 决定新信息写入多少,输出门 $o_t$ 控制记忆单元暴露多少到隐藏状态。GRU 把遗忘门和输入门合并成一个更新门 $z_t$,再加一个重置门 $r_t$,参数少了约三分之一。
| 对比项 | LSTM | GRU |
|---|---|---|
| 门控数量 | 3(输入/遗忘/输出) | 2(更新/重置) |
| 状态变量 | 隐藏状态 + 记忆单元 | 仅隐藏状态 |
| 参数量(同隐藏维度) | 约 4 组权重矩阵 | 约 3 组权重矩阵 |
| 长依赖捕捉 | 更强,门控更精细 | 稍弱,但训练更快 |
| 典型适用 | 长周期、复杂模式 | 短序列、资源受限 |
2.2 级联结构为什么比并联或加权平均更省事
文档里采用的是串行级联:输入序列先过 LSTM 层,LSTM 每个时间步的隐藏状态输出直接作为 GRU 层对应时间步的输入。这样做的逻辑是分工——LSTM 负责把周规律、季节趋势这类慢变量编码进隐藏状态,GRU 在这个已经"提纯"过的表示上再学日内的峰谷波动。相比并联后加权融合,级联不需要额外设计融合权重,梯度路径也更短,调参时少一个维度。代价是层数变深,训练时更容易过拟合,所以后面要配合 Dropout 和早停。
2.3 用 PyTorch 定义这个混合模型
import torch import torch.nn as nn class LSTMGRUModel(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=1, output_size=1, dropout=0.2): super().__init__() # LSTM 层:负责提取长期依赖,batch_first=True 让输入形状为 (batch, seq, feature) self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=dropout if num_layers > 1 else 0) # GRU 层:接收 LSTM 的隐藏状态序列,进一步学习短期动态 self.gru = nn.GRU(hidden_size, hidden_size, num_layers, batch_first=True, dropout=dropout if num_layers > 1 else 0) self.dropout = nn.Dropout(dropout) # 全连接层:把最后一个时间步的隐藏状态映射到预测值 self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): lstm_out, _ = self.lstm(x) # lstm_out: (batch, seq, hidden) gru_out, _ = self.gru(lstm_out) # gru_out: (batch, seq, hidden) last_step = gru_out[:, -1, :] # 只取最后一步做预测 return self.fc(self.dropout(last_step))input_size是特征维度,单变量负荷预测填 1,如果加了温度和星期几就填对应列数;hidden_size是两层共享的隐藏维度,一般从 64 起调;num_layers大于 1 时才会启用层间 Dropout,单层时 PyTorch 会警告,所以代码里做了条件判断。forward里只取gru_out[:, -1, :],是因为预测目标是序列末尾之后的一个点,用最后一步的隐藏状态信息量最全。
3. 电力负荷数据的清洗、归一化与滑窗构造
3.1 缺失值和异常值怎么处理才不留坑
真实负荷数据里最常见的两类脏数据是传感器掉线造成的连续缺失,和计量故障造成的尖峰。线性插值适合填补短缺口,但连续缺失超过 3 个点就不该硬插,宁可整段丢弃。异常值用 3σ 法则筛,但要注意负荷本身有早晚高峰,直接对全天数据算标准差会把正常峰值误判成异常,稳妥做法是按同一小时的历史分布分别算均值和标准差。
import pandas as pd import numpy as np df = pd.read_csv('power_load.csv', parse_dates=['timestamp']) df = df.set_index('timestamp').sort_index() # 短缺口线性插值,长缺口标记后丢弃 df['load'] = df['load'].interpolate(method='linear', limit=3) df = df.dropna(subset=['load']) # 按小时分组做 3σ 异常检测,避免把早晚高峰误杀 df['hour'] = df.index.hour mask = df.groupby('hour')['load'].transform( lambda s: (s - s.mean()).abs() <= 3 * s.std() ) df = df[mask].drop(columns='hour')limit=3限制最多插 3 个连续缺失点,超过就保留 NaN 交给dropna处理。groupby('hour').transform保证每个小时用自己那组统计量判断,这是负荷数据和普通时序数据预处理最大的区别。
3.2 Min-Max 归一化和滑窗样本构造
LSTM 对输入尺度敏感,负荷值动辄几千兆瓦,不归一化会导致梯度尺度失衡。Min-Max 把数据压到 [0,1],比 Z-Score 更适合负荷这种有明确上下界的量。归一化器必须只在训练集上 fit,然后 transform 验证集和测试集,否则会引入未来信息造成数据泄漏。
from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0, 1)) n = len(df) train_end = int(n * 0.7) val_end = int(n * 0.85) # 只在训练集上拟合,避免数据泄漏 scaler.fit(df['load'].values[:train_end].reshape(-1, 1)) scaled = scaler.transform(df['load'].values.reshape(-1, 1)).flatten() def make_windows(series, seq_len=24, pred_len=1): X, y = [], [] for i in range(len(series) - seq_len - pred_len + 1): X.append(series[i:i + seq_len]) y.append(series[i + seq_len:i + seq_len + pred_len]) return np.array(X), np.array(y) X, y = make_windows(scaled, seq_len=24, pred_len=1) X = torch.tensor(X, dtype=torch.float32).unsqueeze(-1) # (N, 24, 1) y = torch.tensor(y, dtype=torch.float32)seq_len=24表示用过去 24 个点预测下一个点,如果数据是小时级就是"用一天预测下一小时"。unsqueeze(-1)把形状从 (N, 24) 变成 (N, 24, 1),因为 LSTM 要求输入是三维的 (batch, seq, feature)。划分比例 7:1.5:1.5 是文档里的做法,比常见的 8:1:1 留了更多验证数据,调参时更稳。
4. 训练循环、梯度裁剪与评估指标落地
4.1 损失函数、优化器和训练循环
负荷预测是回归任务,用 MSELoss 做损失,优化器选 Adam,学习率从 1e-3 起。训练循环里两个关键动作是梯度裁剪和验证集早停:梯度裁剪防止 LSTM 层反向传播时梯度爆炸,早停防止在验证损失回升后继续过拟合。
import torch.optim as optim device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = LSTMGRUModel(input_size=1, hidden_size=64).to(device) criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5) best_val, patience, wait = float('inf'), 10, 0 for epoch in range(200): model.train() optimizer.zero_grad() pred = model(X_train.to(device)) loss = criterion(pred, y_train.to(device)) loss.backward() # 梯度裁剪:把总范数限制在 1.0,防止梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() model.eval() with torch.no_grad(): val_loss = criterion(model(X_val.to(device)), y_val.to(device)).item() if val_loss < best_val: best_val, wait = val_loss, 0 torch.save(model.state_dict(), 'best_lstm_gru.pt') else: wait += 1 if wait >= patience: print(f'early stop at epoch {epoch}') breakweight_decay=1e-5是 L2 正则,配合 Dropout 一起压过拟合。clip_grad_norm_的max_norm=1.0是经验值,如果训练日志里 grad_norm 经常超过 5 就说明该调小学习率或加层归一化。早停的patience=10表示验证损失连续 10 轮不降就停,保存的是验证集最优的权重而不是最后一轮。
4.2 评估指标怎么算才和业务对得上
文档里列了 MSE、RMSE、MAE、MAPE 四个指标,实际汇报时最有说服力的是 MAPE,因为它直接对应"预测偏差百分之几"。但 MAPE 在负荷接近零时会爆炸,所以夜间低谷时段要单独看 MAE。
def evaluate(model, X, y, scaler, device): model.eval() with torch.no_grad(): pred = model(X.to(device)).cpu().numpy() # 反归一化回原始量纲 pred_inv = scaler.inverse_transform(pred) y_inv = scaler.inverse_transform(y.numpy()) mae = np.mean(np.abs(pred_inv - y_inv)) rmse = np.sqrt(np.mean((pred_inv - y_inv) ** 2)) mape = np.mean(np.abs((pred_inv - y_inv) / y_inv)) * 100 return {'MAE': mae, 'RMSE': rmse, 'MAPE': mape}反归一化这一步经常被漏掉,导致算出来的 MAE 是 0.02 这种没有物理意义的数。scaler.inverse_transform要求输入形状是 (N, 1),如果 pred 是 (N,) 要先 reshape。MAPE 乘 100 转成百分比,低于 3% 在短期负荷预测里算合格,低于 2% 属于比较好的水平。
5. 过拟合、梯度异常与推理部署的排错清单
5.1 训练不收敛时按这个顺序排查
先看损失曲线形状。训练损失降但验证损失升,是过拟合,加 Dropout、加 weight_decay、减 hidden_size。两条都不降,是欠拟合或学习率问题,先把 lr 调到 1e-2 试一轮,还不动就检查数据归一化是不是漏了。损失出现 NaN,八成是梯度爆炸,把 clip_grad_norm 的 max_norm 降到 0.5,或者把学习率砍半。如果验证损失震荡剧烈,把 batch_size 从 32 加到 64,减小梯度估计方差。
提示:LSTM 层数超过 2 层时,层间 Dropout 才生效,单层加 dropout 参数不会报错但也不起作用,容易误以为正则已经开了。
5.2 推理阶段的批处理与模型加载
生产环境推理和训练最大的区别是要处理变长输入和单点请求。加载模型时先重建结构再 load_state_dict,注意 map_location 指定设备,否则在只有 CPU 的服务器上加载 GPU 训练的权重会报错。
# 加载时先实例化结构,再灌权重 model = LSTMGRUModel(input_size=1, hidden_size=64) state = torch.load('best_lstm_gru.pt', map_location='cpu') model.load_state_dict(state) model.eval() # 单点推理:输入最近 24 小时负荷,输出下一小时预测 def predict_next(recent_24, scaler): x = torch.tensor(recent_24, dtype=torch.float32).view(1, 24, 1) with torch.no_grad(): out = model(x).item() return scaler.inverse_transform([[out]])[0][0]map_location='cpu'保证跨设备加载不报错,view(1, 24, 1)把单条序列补上 batch 维度。如果要做批量推理,把多条序列 stack 成 (B, 24, 1) 一次前向即可,比循环单条快一个数量级。部署时把 scaler 的参数一起序列化保存,否则线上拿不到训练时的归一化基准,预测值会整体偏移。
本文还有配套的精品资源,点击获取