news 2026/10/2 14:26:16

LSTM+Transformer时间序列预测实战:Pytorch完整源码与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LSTM+Transformer时间序列预测实战:Pytorch完整源码与避坑指南

简介:这份资源面向时间序列预测方向的机器学习学习者与工程实践者,提供一套基于Pytorch实现的LSTM+Transformer混合模型完整源码与配套数据,可用于风电预测、光伏预测、寿命预测、浓度预测等场景,采用多特征输入、单变量输出的建模方式,适合具备一定深度学习基础、希望快速复现并迁移到自身课题的读者。压缩包共132个文件,以31个py源码文件为核心,辅以87个pyc编译文件、6个xml配置、4个csv数据集及少量工程配置文件,整体约1.93MB,代码由作者编写并调试,注释清晰,支持csv与xlsx读取,替换数据集即可运行。内容涵盖数据加载、模型构建与训练预测等模块,目录结构完整,便于对照理解LSTM与Transformer的融合思路。目前已有1613人学习下载,可作为时序预测入门与项目落地的参考方案。

1. 从一份能跑通的 LSTM+Transformer 源码说起

时间序列预测这个方向,最折磨人的不是模型结构本身,而是从数据到训练再到推理这条链路上,每一步都有细节能把人卡住。你可能看过不少 Transformer 架构的讲解,也读过 LSTM 的原理推导,但真正打开编辑器要写一个能跑通的混合模型时,数据怎么切窗、位置编码怎么加、两个模块怎么拼接、损失函数怎么选,这些问题一个都不会自动消失。这份 Pytorch 完整源码和数据,解决的就是这个断层——它给了一套可以直接运行的 LSTM+Transformer 时间序列预测工程,包含数据生成、模型定义、训练循环和预测评估的完整链路。适合已经装好 Pytorch 环境、想快速验证混合模型效果的人,也适合拿它当骨架改造成自己业务场景的从业者。下面我按实际拆包和跑通的顺序,把关键环节和踩过的坑逐个说清楚。

2. 模型结构拆解:LSTM 和 Transformer 各自负责什么

2.1 为什么要把 LSTM 和 Transformer 拼在一起

单用 LSTM 做时间序列预测,优势在于它对局部时序依赖的捕捉很自然,门控机制能记住长距离的模式,但它的瓶颈也很明显:串行计算导致训练慢,而且当序列长度拉到几百上千步时,梯度传播路径太长,远端信息衰减严重。Transformer 的自注意力机制可以并行处理整个序列,任意两个时间步之间的关联都能直接建模,但纯 Transformer 对时序数据的位置感知完全依赖位置编码,而位置编码在长序列上的外推能力并不理想,尤其是当预测窗口超出训练时见过的长度时,效果会明显下滑。

把两者串起来,常见的做法是让 LSTM 先对原始序列做一轮特征提取,把每个时间步的输入压缩成一个更有表达力的隐状态表示,再把这个表示序列喂给 Transformer 做全局注意力建模。这样 LSTM 负责局部模式和平滑,Transformer 负责跨窗口的全局关联,分工明确。源码里就是这么搭的:输入先过 LSTM 层,取每个时间步的 output(不是最后一步的 hidden),再送进 Transformer 的编码器,最后接一个全连接层输出预测值。

注意:LSTM 的输出有两种取法——取最后一步的 hidden state 只适合做单点预测,取所有时间步的 output 才能保留序列结构给 Transformer 用。源码里用的是后者,如果你改成前者,Transformer 的输入就只剩一个向量,自注意力退化成无意义操作。

2.2 源码里的模型定义与参数含义

模型定义部分集中在model.py里,核心是一个继承nn.Module的类。下面把关键代码段拆出来看:

