
简介面向电力负荷预测与时间序列建模的学习者这份六十二页的PyTorch实战文档系统梳理了长短期记忆网络LSTM与Transformer在能源需求预测中的应用。内容从传统循环神经网络的局限性与LSTM门控机制入手逐步过渡到多头注意力、位置编码等Transformer核心组件并给出了基于PyTorch的两类模型融合实现思路同时覆盖数据清洗、缺失值处理、归一化与标准化、滞后特征及滚动统计特征构造以及均方误差、平均绝对误差等损失函数、优化器选择、训练循环、早停策略与超参数调优等完整流程。整个资源包为单一PDF文件大小仅二点三四MB内嵌可跳转目录左侧大纲支持章节快速定位方便按需查阅与对照学习。已有126人学习下载适合作为时间序列预测入门与电力能源工程实践的实用参考。1. 电力负荷预测为什么需要 LSTM 和 Transformer 一起上电网调度员看明天 96 点负荷曲线交易员盯现货价格前的负荷预判工厂能源管理员做需量控制——这三类场景本质上都是同一件事根据过去几周的电力负荷序列预测未来 24 到 72 小时的负荷变化。单纯用 ARIMA 这类统计模型处理节假日、气温突变、工业用户启停带来的非线性波动效果往往撑不住考核精度。于是 PyTorch 时间序列模型成了这个领域的常规武器而 LSTM 和 Transformer 又各有各的脾气LSTM 对序列局部变化敏感训练稳定但长序列上信息衰减明显Transformer 靠自注意力能直接看到任意远处的模式但对数据量和超参数更挑剔。把两者拼成一个混合模型让 LSTM 先把序列里的短期依赖和数值尺度“捋顺”再把特征序列交给 Transformer 去抓长程周期性是这类实战方案最常见的落地路径。适合正在做能源数据分析、微网调度或电力交易辅助决策的工程师也适合想入时序预测坑的 PyTorch 使用者。2. PyTorch 时间序列模型的数据准备从原始负荷到监督学习样本2.1 先想清楚预测目标单步、多步还是滚动多步电力负荷预测的标签设计直接决定模型输出层怎么写。最简单的是单步预测用过去lookback个点预测下一点适合做实时校正类任务。实际工程中更常见的是多步预测——一次给出未来 24 个小时的负荷曲线这要求模型输出层至少有 24 个神经元或者在训练时把未来 24 点依次作为监督标签。还有一种滚动多步方案用历史窗口预测未来第 1 点把预测值拼回输入、丢掉最早一点再预测第 2 点。这种方式节省训练标签但误差会逐步累积预测步数越长偏移越大。我的建议是能直接多输出就别滚动除非你明确需要每步都做状态更新。下面所有代码都按“lookback 小时预测 horizon 小时”的监督学习范式来构造样本。2.2 负荷数据的清洗与特征列设计电力负荷数据通常来自 SCADA 系统或智能电表常见问题有三个时间戳对齐错位、异常尖峰、节假日与气温这类外生变量缺失。清洗规则按优先级排序先删除时间戳重复且负荷值不一致的行再用前后 24 小时中位数替换设备通信中断导致的零值或负值最后对超过 3 倍滚动标准差的值做 Winsorize 截断。气温、相对湿度、星期几、是否节假日这些外生特征在训练集和测试集中必须使用相同的最小最大值参数做归一化。import pandas as pd import numpy as np def load_and_clean_load_data(file_path: str) - pd.DataFrame: 读取原始负荷csv完成时间索引与异常值清洗 df pd.read_csv(file_path, parse_dates[timestamp], index_coltimestamp) df df[~df.index.duplicated(keepfirst)].sort_index() # 负值或零值在居民负荷中不合理用前向窗口的中位数填充 mask (df[load] 0) | (df[load].isna()) df.loc[mask, load] ( df[load].rolling(24, min_periods1, centerTrue).median()[mask] ) # 超过3倍滚动标准差的值截断到边界值避免训练时梯度爆炸 rolling_mean df[load].rolling(24 * 7, min_periods1).mean() rolling_std df[load].rolling(24 * 7, min_periods1).std() upper rolling_mean 3 * rolling_std lower rolling_mean - 3 * rolling_std df[load] df[load].clip(lowerlower, upperupper) # 构造外生特征小时、星期、是否周末 df[hour] df.index.hour df[dayofweek] df.index.dayofweek df[is_weekend] (df[dayofweek] 5).astype(int) return df这段代码里rolling(24).median()的窗口中心化是为了避免用未来信息填充过去的空值——时序数据最忌讳前向填充时混入后视信息。clip操作不会删除样本只是把极端值压回合理区间这比直接丢弃更能保持时间连续性因为在负荷预测里除夕和台风天的负荷往往是模型需要学习的边界案例。2.3 滑动窗口构造样本边界条件怎么处理把连续序列切成(x, y)监督样本的关键是步长选择。步长为 1 时样本量最大但相邻样本高度重叠容易让模型过拟合到近邻样本而非真实模式步长等于horizon时样本量骤减但训练更高效。折中方案是步长取horizon // 2。如果总样本不足 3 万条步长取 1 也可以配合后面的早停策略控制过拟合。from torch.utils.data import Dataset class LoadDataset(Dataset): 把清洗后的DataFrame切成lookback-horizon的训练样本 def __init__(self, df: pd.DataFrame, lookback: int 168, horizon: int 24, step: int 12): values df[[load]].values.astype(np.float32) exog df[[hour, dayofweek, is_weekend]].values.astype(np.float32) self.x_seq, self.y_seq, self.x_exog [], [], [] for start in range(0, len(df) - lookback - horizon 1, step): end start lookback self.x_seq.append(values[start:end]) self.y_seq.append(values[end:end horizon]) self.x_exog.append(exog[start:end]) self.x_seq np.array(self.x_seq) self.y_seq np.array(self.y_seq) self.x_exog np.array(self.x_exog) def __len__(self): return len(self.x_seq) def __getitem__(self, idx): return ( torch.tensor(self.x_seq[idx], dtypetorch.float32), torch.tensor(self.x_exog[idx], dtypetorch.float32), torch.tensor(self.y_seq[idx], dtypetorch.float32), )注意__getitem__返回三个张量负荷历史窗口、外生特征窗口、目标负荷窗口。lookback设为 168 是因为一周有 168 小时这样模型每次都能看到完整的“上周同一天”的负荷形态——这个窗口长度在电力负荷预测里几乎是默认配置太短抓不住周周期性太长则输入维度增大导致 Transformer 注意力矩阵显存压力上升。2.4 归一化必须按训练集统计量测试集只做变换时序预测里最容易犯的错是把整个数据集一起做 MinMaxScaler 拟合。这样测试集的统计信息会提前泄露到训练过程中验证指标会虚高 10% 到 20%上线后立刻现原形。正确做法是只对训练集拟合 scaler再对验证集和测试集调用transform。from sklearn.preprocessing import MinMaxScaler train_df df.iloc[: int(len(df) * 0.7)] val_df df.iloc[int(len(df) * 0.7): int(len(df) * 0.85)] test_df df.iloc[int(len(df) * 0.85):] scaler MinMaxScaler(feature_range(0, 1)) scaler.fit(train_df[[load]]) for subset in (train_df, val_df, test_df): subset[load_scaled] scaler.transform(subset[[load]])连续时间切分而不是随机打乱是为了模拟真实预测场景你永远是用过去预测未来而不是用未来预测过去。负荷数据有强烈的季节趋势如果随机打乱模型会学到“从全年各段取样”的作弊模式换到今年 12 月就失效。外生特征里的hour和dayofweek是周期变量不需要做归一化但如果后续要加入气温就必须和负荷使用同一套 scaler 逻辑。3. 混合模型构建LSTM 抓变化Transformer 抓周期3.1 为什么不用纯 LSTM 或纯 Transformer纯 LSTM 堆叠三四层确实能出不错的基线但对 168 小时这种长输入梯度在反向传播穿过这么多时间步后容易衰减靠遗忘门硬扛长依赖并不划算。纯 Transformer 在电力负荷这类数值序列上又有个毛病它把每个时刻当作独立的 token 做全局注意力对局部数值变化幅度和趋势斜率不敏感而且训练初期收敛比 LSTM 慢得多。混合架构把这些串成一条流水线LSTM 先做序列压缩和局部特征提取输出的隐状态序列代替原始负荷值作为 Transformer 的输入注意力机制再在压缩后的特征序列上建模全局依赖。我一般用一层双向 LSTM 或两层单向 LSTM 做特征提取隐藏维度不宜过大64 到 128 就够。LSTM 输出的每个时间步隐状态拼接成(batch, seq_len, hidden_size)的特征序列送入 TransformerEncoder。这里有个细节LSTM 本身有顺序敏感性所以不需要再额外加位置编码但如果想让 Transformer 分支更稳可以在 LSTM 输出后加一个可学习的线性投影把隐藏维度映射到d_model。3.2 完整的 PyTorch 模型定义import torch import torch.nn as nn import math class LSTMTransformerForecaster(nn.Module): LSTM提取局部时序特征TransformerEncoder建模全局依赖 def __init__(self, input_size1, hidden_size64, num_lstm_layers2, d_model64, nhead4, num_transformer_layers2, dropout0.1, horizon24, exog_size3): super().__init__() self.lstm nn.LSTM( input_sizeinput_size exog_size, hidden_sizehidden_size, num_layersnum_lstm_layers, batch_firstTrue, bidirectionalFalse, dropoutdropout if num_lstm_layers 1 else 0.0, ) # 把LSTM隐状态维度投影到Transformer需要的d_model self.proj nn.Linear(hidden_size, d_model) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforward4 * d_model, dropoutdropout, batch_firstTrue, activationrelu, ) self.transformer nn.TransformerEncoder(encoder_layer, num_layersnum_transformer_layers) # 解码到horizon长度使用最后一个时间步的Transformer输出 self.regressor nn.Sequential( nn.Linear(d_model, 128), nn.GELU(), nn.Dropout(dropout), nn.Linear(128, horizon), ) def forward(self, x_load, x_exog): # x_load: (B, lookback, 1), x_exog: (B, lookback, exog_size) x torch.cat([x_load, x_exog], dim-1) lstm_out, (h_n, c_n) self.lstm(x) # lstm_out: (B, lookback, hidden) proj_out self.proj(lstm_out) # (B, lookback, d_model) transformer_out self.transformer(proj_out) last_hidden transformer_out[:, -1, :] # 取最后一个时间步的上下文 return self.regressor(last_hidden)模型设计里藏着两个容易忽略的参数细节。第一个是input_size把负荷值和三个外生特征拼在一起输入 LSTM这样 LSTM 在每个时间步同时看到当时的负荷和星期信息比把外生特征单独拼到输出端更自然。第二个是dim_feedforward 4 * d_modelPyTorch 的 TransformerEncoderLayer 默认这个比例太小会导致前馈网络容量不足负荷序列的非线性映射表达力不够如果你的训练集小于 2 万样本建议把比例降到 2 倍防止过拟合。3.3 超参数表先照这个跑通再谈调优参数推荐值调整思路lookback168样本量不足时降到 120优先保证 batch 数量hidden_size64序列波动剧烈如工业负荷升到 128num_lstm_layers2超过 3 层收益极小训练时间线性增长d_model64和 hidden_size 保持一致即可不必单独加大nhead4d_model64 时 4 个头比 8 个头稳定num_transformer_layers2序列长度超过 336 时可试 3 层dropout0.1样本量小于 1 万时提高到 0.3batch_size64显存紧张降到 32配合梯度累积这个参数组合在 5 万条小时级数据上的训练耗时大约 10 分钟单张消费级 GPUCPU 跑则要 40 分钟左右可以先调小lookback到 72 做代码验证再恢复到 168。3.4 损失函数与优化器的工程选型负荷预测常用 MSE 作为主损失因为它对大幅偏差的惩罚更重能逼着模型把峰值负荷拟合得更准。但纯 MSE 会让模型在低谷时段显得过于“平滑”峰值被平滑掉。实践里更好的组合是 MSE 加一个 MAE 辅助损失按 7:3 加权。MAPE平均绝对百分比误差不适合直接当损失函数因为凌晨低负荷时很小的绝对误差会被放大成巨大的百分比误差梯度噪声太大。criterion lambda pred, y: 0.7 * nn.functional.mse_loss(pred, y) 0.3 * nn.functional.l1_loss(pred, y) optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-5) scheduler torch.optim.lr_scheduler.CosineAnnealingWarmRestarts( optimizer, T_010, T_mult2, eta_min1e-5 )AdamW 配合余弦退火重启是序列预测里性价比很高的组合。T_010表示每 10 个 epoch 把学习率重启回峰值这能帮模型跳出局部平坦区对 Transformer 这类参数敏感的结构尤其有用。weight_decay 不要设太大1e-5 即可过大的权重衰减会把 LSTM 的门控参数压得过小导致信息选择能力退化。4. 训练、评估与多步预测的坑4.1 训练循环里的早停与模型保存训练循环不需要花哨但有几个关键点每个 epoch 后在验证集上计算损失连续patience10个 epoch 没有下降就加载最佳模型并终止训练保存模型时要连同 scaler 和特征列名一起存否则部署时无法还原预测值。best_val_loss float(inf) patience_counter 0 checkpoint {model: None, scaler: scaler} for epoch in range(50): model.train() train_loss_sum 0.0 for x_load, x_exog, y in train_loader: x_load, x_exog, y x_load.to(device), x_exog.to(device), y.to(device) optimizer.zero_grad() pred model(x_load, x_exog) loss criterion(pred, y) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() train_loss_sum loss.item() model.eval() val_loss evaluate(model, val_loader, criterion, device) if val_loss best_val_loss: best_val_loss val_loss patience_counter 0 checkpoint[model] model.state_dict() else: patience_counter 1 if patience_counter 10: break scheduler.step()clip_grad_norm_这行很容易被忽略但 LSTM 在长序列上经常出现梯度范数超过 10 的情况梯度裁剪后 Transformer 分支的注意力权重更新会稳定很多。早停的 patience 取 10 是因为负荷数据通常 20 个 epoch 内就能收敛等 15 个 epoch 还不下降的话基本可以判定当前超参数组合有问题。4.2 多步预测的三种实现路线对比多步预测输出 24 个点时有三种常见路线。第一种是模型输出层直接输出 24 维向量也就是本节代码里的写法的实现方式速度快、训练简单但 24 步之间相互独立没有显式建模预测步之间的依赖。第二种是把未来 24 步当作序列生成解码时每步把上一步预测作为当前输入类似 seq2seq。这种方式效果最好但训练复杂度高出一截。第三种是预测分位数区间输出 24 步的 P10/P50/P90适合电力交易场景需要把损失函数换成 pinball loss。如果做的是日前调度直接多输出够用如果做日内滚动预测建议用 seq2seq 解码因为每隔 15 分钟就要补充新的实测数据模型需要看到上一步真实值来修正偏差。4.3 评估指标怎么选以及还原归一化结果的细节评估时不能只看整体 MAPE要把一天切分成峰段、平段、谷段分别计算。电力行业里峰段8:00-11:00 和 18:00-21:00的预测精度直接影响调度决策谷段的 MAE 略大反而可以接受。计算指标前要先把预测值inverse_transform回真实负荷单位否则所有指标都是无量纲的相对值无法跟业务考核标准对齐。def evaluate_metrics(pred_scaled, y_scaled, scaler): pred scaler.inverse_transform(pred_scaled.reshape(-1, 1)).ravel() y_true scaler.inverse_transform(y_scaled.reshape(-1, 1)).ravel() mae np.mean(np.abs(pred - y_true)) rmse np.sqrt(np.mean((pred - y_true) ** 2)) mape np.mean(np.abs((pred - y_true) / y_true)) * 100 return {MAE(MW): mae, RMSE(MW): rmse, MAPE(%): mape}注意 MAPE 在负荷接近零的凌晨时段会异常放大可以加一个下限过滤只计算真实负荷大于该时段 P10 的点。峰段和谷段分开算 MAPE 时直接用 DataFrame 布尔索引切分时间窗口即可不需要额外传hour列。4.4 三段最常踩的坑泄露、泄漏、尺度第一个坑是归一化泄露前文已经专门处理过。第二个坑是时间泄露构造样本时滑动窗口的end应该严格小于标签起点代码里用range(0, len(df) - lookback - horizon 1, step)保证了这一点但如果你手动拼接外部特征很容易把当天的气温数据提前放到输入里——预测明天的负荷用的气温必须是明天的预报值不是今天的实测值。第三个坑是尺度敏感性TransformerEncoder 对输入特征的绝对尺度很敏感LSTM 输出的隐状态经过tanh激活后范围是(-1, 1)送入 Transformer 前做一次 LayerNorm 会让收敛更稳定代码里可以加在proj之后。5. 把外部特征真正融入 Transformer一个值得试的进阶配置很多负荷预测项目止步于“LSTMTransformer 效果比纯 LSTM 好 5%”然后不知道还能怎么提升。最容易见效的下一步是把更丰富的外部特征按 token 级别融合进 Transformer而不是只在输入层拼接。具体做法LSTM 输出隐状态序列后把每个时间步对应的温度、湿度、电价等特征拼成一个d_model维的向量加进投影后的隐状态里让注意力机制在不同时间步上动态权衡“历史负荷形态”和“当前外部条件”的贡献。def forward_with_fusion(self, x_load, x_exog, x_temp): x torch.cat([x_load, x_exog], dim-1) lstm_out, _ self.lstm(x) # x_temp: (B, lookback, 1)先线性投影到d_model再叠加 temp_embed self.temp_proj(x_temp) fused self.proj(lstm_out) temp_embed fused self.norm(fused) transformer_out self.transformer(fused) return self.regressor(transformer_out[:, -1, :])temp_embed的加法是一种强先验告诉模型当天温度直接影响负荷水平而不是让模型自己从数值里硬学这个关系。实测中加了温度融合后夏季高温日的峰值预测 MAPE 通常能再降 1 到 2 个百分点代价是推理时需要同步输入对应时段的气象预报数据。验证这个融合是否有用时不要只看整体指标直接把每年 7 月和 12 月的预测误差分别打印出来对比。如果两个极端季节的误差都下降了说明融合的是真正的因果信息如果只是整体指标微降而每个季节各自的表现忽高忽低那大概率是过拟合到了训练集的特定年份。多步预测的误差会随步长增加而累积每步的偏差系数可以按1.02^step做经验校正这个系数在 24 步预测里能把末段误差压回 3% 以内。本文还有配套的精品资源点击获取