ARTICLE DETAIL

资讯详情

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

Transformer+LSTM+TCN+XGBoost混合时间序列预测模板

Transformer+LSTM+TCN+XGBoost混合时间序列预测模板 简介一套基于Python的多模型融合时间序列预测源码与项目说明面向有一定深度学习基础、希望提升时序预测精度的开发者与科研人员。方案同时整合Transformer、LSTM、TCN与XGBoost兼顾序列长期依赖、局部特征与树模型非线性拟合能力适合销量、股价、能耗等典型场景的预测任务。压缩包共5个文件包含Python预测脚本、两份csv演示数据集、shell启动脚本及Markdown项目说明体积仅12KB短小精悍脚本可直接运行便于快速复现和二次改造。目前已有150人学习下载。资源提供的价值在于一是给出多模型融合的完整代码流程二是通过真实结构的演示数据展示数据预处理、训练与预测环节三是附带的说明文档能帮读者理清各模型的组合方式与调参思路尤其适合做算法对比实验或作为论文基线。1. 一个把深度学习波动和树模型稳健性拼在一起的混合预测模板用纯 Transformer 做时间序列预测结果常常被一个只喂滞后特征的 XGBoost 按在地上摩擦。这不是段子是很多人在 Kaggle 和业务基线里踩过的坑。这个标题里的融合思路本质上是把深度学习抓时序模式、树模型抓非线性特征各自的长处拼到一起Transformer 抓全局依赖LSTM 处理中等尺度的记忆TCN 用因果卷积稳定捕捉局部趋势最后让 XGBoost 对前三者的预测结果做非线性校正。适合有 Python 基础、想搭一套能复现也能改的混合预测模板的工程师也适合拿来做比赛基线和业务预演的初版方案。2. 先把四个模型的分工想清楚Transformer、LSTM、TCN、XGBoost 各管哪一段2.1 三个深度模型的边界全局依赖、顺序记忆、局部因果场时间序列预测的核心矛盾是序列里既有长期周期又有短期惯性还有突发噪声。单一模型很难同时处理好这三件事。Transformer 的优势在于注意力机制能直接建立任意两个时间步之间的关联长周期依赖不需要像 RNN 那样一步步传递这也是 transformer 架构近几年被引入时序领域的主要原因。但它的代价是位置编码对序列长度敏感输入太长计算量上涨很快而且对局部细节的刻画不如卷积类模型直接。LSTM 则是经典的顺序建模思路通过遗忘门、输入门、输出门控制信息流动擅长捕捉中等长度的动态变化。它的短板在于训练是串行的难以并行加速而且面对超长序列时梯度传递仍然会衰减。这里有个常见的误解LSTM 不是不能抓长期依赖而是需要靠门控机制“有选择地记住”它更适合那些依赖近期状态和缓慢趋势的信号。TCN 的结构则是因果膨胀卷积每一层只看到当前时刻及其之前的输入不会引入未来信息。通过膨胀率指数增长TCN 可以在层数很少的情况下扩大感受野而且卷积天然支持并行训练速度明显快于循环结构。它的缺点是感受野需要人为设计窗口不够大时周期超过感受野的信号就看不全这在 tcn 模型结构里是必须处理的边界问题。2.2 XGBoost 的定位不学序列只学预测残差与非线性校正把三个深度模型的输出直接平均是一种常见的 ensemble 做法但不是这个标题里最合理的做法。既然加了 XGBoost就应该让它承担更具体的工作把三个深度模型的验证集预测结果当作特征连同原始滞后值一起喂给 XGBoost让它学习“真实值和深度模型预测值之间的差异”。xgboost 本身是梯度提升树擅长处理特征之间的非线性交互对异常值和缺失值也有天然的稳健性。它不关心输入是否有时间顺序所以不能单独拿来做序列预测但用来做 stacking 的 meta learner 非常合适。树模型需要的是“有信息量的特征”而三个深度模型的预测值恰好都是对目标的高度压缩表示这种特征组合往往比直接拼原始序列效果更好。2.3 融合方式特征堆叠与两层架构我一般会把整体流程拆成两层。第一层是三个深度模型输入都是标准化后的滑窗序列输出是单步预测值。第二层是 XGBoost输入是三个模型在验证集上的预测值加上若干手工构造的滞后特征输出是最终预测。这里有一个必须注意的细节XGBoost 训练用的特征必须来自深度模型在验证集上的预测结果而不是训练集上的。如果拿训练集的预测结果去喂 XGBoost会产生严重的过拟合因为深度模型已经记住了训练集的噪声XGBoost 会把这些噪声当作规律学进去。验证集的预测才是模型真实泛化能力的体现。3. 数据形态把时间序列改造成监督学习样本3.1 滑窗函数从 pandas 到 PyTorch Tensor 的完整转换深度时序模型输入的标准形态是(B, T, F)B 是批大小T 是窗口长度F 是特征维度。单变量序列就是(B, T, 1)。下面这个函数把一维序列切成滑窗样本这一步是后面所有模型共同的数据前置。import numpy as np import pandas as pd import torch from torch.utils.data import TensorDataset, DataLoader def make_windows(series, seq_len32, horizon1, step1): 把一维时间序列切成 (样本, seq_len, 1) 的监督学习样本 series : 1D numpy array原始序列 seq_len: 每个样本的输入窗口长度 horizon: 要预测的未来步数 step : 滑窗步长越大样本量越小 X, y [], [] for i in range(0, len(series) - seq_len - horizon 1, step): X.append(series[i:i seq_len]) y.append(series[i seq_len horizon - 1]) X np.array(X, dtypenp.float32).reshape(-1, seq_len, 1) y np.array(y, dtypenp.float32) return X, y参数说明seq_len取多少直接决定模型能看到的视野。经验上至少覆盖一个完整周期月度数据取 12 或 24日度业务数据取 30 到 60。horizon大于 1 时上面这段代码预测的是窗口结束后的第horizon个点不是逐步递归这一点和后面的滚动预测不同。step控制样本重叠程度训练数据量不够时可以设为 1但要注意相邻样本高度相关验证集指标会偏乐观。3.2 按时间切分禁止随机打乱时间序列切分和普通机器学习最大的区别是测试集必须在时间上晚于训练集。随机打乱会把未来的信息泄露到训练集里导致验证指标虚高上线后立刻现原形。train_ratio, val_ratio 0.7, 0.15 n len(series) train_end int(n * train_ratio) val_end int(n * (train_ratio val_ratio)) train_raw series[:train_end] val_raw series[train_end:val_end] test_raw series[val_end:] X_train, y_train make_windows(train_raw, seq_len32) X_val, y_val make_windows(val_raw, seq_len32) X_test, y_test make_windows(test_raw, seq_len32)3.3 归一化只 fit 训练集反归一化放在最后深度模型对输入尺度很敏感LSTM 和 TCN 里的激活函数在输入过大或过小时梯度容易饱和。常见做法是用MinMaxScaler或者StandardScaler但必须注意scaler 只能在训练集上fit然后在验证集和测试集上transform。如果在全量数据上 fit相当于把未来统计量泄露给了训练过程。from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler() train_norm scaler.fit_transform(train_raw.reshape(-1, 1)).flatten() val_norm scaler.transform(val_raw.reshape(-1, 1)).flatten() test_norm scaler.transform(test_raw.reshape(-1, 1)).flatten() X_train, y_train make_windows(train_norm, seq_len32) X_val, y_val make_windows(val_norm, seq_len32) X_test, y_test make_windows(test_norm, seq_len32) train_loader DataLoader(TensorDataset( torch.tensor(X_train), torch.tensor(y_train)), batch_size64, shuffleTrue) val_loader DataLoader(TensorDataset( torch.tensor(X_val), torch.tensor(y_val)), batch_size256)反归一化在计算评估指标之前做把预测值和真实值都还原回原始尺度再算 RMSE 或 MAPE这样指标才是业务上能理解的意义。4. 三个深度模型的 PyTorch 实现与关键参数4.1 用 Transformer 编码器做短序列回归Transformer 做时间序列不需要完整的 encoder-decoder 结构单变量短序列预测用nn.TransformerEncoder就够。核心思路是用线性层把每个时间步的标量映射到d_model维再经过多层自注意力提取全局依赖最后取最后一个时间步的输出接一个回归头。import torch import torch.nn as nn class TransformerBlock(nn.Module): def __init__(self, d_model64, nhead4, num_layers2, dropout0.1): super().__init__() self.input_proj nn.Linear(1, d_model) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforward128, dropoutdropout, batch_firstTrue ) self.encoder nn.TransformerEncoder(encoder_layer, num_layersnum_layers) self.reg_head nn.Sequential( nn.Linear(d_model, 32), nn.ReLU(), nn.Linear(32, 1) ) def forward(self, x): # x: (B, T, 1) h self.input_proj(x) # (B, T, d_model) h self.encoder(h) # (B, T, d_model) return self.reg_head(h[:, -1, :]) # 取最后时间步参数说明d_model是注意力机制的向量维度不用追求过大64 到 128 在短序列上足够nhead必须能整除d_model4 或 8 比较常见num_layers堆到 4 层以上收益很小反而增加训练时间。还有一个容易踩的坑nn.TransformerEncoder默认会对输入做 layer norm如果数据本身已经做了归一化dropout 要设小一点防止叠加后把信号磨平。4.2 LSTM 的输入形状与隐藏层设置LSTM 的输入形状同样是(B, T, 1)但它的输出序列里每个时间步都携带了从序列开头到当前时刻的信息取最后一个时间步的输出作为整段序列的压缩表示是标准做法。class LSTMBlock(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers2, dropout0.1): super().__init__() self.lstm nn.LSTM( input_size, hidden_size, num_layers, batch_firstTrue, dropoutdropout ) self.reg_head nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Linear(32, 1) ) def forward(self, x): # x: (B, T, 1) out, _ self.lstm(x) # out: (B, T, hidden_size) return self.reg_head(out[:, -1, :])参数说明hidden_size决定 LSTM 的记忆容量太小记不住波动模式太大容易过拟合64 到 128 是安全区间。num_layers为 2 层时效果比 1 层明显好但到 3 层以上收益递减且训练时间翻倍。batch_firstTrue必须带上否则输入输出维度对不上。LSTM 对梯度爆炸比较敏感训练时建议配合梯度裁剪clip_grad_norm_这是很多 lstm 时间序列预测 python 实战里最终救场的操作。4.3 TCN 的因果卷积与膨胀感受野TCN 的构建分两步先实现因果卷积保证未来信息不会泄漏到过去再按 1、2、4、8 的膨胀率堆叠让感受野指数级扩大。import torch.nn.functional as F class CausalConv1d(nn.Module): def __init__(self, in_channels, out_channels, kernel_size, dilation): super().__init__() self.padding (kernel_size - 1) * dilation self.conv nn.Conv1d(in_channels, out_channels, kernel_size, dilationdilation) def forward(self, x): # 左侧补零保证输出长度不变且只看过去 x F.pad(x, (self.padding, 0)) return self.conv(x) class TCNBlock(nn.Module): def __init__(self, input_size1, channels32, levels4, kernel_size3, dropout0.1): super().__init__() self.project nn.Conv1d(input_size, channels, 1) layers [] for i in range(levels): dilation 2 ** i layers.append(CausalConv1d(channels, channels, kernel_size, dilation)) layers.append(nn.ReLU()) layers.append(nn.Dropout(dropout)) self.tower nn.Sequential(*layers) def forward(self, x): # x: (B, T, 1) - 转成 (B, 1, T) 进 Conv1d x x.permute(0, 2, 1) h self.project(x) h self.tower(h) return h[:, :, -1] # 取最后一个位置参数说明kernel_size和levels共同决定感受野计算公式是1 sum((kernel_size - 1) * 2^i)3 和 4 的组合对应感受野 31刚好覆盖 32 的窗口长度。如果窗口加长到了 64levels要相应增加到 5 或 6。TCN 里 dropout 建议保留 0.1 左右因为卷积层之间没有类似 LSTM 的隐式正则。这三个模型在源码里通常各自独立成文件训练时也分别保存权重方便后面调 XGBoost 集成时灵活替换个别模型。模型之间没有任何共享参数耦合点只在特征层面。5. 训练、验证与 Stacking 集成流程5.1 一个通用的深度模型训练循环三个模型的训练逻辑几乎一样只差模型实例因此可以直接用一个函数覆盖。import numpy as np def fit_deep_model(model, train_loader, val_loader, epochs80, lr1e-3, patience10): opt torch.optim.Adam(model.parameters(), lrlr) sched torch.optim.lr_scheduler.ReduceLROnPlateau( opt, modemin, factor0.5, patience5) loss_fn nn.MSELoss() best_loss float(inf) bad_epochs 0 for epoch in range(epochs): model.train() for xb, yb in train_loader: opt.zero_grad() loss loss_fn(model(xb).squeeze(), yb) loss.backward() # LSTM 梯度裁剪Transformer 和 TCN 可省略 torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) opt.step() model.eval() val_losses [] with torch.no_grad(): for xb, yb in val_loader: val_losses.append(loss_fn(model(xb).squeeze(), yb).item()) avg_val float(np.mean(val_losses)) sched.step(avg_val) if avg_val best_loss: best_loss avg_val bad_epochs 0 torch.save(model.state_dict(), f{model.__class__.__name__}.pt) else: bad_epochs 1 if bad_epochs patience: break return best_loss逻辑说明每个 epoch 先跑训练集更新权重再跑验证集计算损失ReduceLROnPlateau在验证损失连续 5 个 epoch 不下降时把学习率减半patience是早停阈值避免在验证集上过拟合。三个模型用同一套循环分别训练即可训练顺序没有要求。5.2 用验证集预测结果组装 XGBoost 训练特征三个模型训练完成后冻结权重在验证集上各自输出预测值再用这些预测值构造 XGBoost 的训练特征。import xgboost as xgb def stack_features(models, X_window, raw_last): models : [transformer_model, lstm_model, tcn_model] X_window : 归一化后的滑窗输入 (N, T, 1) raw_last : 原始尺度下窗口最后一个值 (N,) feats [] for m in models: m.eval() with torch.no_grad(): pred m(X_window).cpu().numpy().flatten() feats.append(pred) # 三个深度模型的预测 feats np.column_stack(feats) feats np.column_stack([ feats, raw_last.reshape(-1, 1), # 当前最新值 ]) return feats X_val_tensor torch.tensor(X_val) feats_val stack_features( [transformer_model, lstm_model, tcn_model], X_val_tensor, val_raw[31:-1]) dtrain xgb.DMatrix(feats_train, labely_train) dval xgb.DMatrix(feats_val, labely_val) params { objective: reg:squarederror, max_depth: 4, eta: 0.05, subsample: 0.8, colsample_bytree: 0.8, min_child_weight: 4, } bst xgb.train( params, dtrain, num_boost_round500, evals[(dval, val)], early_stopping_rounds30, verbose_eval50 )参数说明max_depth4控制树复杂度混合预测场景下特征只有 4 到 5 个树太深只会记住验证集的噪声。eta0.05是学习率小学习率配合 500 棵树是比较稳的组合。subsample0.8和colsample_bytree0.8是随机采样比例降低树与树之间的相关性。early_stopping_rounds30表示验证集损失连续 30 轮不下降就停止训练省时间也防止过拟合。5.3 超参速查表与调参顺序下面这张表是四个模型的核心超参起点后续调参按“先深度模型后 XGBoost”的顺序进行不要一开始就同时调所有参数。模型参数推荐初值调整方向Transformerd_model64序列变长时加大到 128Transformernhead4跟随 d_model 同步调整LSTMhidden_size64数据波动复杂时加大到 128LSTMnum_layers2数据量充足时可试 3TCNlevels4感受野要超过主周期XGBoostmax_depth4特征增多时可试 5 或 6XGBoosteta0.05欠拟合时先加树不减学习率调参顺序的常见做法是先用默认参数训练三个深度模型保证验证集上各自不出现明显欠拟合然后固定深度模型权重集中调 XGBoost 的max_depth和min_child_weight最后如果验证集 RMSE 还有明显下降空间再回头微调seq_len和 TCN 的膨胀层数。调参过程中始终以验证集为准测试集只在最终评估时碰一次。6. 落地技巧滚动预测、残差修正与模型导出6.1 递归多步预测的误差累积上面所有设计都是单步预测。要做多步预测最直接的办法是递归式预测把上一步的预测值补到窗口末尾丢掉窗口第一个值继续预测下一步。这个方法实现简单但误差会累积业务上步数越长越偏。下面是一个带残差修正的版本。def recursive_forecast(models, xgb_model, history, horizon, scaler): history : 归一化后的历史窗口长度 seq_len horizon : 要预测的步数 preds [] for _ in range(horizon): win history[-seq_len:].reshape(1, seq_len, 1) win_tensor torch.tensor(win, dtypetorch.float32) feats stack_features(models, win_tensor, win[:, -1, 0]) d xgb.DMatrix(feats) pred_norm xgb_model.predict(d)[0] # 残差修正加一个单调递减的校正项 residual_adj 0.0 if len(preds) 0: residual_adj -0.05 * float(preds[-1] - win[:, -1, 0]) preds.append(pred_norm residual_adj) history np.append(history, pred_norm) preds_raw scaler.inverse_transform( np.array(preds).reshape(-1, 1)).flatten() return preds_raw残差修正的思路是如果上一步预测偏高说明模型当前有正向偏差下一步适度回调一点这里用的是固定系数 0.05实际使用时可以在验证集上调这个系数。它不是严格意义上的统计校正但在业务预演场景里能明显改善多步预测的漂移问题。6.2 模型导出与加载深度模型用torch.save存权重XGBoost 用原生接口存模型文件。加载时注意两个坑一是深度模型必须先实例化再加载state_dict结构要和保存时完全一致二是MinMaxScaler也要保存否则线上预测时无法做归一化和反归一化。# 保存 bst.save_model(xgb_model.json) torch.save(transformer_model.state_dict(), transformer.pt) joblib.dump(scaler, scaler.pkl) # 加载 bst xgb.Booster() bst.load_model(xgb_model.json) transformer_model TransformerBlock(d_model64, nhead4) transformer_model.load_state_dict(torch.load(transformer.pt)) transformer_model.eval()验证加载结果是否正确最简单的方法是拿测试集第一个样本跑一次 predict对比保存前的输出。数值完全一致说明链路正常有一点小误差也正常因为 dropout 在 eval 模式下已经关闭。6.3 一个验证技巧在测试集上分别看单模型和集成的误差集成模型最大的风险是过度依赖某个单一模型导致 XGBoost 形同虚设。验证方法很简单在测试集上分别计算 Transformer、LSTM、TCN、XGBoost 集成四者的 RMSE。正常情况是集成结果最好前三者各有千秋。如果集成结果和某个深度模型几乎一样说明 XGBoost 的特征贡献不够检查一下是否只用了三个模型的预测值而没加原始滞后特征。最后一招是直接用xgb.plot_importance画出特征重要性三个特征重要性得分应该相对均匀如果某一个占了 80% 以上就值得回看那个模型的训练过程是不是出了问题。本文还有配套的精品资源点击获取
返回列表