LSTM与Transformer时间序列预测:PyTorch代码复现与对比
时间序列预测是机器学习里出现频率最高的场景之一LSTM 和 Transformer 也是被放在一起比较最多的两个模型。很多人搜索“LSTM Transformer 时间序列预测 代码复现”时找到的资料要么只讲原理没有可跑代码要么给了完整脚本但输入维度对不上最后卡在数据处理和形状报错上。这篇文章不争论谁一定比谁强而是把两个模型放到同一份时间序列数据上用 PyTorch 完整走一遍从 CSV 读取、窗口切分、模型定义、训练循环到预测结果对比和常见报错排查。我自己动手跑这类项目时有个固定习惯先不问哪个模型更先进先问能不能在 60 秒内把一条像样的训练日志跑出来。看到“2026版”这类标题先别被年份带走注意力。LSTM 和 Transformer 的核心代码不依赖特定年份真正需要确认的是你本地的 Python、PyTorch 版本和数据集格式。下面按实际落地顺序拆开讲。1. 先想清楚这两个模型解决的是同一类预测问题吗很多初学者容易进入一个误区拿到时间序列数据后第一反应是“LSTM 和 Transformer 哪个更强”然后直接套一个看起来更高级的模型。这个顺序其实是反的。你要先回答的问题是我的数据到底需要模型捕捉什么规律。时间序列预测的基本形式都一样给定过去一段连续观测值预测未来一个或多个时间步。但数据内部的结构差异很大。有些序列只依赖最近几个时间点比如设备温度短期内跟随环境变化有些序列依赖长时间周期比如交通流量、电力负荷会同时受一天、一周甚至节假日影响。不同类型的依赖关系适合的模型不一样。1.1 各自擅长的时间序列形态LSTM 是循环神经网络的一种核心思路是按时间步逐个处理数据把前面时刻的信息通过隐藏状态向后传递。它的优点是天然适合“顺序敏感”的序列能把局部变化逐步压缩成更高层特征。对于数据量几千到几万条、序列长度几十到一两百的常见预测任务LSTM 是个很稳的起点。Transformer 的出发点完全不同。它不按时间步逐个读取而是通过自注意力机制一次性看到整个序列直接计算任意两个时间点之间的相关性。这种设计对长距离依赖更友好。比如序列里有很明显的周期性规律Transformer 更容易在训练过程中自动把“每天同一个时段”和“每周同一天”关联起来。但“更友好”不代表“一定更好”。如果序列本身很短或者预测结果主要依赖最近几小时的变化Transformer 的长距离建模优势发挥不出来反而可能因为参数量更大、对数据量更敏感而出现训练不稳定。1.2 Transformer 为什么会出现在时间序列预测里很多人问“为什么最后是 Transformer”而不是别的结构。背后原因不在于它有什么神秘魔法而是自注意力让模型不再需要像 RNN 那样一步步传递状态。RNN 处理长序列时越早的信息经过越多次非线性变换越容易被遗忘或者被无关信息干扰。Transformer 让序列里任意两个位置都有直接通路信息传递路径大大缩短。这个能力放到时间序列里恰好对应一种常见需求跨时间长周期依赖。所以把 Transformer 用于时间序列预测不是一个拍脑袋的搭配而是从文本任务中迁移过来的自然延伸。尤其是电力负荷、交通流量、天气这类有明显周期性的数据Transformer 的结构优势更明显。1.3 什么时候不要盲目换模型我需要把边界说清楚。如果数据量只有一两千条序列长度也只有几十LSTM 往往更容易收敛调参成本也低。Transformer 在这种数据量下很容易过拟合表现出来就是训练集 loss 下降很快验证集 loss 反而波动很大。如果数据量足够大序列长度超过一百而且你能确认数据里有跨时段的依赖关系再考虑把 Transformer 作为对比模型。最稳的做法不是“二选一”而是两个都跑一版用同样的数据切分、同样的评价指标做对比。这也是代码复现最有价值的部分同一个数据集上模型之间的差异才真正可比。2. 复现前的环境准备和一份能说明问题的测试数据很多复现项目跑不起来不是模型代码写错而是环境、数据、形状三者没有对齐。代码复现的核心不是把 GitHub 上的脚本下载下来运行一次而是理解数据从原始文件到训练张量的完整流向。2.1 依赖版本与运行条件先确认基本环境。这里的版本不是某个模型的硬性要求而是常见组合实际以你本地环境为准。Python3.9 或更高版本3.10、3.11 都可以。PyTorch2.x 版本CPU 版也能跑小规模示例但训练速度会慢不少。其他库pandas、numpy、matplotlib、scikit-learn。GPU 不是必须的。如果只是把下面这个示例跑通CPU 完全够用。但如果后续要做多变量、长序列或者批量调参建议显存至少 4GB否则很容易在 batch size 上调不动。我在测试环境里习惯先打印一个系统信息确认import torch print(torch.__version__) print(torch.cuda.is_available())这一步能提前排除很多版本兼容问题。2.2 数据的选择和预处理如果只是为了复现模型流程不建议一上来就用很复杂的业务数据。最好选一份结构简单、规律明显的单变量序列。你可以用公开的电力负荷数据、交通流量数据也可以用自己生产的传感器数据。这里给一个通用读取流程import pandas as pd df pd.read_csv(your_data.csv, parse_dates[date]) df df.sort_values(date) values df[value].values.reshape(-1, 1)数据格式的关键点有几个时间列必须解析成 datetime不能当作字符串。数据要按照时间升序排列不能因为采集顺序混乱而丢失时序关系。如果原始数据里有缺失值先做插值或删除不能带着缺口直接进模型。2.3 时间序列不能乱切训练集、验证集、测试集这一点最容易忽略。普通机器学习任务切分数据时经常随机打乱但时间序列不行。因为样本之间有时间先后关系测试集必须严格放在训练集之后否则等于让模型“偷看未来”。我一般按 7:2:1 或者 6:2:2 切分且严格按照时间顺序train_size int(len(values) * 0.7) val_size int(len(values) * 0.2) train_values values[:train_size] val_values values[train_size:train_size val_size] test_values values[train_size val_size:]另一个容易踩坑的地方是归一化。StandardScaler 只能用训练集数据拟合再用同一个 scaler 去转换验证集和测试集。如果对整个数据集先做标准化再切分测试集的信息会混入训练过程这会高估模型效果。正确顺序是先切分再 fit 训练集上的 scaler然后 transform 三个集合。3. 先把 LSTM 跑通从模型定义到训练循环我建议不管最终要不要用 Transformer都先把 LSTM 完整跑通。LSTM 的结构更直观训练也比 Transformer 稳定适合作为理解时间序列建模的基线。3.1 LSTM 到底在训练什么参数看一个简单的 LSTM 预测模型时重点关注三处参数input_size每个时间步的特征数。单变量预测就是 1。hidden_size隐藏状态维度决定模型容量。num_layersLSTM 层数层数越多模型能表达的非线性关系越复杂但越容易过拟合。LSTM 的输入形状是[batch_size, seq_len, input_size]输出通常是每个时间步的隐藏状态。做单步预测时一般只取最后一个时间步的输出再接一个全连接层映射到预测值。3.2 最小可运行的 LSTM 预测脚本先实现一个简单的 LSTM 模型import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers2, dropout0.1): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout ) self.fc nn.Linear(hidden_size, 1) def forward(self, x): # x: [batch, seq_len, features] out, _ self.lstm(x) # 取最后一个时间步的输出 last out[:, -1, :] # [batch, hidden_size] return self.fc(last)然后用滑动窗口构造训练样本。这里seq_len表示用过去多少个点预测未来一个点import numpy as np def make_windows(data, seq_len24, pred_len1): X, y [], [] for i in range(len(data) - seq_len - pred_len 1): X.append(data[i:i seq_len]) y.append(data[i seq_len:i seq_len pred_len]) return np.array(X), np.array(y) X_train, y_train make_windows(train_values, seq_len24) X_val, y_val make_windows(val_values, seq_len24)训练循环按标准流程写from torch.utils.data import DataLoader, TensorDataset train_dataset TensorDataset( torch.tensor(X_train, dtypetorch.float32), torch.tensor(y_train, dtypetorch.float32) ) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue) model LSTMPredictor(input_size1, hidden_size64, num_layers2) optimizer torch.optim.Adam(model.parameters(), lr0.001) criterion nn.MSELoss() for epoch in range(30): model.train() total_loss 0 for x_batch, y_batch in train_loader: optimizer.zero_grad() pred model(x_batch) loss criterion(pred, y_batch) loss.backward() optimizer.step() total_loss loss.item() print(fepoch {epoch 1}, loss: {total_loss / len(train_loader):.6f})训练时shuffleTrue是可以的因为每个样本本身已经保留内部时间顺序打乱的是样本之间的顺序不会把时间先后破坏掉。验证和测试时shuffleFalse。3.3 第一次训练后怎么看结果训练结束后把模型切到 eval 模式用测试集预测并画图model.eval() with torch.no_grad(): pred_test model(torch.tensor(X_test, dtypetorch.float32)).numpy()判断结果不能只看 loss 数字。我更关注三件事loss 是否在前几个 epoch 明显下降后趋于平稳。预测曲线是否和真实曲线在趋势上贴合。极端变化处是否有明显滞后。如果预测曲线整体平移、峰值滞后常见原因不是模型坏了而是窗口长度太短、序列趋势太强或者直接多步预测时误差累积。先不要急着换模型。4. 再把 Transformer 搬过来结构差异和必须改的输入格式LSTM 跑通之后再上 Transformer 会容易很多。因为数据处理、训练循环、评价方式都可以复用主要改的是模型内部结构。4.1 Transformer 输入和 LSTM 的本质区别LSTM 按时间步递归Transformer 则把整个序列作为一个整体计算。所以 Transformer 对输入形状的要求不同并且通常需要位置编码来告诉模型时间顺序。很多教程会把 Transformer 原版的 encoder-decoder 结构完整搬过来但时间序列单步预测场景不一定需要 decoder。如果任务只是用过去 24 个点预测未来 1 个点用 encoder 部分加一个线性输出层就够了。加上 decoder 反而会让模型更复杂对小数据更容易过拟合。4.2 一个适合时间序列的简化 Transformer 模型这里给出一个基于 PyTorch 自带TransformerEncoder的简化实现import torch import torch.nn as nn import math class TimeSeriesTransformer(nn.Module): def __init__(self, input_size1, d_model64, nhead4, num_layers2, dropout0.1): super().__init__() self.d_model d_model self.input_proj nn.Linear(input_size, d_model) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, batch_firstTrue, dropoutdropout ) self.encoder nn.TransformerEncoder(encoder_layer, num_layersnum_layers) self.fc nn.Linear(d_model, 1) def forward(self, x): # x: [batch, seq_len, input_size] x self.input_proj(x) x x * math.sqrt(self.d_model) x self.encoder(x) last x[:, -1, :] return self.fc(last)这个模型没有显式加入位置编码。对于小规模时间序列有时也能跑通但我不建议长期这么用。序列一旦超过几十个点位置信息就很重要。可以改成在输入投影后叠加正弦位置编码def add_positional_encoding(x): # x: [batch, seq_len, d_model] seq_len x.size(1) d_model x.size(2) pe torch.zeros(seq_len, d_model) position torch.arange(0, seq_len, dtypetorch.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) return x pe.to(x.device)训练循环和 LSTM 一样只需要把模型换成TimeSeriesTransformer。4.3 同样的数据为什么结果和 LSTM 不一样Transformer 在同样的数据上可能比 LSTM 更快看到周期性规律也可能出现 loss 抖动、不收敛、甚至变成 NaN。这不是代码写错而是 Transformer 对训练设置更敏感。最常见的问题是学习率。LSTM 用 0.001 通常可以Transformer 有时需要更小比如 0.0001 到 0.0005。另外batch size 太小时Transformer 训练会很不稳定因为每个 batch 的注意力分布波动更大。如果你发现 Transformer 在验证集上的效果不如 LSTM不需要惊讶。这是常态。Transformer 的优势在长序列和大数据上更容易显现短序列小数据上未必能胜出。5. 训练中的关键参数和排查顺序模型跑通只是第一步真正花时间的是调参和排查。这一部分我按照实际踩坑频率来写。5.1 常见参数的默认起点和调整方向参数常用起点调整方向备注learning rate0.001loss 不降就降低到 0.0005 或 0.0001Transformer 对学习率更敏感batch size32显存不足降到 8 或 16太小容易训练不稳定seq_len24根据数据周期调整可按 7、30、90 灰度太短学不到周期太长增加计算量hidden_size / d_model64数据量足够再增加到 128 或 256小数据不建议过大num_layers2数据量少减到 1数据量多再增加层数多不一定更好dropout0.1过拟合明显时提高到 0.2 或 0.3不能一开始就拉大5.2 报错先看哪里一套稳定的排查链路先看现象。常见的现象和排查顺序如下如果直接报维度错误先打印X.shape和y.shape确认是不是[batch, seq_len, features]的结构。很多 LSTM 报错都出在input_size和最后一个维度的数字不一致。如果 loss 是 NaN先查数据里有没有 NaN 或者极端的缺失值再查学习率是否过大最后查标准化是否完成。如果 loss 一直不下降先降低学习率不要立刻改模型结构。再看是不是归一化把数据压缩到了太小范围。如果训练集 loss 下降但验证集 loss 很高这是典型的过拟合先减层数、加 dropout、减小模型宽度。如果预测曲线整体滞后先检查窗口长度和预测步长是否匹配再看数据是否有强趋势。有一个判断原则先看数据和日志再调参数。不要一上来就怀疑模型结构有问题。实际项目里大部分“模型效果差”都是数据切片、归一化、未来信息泄露和超参数不合适造成的。5.3 低配置机器怎么控制资源占用如果你的机器没有独立 GPU或者显存只有 2GB不要急着跑大模型。可以按这个顺序控制资源先用 CPU 跑但把seq_len降到 12 或 24。把hidden_size或d_model降到 32。batch size 从 16 开始不行再降到 8。先把训练 epoch 数设成 10确认 loss 能下降再逐步增加。低配置能跑通不代表适合批量训练。如果准备做正式的对比实验最好申请一台有 GPU 的机器或者用云主机否则调参效率会很低。6. 复现之后怎么真正用到自己的数据上把两个模型跑通之后很多读者会有一个共同问题我的数据和教程里不一样怎么改才能用这部分我给几个通用的落地思路。6.1 从单变量到多变量单变量输入是[batch, seq_len, 1]多变量输入是[batch, seq_len, feature_num]。模型代码不需要大改只需要把input_size改成特征数量。但多变量有一个隐含风险不是所有特征都对预测有正向作用。如果加入太多无关特征模型可能过度拟合这些噪声。建议先用相关性分析或者简单的特征筛选把和预测目标相关性弱的列去掉。另一个重点是多个特征之间的量纲差异。标准化时不能只处理目标列所有输入特征都要一起标准化并且仍然遵循“只用训练集拟合 scaler”的原则。6.2 预测多个未来时间步的三种做法如果要预测未来多个时间步而不是只预测一个点有三种常见方案单步递推把上一步预测结果作为下一步输入反复预测。实现简单但误差会逐步累积长 horizon 预测容易偏移。直接多步把输出维度从 1 改成 horizon一次性预测多个点。代码改动小但需要调整损失函数和输出层。seq2seq用编码器理解历史序列用解码器逐步生成未来序列。结构更复杂适合长 horizon 和复杂序列但对训练数据量和调参要求更高。如果刚入门我建议先尝试直接多步预测。它最容易在现有代码上改也能清楚看到误差累积和输出结果的关系。6.3 我的建议顺序和后续可选方向如果你现在手头有自己的数据建议按这个顺序走先用 LSTM 做基线把单变量预测跑通。确认数据处理和训练流程没有问题后再复用同一套流程跑 Transformer。两个模型都记录训练损失、验证损失、测试集误差和单条样本的推理耗时。如果 Transformer 没有明显优势不要硬上用 LSTM 作为最终方案。如果数据量大、序列长再尝试改进 Transformer 的位置编码、加入时间特征或者引入更复杂的结构。代码复现最有价值的部分不是把别人的模型原样跑出来而是中间每一次报错、每一次调参都在帮你建立对模型行为的直觉。LSTM 和 Transformer 之所以总是被一起讨论正是因为它们代表了两种不同的建模思路一个靠逐步传递一个靠全局关联。把两种思路都亲手实现一遍你对时间序列预测的理解会扎实很多。
