ARTICLE DETAIL

资讯详情

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

TCN与Transformer混合模型实战:时间序列预测源码与调参指南

TCN与Transformer混合模型实战:时间序列预测源码与调参指南 简介基于TCN与Transformer结合的时间序列预测Python项目源码面向需开展光伏发电功率、风速、风力发电功率或负荷预测等任务的开发者与研究人员。核心包括模型定义与训练脚本借助PyTorch实现并附带CSV示例数据便于直接运行验证压缩包共4个文件含2个Python脚本、1个数据文件及1个预编译缓存文件整体仅33KB轻量易用。已有4796人学习下载项目通过TCN捕获长距离时序依赖、Transformer强化全局特征建模训练脚本封装了数据加载、训练与评估流程可便捷替换自有数据集适配不同场景。整套源码对入门时序建模、对比算法效果或快速产出实验数据均有实用参考价值。 时间序列预测这几年几乎成了深度学习圈子里最“卷”的赛道之一。我最早入坑时用的是 LSTM被它的慢收敛和长序列记忆丢失折磨了很久后来换到纯 Transformer结果在小样本业务数据上又吃了过拟合的亏。直到把 TCN 和 Transformer 串在一起做成混合模型实测在多个数据集上效果和训练速度都明显改善。这篇文章就把我的完整 Python 源码、踩坑记录和调参心得整理出来给正在做时间序列预测的朋友一份可以直接抄作业的参考。1. 为什么是TCN-Transformer被LSTM折腾后的选型反思1.1 时间序列预测三条主流路线做时间序列预测的人一定和这三条路线打过交道RNN/LSTM 系、Transformer 系、CNN/TCN 系。LSTM 系列曾经是标配但用得越久越能感受到它的局限。首先是顺序递归的计算方式当前时刻的输出依赖上一时刻的隐状态无法并行训练速度被死死按住其次是长距离依赖问题虽然 LSTM 通过门控机制缓解了梯度消失但序列超过几百步之后前面的信息该丢还是丢。我经常遇到一种情况训练 loss 曲线很好看但预测曲线总是在波峰位置“差点意思”本质就是它对远距离上下文的记忆力不够。后来 Transformer 成了新宠自注意力机制让每个时间步都能直接和其他所有时间步交互全局建模能力确实是 RNN 系没法比的。但纯 Transformer 也有尴尬的地方——它对数据量要求比较高小样本环境下极其容易过拟合而且位置编码是静态的对时间序列里的局部形态如“连续三根阳线”“突发的尖峰”这类模式并不敏感注意力机制更擅长回答“谁和谁有关系”不擅长回答“局部的形状长什么样”。TCN 我一开始是当配角来看的但认真试过后发现它是被低估的路子。因果卷积天然保证了只用过去信息空洞卷积可以指数级扩大感受野而且卷积是并行计算的训练速度比 LSTM 快一大截。它的短板也明显靠堆层数和膨胀率换感受野覆盖范围有限全局依赖建模能力不如注意力机制。1.2 TCN和Transformer不是替代关系是互补关系如果你把这三条路线当成“竞争对手”那选型确实纠结。但换个思路让 TCN 负责它擅长的局部特征提取让 Transformer 站在 TCN 的输出上做全局关系建模问题就变得顺理成章了。TCN 先对原始序列做波形过滤相当于把一个长序列压缩成有代表性的特征序列把局部模式、尖峰、周期片段的形状这些信息先捞出来Transformer 再把注意力施加在特征序列上进一步捕捉跨时间步的依赖。两个模块各干各的活不会互相抢功。我实测下来这种组合比单独使用任何一个模型都要稳而且因为 TCN 提前做了特征抽象Transformer 的注意力计算量反而更小训练速度比纯 Transformer 还快不少。2. 两个模块各自在干什么核心原理只讲必要的2.1 TCN怎么保证“只用过去”因果卷积与空洞卷积TCN 全称 Temporal Convolutional Network它有两个核心设计。第一个是因果卷积。普通卷积在滑动窗口时会把未来时刻的数据也卷积进当前时刻这在时间序列里是不允许的。TCN 的实现方式是对输入做左侧 padding然后经过卷积后再把右边多余的部分裁掉保证每个输出时刻只看到当前时刻及之前的数据。代码里一般通过一个Chomp1d层来实现这个裁剪逻辑。第二个是空洞卷积。普通卷积要扩大感受野只能增加卷积核大小或层数成本太高。空洞卷积通过在卷积核元素之间插入空洞让卷积核在不增加参数量的情况下覆盖更远的区域。常见做法是每一层的膨胀系数以 2 的指数增长1, 2, 4, 8, 16。这样五层卷积叠下来感受野就能覆盖约 63 个时间步。你可以把 TCN 理解成一套“只看过去、能用很宽的视野扫描局部波形”的过滤器。它输出的每一帧特征都已经融入了当前时刻附近若干个时间步的信息但还没有能力捕获特别遥远的全局关系这个重活就留给 Transformer。2.2 Transformer在时序里的注意力机制与位置编码Transformer 的核心是自注意力。每个时间步生成 Query、Key、Value 三个向量然后通过 Query 和 Key 的点积计算其他所有时间步的权重加权求和得到新的表示。多头注意力相当于让模型同时从多个角度观察序列关系——有的头可能更关注当前波形的局部相关性有的头可能更关注周期性规律。在时间序列场景里位置编码有两种选择固定位置编码三角函数式和可学习位置编码。我个人的经验是对于 TCN-Transformer 这种混合结构输入序列已经携带了较强的局部顺序信息位置编码的作用会被削弱。你可以选择加一层可学习位置编码也可以选择不加直接跑两个方案在小规模数据上差别不大但如果用的是纯 Transformer位置编码则是必须加的。2.3 组合接入点TCN先做特征提取Transformer再做全局建模很多人误以为混合模型是“各跑各的、最后拼结果”实际上更有效的做法是串联原始序列先过 TCN 得到特征序列再把特征序列送入 Transformer Encoder最后取最后一个时间步的隐藏状态接预测头。这种串联方式的逻辑很清晰TCN 相当于给 Transformer 提供了一套“带语义的角色特征”而 Transformer 不再需要从原始波形里从头学习局部模式只需要专注于特征之间的依赖关系。两者分工明确组合起来比单纯堆叠参数更高效。3. 数据流与网络结构设计从(B, L, C)到预测输出3.1 整体数据流我设计并最终采用的模型结构也可以用文字流向描述原始输入形状是(B, L, C)B 是 batch sizeL 是历史窗口长度C 是输入特征维度。首先把维度调成(B, C, L)送入 TCN 卷积网络TCN 输出形状为(B, d_model, L)。然后调回(B, L, d_model)并加一个线性投影层把维度对齐到 Transformer 所需的d_model。接下来进入 Transformer Encoder输出仍然是(B, L, d_model)。取序列最后一个时间步的特征接一个两层 MLP 预测头输出形状为(B, pred_len)也就是未来预测长度。3.2 参数与形状变化对照表数据流阶段形状变化说明原始输入(B, L, C)滑动窗口切出来的样本TCN 输入(B, C, L)维度调转符合 Conv1d 要求TCN 输出(B, d_model, L)局部特征提取完成线性投影(B, L, d_model)对齐 Transformer 输入维度Transformer 输出(B, L, d_model)全局依赖建模完成预测头输出(B, pred_len)最后时间步特征过 MLP3.3 为什么选择这种融合方式我在早期版本里试过“并行融合”TCN 和 Transformer 同时处理原始序列再把两个分支的输出相加。这种方式看起更“均衡”但实际效果并不理想因为两个分支都在做重复的特征提取工作计算量更大而且相加操作会让模型分不清到底该信任哪个分支的特征尺度。串联方式的优势在于接口简单、职责明确。我把 TCN 当作“特征工程层”把 Transformer 当作“逻辑推理层”。调参时也方便特征质量不够就先加深 TCN全局关系没学到就先加 Transformer 层数互不干扰。4. 完整源码逐段拆解可以直接抄走的PyTorch实现4.1 滑动窗口数据构造时间序列建模的第一步是把连续序列切成样本。我用的窗口函数如下import numpy as np def make_windows(data, history128, horizon24): X, y [], [] for i in range(len(data) - history - horizon 1): X.append(data[i: i history]) y.append(data[i history: i history horizon]) return np.array(X), np.array(y)history是模型能看到的历史长度horizon是想要预测的未来长度。注意这里切出来的是三维数据的话data 需先保证是二维(时间步, 特征数)。然后按 8:1:1 的比例切分成训练集、验证集和测试集。4.2 TCN模块代码TCN 的实现网上有很多版本我使用的是基于 Bai 等人论文的经典实现思路包含Chomp1d、TemporalBlock、TCN三个组件import torch import torch.nn as nn from torch.nn.utils import weight_norm class Chomp1d(nn.Module): def __init__(self, chomp_size): super().__init__() self.chomp_size chomp_size def forward(self, x): return x[:, :, :-self.chomp_size].contiguous() class TemporalBlock(nn.Module): def __init__(self, n_inputs, n_outputs, kernel_size, stride, dilation, padding, dropout0.2): super().__init__() self.net nn.Sequential( weight_norm(nn.Conv1d(n_inputs, n_outputs, kernel_size, stridestride, paddingpadding, dilationdilation)), Chomp1d(padding), nn.ReLU(), nn.Dropout(dropout), weight_norm(nn.Conv1d(n_outputs, n_outputs, kernel_size, stridestride, paddingpadding, dilationdilation)), Chomp1d(padding), nn.ReLU(), nn.Dropout(dropout), ) self.downsample nn.Conv1d(n_inputs, n_outputs, 1) if n_inputs ! n_outputs else None self.relu nn.ReLU() def forward(self, x): out self.net(x) res x if self.downsample is None else self.downsample(x) return self.relu(out res) class TCN(nn.Module): def __init__(self, num_inputs, num_channels, kernel_size3, dropout0.2): super().__init__() layers [] num_levels len(num_channels) for i in range(num_levels): in_ch num_inputs if i 0 else num_channels[i - 1] out_ch num_channels[i] dilation 2 ** i padding (kernel_size - 1) * dilation layers.append(TemporalBlock(in_ch, out_ch, kernel_size, stride1, dilationdilation, paddingpadding, dropoutdropout)) self.network nn.Sequential(*layers) def forward(self, x): return self.network(x)TCN 的num_channels我通常设置成[64, 64, 64, 64, 64]五层膨胀系数从 1 到 16。每层的输入输出通道可以相同目的是保持序列长度不变。4.3 Transformer Encoder组装与整体模型把 TCN 和 Transformer 串起来完整模型代码如下class TCNTransformer(nn.Module): def __init__(self, input_size, tcn_channels, d_model, nhead, num_layers, pred_len, dropout0.1): super().__init__() self.tcn TCN(input_size, tcn_channels, kernel_size3, dropoutdropout) self.proj nn.Linear(tcn_channels[-1], d_model) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforwardd_model * 4, dropoutdropout, batch_firstTrue ) self.transformer nn.TransformerEncoder(encoder_layer, num_layersnum_layers) self.head nn.Sequential( nn.Linear(d_model, d_model // 2), nn.GELU(), nn.Linear(d_model // 2, pred_len) ) def forward(self, x): # x: (B, L, C) x x.permute(0, 2, 1) # (B, C, L) x self.tcn(x) # (B, d_model, L) x x.permute(0, 2, 1) # (B, L, d_model) x self.proj(x) # (B, L, d_model) x self.transformer(x) # (B, L, d_model) x x[:, -1, :] # 取最后一个时间步 return self.head(x) # (B, pred_len)这里有两个小细节值得展开说。第一batch_firstTrue要求 PyTorch 1.9 以上版本我使用的 PyTorch 2.0 没问题如果你的环境比较旧记得升级。第二dim_feedforward设为d_model * 4是 Transformer 的常见配置但这个值不是越大越好数据量小的场景下过大的前馈层很容易过拟合。4.4 训练、早停与学习率调度训练部分我用的是 AdamW 优化器配合 HuberLoss 损失函数外加 CosineAnnealingLR 和早停机制from torch.optim.lr_scheduler import CosineAnnealingLR model TCNTransformer( input_sizeX_train.shape[-1], tcn_channels[64, 64, 64, 64, 64], d_model64, nhead4, num_layers2, pred_lenhorizon, dropout0.1 ) criterion nn.HuberLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_maxepochs) best_val_loss float(inf) patience 10 trigger 0 for epoch in range(epochs): model.train() train_loss 0 for xb, yb in train_loader: optimizer.zero_grad() output model(xb) loss criterion(output, yb) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() train_loss loss.item() scheduler.step() model.eval() val_loss 0 with torch.no_grad(): for xb, yb in val_loader: output model(xb) val_loss criterion(output, yb).item() if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), best_model.pth) trigger 0 else: trigger 1 if trigger patience: print(fEarly stopping at epoch {epoch1}) break选 HuberLoss 而不是 MSE 是一个经验决策MSE 对离群点非常敏感时间序列里偶尔出现的尖峰会把梯度拉得很大训练会变得很不稳定HuberLoss 在误差较小的时候表现为平方损失误差大的时候表现为线性损失既保留了 MSE 收敛快的优点又对离群点更友好。5. 实测效果和LSTM纯Transformer对比5.1 实验配置我在两个数据集上做了对比测试一个是公开的 ETTh1 电力变压器温度数据集另一个是我自己业务里的传感器采集数据属于典型的非平稳长周期序列。训练环境是 Python 3.10 PyTorch 2.0单张入门级显卡训练轮数 50早停 patience 10batch size 64。对比模型包括单层 LSTM隐藏单元 64、纯 Transformer Encoder结构和本文模型的 Transformer 部分完全一致以及本文的 TCN-Transformer。历史窗口长度统一设为 128预测长度设为 24。5.2 效果对比与观察模型ETTh1 MAEETTh1 MSE自采数据 MAE自采数据 MSELSTM0.4180.3120.5320.407纯 Transformer0.3860.2710.4910.358TCN-Transformer0.3510.2230.4420.296以上数据是我自己实验环境下的记录不是标准基准但趋势非常明显TCN-Transformer 在两个指标上都优于两个基线模型。更让我在意的是收敛速度。LSTM 大约要 30 个 epoch 以后才有明显下降趋势纯 Transformer 前期波动很大而 TCN-Transformer 通常在 15 到 20 个 epoch 就已进入稳定区域。原因不难理解TCN 用卷积提前把局部特征提取好Transformer 拿到的输入已经不是高维带噪的原始波形而是相对干净的特征序列学习难度自然降低。纯 Transformer 在我自采数据上的测试集表现和验证集表现差距较大过拟合迹象明显TCN-Transformer 由于 TCN 本身带有 dropout 和权值归一化加上参数量比纯 Transformer 更少过拟合程度轻了很多。6. 你可能也会踩的五个坑6.1 感受野不够模型学不到长周期规律这是我踩过的第一个大坑。TCN 的感受野计算公式是R 1 Σ((kernel_size - 1) × dilation)。假设 kernel_size3膨胀系数是 [1, 2, 4, 8, 16]感受野就是1 2×(124816) 63。如果输入序列刚好是 128模型能看到最远的距离大约只有前 63 个时间步那就无法覆盖超过 63 个时间步的周期模式。如果你发现预测曲线“整体平移”且高频抖动很厉害很大概率就是感受野没够到真实周期长度。解决办法增加 TCN 层数、增大膨胀系数序列的终点值或者把 kernel_size 从 3 调整到 5。优先调整感受野带来的收益比盲目堆 Transformer 层数高得多。6.2 归一化时“偷看未来”这个坑特别隐蔽而且后果严重验证集指标非常漂亮一到部署就崩。根因在于数据预处理时用了整个数据集的均值和标准差做 z-score 归一化。测试集的信息在训练阶段就已经被“偷看”到了指标自然虚高。正确做法是只计算训练集的均值和标准差然后把这个统计量应用到验证集和测试集上。任何形式的全局 fit、全局 min-max 缩放在时间序列任务里都属于数据泄漏行为。6.3 注意力头数和d_model必须整除nn.TransformerEncoderLayer内部会把d_model切片成nhead个头如果d_model64而nhead5运行时报错几乎是必然的。这个错误信息虽然明显但在调参过程中很容易手滑写错我建议在模型初始化函数里加一行断言assert d_model % nhead 0, d_model must be divisible by nhead6.4 预测头直接接大线性层容易过拟合我最初版本里预测头就是从d_model直接线性映射到pred_len在自采数据上表现不太稳定。后来改成带 bottleneck 的 MLPd_model - d_model//2 - pred_len中间加 GELU 激活过拟合问题明显缓解最终预测也更平滑。你可以把这个结构理解为一种隐式的特征压缩正则化。6.5 TCN梯度爆炸与学习率TCN 使用weight_norm对卷积核做归一化整体训练稳定性比普通卷积好但学习率设太大仍然可能炸。我最早用lr3e-3第 5 个 epoch loss 直接变成 NaN。把学习率降到1e-3并配合clip_grad_norm_(max_norm1.0)后整个训练过程再也没有出现过这个问题。如果你也想试这个组合建议先跑通最小配置d_model64、nhead4、num_layers2、TCN 五层、窗口长度 128。这套参数在大多数中小规模数据集上都能得到一个不错的起点之后再根据数据特性调整感受野和模型宽度。我自己的体会是混合模型的价值不在于结构有多炫而是让两个模块各干各擅长的活最终换来的是更稳的训练过程和更低的预测误差。先小跑通再谈优化这条路走起来最省心。本文还有配套的精品资源点击获取
返回列表