ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PyTorch中LSTM多变量多输出时间序列预测实战指南

PyTorch中LSTM多变量多输出时间序列预测实战指南 简介面向深度学习初学者与时间序列预测入门者这是一份基于PyTorch实现的LSTM多变量多输出预测最小用例。代码构造了由sinx、cosx、tanx组成的多维序列以滑动窗口方式取前50个点预测第51个点清晰展示多变量输入与多步/多输出结构的数据组织、模型搭建及训练流程。整个资源仅1个Python源文件压缩包大小约1KB轻量聚焦无冗余依赖适合快速读懂核心逻辑后迁移到自身场景。目前已有8299人学习下载属于高人气的基础示例。通过运行该文件读者可以直观对比LSTM对于不同周期函数序列的拟合效果理解如何把原始序列切成样本、如何设置输入张量维度、如何定义多输出回归目标并可作为后续引入注意力机制、更多层LSTM或真实业务数据的改造起点。1. 为什么「多变量多输出」才是 LSTM 预测的常态打开任意一篇 LSTM 时间序列教程十个里有九个在教「多对一」拿过去 24 小时的数据预测下一个时刻的温度。但真实业务里几乎没有这种需求——容量规划要的是未来 12 小时的压力曲线设备预警要看未来几帧的趋势拐点电网负荷预测则直接要求未来 24 点的取值。多对一模型一次只能跑出一步生产上不得不写循环逐点外推误差一点点叠上去预测值很快就退化成一条水平线。「多输出」在这里不是指多任务学习里的一次预测多个标签而是指单次前向计算直接给出未来若干个时间步的预测序列。与它相对的做法是「递归多步预测」把上一步的预测值塞回输入再推下一步。「基于 pytorch 的 LSTM 多变量多输出时间序列预测使用例」这条链路要解决的实际上是一个工程问题输入侧怎么组织多变量窗口模型侧怎么一次性吐出一段未来序列以及训练和评估时哪些环节会暗中破坏你的结果。这篇直接从数据和代码讲起照样能跑新人跟得住有人想直接抄了改自己的字段也能省不少弯路。2. 数据准备把多变量时间序列切成 LSTM 的滑窗样本2.1 多变量输入的 shape从 DataFrame 到 (batch, seq_len, features)LSTM 在 pytorch 里接受的输入是三维张量形状为(batch, seq_len, input_size)。其中seq_len是滑动窗口长度input_size是每一时刻的特征数。如果你的数据是四列特征比如温度、压力、流量、转速那么input_size 4这个数字直接喂给模型构造函数的第一个参数。先造一份模拟数据四列特征、目标列取第一列。多数业务表长这样读进来之后先做一件事把 DataFrame 的行顺序按时间排好索引重置干净否则后面切窗口时会出现跨天错位。常见做法是df.sort_values(timestamp).reset_index(dropTrue)这一步看起来多余但漏掉它的话滑窗样本里会混入乱序片段。import numpy as np import pandas as pd import torch from torch.utils.data import Dataset, DataLoader np.random.seed(42) n 2000 t np.arange(n) data pd.DataFrame({ feature1: np.sin(t / 50) t / 1000, feature2: np.cos(t / 60) np.random.normal(0, 0.1, n), feature3: np.random.randn(n) * 0.5 0.02 * t, feature4: t % 24, })这里故意让四列的量纲完全不同后面会解释为什么必须归一化。接下来把滑窗函数写出来每个样本取连续input_size个时刻的 4 个特征作为x再取紧接其后的pred_size个时刻的目标列作为y。def make_samples(df, input_steps24, pred_steps12, target_colfeature1): x, y [], [] for i in range(len(df) - input_steps - pred_steps 1): x.append(df.iloc[i:i input_steps].values.astype(np.float32)) y.append(df.iloc[i input_steps:i input_steps pred_steps][target_col].values.astype(np.float32)) return np.array(x), np.array(y) x_all, y_all make_samples(data) print(x_all.shape, y_all.shape) # (1965, 24, 4) (1965, 12)注意窗口循环结束时i的上界总样本数不是n - input_steps 1而是还要再减去pred_steps多留出预测段。x_all的维度是(样本数, 24, 4)这正好对得上 LSTM 要求的(batch, seq_len, input_size)。y_all是(样本数, 12)即每个输入窗口对应 12 个未来目标值。2.2 归一化要先 fit 训练集不能全量 fit多变量输入里不同特征量纲不同LSTM 内部的门控机制依赖 sigmoid 和 tanh输入数值过大或者过小都会让门饱和。常规做法是MinMaxScaler缩放到 [0,1]但必须拆成两步from sklearn.preprocessing import MinMaxScaler train_len int(len(data) * 0.8) train_df data.iloc[:train_len] scaler_x MinMaxScaler().fit(train_df.values) scaler_y MinMaxScaler().fit(train_df[[feature1]].values) # 全量转换但 scaler 只用训练集拟合过 scaled_x scaler_x.transform(data.values).astype(np.float32) scaled_y scaler_y.transform(data[[feature1]].values).astype(np.float32)一句话说明这里的坑如果先对整张表fit验证集和测试集的分布信息已经被模型“提前看到”了。归一化参数里包含测试集的最大最小值评估结果会比真实泛化能力好看一截。工业上常见做法是把时序切好后对训练段单独fit再用于 transform 后面所有数据。目标变量单独一个 scaler 也值得说一句如果预测目标要还原成物理单位反归一化时用同一个scaler_y.inverse_transform才能对上。2.3 用 Dataset 封装滑窗DataLoader 的 shuffle 在验证集要关掉pytorch 的数据加载流程里写一个Dataset子类是最稳的做法切窗口、归一化、划分集全都可以收敛到一个类里class TimeSeriesDataset(Dataset): def __init__(self, x, y): self.x torch.tensor(x, dtypetorch.float32) self.y torch.tensor(y, dtypetorch.float32) def __len__(self): return len(self.x) def __getitem__(self, idx): return self.x[idx], self.y[idx] train_ds TimeSeriesDataset(scaled_x[:train_len - 24 - 12], scaled_y[:train_len - 24 - 12]) val_ds TimeSeriesDataset(scaled_x[train_len - 24 - 12:], scaled_y[train_len - 24 - 12:]) train_loader DataLoader(train_ds, batch_size64, shuffleTrue) val_loader DataLoader(val_ds, batch_size256, shuffleFalse)训练集的shuffleTrue验证集和测试集要shuffleFalse。时间序列的验证集是模拟“未来”的打乱顺序会让邻近样本互相泄露误差估计偏乐观。严格一点的做法是滚动验证而不是随机划分但作为使用例用时间序列的最后 20% 当验证集就足够看出问题。3. 搭一个 pytorch 的 LSTM 多变量多输出模型3.1 LSTM 参数与 batch_first 的对应关系nn.LSTM的核心参数是input_size、hidden_size、num_layers、batch_first。多变量输入决定了input_size多输出决定全连接层输出维度。batch_firstTrue是 pytorch 里最容易漏掉的一个参数——默认False时输入形状是(seq_len, batch, input_size)数据维度对不上运行时直接报错。hidden_size没有确定公式从业者一般从input_size * 4起步再看验证集收敛情况往上加。太小欠拟合抓不住多变量之间的关系太大容易把噪声背下来尤其时间序列样本量一般不大。num_layers2是性价比比较高的选择但要注意num_layers 1时模型需要在层间加 dropout否则第二层会退化。3.2 直接多步输出的模型代码和逐行解释import torch.nn as nn class LSTMNet(nn.Module): def __init__(self, input_size4, hidden_size64, num_layers2, pred_steps12): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropout0.2 if num_layers 1 else 0.0, ) self.fc nn.Sequential( nn.Linear(hidden_size, hidden_size * 2), nn.ReLU(), nn.Linear(hidden_size * 2, pred_steps), ) def forward(self, x): out, (hn, cn) self.lstm(x) # out: (batch, seq_len, hidden_size) last_out out[:, -1, :] # 最后一个时间步的隐状态 y self.fc(last_out) # (batch, pred_steps) return y直接多步输出的关键决策在out[:, -1, :]。LSTM 每个时间步都会产出一个隐状态out[:, -1, :]是最后一个输入时刻的隐状态它汇总了整个 24 步窗口的信息。也可以取返回的hn[-1]但当num_layers 1时hn里存的是每一层最后一个状态下标容易搞混直接取最后一层输出的最后一步更直观。fc层把 64 维的隐状态乘以 2 再降到 12相当于在这个紧凑表示上做了一个非线性回归输出 12 个未来时刻的预测。nn.Sequential里塞一个ReLU是惯例隐藏层给一点非线性。还有一个取舍要明确这种结构不是 seq2seq。真正的 seq2seq 还需要 decoder 逐时间步解码训练成本和调试复杂度都更高。直接多步输出的 LSTM 在多数单序列预测场景下已经够用是「使用例」里最不容易翻车的方案。3.3 直接多步输出和递归多步的取舍对比维度直接多步输出递归多步输出训练复杂度一次前向得到全部 12 步loss 直接算需循环或 teacher forcing实现复杂误差累积各步独立输出后续步不依赖前序预测预测步越远误差被逐步放大部署推理单次前向延迟低要循环跑多次延迟随步数翻倍常见问题远期步与近期步共享同一个隐状态表示远期精度通常低于近期累积误差导致远期接近均值回归递归多步的核心思路是把上一步的预测值拼接回输入继续推理下一步。它的优点是天然适配变长预测缺点是训练时若用真实值递归、推理时用预测值递归分布不一致效果往往比直接多步差。直接多步输出的问题则在于模型要在一个向量里同时表达未来 1 步和未来 12 步的规律远期步的监督信号容易被平均掉。这个弱点后面第五章会讲怎么压制。4. 训练循环、反归一化与多输出评估4.1 最小训练循环里的几个关键参数多输出 LSTM 的训练循环和普通分类任务没有本质差别但有几个参数需要专门说明。优化器用 Adam 没问题weight_decay建议开到1e-4到1e-5能把 LSTM 在时序样本上的过拟合压住。学习率从1e-3起步loss 在验证集上震荡不降时衰减为原来的 0.5。import torch.optim as optim model LSTMNet(input_size4, hidden_size64, num_layers2, pred_steps12) optimizer optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) criterion nn.MSELoss(reductionmean) clip_value 1.0 for epoch in range(30): model.train() train_loss 0.0 for xb, yb in train_loader: optimizer.zero_grad() pred model(xb) # (batch, 12) loss criterion(pred, yb) # yb: (batch, 12)直接对齐 loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), clip_value) optimizer.step() train_loss loss.item() * xb.size(0) model.eval() val_loss 0.0 with torch.no_grad(): for xb, yb in val_loader: pred model(xb) val_loss criterion(pred, yb).item() * xb.size(0) print(epoch, train_loss / len(train_ds), val_loss / len(val_ds))预测值pred和真实值yb的形状都可以直接对齐为(batch, 12)这就是多输出模型在 loss 计算上的优势不需要像 seq2seq 那样展开序列。梯度裁剪clip_grad_norm_是 LSTM 训练中很容易被跳过但价值很高的一步——长时间序列容易梯度爆炸不裁剪的话 loss 偶尔会突然跳成nan。model.eval()配合torch.no_grad()是在验证阶段必须成对出现的前者关掉 dropout后者关掉梯度记录两者一起才保证验证数值真实反映模型状态。4.2 多输出预测的评估要用 RMSE 而不是 R² 或单一 loss多输出的评估有一个容易被忽略的特点预测步越远误差整体上会越大。这时候用一个整体 loss 会把 12 步的误差平均成一个数掩盖掉「前 3 步很准、后 9 步已经不能用」的事实。常见做法是分别计算每个预测步的 RMSE 和 MAE。from sklearn.metrics import mean_squared_error, mean_absolute_error model.eval() all_pred, all_true [], [] with torch.no_grad(): for xb, yb in val_loader: all_pred.append(model(xb).numpy()) all_true.append(yb.numpy()) all_pred np.vstack(all_pred) all_true np.vstack(all_true) pred_inv scaler_y.inverse_transform(all_pred) true_inv scaler_y.inverse_transform(all_true) step_rmse np.sqrt( np.mean((pred_inv - true_inv) ** 2, axis0) ) # 长度 12每个元素对应第 k 步预测 for k, rmse in enumerate(step_rmse, 1): print(fstep {k:2d} RMSE {rmse:.4f})step_rmse是在反归一化之后算的不然数字直接是 [0,1] 缩放过后的值业务上没有任何含义。逐 step 的 RMSE 曲线可以清晰地看出预测质量的退化点如果前 6 步 RMSE 还在可接受范围第 7 步之后陡增那就说明该收敛预测长度或者在最后一步的策略——也就是下一章的内容——做操作。4.3 反归一化时注意样本边界对齐反归一化这一步卡住过不少人。scaler_y.inverse_transform要求输入形状必须是(n, 1)或(n, n_features)而多输出模型的pred是(batch, 12)直接把 12 步当特征列送进inverse_transform多数情况下也能工作——因为 12 列和 scaler 训练时的 1 列不一致sklearn 会报错。最稳的姿势是先reshape(-1, 1)展平再变换最后再重塑回(batch, 12)。上面的代码里对(batch, 12)直接调用其实有个隐含前提scaler_y在fit时用的是形状(n,1)要求成n_features必须一致所以要么 reshape 再转要么从一开始就按列数设计 scaler不能想当然。5. 多步预测误差抑制给输出端加指数权重与滚动回填验证5.1 对不同未来步的 loss 施加非均匀权重直接多步输出模型面临一个典型矛盾远期预测本身更难但训练时远期步的误差和近期步被平均对待。常见做法是给 loss 按步数施加递减或递增权重。想让模型优先保证近期精度权重从 1.0 衰减如果业务更关注远期趋势权重递增也一样合理关键是权重结构要符合业务对误差的容忍方向。weights torch.linspace(1.0, 0.5, pred_steps).view(1, -1) for xb, yb in train_loader: optimizer.zero_grad() pred model(xb) loss (criterion(pred, yb, reductionnone) * weights).mean() loss.backward() optimizer.step()torch.linspace(1.0, 0.5, 12)生成的是从 1.0 均匀过渡到 0.5 的 12 个权重代表近期步在 loss 里占的比重大、远期步放宽要求。F.mse_loss(reductionnone)返回逐元素误差矩阵(batch, 12)与权重逐元素相乘后取均值。如果没有这一步模型往往会在后期步上花大量容量去拟合一个信噪比很低的目标反过来拖累近期步的精度。5.2 用滚动回填找出误差退化点加了权重之后验证时再做一次滚动回填。思路是先用直接多步输出预测 12 步取前 6 步作为可信区间把它拼回输入窗口尾部去掉输入窗口最前面的 6 个旧时刻再重新预测得到后半段的更稳结果。这个混合策略在工程里比纯递归效果好因为窗口里保留的是真实观测到第 6 步而不是把第 1 步预测结果当作事实一路滚到底。实现上不需要改模型只需要在验证循环外对输入做切片拼接。torch.cat时要保证类型和维度一致而且输入窗口seq_len必须保持 24否则重新进 LSTM 直接形状错误。判断回填点放在哪一步最直观的依据就是 4.2 节输出的step_rmse曲线看它在哪一步开始超过业务阈值就把回填点设在那里。先把逐步 RMSE 画出来再决定比一上来就改模型结构要省时间得多。下面这段话放到文章末尾正好落在一个可操作的建议上给输出端不同步设置权重、用滚动回填做预测这两件事都建立在对「第几步误差变大」这件事有清晰认知的基础上。建议先把步进 RMSE 曲线打印出来再看权重要不要从 1.0 衰减到 0.5还是反过来增大远期权重。把退化点找到再决定你要调整的是预测长度、输入窗口长度还是 hidden_size。本文还有配套的精品资源点击获取
返回列表