简介:面向能源管理与电力系统分析场景,这份项目基于PyTorch框架,利用长短时记忆网络实现多输入多输出负荷预测。它接收温度、湿度等多源特征,同时输出未来多个时刻的负荷值,适合电力调度、需求响应分析等应用,也适合具备一定深度学习基础的研究者与开发者参考学习。压缩包共包含两个文件,其中一个是可以直接运行的Python脚本,另一个是CSV格式的示例数据集,整体容量约八KB,结构轻量清晰。代码覆盖了数据读取、缺失值填充、标准化处理、训练集与测试集划分,以及LSTM模型搭建、损失函数与优化器配置等全部环节,并附有完整数据集,便于快速复现与二次扩展。目前已有5500人学习使用。通过研读这份资源,读者能够理解多输入多输出时间序列预测的建模流程,掌握基于PyTorch构建循环神经网络并进行训练评估的实用技能。
1. 先看清楚:一份能直接跑通的 LSTM 多输入多输出负荷预测
做负荷预测这行当的老哥应该都有过这种经历:数据集翻出来几十个字段,结果跑起来连维度都对不齐,调了两天还在跟IndexError搏斗。这份资源就是冲着这个痛点来的——PyTorch 实现的多输入多输出 LSTM 负荷预测,自带数据集,路径改一下就能直接跑通,连训练集测试集划分都帮你做好了。多输入指的是可以同时喂温度、湿度、历史负荷等多个特征,多输出指的是模型输出未来多个时点的预测值,比如未来 24 小时的负荷曲线。
这份资源适合三种人:正在做时间序列预测课程设计的学生;刚接触 LSTM 想找一个完整落地方案来参考的开发者;以及在生产项目里需要快速搭建负荷预测原型、验证多步预测效果的工程人员。它不是论文里那种「指导意义」的抽象代码,而是一套能出预测曲线、能算误差、能拿去改的工程骨架。顺着目录结构和核心代码走一遍,你会发现多步预测的拦路虎基本集中在数据组织上,模型反而不是重点。
2. 项目整体架构:单步预测和多步预测的差异到底在哪
2.1 技术选型:为什么 LSTM 适合负荷预测
电力负荷本质上是一个强时间相关性的序列:今天的负荷曲线和昨天、前天同时段的曲线高度相似,同时受温度和节假日影响明显。LSTM 的核心优势在于它通过门控机制保留长期依赖,能捕捉到「昨天同一时刻的负荷对今天有强参考价值」这类跨天特征。相比 RNN 的梯度消失问题,LSTM 的细胞状态(cell state)可以做到十几个时间步之后信息仍然不衰减。
这份资源里采用的策略是「多输入、单步多输出」:输入过去一段连续时间的负荷和相关特征序列,一次性输出未来 N 个时点的预测值。这种做法和 seq2seq 这类自回归多步预测的区别在于:多输出模型不依赖前一时刻的预测值作为下一时刻输入,因此误差不会逐步累积,推理速度也更快。
2.2 目录结构:拿到包之后先看哪几个文件
lstm_load_forecast/ ├── data/ │ ├── load_data.csv # 原始负荷数据,内含时间戳、负荷、温度等列 │ └── readme.txt # 数据字段说明和单位 ├── src/ │ ├── dataset.py # 滑动窗口样本构造与数据集封装 │ ├── model.py # LSTM 多输入多输出模型结构 │ ├── train.py # 训练循环、验证、保存最佳模型 │ ├── predict.py # 加载模型做预测并反归一化出真实值 │ └── utils.py # 归一化、指标计算、绘图等公共函数 ├── config.py # 所有超参集中管理:窗口长度、特征列、预测步长 ├── requirements.txt # 依赖清单 └── README.md # 快速上手说明拿到包先打开config.py,里面几乎集中了所有需要关心的配置:预测起点forecast_start、历史窗口长度window_size、未来步长forecast_horizon、LSTM 隐层维度hidden_size、训练轮数num_epochs、批量大小batch_size。建议你第一次跑通之前不要动任何配置,跑完一次拿到基线效果,再逐个改参数观察变化。
2.3 数据处理链路:从原始表到训练样本
这是整个项目里最不值得做错的地方。原始数据是 CSV 表格,每行一个时刻的负荷值和对应气象特征。模型需要的是三维张量(batch_size, window_size, num_features),中间这步靠滑动窗口完成:给定窗口长度为 48(假设数据粒度为 15 分钟,代表过去 12 小时),预测未来 12 个点,模型看到的第 i 个样本就是「第 i 到 i+47 行的特征矩阵」和「第 i+48 到 i+59 行的负荷值」。
样本构造时要注意一个容易忽略的点:最后一个样本的结束位置必须距离数据尾部至少forecast_horizon步,否则标签越界。资源里用max_start = len(data) - window_size - forecast_horizon来卡边界,这个细节做错了,测试集预测就会悄悄少一段,画图时对不上还找不到原因。
3. 样本构造与数据预处理:多输入模型能否收敛的关键
3.1 特征归一化的正确姿势
LSTM 对输入特征的尺度十分敏感,不归一化会导致模型训练时梯度震荡、loss 一直下不去。常见做法是用sklearn.preprocessing.MinMaxScaler,把每个特征列压缩到[0, 1]区间。这里有一个大多数新手会踩的坑:归一化必须用训练集的统计量(min 和 max)来 fit,再用同一套统计量去 transform 测试集。
先用scaler.fit(train_df),再用同一个 scaler 去 transform 整个数据集,是保证训练分布和预测分布一致的前提。如果偷懒对全量数据一起 fit,测试集的信息就提前泄漏给了训练过程,看起来验证 loss 漂亮,实际上线就会翻车。
from sklearn.preprocessing import MinMaxScaler feature_cols = ["load", "temp", "humidity"] scaler = MinMaxScaler(feature_range=(0, 1)) # 先用训练部分 fit,再对全量 transform,避免数据泄漏 train_len = int(len(df) * 0.8) scaler.fit(df[feature_cols][:train_len]) scaled_data = scaler.transform(df[feature_cols])参数说明:feature_range设为(0, 1)是 LSTM 输入最常见的选择;train_len按 8:2 划分训练与测试。若后续发现模型输出波动大,可以尝试把归一化范围改为(-1, 1),配合tanh激活往往能让 loss 曲线更平滑。
3.2 滑动窗口样本构造:多输入多输出的核心基础
数据经过归一化后需要变成「窗口样本」。有些初学者把表格直接丢给 LSTM,得到维度报错后才明白batch_first的含义。项目里用create_sequences做了这件事:
import numpy as np import torch from torch.utils.data import Dataset def create_sequences(data, window_size=48, horizon=12, feature_dim=3): X_list, y_list = [], [] total = len(data) max_start = total - window_size - horizon # 边界控制,防止标签越界 for i in range(max_start): x = data[i : i + window_size] # (window_size, feature_dim) y = data[i + window_size : i + window_size + horizon, 0] # 只取负荷列 X_list.append(x) y_list.append(y) return np.array(X_list), np.array(y_list)代码说明:create_sequences的核心逻辑是「历史窗口预测未来窗口」。其中x的 shape 是(window_size, feature_dim),feature_dim对应第 3.1 节特征列的数量;y只取负荷列一维,shape 是(horizon,)。这里特别注意:load必须放在特征列的第一列,因为标签提取写了[..., 0]。
一个值得品味的细节是max_start的计算:数据集共total行,第i个样本的结束位置是i + window_size + horizon - 1,要小于等于total - 1,因此i最大为total - window_size - horizon。少了这行减法,训练到后半段会因为索引越界直接崩掉,这是多输出项目特有的边界问题。
3.3 数据集封装:自定义 Dataset 与 DataLoader 的配合
样本是 NumPy 数组,模型期望 Tensor,还需要 shuffle。项目里用继承Dataset的方式完成封装:
class LoadForecastDataset(Dataset): def __init__(self, X, y): self.X = torch.tensor(X, dtype=torch.float32) self.y = torch.tensor(y, dtype=torch.float32) def __len__(self): return len(self.X) def __getitem__(self, idx): return self.X[idx], self.y[idx]这里的核心价值在于把「样本构造」和「数据加载」分离:create_sequences负责整体切窗,Dataset负责按索引取单个样本,DataLoader负责在训练时打乱和分 batch。下面训练时调用DataLoader的shuffle=True,就能保证每个 epoch 进入模型的样本顺序不同,降低过拟合风险。
关于batch_first多说一句:PyTorch 的nn.LSTM默认输入是(seq_len, batch, input_size),但项目里样本的构造顺序是(batch, seq_len, input_size),所以模型里必须设置batch_first=True,否则会报维度不匹配的错。
4. 核心模块实现:从 LSTM 层到多步预测输出的完整链路
4.1 模型结构定义:一个完整的多输入多输出 LSTM
这是整个项目中最值得精读的部分,模型本身只有几十行,但结构上包含三个关键层:嵌入层(输入映射)、LSTM 层、输出层。具体代码如下:
import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size, hidden_size, num_layers, output_size, 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 ) self.regressor = nn.Sequential( nn.Linear(hidden_size, hidden_size // 2), nn.ReLU(), nn.Linear(hidden_size // 2, output_size) ) def forward(self, x): lstm_out, _ = self.lstm(x) # (batch, window, hidden) last_hidden = lstm_out[:, -1, :] # 取最后一个时间步的隐状态 out = self.regressor(last_hidden) # (batch, horizon) return out逐段逻辑说明:nn.LSTM接收的input_size是多输入的维度(特征列数量),hidden_size是隐层维度,默认取 64 或 128;num_layers取 1 或 2,层数越高理论上拟合能力越强但越容易过拟合,小数据集建议 1 层。lstm_out是一个包含每个时间步输出的三维张量,(batch, window, hidden)。[:, -1, :]取出最后一个时间步对应的层输出,它浓缩了整个序列的信息。
最后是regressor:一个两层的全连接网络。这里用hidden_size // 2做中间维度比较稳妥,有降维缓解过拟合的作用,输出维度output_size等于预测未来步数horizon,也就是多输出的「多」字所在。整体看,模型只有十几行,核心不是深度,而是输入组织是否合理。
4.2 训练循环:关注梯度裁剪与验证策略
训练逻辑并不复杂,核心在于「保存最佳模型」的时机。项目里每个 epoch 结束都会在验证集上评估,并保存验证 loss 最小的权重。这保证了最终落地使用的是在新数据上表现最好的模型,而不是训练末尾的模型,毕竟训练集 loss 继续下降不代表泛化变好:
import torch.optim as optim criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=1e-3) best_val_loss = float("inf") for epoch in range(num_epochs): model.train() total_train_loss = 0.0 for X_batch, y_batch in train_loader: optimizer.zero_grad() pred = model(X_batch) loss = criterion(pred, y_batch) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() total_train_loss += loss.item() # 验证阶段 model.eval() with torch.no_grad(): val_pred = model(X_val_tensor) val_loss = criterion(val_pred, y_val_tensor).item() if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), "best_model.pth")这里的几个关键参数值得细看:clip_grad_norm_是 LSTM 训练的保命操作,当梯度范数超过max_norm=1.0时进行截断,避免梯度爆炸导致 loss 变成nan。model.eval()配合torch.no_grad()能关闭 dropout 和梯度计算,让验证集预测输出是确定的;回溯来看,如果省略model.eval(),dropout 会在验证阶段继续生效,导致每次预测结果都不一样。
loss 函数选MSELoss而不是MAELoss,是因为 MSE 对大误差赋予更高惩罚,训练出的模型对大波动负荷更敏感,适合电网这类需要警惕高峰时段的场景。Adam学习率默认取1e-3,如果发现训练 loss 长时间不降,可以试着调大到3e-3或调到5e-4。
4.3 反归一化与结果还原:预测值如何变回真实负荷
模型输出的pred是 0~1 区间内的归一化值,必须用最初保存的scaler.inverse_transform还原为真实负荷,才能画图和评估。这里有一个极易翻车的点:scaler是对整个特征矩阵做变换的,还原时不能只把预测值传进去,需要构造相同维度:
pred_data = scaled_data[:, 0][-len(pred):].copy() # 需要构造 3 列特征矩阵的临时占位 dummy = np.zeros((len(pred), len(feature_cols))) dummy[:, 0] = pred.numpy().squeeze() real_pred = scaler.inverse_transform(dummy)[:, 0]这段代码的含义是:构造一个全是 0 的矩阵,只将预测的负荷值填入第一列,利用scaler.inverse_transform还原第一列的真实数值。因为特征之间的归一化是独立进行的,其余列填 0 不会影响第一列的反算结果。若直接调用scaler.inverse_transform(pred),会因维度不匹配直接报错。
4.4 模型效果评估:你到底要关注哪些指标
训练结束后,项目里会自动计算三个评估指标:RMSE、MAE、MAPE。其中 MAPE(平均绝对百分比误差)在负荷预测里最直观,它告诉你「平均每个时点的预测偏差是真实值的百分之多少」。多数场景下,MAPE 在 3%~8% 都算正常范围,超过 10% 就得回头检查数据和特征。
def evaluate(y_true, y_pred): rmse = np.sqrt(np.mean((y_true - y_pred) ** 2)) mae = np.mean(np.abs(y_true - y_pred)) mape = np.mean(np.abs((y_true - y_pred) / y_true)) * 100 return {"RMSE": rmse, "MAE": mae, "MAPE(%)": mape}这段代码建议原样保留,因为它直接对接你后续所有的模型改造:不管你换成 GRU、Attention 还是 TCN,最后都用这套指标做横向对比。指标的可解释性比模型本身的复杂度对工程落地更有说服力。
5. 避坑指南:跑通后最容易踩的四个常见问题
5.1 训练集 loss 下降但预测曲线整体滞后一拍
现象:验证集 loss 很小,画出的预测曲线和真实曲线形状一致,但整条线「右移」一个时刻,峰值总是比真实值晚点到达。
原因:这个不是模型问题,是数据对齐问题。模型预测的是未来值,如果你把当前时刻的负荷作为输入后,模型的输出目标是同一时刻的负荷,模型就会学到「复制当前值就能让 loss 小」。
解决:检查create_sequences中y的取法,必须确保y从i + window_size位置开始取,即目标位于历史窗口之后,绝不能从i开始取目标。
5.2 在测试集上 loss 很低,但看曲线发现预测出「震荡」
现象:MAPE 在合理范围,但预测曲线出现了肉眼可见的高频抖动,相邻时点忽高忽低。
原因:网络输出彼此独立的未来时点时缺少平滑约束,过大的hidden_size加剧了过拟合,模型记住了训练集的噪声。
解决:尝试把dropout从 0.2 提到 0.4,或者缩小hidden_size。增加全连接层中间的BatchNorm1d也有一定概率缓解输出抖动。这类情况我会优先降复杂度而不是加训练轮数。
5.3 预测结果反归一化后数值整体偏小
现象:曲线形状对,但纵轴量级不对,预测峰值只有真实值的一半。
原因:大概率是用了MinMaxScaler之后,预测输出经过sigmoid或模型本身的输出范围被限制在 0~1 附近,反归一化时如果取的是占位矩阵的默认值,可能把第二、第三列的 0 也反算了一遍,导致数值被压缩。
解决:反归一化时只拿真实预测值构造dummy,并确认换算的是第一列。建议画图前先打印real_pred.max()和y_true.max(),核对量级后再说模型好坏。
5.4 训练时 loss 变成 nan
现象:训练到某个 epoch 后 loss 直接变成nan,后续无法恢复。
原因:梯度爆炸,通常发生在学习率过大或数据中存在NaN值。负荷数据在凌晨或传感器异常时经常有空值,原始 CSV 里一列出现缺失,经过 MinMaxScaler 后就会扩展成异常极大值。
解决:在构造样本前执行df.fillna(method='ffill')对历史负荷做前向填充,同时在训练循环里保留clip_grad_norm_。这是所有 LSTM 训练救命的两个操作,缺一个都可能炸。
6. 二次开发:把这套固定预测改成属于你自己的方案
跑通这个项目之后,你会发现一切改动都绕着config.py里的几个参数转。想扩展更多输入特征,比如加入「是否为节假日」「一周中的第几天」,直接在feature_cols里追加列名。要小心归一化的fit逻辑:新特征的量纲可能差异极大(比如用 0/1 表示节假日),需要和连续型特征分开归一化,否则会干扰 LSTM 的训练过程。我的建议是准备两个 scaler,一个处理连续特征,一个处理离散特征,训练时拼接后再切窗,这个小改动比你在模型参数上折腾一两天收益来得快。
想调整预测步长,比如从 15 分钟粒度改成小时粒度,核心不是改horizon,而是改数据聚合方式。先按小时重采样再做窗口,否则模型输入的时间间隔不一致,学出来的规律就是歪的。我用df.resample('1H').mean()做过一次尝试,效果明显比直接在原粒度上改窗口好。
如果你想进一步提高精度,推荐从两个方向入手。一是把单层 LSTM 换成双向 LSTM 或者加一个简单的注意力层,注意nn.LSTM加上bidirectional=True时lstm_out[:, -1, :]的维度会翻倍到2 * hidden_size,此时nn.Linear第一层维度也要跟着改;二是把「最后一步隐状态接全连接」改成「所有时间步输出接全局平均池化」,这种处理对长时间窗口的影响更平滑,适合负荷这类周期性强的信号。修改前先记录原有的 RMSE 和 MAPE,改完用同样的测试集重新评估,不要凭感觉说变好了。
回想我从拿到这个项目到真正敢改它的过程,最初的痛苦几乎都集中在数据对齐上,模型本身反而不是瓶颈。从那以后我做任何时间序列预测,都强制先做一步:把原始数据、归一化结果、第一个样本的 X 和 y 逐行打印出来人工核对一遍,对齐没问题,再谈调参。希望这次拆解帮你少走一段弯路,能把这套代码真正变成自己的工具。
本文还有配套的精品资源,点击获取