尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Transformer的时间序列预测Python源码实战与注意力机制解析

基于Transformer的时间序列预测Python源码实战与注意力机制解析 简介基于Transformer模型的时间序列预测项目面向期末大作业、课程设计或刚开始接触深度学习的开发者提供一套可直接运行的完整Python源码。压缩包共有12个文件包括11个功能清晰的.py脚本和1个Jupyter Notebook分别承担数据加载与清洗、时间特征提取、编码器/解码器搭建、多头注意力实现、训练与结果评估等任务整包仅28KB结构紧凑且易于按需修改。目前已有1817人浏览学习参考价值较高。全部代码均通过本地编译验证难度适中并经过助教老师审定可以放心下载使用配合代码中的可视化流程能辅助理解Transformer核心结构在时间序列预测中的工作方式。在此基础上还可在数据预处理、模型参数与评估指标方面自行扩展完成从复现到改造的完整学习过程。1. 用 Transformer 做时间序列预测这份源码为什么能直接跑通Transformer 在 NLP 里站稳脚跟之后做时间序列预测的人都想把它搬到自己的数据上。但真正动手你会发现网上教程一抓一大把能一次跑通、数据加载到评估全链路完整的却不多尤其是面向课程设计或期末项目这种既要出图又要能讲清楚原理的场景。这份基于 Transformer 模型的时间序列预测 Python 源码属于典型的高分课程项目结构数据加载、时间特征编码、掩码机制、完整 encoder-decoder 模型拆分、评估指标全部齐整而且是已经编译运行过的版本不用自己再去补一堆缺失的 import。适合正在做期末大作业或想快速上手 Transformer 时序预测的人直接改数据路径就能跑出基线结果再往论文或报告里填实验就行。2. 为什么时序预测选 Transformer 而不是 LSTM注意力机制的取舍2.1 从自注意力到时序相关性能抓住多步之前的依赖LSTM 和 Transformer 都能处理序列数据但在捕捉长期依赖这件事上两个机制完全不同。LSTM 靠门控结构把信息沿着时间步向后传信息每经过一个 step 就会衰减一部分想把 100 步之前的模式搬过来需要靠记忆单元反复筛选实际效果经常打折扣。Transformer 的自注意力Self-Attention机制则不管两个位置之间隔了多远直接计算所有时间步两两之间的关联权重。也就是说预测今天的序列值时模型可以直接把 30 天前那个时间步赋予高注意力权重路径长度是 1不存在梯度消失或信息衰减的问题。这个特性对时间序列预测非常关键。很多真实数据集里的模式不是周期性的而是以事件形式出现——某天的一个峰值可能直接影响 20 天后另一个变量的走势。用 LSTM 去拟合这种关系需要学很久才能把门控权重调对而 Transformer 在理论上天然就有这个能力。当然代价是计算复杂度从 O(n) 涨到 O(n²)序列越长显存压力越大。2.2 项目模块结构从 data_loader 到 model 的调用链看这份源码的目录结构能明显看出它是模仿 Informer 系列项目的工程化组织方式不是那种十几个 cell 堆在 Jupyter Notebook 里的教学 demo。目录里有data_loader.py、timefeatures.py、masking.py、metrics.py、tools.py这些顶层工具文件以及models目录下的embed.py、encoder.py、decoder.py、atten.py、model.py、multi_model.py。每个模块职责分离训练主流程放在seq2seq.ipynb里这种设计对于期末项目答辩特别占优势老师问起来你至少能说清楚每个文件在干什么。整个调用链路是data_loader.py读取原始 CSV 并构造滑动窗口样本timefeatures.py把时间戳转成数值特征然后数据进入embed.py做输入嵌入和位置编码再依次通过encoder.py和decoder.py。注意力计算集中在atten.py里masking.py负责生成 decoder 需要的事件掩码确保预测当前位置时看不到未来数据。model.py负责把组件拼装起来对外暴露接口multi_model.py则是用于对比实验的变体模型。理解这个调用链你改模型结构时才知道动哪个文件。2.3 掩码机制让 decoder 不偷看答案时间序列预测里的掩码和 NLP 里的掩码目的不太一样。NLP 中 decoder 需要防的是预测下一个词时看到了当前的词时序预测里 decoder 要防的是预测第 t1 步时看到了第 t1 步及之后的真实数据。那份masking.py文件生成的就是这种因果掩码——上三角矩阵全为负无穷或者 0注意力分数加上去之后未来位置的权重就被压制掉了。import torch def generate_square_subsequent_mask(sz: int) - torch.Tensor: 生成上三角为 True 的掩码矩阵 sz: 序列长度 返回形状 (sz, sz) 的布尔掩码, 用于 decoder 自注意力 mask torch.triu(torch.ones(sz, sz), diagonal1) 1 return mask # 调用示例: 对长度为 10 的序列生成掩码 mask generate_square_subsequent_mask(10) print(mask.shape) # torch.Size([10, 10])逻辑说明torch.triu取上三角矩阵diagonal1表示从主对角线往上一格开始保留这样对角线及以下全是 False即当前位置只能看到自己和过去的位置。在多头注意力计算里这个布尔矩阵会被转换成-1e9这样的极小值加到attention scores上再进 softmax未来位置的权重就趋近于 0。需要留意的参数是sz它必须和 decoder 输入序列长度一致否则广播时会报维度错误。3. 数据加载与时间特征编码训练前最容易翻车的一步3.1 data_loader 的滑动窗口采样逻辑时间序列预测的数据加载和图像分类不一样不能随机打乱样本。因为样本之间是时间连续的乱序打乱会把未来的信息泄漏到训练集里。这份源码用的是滑动窗口采样也就是固定窗口长度window_size从序列开头逐步向后滑每个窗口生成一个样本对应的标签是未来若干步的值。一般代码结构是这样def create_sequences(data: pd.DataFrame, input_len: int, pred_len: int): 把长序列切成 (输入, 标签) 对 data: 已经按时间排序的 DataFrame, 列是特征 input_len: 模型看到的回看窗口长度 pred_len: 要预测的未来步数 X, y [], [] total_len len(data) for i in range(total_len - input_len - pred_len): X.append(data.iloc[i : i input_len].values) y.append(data.iloc[i input_len : i input_len pred_len].values) return np.array(X), np.array(y)逻辑说明每个样本用前input_len条记录作为输入后pred_len条记录作为标签循环步长是 1这样相邻样本大部分数据是重合的。参数上最需要注意的是input_len的选择——如果你的数据有日周期input_len至少得覆盖一个完整周期比如 24 小时数据就设 24 或 48pred_len则是你的预测目标长度期末项目一般设 1 到 7 比较稳妥。还有一个细节循环结束后样本数量是total_len - input_len - pred_len控制这个差值就能控制训练集大小对于小数据集要留意别把样本切得太少导致训练不稳。3.2 timefeatures.py 和时间戳数值化原始 CSV 里的时间列通常是2024-03-01 14:30:00这样的字符串Transformer 的嵌入层只接受数值和张量所以得先把时间转成数值特征。timefeatures.py文件干的就是这件事——把时间戳拆成hour、day、weekday、month等分量再归一化到合理范围。def time_features_from_datetime(dt_series: pd.Series) - pd.DataFrame: 从 pandas 时间序列中提取周期数值特征 dt_series: pd.to_datetime 之后的时间序列 data pd.DataFrame({ hour: dt_series.dt.hour / 23.0, day: dt_series.dt.day / 31.0, weekday: dt_series.dt.weekday / 6.0, month: dt_series.dt.month / 12.0, }) return data逻辑说明这里没有直接塞1, 2, 3...这种原始整数而是归一化到 01 区间避免数值范围差别太大干扰模型收敛。值得改进的地方是周期特征用归一化整数其实不够准确——比如23 点和0 点数值上差得远但实际只隔 1 个小时。更精细的做法是用sin和cos对进行周期编码如果你要拿这份源码交作业把这个改进加进去答辩时就是加分项。代码里dt.day / 31.0这种粗粒度处理会丢失月份天数差异的信息但胜在简单稳定不会报错。3.3 训练集和测试集怎么切才不会被老师挑毛病课程项目最常见的错误是用随机切分。尾随数据泄漏的表现是训练时指标很好看测试时一塌糊涂——因为模型在训练阶段已经见过测试区间附近的数据了。正确做法是按时序切分前 80% 训练、后 20% 测试中间留一段缓冲区避免数据泄漏。def train_test_split_temporal(data: pd.DataFrame, train_ratio: float 0.8, gap: int 10): 按时间顺序切分训练和测试集 train_ratio: 训练集占比 gap: 训练集末尾和测试集开头之间留出的间隔, 防止窗口数据重叠 train_len int(len(data) * train_ratio) train_data data.iloc[:train_len] test_data data.iloc[train_len gap:] return train_data, test_data需要强调的参数是gap。因为滑动窗口的input_len可能覆盖几十条数据如果训练集最后一个窗口恰好延伸到测试集前几行模型的训练目标里就包含了测试区间的信息这就是隐蔽的数据泄漏。gap至少大于input_len才能彻底隔开。标准化的时候也要注意scaler只能用训练集的均值和方差去拟合再分别 transform 训练集和测试集而不能在整个数据集上先 fit 再切分。这份源码里的utils或tools.py应该封装了这部分逻辑交作业前多检查一眼这个顺序。4. 搭建模型主体embed 层、注意力计算与 seq2seq 训练闭环4.1 embed.py 里的输入嵌入与位置编码Transformer 本身不感知顺序时间序列更强调整体的数值分布所以嵌入层的设计直接影响模型效果。这份源码里的embed.py做法是先对每个特征维度做一层线性映射把输入从input_len × feature_dim变成input_len × d_model然后把可学习的位置编码逐元素加到嵌入结果上。可学习位置编码长这样import torch import torch.nn as nn class PositionalEmbedding(nn.Module): def __init__(self, d_model: int, max_len: int 5000): super().__init__() self.pe nn.Parameter(torch.randn(max_len, d_model), requires_gradTrue) def forward(self, x: torch.Tensor) - torch.Tensor: # x 形状: (batch_size, seq_len, d_model) return x self.pe[:x.size(1)]逻辑说明nn.Parameter把位置向量定义为可学习参数requires_gradTrue意味着位置信息会随训练更新。max_len必须大于训练时可能遇到的最长序列设小了会直接索引越界。这里不如原版 Transformer 的三角位置编码稳定但优势是能根据数据分布自动调整位置向量。如果你发现训练 loss 不降可以试着把PositionalEmbedding换回固定三角编码对比一下。4.2 encoder 和 decoder 的注意力和参数配置atten.py里实现的是标准的缩放点积注意力核心公式是Attention(Q, K, V) softmax(QK^T / sqrt(d_k)) V模型里实际使用多头注意力也就是把d_model拆成多个头各自计算注意力再拼接。参数设置的常见经验值是d_model 128、n_heads 4这里有两个必须匹配的约束d_model必须能被n_heads整除d_k d_model / n_heads决定了每个头能建模多细的局部模式。源码的model.py里一般会有下面这种初始化self.encoder Encoder( attn_layerMultiHeadAttention(d_model64, n_heads4), d_model64, dropout0.1 ) self.decoder Decoder( attn_layerMultiHeadAttention(d_model64, n_heads4), d_model64, dropout0.1 )如果把d_model设为 64、n_heads设为 4那么每个头的维度是 16这个值偏小会导致每个头学到的模式较简单模型表达能力受限如果反过来设太大参数量会涨得很快。对于课程项目这种中小数据集d_model在 32 到 128 之间、n_heads在 2 到 8 之间是比较安全的区域。4.3 seq2seq.ipynb 里的训练流程与 loss 曲线判断训练主流程写在seq2seq.ipynb里典型结构是从数据加载、模型初始化、定义 optimizer 和 loss到循环训练并每个 epoch 打印验证集 loss。优化器一般用 Adam初始学习率 1e-3 到 1e-4 都有人用。时间序列预测的 loss 最常用 MSE因为它的梯度在误差大时更陡收敛更积极。optimizer torch.optim.Adam(model.parameters(), lr1e-4) loss_fn torch.nn.MSELoss() for epoch in range(epochs): model.train() train_loss 0.0 for batch_x, batch_y in train_loader: optimizer.zero_grad() pred_y model(batch_x) loss loss_fn(pred_y, batch_y) loss.backward() optimizer.step() train_loss loss.item() model.eval() with torch.no_grad(): val_pred model(val_x) val_loss loss_fn(val_pred, val_y) print(fEpoch {epoch1}, Train Loss: {train_loss/len(train_loader):.6f}, Val Loss: {val_loss:.6f})判断训练状态时我一般看两条曲线训练 loss 持续下降、验证 loss 也同步下降这是健康状态训练 loss 降但验证 loss 开始回升就要考虑调低学习率或加大 dropout两个 loss 都不动大概率是学习率太小或数据没归一化。batch_x的形状和模型 forward 要求的输入维度必须匹配报错信息里如果出现mat1 and mat2 shapes cannot be multiplied先去检查d_model和特征维度是否对齐。5. 避坑指南Transformer 时序预测的五个血泪经验5.1 现象训练集 loss 很低测试集 loss 直接崩掉其实就是数据泄漏最常见的元凶是标准化切分顺序搞反了。很多人在整个数据集上直接scaler.fit_transform()再做切分模型的统计信息里就混入了测试区间的均值和方差。解决建模时强制把切分放在标准化之前scaler只在训练集上 fit然后分别 transform 训练集和测试集。如果在源码里找到类似scaler MinMaxScaler()先于数据切分执行立刻把顺序调整过来。5.2 现象decoder 预测结果前期准确后期完全失真这是典型的训练阶段和推理阶段行为不一致。训练时 decoder 每一步都喂真实值作为下一步的输入Teacher Forcing推理时只能拿模型自己的预测值继续往后滚误差会逐步累积。解决训练时以一定比例比如 0.3~0.5随机替换成模型上一步的预测结果这算是序列生成里很常见的 trick能显著改善多步预测稳定性。如果这份源码里的 decoder 没有实现这个逻辑可以在seq2seq.ipynb里补一个自定义循环。5.3 现象验证集指标不错但测试集上 MAPE 异常大问题出在metrics.py里的评估指标对接近零的真实值过于敏感。MAPE 的公式是mean(|真实值 - 预测值| / 真实值)如果测试集里真实值接近零即便误差绝对值不大百分比也会爆炸。解决评估时不要只看单一指标把 MSE、MAE、MAPE 并列展示遇到含零的数据集换用 SMAPE 或直接多用 MAE 做判断。源码里的metrics.py本来就有多个指标函数跑完之后把几张图都打出来再下结论。5.4 现象加大 d_model 之后显存直接不够或者训练速度骤降自注意力的显存占用是 O(n²) 量级的d_model加倍意味着注意力矩阵的内存翻两倍以上。我在自己的数据上试过序列长度超过 200 时d_model256的配置在 8GB 显存上很容易 OOM。解决如果数据本身没有太强的长程依赖降n_heads比降d_model更划算或者对长序列做降采样减少输入长度实在要保留长序列考虑用 Informer 里的 ProbSparse 注意力近似计算但改起来工程量大。课程作业不建议把精力耗在这个上面缩短序列长度保证能出结果更重要。5.5 现象训练 loss 完全不动一直是初始值附近的震荡多数情况是学习率过大导致梯度在极小值附近来回跳或者是数据范围差异太大导致 MSE 本身就偏大模型初始几步根本学不到东西。解决先检查数据是否做了归一化再看学习率调成 1e-4 再试。还有一个很隐蔽的原因mask 矩阵用到了torch.triu生成的布尔值但没有在注意力计算里转换成负无穷模型等于没加掩码训练目标虽然没错但网络学不到反向传播的真实信号。出现这种玄学问题时先用一个小数据样本单步调试把 mask 打印出来看一眼是真的在用负无穷屏蔽还是被 softmax 吞掉了。6. 进阶验证技巧用 multi_model 跑对照实验让结论更有说服力如果你只是把训练代码跑通写出预测曲线那这份资源就只用了五六成功力。期末项目拿高分和正常交作业的区别往往在于有没有对照实验。这份源码里的multi_model.py就是干这个用的——它可以让你在同一个数据上跑多个变体模型比如去掉位置编码的版本、不同注意力头数的版本。跑对照实验时我会把每个变体的验证集 loss 和测试集 MAE 记录在一个表格里然后给模型结构加或减组件观察效果变化。python train_model.py --d_model 64 --n_heads 4 --dropout 0.1 --model_type transformer python train_model.py --d_model 64 --n_heads 2 --dropout 0.1 --model_type transformer python train_model.py --d_model 64 --n_heads 4 --dropout 0.3 --model_type transformer三个命令跑完后对比每组超参数下的测试集 MAE哪些参数对结果影响最大一目了然。如果改代码跑对照实验不方便退而求其次的做法是把训练过程中的验证集 loss 曲线画在同一张图里曲线之间的差距就能说明结构差异的影响。这里有一个判断基线的重要习惯先把d_model调小跑通流程再逐步加大参数验证效果变化比一上来就堆最大配置要省时间得多。验证模型是否真正学到序列模式还有一个方法做一步预测和逐步预测对比。先用前 96 步预测下一步再预测未来 10 步如果逐步预测误差和一步预测差距过大说明模型主要依赖的是短期惯性而不是长期依赖关系。真正学会长期模式的模型在多步预测时误差上涨不会太陡。从那以后我每次跑完时序模型都要强制自己走一遍这个验证流程——先看误差曲线再对比多步预测衰减程度最后才敢把结果写进报告里。数据驱动的东西多留一个心眼总不是坏事。希望帮到你。本文还有配套的精品资源点击获取
返回列表