class LSTMTransformer(nn.Module): def __init__(self, input_dim, d_model, nhead, num_layers, dropout=0.1): super().__init__() # LSTM 做局部特征提取,输出维度对齐 Transformer 的 d_model self.lstm = nn.LSTM( input_size=input_dim, hidden_size=d_model, num_layers=2, batch_first=True, dropout=dropout ) # Transformer 编码器,只用到 encoder 部分 encoder_layer = nn.TransformerEncoderLayer( d_model=d_model, nhead=nhead, dim_feedforward=d_model * 4, dropout=dropout, batch_first=True ) self.transformer_encoder = nn.TransformerEncoder( encoder_layer, num_layers=num_layers ) # 输出头,把 d_model 映射回预测维度 self.fc = nn.Linear(d_model, 1) def forward(self, x): # x shape: (batch, seq_len, input_dim) lstm_out, _ = self.lstm(x) # (batch, seq_len, d_model) # 位置编码在 TransformerEncoderLayer 里没有内置,需要外部注入 transformer_out = self.transformer_encoder(lstm_out) # 取最后一个时间步的输出做预测 out = self.fc(transformer_out[:, -1, :]) return out

这段代码有几个参数需要重点理解。d_model是 LSTM 隐藏层维度和 Transformer 内部维度的统一值,必须一致,否则 LSTM 输出没法直接喂给 Transformer。nhead是注意力头数,d_model必须能被nhead整除,源码里默认d_model=64、nhead=4,这是比较稳妥的起点。num_layers同时控制 LSTM 层数和 Transformer 编码器层数,源码里都设成 2,层数再往上加容易在小数据集上过拟合。dim_feedforward一般设成d_model的 4 倍,这是 Transformer 原论文的默认比例。

还有一个容易被忽略的点:nn.TransformerEncoderLayer在 Pytorch 的某些版本里不自动加位置编码,源码里是在 LSTM 输出之后、进 Transformer 之前手动加了一个可学习的位置嵌入。如果你直接拿掉这段,模型对序列顺序的感知会变弱,预测曲线会出现明显的相位偏移。

2.3 数据窗口切分与张量形状对齐

时间序列预测的数据准备和 CV 任务完全不同,核心是把连续序列切成「输入窗口 → 预测目标」的样本对。源码里用的是一个滑动窗口函数:

def create_sequences(data, input_len, pred_len): xs, ys = [], [] for i in range(len(data) - input_len - pred_len + 1): x = data[i : i + input_len] y = data[i + input_len : i + input_len + pred_len] xs.append(x) ys.append(y) return np.array(xs), np.array(ys)

input_len是回看窗口长度,pred_len是预测步数。源码默认input_len=48、pred_len=12,也就是用过去 48 个时间步预测未来 12 个时间步。这个比例不是随便定的:如果input_len太短,LSTM 没有足够上下文提取模式;如果pred_len太长,误差会累积,尤其是多步预测时后面几步基本靠猜。我一般会先把pred_len设成input_len的四分之一左右,跑通后再调。

张量形状方面,Pytorch 的 LSTM 和 Transformer 都要求batch_first=True,也就是输入形状是(batch, seq_len, feature)。源码在 DataLoader 里没有做额外的维度变换,所以create_sequences返回的数组直接就是(样本数, input_len, 特征数),这一点比很多需要手动 transpose 的代码省事。如果你自己的数据是多变量,把data从一维改成二维,input_dim对应改掉就行。

3. 训练流程与关键参数配置

3.1 损失函数选择与学习率调度

时间序列预测的损失函数看起来简单,但选错了会让模型学偏。源码里用的是nn.MSELoss(),这是回归任务的默认选择,对大多数平稳序列够用。但如果你数据里有明显的异常值,MSE 会被拉偏,这时候换成nn.HuberLoss()或者nn.SmoothL1Loss()更稳。源码里没有做异常值处理,所以如果你拿自己的数据跑,先看一眼分布,别直接套。

学习率方面,源码用的是Adam优化器,初始学习率1e-3,配合StepLR每 20 个 epoch 衰减到 0.5 倍。这个配置在中小规模数据上比较稳,但如果你发现 loss 在前几个 epoch 震荡厉害,把初始学习率降到5e-4或者1e-4再试。Transformer 部分对学习率比较敏感,太大容易发散,太小收敛慢,源码里的1e-3是上限,不建议再往上加。

optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.5) criterion = nn.MSELoss()

训练循环里还有一个细节:源码在每次 backward 之前调用了optimizer.zero_grad(),这是标准操作,但如果你加了梯度累积(比如显存不够想用大 batch),zero_grad 的调用位置要相应调整,否则梯度会累加出错。

3.2 训练循环与验证集监控

