ARTICLE DETAIL

资讯详情

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

融合LSTM与Transformer的多特征时间序列预测实践

融合LSTM与Transformer的多特征时间序列预测实践 简介LSTM与Transformer融合的多特征时间序列预测模型以PyTorch实现并提供完整源码与数据集面向机器学习学习者和算法工程师适用于风力发电功率、光伏发电量、设备剩余寿命、环境浓度跟踪等单变量回归预测场景。压缩包内共160个文件整体约1.95MB含31个Python源文件、87个pyc字节码、4个CSV示例数据文件以及XML配置、zbak备份等辅助内容目录结构清晰便于定位主程序、数据与配置文件。已有72人学习下载适合需替换自有数据开展实验或作为基线模型进行二次开发的读者。整个代码由具备多年机器学习算法开发经验的人员编写关键逻辑配有注释模型结合LSTM对局部特征的提取与Transformer对全局依赖的建模在多维特征输入下表现出较好的预测精度与稳定性数据集和示例输出文件一并附上可支持论文复现与课设改造。1. 多特征时间序列预测为什么纯LSTM不够融合模型才是长序列的解法做时间序列预测的人大概率经历过这个阶段先用LSTM效果不错序列变长、特征变多之后LSTM的预测精度开始下滑尤其是预测曲线后段明显滞后于真实值。这时候换Transformer发现它对数值型多特征时序的局部模式捕捉又不够细收敛还慢。基于PyTorch的LSTM与Transformer融合模型就是为了同时拿到LSTM对时序局部依赖的敏感度和Transformer对长程依赖的建模能力专攻多特征时间序列预测这类场景。这份资源附了完整源码和一份可直接用的CSV数据集训练、验证、预测全流程能跑通。适合两类人一是时序预测项目的开发者想找一个比单模型更稳的基线二是正在学PyTorch的从业者想读一份结构清晰、能改参数就能用的融合模型代码。2. 数据预处理与数据集构建滑窗参数、归一化陷阱与张量形状2.1 多特征时序的数据形态与滑窗构造多特征时间序列预测输入不再是单列数值而是一个三维张量(batch_size, seq_len, feature_num)。seq_len是回看窗口长度feature_num是每个时间步的特征数。模型的任务是给定过去seq_len个时间步的全部特征预测未来一个或多个时间步的目标值。资源附带的数据集是CSV格式每一行是一个时间步列包括时间戳、多个特征列和一个目标列。读取之后第一步不是直接进模型而是用滑窗把它切成样本。import numpy as np import pandas as pd def create_sequences(data, feature_cols, target_col, seq_len24, pred_len1, stride1): X, y [], [] for i in range(0, len(data) - seq_len - pred_len 1, stride): X.append(data[feature_cols].iloc[i:iseq_len].values) y.append(data[target_col].iloc[iseq_len:iseq_lenpred_len].values) return np.array(X), np.array(y)这段代码做的事情是从第i行开始取seq_len行所有特征列作为输入X再取紧接着的pred_len行目标列作为输出y。stride控制窗口滑动的步长默认为1表示每个时间步都生成一个样本。seq_len取值很关键如果数据是小时级且带有明显的日周期seq_len24是起步值如果数据带周周期seq_len16824×7通常更合理。pred_len先设为1把单步预测跑通再改多步。2.2 归一化必须只在训练集上拟合多特征时序里各个特征的量纲差异很大比如温度在0到40之间风速在0到15之间设备负荷可能在几百到几千之间。不归一化直接进模型LSTM的遗忘门和输入门会被大数值特征主导Transformer的self-attention算出来的注意力权重也会失真。归一化方式推荐StandardScaler或MinMaxScaler这里建议用StandardScaler因为多特征数据里常出现离群点MinMaxScaler会被极端值压缩正常值的分布区间。from sklearn.preprocessing import StandardScaler # 按时间顺序切分前70%训练后30%验证 split_idx int(len(df) * 0.7) train_df df.iloc[:split_idx] val_df df.iloc[split_idx:] # 关键一步scaler只在训练集上fit scaler StandardScaler() train_scaled scaler.fit_transform(train_df[feature_cols]) val_scaled scaler.transform(val_df[feature_cols])这里最容易被忽视的是顺序。scaler.fit_transform(train_df)拿到的是训练集的均值和标准差再用这个均值标准差去transform(val_df)。如果对整个数据集先fit再切分测试集中的统计信息会泄漏到训练过程中验证指标会虚高上线后真实效果直接打对折。这是时序任务里最常见的翻车点之一。切分数据时只能按时间顺序切不能像分类任务那样随机打乱因为时序样本之间存在时间依赖打乱后训练集里会出现未来数据模型等于开了天眼。2.3 构造PyTorch Dataset与DataLoader数据准备好之后需要封装成Dataset。这里要注意X和y的形状X是(num_samples, seq_len, feature_num)y是(num_samples, pred_len)。DataLoader的batch_size和shuffle参数时序任务里训练集可以shuffleTrue因为每个样本已经是一个独立的时间窗口样本间不再有直接的前后依赖验证集必须shuffleFalse否则评估曲线无法按时间顺序对齐。import torch from torch.utils.data import Dataset, DataLoader class TimeSeriesDataset(Dataset): def __init__(self, X, y): self.X torch.tensor(X, dtypetorch.float32) self.y torch.tensor(y, dtypetorch.float32) def __len__(self): return len(self.X) def __getitem__(self, idx): return self.X[idx], self.y[idx] train_dataset TimeSeriesDataset(X_train, y_train) val_dataset TimeSeriesDataset(X_val, y_val) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse)shuffleTrue在训练集上还有一个额外的好处打破相邻窗口的相似性让每个batch的数据分布更均匀训练收敛更稳定。但注意不要设置drop_lastTrue除非你确定最后一个batch的尺寸不会导致模型崩溃。实际使用中batch_size从64起步显存紧张时降到32验证集没必要用完整数据集一次跑完分batch推理更稳。3. 融合模型PyTorch实现LSTM块、Transformer编码器与特征融合层3.1 融合架构设计并行融合更稳LSTM和Transformer融合主流做法有两种串行和并行。串行是把LSTM的输出当作Transformer的输入或者反过来两个模型串联。这种做法的弊端是第一级模型的信息瓶颈会限制第二级模型的输入质量。并行融合是让LSTM和Transformer各处理一份输入再把两路的特征拼接起来送入回归头。资源源码里用的是并行融合理由很直接LSTM和Transformer关注的是序列的不同维度LSTM擅长捕捉近邻时间步的连续变化趋势Transformer的self-attention能直接建模任意两个时间步之间的依赖。并行让两条支路互不干扰融合层再决定各自贡献多少。模型参数设计如下d_model64nhead4num_layers2dropout0.1。d_model是Transformer隐层维度理论上越大表达力越强但对时序预测来说64已经够用再大容易过拟合。nhead4意味着每个注意力头负责64/416维的子空间多了头数并不能带来质的提升反而增加计算量。num_layers这里指的是TransformerEncoderLayer的堆叠层数2层是性价比最高的选择1层欠拟合3层以上在小数据集上很容易过拟合。3.2 LSTM支路的PyTorch实现class LSTMBranch(nn.Module): def __init__(self, input_size, hidden_size64, num_layers2, dropout0.1): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout, bidirectionalFalse ) def forward(self, x): # x: (batch, seq_len, input_size) output, (h_n, c_n) self.lstm(x) # 取最后一层的最后时间步输出 last_hidden h_n[-1] # (batch, hidden_size) return last_hiddennn.LSTM的参数里batch_firstTrue表示输入形状是(batch, seq_len, feature)不设这个参数默认是(seq_len, batch, feature)新手经常在这里排错。bidirectionalFalse双向LSTM确实能多看一遍反向信息但对时间序列预测来说预测时只能依赖过去双向会引入未来信息属于数据泄漏。h_n的形状是(num_layers, batch, hidden_size)取h_n[-1]就是最后一层的隐状态这个向量包含了LSTM对整个序列的压缩表示。丢掉output序列所有时间步的输出只用最后隐状态是因为后续融合层只需要一个向量级的序列表示。3.3 Transformer支路与位置编码Transformer支路处理同一份输入但需要额外加位置编码。self-attention本身是位置无关的它对序列中所有时间步的计算是对称的如果不加位置编码模型分不清第1个时间步和第24个时间步在顺序上的区别。对时序预测来说顺序信息极其关键——第23步和第24步的关系跟第1步和第24步的关系完全不同。这里用sinusoidal位置编码也就是Transformer原论文里的那种class TransformerBranch(nn.Module): def __init__(self, d_model64, nhead4, num_layers2, dropout0.1, seq_len24): super().__init__() self.d_model d_model # 输入特征先线性投影到d_model维度 self.input_proj nn.Linear(input_size, d_model) # sinusoidal位置编码不需要学习参数 pe torch.zeros(seq_len, d_model) position torch.arange(0, seq_len).unsqueeze(1).float() div_term torch.exp(torch.arange(0, d_model, 2).float() * (-torch.log(torch.tensor(10000.0)) / d_model)) pe[:, 0::2] torch.sin(position * div_term) pe[:, 1::2] torch.cos(position * div_term) self.register_buffer(pe, pe.unsqueeze(0)) self.encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforward128, dropoutdropout, batch_firstTrue ) self.encoder nn.TransformerEncoder(self.encoder_layer, num_layersnum_layers) def forward(self, x): batch, seq_len, _ x.shape x self.input_proj(x) # (batch, seq_len, d_model) x x self.pe[:, :seq_len, :] # 加入位置编码 x self.encoder(x) # (batch, seq_len, d_model) # 取序列维度上的均值池化 return x.mean(dim1) # (batch, d_model)input_proj的作用是把feature_num维输入映射到d_model64维因为Transformer编码器内部的计算维度必须是d_model。位置编码用register_buffer注册不会在反向传播时更新梯度。dim_feedforward128是前馈网络隐层维度一般设为d_model的2倍。均值池化比取最后时间步更稳因为Transformer每一层的输出已经是全局交互后的结果均值能综合所有时间步的信息而最后时间步可能丢失早期的重要模式。3.4 融合层与完整模型class LSTMTFusionModel(nn.Module): def __init__(self, input_size, seq_len, d_model64, hidden_size64, num_layers2, nhead4, dropout0.1, pred_len1): super().__init__() self.lstm_branch LSTMBranch(input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, dropoutdropout) self.transformer_branch TransformerBranch(input_sizeinput_size, d_modeld_model, nheadnhead, num_layersnum_layers, dropoutdropout, seq_lenseq_len) fusion_dim hidden_size d_model self.fusion nn.Sequential( nn.Linear(fusion_dim, 32), nn.ReLU(), nn.Dropout(dropout), nn.Linear(32, pred_len) ) def forward(self, x): lstm_out self.lstm_branch(x) transformer_out self.transformer_branch(x) fused torch.cat([lstm_out, transformer_out], dim-1) return self.fusion(fused)pred_len1时输出一个单值pred_len改成多步时输出多个未来时间步的预测值。融合层的维度是hidden_size d_model这里两个分支都用64融合后128维先降到32维再输出中间加ReLU和Dropout是为了防止融合层直接过拟合。实际跑的时候要注意如果LSTM的hidden_size和Transformer的d_model不同torch.cat维度不匹配会报错这是融合模型最常见的维度错误。引资源源码的模型默认两者一致改参数时要同步修改。4. 训练策略与超参数调优损失函数、学习率调度与早停4.1 损失函数选型MSE还是Huber时序预测的默认损失是MSE但它在预测值和真实值相差较大时梯度过大训练初期容易震荡。HuberLoss是对MSE和MAE的折中误差小于delta时用MSE的平方损失误差大于delta时退化为MAE的线性损失。多特征时序数据里偶尔会有异常点Huber对这类点更鲁棒。delta通常取1.0误差在1以内按平方放大梯度超过1按线性缩小梯度。资源源码里默认用HuberLoss跑出来的收敛曲线比MSE更平滑验证集的指标也更稳定。criterion nn.HuberLoss(delta1.0)用MSE的话模型会把更多注意力放在少数大误差样本上这在负荷预测或设备寿命预测中意味着极端工况主导了训练方向。Huber损失相当于告诉模型正常误差精细优化异常误差别太较劲。如果你的预测目标本身没有明显异常点用MSE也完全可以但从时序数据的常态来看Huber的保险系数更高。4.2 优化器与学习率调度优化器建议用AdamW而不是Adam。AdamW把权重衰减从梯度更新中解耦能更干净地控制模型复杂度对Transformer这类带LayerNorm的结构尤其友好。学习率初始值1e-3到3e-4我一般先跑5个epoch看损失变化如果损失爆炸或震荡降一个量级如果收敛缓慢升到1e-3。配合CosineAnnealingLR调度器比固定学习率效果好它的思路是让学习率从初始值按余弦曲线逐步衰减到最小值前期快速下降逼近最优区域后期精细收敛。optimizer torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay1e-5) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50, eta_min1e-6)T_max是半个周期内的epoch数这里设为50意味着前50个epoch学习率从3e-4降到1e-6。如果你的训练总量是100个epoch刚好一个完整余弦周期。eta_min是学习率下限设1e-6是为了避免后期完全停止更新。实践中如果发现50个epoch后验证指标还在下降可以继续训练但学习率已经压得很低收益不大不如调整模型结构或数据预处理。4.3 训练循环与早停机制def train_epoch(model, train_loader, criterion, optimizer, device): model.train() total_loss 0 for X_batch, y_batch in train_loader: X_batch, y_batch X_batch.to(device), y_batch.to(device) optimizer.zero_grad() pred model(X_batch) loss criterion(pred, y_batch) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() * X_batch.size(0) return total_loss / len(train_loader.dataset)clip_grad_norm_是LSTMTransformer组合里必须有的操作。LSTM在长序列上容易梯度爆炸Transformer叠加多层后梯度范数也不小把梯度范数裁剪到1.0训练稳定性会好很多。验证集上的评估逻辑类似只是不加梯度更新def validate(model, val_loader, criterion, device): model.eval() total_loss 0 with torch.no_grad(): for X_batch, y_batch in val_loader: X_batch, y_batch X_batch.to(device), y_batch.to(device) pred model(X_batch) loss criterion(pred, y_batch) total_loss loss.item() * X_batch.size(0) return total_loss / len(val_loader.dataset)早停的判断标准连续patience10个epoch验证损失不下降就停止训练并回滚到验证损失最低的那个epoch权重。这个策略能有效防止过拟合尤其是融合模型参数量不算小在小数据集上很容易在后期过拟合。每个epoch结束对比当前验证损失和最佳损失如果更小就保存模型权重累计10次没有突破就终止。4.4 评估指标的选择验证损失是模型内部的优化目标对外汇报时要用业务可解释的指标。推荐三个MAE平均绝对误差、RMSE均方根误差、MAPE平均绝对百分比误差。MAPE计算时如果真实值里有0或接近0的值百分比会爆炸需要过滤。比如工业负荷预测里凌晨负荷接近0MAPE算出来几千个百分点没有参考价值。此时改用sMAPE或直接看MAE更实在。一个习惯是训练过程中把这三个指标一起打印如果RMSE远大于MAE说明存在少数大误差样本模型对极端值的拟合还不够好。5. 避坑与常见问题排查五个让新手翻车的细节5.1 归一化泄漏验证指标好看上线必翻车现象训练集和验证集分开划分了但归一化时偷懒对整个数据集一次性fit_transform验证损失看着很低但在新数据上预测结果完全不对偏差很大。原因验证集或测试集的均值方差提前参与了训练模型在验证时已经见过同分布信息这在时间序列预测里等于作弊。解决严格按时间顺序切出训练集只用训练集fit平均值和标准差验证集和测试集只做transform这一步没有妥协余地。5.2 验证集切分时打乱了时间顺序现象为了平衡样本分布按随机方式把数据切分成训练集和验证集训练损失降得很好验证损失也正常但预测曲线和真实值对不上整体滞后。原因时间序列样本之间不是独立同分布的随机切分把未来数据泄漏进了训练集模型学到了提前“预知”的能力。解决按时间顺序切分训练集永远是前70%或前80%验证集用最后一段。如果要用K折交叉验证也要按时间顺序分折不能随机分。5.3 Transformer位置编码和序列长度不匹配现象训练时seq_len24一切正常推理时换了一组更长的数据模型直接报维度错误The size of tensor a (48) must match the size of tensor b (24)。原因位置编码矩阵在初始化时写死了seq_len输入更长时位置编码不够用。解决位置编码矩阵用register_buffer注册后forward里改成语义切片pe[:, :seq_len, :]这样训练和推理时的序列长度可以不一样。如果序列长度变化幅度很大建议直接改成可学习位置编码让模型自己适应不同长度。5.4 显存占用大、训练慢现象seq_len24时训练速度尚可改成seq_len168之后显存直接翻了近十倍训练时间从分钟级变成小时级。原因Transformer的self-attention复杂度是O(seq_len^2)序列长度翻7倍计算量和显存占用翻49倍。LSTM虽然只跟序列长度线性相关但融合模型在Transformer支路上仍然会放大。解决如果确实需要长序列先考虑缩小nhead或d_model或者把长序列切成长度较短的多个窗口做分层建模。还有一个思路是只让LSTM支路吃长序列Transformer支路吃下采样的短序列两路长度不用一致融合层的维度一致就行。5.5 随机种子未固定结果跑飞现象同一份数据和同一个模型每次训练出来的验证损失都不一样有时差5%有时差20%。原因PyTorch的权重初始化、DataLoader的shuffle、Dropout都引入了随机性不固定种子导致结果不可复现。解决训练脚本开头统一固定三个随机源import random import numpy as np import torch random.seed(42) np.random.seed(42) torch.manual_seed(42) torch.cuda.manual_seed_all(42)注意DataLoader设置worker_init_fn才能让多进程数据加载也使用固定种子否则每次迭代数据顺序还是随机的。固定种子之后同一份代码在不同机器上跑出的结果也会有一些细微偏差这是GPU浮点运算的并行差异导致的在可接受范围内。6. 模型验证与多步预测可视化检验和参数量的估算模型训练完第一步不是看指标而是把验证集的预测曲线和真实曲线画在一起用眼睛看趋势是否对齐。指标数值只能反映误差大小看不出滞后和幅值偏差。滞后通常表现为预测曲线比真实曲线晚一个时间步原因多半是归一化泄漏或LSTM的隐状态没有充分捕捉趋势。幅值偏低则可能是HuberLoss的delta设置过小模型对大误差不够敏感。多步预测有两种实现方式。简单的是直接多步——把pred_len设成目标步数模型一次输出多个值。另一种是递归预测——先预测下一个时间步把预测值当作输入再预测下一步缺点是大误差会逐点累积。两个方式都试一下多数时候直接多步更稳因为模型在训练时见过完整的未来窗口不会产生递归累积漂移。模型参数量和感受野可以用下面这段代码快速估算total_params sum(p.numel() for p in model.parameters()) print(fTotal parameters: {total_params})运行后会看到融合模型的参数量通常在10万到30万之间。LSTM支路的参数主要来自四个门的权重矩阵Transformer支路的参数集中在自注意力和前馈网络。如果参数量超过50万而训练数据只有几千条大概率过拟合反之参数量在10万以下训练数据够多但验证损失不降说明模型容量不足。做多步预测时还有一个验证技巧把预测起点依次平移对比每条预测曲线的衰减情况。如果预测步数越远误差越大这是正常的但如果第2步的预测误差就明显劣于第1步好几倍说明模型学到的不是趋势而是最近邻的值需要增大seq_len或加深Transformer层数。从那以后我每次训练融合模型都会强制走一遍固定随机种子、检查归一化是否只fit训练集、验证预测曲线是否滞后、估算参数量是否合理。这套流程帮我挡住了至少一半的无效训练希望帮到你。本文还有配套的精品资源点击获取
返回列表