☰
PyTorch LSTM多输入多输出负荷预测实战:从数据到模型
2026/10/16 12:28:24 网站建设 项目流程

简介:这份资源面向电力系统、能源管理及时间序列预测方向的学习者与开发者,提供一套基于PyTorch的LSTM多输入多输出负荷预测完整实现。模型可同时接收温度、湿度、节假日等多类输入信号,并输出多个地区或时段的负荷预测结果,适合具备一定Python与深度学习基础、希望快速上手多变量时序建模的读者。压缩包共2个文件,包含1个py源码与1个csv数据集,整体约8KB,其中脚本负责定义LSTM网络结构、训练循环与预测流程,数据集则提供负荷预测所需的变量样本,便于直接运行与复现。目前已有5495人学习下载,说明该方案在同类任务中具有较高的参考价值。读者可借此掌握多输入多输出LSTM的搭建思路、数据预处理与训练评估方法,并在此基础上迁移到自身负荷预测场景中,提升时间序列分析与PyTorch实战能力。

1. 多输入多输出负荷预测:一份能直接跑的 LSTM 时序项目长什么样

很多做电力负荷预测的开发者都遇到过这种局面:手头有温度、湿度、历史负荷、节假日标签好几路数据,想用 LSTM 做多变量预测,结果卡在数据对齐和维度拼接上,模型代码写了一半就搁置了。这份资源解决的就是这个问题——它把多输入多输出的 LSTM 负荷预测流程完整封装成一个可直接运行的 PyTorch 项目,数据集随包附带,不需要你去公开平台另找数据。适合正在做时间序列预测的算法工程师、电力/能源方向的研究生,以及想拿一个完整时序项目练手的 PyTorch 学习者。项目结构不复杂,但该有的环节一个不少:数据加载、滑动窗口构造、多变量输入、多步输出、训练循环、预测可视化。拿到手改改路径就能跑,跑完就能看到预测曲线和误差指标。

2. 项目结构与数据流:从原始 CSV 到模型输入张量

2.1 目录组织与文件职责

这类负荷预测项目通常不会搞太复杂的工程分层,核心文件就那么几个,但每个都有明确分工。下面是我拆过多个同类项目后总结的典型结构,这份资源基本也遵循这个套路:

lstm_load_forecast/ ├── data/ │ ├── load_data.csv # 原始负荷+气象数据 │ └── processed/ # 归一化后的中间文件 ├── config.py # 超参数与路径配置 ├── dataset.py # Dataset类与滑动窗口逻辑 ├── model.py # LSTM网络定义 ├── train.py # 训练入口 ├── predict.py # 推理与可视化 └── utils.py # 归一化、指标计算等工具函数

config.py把所有可调参数集中管理,包括窗口长度、预测步长、学习率、batch size、训练轮数。这个设计的好处是改实验时不用翻遍所有文件,只动一个地方就行。dataset.py负责把连续时间序列切成监督学习样本,这是时序预测里最容易写错的部分。model.py定义网络结构,多输入多输出场景下输入维度等于特征数,输出维度等于预测步长。train.py和predict.py分别是训练和推理入口,职责清晰。

2.2 滑动窗口构造:多输入多输出的关键逻辑

时间序列预测的核心操作是把一维序列切成 (输入窗口, 输出窗口) 的样本对。多输入意味着每个时间步有多个特征,多输出意味着一次预测未来多个时间点。下面这段代码是这类项目的标准写法:

import torch from torch.utils.data import Dataset import numpy as np class LoadDataset(Dataset): def __init__(self, data, input_len=24, output_len=4, feature_cols=None): """ data: numpy array, shape [total_timesteps, num_features] input_len: 历史窗口长度,比如用过去24小时预测 output_len: 预测步长,比如预测未来4小时 feature_cols: 指定哪些列作为输入特征 """ self.input_len = input_len self.output_len = output_len # 只保留需要的特征列,最后一列通常是目标负荷值 self.data = data[:, feature_cols] if feature_cols else data self.samples = [] total_len = len(self.data) # 滑动窗口切分,保证不越界 for start in range(total_len - input_len - output_len + 1): x = self.data[start : start + input_len] # [input_len, num_features] y = self.data[start + input_len : start + input_len + output_len, -1] # 只取负荷列 self.samples.append((x, y)) def __len__(self): return len(self.samples) def __getitem__(self, idx): x, y = self.samples[idx] return torch.FloatTensor(x), torch.FloatTensor(y)

这段逻辑有几个参数需要说清楚。input_len=24表示用过去 24 个时间步作为历史信息,如果数据是小时级,就是过去一天。output_len=4表示预测未来 4 个时间步。feature_cols控制哪些列参与输入,比如温度、湿度、星期几的 one-hot 编码都可以放进去,但目标列必须放在最后一列,因为y的提取依赖这个约定。滑动窗口的循环范围是total_len - input_len - output_len + 1,少一个都会导致最后一个样本越界或者丢失。我见过有人写成total_len - input_len - output_len,结果最后一个样本永远取不到,训练集莫名其妙少了一条。

2.3 数据归一化与反归一化

