
聊个比较新的时序预测架构PhaseFormer。如果你最近在折腾长序列预测应该已经注意到这类问题早就不是“把LSTM堆深一点”就能解决的了。Transformer入局后PatchTST、TimesNet这些名字轮番出场但真正把“分块patch”和“相位phase”放到一起作为核心设计思想的网络PhaseFormer是目前比较值得花时间拆解的一个。它的核心思路一句话就能说清先把原始时间序列切成若干片段也就是标题里说的“补丁”再把这些片段映射到相位空间让网络去学“相位演化”而不是直接死磕原始波形。这篇博文适合三类人看一是做时序预测项目、想找一个新的基线模型来对比效果的工程师二是刚接触时序深度学习、想搞明白patch机制和相位表示到底在做什么的研究生三是已经在跑Transformer类预测模型、但觉得结果总是“太钝、太平滑”想换思路的人。接下来我会把PhaseFormer的设计动机、核心模块、实现细节和训练中我踩过的坑都过一遍代码部分基于PyTorch给出可直接改的参考实现。1. 这个网络到底想解决什么问题1.1 长序列预测的三座大山误差累积、分布漂移、计算开销先说说为什么要搞这么一个新的网络结构。长序列预测Long Sequence Forecasting和短序列预测完全不是一回事。短序列你拿个ARIMA、LightGBM加一堆滞后特征都能打但一旦要预测未来几十个甚至上百个时间点问题就变得非常不讲武德。第一座大山是误差累积。传统做法是“递推预测”预测出t1把它当输入再去预测t2这样每一步的小误差都会像滚雪球一样放大。到第50步时预测序列基本已经变成一条水平线了这就是为什么现在主流模型越来越喜欢“直接多步预测”Direct Multi-Step一次把未来50步全部输出。PhaseFormer走的就是这个路线。第二座大山是分布漂移。训练数据里看到的分布预测阶段往往已经变了尤其是带有明显趋势和周期成分的序列。你让网络死记硬背训练集里的均值、方差换一段新数据就原形毕露。所以模型必须学某种“结构不变性”周期、相位、相对变化趋势这些比绝对值更稳定。第三座大山是计算开销。标准Transformer用全局注意力复杂度是O(n²)。序列长度一上去显存和时间都受不了。Patch化其实最早就是为了解决这个问题的把n个点压缩成n/p个片段注意力长度大幅缩短。PhaseFormer继承了这一优点同时希望通过相位表示进一步降低建模难度。1.2 “补丁到相位”解决了什么问题我最早看到“从补丁到相位”这个说法第一反应是“补丁”是不是软件补丁当然不是。这里的补丁就是patch的直译意思是把时间轴切成一块一块的片段。这名字其实取得挺形象每个patch就像打在上面的一个补丁把局部信息缝起来。那“到相位”又是怎么回事如果你做过信号处理对“相位phase”这个词一定不陌生正弦波里相位决定了波形在某个时刻处于上升、峰值、下降还是谷值它描述的是“循环状态”。PhaseFormer借用了这个思想但不是简单地对原始序列做傅里叶变换取相位角而是通过神经网络把每一个patch映射到一个高维相位空间让“片段A当前处于周期的哪个阶段”“片段B比片段A领先了多少”这些信息变得可计算、可学习。这样做的好处是显而易见的对于带有周期性、季节性、节律性的数据——电力负荷有日周期和星期周期交通流量有早晚高峰股票虽然没有严格周期但也有明显的节律性——网络不需要从头去硬拟合每个点的绝对值而是学习“相位差”。相位差这个量在时间平移下是不变的所以模型对数据分布漂移的抵抗力更强这正是长序列预测最需要的东西。1.3 适用场景边界先用它还是别用它不看场景就吹模型是时序预测最容易犯的错误。PhaseFormer并不是万能药我实际测试下来的感受是它有三个非常对口的场景。第一类是高周期性的数据。电力负荷、气象温度、能耗监控、传感器读数这些序列有稳定或半稳定的周期Effect非常明显。第二类是“多尺度周期叠加”的数据比如同时存在日周期和星期周期的序列patch加相位的方式可以在不同尺度上捕捉这种叠加关系。第三类是训练数据相对稀缺的场景由于相位表示本质上是把原始信号的复杂模式压缩成更少但更本质的自由度模型在小样本下比普通Transformer更不容易过拟合。反过来如果你面对的是纯随机事件驱动型序列比如某APP的突发流量、完全没有规律的异常告警计数那PhaseFormer的优势就不明显了。相位描述的是“循环状态”一个没有周期性的序列相位学出来也是噪声效果可能还不如一个简单的MLP加滞后特征。这是第一点要先提醒的先看清楚数据里有没有周期结构再决定要不要上这个模型。2. 整体结构与设计思路拆解2.1 patch长度和步长怎么选周期长度是第一参考系PhaseFormer的第一步是序列分块。这一步看起来简单实际却是整个模型效果的分水岭。我见过不少人把patch_len直接设成Transformer里常见的16、32然后发现效果还不如DLinear问题就出在“尺度匹配”上。这里的核心原则是patch长度应该和数据的周期结构挂钩而不是拍脑袋。如果序列有一个明显的周期比如24小时patch_len取周期的四分之一到二分之一也就是6到12是比较合理的区间。为什么不是取整个周期因为单个patch只负责提供“局部波形特征”而跨patch的序列关系才是用来建模周期演化的。如果patch太长每个patch内部就包含了好几个周期相位信息的区分度反而下降。另一个关键参数是步长stride也就是相邻patch起点之间的距离。stride等于patch_len时就是非重叠切分信息丢失少、计算开销小stride小于patch_len时是重叠切分能缓解边界信息丢失问题但带来的冗余会明显拉长训练时间。我的习惯做法是先用非重叠切分训练一个baseline如果预测结果在周期切换点有明显毛刺再尝试重叠率50%的切分。还需要考虑一个容易被忽略的问题patch数量。假设原始长度是Tpatch_len是p那么patch数约等于T/p。如果patch数太少比如少于8个主干Transformer的注意力机制基本学不到什么东西因为注意力需要一定的序列长度才能发挥出上下文建模能力。这种情况下要么把输入窗口拉长要么减小patch_len。2.2 如何把片段变成相位复数域映射分块完成之后每个patch是一个p维向量。PhaseFormer要做的是把这个向量映射到相位空间。这里最自然的方式是用复数域一个复数可以分解成模长和幅角模长对应片段的“强度/幅值”幅角对应片段的“相位/状态”。具体做法是用一个线性层或一维卷积把每个patch的向量投影到复数域。严格说神经网络不能直接操作复数所以实现时是把向量映射到一个维度为“2×d_phase”的实数空间前一半当作实部后一半当作虚部。然后通过atan2计算幅角得到相位通过模长公式得到振幅。这样每个patch最终被表示成一组“振幅向量相位向量”合起来其实就是极坐标下的高维表示。为什么用极坐标而不是直接用直角坐标因为相位这个量对于时间平移具有天然的周期不变性。两个相同波形、只是平移了两个采样点的patch在直角坐标系下欧氏距离可能很大但在极坐标系下只是相位角的整体偏移网络学起来会轻松很多。另外极坐标表示让“振幅和相位解耦”网络可以选择性地放大对相位路径的学习这是普通线性Embedding做不到的。当然这里要强调一下“相位”不是只有傅里叶变换才能得到。PhaseFormer里的相位完全是由网络学出来的没有显式的三角函数基。这一点理解起来比较绕但反过来想更清晰网络只需要一个能表征“循环相对位置”的空间复数域恰好提供了这种几何结构。如果你所在领域对可解释性要求很高可以在训练后把学习到的相位角做PCA或者投影到2D空间往往能看到序列的周期轨迹。2.3 主干网络信息流相位演化建模patch变成相位向量之后就进入主干网络。主干的作用可以理解成“相位演化模型”输入是一系列随时间排列的相位向量输出是未来若干个时间步对应的相位向量。主干网络可以用Transformer Encoder这是最常规的选择也是PhaseFormer名字里“Former”的来源。注意力机制天然适合建模“第i个patch相对于第j个patch的相位差关系”因为attention score本身就是一种相对关系度量。不过这里有一个需要留意的地方既然已经工作在相位空间位置编码就不能乱用。如果直接加绝对位置编码会把相位信息的“相对性”破坏掉更合理的做法是像PatchTST一样只加可学习的通道表征或者把位置信息也编码成相位差的旋转形式让位置编码和相位表示在几何上兼容。主干网络内部层数不需要很深。我做对比实验时发现2到4层Encoder配上合适的patch大小效果已经能超过6层Transformer直接作用在原始点上。原因是相位表示已经把非线性复杂度消化了一部分主干网络不需要再承担“从零学习周期模式”的重担。如果你用卷积主干比如TCN、Inception模块也不是不行但注意力在灵活建模跨patch长程依赖上更有优势。解码端则是把预测出的相位向量连同振幅向量一路反变换回原始数值空间。常见做法是接一个全连接层把每个patch的相位振幅拼接成向量再映射回patch_len维。预测输出时要注意振幅和相位要同步预测不能只预测相位。因为相位告诉我们“波形走到哪一步了”但“这一段的强度有多大”还得由振幅决定两者缺一不可。2.4 损失函数设计别把相位当作普通数值损失函数是PhaseFormer最容易翻车的地方。如果你直接对相位向量算MSE会遇到一个非常尴尬的问题相位是一个圆上的量2π和0在数值上距离很远但在物理上完全一样。假设真实相位是359度模型预测是1度MSE会认为误差是358度实际误差只有2度。这就是相位回绕问题。解决思路有两种。第一种是把相位表示返回成复数后计算损失也就是对比复平面上的实部、虚部而不是直接对比角度。这样2π和0天然重合梯度也连续。第二种是对角度差做一个周期规整操作先算差值加π对2π取模再减π把差值映射到[-π, π]区间然后再算平方误差。我在代码里实现的是第二种简单直观效果也不错。除此之外损失函数还可以加上径向损失。径向指的是振幅这一路如果你只关注相位而不管振幅模型最后预测出来的波形幅度会非常保守整体偏向均值。所以在总损失里我会把相位损失和振幅损失分开设置权重通常相位损失权重可以略微高于振幅损失因为相位误差在长序列预测中导致的“错位惩罚”远大于幅度偏差。再往后可以加一个一阶差分损失让相邻输出之间的变化趋势更平滑这个对抑制过平滑也有一定帮助。3. 实操实现与核心环节3.1 数据预处理与patch数据集构建先约定输入格式一个形状为(B, C, T)的张量B是batch大小C是变量数T是时间步数。PhaseFormer实际处理的不是原始点序列而是一串patch嵌入序列所以预处理阶段就要把T维切分并重排为patch序列。切分前我强烈建议先做归一化。时序模型最常用的策略是实例归一化Instance Normalization也就是对每个样本、每个变量独立计算均值和标准差然后做标准化预测完成后再逆变换回原始尺度。这样做的好处是让模型专注学习形态而不是被不同量纲的绝对值带偏。对PhaseFormer来说归一化还有一个额外效果它会放大序列中微小的周期结构让相位学习更容易收敛。切分时需要注意最后一段不足patch_len的情况。最省事的方法是直接丢弃尾部不足一个patch的样本点但如果剩余部分占整体比例很高比如超过10%丢弃会浪费信息更好的办法是做零填充然后加一个mask。实际使用中如果窗口长度T选得合适——比如T能被patch_len整除——这个头疼问题基本可以避免所以我在选窗口长度时会刻意让Tpatch_len×N。def create_patches(x, patch_len, strideNone): # x: (B, C, T) if stride is None: stride patch_len B, C, T x.shape num_patches (T - patch_len) // stride 1 patches [] for i in range(num_patches): start i * stride patches.append(x[:, :, start:start patch_len]) return torch.stack(patches, dim1) # (B, num_patches, C, patch_len)这里把patch维度放在第1维后面送入编码器时再变形成(B, num_patches, C×patch_len)。3.2 相位编码实现atan2的数学细节相位编码层的输入是经过线性投影的patch表示输出是振幅和相位。实现时有两个关键细节先说第一个为什么用atan2而不是acos或者sin。atan2(y, x)是四象限反正切函数它能从实部和虚部直接得到角度并且根据实部虚部的符号自动判断角度落在哪个象限。如果你用acos(real / amp)会因为浮点误差导致传进去的参数略大于1或略小于-1直接出现NaN如果用sin或cos单独表示相位又会丢失角度唯一性。atan2是数值上最稳的选择。第二个细节相位向量的维度。我不会让每个patch只产生一个标量相位因为单一标量无法表征复杂波形。实际做法是让每个patch产生d_phase个相位分量也就是一组角度向量。比如d_phase取32每个patch最终表示为一个32维相位向量加一个32维振幅向量。为什么要这么多维因为真实数据的周期结构不是简单正弦波它可能由多个不同频率的循环叠加而成高维相位空间允许网络用不同维度去建模不同频率的循环成分等于给模型配了多把“尺子”去量不同尺度的周期。class PhaseEncoder(nn.Module): def __init__(self, patch_len, n_vars, d_model, d_phase): super().__init__() self.input_proj nn.Linear(patch_len * n_vars, d_model) self.phase_proj nn.Linear(d_model, 2 * d_phase) def forward(self, patches): # patches: (B, num_patches, C, patch_len) B, N, C, P patches.shape x patches.reshape(B, N, C * P) x self.input_proj(x) # (B, N, d_model) z self.phase_proj(x) # (B, N, 2 * d_phase) real, imag z.chunk(2, dim-1) amplitude torch.sqrt(real ** 2 imag ** 2 1e-6) phase torch.atan2(imag, real) # 角度范围 [-pi, pi] return amplitude, phase3.3 模型骨架代码PyTorch实现把上面的模块串起来一个最小可运行的PhaseFormer参考实现大概长这样。这里我刻意省略了很多花哨技巧保留最核心的路径patch化、相位编码、Transformer主干、解码输出。import torch import torch.nn as nn class PhaseFormer(nn.Module): def __init__(self, n_vars, patch_len, stride, d_model128, d_phase32, n_layers3, pred_len24): super().__init__() self.patch_len patch_len self.stride stride self.encoder PhaseEncoder(patch_len, n_vars, d_model, d_phase) self.pe PositionalEncoding(d_modeld_phase * 2) encoder_layer nn.TransformerEncoderLayer( d_modeld_phase * 2, nhead4, dim_feedforward256, batch_firstTrue, dropout0.1 ) self.transformer nn.TransformerEncoder(encoder_layer, num_layersn_layers) self.head nn.Linear(d_phase * 2, patch_len) def forward(self, x): # x: (B, C, T) patches create_patches(x, self.patch_len, self.stride) # (B, N, C, P) amp, phase self.encoder(patches) # (B, N, d_phase) feat torch.cat([amp, phase], dim-1) # (B, N, 2*d_phase) feat self.pe(feat) feat self.transformer(feat) out self.head(feat) # (B, N, patch_len) # 这里只做了“patch级映射”实际使用时还要还原回点级序列 return out这个实现是简化的重点是展示信息流。真正工程落地时解码端通常不是直接全连接输出patch_len而是先把整个patch序列通过一个Conv1d上采样还原到原始分辨率再截取pred_len步。另外PhaseFormer预测的应该是未来若干个patch而不仅是当前patch的还原所以实际输出头要设计成“将最后的patch表示线性变换成pred_len/patch_len个未来patch的振幅和相位”然后再重构波形。3.4 训练配置与指标定义训练配置这部分我直接给出自己跑通后比较稳的一组经验值。优化器用AdamW初始学习率1e-3配合CosineAnnealingLR逐步降到1e-5weight_decay设为1e-4batch大小取决于序列长度一般16到64训练轮数50到100轮配合早停。损失函数部分刚才说过的相位规整MSE是核心。我在实际项目里用它的变体def periodic_mse(pred_phase, true_phase): diff (pred_phase - true_phase torch.pi) % (2 * torch.pi) - torch.pi return torch.mean(diff ** 2)振幅损失用普通MSE。总损失记为L λ_phase × periodic_mse λ_amp × mse(amp_pred, amp_true) λ_diff × 差分损失。λ_phase我会设得大一点比如1.0λ_amp设0.5λ_diff设0.1到0.3之间。如果你发现预测结果过于尖锐、抖动明显就把λ_diff调大如果预测太钝、细节丢失就调小。评价指标上除了通用的MSE、MAE、sMAPE之外我强烈建议加一个自定义的“相位误差指标”。做法是把预测序列和真实序列都做希尔伯特变换提取瞬时相位然后计算相位差的绝对值平均。这个指标对周期错位非常敏感一旦模型学歪了它比MSE更能暴露问题。别小看这一步很多情况下MSE降了但预测曲线和真实曲线整体错位一个相位肉眼看着极其别扭可数值却“看起来不错”相位误差指标能堵住这个漏洞。4. 训练过程中的常见问题与排查4.1 相位回绕MSE在2π边界上翻车这个问题我在2.4节提过但实际踩坑时远远比理论描述要严重。第一次跑PhaseFormer时我没有用相位规整损失直接对角度做MSE训练到一半loss开始剧烈震荡验证集甚至出现过loss飙到几千的情况。排查之后发现就是相位回绕造成的当预测角度越过π边界时误差忽然从一个很小的值跳到接近2π梯度也跟着爆掉。解决方案就是前面那个periodic_mse所有涉及相位的损失都必须做“圆上的距离”计算。另外还有一个更稳妥的办法不要直接输出角度而是让网络输出经过单位化的复数向量损失也定义在复数空间。这样网络永远学不到“跨越边界”的概念自然也就不存在回绕问题。两种方案我都试过相位规整MSE在收敛速度上有优势复数损失在极端长序列上更稳建议大家根据数据情况都跑一版对比。4.2 patch大小与预测步长的尺度匹配很多人在PhaseFormer上效果差不是模型问题是patch_len和pred_len的比例没配对。设想一下你要预测未来24小时patch_len却设成2那么模型需要预测12个patch才能凑满24步任何一个patch的相位预测失误都会被放大反过来patch_len设成48那模型只能输出半个patch的预测信息细节几乎全部丢失。我自己的经验公式是patch_len设置在预测步长的1/4到1之间。如果预测步长是24patch_len可以取6、12、24分别代表“细粒度”“中粒度”“粗粒度”三档用验证集去选。换个角度想预测的本质是“外推”如果每个输出patch太窄模型学的更多是噪声太宽又容易丢失相位切换点。这个平衡只能通过实验找没有绝对正确答案。顺带提一句输入窗口长度也要跟着调整。输入窗口太短只有三四个patch注意力机制几乎不起作用太长训练开销又大。我的默认值是让输入窗口产生至少16个patch最好32个。以patch_len12为例输入长度T就是192到384个时间点这在长序列预测里属于比较合理的范围。4.3 曝光偏差与预测过平滑预测过平滑是几乎所有transformer时序模型的通病PhaseFormer如果不做特殊处理在长预测长度上也会出现“预测曲线变成均值线”的问题。原因在于解码时模型倾向于输出一个“保险的中间值”来最小化平均误差尤其是使用MSE损失时中位数/均值确实是最小化L2误差的解。这不是PhaseFormer独有的问题但相位表示会稍微缓解它因为相位本身是周期性的不容易被困在均值上。缓解过平滑有几个实际操作一是差分或去趋势预处理把序列变成平稳序列后再训练二是用分位数损失或负对数似然损失替代纯MSE逼迫模型给出更明确的分布三是输入输出都增强高频成分比如对原始序列做前向差分并作为额外通道喂给模型。我实测下来效果最明显的是第三条代价是训练时间增加30%左右。曝光偏差主要出现在递推解码的变体里。如果你把PhaseFormer改造成自回归模式也就是用预测出的patch替换输入到下一步那么训练时一定要加真实patch混合Scheduled Sampling或者直接用并行解码。我的建议很干脆长序列预测尽量用一次并行输出所有patch不要自回归。自回归的误差累积成本远比收益高。4.4 指标不涨先检查归一化与基准划分模型跑了十来个epochloss死活不降不要急着调网络结构先检查三个地方。第一是归一化方向。时序预测里常见的坑对全序列算均值方差导致标准化后的训练数据“看到了未来”验证集和测试集的信息泄漏到训练过程里结果指标虚高模型实际部署时被打回原形。正确做法是只用训练集的统计量来归一化训练集和测试集或者在每个样本内部独立归一化。PhaseFormer更推荐后者因为样本内的均值方差对应“相对波动”和相位表示的思路一致。第二是基准划分是否合理。很多人直接随机打乱数据划分训练集和验证集这对时序预测是致命的。随机划分会让验证集里出现训练集未来时间段的数据模型等于提前看到了答案训练曲线自然一片祥和。时序数据必须按时间顺序切分前70%训练中间10%验证最后20%测试。第三是损失函数里各项的尺度不匹配。振幅损失的数值天然比相位损失大如果直接把两个loss相加梯度会被振幅主导相位根本学不动。所以训练前先打印两个loss的初始值把权重调成同一量级我一般在0.1到10之间手动调一轮。5. 我的实验体会与可扩展方向5.1 我在实际跑模型过程中的几点体会第一次跑通PhaseFormer时最大感受是“数据形态变了模型学到的东西完全不同”。同样的数据普通Transformer学出来的是“每个时刻应该输出什么值”PhaseFormer学出来的是“当前片段处在周期的什么位置、下一个片段应该往哪个相位方向走”。后者的泛化性明显更好尤其当我换了一段不同年份的电力负荷数据做测试时普通模型直接崩而PhaseFormer只出现了轻微相位偏移。但也要说句公道话PhaseFormer的前置条件比较多。它对数据周期性的依赖很强如果序列是近似随机的相位空间基本是个无序球模型很难学到有价值的结构。另外它的调参维度比普通Transformer多了一层patch_len和d_phase都需要额外搜索对团队实验成本是不小的消耗。我建议把它作为一个高潜力基线而不是无脑默认模型。还有一个细节值得分享训练PhaseFormer时先把主干Transformer层数设为1只训练相位编码和解码头观察loss能否降到比较低的值再逐渐加深主干。如果一层Transformer都训不下来问题基本出在patch化或相位编码而不是深度不够。这个“由简到繁”的排查策略能省不少试错时间。5.2 后面值得继续扩展的几个方向PhaseFormer这套“patch相位”的框架其实还可以延伸出不少变体。一个方向是把它和多尺度patch结合同时在多个patch_len下提取相位然后融合这样能同时建模日周期、周周期甚至月周期叠加的数据。另一个方向是引入概率输出把相位和振幅建模成分布参数配合似然损失这样不仅能量化不确定性还能天然缓解过平滑问题。还有一个我最近在试的方向是把相位空间可视化作为数据探针在训练好的模型里把验证集的patch编码成相位向量用UMAP投影到2D你能直观看到数据点在圆环、球面或螺旋线上运动。这种做法对理解数据的周期结构非常有帮助算是PhaseFormer带来的一个意外收获。如果要给一个最小闭环建议我会说下次拿到一个新的时序预测任务先花20分钟算一下数据有没有显著性周期有就认真调一版PhaseFormer没有直接上MLP基线。别让模型替你做数据探索的功课合适的数据遇上合适的网络才是这个架构真正发挥威力的前提。