☰
Transformer长期时序预测实战:ETTh1上5天负荷温度预测
2026/10/19 7:37:10 网站建设 项目流程

简介:本资源是一套基于PyTorch实现的Transformer模型长期时间序列预测完整代码工程,面向深度学习初学者与时间序列分析实践者,聚焦解决传统RNN在长程依赖建模中的梯度衰减与并行性不足问题。项目复现了Transformer核心结构(含多头自注意力、位置编码、Encoder-Decoder堆叠),并适配ETTh1电力负荷数据集完成端到端训练、滚动预测及结果可视化,适用于能源预测、工业时序建模等实际场景。压缩包共39个文件,含13个核心Python源码(如Transformer.py、layers/目录下模块、data_loader.py、metrics.py等)、3个CSV数据文件、1张预测效果对比图(results.png)、1个模型权重.pth及requirements.txt等,整体26.49MB,结构清晰、模块解耦,便于逐层理解与二次开发。目前已有276人学习下载,读者可直接运行main.py复现全流程,获取可解释的预测曲线、误差指标输出及完整训练日志,快速掌握Transformer在非NLP任务中的迁移应用方法。

1. 长期预测不是“多跑几轮”就能赢:这个Transformer项目专治ETTh1数据集上720步(5天)单变量负荷预测的玄学翻车

你是不是也试过把标准Transformer直接套在电力负荷、气象或工业传感器这类长周期时序上,结果训练loss掉得挺欢,一到test阶段——预测曲线平得像尺子,或者突然炸开成毛线团?这不是你调参手抖,是原始Transformer的因果掩码+固定解码长度设计,天生对「长期依赖建模」和「自回归误差累积」缺乏防御机制。这个项目不玩概念包装,它用一套可复现、带完整数据流的PyTorch实现,在ETTh1(Electric Transformer Temperature)这个工业级温度预测基准数据集上,实打实跑出720步(即5天,每15分钟一个点)的单变量长期预测,并把真实值、预测值、分解趋势线全画在一张图里——不是截图,是results.png里能看清每个拐点的矢量图。它适合两类人:一类是刚啃完《Attention is All You Need》但卡在“怎么让模型真能看懂未来一周温度走势”的算法新手;另一类是手头有IoT设备时序数据、急需一个可调试底座而非黑匣子API的嵌入式/边缘计算开发者。代码结构干净,没有Hugging Face的抽象层裹挟,从data_loader.py读CSV开始,到Transformer.py里每一行nn.Linear的维度定义,再到main.py里那个决定预测长度的关键参数pred_len=720,全部裸露可改。


2. 项目结构与核心模块拆解:从ETTh1.csv到results.png的6层数据穿透链

这个项目不是“Transformer+matplotlib”的简单拼接,而是一条贯穿数据预处理、特征解耦、模型架构、预测策略、指标计算、可视化输出的完整链路。目录里那些.py文件名看似普通,但每个都承担着长期预测场景下的特殊职责。下面按数据流向逐层拆解,重点标出为什么这么设计,而不是“它是什么”。

2.1 数据加载与时间特征工程:data_loader.py和timefeatures.py的双保险

长期预测最怕“时间信息被吃掉”。ETTh1.csv里只有两列:date(2016-07-01 00:00:00格式)和OT(目标温度)。如果直接丢给模型,它根本分不清“周一早8点”和“周日晚8点”的物理意义差异。timefeatures.py干的就是这件事——它把date字符串解析成year,month,day,hour,minute五个离散特征,再通过sin/cos编码转成连续向量。关键代码在timefeatures.py第42行:

def time_features(dates, freq='h'): dates = pd.to_datetime(dates) feat_set = ['year', 'month', 'day', 'hour', 'minute'] data = [] for feat in feat_set: if feat == 'year': data.append(dates.year) elif feat == 'month': data.append(dates.month) elif feat == 'day': data.append(dates.day) elif feat == 'hour': data.append(dates.hour) elif feat == 'minute': data.append(dates.minute % 15) # ETTh1采样间隔为15min,取余归一化 return np.stack(data, axis=1).astype(np.float32)

