资讯详情 LSTM与Transformer时间序列预测实战对比:源码解析与调参避坑指南
📅 2026/10/20 10:53:36
简介面向时间序列预测任务的LSTMTransformer混合模型PyTorch实现适合需要多特征输入、单变量输出场景的研究者与开发者可直接迁移到风电、光伏、寿命预测、浓度预测等项目中也适合作为学习Transformer与LSTM结合的入门范例。压缩包共160个文件、约1.94MB以31个Python源码和87个pyc编译文件为主另含4个csv示例数据、xml工程配置、txt说明及备份文件目录结构清晰便于按模型定义、数据加载、训练评估等模块查阅。模型融合Transformer的全局建模能力与LSTM的时序记忆能力代码注释完整支持csv和xlsx格式读取内置ETTh2、ETTm2等多组公开数据以及真实值与预测值对比文件替换数据即可完成训练与验证。已有145人学习可作为时间序列预测的完整参考实现覆盖从数据处理、模型训练到结果评估的完整链路也能快速改造用于自有数据集。1. 时间序列预测选型LSTM与Transformer为什么总被放在一起比在拿到一套带完整源码和数据的 LSTM Transformer 时间序列预测项目时我通常先不急着跑训练而是先看它把两个模型放在一起对比的边界条件。很多人默认 Transformer 一定更强但换到小样本、强噪声的时间序列上LSTM 反而更稳而数据量足够大、序列足够长时Transformer 的多头注意力才能体现出优势。这篇笔记会从源码目录、数据预处理、训练参数、调参对比和常见坑几个层面把这个对比实验完整拆开适合正在做预测、又想在两个模型之间做技术选型的开发者。这里说的“完整源码和数据”核心价值不是两个模型类的实现而是数据怎么切、参数怎么设、坑在哪里正好是新手最容易卡住的地方。2. 源码包的文件结构与数据格式动手前先核对这三处2.1 目录设计与数据文件说明先确认你拿到的是完整源码一个能直接复现的时间序列预测源码文件不应该只有模型定义至少要有数据入口、预处理脚本和训练入口。我拿到压缩包先做三件事打开 config.py 看全局参数打开 data 下的 csv 看第一行和最后一行的时间打开 train.py 看 DataLoader 有没有 shuffle。如果数据是从某天中间开始、时间间隔不固定后面所有结果都没有意义。完整度比较高的源码包目录通常长这样project/ ├── config.py # 全局超参数 ├── train.py # 训练入口 ├── evaluate.py # 测试集评估与绘图 ├── data/ │ ├── raw_series.csv # 原始单变量时间序列 │ └── preprocess.py # 滑动窗口构造样本 ├── models/ │ ├── lstm_model.py # LSTM模型定义 │ ├── transformer_model.py # Transformer模型定义 │ └── __init__.py └── output/ ├── checkpoints/ # 模型权重 └── figures/ # loss曲线和预测对比图这里的核心是 data/preprocess.py 和 models 下的两个模型文件。如果只给了模型代码而没有数据预处理你换自己的数据时还是要重写一遍那这个源码包的复用价值就少了一半。我会先确认原始数据是不是只有时间和数值两列列名是什么时间间隔是小时还是分钟因为后面构造滑动窗口时 seq_len 的单位完全跟着间隔走。时间戳缺失会造成隐性的样本错位比数值异常更难发现。2.2 数据与预处理脚本滑动窗口构造样本的写法时间序列预测本质上是把历史窗口映射到未来窗口的监督学习。常见做法是把一条长度为 N 的序列切成很多个长度为 seq_len 的输入每个输入对应一个长度为 pred_len 的标签。这个步骤看似简单但归一化顺序和时间顺序非常容易写错。# create_sequences.py import numpy as np import pandas as pd def create_sequences(data, seq_len24, pred_len1): xs, ys [], [] for i in range(len(data) - seq_len - pred_len 1): x data[i : i seq_len] y data[i seq_len : i seq_len pred_len] xs.append(x) ys.append(y) return np.array(xs), np.array(ys) df pd.read_csv(data/raw_series.csv) values df[value].values.astype(np.float32) # 先归一化再划分顺序不能反 mean, std values.mean(), values.std() values (values - mean) / std seq_len, pred_len 24, 1 X, Y create_sequences(values, seq_len, pred_len) # 按时间顺序划 70% / 15% / 15% split1 int(len(X) * 0.7) split2 int(len(X) * 0.85) X_train, Y_train X[:split1], Y[:split1] X_val, Y_val X[split1:split2], Y[split1:split2] X_test, Y_test X[split2:], Y[split2:] print(X_train.shape, X_val.shape, X_test.shape)这段代码的关键点有三个。第一先做归一化再切序列不能先切训练集再各自算 mean 和 std否则验证集和测试集的统计信息会泄漏到训练过程里。第二划分必须按时间顺序绝对不能用 sklearn 的 train_test_split 随机打乱因为时间序列一旦打乱模型会看到未来数据验证集分数会虚高。第三seq_len 最好覆盖一个完整周期小时数据至少 24日数据至少 7否则模型学不到周期性。2.3 LSTM和Transformer的模型定义输入输出怎么对齐两个模型在源码包里的定义都比较直接核心是理解输入维度是 (batch, seq_len, input_size)输出维度是 (batch, pred_len)。LSTM 取最后一个时间步的隐藏状态接线性层Transformer 则先做线性升维再经过自注意力编码器同样只取最后一个位置输出。# models/lstm_model.py import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers2, pred_len1): super().__init__() self.lstm nn.LSTM(input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropout0.2) self.reg nn.Linear(hidden_size, pred_len) def forward(self, x): # x shape: (batch, seq_len, input_size) out, _ self.lstm(x) # out: (batch, seq_len, hidden_size) return self.reg(out[:, -1, :]) # 取最后一个时间步# models/transformer_model.py import torch import torch.nn as nn class TransformerPredictor(nn.Module): def __init__(self, input_size1, d_model64, nhead4, num_layers2, pred_len1): super().__init__() self.embed nn.Linear(input_size, d_model) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforward256, dropout0.1, activationrelu, batch_firstTrue) self.encoder nn.TransformerEncoder(encoder_layer, num_layersnum_layers) self.reg nn.Linear(d_model, pred_len) def forward(self, x): # x shape: (batch, seq_len, input_size) src self.embed(x) # 线性升维到 d_model memory self.encoder(src) # 自注意力编码 return self.reg(memory[:, -1, :])需要注意两个实现上的细节。Transformer 里 batch_firstTrue 必须显式设置否则输入输出维度顺序会和 LSTM 混在一起排查起来很耗时间。另一个是位置编码问题上面这个简写版本没有加位置编码自注意力层本身不带顺序信息所以完整源码里通常会在 embed 之后加一个 sinusoidal positional encoding否则序列顺序对模型来说是完全对称的。实际跑的时候我会先用 LSTM 确认数据和 loss 流程没问题再切到 Transformer这样能把模型实现的 bug 和数据问题分开。3. 训练跑通参数配置、训练循环与最小复现命令3.1 参数配置seq_len、batch_size、lr这几个数怎么联动训练时间序列预测模型参数之间不是孤立的。seq_len 决定模型看到多长的历史但它也直接决定样本数量seq_len 越大能切出来的样本越少batch_size 越大梯度越稳定但内存压力也越大学习率往往要根据模型和批量大小一起调。Transformer 对学习率更敏感同样数据下我用 3e-4 能训换成 LSTM 可以稍微加大到 5e-4 或 1e-3。参数建议范围影响seq_len8~128上下文长度也影响样本数pred_len1~24多步预测时误差会累积batch_size16~128太小 loss 不稳太大显存不够lr1e-4~1e-3Transformer 通常比 LSTM 更需要小学习率hidden_size / d_model32~128模型容量小数据别贪大num_layers2~4层数增加拟合能力也增加过拟合风险dropout0.1~0.3防止过拟合尤其是 Transformer源码包里的 config.py 一般写成这样# config.py device cuda if torch.cuda.is_available() else cpu seq_len 24 pred_len 1 batch_size 64 lr 3e-4 epochs 100 hidden_size 64 num_layers 2 d_model 64 nhead 4 dropout 0.2 seed 42这里面需要特别注意 seq_len 和 batch_size 的联动如果你把 seq_len 从 24 调到 48样本量会明显减少这时候 batch_size 可以适当调小否则一个 epoch 里更新次数太少模型收敛变慢。学习率在 batch_size 调大时要对应降低常见的经验是 batch_size 翻倍lr 可以乘以 0.8 左右但这只是起点。3.2 训练主循环早停、梯度裁剪与模型保存训练循环最值得你抄的其实是三句话梯度裁剪、验证集早停、保存最优权重而不是最后一轮权重。时间序列模型在梯度回传时非常容易因为个别尖峰样本产生爆炸梯度不裁剪的话 loss 会突然跳到 NaN。torch.manual_seed(seed) model LSTMPredictor(...).to(device) # 或 TransformerPredictor optimizer torch.optim.Adam(model.parameters(), lrlr) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience5) loss_fn nn.MSELoss() best_val_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 loss_fn(pred, yb) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() train_loss loss.item() val_loss evaluate(model, val_loader) scheduler.step(val_loss) if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), output/checkpoints/best.pth) print(fepoch {epoch} train_loss{train_loss:.4f} val_loss{val_loss:.4f})clip_grad_norm_ 的 max_norm 我一般取 1.0 到 2.0太小会拖慢收敛太大就没有保护作用。保存 state_dict 而不是整个 model好处是后续做模型对比时可以很方便地把同一个结构加载到新的类里。ReduceLROnPlateau 是这套代码里最实用的调度器它会看验证集 loss连续 patience 轮不下降就降低学习率能省下很多手动调 lr 的时间。3.3 判断训练是否正常loss曲线和预测图只看终端打印的 loss 不够我每次训练完都会强制自己画两张图一张是 train/val loss 曲线一张是测试集前 200 个点的真实值对比预测值。很多模型 loss 看起来很漂亮画出来才发现预测曲线就是真实值往右平移了一个点。import matplotlib.pyplot as plt def plot_prediction(y_true, y_pred, pathoutput/figures/pred.png): plt.figure(figsize(10, 4)) plt.plot(y_true[:200], labeltrue) plt.plot(y_pred[:200], labelpred) plt.legend() plt.savefig(path, dpi150) plt.close()画图时需要注意 y_true 和 y_pred 都必须是反归一化之后的值否则标准化过的数据画出来你根本看不出预测值是不是落在真实量纲的合理范围内。如果 val_loss 比 train_loss 明显高一大截基本可以判定过拟合先去调 dropout 或者减少 num_layers如果 train_loss 根本不降先检查学习率和归一化是否生效而不是急着调模型结构。4. 调参对比LSTM 和 Transformer 在时间序列上的真实差距4.1 收敛速度与过拟合该盯训练loss还是验证loss在模拟项目X的逐小时数据上我用同一份数据、同一个训练轮数做过多次对比整体规律很稳定LSTM 在 30 到 50 轮左右开始收敛Transformer 往往要跑到 80 轮以上。更值得警惕的是过拟合Transformer 在小样本上训练 loss 会下降得很快但验证 loss 经常在第 40 轮左右开始反弹而 LSTM 的过拟合来得更晚。所以对比两个模型时千万别只看最后一个 epoch 的结果。正确做法是记录每个 epoch 的验证 loss取最小值对应的权重做测试同时打印那个最小值出现的 epoch。如果 Transformer 的 best val loss 出现在第 90 轮而 LSTM 在第 35 轮那 Transformer 的“更好”是以多出一倍多训练轮数为代价的这个成本要在技术选型里算清楚。4.2 Transformer 的四个必调参数d_model、nhead、num_layers、dropout的边界Transformer 参数比 LSTM 多但真正需要反复动的就四个d_model、nhead、num_layers 和 dropout。d_model 是自注意力向量的维度它必须能被 nhead 整除这是 PyTorch 里最常见的报错来源。nhead 一般取 4 或 8小数据上取 4 更稳num_layers 推荐 2 到 3超过 4 层在小样本上几乎必过拟合。dropout 我建议从 0.1 起步验证集 loss 反弹就加大到 0.2 或 0.3。d_model 64 nhead 8 # 必须保证 d_model % nhead 0 num_layers 3 dropout 0.2 # dim_feedforward 默认是 d_model * 4源码包里通常保留默认这里有一个隐藏陷阱如果你把 d_model 从 64 调到 128dim_feedforward 如果不跟着改默认还是 512模型参数量会瞬间翻倍训练时间增加非常多但预测精度未必提升。我的经验是样本量不到一万时d_model 64 和 128 的差距通常小于 0.5%完全没必要追大模型。4.3 数据规模与序列长度什么时候Transformer不值得上判断用 LSTM 还是 Transformer不需要看论文里的理论优势直接看三个条件样本量、序列长度、预测步长。样本少于 5000 条Transformer 很难发挥注意力机制的全局建模能力反而因为参数量大更容易过拟合序列长度小于 50注意力能看到的上下文有限优势同样不明显如果是多步递归预测LSTM 的误差累积通常更可控。场景推荐模型原因样本 5000LSTMTransformer 参数量大容易过拟合样本 10000 且 seq_len 64Transformer长序列下注意力优势明显单步预测差距不大模型容量不是主要瓶颈多步递归预测LSTM 更稳误差累积对注意力扰动更敏感这个表不是绝对的但它能帮你在一开始就把精力放在正确的模型上。我见过不少开发者拿着上千条数据硬上 Transformer调了三天参数还不如 LSTM 跑一个默认配置问题就出在选型前置条件没想清楚。5. 复现与部署避坑指南时间序列预测源码里最常见的5个坑5.1 预测曲线整体滞后一个点现象验证集 loss 很低但把预测结果画出来发现曲线明显比真实值晚了一个时间步峰值全部向右错位。原因单步预测任务中模型学到的最省力策略是“用上一个时刻的值近似下一个时刻”尤其数据连续性强时这种滞后预测的 loss 并不会很大。解决第一用差分数据训练把原始值变成相邻时刻的差值模型必须学会变化趋势而不是抄上一时刻第二评估指标不要只用 MSE加一个对滞后敏感的指标或者直接看多步预测结果第三检查数据是否被误排序过比如 pandas 的 sort_values 把原本的时间顺序打乱就会造成这种看起来正确其实是泄漏的表现。5.2 训练到一半 loss 变成 NaN现象前几十个 epoch loss 正常下降突然某一个 batch 后 loss 变成 nan后续训练无法恢复。原因最常见的三个来源是数据里有 inf 或缺失值、学习率过大、模型梯度爆炸。时间序列原始数据里偶尔会出现空值如果用某些方式填充成极大值归一化后 std 可能变成异常值。解决在预处理脚本里加一行np.isfinite(values).all()确保没有异常值把学习率从 3e-3 降到 3e-4训练循环里加上梯度裁剪clip_grad_norm_。如果是 Transformer还要检查序列里有没有 padding 位置参与了注意力计算未加 mask 的话 padding 位置会放大梯度。5.3 Transformer 训练比 LSTM 慢好几倍现象同一份数据Transformer 每个 epoch 的耗时是 LSTM 的三到五倍显卡利用率还不高。原因自注意力复杂度是 O(seq_len^2)seq_len 越长差距越明显另外 TransformerEncoderLayer 内部包含两次 layer norm 和一次前馈网络计算量本身就大。解决如果序列长度没超过 64LSTM 通常更划算。非要上 Transformer 的话先把 num_layers 降到 2dim_feedforward 降到 128d_model 降到 32很多任务不会因此变差。GPU 上可以试一下 torch.compile但对小模型提升有时候只有 10%不值得为了它折腾环境。5.4 预测结果几乎是一条水平线现象测试集上预测值全部落在真实值均值附近完全看不到波动峰值被抹平。原因回归任务用 MSE模型对离群值惩罚很大会倾向于输出更安全的条件均值另一种可能是输出层被误加了 Sigmoid导致输出被压缩到 0 到 1 之间还有一种情况是标签做了归一化但训练代码忘了对预测做反归一化导致画图时所有值都挤在零附近。解决检查模型最后的线性层是否被激活函数覆盖确认评估脚本里对预测值和真实值用了同一组 mean 和 std 做还原如果数据方差大可以考虑 HuberLoss 代替 MSE它不会让模型为了几个尖峰牺牲整体波动。5.5 复现指标和源码包描述对不上现象在本地跑出来的结果比描述里的差很多有时候换一台机器结果也不同。原因随机种子没有固定、DataLoader 里 shuffleTrue 导致训练样本顺序变化、GPU 上的浮点计算本身存在不确定的累加顺序、PyTorch 版本差异也会改变 Transformer 的实现细节。解决在训练入口最前面固定所有随机源代码可以这样写def seed_everything(seed): import random, numpy as np, torch random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)DataLoader 里要传generatortorch.Generator().manual_seed(seed)并且设置shuffleFalse或固定 generator否则即使固定了脚本里的 seed每次加载数据的顺序还是可能变。记录环境版本比重新训练更重要我一般会把 PyTorch 版本、操作系统、GPU 型号直接写在实验记录文本里方便后面排查差异。6. 把源码改成自己的数据递归多步预测与反归一化6.1 用训练好的模型做递归多步预测一个可套用的脚本源码包里的模型输出 pred_len 个点但实际业务里往往需要预测未来更长的区间。常见做法是递归预测把模型输出的第一个点拼到历史窗口末尾去掉最旧的点再继续预测下一个点直到生成想要的长度。这个脚本可以直接套到训练好的 LSTM 或 Transformer 上def recursive_predict(model, history, pred_len, device): model.eval() preds [] x torch.tensor(history, dtypetorch.float32).unsqueeze(0).to(device) with torch.no_grad(): for _ in range(pred_len): y model(x) # y: (1, pred_len) pred y[0, -1].item() preds.append(pred) new_val torch.tensor([pred], dtypetorch.float32).to(device) new_val new_val.unsqueeze(0).unsqueeze(-1) x torch.cat([x[:, 1:, :], new_val], dim1) return torch.tensor(preds)这里面有两个容易踩的地方。历史窗口必须和训练时的 seq_len 一致否则维度拼接会报错输出结果 y 取y[0, -1]是因为模型多步输出时最后一个位置通常聚合了更多信息实际效果可以试。递归预测的误差会随着步长累积预测 24 步以后曲线往往会变成一条斜线这时候不要硬调递归而是改成窗口滚动的方式每预测一个点就重新从真实数据里取新的历史窗业务允许延迟时这种半滚动方式更准。我之前吃过一次亏模型训练完直接拿标准化后的输出画图所有预测值都缩在零附近后来才想起要对预测做反归一化。现在我的习惯是把还原真实尺度的逻辑单独写成一个函数放在 predict 脚本里训练脚本永远只输出标准化后的数值。这样不管你换了哪份数据画图之前都会先经过反归一化不会再犯这个低级错误。希望帮到你。本文还有配套的精品资源点击获取