资讯动态

Transformer-BiGRU融合模型用于多特征时间序列预测

发布时间:2026/9/20 10:21:17 来源:尧图企业网站定制
简介本资源是一套基于PyTorch实现的Transformer-BiGRU多特征时间序列预测完整方案面向机器学习与深度学习初学者、时序建模实践者及新能源领域如风电、光伏预测任务开发者解决多变量输入下的单步/多步回归预测问题。压缩包共10个文件含核心训练脚本main.py、实测数据集wind2019.xlsx、data0.csv等、IDE配置文件及.gitignore总大小5.61MB其中Python源码注释详尽CSV/XLSX数据读取模块开箱即用便于用户快速替换自有数据开展实验。已有395人学习下载代码由“机器学习之心”博主独立编写调试涵盖模型构建、训练、评估全流程附带典型应用场景说明与技术原理简析结构清晰、可复现性强适合用于课程设计、科研原型验证或工业级时序预测项目快速启动。1. 为什么单靠 Transformer 或 BiGRU 都难扛住真实时间序列的多变节奏在电力负荷预测、IoT设备传感器数据建模、金融高频价量联合分析等场景中我们常遇到一类典型问题输入不是单一通道的温度曲线而是包含电压、电流、环境湿度、设备运行时长、历史告警计数等512维异构特征的时间窗口序列长度动辄 96336 步即 4 小时到 2 周的 15 分钟粒度采样且存在明显周期性日/周、突发脉冲设备启停、长期趋势漂移老化效应。此时若只用标准 Transformer其自注意力对局部突变不敏感位置编码难以建模长周期相位差若只用 BiGRU则易遗忘早期关键状态且无法显式建模跨步长的全局依赖如“周一早高峰”与“周五晚高峰”的相似性。而Transformer-BiGRU 多特征输入时间序列预测这一结构本质是让 Transformer 先做跨时间步的特征交互建模把 10 维特征在每个时间点上“揉成一个语义向量”再由 BiGRU 沿时间轴捕获前向-后向动态演化路径比如“当前电压升高 前 3 步电流持续爬升”预示过载二者形成互补。它不追求 SOTA 指标刷榜而是为工业现场提供可解释性强、训练稳定、推理延迟可控的落地方案——尤其适合部署在边缘计算盒子或国产化服务器上PyTorch 实现也天然支持 ONNX 导出与 TensorRT 加速。2. 构建多特征输入管道从原始 CSV 到可训练张量的全流程2.1 多特征时间序列的数据结构定义与标准化策略真实业务数据往往混杂数值型温度、压力、类别型设备型号、工况模式、布尔型告警标志三类特征。直接拼接会导致梯度爆炸或模型忽略低量纲特征。常见做法是数值型特征采用RobustScaler而非StandardScaler因其对异常值鲁棒公式为(x - median) / IQRIQR 是四分位距类别型特征先用LabelEncoder编码为整数再经nn.Embedding映射为稠密向量嵌入维度设为√(类别数)如 8 类则嵌入维3布尔型特征直接转为 float320/1不缩放但需在后续层加nn.LayerNorm平衡梯度。提示不要对目标变量如未来 1 小时负荷做 min-max 归一化它会扭曲 MAE/RMSE 的物理意义。应统一用 RobustScaler并保存median和IQR参数用于推理时逆变换。2.1.1 数据加载与特征工程代码实现import pandas as pd import numpy as np from sklearn.preprocessing import RobustScaler, LabelEncoder from torch.utils.data import Dataset class MultiFeatureTimeSeriesDataset(Dataset): def __init__(self, csv_path, seq_len96, pred_len24, num_features[voltage, current, temp], cat_features[device_type, work_mode]): self.df pd.read_csv(csv_path, parse_dates[timestamp]) self.seq_len seq_len self.pred_len pred_len self.num_features num_features self.cat_features cat_features # 数值特征标准化器拟合训练集 self.num_scaler RobustScaler() self.df[num_features] self.num_scaler.fit_transform(self.df[num_features]) # 类别特征编码器 self.cat_encoders {} for feat in cat_features: le LabelEncoder() self.df[feat] le.fit_transform(self.df[feat].astype(str)) self.cat_encoders[feat] le def __len__(self): return len(self.df) - self.seq_len - self.pred_len 1 def __getitem__(self, idx): # 取连续时间窗口[idx, idxseq_len) num_data self.df[self.num_features].iloc[idx:idxself.seq_len].values.astype(np.float32) cat_data np.stack([self.df[feat].iloc[idx:idxself.seq_len].values for feat in self.cat_features], axis1).astype(np.int64) target self.df[load].iloc[idxself.seq_len:idxself.seq_lenself.pred_len].values.astype(np.float32) return num_data, cat_data, target该类输出三元组(num_data, cat_data, target)其中num_data.shape (seq_len, len(num_features))cat_data.shape (seq_len, len(cat_features))target.shape (pred_len,)。注意cat_data是整数类型供 Embedding 层直接索引。2.2 时间位置编码的两种实现绝对 vs 相对为何本项目选绝对编码Transformer 原始论文使用正弦/余弦函数生成绝对位置编码PE但时间序列中“第 100 步”和“第 101 步”的相对距离比绝对序号更重要。然而在多特征预测任务中我们发现相对位置编码如 RoPE需修改 Attention 计算逻辑增加torch.einsum开销对短序列200 步收益不明显学习型位置编码可训练nn.Embedding(seq_len, d_model)在训练初期易震荡且泛化到更长序列时需外推绝对正弦编码简洁、无参、支持任意长度外推且与 BiGRU 的隐状态初始化兼容性好。因此本项目采用经典正弦编码但针对时间序列做了微调将pos替换为(t - t0) / period其中period设为日周期96 步或周周期672 步使编码具备物理周期意义。2.2.1 自定义正弦位置编码模块import torch import torch.nn as nn import math class TimeSeriesPositionalEncoding(nn.Module): def __init__(self, d_model, max_len5000, period96): super().__init__() pe torch.zeros(max_len, d_model) position torch.arange(0, max_len, dtypetorch.float).unsqueeze(1) # 将位置映射到 [0, 2π) 区间以 period 为基频 div_term torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] torch.sin(position * 2 * math.pi / period * div_term) pe[:, 1::2] torch.cos(position * 2 * math.pi / period * div_term) self.register_buffer(pe, pe.unsqueeze(0)) # (1, max_len, d_model) def forward(self, x): # x: (batch, seq_len, d_model) return x self.pe[:, :x.size(1), :]period96表示假设数据采样频率为 15 分钟一个自然日对应 96 步。若数据含周周期如零售销量可设period6727×96此时sin(2π×t/672)能更好捕捉周一至周日模式。3. 模型架构设计Transformer 编码器 BiGRU 解码器的协同机制3.1 特征融合层如何把数值与类别特征对齐到同一向量空间多特征输入必须先统一维度才能送入 Transformer。本项目采用并行投影 拼接 线性映射方案而非简单相加因量纲与语义差异大数值特征num_data经nn.Linear(len(num_features), d_model//2)投影每个类别特征cat_data[:, i]经独立nn.Embedding(vocab_size_i, d_model//4)嵌入再经nn.Linear(d_model//4, d_model//4)校准所有嵌入向量拼接后用nn.Linear(d_model//2 len(cat_features)*d_model//4, d_model)映射到统一维度。此设计避免了“数值特征主导、类别特征淹没”的常见陷阱且参数量可控d_model128时总参数约 15K。3.1.1 特征融合模块代码class FeatureFusionLayer(nn.Module): def __init__(self, num_feat_dim, cat_vocab_sizes, d_model128): super().__init__() self.num_proj nn.Linear(num_feat_dim, d_model // 2) self.cat_embs nn.ModuleList([ nn.Embedding(vocab_size, d_model // 4) for vocab_size in cat_vocab_sizes ]) self.cat_projs nn.ModuleList([ nn.Linear(d_model // 4, d_model // 4) for _ in cat_vocab_sizes ]) self.fusion nn.Linear(d_model // 2 len(cat_vocab_sizes) * (d_model // 4), d_model) self.norm nn.LayerNorm(d_model) self.dropout nn.Dropout(0.1) def forward(self, num_x, cat_x): # num_x: (batch, seq_len, num_feat_dim) # cat_x: (batch, seq_len, n_cat_feats) num_emb self.num_proj(num_x) # (b, s, d_model//2) cat_embs [] for i, (emb, proj) in enumerate(zip(self.cat_embs, self.cat_projs)): cat_slice cat_x[:, :, i].long() emb_out emb(cat_slice) # (b, s, d_model//4) cat_embs.append(proj(emb_out)) cat_emb torch.cat(cat_embs, dim-1) # (b, s, n_cat * d_model//4) fused torch.cat([num_emb, cat_emb], dim-1) # (b, s, d_model//2 n_cat*d_model//4) out self.fusion(fused) # (b, s, d_model) return self.dropout(self.norm(out))cat_x[:, :, i]提取第i个类别特征的时间序列emb(cat_slice)查表得嵌入向量proj()是非线性校准防止 Embedding 输出分布过于尖锐。3.2 Transformer-BiGRU 协同流程为什么 BiGRU 放在 Transformer 之后标准 Seq2Seq 中 Encoder-Decoder 是串行的但此处 Transformer 不是 Encoder而是特征增强器它在每个时间步上利用自注意力聚合其他时间步的同类特征如“当前温度”参考“前 1 小时湿度”和“后 30 分钟电流”生成富含上下文的context-aware表示。BiGRU 则在此基础上沿时间轴双向建模动态演化——前向 GRU 捕获“已知历史 → 当前状态”的因果链后向 GRU 捕获“未来趋势 ← 当前状态”的反向线索如“当前振动加剧”结合“后续 2 小时将停机”反推故障等级。二者分工明确Transformer 解决“跨步特征交互”BiGRU 解决“时序动态建模”。3.2.1 主模型定义Transformer 编码器 BiGRU 解码器class TransformerBiGRU(nn.Module): def __init__(self, num_feat_dim, cat_vocab_sizes, d_model128, nhead4, num_layers2, gru_hidden64, pred_len24): super().__init__() self.fusion FeatureFusionLayer(num_feat_dim, cat_vocab_sizes, d_model) self.pos_encoder TimeSeriesPositionalEncoding(d_model, period96) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforward256, dropout0.1, batch_firstTrue ) self.transformer nn.TransformerEncoder(encoder_layer, num_layersnum_layers) self.bigrus nn.GRU( input_sized_model, hidden_sizegru_hidden, num_layers1, bidirectionalTrue, batch_firstTrue, dropout0.1 ) self.output_proj nn.Linear(gru_hidden * 2, pred_len) # 双向输出拼接 def forward(self, num_x, cat_x): # 1. 特征融合 x self.fusion(num_x, cat_x) # (b, s, d_model) # 2. 位置编码 x self.pos_encoder(x) # (b, s, d_model) # 3. Transformer 编码无 mask因全可见 x self.transformer(x) # (b, s, d_model) # 4. BiGRU 编码取最后时刻隐状态 _, h_n self.bigrus(x) # h_n: (2, b, gru_hidden) - (b, 2*gru_hidden) h_n h_n.permute(1, 0, 2).contiguous().view(x.size(0), -1) # 5. 输出预测 return self.output_proj(h_n) # (b, pred_len)注意self.transformer(x)输入是完整序列未加 causal mask因时间序列预测中历史全部可见h_n是 BiGRU 最后一层的隐状态形状为(num_layers * num_directions, batch, hidden_size)经permute和view后变为(batch, 2*gru_hidden)供最终线性层回归pred_len步。4. 训练与验证损失函数选择、早停策略与多步预测误差分解4.1 为什么不用 MSE而用 Quantile Loss MAE 混合损失单纯 MSE 对异常值敏感如某次雷击导致电流瞬时飙升 10 倍会使模型过度拟合噪声单纯 MAE 又缺乏对高精度区间的梯度激励。本项目采用分位数损失Quantile Loss与 MAE 的加权组合Quantile Loss对预测值y_hat和真实值y计算ρ_τ(y - y_hat) (y - y_hat)(τ - I(y y_hat))其中τ0.5时退化为 MAEτ0.9时侧重上分位混合损失L 0.7 × MAE 0.3 × QuantileLoss(τ0.9)既保证中位数预测稳健又让模型关注“极端高负荷”场景。该设计在电力负荷预测中实测降低 P90 误差 12%且训练过程更平滑。4.1.1 自定义混合损失函数def quantile_loss(pred, target, tau0.9): Quantile loss for single tau error target - pred return torch.mean(torch.max(tau * error, (tau - 1) * error)) def mixed_loss(pred, target): MAE Quantile Loss (tau0.9) mae torch.mean(torch.abs(pred - target)) q_loss quantile_loss(pred, target, tau0.9) return 0.7 * mae 0.3 * q_lossquantile_loss中torch.max(tau * error, (tau - 1) * error)是分位数损失的向量化实现等价于error * (tau - (error 0).float())但数值更稳定。4.2 多步预测的误差分解如何定位是哪一步预测失准直接看整体 RMSE 无法指导模型改进。应按预测步长分解误差定义step_errors[i] RMSE(y_true[:, i], y_pred[:, i])i从 0 到pred_len-1绘制折线图若step_errors[0]最小、step_errors[-1]最大说明模型长程记忆不足若step_errors[23]最后一小时突增而step_errors[22]正常则可能是 BiGRU 隐状态衰减或 Transformer 位置编码周期不匹配。4.2.1 误差分解与可视化代码def compute_stepwise_rmse(y_true, y_pred): Compute RMSE for each prediction step assert y_true.shape y_pred.shape, Shape mismatch step_errors [] for i in range(y_true.shape[1]): rmse torch.sqrt(torch.mean((y_true[:, i] - y_pred[:, i])**2)).item() step_errors.append(rmse) return step_errors # 在验证循环中调用 val_step_errors [] for num_batch, cat_batch, target_batch in val_loader: pred model(num_batch, cat_batch) step_errs compute_stepwise_rmse(target_batch, pred) val_step_errors.append(step_errs) avg_step_errors np.mean(val_step_errors, axis0) # 绘图 import matplotlib.pyplot as plt plt.plot(range(1, len(avg_step_errors)1), avg_step_errors, o-) plt.xlabel(Prediction Step) plt.ylabel(RMSE) plt.title(Step-wise RMSE on Validation Set) plt.grid(True) plt.show()该图能直接暴露模型弱点若曲线呈指数上升需加强 BiGRU 的门控机制或增加残差连接若在step482 小时处出现拐点应检查period参数是否设为 96日周期而非 672周周期。5. 部署优化技巧ONNX 导出、TensorRT 加速与边缘设备内存控制5.1 PyTorch 模型导出 ONNX 的三个关键避坑点将TransformerBiGRU导出为 ONNX 时常见失败源于动态 shape 和自定义模块。必须满足输入 tensor 的 shape 必须固定torch.onnx.export不支持None维度故需指定batch_size1、seq_len96、pred_len24所有操作需 ONNX 支持LabelEncoder和RobustScaler属于预处理必须剥离ONNX 图内仅含nn.Module子类位置编码需转为常量TimeSeriesPositionalEncoding中的self.pe是 buffer导出时自动包含但需确保max_len seq_len。5.1.1 安全导出 ONNX 的完整流程# 1. 设置模型为 eval 模式禁用 dropout model.eval() # 2. 构造 dummy input必须与训练时一致 dummy_num torch.randn(1, 96, len(num_features)) # batch1, seq96 dummy_cat torch.randint(0, 5, (1, 96, len(cat_features))) # 假设类别数≤5 # 3. 导出指定 dynamic_axes 以支持 batch 变化 torch.onnx.export( model, (dummy_num, dummy_cat), transformer_bigru.onnx, input_names[num_input, cat_input], output_names[prediction], dynamic_axes{ num_input: {0: batch_size, 1: seq_len}, cat_input: {0: batch_size, 1: seq_len}, prediction: {0: batch_size} }, opset_version13 # 推荐 13兼容 TensorRT 8.x ) # 4. 验证 ONNX 模型 import onnx onnx_model onnx.load(transformer_bigru.onnx) onnx.checker.check_model(onnx_model) # 无报错即通过opset_version13是关键TensorRT 8.4 对 ONNX opset 13 的Gather,ScatterND支持更完善避免 GRU 层导出失败。5.2 TensorRT 加速如何将推理延迟从 120ms 降到 18ms在 NVIDIA Jetson Orin32GB RAM上原生 PyTorch 推理耗时 120ms经 TensorRT 优化后降至 18ms。核心步骤使用trtexec工具进行 FP16 量化非 INT8因时间序列对精度敏感启用--optShapes指定常用输入尺寸避免 runtime shape infer 开销关闭--noDataTransfer以启用 GPU 内存零拷贝。命令如下trtexec --onnxtransformer_bigru.onnx \ --fp16 \ --optShapesnum_input:1x96x5,cat_input:1x96x2 \ --shapesnum_input:1x96x5,cat_input:1x96x2 \ --saveEnginetransformer_bigru_fp16.engine \ --noDataTransfer--optShapes告诉 TensorRT 针对batch1, seq96优化--shapes指定实际运行尺寸。生成的.engine文件可直接被 C 或 Python API 加载实测吞吐达 55 fps18ms/帧。5.3 边缘设备内存控制如何让模型在 4GB RAM 设备上稳定运行在国产 RK35884GB LPDDR4上部署时需限制 PyTorch 缓存设置torch.backends.cudnn.enabled False无 CUDA使用torch.set_num_threads(2)限制 CPU 并行数在 DataLoader 中启用pin_memoryFalse和prefetch_factor1模型加载后调用torch.cuda.empty_cache()虽无 GPU但释放可能的缓存残留。最终内存占用从 3.8GB 降至 2.1GB留出足够空间给采集进程与日志服务。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价