训练部分的结构很常规:外层 epoch 循环,内层 batch 循环,每个 batch 做前向、算 loss、反向、更新。源码里把训练集和验证集分开,每个 epoch 结束后在验证集上算一次 loss,并保存验证 loss 最低的模型权重。这个「保存最优」的逻辑很关键,因为时间序列数据很容易过拟合,训练 loss 一直降但验证 loss 早就开始升了。

best_val_loss = float('inf') for epoch in range(num_epochs): model.train() for x_batch, y_batch in train_loader: optimizer.zero_grad() pred = model(x_batch) loss = criterion(pred, y_batch) loss.backward() optimizer.step() model.eval() val_loss = 0 with torch.no_grad(): for x_val, y_val in val_loader: pred_val = model(x_val) val_loss += criterion(pred_val, y_val).item() val_loss /= len(val_loader) if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), 'best_model.pth') scheduler.step()

num_epochs源码里默认 100,但实际跑下来通常 30 到 50 个 epoch 就能收敛。如果你发现验证 loss 在第 10 个 epoch 就开始升,别犹豫,直接把 epoch 数砍到 20 左右,或者加 dropout 和 weight decay。源码里 dropout 默认 0.1,可以加到 0.2 到 0.3 试试。

提示:验证集的划分不能用随机切分,时间序列必须按时间顺序切,否则会出现「用未来数据预测过去」的泄漏问题。源码里是按 8:2 从尾部切验证集,这是正确做法。

3.3 预测输出与反归一化

源码在数据预处理阶段做了 MinMax 归一化,把原始值缩放到 [0,1] 区间。模型输出的是归一化后的值,要得到真实预测值,必须做反归一化。这一步很多新手会忘,结果预测曲线和真实曲线形状对但数值完全不对。

# 假设 scaler 是训练时 fit 的 MinMaxScaler pred_real = scaler.inverse_transform(pred.numpy()) true_real = scaler.inverse_transform(true.numpy())

反归一化用的 scaler 必须是训练集上 fit 的那个,不能用全量数据重新 fit,否则验证集和测试集的变换基准不一致,评估指标会失真。源码里把 scaler 和模型一起保存,推理时直接加载,这个习惯值得保留。

4. 避坑与排查:跑不通时先看这几条

4.1 损失不下降或变成 NaN

现象:训练几个 epoch 后 loss 突然变成 NaN,或者一直停在初始值附近不动。原因通常是学习率太大导致梯度爆炸,或者输入数据里有 NaN/Inf。解决:先把学习率降到1e-4试一轮,同时在数据加载后加一句np.isnan(data).sum()检查缺失值。如果数据里有 NaN,用前向填充或插值补上,别直接丢,时间序列丢点会破坏连续性。

4.2 预测曲线整体平移或相位偏移

现象:预测值和真实值形状很像,但整体高了一截或低了一截,或者波峰波谷对不上。原因一般是位置编码没加或者加错了,Transformer 对序列顺序不敏感,没有位置信息时它会把序列当成无序集合处理。解决:检查forward里 LSTM 输出之后有没有加位置嵌入,源码里用的是可学习的位置编码,如果你改成正弦编码,注意频率参数要和d_model匹配。

4.3 验证 loss 远高于训练 loss

现象:训练 loss 降到 0.001 以下,验证 loss 还在 0.01 以上,差距一个数量级。原因基本是过拟合,模型把训练集的噪声也学进去了。解决:先把num_layers从 2 降到 1,再把 dropout 从 0.1 加到 0.3,如果还不行就加 weight decay(optimizer里设weight_decay=1e-4)。另外检查一下训练集和验证集的分布是不是差太多,时间序列里如果验证集对应的时间段有突变,模型没见过这种模式,loss 高是正常的。

4.4 显存不够或 batch 太大跑不动

现象:报CUDA out of memory,或者 batch 设大了直接卡死。原因:Transformer 的注意力矩阵是seq_len × seq_len,序列一长显存占用是平方级增长。解决:先把input_len从 48 降到 24,或者把 batch_size 从 64 降到 16。如果还不够,用梯度累积模拟大 batch:每 4 个 batch 做一次optimizer.step(),中间不清梯度。

4.5 推理时结果和训练时对不上