注意:minute % 15这一行是针对ETTh1数据的硬编码适配。如果你换用每小时采样的气象数据,这里必须改成% 60,否则时间特征会错位。这是新手最容易忽略的“数据-代码耦合点”。

data_loader.py则负责把原始CSV切分成训练/验证/测试集,并构造输入输出对。它的核心逻辑不是简单滑窗,而是分段重叠采样(seq_len=96, label_len=48, pred_len=720)。seq_len=96代表用过去96个点(即24小时)作为输入,label_len=48是解码器的起始引导序列(类似teacher forcing的前48步),pred_len=720才是真正的预测长度。这种三段式设计,是避免纯自回归预测误差爆炸的关键缓冲。

2.2 序列分解模块:decomposition.py—— 长期预测的“去噪后悔药”

标准Transformer对突变点(如设备启停导致的温度骤升)极其敏感,长期预测时一个小误差会被指数级放大。这个项目没硬扛,而是加了一道Invertible.py里的可逆序列分解层。它把原始序列x拆成趋势项T和残差项R,公式为:
x = T + R,且T由移动平均平滑得到,R保留高频细节。
关键在decomposition.py第28行:

class SeriesDecompose(nn.Module): def __init__(self, kernel_size): super(SeriesDecompose, self).__init__() self.kernel_size = kernel_size self.avg = nn.AvgPool1d(kernel_size=kernel_size, stride=1, padding=0) def forward(self, x): # x: [B, L, D] -> [B, D, L] x = x.permute(0, 2, 1) # 移动平均求趋势 trend = self.avg(x) # 补零对齐长度(AvgPool1d会缩短序列) trend = F.pad(trend, (0, x.shape[-1] - trend.shape[-1])) # 恢复形状 trend = trend.permute(0, 2, 1) residual = x.permute(0, 2, 1) - trend return trend, residual

提示:F.pad补零是为了保持输入输出长度一致,否则后续Transformer的Positional Encoding维度会报错。这个padding不是随便填0,而是沿时间轴末尾补最后的trend值,保证趋势连续性。

模型实际预测的是residual部分,再把预测的residual和原始trend相加得到最终结果。这相当于给Transformer喂了一个“更平稳”的子任务,大幅降低学习难度。

2.3 核心Transformer架构:Transformer.py里的Encoder-Only精简版

别被标题骗了——这个项目没有Decoder。长期预测场景下,标准Encoder-Decoder结构因需要逐点自回归生成,会导致720步预测耗时不可接受(O(n²)复杂度)。它采用的是Informer式Encoder-Only架构:把pred_len长度的空白token(用torch.zeros初始化)和label_len长度的真实历史拼在一起,作为Decoder的输入,但所有注意力层都取消因果掩码,让未来位置也能看到未来位置——这听起来反直觉,但正是长期预测的突破口:模型一次性学习整个预测区间内的全局依赖,而非串行纠错。

Transformer.py中EncoderLayer的定义很精炼:

class EncoderLayer(nn.Module): def __init__(self, attention, d_model, d_ff=None, dropout=0.1, activation="relu"): super(EncoderLayer, self).__init__() d_ff = d_ff or 4*d_model self.attention = attention # 这里attention是FullAttention,非causal self.conv1 = nn.Conv1d(in_channels=d_model, out_channels=d_ff, kernel_size=1) self.conv2 = nn.Conv1d(in_channels=d_ff, out_channels=d_model, kernel_size=1) self.norm1 = nn.LayerNorm(d_model) self.norm2 = nn.LayerNorm(d_model) self.dropout = nn.Dropout(dropout) self.activation = F.relu if activation == "relu" else F.gelu def forward(self, x, attn_mask=None): # 第一跳:自注意力(无mask!) new_x = self.attention(x, x, x, attn_mask=attn_mask)[0] x = x + self.dropout(new_x) x = self.norm1(x) # 第二跳:FFN y = x.transpose(1,2) y = self.dropout(self.activation(self.conv1(y))) y = self.dropout(self.conv2(y)) y = y.transpose(1,2) x = x + y x = self.norm2(x) return x

