最近在B站刷到很多关于LSTM和Transformer做时间序列预测的视频评论区经常出现的几类问题就是这两个模型到底解决了什么问题它们之间是什么关系论文里那些公式是怎么回事为什么别人跑得通的代码到自己电脑上就报错或者不收敛这篇文章会一次性把LSTM和Transformer这两大时间序列预测模型的来龙去脉讲清楚并且用PyTorch从零到一复现完整的预测流程。不管你是刚接触机器学习的新手还是在做电量预测、流量监控、股票价格分析等业务项目的开发者这篇文章都能给你一份可以直接上手的参考。文章内容分为两大块先是论文核心思路的通俗拆解再是完整的代码复现包括数据构造、模型定义、训练评估和可视化对比。1. LSTM与Transformer为什么是时序预测的“标配”时间序列预测并不是一个新概念。传统方法里有ARIMA、移动平均、指数平滑等统计模型它们在小规模、平稳性较强的数据上表现尚可。但一旦数据规模变大、非线性增强、存在长期依赖关系传统模型就显得力不从心。深度学习的出现尤其是循环神经网络RNN家族的崛起让时间序列预测进入了一个新阶段。LSTMLong Short-Term Memory长短期记忆网络是RNN的一种改进结构它通过引入门控机制解决了RNN在长序列训练中容易出现的梯度消失和梯度爆炸问题。你可以把LSTM理解为“一个懂得什么时候记住、什么时候遗忘的神经网络”它非常适合处理具有时间顺序、前后关联的数据。Transformer则是2017年由Google团队在论文《Attention Is All You Need》中提出的架构。它最初用于机器翻译但随后迅速席卷了NLP、CV、语音等多个领域。Transformer的核心是自注意力机制Self-Attention它可以让模型在计算某个位置的输出时同时关注序列中所有其他位置的信息而不是像RNN那样只能按顺序一步步向后传递。很多初学者会问既然LSTM已经在时序任务上表现很好为什么还要用Transformer答案是LSTM虽然能建模时间依赖但它本质上是串行计算的随着序列长度增加训练效率会明显下降而且长期依赖的建模能力依然有限。Transformer通过注意力机制实现了并行计算并且可以捕捉更长距离的依赖关系。在数据量足够大的条件下Transformer在很多时序任务上都能超过LSTM。不过要注意这并不意味着LSTM已经过时。在中小规模数据集、业务模型快速迭代、计算资源有限的情况下LSTM依然是一个极其稳定、好用的基线模型。实际工程中把LSTM作为baseline再把Transformer作为进阶方案是一种很常见的做法。这也正是本文把两个模型放在一起讲的原因你先理解LSTM的门控思想再理解Transformer的注意力机制两者对比着学会比单独学任何一个都更深刻。2. 环境准备与数据集构造复现文章里的代码之前先确认一下运行环境。本文使用PyTorch作为深度学习框架代码在以下环境中验证通过依赖建议版本说明Python3.8 及以上推荐 3.9 或 3.10PyTorch1.9 及以上Transformer 的batch_firstTrue需要较新版本NumPy1.21 及以上数据处理Pandas1.3 及以上数据读取和预处理scikit-learn1.0 及以上数据标准化Matplotlib3.5 及以上可视化pip install torch numpy pandas scikit-learn matplotlib为了让大家能直接跑通代码这里使用一条合成时间序列来演示它包含多个周期性成分、一个缓慢上升的趋势项以及随机噪声非常接近真实业务中“趋势周期噪声”的数据结构。import numpy as np import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset import matplotlib.pyplot as plt from sklearn.preprocessing import StandardScaler np.random.seed(42) torch.manual_seed(42) # 生成时间序列两个周期性成分 上升趋势 噪声 t np.arange(0, 1000, 0.1) season1 np.sin(t) season2 np.sin(0.3 * t) trend 0.0005 * t noise 0.1 * np.random.randn(len(t)) data season1 season2 trend noise plt.figure(figsize(12, 4)) plt.plot(data[:500]) plt.title(示例时间序列前500个点) plt.show()运行后会得到一条看起来有一定规律、又带噪声的曲线。接下来对它做标准化处理然后构造滑动窗口样本。3. LSTM 原理拆解与 PyTorch 实现3.1 从RNN到LSTM门控机制到底在解决什么RNN循环神经网络的核心思想是“循环”每一步的隐藏状态不仅由当前输入决定还由上一步的隐藏状态决定。这样网络就有了“记忆力”。但标准RNN在反向传播时梯度需要沿着时间步不断连乘。如果序列较长梯度很容易指数级缩小或增大这就是梯度消失/梯度爆炸问题。梯度一消失网络就无法学习到远距离的依赖关系。LSTM的论文发表于1997年作者是Hochreiter和Schmidhuber。它的思路是引入一条“细胞状态”Cell State就像一条传送带贯穿整个序列。这条传送带上可以轻松传递信息而门控结构决定“记住什么、遗忘什么、输出什么”。LSTM有三个门遗忘门决定上一时刻的细胞状态中哪些信息要丢弃。输入门决定当前时刻的新信息中哪些要写入细胞状态。输出门决定当前时刻的细胞状态中哪些信息要输出到隐藏状态。每个门都是一个sigmoid函数加一个逐元素乘法。sigmoid的输出在0到1之间相当于一个开关程度0表示彻底关闭1表示完全打开。公式如下遗忘门 ( f_t \sigma(W_f \cdot [h_{t-1}, x_t] b_f) )输入门 ( i_t \sigma(W_i \cdot [h_{t-1}, x_t] b_i) )候选状态 ( \tilde{C}t \tanh(W_C \cdot [h{t-1}, x_t] b_C) )细胞状态更新 ( C_t f_t \odot C_{t-1} i_t \odot \tilde{C}_t )输出门 ( o_t \sigma(W_o \cdot [h_{t-1}, x_t] b_o) )隐藏状态 ( h_t o_t \odot \tanh(C_t) )初学者看到公式不用紧张。你只需要知道LSTM通过“门”控制信息流通让梯度可以沿着细胞状态这条“高速公路”传播从而缓解了梯度消失问题。这就足够了。3.2 PyTorch 中的 LSTM APIPyTorch对LSTM提供了非常友好的封装。核心参数有input_size输入特征维度单变量时序是1多变量时序是特征数。hidden_size隐藏层神经元数量也就是隐状态维度越大拟合能力越强但越容易过拟合。num_layersLSTM堆叠层数层数越多模型越深但训练越难。batch_firstTrue让输入形状为(batch, seq_len, input_size)更加直观。dropout除最后一层外层间插入Dropout防止过拟合。下面定义LSTM预测模型。我们取最后一个时间步的隐藏状态经过一个全连接层输出预测值。class LSTMPredictor(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers2, output_size1, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout ) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): # x: (batch, seq_len, input_size) out, (h_n, c_n) self.lstm(x) # 取最后一个时间步的输出 last_out out[:, -1, :] return self.fc(last_out)这里有个常见误区初学者很容易把out和h_n搞混out的形状是(batch, seq_len, hidden_size)保存了每个时间步的输出。h_n的形状是(num_layers, batch, hidden_size)保存的是最后一层每个方向的最终隐藏状态。在单步预测场景下我们只需要最后一个时间步的输出out[:, -1, :]然后用一个全连接层把它映射成预测值。3.3 LSTM的常见使用误区使用LSTM时最容易出问题的点包括忘记做数据标准化。LSTM对输入数据的尺度非常敏感不做标准化会导致loss不下降或者收敛极慢。序列长度选得太短或太长。太短学不到长期依赖太长会导致训练成本高、信息冗余。把整个序列随机打乱。时间序列不同于普通表格数据随机打乱会破坏时间顺序造成数据泄漏导致验证集效果虚高。误以为LSTM只能处理单变量。其实它天然支持多变量输入把多个特征拼到最后一维即可。4. Transformer 原理拆解与 PyTorch 实现Transformer在时间序列预测中的用法和它在NLP中不太一样。完整的Transformer包含Encoder和Decoder两部分但在很多时序预测场景中我们往往只需要Encoder部分把最后一步的输出接一个全连接层就能完成预测任务。先来看Transformer到底在做什么。4.1 Self-Attention每个位置都能看到全局自注意力机制的核心是三个向量Query、Key、Value。你可以这样理解Query是“我在找什么”。Key是“我有什么”。Value是“我能提供什么”。对于序列中的每个位置模型会计算它与所有其他位置的匹配程度。匹配度高的位置其Value会被赋予更大的权重。最终输出是所有Value的加权和。注意力权重的计算公式是( \text{Attention}(Q, K, V) \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V )其中 ( d_k ) 是Key向量的维度。除以 ( \sqrt{d_k} ) 是为了防止点积结果过大导致softmax进入饱和区梯度变得很小。正是通过这种方式Transformer可以一步看到整个序列而不是像RNN/LSTM那样按时间步逐步传递信息。4.2 多头注意力多头注意力Multi-Head Attention就是把注意力计算并行做多次每次使用不同的线性投影。每个“头”关注序列中不同方面的关系最后把多个头的输出拼接起来再通过一个线性变换得到最终结果。举个例子在一个句子中一个头可能更关注相邻词的关系另一个头可能更关注指代关系。在时间序列中有的头可能更关注近期波动有的头可能更关注周期性规律。多头注意力的好处是让模型拥有多个“视角”表达能力更强。4.3 位置编码Transformer没有循环结构它天然不关心输入的顺序。如果把序列顺序打乱Transformer看到的输入完全一样。但时间序列的顺序恰恰至关重要所以必须显式地把位置信息注入模型。论文中使用的是正弦余弦位置编码( PE_{(pos, 2i)} \sin(pos / 10000^{2i/d_{model}}) )( PE_{(pos, 2i1)} \cos(pos / 10000^{2i/d_{model}}) )这里 ( pos ) 是位置( i ) 是维度下标。这种编码方式的好处是不同位置产生的编码是唯一的而且模型容易学习到相对位置的规律。代码实现如下class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len5000, dropout0.1): super().__init__() self.dropout nn.Dropout(pdropout) pe torch.zeros(max_len, d_model) position torch.arange(0, max_len, dtypetorch.float).unsqueeze(1) div_term torch.exp(torch.arange(0, d_model, 2).float() * (-np.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) # (1, max_len, d_model) self.register_buffer(pe, pe) def forward(self, x): x x self.pe[:, :x.size(1), :] return self.dropout(x)4.4 Transformer 用于时间序列预测的结构选择在处理时间序列预测时常见的做法有两种第一种是Encoder-Decoder结构适合序列到序列的多步预测比如输入过去60个点预测未来30个点。第二种是Encoder-only结构适合单步预测或者“输入一段历史预测一个未来值”的场景。我们先从Encoder-only开始它更容易理解也更容易训练。实现代码如下class TransformerPredictor(nn.Module): def __init__(self, d_model64, nhead8, num_layers2, dropout0.2, output_size1): super().__init__() self.input_proj nn.Linear(1, d_model) self.pos_encoder PositionalEncoding(d_model, dropoutdropout) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dropoutdropout, batch_firstTrue ) self.transformer_encoder nn.TransformerEncoder(encoder_layer, num_layersnum_layers) self.fc nn.Linear(d_model, output_size) def forward(self, x): # x: (batch, seq_len, 1) x self.input_proj(x) # (batch, seq_len, d_model) x self.pos_encoder(x) # 加位置编码 x self.transformer_encoder(x) # (batch, seq_len, d_model) x x[:, -1, :] # 取最后一个位置的输出 return self.fc(x)注意nn.TransformerEncoderLayer(batch_firstTrue)需要 PyTorch 1.9 及以上版本如果你的版本较旧需要把输入维度调整为(seq_len, batch, d_model)去掉batch_firstTrue。5. 完整代码复现从数据处理到模型对比到这里原理部分基本讲完了。接下来进入完整实战环节。5.1 滑动窗口数据集构造时间序列预测最关键的一步是把原始序列转成监督学习样本。我们使用60个历史点预测未来1个点。def create_sequences(data, seq_len60, 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) # 标准化 scaler StandardScaler() data_scaled scaler.fit_transform(data.reshape(-1, 1)).flatten() seq_len 60 pred_len 1 X, y create_sequences(data_scaled, seq_len, pred_len) # 按照8:1:1切分训练集、验证集、测试集 total len(X) train_size int(total * 0.8) val_size int(total * 0.1) X_train, y_train X[:train_size], y[:train_size] X_val, y_val X[train_size:train_size val_size], y[train_size:train_size val_size] X_test, y_test X[train_size val_size:], y[train_size val_size:] print(f训练集: {X_train.shape}, 验证集: {X_val.shape}, 测试集: {X_test.shape})5.2 转换为PyTorch张量def to_tensor(X, y): X_t torch.tensor(X, dtypetorch.float32).unsqueeze(-1) y_t torch.tensor(y, dtypetorch.float32) return X_t, y_t X_train, y_train to_tensor(X_train, y_train) X_val, y_val to_tensor(X_val, y_val) X_test, y_test to_tensor(X_test, y_test) batch_size 64 train_dataset TensorDataset(X_train, y_train) val_dataset TensorDataset(X_val, y_val) test_dataset TensorDataset(X_test, y_test) train_loader DataLoader(train_dataset, batch_sizebatch_size, shuffleFalse) val_loader DataLoader(val_dataset, batch_sizebatch_size, shuffleFalse) test_loader DataLoader(test_dataset, batch_sizebatch_size, shuffleFalse)这里使用了shuffleFalse因为时间序列的前后顺序不能被打破。5.3 通用训练函数为了方便对比两个模型写一个通用的训练函数。它接收模型对象返回训练和验证阶段的loss记录。def train_model(model, train_loader, val_loader, epochs25, lr0.001, devicecpu): criterion nn.MSELoss() optimizer optim.Adam(model.parameters(), lrlr) scheduler optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.5) train_losses, val_losses [], [] for epoch in range(epochs): model.train() epoch_loss 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() optimizer.step() epoch_loss loss.item() * len(xb) train_loss epoch_loss / len(train_loader.dataset) train_losses.append(train_loss) model.eval() val_loss 0 with torch.no_grad(): for xb, yb in val_loader: xb, yb xb.to(device), yb.to(device) pred model(xb) loss criterion(pred, yb) val_loss loss.item() * len(xb) val_loss val_loss / len(val_loader.dataset) val_losses.append(val_loss) if (epoch 1) % 5 0: print(fEpoch {epoch 1:3d}/{epochs} | Train Loss: {train_loss:.6f} | Val Loss: {val_loss:.6f}) scheduler.step() return train_losses, val_losses这里加入了一个学习率调度器每10个epoch将学习率缩小为原来的0.5倍有助于模型在训练后期收敛得更稳定。5.4 训练LSTM模型device torch.device(cuda if torch.cuda.is_available() else cpu) lstm_model LSTMPredictor( input_size1, hidden_size64, num_layers2, output_size1, dropout0.2 ).to(device) lstm_train_loss, lstm_val_loss train_model( lstm_model, train_loader, val_loader, epochs25, lr0.001, devicedevice )训练完成后用测试集评估def evaluate_model(model, test_loader, scaler, devicecpu): model.eval() preds, trues [], [] with torch.no_grad(): for xb, yb in test_loader: xb, yb xb.to(device), yb.to(device) pred model(xb) preds.append(pred.cpu().numpy()) trues.append(yb.cpu().numpy()) preds np.concatenate(preds, axis0) trues np.concatenate(trues, axis0) preds_inv scaler.inverse_transform(preds.reshape(-1, 1)).flatten() trues_inv scaler.inverse_transform(trues.reshape(-1, 1)).flatten() mse np.mean((preds_inv - trues_inv) ** 2) rmse np.sqrt(mse) mae np.mean(np.abs(preds_inv - trues_inv)) return preds_inv, trues_inv, mse, rmse, mae lstm_preds, lstm_trues, lstm_mse, lstm_rmse, lstm_mae evaluate_model( lstm_model, test_loader, scaler, device ) print(fLSTM - MSE: {lstm_mse:.4f}, RMSE: {lstm_rmse:.4f}, MAE: {lstm_mae:.4f})5.5 训练Transformer模型Transformer的训练逻辑完全一样只需替换模型类transformer_model TransformerPredictor( d_model64, nhead8, num_layers2, dropout0.2, output_size1 ).to(device) transformer_train_loss, transformer_val_loss train_model( transformer_model, train_loader, val_loader, epochs25, lr0.001, devicedevice ) transformer_preds, transformer_trues, transformer_mse, transformer_rmse, transformer_mae evaluate_model( transformer_model, test_loader, scaler, device ) print(fTransformer - MSE: {transformer_mse:.4f}, RMSE: {transformer_rmse:.4f}, MAE: {transformer_mae:.4f})这里有个经验需要说明Transformer在小数据集上的表现不一定比LSTM好这是正常的。因为注意力机制的参数量更大需要更多数据才能发挥优势。如果你在业务场景中发现Transformer效果不如LSTM不必惊讶这恰恰说明模型选型不能盲目追新。5.6 可视化对比为了直观对比两个模型的预测效果在测试集上取前300个点绘图。plt.figure(figsize(14, 6)) plt.subplot(1, 2, 1) plt.plot(lstm_trues[:300], label真实值, linewidth1.5) plt.plot(lstm_preds[:300], labelLSTM预测, linewidth1.5, alpha0.8) plt.title(LSTM 预测效果) plt.legend() plt.subplot(1, 2, 2) plt.plot(transformer_trues[:300], label真实值, linewidth1.5) plt.plot(transformer_preds[:300], labelTransformer预测, linewidth1.5, alpha0.8) plt.title(Transformer 预测效果) plt.legend() plt.tight_layout() plt.show()如果一切顺利你会看到两条预测曲线都和真实曲线非常接近。比较突出的一个现象是在序列的波峰和波谷附近预测往往会“钝化”一点也就是峰谷处的拟合不如平滑段好。这是单步预测模型常见的问题因为损失函数是均方误差模型倾向于预测条件均值难免会把尖峰“抹平”。6. 常见问题与排查思路在实际复现过程中你可能会遇到下面这些问题。我整理成一张表格方便快速查阅问题现象常见原因解决思路loss出现NaN学习率过大、数据未标准化、梯度爆炸减小学习率检查数据是否标准化必要时使用梯度裁剪训练loss下降但验证loss不降过拟合增加Dropout减小hidden_size或num_layers引入早停预测曲线明显滞后单步预测的累积误差或序列长度不足增加seq_len或改用多步预测结构Transformer训练非常慢序列过长、batch过大、模型维度太高缩短seq_len降低d_model使用GPU训练集效果很好但测试集差距大数据泄漏检查是否在切分前做了标准化是否随机打乱了序列LSTM比Transformer效果好小数据集下注意力机制优势不明显先用LSTM做基线再逐步增大Transformer规模程序报错batch_first参数无效PyTorch版本过低升级PyTorch或去掉batch_first并调整输入维度预测值几乎等于某个常数模型退化学习率过低或特征尺度不一致重新初始化模型适当调大学习率检查数据分布如果你遇到“预测未来很多步时误差越滚越大”的问题这里单独说两句。单步预测模型训练时每一步都使用真实历史值作为输入但推理时会用模型预测值作为下一步输入误差会累积。解决办法有几种在训练时使用教师强制Teacher Forcing的变体随机混合真实值和预测值作为输入。改为多步预测模型直接输出未来N个时间步。使用序列到序列结构Encoder输入历史序列Decoder逐步生成未来序列。7. 工程实践建议下面这些建议来自我在实际项目中使用LSTM和Transformer做时序预测的经验希望对你有帮助。7.1 数据标准化是第一步也是最重要的一步LSTM和Transformer都对输入尺度非常敏感。如果不做标准化模型很容易在训练初期就发散。推荐使用StandardScaler它会把数据变换为均值为0、标准差为1的分布。注意只能使用训练集的部分进行fit然后transform训练集、验证集和测试集。不能把整个数据集一起fit否则验证集和测试集的信息会在训练阶段就被模型“看到”造成数据泄漏。7.2 先跑通最小案例再上复杂度很多同学一上来就用几十万条数据、几百维特征训练一个大模型结果不仅慢还很难调试。正确做法是先在5000条数据、单变量、小模型上跑通完整流程确认代码逻辑正确再逐步扩大数据规模、增加特征维度、调大模型容量。7.3 关于超参数不必过度纠结在时间序列预测中下面这几个超参数的影响最大序列长度seq_len50到200是常见范围。可以结合业务周期来定比如预测日用电量时可以尝试按7天、30天的历史窗口输入。隐藏层维度hidden_size/d_model32到128是常见区间维度太大会显著增加训练时间和过拟合风险。层数num_layers1到3层足够了盲目加深对效果提升有限。学习率lr1e-3是常见的初始值如果loss不降可以考虑降到1e-4或使用学习率预热。7.4 多步预测策略要提前想清楚如果业务需要预测未来多个时间点一定要在建模前想好策略递推预测用单步模型循环预测实现简单但误差会累积。直接多输出模型最后输出N个值训练时用真实值计算每个时间步的loss实现简单但各预测时刻之间缺少依赖建模。Seq2SeqEncoder-Decoder结构建模能力最强训练和推理也最复杂。建议先在单步预测上把效果做到位再根据业务需求升级为多步预测。7.5 保存模型并记录训练参数训练结束后用torch.save保存模型参数同时建议把超参数、数据切分方式、标准化器等全部保存下来方便后续复现。# 保存模型 torch.save(lstm_model.state_dict(), lstm_model.pth) torch.save(transformer_model.state_dict(), transformer_model.pth) # 保存标准化器 import joblib joblib.dump(scaler, scaler.pkl)8. 总结到这里LSTM和Transformer从论文原理到代码复现的完整流程就讲完了。这篇文章需要掌握的核心内容有三个一是LSTM的门控机制和它在长序列建模中的价值二是Transformer的自注意力机制、多头注意力和位置编码三是用PyTorch构建完整的时间序列预测流程包括数据处理、模型训练、评估和可视化。接下来的学习路线我的建议是先在你自己业务的数据集上跑通LSTM和Transformer记录它们的表现差异然后尝试多步预测结合更多特征构造多变量输入最后可以继续了解Informer、Autoformer等专门为长时间序列设计的Transformer变体。如果你在复现过程中遇到问题欢迎在评论区留言也欢迎把文章收藏起来备用。动手跑通一遍比看十遍视频更有效。