首页 / 资讯中心 / 文章详情

PatchTST全解析:用Patch化与通道独立重构时序Transformer

PatchTST全解析:用Patch化与通道独立重构时序Transformer ★ FEATURED ARTICLE
在Transformer架构横扫NLP和CV领域之后时序预测这个方向也迎来了一波Transformer改造热潮。但早期工作基本都是照搬NLP的做法把每个时间点当成一个token送进注意力机制在长序列预测场景下效果不尽如人意甚至在某些数据集上被简单的线性模型吊打。PatchTSTPatch Time series Transformer的出现算是把Transformer在时序领域的应用拉回了正轨核心改进就两个点一是把时间序列切成patch再送进模型二是通道独立Channel Independence这两个看似简单的改动却带来了远超之前一系列花哨变体的效果。如果你是做时序预测的不管是电力负荷、金融序列还是工业传感器数据这篇文章值得花十分钟仔细看完。我会从头拆解PatchTST的设计思路、核心机制附上完整的PyTorch实现流程和调参经验最后聊聊我在实际项目中踩过的坑以及这种patch化思路对其他时序模型的启发。1. 设计思路拆解为什么之前的时序Transformer普遍不给力在PatchTST之前主流时序Transformer的做法基本都是沿用NLP那套把序列的每一个时间点做embedding后当做一个token送入self-attention。这种做法在短序列上看起来还行但一旦把预测长度拉到192、336甚至720这个级别问题就全暴露出来了。第一个问题是计算复杂度和内存开销。注意力机制的计算复杂度是O(N²)的N是序列长度。预测192步往往需要喂入至少512个历史时间点512个token做两两注意力计算这个开销已经不小了。如果输入序列再长一些训练速度会急剧下降显存也扛不住。第二个问题是每个时间点作为token带来的语义碎片化。单看某一个时间点的数值其实很难说有什么语义信息它只是连续过程中的一帧。而注意力机制最擅长的是捕捉token之间的语义相关性单个时间点的信息量太小导致注意力分布高度波动噪声很大。这个问题在NLP领域早有前车之鉴BERT里一个token是一个词或子词天然带有语义把同样的逻辑硬套到单时间点上本质上是错位的。第三个问题是过平滑现象。我之前在实验里观察到一个明显现象预测序列越长Transformer输出的曲线就越趋近于一个均值回归的平缓曲线高频细节全部丢失。原因在于注意力权重在长序列上趋于均匀分布模型很难精确聚焦到真正关键的局部模式。PatchTST针对这三个问题给出的解决方案非常直接既然单点token不行那就把数個连续时间点打包成一个patch作为一个token。这样一来token数量直接降为原来的1/PP是patch长度计算量显著下降同时每个token携带了局部时间段的信息语义密度比单点高了不止一个量级注意力也能学到有意义的依赖关系。通道独立这个设计同样值得一提。之前很多模型会把多变量序列的各个通道在embedding阶段就混合在一起PatchTST反其道而行之每个通道单独走一遍完整的模型流程。我们一开始觉得这会不会丢失通道间相关性实验结果却表明通道独立不仅没让效果变差反而在很多数据集上稳定性更好。背后的原因也不难理解通道独立等于把多变量预测问题拆成了若干个单变量预测问题并行求解每个通道能够按自己的分布特性独立建模归一化参数和注意力模式避免了不同通道量纲差异带来的干扰。而通道间的关联信息模型依然能够通过最终输出的多通道组合间接保留。2. 核心机制解析patching和channel independent是怎么起作用的2.1 patching操作把一个时间窗口压缩成一个tokenpatching的实现思路非常朴素就是把输入序列按固定窗口长度P切分成若干重叠或不重叠的patch。假设输入序列长度为Lpatch长度为P步长为S允许相邻patch之间有重叠或间隔那么patch数量N可以按这个公式算出来N floor((L - P) / S) 1举例来说L512P16S16不重叠切分得到N32个patch。token数量从512直接降到32注意力计算的复杂度从512² 262144降到32² 1024计算量缩减了256倍。当然实际训练里我们不会用这么极端的参数但数量级的差异是实实在在的。每个patch在送入Transformer的Encoder之前需要通过一个线性映射层投影到模型的d_model维。这一步其实就是对一个长度为P的窗口做一次线性变换相当于一个“局部特征提取器”。我在实现时习惯用Conv1d来做这个映射因为它在处理这种固定窗口变换时效率更高效果也等价。卷积核宽度就是patch长度P输入通道是1输出通道是d_modelstride设成S。实验对比下来patch长度P的取值对最终效果的影响相当大。P太小比如2到4patch化带来的语义增强效果就不明显计算量的优势也体现不出来P太大比如64以上单个patch内可能包含了多种不同的模式反而模糊了局部特征。我在多个数据集上测试P取16到32之间是比较稳妥的选择。后面我会单独用一个章节讲参数调优。2.2 Channel Independence一对多的训练策略channel independent的含义是输入数据的每个通道独立地经过同一个Transformer Encoder共享一套模型参数但各自的输入归一化统计量是独立的。这相当于把N个通道分别做N次单变量预测然后并行拼接结果。这个设计在训练时有一个很实用的技巧批量维度和通道维度可以合并把(N, C, L)形状的数据重排成(N×C, 1, L)一次性把所有通道都过一遍模型。这样一来无论变量通道数是多少模型的输入输出形状始终是单通道的代码实现非常干净。我在实际项目里用这个方法同时跑了二三十个通道的预测训练速度和单通道几乎没差别因为计算量是由批量大小决定的合并通道等于变相扩大了batch。这里有个细节值得注意每条通道做归一化的时候是用这条通道历史数据的均值和方差而不是用一个全局统计量。这个差异很重要因为不同通道的量纲和波动范围可能差好多个数量级比如某个通道是温度数值在十几到三十几之间另一个通道是转速可能上千如果共用归一化参数后者的预测精度会严重恶化。2.3 位置编码patching之后还需要吗怎么加PatchTST在patch序列上使用的是可学习的位置编码learnable positional encoding而不是Transformer原始论文里的正弦余弦固定编码。原因也很直接patch序列的长度一般比较短可学习的位置向量在这种规模下完全可以通过训练学出来效果比固定公式更好。位置编码的添加时机是在patch embedding之后、进入Transformer Encoder之前。具体操作是初始化一个(N, d_model)的可学习参数矩阵加到patch embedding的输出上。我试过把位置编码换成相对位置编码或者直接不加位置编码效果都有明显下降。这说明patch之间的顺序信息对这个任务是不可或缺的模型需要知道不同patch的先后关系才能捕捉时序模式。2.4 Encoder的输出怎么变成预测结果Encoder部分就是标准的Transformer Encoder结构我沿用的配置是2层、8个注意力头、d_model为128、前馈网络维度512。输出的是每个patch对应的表征序列形状为(N, d_model)。最后的预测头是一个全连接层输入是Encoder输出的最后一个patch的表征输出维度是预测长度H。这里不是把全部patch的输出都接进来而是只取了最后一个patch的表征接预测头。为什么这样设计因为最后一个patch距离预测区间最近包含的信息对未来的推断最直接相关。我在项目里试过把全部patch的输出拼接后接一个大MLP效果反而变差了可能是有效信息被前面那些相对久远的patch表征稀释了。3. 实操全过程从数据准备到模型训练每一步都给你捋清楚3.1 数据处理与数据集划分PatchTST的数据处理比很多模型都要简单直接核心就两步按通道独立归一化然后构造滑窗样本。我这里的实验场景是某工业设备的关键参数预测传感器采集了温度、压力、振动、电流等共6个通道的数据每分钟记录一条一共采集了约3万条样本。训练集、验证集、测试集按7:1:2的比例切分这里有个容易忽视的细节时序数据切分不能像CV那样随机打乱必须按时间顺序切否则会造成未来信息泄露验证集和测试集的评估结果会虚高。滑窗构造样本时我把输入长度L设为512预测长度H设为96。每个样本由一个长度为L的历史窗口和紧邻的H步未来值组成。窗口滑动步长我习惯设为128这样相邻样本之间有重叠能有效扩充训练样本数量。最终得到大约两千多个训练样本。归一化是对输入的历史窗口单独做的计算窗口内的均值和标准差然后做标准化。预测目标也就是未来那H步必须用同一个窗口统计出来的均值方差做归一化不能重新计算未来窗口的统计量否则等于引入了未来信息。3.2 模型结构关键代码实现直接用PyTorch复现PatchTST的完整代码也不过一百多行我把核心部分贴出来供参考。首先是patch embedding层class PatchEmbedding(nn.Module): def __init__(self, patch_len, stride, d_model): super().__init__() self.patch_len patch_len self.stride stride # 通过一维卷积完成patch的线性映射 self.proj nn.Conv1d(1, d_model, kernel_sizepatch_len, stridestride, biasFalse) # 可学习的位置编码 self.pos_embed nn.Parameter(torch.randn(1, 1, d_model)) def forward(self, x): # x: (B, 1, L) x self.proj(x) # (B, d_model, N) x x.permute(0, 2, 1) # (B, N, d_model) B, N, _ x.shape pos self.pos_embed[:, :N, :].expand(B, -1, -1) return x posConv1d的输入通道为什么是1而不是通道数因为channel independent的策略下每个通道是独立过模型的所以进入patch embedding的数据形状永远是(B, 1, L)。这也是整个模型设计中最巧妙的地方之一单通道输入让整个模型天然适配任意数量通道的输入数据。然后是Transformer Encoder部分我直接用了PyTorch内置的TransformerEncoderLayer设置batch_firstTrue让输入输出形状都是(B, N, d_model)class TransEncoder(nn.Module): def __init__(self, d_model, n_heads, n_layers, d_ff, dropout0.1): super().__init__() layer nn.TransformerEncoderLayer( d_modeld_model, nheadn_heads, dim_feedforwardd_ff, dropoutdropout, batch_firstTrue, activationgelu ) self.encoder nn.TransformerEncoder(layer, num_layersn_layers) def forward(self, x): return self.encoder(x)预测头就简单了一个线性层把最后一层Encoder输出的最后一个patch位置映射到预测长度Hhead nn.Linear(d_model, pred_len)。整体调用流程是对一个样本逐个通道过PatchEmbedding - TransEncoder - head输出(B, H)循环所有通道后拼接成(B, C, H)。3.3 训练超参配置和结果分析我整理了一份实际项目里效果不错的超参组合直接抄作业问题不大输入长度 L 512patch长度 P 16patch步长 S 16不重叠patch数量N 32预测长度 H 96d_model 128注意力头数 8Encoder层数 2前馈网络维度 d_ff 512dropout 0.1batch size 64学习率 1e-3使用余弦退火调度损失函数 MSE优化器 AdamW训练轮数 60在这个配置下模型在验证集上的MSE能收敛到0.013左右测试集上0.016。作为对比我之前用同场景的数据跑过直接线性层的前向模型效果差不少。PatchTST在长序列预测上的优势非常明显特别是在预测曲线能捕捉到周期性的动态变化而不是退化成一条平滑的均值线。训练过程的监控我重点关注两个东西一是损失曲线是否平滑下降如果出现剧烈震荡十有八九是学习率设大了二是验证集MSE和测试集MSE的差距如果差距持续拉大说明过拟合风险在上升这时减小dropout或增大训练数据的窗口滑动步长都能缓解。3.4 通道独立模式的完整训练流程channel independent模式下的训练代码有个关键的重排操作很多人在复现时容易卡在这里。输入原始数据形状是(B, C, L)因为每个通道独立过模型代码里第一步就是做维度重排# x: (B, C, L) B, C, L x.shape x x.permute(0, 1, 2).reshape(B * C, 1, L) # 经过模型 out model(x) # (B*C, H) out out.reshape(B, C, H)重排的本质是“批量维度和通道维度交换角色”把C个通道当成B×C个独立样本处理。好处前面说过是能充分利用GPU并行能力坏处是如果C特别大等于隐式变相增大batch size对学习率会比较敏感。我在处理24通道数据时学习率从1e-3降到3e-4稳定性立刻上来了。这一点要特别留意。4. 横向对比PatchTST相对几种主流方法的真实表现既然这篇文章主题是PatchTST有必要把它跟之前主流的几类模型放在同一数据集上做个公平对比。公平的含义是输入输出设置完全一致都用历史512步预测未来96步训练轮数和优化策略相同只替换模型结构本身。我把对比结果整理成了下面这张表模型测试集MSE测试集MAE推理耗时毫秒/样本备注线性D模型0.0380.1510.8可视为强基线N-BEATS0.0310.1341.2纯MLP堆叠Informer0.0440.1676.5稀疏注意力Autoformer0.0360.1525.9自相关机制PatchTST0.0160.0893.1本文实现几个值得注意的结论第一之前几年大热的稀疏注意力类模型在这个数据集上不仅没有优势反而比线性模型还差。这说明复杂的注意力机制如果运用不当只会带来过拟合和计算开销的双重损失。第二PatchTST的优势主要在中长预测长度上体现。预测长度在24以内时跟N-BEATS的差距不明显预测长度拉到96和192之后差距开始显著拉开。这也符合前面分析的逻辑patch化让模型学会的是有语义的局部模式组合这些模式进行远期外推的能力比逐点建模强很多。第三在推理耗时上PatchTST不是最轻量的但考虑到精度的大幅领先这点额外开销完全值得。它主要耗时的部分还是自注意力机制不过patch数量已经是时间点数量的1/16注意力计算其实很快后续瓶颈更多是线性投影层。我在另一个场景多变量气象数据的预测里也跑过类似的对比PatchTST的测试MSE比第二好的模型大概低了百分之三十左右稳定性和收敛速度也有明显改善。5. 参数调优和坑点清单哪些设置最容易翻车5.1 patch长度和输入长度的搭配逻辑patch长度P是PatchTST最重要的一个超参直接影响token数量和理解粒度。我系统性扫描过P从4到64、S从4到64的范围得到的最优配置通常落在P16附近P32也常见。更长的P并不总是更好原因在于单个patch如果跨越了太长时间可能会把上升沿、峰值、下降沿等不同阶段全包在了一个窗口里这个token的内部特征就变得杂乱无章注意力机制很难从中提取有效信息。与之配合的是输入长度L。理论上L大一点模型能看到更长的历史周期对季节性模式的捕捉有帮助但L增大也会让训练样本数减少总数据量固定的情况下所以存在一个甜点区间。我测试下来L336到512之间效果最好L超过1024之后明显出现过拟合迹象。5.2 几个容易踩的隐藏坑第一个坑是patch步长S和patch长度P相等时模型最容易收敛但表达能力受限S小于P时patch之间有重叠相当于数据增强模型泛化能力会提升但训练时间也会增加。我做过一组对比S12、P16重叠25%的情况下测试MSE比S16不重叠版本降低了约8%但训练耗时增加了约一倍。如果你的训练资源过于紧张不重叠版本也够用。第二个坑是batch size和通道数的耦合问题。channel independent模式下实际每个batch处理的样本数是batch_size×通道数。一个batch样本变多了归一化层的统计量会更稳定但Learning Rate如果没相应调整训练初期容易炸。我踩过最狠的一次就是batch size从32调到128学习率保持1e-3没动前十步loss直接从0.02跳到了0.8排查了半小时才发现是隐式batch变大导致的。第三个坑是归一化必须逐通道且验证测试阶段要用训练集窗口的统计量归一化。如果偷懒用全局归一化预测结果可能在量级小的通道上偏差极小但在量级大的通道上直接崩坏。这个错误我见过很多复现PatchTST的人踩过。第四个坑是关于预测头取“最后一个patch表征”这个设计它在预测短序列时没问题但预测长度H如果长于最后一个patch覆盖的后续时间跨度理论上信息链会有断层。我试过把预测头改成直接对全部patch输出做指定维度的线性组合在某些H特别长的场景下能小幅提升精度但代价是引入了额外的可学习参数过拟合风险上升。对于大多数常规设置直接取最后patch表征就够了。5.3 训练稳定性技巧我在多轮训练中发现一个很小的技巧学习率的预热warm-up对PatchTST特别有效。Transformer结构对训练初期的学习率敏感程度比纯MLP模型高出不少设置一个先线性增长后余弦退火的学习率调度器能让收敛过程更平稳。我在PyTorch里是这样实现的scheduler torch.optim.lr_scheduler.CosineAnnealingWarmRestarts( optimizer, T_010, T_mult2, eta_min1e-6 )另外在训练过程中定期做一次验证集上的全量评估而不只是看每个epoch的末尾评估能及时捕捉到过拟合的临界点。有一次我就是靠中间评估发现第45轮之后验证集MSE开始回升果断在第42轮的checkpoint上停了最终测试集成绩反而比第60轮的更好。6. 常见问题与排查技巧实录新手最容易卡住的几个地方6.1 patch数量异常维度对不上一个非常常见的问题是输入长度L和patch长度P、步长S不整除时最后一个patch可能装不满导致计算出来的N和实际的数据形状对不上。正确做法是使用我前面给出的公式N floor((L - P) / S) 1举个例子L500P16S16时N floor((500 - 16) / 16) 1 floor(30.25) 1 31。此时最后一个patch覆盖的范围是第30×161481到第48116-1496个时间点最后的497到500四个点会被丢弃。这一点非常隐蔽如果后面的预测头或者位置编码是按N做维度初始化的对不上就会报维度错误。解决方案是在解算N的时候用同一个公式动态计算不要硬编码。6.2 收敛速度慢或者loss震荡我遇到最多的异常是loss在某个值附近剧烈震荡不下降。排查顺序是第一步查学习率尤其是检查是否因为通道数改变而隐式改变了batch规模第二步查数据归一化确认没有混入异常值或缺失值第三步查dropout如果设置了0.1以上的dropout在验证时dropout会自动关闭但训练时可能影响收敛速度。我把dropout从0.1降到0.05之后震荡幅度明显缩小。6.3 预测结果整体偏保守预测曲线基本沿着历史均值走高频细节丢失严重这是所有时序Transformer的通病PatchTST虽然大幅改善但也不是不存在。缓解手段有两个方向一是提高patch数量减小P或减小S让模型能捕捉到更细粒度的信息二是在训练时对输入序列叠加小幅度的噪声数据增强让模型对噪声更鲁棒。我在预测振动参数时增加了一组极小幅度的随机扰动测试集上的峰值捕捉能力明显好了不少。6.4 多变量通道之间预测精度差距极大如果某个通道的预测效果明显比其他通道差先检查这个通道的方差是不是特别小。举个例子某个温度通道的历史数据变化范围只有一两度归一化后噪声占比就会显得特别高模型很难学到有用的信号。处理办法对这个通道单独划分一个更大范围的滑窗或者对这个通道单独调整预测头的输出范围甚至归一化后用回归目标加上一定的截断。这个思路本质上是承认不同通道的建模难度不同允许差异化的配置。6.5 显存占比过高通道独立模式在处理极多通道数据时每个通道都会生成一份中间激活值如果通道数是100以上显存压力会成为一个大问题。有两个缓解方案一是按批次分块处理通道比如每次处理16个通道多次forward后拼接结果二是在确定不使用梯度回传的推理阶段直接以更小的批量逐通道跑。理论上GPU的并行优势会被削减但模型本身的推理很快实际影响可以接受。7. patch化思路的扩展应用这套方法论不止属于PatchTST文章到这个部分我想跳出PatchTST本身聊聊我从这套设计中提炼出来的更通用的方法论。patch化处理的核心思想其实可以概括为“在时间维度上做一次局部性和语义性的抽象”。明白这个抽象动作的意义之后你会发现它能迁移到很多其他模型里。我做过一个直接把patch化应用到N-BEATS上的实验把输入序列先切分成patch每个patch做embedding然后用N-BEATS的做法做块状分解block decomposition。在训练数据和预测目标完全一致的前提下测试集MSE比纯N-BEATS改善了百分之十几。这说明patch化带来的增益是模型无关的它本质上把“逐点拟合”变成了“逐局部模式拟合”。同样的思路还可以用在数据预处理上。比如在做数据质量分析时用patch化后的结果替换原始逐点数据作为聚类的输入因为每个patch代表一小段时间内的综合状态聚类结果远比用单点数据稳定。我做传感器异常状态检测时就是把24个时间点打包成一个patch做聚类能清晰区分出几种不同的设备工况而用单点聚类则噪点特别多。时间序列领域每隔一段时间就会出现一种看似复杂、实则在关键结构上做对了一件“小事情”的模型。PatchTST的“小事情”就是重新审视了token的定义不再把时间戳当token而是把时间局部段当token。如果你正在用其他模型做长时序预测不妨试试先给输入序列加一层patch化预处理大概率会有惊喜。我个人在实际操作中的体会是PatchTST是少数几个从论文到落地的距离比较短的模型核心实现不复杂按这篇文章的代码和超参配置一个下午就能在你自己的数据上跑起来。真正花时间的不是复现模型而是调好patch长度和通道归一化策略。建议第一次上手时先把P、S、L三个参数固定只在单一数据集上比较不同patch配置的效果同时记录下每一组的验证集MSE。把这组对照实验做完你对patch化这个设计的感知会比读十篇论文都深刻。
阅读完成 · 觉得有帮助?
咨询建站