ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Transformer多源时间序列预测:从数据对齐到PyTorch实现

Transformer多源时间序列预测:从数据对齐到PyTorch实现 简介面向熟悉深度学习及Transformer架构、从事金融预测或能源估算等时间序列分析的技术人员这份资源以电力、汇率、交通、天气四个多源数据集为完整主线细致演示了从数据加载合并、缺失值填补、异常值剔除、时间戳解析、非数值特征转换、标准化处理到训练集/验证集/测试集划分再到Transformer模型超参数调节、学习率衰减策略与回归测评指标MAE、RMSE等的完整流程。压缩包内共1个docx文档仅19KB内容紧凑但覆盖了可复现的Python代码片段、代码解释及调参思路便于直接对照并迁移至自身项目。已有130人学习下载。读者可借助其中的数据预处理方法和模型调参思路快速搭建适用于多类型时间序列的预测基线并依据实际业务灵活调整学习步长与网络结构以获得更稳健的预报效果。1. 电力、汇率、交通与天气为什么多源数据集反而需要Transformer电力负荷预测里有个很真实的痛只拿历史用电曲线做输入模型在暴雨、高温或者节假日面前几乎必然失真。一次极端天气带来的误差能把前面一个季度的精度提升全部抹平。把天气、交通、甚至汇率信号一起接入预测管道准确率会有可见的提升但代价是模型要同时处理四类来源、不同采样间隔、不同量纲的数据。传统的RNN/LSTM在这类多源混合序列上很容易顾此失彼因为它们依赖逐步传递的隐状态一旦某个源的信息出现弱相关或长周期滞后梯度信号已经被稀释掉了。Transformer的注意力机制提供了一个不同解法它不预设“过去多久”才是有效窗口而是让模型自己学习“哪个变量、在哪个时间点、对下一个预测结果最有决定作用”。这篇文章就用电力、汇率、交通、天气四个公开数据集作为载体从数据清洗、滑窗样本构造、PyTorch模型实现到参数优化走一条可以复现的完整流程。涉及代码的部分会给出可直接运行的片段和逐行解释适合有一定PyTorch基础、想把Transformer用在多源时间序列回归任务上的工程师和数据学习者。2. Transformer原理自注意力如何完成多源信号的跨变量信息交互要理解Transformer为什么适合电力、汇率、交通、天气这种多源数据集得先把注意力机制的本质拆开。传统序列模型面对一个时间步的输入需要把历史信息压缩进一个固定长度的隐状态向量再把这个隐状态传给下一步。这意味着最早的信息每经过一个时间步就要和最新信息发生一次混合计算距离稍长早期特征就被稀释。自注意力不这样做它在一个时间步内对全部位置两两计算关联度无论两个时间点之间隔了多远关联权重都只由它们的向量内积决定不存在“逐步衰减”的路径损耗。这一点对多源数据尤其重要——天气对电力负荷的影响往往滞后好几小时汇率和交通流量的相关性甚至藏在周级别的周期里注意力机制可以直接建立这种远距离跨变量连接。得到某个位置的输出表示时自注意力会计算出该位置对所有其他位置的注意力权重然后按权重加权求和所有位置的值向量。每个源的特征在同一套QKV机制里被统一映射到共同向量空间模型可以学习到“温度”与“用电负荷”之间的关联权重高于“汇率”与“用电负荷”之间的关联权重。这种机制天然具备特征选择的能力不需要人工显式指定。另外一个容易被忽视的优势是并行性。LSTM必须按时间步串行展开Transformer的输入是一个序列整体可以一次性喂入所有时间步的矩阵在GPU上并行计算整段注意力矩阵这对工业级多源数据集的迭代效率非常关键。从网络结构上看使用Transformer做多源时间序列回归通常只需要其编码器部分而不是完整的编码器-解码器架构。解码器是为序列生成任务设计的自回归地逐token输出而我们的需求是把一段历史窗口映射成一个或多个未来数值属于序列到序列的回归任务只用编码器提取特征、再接一个线性层输出预测结果就能获得不错的效果并且训练速度更快。输入的每个时间步样本会被组织成形状为[batch_size, seq_len, input_dim]的张量其中input_dim是所有数据源的特征总数。比如同时使用电力负荷、汇率、交通流量、温度四个变量input_dim就是 4如果第二个时间步还包含小时、星期几、是否节假日等派生特征那input_dim就会变成 7 或更多。位置编码的加入也非常重要没有它注意力机制会把所有时间步看作同时发生的事件完全丧失顺序概念。常见做法是用正余弦函数生成位置编码向量加到输入嵌入上让模型同时感知特征内容和时间顺序。3. 多源数据集预处理电力、汇率、交通、天气的对齐与滑窗样本构建四类数据在实际项目中往往来自完全不同的系统电力数据是15分钟或1小时的计量值汇率是每日收盘价交通是断面流量天气则可能有逐小时、逐三小时的区别。直接拼接会因为采样频率不对齐而产生大量空值。常见做法是先指定统一的时间频率再用前向填充和线性插值补齐缺失位置。统一到小时粒度比较合适既保留了电力负荷和天气的日内波动特性又不会因为颗粒过细而让汇率数据无意义放大。import pandas as pd import numpy as np # 读取四个数据源date列统一为datetime类型 df_power pd.read_csv(power.csv, parse_dates[date]) df_exchange pd.read_csv(exchange.csv, parse_dates[date]) df_traffic pd.read_csv(traffic.csv, parse_dates[date]) df_weather pd.read_csv(weather.csv, parse_dates[date]) # 统一以小时为频率重建时间索引 all_dates pd.date_range(start2020-01-01, end2023-12-31, freqh) df_power.set_index(date, inplaceTrue) df_exchange.set_index(date, inplaceTrue) df_traffic.set_index(date, inplaceTrue) df_weather.set_index(date, inplaceTrue) # 用reindex对齐到统一时间轴缺失值先用前向填充再用线性插值 df_power df_power[load_kw].reindex(all_dates).ffill() df_exchange df_exchange[usd_cny].reindex(all_dates).ffill().interpolate() df_traffic df_traffic[vehicles_per_hour].reindex(all_dates).ffill() df_weather df_weather[temp_c].reindex(all_dates).interpolate()这段代码先把四类数据的时间索引统一到一个由pd.date_range生成的完整小时序列上。reindex会为缺失的时点补入NaN然后分别处理电力负荷和交通流量用ffill()前向填充因为这类连续性指标相邻时点的值非常接近汇率和气温用interpolate()线性插值能保留两个有效点之间的趋势。之所以给汇率和气候用插值而不是前向填充是因为它们天然存在中间趋势前向填充会让模型在输入里学到不必要的阶跃跳变干扰注意力机制对连续变化特征的判断。数据对齐完成后下一步是做归一化。Transformer对输入量纲非常敏感电力负荷的数值范围可能是几千汇率在 7 附近波动交通流量可能上万如果直接用原始数值注意力分数的计算会被量纲更大的特征主导模型基本学不到小量纲变量的有效信息。常见做法是对每个变量单独做零均值标准化from sklearn.preprocessing import StandardScaler # 将四个源的特征合并到一个DataFrame data pd.DataFrame({ load_kw: df_power, usd_cny: df_exchange, traffic: df_traffic, temp_c: df_weather }) scaler StandardScaler() scaled_data scaler.fit_transform(data) scaled_df pd.DataFrame(scaled_data, columnsdata.columns, indexdata.index)StandardScaler计算每个变量的均值和标准差让每个特征都落在零均值、单位方差的分布上。保存这个scaler对象非常重要推理阶段要对新数据做完全相同的变换。在Transformer类模型中尽量不使用MinMax归一化到[0, 1]因为它会压缩极端天气或异常负荷的值域而这些极端值恰恰是预测任务中最需要关注的样本点。归一化之后就是滑窗样本构建。预测任务的定义是给定过去seq_len个时间步的全部特征预测未来pred_len个时间步的电力负荷。每一行训练样本包含输入矩阵X和标签向量y。def create_sequences(data, seq_len96, pred_len24): data: 标准化之后的完整特征矩阵 seq_len: 输入窗口长度96小时即过去4天 pred_len: 预测长度24小时即未来1天 X, y [], [] for i in range(len(data) - seq_len - pred_len 1): X.append(data.iloc[i : i seq_len].values) y.append(data.iloc[i seq_len : i seq_len pred_len, 0].values) return np.array(X), np.array(y) X, y create_sequences(scaled_df) print(X.shape, y.shape) # (样本数, 96, 4) (样本数, 24)seq_len取 96 是因为电力负荷有明显的日周期和天气周期输入要覆盖至少四个完整日期模式pred_len取 24 对应未来一天的负荷曲线。y只取第一列也就是电力负荷因为在这类模型中通常只把电力作为核心预测目标其余变量被当作辅助解释特征。在构建样本时还要注意按时间顺序划分训练集、验证集、测试集不能随机打乱。时间序列的数据一旦随机打乱评估结果会因为泄漏未来信息而虚高。常见比例是前 70% 训练、中间 15% 验证、最后 15% 测试。训练集和验证集都从create_sequences构造的结果中按索引切分。提示seq_len和pred_len是模型效果最敏感的两个参数。做短期预测时seq_len可以压缩到 48做中长期预测时增加到 168一周通常有明显收益。值的选取要贴合数据本身的周期特征。4. 构建PyTorch多源Transformer模型编码器加回归头的实现完成数据对齐和样本构造后下一步是搭建模型。针对多源时间序列回归场景网络结构包含四个模块输入映射层把每个时间步的多源特征映射到d_model维嵌入向量位置编码为序列引入时间顺序标准Transformer编码器负责跨时间步和跨变量的特征提取最后是回归头把编码器输出映射成pred_len个预测值。import torch import torch.nn as nn import math class MultiSourceTransformer(nn.Module): def __init__(self, input_dim, d_model128, nhead8, num_layers3, pred_len24, dropout0.1): super().__init__() self.input_proj nn.Linear(input_dim, d_model) self.pos_encoder PositionalEncoding(d_model, dropout) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforward512, dropoutdropout, batch_firstTrue ) self.encoder nn.TransformerEncoder(encoder_layer, num_layersnum_layers) self.reg_head nn.Sequential( nn.Linear(d_model, 128), nn.GELU(), nn.Dropout(dropout), nn.Linear(128, pred_len) ) def forward(self, x): # x: [batch_size, seq_len, input_dim] x self.input_proj(x) # 映射到d_model维度 x self.pos_encoder(x) # 叠加位置编码 x self.encoder(x) # 自注意力编码 x x.mean(dim1) # 对seq_len维度做池化 return self.reg_head(x) # 输出预测序列 class PositionalEncoding(nn.Module): def __init__(self, d_model, dropout0.1, max_len1000): super().__init__() self.dropout nn.Dropout(dropout) pe torch.zeros(max_len, d_model) position torch.arange(0, max_len, dtypetorch.float).unsqueeze(1) div_term torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] torch.sin(position * div_term) pe[:, 1::2] torch.cos(position * div_term) pe pe.unsqueeze(0) self.register_buffer(pe, pe) def forward(self, x): x x self.pe[:, :x.size(1)] return self.dropout(x)input_proj是一个线性层输入维度是特征数输出是d_model。nn.TransformerEncoderLayer内部已经封装了多头自注意力、前馈网络、层归一化和残差连接不需要自己实现注意力公式。nhead设为 8 表示把d_model128分成 8 个 16 维的注意力头每个头独立关注不同子空间的特征模式。编码器输出形状是[batch_size, seq_len, d_model]在seq_len维度上取平均作为整个序列的汇总表示这样无论输入窗口多长回归头接收的向量维度是固定的。reg_head用两层全连接输出pred_len个值构成未来一天24小时的预测曲线。训练循环本身并不比普通PyTorch模型复杂但有几个细节需要说明。Transformer对学习率极其敏感直接用固定的高学习率经常导致训练初期loss发散。常见做法是使用AdamW优化器配合warmup学习率策略先让学习率线性上升几百步再按余弦退火衰减。nn.MSELoss是回归任务最基础的损失函数但对存在离群点的负荷数据部分异常高负荷会主导梯度建议在训练后期切换到HuberLoss减少极端值影响。from torch.utils.data import TensorDataset, DataLoader X_tensor torch.tensor(X, dtypetorch.float32) y_tensor torch.tensor(y, dtypetorch.float32) dataset TensorDataset(X_tensor, y_tensor) dataloader DataLoader(dataset, batch_size64, shuffleTrue) device torch.device(cuda if torch.cuda.is_available() else cpu) model MultiSourceTransformer(input_dim4, d_model128, nhead8, num_layers3, pred_len24) model.to(device) optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-5) criterion nn.MSELoss() for epoch in range(50): model.train() total_loss 0.0 for batch_X, batch_y in dataloader: batch_X, batch_y batch_X.to(device), batch_y.to(device) optimizer.zero_grad() pred model(batch_X) loss criterion(pred, batch_y) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() print(fEpoch {epoch1}, Loss: {total_loss / len(dataloader):.6f})shuffleTrue在这里是可接受的因为滑窗样本的标签只来自未来的数据训练样本内部不会发生跨样本泄漏真正不能打乱的是训练集和测试集的划分。clip_grad_norm_设置了梯度裁剪把梯度的L2范数限制在 1.0 以下防止自注意力层中出现梯度爆炸这一项在多源数据上特别值得保留因为不同特征量纲差异在训练早期会引发较大的梯度波动。5. 模型优化实践损失函数、学习率策略与超参数调整用Transformer处理电力、汇率、交通、天气这类多源数据时模型架构带来的收益往往只占一半另一半来自优化策略的取舍。很多人直接沿用自然语言处理场景的参数配置比如 512 的d_model、6 层编码器放到时间序列上多半会得到严重过拟合的结果。因为表格型时序数据的信息密度远低于文本参数量不需要那么大。常见做法是d_model取 64 到 128编码器层数取 2 到 4 层在大部分场景就足够表达跨变量依赖。层数增加带来的收益在数据量小于十万条样本时几乎可以忽略只会拖慢训练速度并增加过拟合风险。学习率调度是Transformer类模型训练成败的分水岭。注意力机制在初始化阶段对输入尺度非常敏感如果一开始就用较大的学习率Q、K、V矩阵的更新幅度过大会导致注意力分布迅速坍缩到少数几个位置之后很难纠正。warmup阶段让学习率从接近 0 逐步爬升到预设峰值模型在前几百步内学会稳定的注意力模式再进入余弦退火阶段逐步收敛。峰值学习率一般取1e-4到3e-4AdamW优化器的weight_decay取1e-5量级即可过大的权重衰减会让模型欠拟合。损失函数的选择上MSE是回归任务的默认起点它让模型关注大误差样本这是电力负荷预测中极端天气场景需要的特性。但MSE的梯度在误差大于 1 时会显著放大碰到个别异常负荷点损失值会瞬间跳到正常样本的几十倍。HuberLoss在误差绝对值小于delta时表现为二次函数大于delta时表现为线性函数既保留了MSE在大误差上的敏感性又不会让个别离群点主导整个训练方向。delta取 1.0 或 2.0 在标准化后的数据上效果适中。下面是一个可以直接记录到训练日志里的超参数速查表参数推荐值范围调整方向seq_len48~168数据周期越长取越大pred_len1~72预测时长越远取越大d_model32~128特征越多取越大nhead4~8需要能被 d_model 整除num_layers2~4样本量超过十万可增加dropout0.05~0.2数据越少取值越大batch_size32~128显存充足取大值峰值学习率1e-4~3e-4和batch_size对应调整warmup_steps500~2000数据量越大取越大nhead调整时不需要显式计算修改后只需确认d_model % nhead 0即可。dropout在时间序列任务里常常被低估多源数据因为特征间存在相关性过拟合可能性比单源数据更高在回归头和编码器输出层各加一层 0.1 的dropout是性价比最高的正则化手段。早停策略也比单纯定义epoch数更可靠。验证集loss在多轮训练后没有继续下降时就说明模型已经过拟合了继续训练只会记住训练集上的噪声模式。另一个值得观察的指标是验证集loss的波动幅度如果曲线在几个epoch内反复上下跳动而不平滑下降通常意味着学习率峰值设置过高或batch_size太小导致梯度估计不稳定。6. 模型验证与一个有用的进阶技巧从回归指标到注意力解释训练完成后验证模型的预测能力不能只看训练集loss。用一个标准化的评估流程才能判断模型是否真正学到了有用的跨变量依赖。下面这段代码加载测试集输出预测值并计算三个回归指标from sklearn.metrics import mean_absolute_error, mean_squared_error model.eval() all_preds, all_truths [], [] with torch.no_grad(): for batch_X, batch_y in test_loader: batch_X batch_X.to(device) pred model(batch_X).cpu().numpy() all_preds.append(pred) all_truths.append(batch_y.numpy()) y_pred np.concatenate(all_preds, axis0) y_true np.concatenate(all_truths, axis0) # 取第一列全部预测目标做指标计算 mae mean_absolute_error(y_true, y_pred) mse mean_squared_error(y_true, y_pred) rmse np.sqrt(mse) # MAPE需要把标准化后的数值还原到原始量纲 y_pred_inv scaler.inverse_transform(np.column_stack([y_pred[:, 0], np.zeros((len(y_pred), 3))]))[:, 0] y_true_inv scaler.inverse_transform(np.column_stack([y_true[:, 0], np.zeros((len(y_true), 3))]))[:, 0] mape np.mean(np.abs((y_true_inv - y_pred_inv) / y_true_inv)) * 100 print(fMAE: {mae:.4f}, RMSE: {rmse:.4f}, MAPE: {mape:.2f}%)inverse_transform时要把列数补回原始特征数否则StandardScaler会报维度不匹配。MAPE在电力负荷场景中有个明显陷阱真实负荷接近 0 的深夜时点分母极小MAPE会被极小值拉到一个很大且不稳定的数值。如果数据包含夜间低谷时段建议过滤掉真实值低于某个阈值的样本后再计算MAPE或改用基于中位数的绝对百分比误差。一个实用的进阶技巧是直接可视化编码器最后一层某个注意力头的权重判断模型是否真的建立起了天气与电力之间的跨变量关联。# 通过注册forward hook提取编码器第一层的注意力权重 attention_maps {} def get_attention(name): def hook(module, input, output): attention_maps[name] output return hook model.encoder.layers[0].self_attn.register_forward_hook(get_attention(layer0)) # 取一个测试样本 sample X_tensor[:1].to(device) model(sample) attn attention_maps[layer0][0] # [batch, heads, seq_len, seq_len] avg_attn attn[0].mean(dim0).cpu().numpy() # 观察某个时间步比如第72小时对其他时间步的平均注意力权重 import matplotlib.pyplot as plt plt.plot(avg_attn[72])注意nn.MultiheadAttention的hook输出格式是(attn_output, attn_output_weights)元组如环境版本不同可能需要单独处理output[1]来取权重。如果注意力分布集中在几个固定的滞后时间点并且这些时间点与天气变量输入时刻吻合说明模型确实在学习结构化依赖。若分布接近均匀说明模型退化成简单的历史均值回归需要增大d_model或调整seq_len重新训练。把这一小段可视化代码嵌入现有脚本每当模型效果不达标时先看注意力分布再决定调整方向比盲调超参数有效得多。实际项目中还可以用同样的思路把测试集不同季节的误差单独拆开定位出模型在哪类天气工况下失效再决定要不要补充对应时段的历史样本。本文还有配套的精品资源点击获取
返回列表