☰
LSTM与Transformer时序预测实战:PyTorch实现与避坑指南
2026/10/1 3:40:12 网站建设 项目流程

简介:这是面向深度学习初学者和时间序列分析人员的预测源码包,基于长短期记忆网络和Transformer模型,用Python实现,覆盖数据读取、预处理、模型构建、训练评估与预测全流程,适合空气质量、电力负荷、交通流量等时序预测实验。压缩包共有三十一个文件,约二十八点四兆,主要类型为三个Python脚本、三份CSV时序数据、十四张示意图、七个工程配置文件及一份说明文档;时序数据涵盖电力变压器温度、空气质量污染指数等历史记录,模型状态文件可保存训练好的权重,便于加载后直接复现。示意图展示网络结构和预测曲线对比,文档结合图片讲解数据归一化、序列划分、损失函数、优化器以及均方误差和均方根误差等关键指标,便于新手理解长短期记忆网络的门控机制与Transformer的自注意力机制在时序任务中的实际运用,同时指出环境配置与超参数调整中的常见问题。已有二百一十六人学习,适合课程设计、毕业设计或深度学习入门实践参考。

1. LSTM和Transformer做时序预测的选型逻辑:数据规模和序列长度决定一切

先把话放在前面:LSTM和Transformer做时序预测,并不是谁比谁先进的问题。我在设备寿命预测和金融时序预测上折腾了大半年,最直观的体会是——数据量小、序列短、周期明显的场景,LSTM足够稳;数据量大、序列长、规律杂的信号,Transformer上限更高,但源码实现里到处都是坑,位置编码、学习率、掩码,哪一步不到位预测结果就废掉。这篇要讲的是一套基于PyTorch的可复现源码和落地路径,从数据窗口化、模型定义、训练循环到避坑,适合已经会Python、想把自己的时序数据交给模型跑出结果的工程师直接抄作业。

2. 时序数据窗口化与归一化:把时间序列切成模型能吃的样本

2.1 先切分再归一化:这条顺序错了模型会偷看未来

数据准备是时序预测里最容易翻车的环节。常见错误是拿着整条序列,先归一化再做窗口切分,再随机切训练集和验证集。序列预测的样本本质上是同一条时间线的不同片段,随机切会让训练集和验证集互相重叠,验证结果虚高。正确顺序是先按时间线切出训练段和验证段,再在训练段上 fit 归一化参数,用同一套参数 transform 验证段。

用一段模拟数据演示:

import numpy as np from sklearn.preprocessing import MinMaxScaler np.random.seed(42) t = np.arange(0, 1000) # 模拟:线性趋势 + 30 步周期 + 噪声 data = 10 + 0.02 * t + np.sin(2 * np.pi * t / 30) + np.random.randn(1000) * 0.1 train_data, valid_data = data[:800], data[800:] scaler = MinMaxScaler(feature_range=(-1, 1)) train_scaled = scaler.fit_transform(train_data.reshape(-1, 1)).reshape(-1) valid_scaled = scaler.transform(valid_data.reshape(-1, 1)).reshape(-1)

这段代码的核心逻辑是:MinMaxScaler 只在 train_data 上 fit,valid_data 用同一个缩放器做 transform。这样做能避免验证集的最大最小值悄悄进入训练阶段,造成数据泄露。feature_range 取 (-1, 1) 而不是默认的 (0, 1),是因为 LSTM 内部用 tanh 激活,输出值域恰好也是 (-1, 1),对齐后模型更容易收敛;Transformer 虽然对输入值域宽容一些,但统一用 (-1, 1) 也便于两个模型共享同一套预处理。

2.2 滑动窗口构造样本:window_size 与 horizon 怎么设定

拿到归一化后的序列,下一步是把一维序列切成监督学习样本。我习惯把 window_size 理解为模型能看到的历史长度,horizon 是它要预测的未来步数。

def create_dataset(series, window_size=24, horizon=1): X, y = [], [] for i in range(len(series) - window_size - horizon + 1): X.append(series[i:i + window_size]) y.append(series[i + window_size:i + window_size + horizon]) return np.array(X), np.array(y) X_train, y_train = create_dataset(train_scaled, window_size=24, horizon=1) X_valid, y_valid = create_dataset(valid_scaled, window_size=24, horizon=1)