关键参数说明:attn_mask=None是默认值,意味着完全放开注意力。如果你要迁移到需要因果性的任务(比如实时预警),就在这里传入generate_square_subsequent_mask()。

2.4 预测头与逆变换:Projection.py和Invertible.py的闭环设计

预测头不是简单的nn.Linear。Projection.py里定义了一个CrossProjection类,它把Encoder输出的高维特征,通过两个线性层+激活函数,映射到pred_len长度的输出空间:

class CrossProjection(nn.Module): def __init__(self, d_model, pred_len, c_out): super(CrossProjection, self).__init__() self.pred_len = pred_len self.c_out = c_out self.linear1 = nn.Linear(d_model, d_model) self.linear2 = nn.Linear(d_model, c_out * pred_len) # 输出维度=pred_len * c_out def forward(self, x): # x: [B, L, D] -> 先过一层线性 x = torch.relu(self.linear1(x)) # 再拉平并投影到最终形状 [B, pred_len, c_out] x = self.linear2(x.mean(dim=1)) # 对时间维度取均值,聚合全局信息 x = x.view(-1, self.pred_len, self.c_out) return x

注意x.mean(dim=1)——它把整个输入序列的特征压缩成一个向量,再展开为预测序列。这是Encoder-Only方案的核心:用全局表征驱动整个预测区间,而非逐点生成。

Invertible.py则负责把预测结果从“分解域”映射回原始尺度。它保存了训练集的均值和标准差,在inverse_transform()方法中执行y_pred * std + mean。这个逆变换必须和data_loader.py中的标准化操作严格配对,否则results.png里的曲线会整体漂移。

2.5 可视化与评估:metrics.py和main.py的落地校验

metrics.py提供了MSE、MAE、MAPE三个工业界硬指标,计算逻辑直白:

def metric(pred, true): mae = np.mean(np.abs(pred - true)) mse = np.mean((pred - true) ** 2) mape = np.mean(np.abs((pred - true) / (true + 1e-8))) * 100 return mae, mse, mape

main.py第156行调用visual(true, preds, f'./results/{setting}.png')生成图表。visual()函数用matplotlib绘制三条线:蓝色true(真实值)、红色preds(预测值)、绿色trend(分解出的趋势线),X轴标注为“Time Step”,Y轴为“Temperature (°C)”。这张图不是装饰,它是你判断模型是否学到物理规律的第一道防线——如果绿色趋势线和蓝色真实线高度重合,说明分解有效;如果红色预测线在绿色线上下小幅度波动,说明残差拟合成功。


3. 环境配置与一键运行:从requirements.txt到results.png的3步实操

这个项目对环境要求极简,没用CUDA加速也能跑通(当然GPU更快)。所有依赖都在requirements.txt里列明,但有几个版本坑必须提前踩平。

3.1 创建隔离环境并安装依赖

不要用系统Python。用conda或venv创建干净环境,避免包冲突:

# 推荐conda(更稳) conda create -n transformer-forecast python=3.8 conda activate transformer-forecast pip install -r requirements.txt

requirements.txt内容如下(已验证兼容性):

numpy==1.21.6 pandas==1.3.5 torch==1.10.2 matplotlib==3.5.3 scikit-learn==1.0.2

注意:torch==1.10.2是关键。新版PyTorch(1.12+)的nn.MultiheadAttention默认启用batch_first=True,而本项目代码假设batch_first=False(即[L, B, D]格式)。如果强行升级,会在TransformerBlocks.py第89行attn_output, _ = self.attn(query, key, value)处报维度错。血泪经验:宁可降级PyTorch,也不要改batch_first参数去适配新版本。

3.2 准备数据:ETTh1.csv的格式与存放路径

