资讯动态

拆解PatchTST:Patch分块、通道独立与自监督预训练实战

发布时间:2026/10/22 5:11:12 来源:尧图企业网站定制
如果让我给近两年时间序列预测里最有启发性的方向排个序**PatchTSTPatch Time series Transformer**绝对是我会第一个提到的那一个。这个模型让我第一次意识到以前用Transformer做时间序列预测老是干不过MLP和线性模型瓶颈并不一定在注意力机制本身而很可能出在我们喂给注意力的输入粒度上——我们习惯把每个时间点当成一个独立token却忽略了一个基本事实时间序列里真正有意义的信息单元从来不是孤立的时间点而是连续的波形片段。这篇文章我会把PatchTST背后的三块核心设计拆开讲清楚Patch分块机制、通道独立设计、以及无监督自监督预训练并给出一套我在某个模拟项目里完整跑通的复现流程和踩坑记录。整个系列读下来你不仅能理解这个模型为什么有效还能直接照着搭出一份可用代码。无论你是刚开始接触这个方向的研究者还是想把时序Transformer落地到业务里的算法工程师这篇内容都值得静下心看完。1. 为什么逐点Transformer会输给简单模型把输入粒度从点改成段在聊PatchTST之前得先回答一个问题2020年前后Transformer刚被大规模引入时间序列时效果普遍不如预期甚至不如上世纪八十年代的ARIMA问题到底出在哪里1.1 逐点建模的三个老毛病第一个问题是语义粒度过细。单个时间点的数值基本没有信息量比如负载曲线里一个点的数值是120还是121在企业业务里几乎不承载任何模式含义。而Transformer学习的本质是找token之间的关联给它一堆信息密度极低的点token它自然很难学到有意义的特征。就像是让一个人逐字逐句读一篇长文每个字都认识但读完整篇文章却说不出主旨。第二个问题是序列长度带来的计算压力。自注意力的计算复杂度是O(n²)这里n是token数。当时预测任务动辄输入336个点、720个点变成token之后就是336个、720个token注意力矩阵规模是112k级甚至518k级训练速度惨不忍睹。为了跑得动大家只能缩短输入窗口可窗口一短长期依赖又学不到了两头不讨好。第三个问题其实更隐蔽局部时序模式被稀释。季节性、趋势性这些时序特征通常体现在连续几个点到几十个点的形状上比如一个上升沿、一个波峰、一段平缓区。逐点建模时注意力要在几十个点之间做相关性匹配才能隐约拼出一个形状这不仅慢而且极易被噪声干扰。1.2 Patch的本质从逐字读文章到按词组读文章Patch机制换了一个角度既然单个时间点不是合适的语义单元那就把连续的若干个点打包成一个片段。用公式来说会比较直观。假设输入序列长度是LPatch长度为P相邻两个Patch的滑动步长为S那么切出来的Patch数量K满足K (L - P) / S 1比如L336P16S16那么K就是21。换句话说我们把336个点token压缩成了21个patch token自注意力的复杂度一下子从336²降到了21²大约是原来的1/256。这件事跟读文章特别像。逐点建模是逐字读Patch化则是按词组读——时间序列预测注意力机制这些词本来就是一个整体你非要把时间、序列、预、测拆开看反而丢失了语义。把连续的点打包成一个片段等于先替模型完成了第一层特征提取模型看到的每个token本身就已经携带了局部波形信息。1.3 一个让我将信将疑、后来反复验证的事实我最早看到关于PatchTST的论文摘要时心里其实有点怀疑仅仅把token粒度变粗效果就能有质的提升为了验证我在几个常见的公开时序数据集上做了一组对照实验不涉及任何额外技巧一边是逐点token的Transformer另一边是同样结构、只把输入换成patch token的版本。结果让我挺意外在中等长度预测任务上Patch版本相对逐点版本在MSE上普遍有十几个百分点的下降。某些数据上甚至只改这一处就追平了之前调了一堆技巧才达到的效果。这个结果给我一个很深的印象模型能力固然重要但输入建模比模型结构更容易被忽视也更容易带来收益。PatchTST正是抓住了这一点把本来属于特征工程的如何描述数据做成了模型设计的一部分。2. Channel Independence与Patch的配合拆解两个设计各自承担的角色Patch机制解决的是输入粒度问题但PatchTST还有一个同样关键的设计——通道独立Channel Independence。这一节我们把这两个设计拆开看看它们分别解决了什么又是怎么配合的。2.1 通道独立到底在做一件什么事现实中的多变量时间序列通常有D个通道比如电力负荷场景下会有负荷、温度、湿度、风速等多个监测变量。处理这种多通道数据早期主流做法是通道混合把所有通道的特征拼成一个高维向量每个时间步对应一个d_model维的token相当于让模型同时看所有通道。这种做法的问题是不同的通道噪声水平、量纲、变化模式差异很大。把温度通道和负荷通道硬塞进同一个向量里训练时梯度会被噪声较大的通道带偏而且一旦将来新增或减少通道模型结构就得改复用性很差。PatchTST的做法完全不同每个通道独立切Patch、独立送入同一个Transformer编码器所有通道共享同一套权重。不把一个通道的信息混进另一个通道的注意力计算里。这里要特别澄清一个常见误解通道独立不代表完全切断跨通道信息。虽然前向计算时各通道是分开的但训练时如果batch维度是B×D那么每个batch里实际包含不同通道的多个切片样本梯度计算会把所有通道的误差叠加起来共享权重梯度自然携带了跨通道的统计共性。换句话说通道独立是注意力层面不混合不是信息层面不共享。2.2 P/S/L的组合选择先定事件片段的尺度复现PatchTST时最先碰到的可调参数就是P和S。我建议不要直接照抄别人的参数而是先看自己的数据你手头序列里最小的可识别事件一般持续多少个点如果负荷数据每天有两个高峰每个高峰大约持续4个小时而数据是15分钟一个点那一个完整高峰就是16个点左右。P选16左右就刚好把事件放进一个patch里。整理一下我试过的几组常用组合输入窗口LPatch长度P滑动步长SPatch数量K备注968812短窗口无重叠19216823重叠一半样本量更大336161621经典配置计算最省512321631长窗口带重叠720321644适合需要更长依赖的场景当S小于P时相邻patch有重叠这相当于给模型多提供了一些边界上下文能缓解patch边界切断完整事件的问题当S等于P时计算量最小patch之间没有重叠。我的经验是如果数据规模不是特别大优先选SP的无重叠配置如果过拟合明显再考虑SP/2的重叠方案来扩充样本。2.3 这两个设计合在一起为什么刚好work逐点Transformer的注意力匹配更像是在两个散点之间找相关噪声一多就找不到北。而Patch化之后注意力匹配变成了片段形状之间的匹配——这一段时间序列是一个上升段另一段时间序列也是一个上升段它们的patch嵌入在特征空间里自然更接近。Q和K的距离度量有了真正的语义基础。通道独立则保证了这种形状匹配学到的是通用的时序动态模式而不是某一通道特有的幅度偏移。毕竟patch嵌入处理的是局部波形不同通道之间波形形态常有相似之处比如都有周期性共享权重可以让模型把从温度通道学到的周期模式复用到负荷通道上。这套组合逻辑上非常自洽一个负责让token有语义一个负责让语义可复用。3. 无监督自监督预训练让模型先学会描述时间序列再学会预测它PatchTST最让我感兴趣的并不是结构本身而是它把自监督预训练和Patch机制结合到了一起。这一节重点讲清楚无监督自监督到底在学什么以及它和直接监督训练相比有多大区别。3.1 掩码patch重建任务的完整设定自监督的核心任务是掩码Patch重建思路和BERT的完形填空很像。训练时先把一条输入序列切成Patch并做嵌入然后随机遮蔽其中一部分patch位置。被遮蔽的位置用一个可学习的mask token替换而不是把该位置从序列里删除输入到Transformer编码器。编码器输出的每个位置都对应一个patch的表示我们在被遮蔽的位置接一个线性投影头目标是还原出这个patch对应的P个原始数值。损失函数就是被遮蔽位置的MSE公式上可以写成只对被遮盖位置计算lossloss mean((x_masked_patch - x_predicted_patch)²)这里有一个细节需要注意最小遮蔽单元是patch而不是时间点。如果逐点做掩码模型看到的上下文还是完整波形里的零星点太容易从左右邻居插值出来任务简单到没有学习价值。把整段patch遮掉模型就必须在更大尺度上理解趋势、周期和局部形状才能把缺失片段重建出来。3.2 为什么掩码重建能学到时序规律我听到过一个很好的类比掩码重建本质上在让模型做时间序列的完形填空。如果模型能从一段连续上下文里推断出被遮掉的负荷峰值大致是什么形态、什么时候出现说明它已经掌握了该序列的趋势延续性和周期节律这些能力对下游预测任务非常有价值。和NLP里的自监督相比时序数据有一个特殊之处数值是连续的、没有离散词表。所以分类式的损失函数并不合适MSE这类连续损失是自然选择。另一方面时间序列的局部结构有很强的物理连续性——温度不会瞬间从20度跳到40度负荷也不会毫无原因地突然翻倍。这种连续性让掩码重建任务变得有迹可循模型能真正学到关于数据生成过程的知识而不只是记忆训练集。3.3 预训练与微调什么时候值得做什么时候别硬做我试过的模式有三种直接从零开始监督训练、先自监督预训练再微调、以及只用自监督预训练提取特征后接一个小预测头。结合在不同数据规模下的结果可以给出一张实用决策表场景数据规模建议模式小数据标签充足几千个窗口直接监督训练即可预训练收益有限大数据标签充足十万以上窗口直接监督已经很好自监督多1%-2%提升但成本翻倍大数据标签稀缺历史无标签数据远多于有标签强烈推荐自监督预训练微调多通道传感器任意规模通道独立设计让无标签通道也能参与预训练样本量等效放大D倍在多通道场景下自监督还有一个隐性福利因为通道独立训练每条通道都被当成一条独立序列样本。哪怕有标签的数据不多所有通道的无标签历史数据都能拿来预训练实际上相当于把样本量扩大了好几倍。3.4 自监督实现里最容易翻车的三个细节第一掩码必须用可学习的mask token替换不能直接把位置删除。如果把被遮位置删掉序列长度就变了位置编码对不上模型也没法通过邻域关系推断缺失语义。这个错误会在训练loss上表现得很好看但下游任务完全拉胯。第二预训练和微调要保持位置编码一致性。预训练时输入窗口可能是336个点对应21个patch位置微调时如果你换了窗口长度位置编码维度对不上直接加载预训练权重就会报错。要么预训练和微调固定同一窗口要么对位置编码做插值处理。第三归一化方式必须全程一致。预训练时如果用标准化后的数据微调和预测时也必须用同一个scaler参数不能重新在测试集上fit。这个坑太常见了后面第五部分还会单独讲。4. 从0到1复现我在模拟项目X上的完整实践理论讲完下面给出一套我在模拟项目X里完整跑通的复现记录。项目背景我做了脱敏处理但数据流、模型结构、训练配置都是真实操作过的你可以照着搭。4.1 项目设定与数据处理模拟项目X是一个区域电力负荷预测任务数据粒度为15分钟一条一共7个通道包括负荷、温度、湿度、风速、气压等监测值覆盖约18个月。目标是用过去336个点即最近3.5天因为96个点是1天预测未来96个点即未来1天。数据处理遵循一个铁律严格按时间顺序划分不做随机打乱。我按70%训练、15%验证、15%测试切分且训练集只取历史最早的70%验证集和测试集依次往后排。很多人习惯先把所有数据打乱再划分这在时间序列里是严重错误会造成未来信息泄漏。归一化方面每个通道独立标准化。scaler只在训练集上fit保存下来的均值方差直接用于验证集和测试集。预测结果要在反标准化之后才算评估指标。4.2 模型输入输出结构从原始窗口到预测值整个数据流可以拆成五个步骤原始输入形状是[B, D, L]B是batch sizeD是通道数L是输入窗口长度。按通道拆开每个通道独立处理得到D个[B, L]的张量。对每个通道序列做Patch切分并用一个Conv1d把每个patch线性投影成d_model维嵌入得到[B, K, d_model]。在patch维度上加入位置编码送入Transformer编码器。编码器输出拼接后接一个线性预测头直接映射到未来H个时间点的预测值输出形状[B, D, H]。实际代码里第2-3步可以优化成一次reshape把B×D合到batch维度用Conv1d同时对每个通道切片做embedding这样速度比for循环快很多下面代码会体现这一点。4.3 推荐配置与训练参数我最终跑通的参数如下供你参考参数取值说明输入窗口L3363.5天的15分钟粒度数据Patch长度P16大约覆盖4小时匹配负荷事件尺度滑动步长S16无重叠K21Encoder层数3我试过1到6层3层性价比最高d_model128嵌入维度注意力头数8确保d_model能整除FFN隐藏层256两倍d_modelDropout0.2防止过拟合掩码比例0.4自监督阶段遮蔽40%的patch优化器用AdamW学习率1e-3权重衰减0.05带5个epoch的warmup和cosine退火。自监督阶段训练100个epoch之后丢掉重建头、接上预测头微调30个epoch。监督训练阶段batch size设64。4.4 核心代码结构下面给出一份简化但可以运行的PyTorch风格代码省去了部分工程细节保留了核心逻辑。import torch import torch.nn as nn import torch.nn.functional as F class PatchEmbedding(nn.Module): def __init__(self, patch_len, stride, d_model): super().__init__() self.proj nn.Conv1d( in_channels1, out_channelsd_model, kernel_sizepatch_len, stridestride, biasFalse ) def forward(self, x): # x形状: (N, 1, L)N可以是 B*D return self.proj(x).transpose(1, 2) # 输出 (N, K, d_model) class PositionalEncoding(nn.Module): def __init__(self, max_len, d_model): super().__init__() self.pos nn.Parameter(torch.randn(1, max_len, d_model) * 0.02) def forward(self, x): return x self.pos[:, :x.size(1), :] class PatchTST(nn.Module): def __init__(self, in_channels, patch_len, stride, d_model, n_layers, n_heads, d_ff, dropout, pred_len): super().__init__() self.in_channels in_channels self.patch_len patch_len self.stride stride self.pred_len pred_len self.patch_embed PatchEmbedding(patch_len, stride, d_model) self.pos_enc PositionalEncoding(1000, d_model) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadn_heads, dim_feedforwardd_ff, dropoutdropout, batch_firstTrue, activationgelu ) self.encoder nn.TransformerEncoder(encoder_layer, num_layersn_layers) self.head nn.Linear(1000 * d_model, pred_len) def forward(self, x): # x形状: (B, D, L) B, D, L x.shape x x.reshape(B * D, 1, L) # 通道独立合并到batch维 emb self.patch_embed(x) # (B*D, K, d_model) emb self.pos_enc(emb) h self.encoder(emb) # (B*D, K, d_model) h h.reshape(B, D, -1) # 展平所有patch位置 pred self.head(h) # (B, D, pred_len) return pred自监督掩码重建部分的逻辑大致如下先切patch得到(N, K, d_model)的嵌入序列生成形状相同的随机mask矩阵把mask位置替换成可学习的mask_token送入编码器再接一个线性头映射回P个原始点位只对被mask位置算MSE。为了控制篇幅这里不展开完整代码关键点是mask_token和位置编码都要参与训练且梯度可回传。4.5 评估指标与对比结果预测任务我采用MSE和MAE两个指标。表格里的数值是在我的模拟项目上复现得到的相对量级仅用于展示趋势不同的数据和随机种子会造成明显波动不建议直接当横向结论使用。模型MSE(96步)MAE(96步)备注ARIMA0.610.50传统统计基线直接多步线性回归0.520.43窗口展平回未来值逐点Transformer0.440.39同样结构但逐点tokenPatchTST直接监督0.350.34不使用自监督PatchTST自监督预训练微调0.310.32预训练100轮后再微调实际跑下来最直观的两个结论是Patch化相对逐点token带来的提升非常稳定数据量不大的场景下自监督预训练带来的额外增益并没有想象中那么夸张但总归是正向的。4.6 训练过程的实用技巧几个我在项目里验证过有效的训练细节关注验证集loss而不是训练集loss。时间序列模型的训练曲线容易震荡我习惯叠加早停patience设10个epochval loss连续10轮不降就停。梯度裁剪很必要。patch嵌入配合Transformer容易出现梯度爆炸我设max_grad_norm1.0训练稳定性明显改善。混合精度值得开。输入窗口长、batch又大时单卡显存容易吃紧开AMP后训练速度能提升50%以上。固定随机种子再比较模型。不同seed下时序模型指标波动可能超过2%不固定seed做对比很容易得出错误结论。5. 踩坑实录文档没写清楚、但复现一定会遇到的五个问题复现PatchTST的过程中我踩了不少坑。这些坑论文里基本不会写但实操中几乎每个都会遇到。5.1 归一化统计量泄漏指标好看但一上线就崩我第一次跑通整个流程时验证集MSE非常漂亮但换到新一段数据上做样本外测试性能惨不忍睹。排查了半天发现原因是整个数据集统一做了标准化scaler的均值和方差是用全量数据算的等于让验证集信息参与了训练预处理。这种错误特别坑人因为它不会让代码运行报错只会让所有离线指标虚高上线后立刻现原形。修复方案只有一个scaler只从训练集fit保存下来验证集和测试集用同一套参数做变换。如果用了自监督预训练预训练阶段的无标签数据如果来自不同分布还得用单独统计量这个细节容易被忽略。5.2 L不能被P整除时的处理方案把336个点切成长度16的patch正好能整除但如果你把输入窗口改成350个点350除以16除不尽直接用view/reshape函数就会报维度错误。我试过三种方案尾部截断、右侧补零、动态调整stride。从指标上看差距不大但代码鲁棒性差异很大。最省心的做法是设计窗口和patch参数时就保证整除关系如果团队里有人换了窗口长度用动态stride公式K (L - P) / S 1推导出S再运行避免硬编码。5.3 通道独立训练时的batch与mask设计陷阱通道独立的实现本质上是把B×D合并到batch维度。这样在自监督阶段生成mask时mask矩阵的形状应当是[B×D, K]也就是每个通道独立随机生成掩码位置而不应该让同一batch内不同通道共用同一份mask。我在早期版本里偷懒对一整个batch只生成一份mask然后广播给所有通道。结果是模型逐渐学会了当其他通道某位置被遮时这个通道相同位置也很可能被遮这种伪规律预训练的重建loss倒是正常下降但下游预测没有任何正向收益。改成通道独立mask之后问题才消失。5.4 自监督预训练的收益边界在哪里自监督不是万灵药。我在模拟项目X上做了一组消融当有标签的滑动窗口样本超过10万个时自监督预训练微调和直接监督训练相比MSE只低了1%左右而训练时间翻了一倍性价比非常低。但当有标签样本只有几千个时自监督预训练带来的收益可以达到5%-8%的MSE下降。所以我的建议是如果业务上有大量无标签历史数据和少量有标签数据自监督是利器如果标签充足老老实实直接监督训练没必要为了用了自监督这个名头付出双倍算力。这个边界问题论文里不会给你答案完全取决于你自己的数据规模。5.5 位置编码跨窗口长度迁移的坑预训练时固定了L336对应的patch位置编码只有21个patch位置。后来业务方提出能不能输入更长的历史窗口比如L512我一加载预训练权重就报shape不匹配。解决思路有两种要么预训练和微调始终用相同窗口长度要么位置编码改造成可插值模式新的位置用双线性插值生成。我最终选了固定L336的方案因为改动最小、风险最低。如果确实需要动态长度强烈建议一开始就在位置编码层预留足够大的max_len并测试插值方式对指标的影响。5.6 一个关于推理速度的意外收获顺带分享一个意外的发现因为Patch机制把注意力长度从336降到了21推理时注意力矩阵计算量大幅下降。在我这个7通道、336输入窗口的项目里PacthTST的单次推理延迟大约只有逐点Transformer的1/4显存占用也低了很多。对于线上预测系统来说这算是个很大的隐藏红利——精度提升的同时服务成本反而降下来了。关于PatchTST的复现我最深的体会不是某个参数设多少、某个结构怎么堆而是把数据喂给模型的粒度这件事常常比模型本身更值得花时间。很多团队一上来就调Transformer层数、学习率却忽略了最基础的输入表征方式。如果你也正打算在一个时间序列项目里试试Transformer我建议先别急着照搬大模型配置花五分钟把你的一条原始序列打印成波形图认真看看上面最小的可识别事件持续了多久然后按这个尺度来定Patch长度。很多建模问题的答案其实早就写在波形图里了。

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

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

免费获取报价 →
↑