这里 X 的形状是 (样本数, window_size),y 的形状是 (样本数, horizon)。window_size=24 意味着用过去 24 个时间步预测未来 1 个时间步。如果你的数据是小时级,24 代表预测下一个 24 小时的值;如果是日级,window_size 最好能覆盖完整周期,比如周周期就取 7,月度周期取 30,否则模型永远看不到周期性规律。horizon=1 时是最简单的单步预测,做多步预测时 horizon 取 7 或 30,但要注意:horizon 越大,预测误差累积越明显,训练时 loss 也会被多步的误差平均值放大,收敛会变慢。

2.3 用 DataLoader 组织训练批次:别忽视样本重叠问题

切完样本后,把 numpy 数组转成 PyTorch 的张量并组批。多特征数据在 create_dataset 里稍改一下即可,把 series 改成二维数组,窗口切片时按行切。

import torch from torch.utils.data import TensorDataset, DataLoader X_train_t = torch.tensor(X_train, dtype=torch.float32).unsqueeze(-1) y_train_t = torch.tensor(y_train, dtype=torch.float32) X_valid_t = torch.tensor(X_valid, dtype=torch.float32).unsqueeze(-1) y_valid_t = torch.tensor(y_valid, dtype=torch.float32) train_loader = DataLoader(TensorDataset(X_train_t, y_train_t), batch_size=64, shuffle=True) valid_loader = DataLoader(TensorDataset(X_valid_t, y_valid_t), batch_size=64, shuffle=False)

unsqueeze(-1) 是把特征维度补出来,最终 X 的形状是 (样本数, window_size, 1),表示每个时间步只有一个特征。如果你的序列有多个指标,比如同时读温度、压力、振动三个通道,把最后一维改成 3,模型输入尺寸 input_size 跟着改成 3 就行。shuffle=True 对训练集是常规操作,但要注意一点:滑动窗口构造的样本是高度重叠的,相邻样本之间共享大量时间点,随机打乱后批次内的多样性其实有限,这会在后面的避坑章节单独展开。

3. 用PyTorch实现LSTM预测源码:模型定义、训练循环与参数调优

3.1 LSTM为什么适合时序预测:隐藏状态与遗忘门

LSTM 通过遗忘门、输入门和输出门控制信息的长期流动,核心思想是让模型自己决定哪些历史信息要记住、哪些要丢掉。在设备寿命预测这类场景里,传感器序列前期的退化趋势和近期的异常波动都很关键,LSTM 的门控机制天然适合这种长短期混合的信号。相比 Transformer,LSTM 的参数量小得多,几千条训练样本也能训得动,这是它到现在仍是时序预测主力选手的原因。

3.2 LSTM模型定义:hidden_size、num_layers 与 batch_first

直接给一个可用的 LSTM 预测模型定义:

import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, output_size=1, 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.fc = nn.Linear(hidden_size, output_size) def forward(self, x): out, _ = self.lstm(x) # out: (batch, seq_len, hidden_size) out = out[:, -1, :] # 取最后一个时间步的隐藏状态 return self.fc(out)

几个关键参数的直观理解:hidden_size 是隐藏状态维度,决定 LSTM 内部记忆的容量,64 是起步值,数据规律复杂时提到 128 或 256;num_layers 是隐藏层数,2 层是常见配置,超过 4 层在数据量小的时候只会让过拟合更严重;dropout 只对多层 LSTM 的层间连接生效,单层时 dropout 参数会被忽略,所以看到单层模型配 dropout 没有效果不是代码 bug。batch_first=True 让输入形状变为 (batch, seq_len, feature),符合大多数人组织数据的习惯,不加这个参数的话,第一维会被当成 seq_len,很容易在训练时报形状错误。

3.3 训练循环:梯度裁剪与最佳模型保存

