PyTorch Transformer长期预测实战:原理、调优与ETTh1案例
发布时间:2026/9/28 2:29:10来源:尧图网络
简介面向使用PyTorch进行时间序列预测的开发者这份资源将Transformer模型应用于长期预测任务内含完整源码、ETTh1标准数据集及原理说明可帮助理解自注意力机制如何捕捉序列中的长期依赖。压缩包共38个文件其中13个Python脚本涵盖模型构建、数据加载、训练与评估流程3个CSV提供实验数据另有训练好的模型权重pth、可视化结果图png及配置说明文件整体大小26.48MB目录结构清晰便于直接运行。目前已有984人学习下载。资源不仅给出可直接调用的Transformer实现还演示了从数据预处理到结果可视化的完整链路配合原理文档可快速迁移到其他时序场景适合希望结合理论与实践的中高级机器学习爱好者。1. Transformer 做长期预测不是拿过来就能跑先想清楚三个问题长期预测Long-term Forecasting和短期预测最大的区别在于输出长度可能等于甚至超过输入长度模型一旦把输入末尾的惯性当成了答案结果就是一条延迟后的复制曲线。我亲测这套基于 PyTorch 的 Transformer 时间序列预测资源时最先确认的就是三件事自注意力机制到底能不能抓住跨 7 天左右的周期依赖位置编码怎么处理小时级数据的时间顺序以及归一化可逆实例归一化 RevIN在这个场景里是不是必须。资源附带完整代码、ETTh1 电力变压器油温数据集和训练好的权重 model.pth输入 96 个小时的负荷与温度数据能直接预测未来 96 个小时的油温变化并输出 results.png 可视化对比图。适合做课程设计、毕业设计或者想验证 Transformer 在时序回归任务上真实表现的人。2. Transformer 做时间序列预测的原理拆解自注意力、位置编码与可逆归一化把 NLP 里的 Transformer 搬到时间序列上不是换个输入张量那么简单。文本是一串离散 token时间序列是一串连续数值两者的尺度、周期和噪声模式完全不同。这套资源的代码结构正好体现了做时序任务必需的四块改造自注意力编码器、位置编码、解码器输入拼接、以及 RevIN 归一化。2.1 自注意力机制为什么适合长期依赖RNN 处理长序列的核心痛点是信息要经过一步一步传递距离越远梯度越弱。Transformer 的自注意力绕开了这个限制任意两个位置之间只需要一次矩阵乘法就能建立关联。这个特性对时间序列尤其值钱ETTh1 里油温 OT 在早上和晚上呈现明显的日周期同时又受一周内工作日与周末的影响这种长程依赖正是自注意力擅长的。def scaled_dot_product_attention(Q, K, V, maskNone): d_k Q.size(-1) scores torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k) if mask is not None: scores scores.masked_fill(mask 0, -1e9) attn torch.softmax(scores, dim-1) out torch.matmul(attn, V) return out这段是经典的缩放点积注意力。除以math.sqrt(d_k)是为了防止 score 过大导致 softmax 进入饱和区。mask由资源里的masking.py生成作用是保证解码器在预测阶段看不到未来时刻的信息否则训练时等于把答案提前泄露给模型。Q、K、V 在代码里都来自经过嵌入的输入序列多头情况下每个头独立计算再拼接。实际训练中你会看到多个头里有的聚焦短时突变比如电压骤降有的关注全天趋势这就是多头注意力的价值。2.2 位置编码怎么计算以及长输入外推问题时间序列是数值序列本身没有顺序概念但预测任务里“第 3 个小时”和“第 27 个小时”含义完全不同。位置编码的作用就是把顺序信息注入模型。这套资源沿用的是 Transformer 原版的正弦位置编码偶数维度用正弦、奇数维度用余弦不同频率对应不同波长的周期模式。def sinusoidal_position_embedding(seq_len, d_model): pos torch.arange(seq_len, dtypetorch.float).unsqueeze(1) div_term torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model)) pe torch.zeros(seq_len, d_model) pe[:, 0::2] torch.sin(pos * div_term) pe[:, 1::2] torch.cos(pos * div_term) return pediv_term的计算很关键它让不同维度的正弦频率不同低频维度适合分辨长距离差异高频维度适合分辨相邻时刻。需要特别注意一个坑如果训练时seq_len96位置编码矩阵只学/生成到 96 个位置之后改成 336 直接加载权重多出来的位置没有对应编码模型输出会明显恶化。解决办法是重新训练或改成可外推的位置编码方式。2.3 资源文件结构与每个模块的职责解压之后建议先按目录过一遍这份代码把模型、数据、工具拆得很清楚路径作用layers/Embedding.py输入嵌入与位置编码叠加layers/TransformerBlocks.py多头自注意力、前馈网络、残差连接layers/Transformer.pyEncoder-Decoder 整体组装layers/Projection.py将解码特征投影为预测值layers/Invertible.pyRevIN 可逆实例归一化utils/data_loader.py读取 CSV、滑动窗口切分、构造批次utils/data_factory.py数据集入口切换utils/timefeatures.py提取小时、星期、月份等时间特征utils/metrics.pyMSE、MAE、RMSE、MAPE 评估utils/masking.py注意力掩码utils/decomposition.py序列趋势与季节分解data/ETTh1.csv电力变压器油温原始数据Transformer 前向计算的组装逻辑在Transformer.py里简化后大概是这样的流程# 前向逻辑简化示意 enc_out self.encoder(self.enc_embedding(x)) # 编码输入序列 dec_in torch.cat([x[:, -label_len:, :], zeros], dim1) # 真实尾段 填充 dec_out self.decoder(self.dec_embedding(dec_in), enc_out) output self.projection(dec_out) # 投影到预测维度这里label_len是解码器能看到的一小段真实历史尾段比如 48 个小时。它的意义是给解码器一个“锚点”让模型知道当前数据处于什么状态而不是从零开始盲猜。如果你把label_len设成 0预测难度会显著上升损失可能翻倍。2.4 RevIN 可逆实例归一化为什么时序场景离不开它这是时间序列预测里容易被忽略、但影响极大的一环。Invertible.py实现的可逆实例归一化思想非常简单对每个样本序列做一次自己的均值方差归一化送入模型预测完成后反归一化还原到原始量纲。class RevIN(nn.Module): def __init__(self, num_features, eps1e-5): super().__init__() self.eps eps self.num_features num_features def forward(self, x, mode): if mode norm: self.mean x.mean(dim1, keepdimTrue) self.std x.std(dim1, keepdimTrue) self.eps return (x - self.mean) / self.std else: # mode denorm return x * self.std self.meanETTh1 是跨两年的小时级数据不同季节的油温均值差异很大如果不做归一化模型要同时拟合不同偏移的分布训练会非常吃力。RevIN 的做法等于让模型只关注“相对变化模式”而不是绝对数值。eps1e-5防止某个窗口的标准差为 0 导致除零错误。这份资源在编码器输入前调用norm在投影输出后调用denorm顺序错了预测结果就会完全偏离真实值。3. 跑通 ETTh1 实战从依赖安装到训练出 results.png这章的目标是让你在自己机器上把整套流程跑完看到结果图生成。ETTh1 是 Electric Transformer Temperature 数据集的小时级版本记录的是电力变压器油温和六路负荷特征。七个特征列分别是高压负荷 HUFL、高压无功 HULL、中压负荷 MUFL、中压无功 MULL、低压负荷 LUFL、低压无功 LULL以及目标列油温 OT。这个数据集专门测试模型对长周期电力负荷的捕捉能力。3.1 环境准备与依赖安装先把依赖装齐。requirements.txt里主要是 torch、numpy、pandas、matplotlib、scikit-learn、tqdm 这几样版本不需要非常苛刻但建议 torch 不低于 1.9.0。我一般新建一个独立 Python 3.9 环境再装避免和系统环境冲突。pip install -r requirements.txt # 若没有 requirements.txt手动安装核心依赖 pip install torch numpy pandas matplotlib scikit-learn tqdm装完之后验证一下 torch 是否可用python -c import torch; print(torch.__version__, torch.cuda.is_available())不要求一定有 CUDACPU 也能跑只是训练速度慢很多。这份资源在 CPU 上训练 10 个 epoch、输入长度 96 的规模大致需要几十分钟到一小时GPU 上几分钟就能完成。如果显存不够先把d_model从 512 降到 256后续能省一半以上显存。3.2 数据加载与滑动窗口data_loader.py的核心逻辑是滑动窗口切分。给定一个长度为 L 的连续时间序列每次取seq_len作为模型输入取label_len pred_len作为标签窗口然后按步长滑动取下一段。class DatasetETT(Dataset): def __init__(self, root_path, seq_len, label_len, pred_len): df pd.read_csv(os.path.join(root_path, ETTh1.csv)) self.data df[[HUFL, HULL, MUFL, MULL, LUFL, LULL, OT]].values self.seq_len seq_len self.label_len label_len self.pred_len pred_len def __getitem__(self, index): s_begin index * 24 # 采样步长每 24 小时取一段 s_end s_begin self.seq_len r_begin s_end - self.label_len r_end r_begin self.label_len self.pred_len seq_x self.data[s_begin:s_end] seq_y self.data[r_begin:r_end] return torch.tensor(seq_x, dtypetorch.float32), \ torch.tensor(seq_y, dtypetorch.float32)注意这里两个张量的形状seq_x是[seq_len, 7]seq_y是[label_len pred_len, 7]。index * 24的步长设计是为了抽稀样本让训练集更独立减小相邻窗口之间的重叠。如果你希望训练集更大可以把步长改成 1代价是训练时间变长、相邻样本高度相似模型容易过拟合到短期波动。3.3 训练主流程与参数入口main.py是训练入口。常见训练循环的结构如下criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lrargs.learning_rate, weight_decay1e-4) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience2) for epoch in range(args.train_epochs): model.train() train_loss 0 for batch_x, batch_y in train_loader: batch_x batch_x.float().to(device) batch_y batch_y.float().to(device) outputs model(batch_x) # 完整编码-解码 pred outputs[:, -args.pred_len:, :] # 取预测段 true batch_y[:, -args.pred_len:, :] loss criterion(pred, true) # 只在预测段上算损失 optimizer.zero_grad() loss.backward() optimizer.step() train_loss loss.item()pred和true都只取每个序列的最后pred_len个时间步label_len部分虽然参与了计算但不计入损失。这个细节很重要它强制模型把注意力放在“真正需要预测的未来段”。ReduceLROnPlateau的patience2表示如果验证损失连续两个 epoch 没有下降学习率就减半这在长期预测任务里比固定学习率更保险能有效防止后期 loss 震荡。3.4 完整训练命令示例资源里没有强制要求统一命令行入口但常见的启动方式是把参数集中在 main.py 里或者通过 argparse 传入。下面是我在实际运行中验证过的一组配置python -u main.py \ --model Transformer \ --data ETTh1 \ --features M \ --seq_len 96 \ --label_len 48 \ --pred_len 96 \ --d_model 512 \ --n_heads 8 \ --e_layers 2 \ --d_layers 1 \ --batch_size 32 \ --learning_rate 1e-4 \ --train_epochs 10各参数含义--features M表示多变量输入、多变量输出即用全部 7 个特征预测全部 7 个特征后续你可以只取 OT 列做展示--e_layers 2 --d_layers 1表示编码器 2 层、解码器 1 层这是时间序列任务里比较平衡的配置再加深度提升有限但训练时间几乎翻倍。训练结束后资源会在根目录生成model.pth保存最优权重评估指标 MSE、MAE 会打印终端同时在predict/OT-ForecastResults.csv里落盘预测结果。3.5 可视化从 results.png 判断模型好坏训练完成后打开results.png它会像下面这样画出一段测试集上的真实值曲线与预测值曲线# 可视化脚本核心逻辑 plt.figure(figsize(12, 4)) plt.plot(true_cpu, labelTrue OT, linewidth2) plt.plot(pred_cpu, labelPred OT, linewidth2, linestyle--) plt.legend() plt.tight_layout() plt.savefig(results.png, dpi150)判断模型好坏不能只看 loss 数值还要看曲线的形状预测曲线如果紧紧贴着真实曲线、只是边缘有轻微抖动说明模型确实学到了周期模式如果预测段是一条接近水平的直线说明模型把未来预测成了“均值回归”这种情况通常发生在pred_len过大、seq_len不够长时。资源给出的model.pth是训练好的权重想快速看效果可以直接加载它做推理不用重新训练。4. 关键参数调优预测长度、模型宽度与学习率怎么选长期预测任务的参数选择直接影响结果质量而且各个参数之间存在耦合。这章讲清楚每个参数在什么场景下调、调到什么范围、以及调错了会出现什么现象。4.1 序列长度、标签长度、预测长度的搭配三个长度参数是所有时序预测实验的起点。核心原则是seq_len要覆盖数据的主要周期。ETTh1 是小时级数据一天 24 点、一周 168 点所以seq_len96能覆盖 4 个日周期seq_len336能覆盖 2 个完整周周期。参数建议范围场景说明seq_len输入长度96 ~ 336有日周期用 96有周周期或月周期用 336label_len解码器锚点0 ~ 48长预测时建议保留 48给解码器真实起点pred_len预测长度96 ~ 720越大难度越高观察测试集 MSE 随 pred_len 的增量如果你预测长度是 720 小时30 天seq_len96基本不可能学好因为模型连一个月的周期都没看全这时要把seq_len提到 336 甚至 512label_len设 48。反过来的问题是显存seq_len越大注意力矩阵越大显存占用随长度平方增长。4.2 d_model、n_heads、层数的组合逻辑模型宽度参数在layers/Embedding.py和layers/TransformerBlocks.py里定义。d_model是嵌入维度n_heads是注意力头数有一个硬性约束d_model必须能被n_heads整除。比如d_model512、n_heads8时每个头分到 64 维如果设成d_model128、n_heads8每个头只有 16 维表达能力锐减而且参数利用率很低。层数的选择上时间序列任务和 NLP 不太一样不需要堆 6 层编码器。e_layers2, d_layers1是性价比最高的起点数据复杂度不够时加深层数不仅不会提升反而更容易过拟合到训练集的噪声上。想从结果上看层数影响可以把编码器从 1 层加到 3 层观察验证集 MAE 是下降还是徘徊后者说明模型容量已经饱和。4.3 学习率、batch_size、dropout 的实践经验学习率在时序回归里比分类任务更敏感。这个资源在 Adam 默认学习率1e-3下训练 ETTh1 时前几个 epoch 的 loss 容易在 1.0 附近震荡不下降到1e-4之后曲线就平滑很多。如果显存有限制batch_size从 32 降到 16 时学习率最好也相应下调一半否则梯度噪声变大。dropout 设置在 0.05 到 0.1 之间数据量小几万样本用 0.1 防过拟合数据量大或模型规模小用 0.05。注意 dropout 只加在注意力输出和前馈网络之后不要在输入嵌入层乱加否则会破坏数值尺度。4.4 模型参数量估算改参之前先算一下账。参数量估算可以这样跑total_params sum(p.numel() for p in model.parameters() if p.requires_grad) print(fTotal trainable params: {total_params / 1e6:.2f}M)d_model512、编码器 2 层解码器 1 层的配置参数量通常在 10M 左右。这个数字决定了训练速度和显存占用10M 参数在 CPU 上跑一个 epoch 大约是 5~8 分钟GPU 上 30 秒以内。如果你只是想验证思路建议把d_model降到 256参数量能降到 3M 左右训练快三倍以上预测精度损失通常在可接受范围内。5. 避坑排查五处高频翻车点与解决办法长期预测的坑很多是数据层面的不是模型层面的。这一章记录了实际运行中最高频的五个问题每一条都按现象、原因、解决来写。5.1 训练 loss 卡住不降结果图是一条水平线现象MSE 在 1.0 到 1.5 之间长时间不下降最终预测曲线近似于均值线。原因ETTh1 的油温在夏冬两季均值差异很大模型没有经过归一化等于同时拟合多种不同偏移的分布。另一个常见诱因是seq_len太短比如 24 个小时模型根本看不到完整日周期。解决确认Invertible.py的norm在编码器之前调用、denorm在输出之后调用同时把seq_len提到至少 96让输入覆盖多个周期。5.2 预测结果像是输入曲线的平移复制现象预测段的形状和输入段最后几十个点几乎一样只是时间上延后了完全没有展现出未来趋势变化。原因模型学到的是“惯性外推”——注意力权重过多集中在输入末尾本质上是残差连接让输出近似等于最后输入的映射。pred_len相对seq_len越大这个现象越严重。解决把seq_len从 96 加到 336给模型更多历史观察窗口同时把label_len设为 48让解码器有真实锚点预测结果是“基于状态的推演”而不是“复制尾巴”。还有一种做法是训练早停因为过拟合后期模型会强化这种惰性模式。5.3 GPU 显存溢出OOM现象设置seq_len336, d_model512, batch_size32后训练刚开始就报CUDA out of memory。原因自注意力的复杂度是 O(L²)L 是序列长度。336×336的注意力矩阵乘上 8 个头和 32 的 batch显存消耗非常大。解决优先降batch_size到 16然后降d_model到 256。如果这两步之后还溢出可以把seq_len降到 168换输入长度比换模型深度更省显存。5.4 加载预训练模型报 size mismatch现象直接加载model.pth推理时报 Deconv 或 Embedding 层的 shape 不匹配。原因model.pth是在seq_len96, pred_len96, c_in7的配置下保存的你改成了pred_len192或换成了 10 列数据模型最后一层投影维度就不同了。解决权重文件只能匹配完全相同的模型结构。要换预测长度必须先用torch.load(..., map_locationcpu)加载后用load_state_dict(..., strictFalse)跳过不匹配的层或者直接重新训练。想省时间的话只改pred_len时把投影层单独重新初始化其余层用预训练权重这个技巧能省一半训练时间。5.5 时间特征没生效换数据集后结果波动剧烈现象换自己的数据集后模型训练速度很快但预测误差明显偏大而且怎么调seq_len和pred_len结果都不改善。原因timefeatures.py生成的小时、星期特征没有拼接到数值特征里或者拼接顺序和 Embedding 的输入通道数不匹配。Embedding.py的c_in写死成了 7ETTh1 的特征数换 10 列数据时会静默截断或报错。解决打印一个 batch 的输入形状确认是[batch, seq_len, c_in time_feat]而不是[batch, seq_len, 7]再把Embedding.py里的c_in改为自己数据的特征列数。换数据集时这几乎是指定操作别直接拿 ETTh1 的配置套。6. 换自己的数据集复现格式整理、训练入口与验证技巧拿到这份资源后最常问的问题是我想预测自己的 CSV怎么改其实只需要三步把 CSV 整理成 ETTh1 同构格式、改数据入口参数、按一个固定的验证流程检查结果。6.1 把 CSV 整理成模型认识的格式ETTh1 的格式有一个隐含约定第一列是时间戳最后一列是目标变量中间所有列都是特征变量。时间戳需要能解析为标准时间常见格式2016-07-01 00:00:00或2016-07-01都可以但采样间隔必须统一。如果你的数据是分钟级需要重采样成小时级否则timefeatures.py里对“小时”的提取会错位。# 用 pandas 重采样为小时级示例 df[date] pd.to_datetime(df[date]) df df.set_index(date).resample(1h).mean().reset_index()重采样之后检查缺失值Transformer 不像 RNN 有天然的缺失容忍能力空缺位置最好前向填充缺失超过一小时的片段优先用插值补齐。6.2 改数据入口与模型参数data_factory.py会在data路径下按文件名读数据。把你的 CSV 放到data/目录下比如data/my_data.csv然后改训练入口的参数python -u main.py \ --model Transformer \ --data my_data \ --features MS \ --seq_len 96 \ --label_len 48 \ --pred_len 96 \ --d_model 256 \ --n_heads 8 \ --e_layers 2 \ --d_layers 1--features MS表示多变量输入、单变量输出输入所有特征列但只预测最后一列目标。这样更贴近“用一堆传感器数据预测一个核心指标”的真实场景。如果你的数据特征列不是 7 个记得把Embedding.py的c_in改掉我反复强调这一点是因为踩过太多次形状不匹配的坑。6.3 验证预测质量的进阶技巧我换数据集后一定会做两件事。第一是分段回测把数据按时间顺序切成训练前 70%、验证中间 15%、测试最后 15%并且保证测试段不参与任何归一化统计量的计算。第二是画“基线对比”图用最简单的方式——“预测值等于一年前同期值”做对照如果 Transformer 的曲线还不如这个基线贴合说明模型没学到周期规律需要回头加长seq_len或检查时间特征拼接。判断预测曲线时重点看三个信号峰值位置是否对齐上升沿是否滞后以及预测段末尾是否出现过度平滑。这三个信号分别对应周期捕捉能力、相位偏差和长程不确定性处理。从那以后我每次换数据集都强制走一遍“查列数、查采样频率、查时间特征拼接、查测试段泄漏”四步流程这套流程基本能拦住九成的翻车现场希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网