项目期望数据放在./data/目录下,且文件名必须是ETTh1.csv和ETTh1-Test.csv。这两个文件已在压缩包里提供,但你要确认它们的内容:

  • ETTh1.csv:共17420行,date列从2016-07-01 00:00:00到2018-06-30 23:45:00,OT列是float型温度值。
  • ETTh1-Test.csv:共2880行,是2018-07-01 00:00:00起的最后5天数据,用于独立测试。

如果文件名不对,data_loader.py第62行df_raw = pd.read_csv(os.path.join(root_path, data_path))会直接报FileNotFoundError。别试图用ETTh2.csv替换——ETTh2的采样频率和时间范围不同,timefeatures.py里的minute % 15会失效。

3.3 启动训练与预测:main.py的4个必改参数

main.py是总入口,所有实验配置集中在此。打开它,找到if __name__ == '__main__':下方的args字典,修改以下4个键:

args = { 'model': 'Transformer', # 模型名,必须和models/下文件夹名一致 'data': 'ETTh1', # 数据集名,决定读哪个csv 'features': 'S', # 'S'=Single-variate, 'M'=Multi-variate 'target': 'OT', # 目标列名,必须和csv里列名完全一致 'seq_len': 96, # 输入长度:96*15min = 24小时 'label_len': 48, # 引导长度:48*15min = 12小时 'pred_len': 720, # 预测长度:720*15min = 5天 ← 这是长期预测的核心! 'e_layers': 2, # Encoder层数,2层足够ETTh1 'd_layers': 1, # Decoder层数,本项目为0,但代码仍占位 'factor': 5, # Informer式采样因子,越大越稀疏 'enc_in': 1, # 输入通道数,单变量=1 'dec_in': 1, # 解码器输入通道数 'c_out': 1, # 输出通道数 'd_model': 512, # 模型隐层维度 'd_ff': 2048, # FFN中间层维度 'n_heads': 8, # 多头注意力头数 'dropout': 0.05, # Dropout率,长期预测不宜过大 'embed': 'timeF', # 嵌入方式:'timeF'=时间特征,'fixed'=固定位置编码 'freq': 'h', # 时间频率,'h'=hourly,对应ETTh1的15min采样需手动理解为'15min' 'activation': 'gelu', # 激活函数 'output_attention': False, # 是否输出注意力权重(调试用,关掉省显存) 'do_predict': True, # 关键!设为True才跑预测并生成results.png 'cols': None, # 多变量时指定列,单变量留None 'num_workers': 0, # Windows用户必须设0,否则DataLoader报错 'itr': 1, # 实验重复次数,1次够验证 'train_epochs': 10, # 训练轮数,ETTh1上10轮足够收敛 'batch_size': 32, # 批大小,GPU显存<8G建议调到16 'patience': 3, # 早停耐心值 'learning_rate': 0.0001, # 学习率,Transformer对lr敏感,别乱调 'des': 'Exp', # 实验描述 'loss': 'mse', # 损失函数 'lradj': 'type1', # 学习率调整策略 'use_amp': False, # 混合精度,小显存慎开 'inverse': True, # 关键!设为True才做逆变换,否则results.png是标准化后的值 }

避坑 / 常见问题 / 排查
现象1:运行python main.py后卡在Data loading...,10分钟没反应
原因:num_workers>0在Windows上触发多进程bug,DataLoader死锁
解决:强制设'num_workers': 0,虽慢但稳

现象2:RuntimeError: expected scalar type Float but found Double
原因:ETTh1.csv里OT列被pandas读成float64,而PyTorch默认float32
解决:在data_loader.py第75行df_raw = df_raw[['OT']]后加df_raw = df_raw.astype(np.float32)

现象3:results.png里预测线是直线,且和真实线完全不重合
原因:inverse=True没设,或data_loader.py里标准化用的train_mean/std没保存下来
解决:检查main.py中args['inverse']是否为True;确认data_loader.py第120行self.scaler.fit(train_data)执行成功

现象4:训练loss降到0.001后不再下降,但test MAE高达8.5(远超baseline的3.2)
原因:pred_len=720太大,模型过拟合训练集短期模式
解决:降低d_model到256,或增加dropout到0.1,或减少train_epochs到5

现象5:ImportError: cannot import name 'BatchNorm1d' from 'torch.nn'
原因:PyTorch版本太低(<1.8)
解决:按前述pip install torch==1.10.2重装

3.4 查看结果:results.png和OT-ForecastResults.csv的双重验证

运行结束后,你会在根目录看到:

  • results.png:主可视化图,包含真实值(蓝)、预测值(红)、趋势线(绿)
  • results/文件夹:里面是Transformer_ETTh1_S_96_48_720这样的子文件夹,含每次实验的checkpoint.pth和result_long_term_forecast.txt
  • OT-ForecastResults.csv:结构化预测结果,列名为date, true, pred, trend,可直接导入Excel分析

打开results.png,第一眼盯住X轴末尾——那里是第720步(第5天最后一刻)。如果红色预测线在此处仍紧贴蓝色真实线,说明长期泛化能力成立;如果此处发散,则需回头调d_model或dropout。OT-ForecastResults.csv则让你用Excel的CORREL()函数算true和pred的相关系数,>0.95才算过关。


4. 模块级改造指南:把ETTh1方案迁移到你的业务数据上

这个项目的价值不在跑通ETTh1,而在提供一个可手术刀式修改的骨架。我把它拆成4个可独立替换的模块,每个都附上你改时必查的3个文件、2个参数、1个验证点。

4.1 替换数据源:从ETTh1到你的CSV

模块必查文件必改参数验证点
数据路径main.py第58行root_path,data_loader.py第62行data_pathroot_path='./your_data/',data_path='your_file.csv'运行后打印df_raw.shape,确认行数列数符合预期
时间列解析timefeatures.py第42行freq,第55行minute % 15freq='15min'(或'h'),minute % your_interval(如% 60)print(time_features(df_raw['date']).shape)应为(N, 5)
目标列名main.py第72行target,data_loader.py第75行df_raw[['OT']]target='your_column_name',df_raw[['your_column_name']]df_raw['your_column_name'].dtype == np.float32

实操技巧:你的数据如果有缺失值,别在data_loader.py里插值——在main.py读取后、传入Dataset前,用df.fillna(method='ffill')一次补齐。否则SeriesDecompose的移动平均会出NaN。

4.2 调整预测长度:从720步到任意步长

pred_len不是越大越好。它和seq_len构成输入-输出比例,直接影响模型容量需求。我们用一张表说明如何平衡:

业务场景seq_lenpred_lenlabel_len理由说明
电力负荷(日级)96(24h)96(24h)48(12h)日内调度需覆盖完整周期,label_len提供强引导
工业设备(周级)168(7d)336(14d)168(7d)周期性强,用整周数据作输入,预测两周,label_len等于seq_len增强稳定性
气象预报(小时级)24(24h)168(7d)24(24h)小时级数据噪声大,缩短输入长度防过拟合,拉长pred_len靠Encoder全局建模弥补

改法:只动main.py里的pred_len和label_len,其他参数按上表比例缩放。切记:pred_len必须是数据采样间隔的整数倍,否则timefeatures.py的minute % interval会报错。

4.3 替换模型组件:从Transformer到Informer或Autoformer

项目预留了model参数开关。当前'model': 'Transformer'指向models/Transformer/Transformer.py。若想换Informer(更适合长序列),只需:

  1. 把models/Informer/文件夹(需自行实现或从GitHub克隆)放到同级目录
  2. 改main.py中'model': 'Informer'
  3. 在models/Informer/Informer.py里确保forward()接口和Transformer.py一致:接收x_enc, x_mark_enc, x_dec, x_mark_dec,返回pred

关键适配点:Informer的ProbSparse注意力需要factor参数,而Autoformer的Auto-Correlation需要moving_avg窗口大小。这些参数都在main.py的args字典里预留了位置('factor': 5,'moving_avg': 25),改模型时顺手填上即可。

4.4 定制可视化:从results.png到交互式Dashboard

main.py第156行visual()函数是可视化入口。它调用util/tools.py里的plot_prediction()。如果你想加置信区间,改tools.py第201行:

def plot_prediction(true, preds, path): plt.figure(figsize=(12, 6)) plt.plot(true, label='True', color='blue') plt.plot(preds, label='Pred', color='red') # 新增:假设你有lower/upper边界(从模型输出获得) # plt.fill_between(range(len(true)), lower, upper, alpha=0.2, color='red') plt.legend() plt.savefig(path, bbox_inches='tight') plt.close()

生产建议:别用matplotlib出图。把preds和true写入results.json,用flask搭个轻量API,前端用ECharts渲染动态折线图。这样客户能拖拽看任意时间段,比静态results.png实用十倍。


5. 二次开发实战:给Transformer加上“物理约束”,让预测不违背热力学常识

这是我在线上项目踩过的最深的坑:模型预测出“变压器温度在凌晨3点比正午还高10°C”,数学上MSE很低,物理上完全荒谬。单纯靠数据无法教会模型热传导定律,必须在损失函数里注入先验知识。下面教你三步给这个Transformer项目加上可微分的物理约束。

5.1 构造物理损失项:热平衡不等式

变压器温度变化受散热速率限制。设T[t]为t时刻温度,Δt=15min,则理论最大升温速率为k*(T_env - T[t]),其中k是散热系数,T_env是环境温度。我们把它转化为可微分的软约束:

# 在main.py的train_one_epoch()函数里,loss计算后追加 def physical_loss(pred, env_temp, k=0.01): # pred: [B, pred_len, 1], env_temp: [B, pred_len, 1](需你提供环境温度序列) dt = 15 / 60 # 小时 # 计算相邻时刻温差 delta_T = pred[:, 1:, 0] - pred[:, :-1, 0] # [B, pred_len-1] # 计算理论最大允许温差 max_delta_T = k * (env_temp[:, 1:, 0] - pred[:, :-1, 0]) * dt # 软约束:超出部分的L2惩罚 violation = torch.relu(delta_T - max_delta_T) return torch.mean(violation ** 2) # 在train_one_epoch()中 loss = criterion(outputs, true) phy_loss = physical_loss(outputs, env_temp_batch) # env_temp_batch需从data_loader提供 total_loss = loss + 0.5 * phy_loss # 权重0.5可调 total_loss.backward()

注意:env_temp_batch必须和outputs同batch同长度。你得在data_loader.py的__getitem__里,把环境温度作为额外列读进来,并在main.py的data_parser中扩展enc_in。

5.2 修改数据加载器:注入环境温度特征

data_loader.py第75行后插入:

# 假设你的CSV多一列'EnvTemp' df_raw = df_raw[['OT', 'EnvTemp']] # 读两列 # 后续标准化时,对两列分别做 self.scaler.fit(df_raw[['OT']]) self.env_scaler = StandardScaler() self.env_scaler.fit(df_raw[['EnvTemp']])

然后在__getitem__返回时,把env_temp也打包进去:

return seq_x, seq_y, seq_x_mark, seq_y_mark, env_temp_seq_y

main.py里接收时加一句:

seq_x, seq_y, seq_x_mark, seq_y_mark, env_temp = batch

5.3 验证物理约束生效:用梯度检查法

别等训练完看results.png。在train_one_epoch()里加断点,检查phy_loss是否随epoch下降:

if epoch % 10 == 0: print(f"Epoch {epoch}, PhyLoss: {phy_loss.item():.6f}") # 检查梯度是否回传到embedding层 grad_norm = torch.norm(model.enc_embedding.token_emb.weight.grad) print(f"Embedding grad norm: {grad_norm:.6f}")

如果PhyLoss从0.12降到0.003,且Embedding grad norm始终>0,说明约束已生效。此时再跑results.png,你会发现凌晨预测温度不再异常飙升,而是平缓跟随环境温度曲线——这才是工程可用的预测。

从那以后我每次做时序预测项目,都强制走一遍“物理定律→数学不等式→可微分损失”的推导。不是为了炫技,是让模型学会敬畏现实世界的规则。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询