LSTM 训练时最常见的现象是 loss 在某个 epoch 突然飙升到 NaN,这是梯度爆炸的典型症状。RNN 系模型存在长时依赖路径,反向传播的梯度在时间维度上连乘,数值很容易溢出。解决办法是梯度裁剪,把梯度范数限制在一个范围里。

def train_model(model, train_loader, valid_loader, epochs=50, lr=1e-3, device='cpu'): model.to(device) criterion = nn.MSELoss() optimizer = torch.optim.AdamW(model.parameters(), lr=lr) best_loss = float('inf') for epoch in range(epochs): model.train() train_loss = 0.0 for xb, yb in train_loader: xb, yb = xb.to(device), yb.to(device) optimizer.zero_grad() pred = model(xb) loss = criterion(pred, yb) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() train_loss += loss.item() * xb.size(0) train_loss /= len(train_loader.dataset) model.eval() valid_loss = 0.0 with torch.no_grad(): for xb, yb in valid_loader: xb, yb = xb.to(device), yb.to(device) pred = model(xb) loss = criterion(pred, yb) valid_loss += loss.item() * xb.size(0) valid_loss /= len(valid_loader.dataset) if valid_loss < best_loss: best_loss = valid_loss torch.save(model.state_dict(), 'best_lstm.pt') if epoch % 10 == 0: print(f'epoch {epoch}, train {train_loss:.6f}, valid {valid_loss:.6f}') return best_loss

这段循环的核心逻辑是:每个 epoch 先用训练集更新权重,再用验证集计算一次不参与反向传播的损失;只有在验证集 loss 创新低时才保存模型权重,避免训练末期过拟合把验证 loss 拉高后,你还在用最后一轮的差模型。clip_grad_norm_ 对全部参数生效,max_norm=1.0 是常用起步值,训练不稳定时降到 0.5,损失长时间不降时放大到 2.0 试试。AdamW 相比 Adam 修正了权重衰减的实现方式,时序预测里用它比用传统 Adam 更稳,学习率先用 1e-3,LSTM 一般不用像 Transformer 那样的 warmup。

3.4 损失函数与评估指标的差别

训练用 MSELoss 是因为它能够让大误差样本产生大的梯度,帮助模型优先学掉明显偏离的部分,而且 MSE 可导、优化平稳。但实际评估模型好坏时,我还会看 MAE 和 MAPE,原因是 MSE 对离群点敏感,金融时序里偶发的跳空数据会把 MSE 撑得很大,掩盖模型在常规区间的真实表现。评估代码一般这样写:

from sklearn.metrics import mean_absolute_error, mean_squared_error pred = model(X_valid_t.to(device)).detach().cpu().numpy() mae = mean_absolute_error(y_valid, pred) mse = mean_squared_error(y_valid, pred) print(f'MAE={mae:.6f}, RMSE={mse**0.5:.6f}')

到这一步,LSTM 路线的完整闭环已经跑通。

4. Transformer时序预测源码拆解:位置编码、编码器配置与训练技巧

4.1 Transformer为什么能用于时序预测:注意力机制与长距离依赖

接着跑Transformer路线。Transformer 的核心优势是自注意力机制让每个时间步都能直接和序列里任意其他时间步交互,路径长度是 1,不像 LSTM 那样要穿越 num_steps 层门控。这意味着在长序列上,Transformer 对远距离依赖的捕获能力更强。常见做法是用编码器结构做监督预测——把滑动窗口的序列喂进编码器,取最后一个位置的输出接全连接头预测未来值。这个结构简单、收敛快,适合回归任务;如果做生成式预测才需要解码器加因果掩码,那是另一个故事。

4.2 位置编码必须自己实现:sin/cos 位置编码与 register_buffer

自注意力是排列不变的,序列顺序信息全靠位置编码带进去,否则模型眼里“昨天的数据”和“三个月前的数据”没有区别。最常见的是用 sin/cos 位置编码,代码可以直接抄:

import math import torch import torch.nn as nn class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len=5000): super().__init__() pe = torch.zeros(max_len, d_model) position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) pe = pe.unsqueeze(0) self.register_buffer('pe', pe) def forward(self, x): return x + self.pe[:, :x.size(1)]