负荷预测里归一化不是可选项,是必选项。负荷值动辄几千上万千瓦,温度可能是个位数,湿度百分比又是几十,不同量纲直接喂给 LSTM,梯度会被大数值特征主导。常见做法是对每一列单独做 Min-Max 归一化:

def normalize(data): """按列做min-max归一化,返回归一化后的数据和缩放参数""" min_vals = data.min(axis=0) max_vals = data.max(axis=0) # 防止除零,加一个极小值 range_vals = max_vals - min_vals + 1e-8 normalized = (data - min_vals) / range_vals return normalized, min_vals, max_vals def denormalize(pred, min_val, max_val): """把预测结果还原到原始量纲""" return pred * (max_val - min_val + 1e-8) + min_val

注意反归一化时只能用目标列对应的min_val和max_val,不能把整个矩阵的缩放参数拿来用。这个坑很隐蔽,因为归一化后的预测值看起来很正常,但反归一化后数值完全不对,排查半天才发现是索引取错了。我一般会在config.py里把目标列的索引单独定义成一个常量,归一化和反归一化都引用它,避免手滑。

3. LSTM 模型定义与训练循环:多输出头的实现细节

3.1 网络结构:从多特征输入到多步输出

多输入多输出的 LSTM 结构本身不复杂,关键在于最后一层全连接怎么映射。输入是[batch, seq_len, num_features],LSTM 输出[batch, seq_len, hidden_size],我们只需要最后一个时间步的隐状态,然后接一个线性层映射到output_len维度:

import torch.nn as nn class LSTMForecaster(nn.Module): def __init__(self, input_size, hidden_size=64, num_layers=2, output_size=4, dropout=0.2): 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, # 输入格式为 [batch, seq, feature] dropout=dropout if num_layers > 1 else 0 ) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): # x: [batch, input_len, input_size] lstm_out, (h_n, c_n) = self.lstm(x) # 取最后一个时间步的输出 last_step = lstm_out[:, -1, :] # [batch, hidden_size] out = self.fc(last_step) # [batch, output_size] return out

input_size等于特征列的数量,比如负荷、温度、湿度三列就是 3。hidden_size控制 LSTM 的记忆容量,64 到 128 是负荷预测里比较常见的取值。num_layers=2表示两层堆叠,第二层可以捕捉更高层的时序模式。dropout只在多层时生效,单层 LSTM 加 dropout 没有意义。batch_first=True这个参数必须显式设置,否则 PyTorch 默认输入格式是[seq, batch, feature],和大多数人习惯的维度顺序相反,不设置的话训练时维度对不上,报错信息又不够直观,容易卡住。

3.2 训练循环:损失函数与优化器选择

负荷预测是回归任务,损失函数用 MSE 或 MAE 都行。MSE 对大误差更敏感,适合希望模型尽量避免大偏差的场景;MAE 对异常值更鲁棒。优化器用 Adam 基本是默认选择,学习率 1e-3 起步:

import torch from torch.utils.data import DataLoader def train_model(model, train_dataset, val_dataset, config): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) train_loader = DataLoader(train_dataset, batch_size=config.batch_size, shuffle=True) val_loader = DataLoader(val_dataset, batch_size=config.batch_size, shuffle=False) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=config.lr) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=5, factor=0.5) best_val_loss = float("inf") for epoch in range(config.epochs): model.train() train_loss = 0.0 for x, y in train_loader: x, y = x.to(device), y.to(device) optimizer.zero_grad() pred = model(x) loss = criterion(pred, y) loss.backward() # 梯度裁剪,防止LSTM梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() train_loss += loss.item() * x.size(0) train_loss /= len(train_dataset) # 验证阶段 model.eval() val_loss = 0.0 with torch.no_grad(): for x, y in val_loader: x, y = x.to(device), y.to(device) pred = model(x) val_loss += criterion(pred, y).item() * x.size(0) val_loss /= len(val_dataset) scheduler.step(val_loss) if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), "best_model.pth") print(f"Epoch {epoch+1}/{config.epochs} | train_loss={train_loss:.6f} | val_loss={val_loss:.6f}") return model

clip_grad_norm_这行是 LSTM 训练的标配,梯度爆炸在时序任务里太常见了,不加的话 loss 可能突然变成 nan。ReduceLROnPlateau让学习率在验证损失不再下降时自动减半,比手动调学习率省事。保存最佳模型而不是最后一个 epoch 的模型,因为过拟合往往发生在训练后期,最后一个 epoch 的验证损失可能已经回升了。

3.3 评估指标:MAPE 和 RMSE 怎么算

训练完要看模型效果,光看 loss 不够直观,业务方更关心百分比误差。MAPE 和 RMSE 是两个最常用的指标:

import numpy as np def compute_metrics(y_true, y_pred): """ y_true, y_pred: numpy array, shape [num_samples, output_len] 返回每个预测步长的MAPE和RMSE """ mae = np.mean(np.abs(y_true - y_pred), axis=0) rmse = np.sqrt(np.mean((y_true - y_pred) ** 2, axis=0)) # MAPE计算时避免除零,加一个小量 mape = np.mean(np.abs((y_true - y_pred) / (y_true + 1e-8)), axis=0) * 100 return {"MAE": mae, "RMSE": rmse, "MAPE": mape}