现象:训练完保存模型,重新加载做推理,预测结果和训练过程中验证集上的输出不一致。原因通常是推理时忘了调model.eval(),dropout 还在起作用,或者输入数据的归一化参数和训练时不一致。解决:推理前固定加model.eval()和torch.no_grad(),并且确保 scaler 是从训练时保存的文件里加载的,不要重新 fit。

5. 进阶用法:把混合模型改造成多变量多步预测

源码默认是单变量输入、单步输出,但实际业务里更多是多变量多步预测。改造的核心在两处:数据侧的input_dim和模型侧的输出维度。假设你有 5 个特征,要预测未来 12 步的 1 个目标变量,数据准备阶段把create_sequences的输入从一维改成二维,input_dim=5,pred_len=12。模型侧把最后的全连接层从nn.Linear(d_model, 1)改成nn.Linear(d_model, pred_len),forward 里取最后一个时间步的输出直接映射到 12 维。

# 多变量多步改造后的输出头 self.fc = nn.Linear(d_model, pred_len) def forward(self, x): lstm_out, _ = self.lstm(x) transformer_out = self.transformer_encoder(lstm_out) out = self.fc(transformer_out[:, -1, :]) # (batch, pred_len) return out

这样改完之后,损失函数还是 MSE,但计算的是 12 个预测步和 12 个真实值的均方误差。如果你想让模型对不同步长的预测误差有不同的权重,可以自己写一个加权 MSE,比如前面几步权重高、后面几步权重低,因为多步预测里远端误差天然更大。

另一个进阶方向是替换位置编码。源码用的是可学习位置嵌入,参数量随input_len固定,外推到更长序列时无能为力。如果你需要预测窗口动态变化,换成正弦位置编码或者 Rotary 位置编码,这两种都不依赖固定长度,外推能力更好。改的时候注意d_model必须是偶数,否则正弦编码的维度对不上。

还有一个我踩过的坑:多变量输入时,不同特征的量纲差异很大,MinMax 归一化要按列做,不能对整个矩阵做全局归一化。源码里单变量时没这个问题,改成多变量后如果忘了按列归一化,量纲大的特征会主导 loss,模型学出来的东西基本没用。从那以后我每次改多变量输入,都强制先跑一遍data.describe()看每列的均值和方差,确认归一化是按列做的再开始训练。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 14:26:15

Linux根分区空间不足排查与扩容:从df到resize2fs的完整实践

先说个几天前刚遇到过的事。一块 RK3568 开发板&#xff0c;SD 卡里烧了 Ubuntu 根文件系统&#xff0c;启动倒是很顺利&#xff0c;结果一执行df -h&#xff0c;挂载根/的那个分区可用空间只剩 400 多 MB&#xff0c;而系统本身才刚装上不到三天。然后我想往/opt里放一个交叉编…

作者头像 李华
网站建设 2026/10/2 14:25:30

串口助手C#源码解析:从SerialPort封装到自定义协议与CRC校验

简介&#xff1a;这是一份基于C#与Visual Studio 2010开发的串口助手源码&#xff0c;功能仿照经典SSCOM工具&#xff0c;面向需要学习串口通信编程、上位机开发或课程设计的初学者与进阶开发者。源码完整呈现了串口打开关闭、参数配置、数据收发与界面交互等核心逻辑&#xff…

作者头像 李华
网站建设 2026/10/2 14:25:30

Spring Boot微信扫码登录实战:OAuth2授权码流程与开放平台配置指南

标题里的So Easy不是标题党&#xff0c;但前提是你把流程底层先捋清楚。Spring Boot 做微信登录&#xff08;准确说是微信扫码登录&#xff09;这件事&#xff0c;拆开了看就是三个HTTP调用加一个回调接口&#xff1a;跳转授权页、拿code换access_token、拿access_token换用户信…

作者头像 李华
网站建设 2026/10/2 14:24:51

W25Q256JV的QE位陷阱:QSPI模式失效排查与状态寄存器配置指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 14:24:04

霍夫丁不等式:机器学习泛化分析的有限样本基石

1. 为什么一个“看起来很弱”的不等式&#xff0c;成了机器学习理论的基石&#xff1f;你第一次在《统计学习方法》或《Learning from Data》里看到霍夫丁不等式时&#xff0c;大概率会愣一下&#xff1a;设 $X_1, \dots, X_n$ 是独立随机变量&#xff0c;且对每个 $i$&#xf…

作者头像 李华