这段代码做的事情是生成一个 (max_len, d_model) 的位置矩阵,偶数维度填 sin,奇数维度填 cos。频率沿维度递增降低,使得不同位置获得不同的相位组合,相当于给序列里的每个时间步一个唯一指纹。register_buffer 注册的张量不会参与梯度更新,但会随模型一起迁移到 GPU,所以不需要担心 device 问题。max_len 设 5000,序列长度超过 5000 时把 x 截断或增大 max_len 都行。

4.3 Transformer预测模型:nn.TransformerEncoder 的配置

模型定义部分,我一般把输入投影、位置编码、TransformerEncoder 和输出头串起来。d_model、nhead、dim_feedforward 三个参数是配置重点。

class TransformerPredictor(nn.Module): def __init__(self, input_size=1, d_model=128, nhead=8, num_encoder_layers=3, dim_feedforward=256, dropout=0.1, output_size=1): super().__init__() self.input_proj = nn.Linear(input_size, d_model) self.pos_enc = PositionalEncoding(d_model) encoder_layer = nn.TransformerEncoderLayer( d_model=d_model, nhead=nhead, dim_feedforward=dim_feedforward, dropout=dropout, batch_first=True ) self.encoder = nn.TransformerEncoder( encoder_layer, num_layers=num_encoder_layers ) self.fc = nn.Linear(d_model, output_size) def forward(self, x): x = self.input_proj(x) # (batch, seq_len, d_model) x = self.pos_enc(x) x = self.encoder(x) # (batch, seq_len, d_model) out = x[:, -1, :] return self.fc(out)

参数选型的要点:d_model 必须能被 nhead 整除,128 配 8 头是最稳的组合;d_model 过小表达能力不够,过大在数据量不足时非常容易过拟合。dim_feedforward 是 FFN 中间层维度,一般取 d_model 的 2 倍到 4 倍,128 的 d_model 配 256 或 512 都常见。num_encoder_layers 从 2 到 4 是常用范围,超过 6 层在普通规模的数据集上只会增加训练难度,不会带来精度提升。nn.TransformerEncoderLayer 从 PyTorch 1.9 开始支持 batch_first=True,这个参数一定要显式写出来,否则默认第一维是 seq_len,你的输入形状会对不上。

4.4 Transformer训练技巧:AdamW 加 warmup

Transformer 对学习率非常敏感,直接用一个固定大学习率训练,前几个 step 很容易把权重推出正常范围,然后 loss 卡在高位不再下降。常见做法是前几个 epoch 线性升温,后面的环节恢复正常策略。

optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3) scheduler = torch.optim.lr_scheduler.LambdaLR( optimizer, lr_lambda=lambda epoch: min(1.0, (epoch + 1) / 5) )

lr_lambda 表示前 5 个 epoch 把学习率从 0.2 倍线性拉升到 1.0 倍,之后保持在设定值。相比 CosineAnnealing 的复杂曲线,这个暖启动方案在回归任务上简单有效,少了很多玄学调参。训练循环和 LSTM 完全一样,但需要把 scheduler 挂在每个 epoch 结束时 step 一下。如果验证集 loss 一直震荡,把 lr 从 1e-3 降到 3e-4,然后重新跑。

5. 时序预测避坑记录:数据泄露、滞后预测与训练震荡的排查经验

5.1 数据泄露:归一化时 fit 到了验证集

现象:训练集和验证集 loss 都很好,模型一上真实环境就完全失效。原因:常见错误是对整条序列做 MinMaxScaler.fit_transform 再做切分。验证集的最大最小值是通过全量数据统计出来的,等价于模型在训练阶段偷偷看到了验证数据的分布范围。解决:先切分,再在训练段上 fit,验证段只用 transform。另一个泄漏点是窗口数据重叠,把训练段最后一个样本之后的验证段样本带进训练集,切分时注意按时间顺序严格断开。

5.2 预测结果滞后:模型学会了把昨天的值搬过来

