LSTM与Transformer时序预测实战:从原理到源码对比

LSTM与Transformer时序预测实战:从原理到源码对比 简介一套基于LSTM和Transformer模型的时间序列预测实践源码面向机器学习初学者与时序分析开发者覆盖数据预处理、模型构建、训练和预测的完整流程适用于金融、气象、工业传感等场景的研究与课程设计。压缩包共31个文件约43.58MB主要包括3个Python核心脚本Transformer.py、RNN_gpu.py、RNN.py、3个CSV时序数据集、14个可视化图片、7个XML配置以及模型状态文件等方便对照原理、复现实验与继续调优。目前已有907人学习下载。项目同时提供LSTM/RNN与Transformer的对比实现并内置Idea工程配置、README说明和Git忽略规则可快速导入开发环境运行模型状态文件保留了训练关键信息支持直接加载验证。整体目录结构清晰既能支撑教学实验与论文复现也可为工程化时序预测提供可参考的基线方案。 时序预测这个方向我前后折腾了快两年。最早是从LSTM入的门后来Transformer火遍了NLP和视觉我也尝试把它搬到时间序列上发现这两套模型在同一个业务数据上的表现差异非常大。如果你的工作也需要预测销量、负荷、温度或者行情趋势并且想直接拿到一份能跑的LSTM和Transformer时序预测源码这篇文章应该能帮你省掉大量试错时间。项目本身并不复杂核心是把数据处理、滑窗构造、模型定义、训练评估全部封装成统一模块LSTM和Transformer通过命令行参数一键切换。我会把源码里的关键设计、模型差异、调参心得和踩过的坑都展开讲清楚。代码量不大适合有一点点PyTorch基础、想认真做时序预测的开发者参考。1. 项目整体设计与模型选型1.1 为什么同一份数据要跑两个模型很多刚接触时序预测的人会纠结选LSTM还是Transformer我的建议是别纠结两个都跑一遍用同一份数据做对比这样你对模型的性格会有更直观的认识。LSTM属于循环神经网络核心机制是三个门遗忘门、输入门、输出门。它通过门控控制信息在时间步之间的流动天然适合处理逐点递推的序列数据。我最早用LSTM预测电力负荷几千条样本就能训练出像样的结果训练速度快调参空间也够大作为基线非常合适。Transformer则完全不同。它靠多头自注意力机制直接计算序列中任意两个位置之间的依赖关系不按时间顺序逐个处理。这带来两个好处一是可以并行训练速度优势明显二是能捕捉更远距离的关联比如股票价格里相隔几十个交易日的模式。缺点是它对数据量更敏感样本太少时很容易过拟合或者训练不稳定。所以这个项目把两套模型放在同一套代码框架里用同样的数据预处理和评估流程最终输出对比结果。跑完一遍你会明白不存在“哪个模型一定更好”只有“哪个模型在什么数据规模下更适合”。1.2 源码结构与一键运行方式源码的目录组织我做了简化方便直接迁移到自己的项目里timeseries_predict/ ├── data/ │ └── load_data.py # 数据读取、滑窗、归一化 ├── models/ │ ├── lstm_model.py # LSTM预测模型 │ └── transformer_model.py # Transformer Encoder预测模型 ├── config.py # 统一超参数配置 ├── train.py # 训练入口支持 --model 参数 ├── evaluate.py # 指标计算与预测曲线绘制 └── requirements.txt # numpy, pandas, torch, matplotlib, scikit-learn依赖库就五个PyTorch版本建议2.0以上因为新版对Transformer Encoder的batch_first支持更完善。安装完依赖后跑LSTM和Transformer分别是这两条命令python train.py --model lstm python train.py --model transformer训练完会自动调用evaluate.py生成评估指标和预测对比图。后面所有讲解都会围绕这套代码结构展开。2. 数据预处理时序预测的地基2.1 从原始序列到监督学习样本时序预测的第一步不是建模型而是把一维或多维时间序列转成监督学习的样本格式。常用的方法叫滑窗也就是用过去N个时间步预测下一个时间步。我习惯把窗口大小命名为look_back它决定模型能看到多长的历史信息。如果原始数据是小时级look_back24就是看过去一天预测未来一小时如果是日级look_back30就是看过去一个月预测明天。核心转换代码很简洁import numpy as np def create_sequences(data, look_back): X, y [], [] for i in range(len(data) - look_back): X.append(data[i:i look_back]) y.append(data[i look_back]) return np.array(X), np.array(y)转换完之后X的形状是[样本数, look_back, 特征数]这个三维格式正好同时满足LSTM和Transformer的输入要求。有一点需要特别说明如果你做的是多变量预测比如同时用温度和湿度预测电力负荷不要把多个特征拼成一个二维表就完事要保证特征维放到最后一个维度否则模型input_size会算错。2.2 归一化要小心“数据泄漏”归一化几乎是所有深度学习模型必需的步骤但时序预测里有个非常隐蔽的坑如果你先对全量数据做归一化再切训练集和测试集测试集的信息已经通过scaler混进了训练过程验证分数会虚高。这在金融时序预测里是致命的因为交易策略回测会因此得出完全不真实的结果。正确做法是只用训练集拟合scaler再用同样的scaler去转换验证集和测试集from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler() train_scaled scaler.fit_transform(train) # 只用训练集 fit val_scaled scaler.transform(val) # 验证集只 transform test_scaled scaler.transform(test)另一个容易忽略的点是数据切分顺序。时序数据千万不能像普通机器学习那样随机打乱必须按时间顺序切分否则模型会提前“看到”未来信息。我通常按7:2:1切成训练、验证、测试三段切分前沿时间轴直接索引即可。3. 两个模型的核心源码解读3.1 LSTM基线模型简单但有效LSTM模型结构很经典一个LSTM层提取序列特征取最后一个时间步的隐状态接一个全连接层输出预测值。import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size, hidden_size, num_layers, output_size1, dropout0.2): super().__init__() self.lstm nn.LSTM( input_size, hidden_size, num_layers, batch_firstTrue, dropoutdropout ) 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)这里的关键是out[:, -1, :]。LSTM每个时间步都会输出一个隐状态但做单步预测时我们只关心最后一个时间步携带的信息把它压缩成一个预测值。hidden_size一般取32到128就够太小表达力不足太大容易在小数据集上过拟合。3.2 Transformer时序版Encoder 因果掩码很多新手把NLP里的Transformer原封不动搬过来做预测这是一个很容易走弯路的地方。做数值回归预测时往往不需要Encoder-Decoder结构一个Encoder就足够了输入过去N个时间步输出隐含表示最后取序列末位的表示通过全连接层得到预测值。和NLP版本相比时序版有三个关键改法第一输入嵌入层改成线性投影。原始时间序列是连续数值不需要词嵌入直接用一个线性层把输入特征映射到d_model维。第二必须加因果掩码。预测下一个时间步时当前位置不能看到未来信息。Transformer的注意力是全局的如果不加掩码模型训练时会偷看未来的真实值测试时表现会崩溃。核心代码里这个掩码叫generate_square_subsequent_mask它会生成一个上三角为负无穷的矩阵让未来位置在softmax时权重归零。第三位置编码不能省。LSTM按时间顺序输入天然包含顺序信息Transformer是并行计算如果不加位置编码模型会把序列当成无序集合完全丢失时间先后关系。位置编码我用经典的三角函数方案不同频率的sin/cos函数让每个时间位置有唯一编码。import math import torch import torch.nn as nn class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len1000, dropout0.1): super().__init__() self.dropout nn.Dropout(dropout) 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() * (-math.log(10000.0) / d_model) ) pe[:, 0::2] torch.sin(position * div_term) pe[:, 1::2] torch.cos(position * div_term) self.register_buffer(pe, pe) def forward(self, x): return self.dropout(x self.pe[:x.size(1)]) class TimeSeriesTransformer(nn.Module): def __init__(self, input_size, d_model, nhead, num_layers, dropout0.1): super().__init__() self.embed nn.Linear(input_size, d_model) self.pos PositionalEncoding(d_model, dropoutdropout) encoder_layer nn.TransformerEncoderLayer( d_model, nhead, dim_feedforward2048, dropoutdropout, batch_firstTrue ) self.encoder nn.TransformerEncoder(encoder_layer, num_layers) self.fc nn.Linear(d_model, 1) def forward(self, x): mask nn.Transformer.generate_square_subsequent_mask(x.size(1)).to(x.device) src self.pos(self.embed(x)) out self.encoder(src, maskmask) return self.fc(out[:, -1, :])这套结构就是目前很多时间序列Transformer变体模型的简化版。跑通之后你再看那些叫Informer、Autoformer、PatchTST的论文会发现核心思想底子都是一样的特征嵌入、位置编码、注意力掩码、末位输出。3.3 训练循环的关键细节LSTM和Transformer共用一套训练循环方便对比。损失函数用MSE优化器用Adam学习率初始设1e-3。训练时我额外做了两件事梯度裁剪和早停。梯度裁剪对LSTM尤其重要因为长序列反向传播容易梯度爆炸clip_grad_norm_能把梯度范数限制在一定范围内避免训练发散。早停则监控验证集loss连续多个epoch不下降就恢复最优模型权重防止后期过拟合。import torch from torch.nn.utils import clip_grad_norm_ criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) for epoch in range(epochs): model.train() for xb, yb in train_loader: optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step()我习惯每个epoch结束后在验证集上算一次loss如果比之前最优值好就保存当前权重超过patience次没有改进就提前结束训练。对Transformer来说这个机制几乎是救命稻草因为它的训练曲线比LSTM波动更大。4. 实验对比与调参经验4.1 评价指标别只盯loss训练损失下降不代表预测效果好因为MSE对异常值惩罚很大两个模型可能loss接近但实际曲线形态完全不同。我评估时会同时看三个指标MAE平均绝对误差单位和原始数据一致直观好解释。RMSE均方根误差对大的预测偏差更敏感适合关注风险场景。MAPE平均绝对百分比误差适合比较不同量纲的数据但真实值接近0时MAPE会异常放大要慎用。绘制预测曲线时不要把全部测试集画到一张图上那样看不出细节。我通常随机截取连续200个时间点把真实值和预测值画在一起能直观看到滞后和拟合程度。4.2 超参数速查表我整理了一份基于实测的推荐参数范围不同数据规模会有浮动但起步时照着这个表设置能少走很多弯路。超参数LSTM推荐范围Transformer推荐范围look_back窗口24 ~ 9648 ~ 192hidden_size / d_model32 ~ 12864 ~ 256层数1 ~ 32 ~ 4注意力头数 nhead不使用4 或 8dropout0.1 ~ 0.30.1 ~ 0.5batch_size32 ~ 12832 ~ 128learning_rate1e-3 附近1e-3 到 5e-4一个容易被忽视的参数是dim_feedforwardTransformer里前馈网络维度一般设成d_model的4倍左右。2048是NLP常用值但对小规模时序数据偏大如果你只有几千条样本建议降到512或1024能明显缓解过拟合和显存压力。4.3 我实测出来的几条规律第一数据量小于五千条时LSTM通常比Transformer更稳。Transformer的注意力矩阵参数量大小样本撑不住训练一轮后验证loss波动明显。第二序列越长Transformer相对LSTM的优势越大。当look_back超过100时LSTM会出现长程遗忘和信息压缩问题而Transformer几乎不受距离限制。第三多变量预测场景中Transformer对特征间交互关系的建模更充分如果输入特征有十个以上可以优先试Transformer。5. 踩坑实录常见问题与排查技巧5.1 验证集评分很高上线就翻车这个问题几乎都是数据泄漏导致的。最常见的有三种第一归一化时用全量数据fit了scaler第二切分数据时混进了随机打乱操作第三特征里不小心包含了目标变量的滞后值。我排查数据泄漏的方法是做一次对照实验在测试集末尾拼接一段未来数据看模型预测是否突然变得很准。如果拼接后效果突飞猛进说明模型学会了偷看未来。5.2 Transformer在小数据集上直接飞了我试过在两千条样本上用四层Transformer训练loss降得很好验证loss却一路走高典型的过拟合。解决办法是先降层数从1层或2层开始然后加大dropout到0.3以上最后要配合早停。如果降层后loss还是不稳建议老老实实换回LSTM基线。Transformer不是万能药数据量不够时强行使用反而会浪费时间。5.3 预测曲线总是滞后真实值一拍这是时序预测里非常典型的场景预测值曲线形状和真实值很像但整体向右平移了一点看起来就像慢半拍。原因是模型在MSE损失下学会了输出一个接近上一时刻的值因为历史均值往往是“最安全”的预测。解决思路有几种一是预测目标改为差分值y_t - y_{t-1}让模型学习变化趋势而不是绝对值二是在评估时用滚动预测方式把每一步的输出作为下一步的输入而不是每一步都用真实值这样能暴露误差累积问题三是如果业务允许适当增加look_back长度让模型有更多上下文来感知拐点。5.4 训练不收敛和梯度异常LSTM遇到梯度爆炸很常见现象是loss突然变成nan。我一般先检查学习率是不是太大再确认lstm层数没有堆太高最后在反向传播后加梯度裁剪。Transformer如果loss一直在一个平台上不去大概率是学习率设置不合适可以试试热身策略前几个epoch用很小的学习率再慢慢升到目标值能显著缓解训练不稳定的问题。如果你在跑代码时遇到维度报错优先检查这三个地方输入张量是否是[batch, seq_len, features]的三维形状是否设置了batch_firstTrueTransformer的mask形状是否和seq_len一致。这些问题排查熟练之后整个项目跑通只需要几分钟。6. 一点个人经验收尾我做这个项目最大的体会是模型不是越复杂越好。LSTM在中小数据量上往往是最稳的基线先把它跑通再上TransformerTransformer的优势要等数据量足够大、序列足够长的时候才会显现出来。角度再新、结构再花哨的模型底层也离不开数据处理和评估设计。代码细节比模型本身更容易让人翻车数据泄漏、掩码写错、维度不对这些问题我都踩过所以上面把这些坑全部摊开写出来了。希望这份源码和总结能让你少走几步弯路。本文还有配套的精品资源点击获取