注意 MAPE 在负荷值接近零的时候会爆炸,所以分母加了1e-8。如果数据里存在零负荷时段,MAPE 这个指标本身就不太适用,建议换成 SMAPE 或者直接看 RMSE。我一般会把三个指标都打印出来,哪个异常一眼就能看出来。

4. 避坑与排查:跑通这个项目最容易翻车的几个地方

4.1 维度不匹配报错

现象:运行train.py时报RuntimeError: input.size(-1) must be equal to input_size,或者expected input batch_size (X) to match target batch_size (Y)。

原因:最常见的是input_size和实际特征列数不一致。比如config.py里写了input_size=3,但 CSV 里实际有 4 列特征,或者feature_cols选多了选少了。另一个原因是batch_first没设成True,导致 LSTM 把 batch 维度和 seq 维度搞反了。

解决:在dataset.py里打印一下x.shape,确认是[input_len, num_features]。然后在model.py里打印input_size,两边对齐。batch_first=True这个参数检查三遍都不为过。

4.2 预测结果是一条直线

现象:训练 loss 降下去了,但预测曲线几乎是一条水平线,完全没有跟随真实负荷的波动。

原因:归一化没做对,或者反归一化时缩放参数用错了列。还有一种可能是学习率太大,模型直接收敛到了均值附近,LSTM 的时序建模能力根本没发挥出来。

解决:先检查归一化后的数据范围是不是在 [0,1] 之间。然后检查反归一化时用的min_val和max_val是不是目标列的。如果都没问题,把学习率降到 1e-4 再试,同时观察训练 loss 是不是稳定下降。

4.3 验证损失震荡不收敛

现象:训练 loss 正常下降,但验证 loss 上下跳动,没有明显下降趋势。

原因:batch size 太小导致梯度噪声大,或者数据没有打乱,验证集和训练集分布差异大。另外 LSTM 层数太多、hidden_size 太大也会导致过拟合,验证损失反而上升。

解决:把 batch size 调到 32 或 64,训练集shuffle=True。如果还不行,减少 LSTM 层数到 1 层,hidden_size 降到 32。加 dropout 也有帮助,但注意只在多层 LSTM 里加。

4.4 训练速度慢得离谱

现象:一个 epoch 跑了好几分钟,GPU 利用率很低。

原因:数据没有提前转成 tensor,每次__getitem__都在做 numpy 到 tensor 的转换。或者 DataLoader 的num_workers设成了 0,数据加载成了瓶颈。

解决:在Dataset.__init__里就把所有样本转成 tensor 存起来,__getitem__直接返回。DataLoader 的num_workers设成 2 或 4,Windows 下如果报错就设回 0。另外确认模型和数据都在同一个 device 上,CPU 和 GPU 之间来回拷贝也会拖慢速度。

5. 二次开发与改造:从能跑到好用

5.1 换数据集要改哪些地方

这份资源自带数据集,但实际工作中肯定要换成自己的数据。改动集中在三个地方:config.py里的文件路径、dataset.py里的feature_cols索引、以及归一化时目标列的索引。如果新数据的列顺序和原来不一样,feature_cols必须重新指定。我一般会在config.py里加一个TARGET_COL常量,所有涉及目标列的地方都引用它,换数据时只改这一个值。

5.2 加特征和改预测步长

想加入更多输入特征,比如把节假日标签、前一天同时刻负荷都加进去,只需要在 CSV 里增加列,然后更新feature_cols和input_size。预测步长从 4 改成 24 也很简单,改output_len和output_size就行,但要注意预测步长越长,模型越难学,可能需要增加hidden_size或num_layers。我试过把output_len从 4 直接拉到 24,结果 MAPE 从 3% 涨到了 8%,后来把hidden_size从 64 加到 128,又降回了 5% 左右。

5.3 模型替换与对比实验

LSTM 不是唯一选择,GRU 参数更少训练更快,Transformer 在长序列上可能表现更好。替换模型只需要改model.py,保持输入输出维度一致就行。下面是一个 GRU 版本的对比:

class GRUForecaster(nn.Module): def __init__(self, input_size, hidden_size=64, num_layers=2, output_size=4, dropout=0.2): super().__init__() self.gru = nn.GRU( 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): gru_out, _ = self.gru(x) return self.fc(gru_out[:, -1, :])

GRU 把 LSTM 的输入门、遗忘门、输出门简化成了更新门和重置门,参数量少了约 25%,在小数据集上往往收敛更快。我一般会先用 GRU 跑一个 baseline,再用 LSTM 对比,如果差距不大就选 GRU,部署时推理速度更有优势。

5.4 一个容易忽略的验证习惯

改完模型或数据后,不要只看最终的 MAPE,一定要把预测曲线和真实曲线画在一起看。我踩过一次坑:MAPE 只有 2.8%,看起来很好,但画图发现模型在负荷尖峰处全部预测偏低,只是平时平稳时段预测准把整体指标拉回来了。从那以后我每次评估都强制走一遍可视化,先看曲线再看指标,顺序不能反。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询