现象:验证集 MAE 很低,但把预测曲线画出来,发现它和真实值之间有一个明显的平移,看起来像把前一天的观测值往后挪了一步。原因:自相关性强的平稳序列里,最简单的策略就是让预测值约等于输入窗口最后一个值,这种策略在 MSE 下其实是很强的基线。解决方法有三个:一是对序列做差分,用差分序列做训练再对预测结果反向累计还原;二是把 horizon 提高到多步,用更长的误差积累检验模型是否真正学到了动态规律;三是把 MAE、RMSE 之外,额外看拐点时刻的预测误差,滞后预测在信号拐点处通常错得最离谱。

5.3 loss 不降或NaN:梯度爆炸与学习率过大

现象:训练到某个 epoch,loss 突然变成 nan,或者前 10 个 epoch loss 一点不降。原因:LSTM 时间反向传播梯度连乘容易爆炸,Transformer 则是对学习率敏感,过大学习率会把注意力层的权重推向发散。解决:LSTM 用 clip_grad_norm_ 限制梯度范数,从 max_norm=1.0 起步;Transformer 改小 lr 或用 train_model 里的 warmup;还不行就把 batch_size 调小,大 batch 在回归任务上容易让 loss 在初期波动剧烈。给每个方法单独排查比把学习率、层数、batch 一起改有效得多。

5.4 验证集不错、测试集崩掉:切分方式不符合时间顺序

现象:验证集 per-epoch 一直下降,换成后面的测试段,误差直接成倍上涨。原因:滑动窗口把同一时刻附近的样本高度重叠,如果验证集的切分点离训练集太近,验证样本和训练样本共享大量时间点,结果偏高是必然。解决:验证集不要紧贴着训练集切,中间留一段缓冲带,或者直接用滚动回测,模拟出“用过去 T 个点预测未来 T 个点”的真实使用方式。

5.5 样本重叠之下,模型过拟合的判断失真

现象:训练集 loss 越来越低,验证集也跟着降,但是图上看预测曲线几乎是拟合噪声。原因:滑动窗口 stride=1 时,相邻两个样本只有极小的差异,模型可能不是在学规律,而是在记忆相似的片段。解决:构造样本时把步长从 1 调到 2 或 3,减少样本间重叠;还在过拟合的话,把窗口缩短到需要的下限并增加 dropout。

6. 多步预测与模型集成:三个硬指标验证模型值不值得上线

多步预测有两种策略:递归预测是预测一步,把输出拼回输入再预测下一步;直接预测是让模型一次输出多步。我一般优先用直接预测,horizon=7 时输出维度设为 7,损失是多步误差的平均值,梯度可以同时覆盖整个窗口;递归预测的好处是模型结构简单,但误差会沿着 step 累积放大,预测到第 5、6 步时曲线往往已经开始发飘。设备寿命预测这类场景我还会叠加一层回测:用训练好的模型从时间序列的中间点开始,每预测完一个 batch 就把真实值连日更新输入,看误差随步数的累积曲线而不是只看平均 loss,这一步能暴露滞后问题。

模型集成在这个场景下成本低、效果好。把 LSTM 和 Transformer 的预测结果做简单平均,通常比单独调优其中一个模型的极限精度更稳。原因是 LSTM 偏向局部平滑,Transformer 偏向长距离依赖,两者误差的分布模式不同,取平均可以抵消一部分系统偏差。加权平均权重不用精细调,固定 0.5 / 0.5 往往就够用,若某一模型在验证集上明显更优,再往那边倾斜到 0.6 / 0.4。

验证模型价值时,我只看三个硬指标:RMSE 反映整体误差水平;MAE 反映常规区间的平均偏差;MAPE 反映相对误差,适合对比不同量纲的序列。最后看预测序列与真实序列的拟合图,比任何单点指标都诚实。我现在的习惯是:任何时候先跑通 LSTM 基线,再用 Transformer 尝试提升,两套源码共用同一套数据预处理和评估函数,最终选择线上误差更小的模型,或者直接集成。时序预测真正难的从来不是模型,而是怎样把数据切干净、把验证做严谨。希望帮到你。

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

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

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

立即咨询