资讯动态

Transformer+LSTM+TCN+XGBoost融合模型的时间序列预测实战

发布时间:2026/10/5 12:36:26 来源:尧图企业网站定制
简介面向时间序列预测的Python混合模型源码包融合Transformer、LSTM、TCN与XGBoost四种算法适合有一定深度学习基础、希望突破单一模型精度瓶颈或研究模型集成的开发者。包内共5个文件含2个csv演示数据、1个Python主程序run_hybrid_forecast.py、1个Shell启动脚本及1个Markdown项目说明压缩包仅12KB轻量易部署可直接运行体验。目前已有151人学习浏览具备一定参考热度。源码并非简单堆叠模型而是结合深度时序特征提取与梯度提升回归能力附带演示数据可快速验证完整流程项目说明则梳理了各模型的分工与调用方式适合作为混合预测方案的学习模板或二次开发起点。1. 融合四种模型的时序预测TransformerLSTMTCNXGBoost这套组合好在哪时间序列预测这几年有个明显趋势单一模型做到头就是成绩平平混合模型却经常在真实场景里拿更好的离线指标。这份源码包走的正是这条路——把 Transformer、LSTM、TCN 三个深度模型融合成一个编码器再让 XGBoost 对深度模型学不干净的残差做二次回归。四个模型各有各的视野Transformer 抓长程依赖LSTM 抓序列顺序TCN 控制感受野XGBoost 负责把漏掉的非线性补回来。拆完代码我发现结构不复杂但把特征提取和误差补位分得很清楚比单纯堆模型要干净。适合手里有价格、销量、流量类历史数据想把预测精度再往前顶一截的 Python 从业者。2. 数据预处理与演示数据从CSV到监督学习样本的完整流程2.1 读入数据与时间对齐实际拆这份资源时我是先跑run_hybrid_forecast.py看能不能出图再倒回去读 README 和两个演示 CSV。项目里放了price_data_演示.csv和ahead_data_演示.csv从命名看一个是价格类历史序列一个是要对齐的前置数据。工程里常见的主序列一套字段、外生变量一套字段所以我习惯先把列名统一再检查缺失和重复时间戳。import pandas as pd import numpy as np def load_series(path): df pd.read_csv(path, encodingutf-8) df.columns [date, value] if df.shape[1] 2 else df.columns df[date] pd.to_datetime(df[date]) df df.sort_values(date).reset_index(dropTrue) df df[~df[date].duplicated(keeplast)].reset_index(dropTrue) df[value] df[value].astype(np.float32) return df第3行处理列名对齐从 Excel 导出的 CSV 列名经常是中文或带空格统一改掉之后下游逻辑不被字段名绑架。第5行处理重复时间戳duplicated(keeplast)保留同一时刻最后一条记录重复时间戳是时间序列最容易翻车的地方一旦出现后续滑窗样本顺序就乱了指标还特别好看。读入后我会先打印df.head()和df[value].isna().sum()确认没有解析成字符串的数值列和整列 NaN。如果ahead_data_演示.csv里是外生特征比如天气、活动标记这类辅助变量处理逻辑和主序列一样先解析日期再按日期合并回主表。我一般用左连接main_df.merge(ahead_df, ondate, howleft)外生变量缺失时不会把主序列整行删掉。缺失位置用前向填充或线性插值别用 0 填充除非那个特征语义上允许 0。还要注意编码问题很多国产 Excel 导出的 CSV 是 GBK 编码read_csv会抛 UnicodeDecodeError这时把encoding换成gbk再读一次我自己被这个坑过不止一次。2.2 滑窗构造监督样本滑窗是整个工程的地基窗口长度seq_len决定模型一次看多少历史步预测长度horizon决定一次往前想多远。这套源码里深度模型的输入和输出共用同一个窗口函数所以先把窗口函数说透。def create_sequences(values, seq_len, horizon): X, y [], [] for i in range(len(values) - seq_len - horizon 1): X.append(values[i:i seq_len]) y.append(values[i seq_len:i seq_len horizon].squeeze(-1)) X np.array(X, dtypenp.float32) y np.array(y, dtypenp.float32) return X, y第3行里当前样本是第i到iseq_len个时刻标签是紧跟在后面的horizon个时刻前后不重叠避免标签里混进输入已经见过的值。常见错误是标签写成values[i seq_len - 1]输入包含同一个值模型就学会复制上一步而不是预测未来。建议先写死seq_len24, horizon1打印 X 和 y 的形状确认X.shape (样本数, 24)、y.shape (样本数, 1)再调参数。这个函数生成的是强重叠样本相邻样本只平移一个时间步训练时模型会反复看到几乎一样的历史段。数据量小时是优点但也容易放大过拟合所以后面必须配 dropout 和早停。如果数据是多变量比如主序列加外生特征就把多列拼成一个二维窗口标签仍只取目标列create_sequences的输入变成二维数组输出 X 就是三维(样本数, seq_len, 特征数)Transformer 的batch_firstTrue正好接这种形状。2.3 归一化与数据划分一条顺序原则混合模型里 Transformer 和 LSTM 都依赖激活函数的稳定区间不归一化直接训第一轮 Loss 很可能直接变成 inf。这份资源用 MinMaxScaler 把数值压到 [0,1]最后再用逆变换把预测还原成原始量纲。关键在 fit 的位置。from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) train_values scaler.fit_transform(train_values.reshape(-1, 1)) valid_values scaler.transform(valid_values.reshape(-1, 1)) test_values scaler.transform(test_values.reshape(-1, 1))fit_transform只用在训练集验证集和测试集只能transform。原因很直接scaler 存的是训练集的 min 和 max测试集用它归一化相当于假设测试分布的边界在训练时已经见过如果拿全量数据 fit等于模型在训练时就知道了未来区间的数值范围验证指标会比实际上线乐观。这是时间序列项目里最典型的好看指标上线翻车来源之一。时间序列的划分还多一条铁律不能随机打乱。随机 shuffle 对时序数据直接破坏序列顺序模型在验证集上看到的其实是训练集旁边的未来样本。我用顺序切分def sequential_split(X, y, test_ratio0.2): n len(X) split int(n * (1 - test_ratio)) return X[:split], X[split:], y[:split], y[split:]按时间先后切分切分点之前是训练之后是验证/测试。数据量充足的话我会再留出一段独立的验证集做早停测试集只在最后跑一次避免手贱反复调参把测试集调进训练过程。3. 混合模型架构拆解粗粒度时序特征加残差学习的两种实现3.1 三个深度模型的分工Transformer 用标准编码器位置编码、多头自注意力、前馈网络。注意力可以跨整个序列做信息交互对季节性周期这类长程依赖抓得准但对局部趋势的刻画不如卷积和循环结构细腻序列长了计算量也涨得快。LSTM 按时间步顺序处理隐状态携带前一刻的上下文对短期转折敏感。工程上常用单层或双层 LSTM取最后一个时间步的 hidden state 作为序列表示。虽然模型老中小规模数据上依然是最稳的基线和 Transformer 形成互补。TCN 用因果膨胀卷积撑开感受野避免 RNN 逐时间步递推的串行开销训练可以并行。核大小和膨胀率共同决定感受野两层就能覆盖很长历史。下面是这个项目的分工视角组件擅长输出聚合方式弱点Transformer长程依赖、周期模式序列均值局部趋势粗糙LSTM短中期转折、顺序依赖最后时间步长序列梯度衰减TCN局部模式、并行训练通道均值超长依赖受限XGBoost深度模型残差的非线性补位直接用残差需要深度模型特征先就位三种深度模型输出拼接后接一个全连接映射到horizon个预测值。XGBoost 不参与第一步训练它吃深度模型剩下的残差这是融合的关键。3.2 PyTorch 实现位置编码、TCN 与融合层代码按分层方式写方便逐段调试。位置编码直接手写一个不依赖 PyTorch 内部版本在 CPU 上跑演示数据也更可控。class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len1000): super().__init__() pe torch.zeros(max_len, d_model) pos torch.arange(0, max_len, dtypetorch.float).unsqueeze(1) div_term torch.exp(torch.arange(0, d_model, 2).float() * (-np.log(10000.0) / d_model)) pe[:, 0::2] torch.sin(pos * div_term) pe[:, 1::2] torch.cos(pos * div_term) self.register_buffer(pe, pe.unsqueeze(0)) def forward(self, x): return x self.pe[:, :x.size(1)]div_term里的 10000 控制位置编码的正弦周期范围数值越大低频分量越多实际很少动它。register_buffer把位置编码挂到模型上随模型保存和加载不会出现在优化器更新里。如果seq_len超过 1000把max_len调大就行注意位置编码和输入要拼接。TCN 的核心是因果卷积和残差连接。因果卷积要求当前时刻输出只依赖当前及之前的输入实现时给输入左侧 pad 而不是右侧 pad。class TCNBlock(nn.Module): def __init__(self, in_ch, out_ch, kernel_size3, dilation1, dropout0.2): super().__init__() self.padding (kernel_size - 1) * dilation self.conv1 nn.Conv1d(in_ch, out_ch, kernel_size, dilationdilation, paddingself.padding) self.conv2 nn.Conv1d(out_ch, out_ch, kernel_size, dilationdilation, paddingself.padding) self.relu nn.ReLU() self.dropout nn.Dropout(dropout) def forward(self, x): h self.dropout(self.relu(self.conv1(x))) h self.dropout(self.relu(self.conv2(h))) return h, h[:, :, :-self.padding]第8行返回两个值全尺寸特征给残差支路截掉右侧 padding 的因果部分才是真正输出保证看到的是左填的历史信息。实际堆叠时 dilation 从 1、2、4 翻倍感受野指数增长几层就能覆盖很长的历史。这里有个值得养成习惯的细节TCN 的降采样我一般不用 stride而是靠 dilation 控制视野避免过度压缩时间维度。融合层把三个模型表示拼在一起class HybridEncoder(nn.Module): def __init__(self, seq_len, d_model, nhead, num_layers, lstm_hidden, tcn_channels, horizon): super().__init__() self.pos_enc PositionalEncoding(d_model) encoder_layer nn.TransformerEncoderLayer(d_modeld_model, nheadnhead, batch_firstTrue) self.transformer nn.TransformerEncoder(encoder_layer, num_layersnum_layers) self.proj_in nn.Linear(1, d_model) self.lstm nn.LSTM(input_sized_model, hidden_sizelstm_hidden, batch_firstTrue) self.tcn TCNStack(d_model, tcn_channels) self.head nn.Sequential( nn.Linear(d_model lstm_hidden tcn_channels[-1], 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, horizon) ) def forward(self, x): h self.proj_in(x) h self.pos_enc(h) t_out self.transformer(h).mean(dim1) l_out, _ self.lstm(h) l_out l_out[:, -1, :] c_out self.tcn(h.transpose(1, 2)).mean(dim2) return self.head(torch.cat([t_out, l_out, c_out], dim-1))注意 forward 里的三种聚合方式Transformer 输出取序列均值LSTM 取最后时间步TCN 输出取通道均值。三种聚合保留的证据不一样全局证据、末尾趋势、局部模式拼接后互不冲突。d_model lstm_hidden tcn_channels[-1]就是融合特征总维度改超参时保持对齐否则nn.Linear会直接报维度不匹配。实验时想验证某个分支的价值把对应聚合段设成 0 或直接删掉拼接段就能对照。3.3 残差学习把 XGBoost 接在深度模型后面深度模型输出连续预测但对极端波动和尾部非线性拟合有限。XGBoost 在这里不直接预测原始值而是学习残差——「真实值减深度模型预测值」的部分。分两步先在训练集上把深度模型的中间特征和预测都存下来再训练 XGBoost 拟合残差预测时深度模型输出加 XGBoost 的残差预测得到最终值。import xgboost as xgb train_pred hybrid_model.predict(X_train_tensor).detach().numpy() residual y_train_true - train_pred xgb_model xgb.XGBRegressor( n_estimators300, max_depth5, learning_rate0.05, subsample0.8, colsample_bytree0.8, random_state42 ) xgb_model.fit(X_train_raw, residual) final_pred train_pred xgb_model.predict(X_test_raw)subsample控制每轮样本采样比例colsample_bytree控制特征列采样比例都是防过拟合的参数。learning_rate从 0.05 起步残差分布杂乱就先降到 0.01 并增大n_estimators。另一个常用技巧把深度模型的融合特征直接拼进 XGBoost 的特征矩阵相当于让树模型看到人工特征通常比只用残差更稳代价是特征矩阵变大、训练变慢。残差学习的适用条件值得说透它适合深度模型已经收敛到 baseline、但误差里还有明显结构的场景。如果残差基本是白噪声XGBoost 只会徒增方差这时候不如直接输出深度模型的预测。判断方法很简单——训练完深度模型后画一张残差自相关图看滞后几阶还有显著相关性有再上 XGBoost。4. 训练配置与调参路线run_hybrid_forecast.py 的代码就这么看4.1 入口脚本的执行流程run_hybrid_forecast.py是这套源码的启动入口。整体流程解析参数 → 加载 CSV → 滑窗构样本 → 顺序切分 → 初始化模型 → 训练深度模型 → 导出特征 → 训练 XGBoost → 预测 → 反归一化 → 画图评估。源码包里还有个start.sh一般就是一行 python 调用指定数据和超参真正要换的参数都集中在命令行参数上。python run_hybrid_forecast.py \ --data price_data_演示.csv \ --seq_len 24 \ --horizon 3 \ --epochs 50 \ --lr 3e-4 \ --batch_size 32 \ --patience 10 \ --seed 42这个命令从下到上顺着读就是一份完整的超参快照。seq_len24 表示模型看最近 24 个时刻horizon3 表示一次预测未来 3 个时刻patience10 表示验证 Loss 连续 10 个 epoch 不下降就早停。保存实验记录时我会把命令行参数原样贴进实验笔记模型调参最怕的就是忘了上次跑的是哪组参数。训练主体是标准 PyTorch 循环核心骨架大致是这样的写法for epoch in range(args.epochs): model.train() for batch_x, batch_y in train_loader: batch_x, batch_y batch_x.to(device), batch_y.to(device) optimizer.zero_grad() loss criterion(model(batch_x), batch_y) loss.backward() clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() valid_loss evaluate(model, valid_loader, device) if valid_loss best_loss: best_loss valid_loss torch.save(model.state_dict(), best_model.pt) if early_stop(valid_loss): breakclip_grad_norm_设max_norm1.0是为了防止梯度爆炸Transformer 和 LSTM 堆在一起时这步必须有不然前几个 epoch 就可能出现 NaN。best_model.pt存的是验证集最优权重最后预测时必须加载这份权重而不是最后一个 epoch 的权重早停后的权重往往已经过拟合了。XGBoost 部分在深度模型训练完、特征导出后启动和深度模型训两条线。4.2 关键超参数表与调整方向参数常见起始值调整方向seq_len24日数据调到 90168horizon3单步精度远高于多步d_model32/64数据量小用 32nhead4/8必须能整除 d_modelnum_layers1/2加深收益不如加 LSTM 宽度lstm_hidden32/64小数据 32 足够tcn_channels[16, 32, 64]逐层翻倍lr3e-4过高直接发散batch_size32CPU 上降到 16 反而更快dropout0.2/0.3过拟合上调到 0.5weight_decay1e-4和 dropout 搭配xgb_depth5残差学习用 36这个表是项目跑顺手后的默认值不是最优值。最该优先调的是seq_len、horizon、lr三个seq_len决定视野horizon决定任务难度lr决定能不能收敛。其他参数先保持默认记一组 baseline 指标后每改一个参数跑一次对照着调不要一次改三个参数否则哪个起作用根本说不清。训练日志我会重点看三行第一个 epoch 的 train loss、第 20 个 epoch 的 train/valid loss、早停时的 valid loss。第一个 epoch loss 如果比随机瞎猜还差一个数量级先查数据归一化和 lr第 20 个 epoch 发现 train loss 和 valid loss 差距拉大dropout 和 weight_decay 往上加早停位置过早说明模型还没进入拟合区把 lr 调小、patience 加长。4.3 CPU、GPU 环境下的运行注意事项这套代码 CPU 上完全能跑前提是batch_size不要太大seq_len尽量别超过 128。CPU 上推理时用torch.no_grad()关掉梯度计算能省不少内存和耗时。model.eval() with torch.no_grad(): test_pred model(X_test_tensor).numpy()有 CUDA 时先建 device 再移动模型和数据device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) X_train_tensor X_train_tensor.to(device)张量和模型必须同一设备否则直接 RuntimeError。我自己习惯加torch.backends.cudnn.benchmark True输入形状固定时自动选最优卷积算法能省不少时间。Apple Silicon 的 MPS 后端也可以跑装的是带 MPS 支持的 PyTorch 后把device设成mps不过注意某些老算子对 MPS 支持不全遇到就退回 CPU 那一路。不管什么设备固定随机种子都值得做def setup_seed(seed): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)seed 固定后同一份数据跑两次的指标差异应在千分位以内。如果两次差异超过 5%先查数据里有没有未填充的 NaN再查滑窗样本的边界是不是切斜了这两个原因比模型随机性更容易被忽略。5. 常见问题与避坑时间序列预测的五个典型错误时间序列混合模型翻车十次有八次不是模型结构的问题而是数据在进模型之前就被污染了。下面五条是我按实际频率排的每一条都踩过后面接的是我现在固定用的排查姿势。5.1 归一化统计量算到全局数据上现象训练 Loss 正常下降验证指标很漂亮模型一到线上就明显跑偏峰值位置和真实值错开一大截。原因scaler.fit_transform套在了包含测试集的完整数据上MinMaxScaler 记录了全量数据的 min 和 max测试集范围在训练阶段就暴露给了模型属于标准的数据泄漏。解决训练集上fit_transform验证集和测试集只用transform。更严格的做法是先在时间上切分再对训练切片算归一化统计量把这个顺序写进数据流水线就不会再犯。5.2 标签错位滑窗丢 step 导致泄漏现象验证 RMSE 低得离谱把预测曲线拉出来看整条曲线是真实曲线右移一个时间步。原因滑窗实现时标签取成了values[i seq_len - 1]输入窗口里包含同一个值模型学的是「复制上一步」而不是预测未来。解决标签从输入结束后的下一步开始取保证输入和标签不重叠。# 错误写法标签和输入最后一个时间步重叠 # y.append(values[i seq_len - 1]) # 正确写法从输入结束后的下一步开始 y.append(values[i seq_len:i seq_len horizon])修复后 RMSE 通常会变差这是好事说明之前那个漂亮指标本来就是假的。5.3 随机划分验证集现象验证集上表现良好按时间外推或者跨周期预测时崩掉。原因用了train_test_split(shuffleTrue)验证样本在时间上混入了训练区间等于模型见过它后面的序列。解决顺序切分或者用 TimeSeriesSplit 做交叉验证gap参数至少等于horizon避免训练集最后几步和验证集开头重叠。交叉验证跑得慢但能给出比单次划分更可信的误差估计数据量小的场景值得多花这点时间。注意TimeSeriesSplit 的gap参数指的是每次划分时训练集末尾和验证集头部之间必须空出的样本数量数值上至少等于horizon否则滑窗样本会跨过切分边界。5.4 深度模型不收敛现象Loss 停在某个固定值附近权重更新不起作用。原因最常见是三个——输入没归一化、学习率过大、梯度出现 NaN。Transformer 对 learning rate 尤其敏感固定大 lr 很容易在前几个 epoch 训练就冲飞。解决加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)先跑 5 个 epoch 观察 Loss 是否在下降不降就按 1/10 递减 lr 试同时检查输入张量有没有 NaN。还有个小技巧把模型第一层的输出打印出来看数值范围如果出现几百上千的峰值问题基本在输入侧。5.5 XGBoost 特征与标签泄漏现象XGBoost 训练集拟合完美测试集一塌糊涂。原因把深度模型在验证集上的预测值也当作特征喂给了 XGBoost 训练验证信息渗透进了训练阶段。解决XGBoost 的中间特征、残差、标签全部来自训练集切片测试集完全隔离。我会用一个split_idx贯穿全流程深度模型特征导出、XGBoost 特征矩阵、残差计算共用同一个切分索引从根源上避免两边各切各的。还有一个习惯是训练完 XGBoost 后强制检查特征重要度和训练/验证容差两者差距过大就说明训练流程有问题。6. 滚动预测与验证流程直接多步预测是一次性输出horizon个未来值速度快、误差不累积但远期精度衰减快。工程上更常做滚动预测——每次预测 1 步把预测值拼回输入序列再预测下一步。def rolling_forecast(model, init_seq, seq_len, steps): history init_seq.copy() preds [] for _ in range(steps): with torch.no_grad(): out model(torch.tensor(history[-seq_len:], dtypetorch.float32).unsqueeze(0)) p out[0, 0].item() preds.append(p) history np.append(history, p) return np.array(preds)第4行每次只取最近seq_len个值送入模型预测出一大步后拼回history下一步再取最近seq_len个注意这里预测值被当成已知历史使用误差会沿序列累积滚动步数越多越偏。实际做对比时我会同时跑直接多步和滚动两种方式看业务场景里哪种更抗扰动。预测输出还要反归一化再算指标否则 RMSE 小得没有业务含义而且无法和原始量纲对齐。pred_raw scaler.inverse_transform(pred.reshape(-1, 1)) true_raw scaler.inverse_transform(true.reshape(-1, 1)) mse np.mean((pred_raw - true_raw) ** 2) rmse np.sqrt(mse) mae np.mean(np.abs(pred_raw - true_raw)) mape np.mean(np.abs((pred_raw - true_raw) / (true_raw 1e-8))) * 100MAPE 分母加1e-8防止真实值为 0 时除零。价格类数据还要警惕一个现象真实值接近 0 时 MAPE 瞬间爆掉这时候用对称 MAPE 或直接看 MAE 更稳。画图时把真实值和预测值画在一条时间线上一眼能分辨三种常见故障整体右移是标签错位局部偏大是未收敛远期扩散是滚动误差累积三种情况对应完全不同的调参方向。这套流程跑完等于建立了一份可复现的 baseline。从那以后我每次跑混合模型都会强制走这四步先单独看深度模型在验证集上的误差曲线再看 XGBoost 补回来的残差有没有结构接着检查滚动预测前 20 步有没有系统性偏置最后才把完整预测曲线交给业务。整个检查十分钟出头能砍掉我一半的无效调参次数希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