ARTICLE DETAIL

资讯详情

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

LSTM与Transformer时序预测:源码实现、调参思路与踩坑记录

LSTM与Transformer时序预测:源码实现、调参思路与踩坑记录 简介这套基于长短期记忆LSTM与Transformer模型的时序预测实践源码面向Python开发者和机器学习入门者聚焦金融、气象、经济等时间序列预测场景提供可直接运行、便于二次改造的完整工程。资源共31个文件压缩包约43.58MB包含3个Python核心程序、3个CSV数据集、7个工程配置文件、14张可视化结果图及模型状态文件从数据预处理到模型训练与效果对比均有覆盖方便快速跑通流程。目前已有907人学习浏览代码结构清晰目录划分明确适合对比两类模型在实际数据上的建模差异。通过项目可掌握Transformer、GPU优化版RNN和标准RNN的实现细节理解自注意力机制与门控机制在时序预测中的应用并参考数据清洗、归一化、可视化等预处理思路为后续更复杂的时序任务打下基础。 做了几年时间序列预测我发现一个现象很多人一上来就抱着LSTM调参但换了Transformer之后效果未必更好反而容易踩一堆坑。这篇东西不讲虚的直接把我实际跑通的LSTM和Transformer预测源码、踩过的坑、调参思路一并整理出来你自己对照着改数据就能用。先说清楚这套代码的定位。它不是一个端到端的完整项目而是两个可独立训练的预测模型核心解决的是单变量和多变量时序数据的回归预测问题。LSTM走经典的seq2seq思路Transformer做了针对时序的改造——不是直接套NLP那套而是用因果掩码防止未来信息泄漏同时去掉Decoder只保留Encoder内部状态做映射。这两种思路正好覆盖了时序预测里最主流的两种范式循环迭代和全局注意力。1. 为什么我不用ARIMA选了LSTM和Transformer作为基线模型对于时序预测很多人第一反应还是ARIMA。ARIMA在平稳序列、线性关系、数据量不太大的场景下确实够用但一旦数据带明显的周期性叠加趋势性或者特征维度变多ARIMA的参数识别就变得非常痛苦差分阶数、AR项、MA项光是用ACF和PACF图定阶就得折腾半天而且它对突变点的适应性很差。LSTM则是把记忆这件事做成了显式设计。它通过三个门结构控制信息的写入、遗忘和输出相当于给网络装了一道可学习的闸门。你在训练时喂进去的每一个时间步它都会决定哪些信息要保留、哪些要丢弃这样面对长期依赖时梯度消失问题比传统RNN轻得多。Transformer走的是另一条路它不按时间步逐步处理而是把整个序列扔进自注意力机制里一次性算出每个位置和其他所有位置的相关性权重。这意味着它可以捕捉到相隔很远的时间点之间的依赖关系——比如7月1日的气温和12月30日的气温可能没有直接关联但某些周期性数据中50步之前的信息可能比5步之前还要重要。加上多头注意力机制它可以从多个维度同时观察序列内部的关联模式。我的建议是数据量比较小、特征相对简单时先用LSTM数据量充足、序列足够长比如100步以上、且周期性模式明确时Transformer更有可能跑出上限更高的结果。两者没有绝对的优劣我在实际项目里经常把这两个模型作为对比基线一起跑看谁在当前数据集上更稳。2. 时序预测的数据预处理滑动窗口、归一化与防数据泄漏在做任何建模之前数据预处理这一关直接决定了最终效果的天花板。很多人模型调了半天没效果回过头来看往往是数据划分出了问题。2.1 滑动窗口参数怎么选LSTM和Transformer都不能直接吃原始序列需要把数据切成固定长度的窗口。这个窗口长度也就是seq_len代表模型每次能看到多少历史时间步。我一般这样算如果数据有明显的周期至少要覆盖一到两个完整周期。比如每条数据是24小时的采样周期是7天那seq_len至少128甚至168。如果数据没有明显的周期可以先试24、48、72这样的小窗口再用验证集效果去反推。代码实现上用滑动窗口去切片def create_sequences(data, seq_len, pred_len): X, y [], [] for i in range(len(data) - seq_len - pred_len 1): X.append(data[i : i seq_len]) y.append(data[i seq_len : i seq_len pred_len]) return np.array(X), np.array(y)这里注意返回的y是一个序列而不是一个点。单步预测的时候pred_len1就行多步预测则可以根据业务需要调整。多步预测对模型的挑战大得多因为误差会随着预测步长的增加逐步累积。2.2 归一化是个细节活时间序列预测里MinMaxScaler是最常用的归一化手段。因为LSTM的激活函数tanh、sigmoid对输入范围非常敏感数值过大会导致梯度饱和过小则会让梯度信号变弱。Transformer里的注意力机制通过对Q和K做点积计算结果也会受到数值尺度的影响如果不归一化softmax很容易趋于one-hot分布注意力权重几乎不可解释。归一化的关键操作是先在训练集上fit再用同样的scaler去transform验证集和测试集千万不能每段数据单独fit。不然验证集和测试集的分布就被人为改变了评估结果就是假的。反归一化同样重要。预测结果输出后如果不换回原始尺度你得到的只是一个无量纲的归一化数值业务上无法解释。注意要用全局的同一个scaler去inverse_transform不能每段单独做。2.3 时间序列不能随机打乱这是最容易踩的一个雷。普通的机器学习任务里通常需要shuffle数据来消除样本相关性但时序预测不同——样本之间天然存在时间顺序一旦打乱训练集里就可能混入未来信息验证集里也可能出现过去的数据评估指标马上失真。正确做法是按时间顺序划分train / val / test比如前70%训练、中间15%验证、后15%测试。窗口切片生成样本之后在训练阶段可以适度shuffle每个batch内的顺序这样既能减轻循环神经网络对序列开头数据的偏置又不会破坏样本内部的顺序关系。我用一个具体的例子来说明假设你有8760个小时的数据一年如果设置seq_len168一周pred_len24一天那总样本数大约是8760-168-248568个。训练集取前70%约5997个验证集取中间15%测试集取最后15%。这样划分可以确保模型不会提前看到未来要预测的那段趋势。2.4 数据泄漏的隐蔽场景我见过不少人把一个重要变量——比如前一天的涨跌幅——直接作为特征放进模型里然后在预测目标里又包含了这个特征变量本身结果测试集效果异常好一上真实场景立刻崩盘。这是因为特征和标签之间存在信息重叠模型学会了抄近路。处理办法是所有特征中凡是与目标变量同源的滞后变量都要严格控制滞后步数不能让它直接出现在当前时刻的输入特征里。3. LSTM模型源码实现搭骨架、调参数、跑训练LSTM在PyTorch里的实现思路很直接定义一个LSTM层取出每个时间步的隐状态序列或者最后一个隐状态再接一个全连接层输出预测值。3.1 基础模型代码import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size, hidden_size, num_layers, output_size, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout, bidirectionalFalse ) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): # x shape: (batch, seq_len, input_size) out, _ self.lstm(x) # out shape: (batch, seq_len, hidden_size * num_directions) # 取最后一个时间步的隐状态 last_out out[:, -1, :] return self.fc(last_out)这里有几个参数值得展开说。3.2 hidden_size和num_layers怎么定hidden_size是LSTM隐藏状态的维度可以理解为网络记忆容量的大小。太小了记不住长序列中的关键模式太大了容易过拟合且训练变慢。我的经验是样本量在几千这个量级时hidden_size从32到128之间试一般来说64是比较稳妥的起点。num_layers是堆叠的LSTM层数。层数多可以让模型学习到更高层次的抽象表示但也带来更大的参数量和更严峻的梯度传播问题。两层就够用三层以上需要更强的正则化和更大的数据集支撑否则很容易过拟合。补充一句堆叠LSTM时第一层的输入是原始序列后面层的输入是前一层输出的隐状态序列各层之间不是简单串联而是在做不同层级的特征抽象。3.3 训练循环里的经典操作LSTM最怕梯度爆炸所以梯度裁剪几乎是标配。我通常在每轮backward之后调用clip_grad_norm_把梯度的L2范数限制在1.0以内。这个操作只影响梯度大小不影响方向能有效防止训练出现NaN或loss剧烈抖动。学习率方面我习惯用Adam优化器初始学习率从0.001起步。如果训练过程稳定可以用余弦退火或者ReduceLROnPlateau在loss进入平台期时自动降低学习率效果通常比固定学习率好。另外batch_size不宜过大时序数据通常batch_size取32到128之间比较合适太大容易让梯度更新方向过于平滑反而学不到细节。下面是训练循环的核心骨架optimizer torch.optim.Adam(model.parameters(), lr0.001) criterion nn.MSELoss() scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5 ) for epoch in range(epochs): model.train() for X_batch, y_batch in train_loader: optimizer.zero_grad() pred model(X_batch) loss criterion(pred, y_batch) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() model.eval() val_loss evaluate(model, val_loader) scheduler.step(val_loss)3.4 单向LSTM与双向LSTM的选择很多人做时序预测时下意识觉得双向LSTM一定更好其实这是一个典型的方向性错误。双向LSTM在训练时会同时利用时间点前后两个方向的信息这对NLP任务来说是合理的但在时序预测里t时刻的未来信息本身就是预测目标的一部分——用未来去预测当前等于是在作弊推理阶段根本拿不到后面几步的数据。所以在标准的时序预测任务中我坚持用单向LSTM。单向结构强制模型只能依赖历史信息做出预测这更符合真实业务场景的时间因果约束。4. Transformer做时序预测的三个关键改动直接拿NLP里的Transformer套用时序数据通常会遇到两个问题一是位置编码的语义不对二是注意力机制会看到未来的信息。所以要针对预测任务做改动。4.1 位置编码时间步的先后顺序必须显式告诉模型Transformer没有循环结构它对序列中各个位置的处理在结构上是完全对称的。如果不加位置编码把序列倒过来喂进去模型输出的结果会一模一样。这在时序预测中是不允许的——昨天和今天对未来一天的意义完全不同。我用的是标准的正弦位置编码方案。它比可学习的绝对位置嵌入更稳对长序列的泛化性更好而且不会因为训练集中没有出现过某一段位置而失效。代码里实现一下import math def positional_encoding(seq_len, d_model): pe torch.zeros(seq_len, d_model) position torch.arange(0, seq_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) return pe.unsqueeze(0) # shape: (1, seq_len, d_model)需要注意的是d_model注意力层的向量维度如果是奇数上面代码里最后的偶数索引切分会报错。所以d_model一般取偶数或者对奇数维情况做padding处理这个小坑我在调维度时踩过。4.2 因果掩码模型不能偷看未来TransformerEncoder在计算每个位置的注意力时默认会看到序列里的全部位置。如果直接拿它做预测等于让模型在预测t1的时候看到了t1时刻的输入。序列长度短的话测试集上会被这种自泄漏误导出虚高的效果。解决办法是给注意力矩阵加一个上三角掩码矩阵把当前位置之后的注意力分数全部替换为负无穷这样softmax之后后续位置的权重会变成0。在PyTorch里TransformerEncoderLayer的forward自带src_mask参数传一个seq_len, seq_len的掩码矩阵就行def create_causal_mask(seq_len): mask torch.triu(torch.ones(seq_len, seq_len), diagonal1) return mask.masked_fill(mask 1, float(-inf))这个掩码把注意力严格限制在只能看当前位置及其之前的范围从结构上保证模型不会使用未来信息。4.3 Transformer预测模型的网络结构时序Transformer最关键的一点是预测头不能像LSTM那样只取最后一个隐藏状态再接全连接而应该对编码后的每个位置输出做聚合。我实践下来最直接有效的做法是取所有时间步输出的平均池化后再接全连接层。这比只取最后一个位置输出更平滑模型对序列中间可能出现的局部峰值不会过度敏感。class TimeSeriesTransformer(nn.Module): def __init__(self, input_size, d_model, nhead, num_layers, output_size, dropout0.1): super().__init__() self.input_proj nn.Linear(input_size, d_model) self.pos_encoder positional_encoding(seq_len, d_model).detach() # 实际seq_len需要传入 encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforwardd_model * 4, dropoutdropout, batch_firstTrue ) self.encoder nn.TransformerEncoder(encoder_layer, num_layersnum_layers) self.fc nn.Linear(d_model, output_size) def forward(self, x): # x shape: (batch, seq_len, input_size) x self.input_proj(x) x x self.pos_encoder mask create_causal_mask(x.size(1)).to(x.device) out self.encoder(x, maskmask) # (batch, seq_len, d_model) pooled out.mean(dim1) # 平均池化 return self.fc(pooled)关于参数设置d_model一般取64或128nhead取4或8num_layers取2到4层。d_model必须是nhead的整数倍否则多头注意力的维度切分会出错。dim_feedforward设置为d_model的4倍是Transformer的常见设计太多会增加过拟合风险。5. 实测对比结果与踩坑记录我在一个电力负载数据集上做了对比实验数据为每15分钟一个采样点持续一个月共2880个点。seq_len取96即过去24小时pred_len取1归一化使用MinMax训练集、验证集、测试集按7:1.5:1.5比例按时间顺序划分。模型配置LSTMhidden_size128num_layers2dropout0.2训练60轮Transformerd_model64nhead4num_layers2dropout0.1训练60轮评估指标用MAE和RMSE。结果如下模型参数量MAERMSE训练耗时/轮LSTM91k0.0210.034约3sTransformer268k0.0180.031约5s单个预测点的Transformer略优但优势不算特别大。后来我把pred_len从1改成24预测后一天的负载差距明显拉开LSTM的RMSE到了0.058Transformer为0.043。这说明序列内部长程依赖关系越强Transformer的优势越能体现出来。踩坑经历方面记录了三个值得说的第一个坑没有加因果掩码时Transformer在训练集上loss降得极快MAE只有0.008但测试集MAE达到0.037比加掩码后的结果明显要差。这就是典型的信息泄漏——模型学到了用未来数据预测未来的捷径真实场景里完全失效。第二个坑LSTM调参到hidden_size256、num_layers3后训练集loss更低但验证集loss反弹过拟合明显。加了dropout0.3和权重衰减1e-4之后才压下来。如果你发现训练集效果很好但泛化差优先考虑的不是加数据而是加正则化。第三个坑学习率设成0.01时Transformer的前几个epoch出现loss为NaN的情况原因是注意力内部的矩阵数值随层数加深迅速膨胀。用0.001初始学习率配合warmup策略后稳定了。如果你想加速收敛可以先让学习率从极小值线性涨到预设值再正常衰减。6. 进阶方向与部署经验分享如果上面的基础模型你已经跑通了接下来可以考虑几个实打实的扩展方向。第一个方向是注意力可视化把Transformer里多头注意力的权重矩阵抽出来画成热力图看看模型在预测时重点关注了哪些历史时段。这对理解模型行为很有帮助有时你会惊讶地发现模型关注的并不是离预测点最近的几个时刻而是周期上相近的历史时刻——这其实正是它捕捉到了周期性规律的证据。第二个方向是多步预测的两种策略对比。一种是迭代预测把上一步的输出作为下一步的输入步步递推另一种是直接预测一次性输出未来N个时间步的序列。前者模型简单但误差累积明显后者训练目标更复杂但预测结果更稳定。两者我都用过在预测步数较短小于10步时差距不大长步数预测直接输出的稳定性更好。第三个方向是模型部署。训练好的PyTorch模型用TorchScript或者ONNX导出后可以脱离PyTorch环境进行推理。ONNX格式的推理速度通常比原版快20%到40%模型体积也更小方便部署到服务端。导出时有几个关键点一是要把归一化和反归一化参数一并导出或者在服务端单独保存scaler对象否则线上推理的数据尺度会对不上二是要固定输入维度ONNX导出时如果seq_len是动态的需要在export时就指定动态维度不然后续输入不同长度的序列会报错。结合我个人的项目经验最后提醒一点不要盲目追求复杂模型。如果你手里的数据量只有几百条LSTM往往比Transformer更靠谱。Transformer是个吃数据的怪兽数据不够时学不到注意力模式过拟合反而更严重。先把LSTM作为基线跑通再考虑上Transformer对比这是最稳妥的技术路线。本文还有配套的精品资源点击获取
返回列表