ARTICLE DETAIL

资讯详情

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

FreqCycle:显式补齐中高频的时间序列预测框架

FreqCycle:显式补齐中高频的时间序列预测框架 时间序列预测这个圈子做久了都会撞上同一堵墙低频趋势好抓中高频的周期波动怎么都学不干净。你可能也遇到过这种场景——模型在训练集上MSE压得很低一到真实预测曲线整体走向对了但那些该有的小波动、局部起伏全被抹平看起来像被熨斗烫过一样。FreqCycle 这个框架就是冲着这个痛点来的它不满足于只拟合低频骨架而是显式地把中高频成分补回来用多尺度的时频视角重新组织预测流程。如果你正在用 LSTM 做时间序列预测或者已经试过 Transformer 类模型但总觉得高频细节丢得厉害那这套思路值得你花时间啃一啃。下面我按自己复现和调参的过程把 FreqCycle 的设计逻辑、核心细节、实操步骤和踩过的坑完整拆一遍。1. 为什么中高频总是被模型“吃掉”1.1 从一次失败的电力负荷预测说起我最早意识到中高频丢失这个问题是在做一个短期电力负荷预测项目的时候。数据是15分钟粒度的负荷曲线明显能看到两层结构一层是日周期的大起伏另一层是叠加在上面的、幅度不大但很规律的短周期波动可能来自某些设备的定时启停。我用了一个标准的 LSTM 堆叠结构两层 128 隐藏单元训练 loss 降得很漂亮验证集 MSE 也还行。但把预测曲线和真实曲线叠在一起看问题就暴露了模型把日周期抓得挺准可那些短周期的小波动几乎全被平滑掉了预测出来的曲线比真实曲线“干净”太多。一开始我以为是过拟合或者欠拟合的问题调了 dropout、加了层数、换了优化器效果都不明显。后来把预测结果做了一次频谱分析才看明白模型输出的能量几乎全部集中在低频段中频和高频段的能量被严重衰减。这不是训练不充分而是模型在优化 MSE 的过程中天然倾向于优先拟合能量占比大的低频成分中高频因为能量小对总 loss 的贡献微乎其微梯度信号太弱学不动。1.2 MSE 损失函数的“偏心”本质这里得把原理说透一点。MSE 是逐点误差的平方和低频成分幅度大一个点的偏差可能就是中高频偏差的好几倍平方之后差距更大。所以优化器在梯度下降时绝大部分梯度都来自低频误差的修正中高频的梯度被淹没在里面。你可以把这件事类比成调音台低频是鼓点中高频是镲片如果只用总音量来衡量混音好坏那工程师肯定会先把鼓点调准镲片差一点听不出来但对整体音色的影响其实很大。传统做法有两种应对思路。一种是给损失函数加权比如对高频误差乘一个系数但权重怎么设很玄学设大了训练不稳定设小了没效果。另一种是在输入侧做分解把序列拆成趋势项和周期项分别建模但大多数分解方法比如简单的移动平均会把中高频混在一起拆不干净。FreqCycle 的思路不一样它不跟损失函数较劲而是在架构层面显式地构造多尺度时频表示让中高频成分有自己的“通道”去学习。1.3 多尺度时频视角到底新在哪市面上讲“多尺度”的时序模型不少但很多只是在时间轴上做不同大小的卷积核或者不同步长的下采样本质上还是在时域里打转。FreqCycle 的关键在于“时频”两个字它把序列变换到频域在频域上按尺度切分再回到时域做预测。这样做的好处是中高频成分在频域里是独立的分量不会被低频压制模型可以针对每个频带单独学习它的演化规律。我自己的理解是这有点像图像处理里的多尺度分解。一张图里既有大块的颜色区域也有细密的纹理如果只用一个大卷积核纹理就糊了。得用不同尺度的滤波器分别处理再把结果融合。FreqCycle 对时间序列做的事是一样的低频用大尺度看趋势中频用中等尺度看周期高频用小尺度看局部扰动最后把各尺度的预测结果合成。这个“显式补齐”的思路是它区别于普通多尺度模型的核心。2. FreqCycle 的整体架构拆解2.1 从输入到频域变换方式的选择FreqCycle 第一步是把原始时间序列变换到频域。这里有个选型问题用 DFT 还是 DWT我复现的时候两种都试过。DFT 的好处是频率分辨率均匀实现简单用 PyTorch 的torch.fft.rfft一行就能搞定缺点是它假设信号是平稳的对非平稳序列的局部频率变化不敏感。DWT 的好处是时频局部化好能捕捉频率随时间变化的情况但小波基的选择和分解层数需要调实现也复杂一些。FreqCycle 原文用的是基于 DFT 的频带切分我猜主要是出于工程简洁性的考虑。实际用下来对于周期性比较稳定的数据比如日周期明显的负荷、流量数据DFT 足够了。如果你的数据频率成分随时间漂移很明显可以考虑换成 DWT 或者 STFT但那就偏离原框架了得自己改。我建议先用 DFT 跑通确认中高频补齐有效果再考虑要不要上更复杂的变换。具体操作上对长度为 L 的输入序列做 rfft得到 L/21 个频率分量。然后按频率从低到高切成 K 个频带每个频带包含若干个频率分量。切分方式有两种等宽切分和按能量自适应切分。等宽切分简单但可能把能量集中的频带切得太碎自适应切分根据累积能量分布来定边界更合理但多一步计算。我实测下来对于大多数数据等宽切分配合 4 到 6 个频带就够用了频带太多反而每个频带样本太少学不好。2.2 多尺度分支的设计逻辑切完频带之后每个频带走一个独立的分支。这里的分支结构可以灵活选FreqCycle 原文用的是轻量的卷积加线性层我试过换成 LSTM 或者 GRU效果各有千秋。用卷积的好处是参数少、训练快而且卷积核在频域上滑动天然适合捕捉频带内的局部模式。用 LSTM 的好处是能建模频带内分量之间的时序依赖但参数多小数据集上容易过拟合。我的建议是数据量大的话几万条以上可以用 LSTM 分支数据量小就用卷积。分支的深度不用太深两层就够了因为每个频带的维度本来就不高堆太深容易过拟合。分支之间是并行的互不干扰这样每个频带可以独立学习自己的模式不会被其他频带带偏。这一点很关键也是“显式补齐”的体现中高频频带有自己的分支梯度直接作用在上面不会被低频分支的梯度淹没。2.3 频域到时域的还原与融合每个分支输出的是该频带的预测结果还是在频域里。要把它们合成最终的时域预测需要做逆变换。这里有个细节每个分支只预测了自己频带内的分量其他频带的分量置零然后做 irfft 得到该频带对应的时域信号。最后把所有频带的时域信号相加得到完整预测。融合方式也有讲究。最简单的是直接相加因为频域分量本来就是正交的逆变换后相加等价于频域拼接后再逆变换。但 FreqCycle 加了一个可学习的融合权重每个频带一个标量权重让模型自己决定每个频带的重要性。这个权重初始化为 1训练中自适应调整。我实测下来这个权重确实有用模型会自动给中高频频带分配比低频小的权重但不是零说明它确实在利用中高频信息只是知道这部分能量小不能喧宾夺主。注意融合权重不要加 softmax 归一化否则会强制权重和为 1反而限制了模型表达。用独立的 sigmoid 或者直接不加激活函数都行我试过直接线性加权效果最稳。3. 核心细节与实操要点3.1 频带切分的参数计算频带切分是 FreqCycle 的第一个关键参数。假设输入序列长度 L96比如 15 分钟粒度的一天数据rfft 后得到 49 个频率分量包括直流分量。如果切 4 个频带等宽切分就是每 12 个分量一个频带最后一个频带 13 个。但这样切有个问题直流分量和最低频的几个分量能量极大单独占一个频带可能更好。我通常的做法是把直流分量单独拿出来作为一个特殊频带它代表序列均值剩下的 48 个分量按频率高低切成 3 到 5 个频带。切分边界可以用累积能量来确定比如让每个频带的能量大致相等。具体计算对每个频率分量的幅度平方求和得到能量分布然后找分位点。这样切出来的频带低频带窄因为低频能量集中高频带宽因为高频能量分散更合理。代码上用 numpy 的cumsum和searchsorted几行就能实现。我贴一段我常用的切分函数import numpy as np def split_freq_bands(energy, n_bands): # energy: 每个频率分量的能量形状 (n_freq,) cum_energy np.cumsum(energy) total cum_energy[-1] boundaries [0] for i in range(1, n_bands): target total * i / n_bands idx np.searchsorted(cum_energy, target) boundaries.append(idx) boundaries.append(len(energy)) return boundaries这个函数返回每个频带的起止索引。注意边界要去重如果两个边界重合说明该频带能量太小可以合并。3.2 分支网络的初始化技巧分支网络的初始化对训练稳定性影响很大。我踩过的坑是一开始用默认的 PyTorch 初始化训练初期 loss 震荡很厉害后来发现是因为高频分支的输出幅度和低频分支差了几个数量级相加之后低频被高频的噪声干扰。解决办法是给每个分支的输出乘一个缩放因子缩放因子初始化为该频带能量占总能量的比例的平方根。这样高频分支初始输出幅度小不会干扰低频。具体实现在分支最后加一个nn.Parameter作为缩放因子初始值设为sqrt(band_energy / total_energy)。训练中这个参数也会更新但好的初始化能让训练初期稳定很多。我实测下来加了这一步之后训练收敛需要的 epoch 数少了大概三分之一。另一个技巧是分支内部的归一化。频域分量的数值范围差异很大低频分量幅度可能是高频的几十倍。如果直接送进网络高频分支的输入太小梯度也小。我通常在分支入口加一个 LayerNorm 或者 BatchNorm把每个频带的输入归一化到相近的尺度。注意 BatchNorm 在时序任务里要小心因为 batch 内的序列可能长度不一用 LayerNorm 更稳。3.3 损失函数的设计与权重虽然 FreqCycle 的核心思想是在架构层面解决中高频问题但损失函数的设计仍然重要。最直接的做法是只用时域 MSE因为最终预测是时域信号频域只是中间表示。但我试过在损失里加一项频域损失效果有提升。具体做法对预测序列和真实序列分别做 rfft计算频域幅度的 MSE乘以一个小的权重比如 0.1加到总损失里。这样模型在优化时域误差的同时也会关注频域误差中高频的频域误差不会被忽略。权重不能太大否则时域预测会变差因为频域相位信息对时域波形影响很大但频域幅度损失不直接约束相位。还有一个细节时域 MSE 可以对不同时间段加权。如果数据有明显的周期性可以在周期波峰波谷附近加大权重因为这些地方中高频成分最活跃。但权重设计比较麻烦我一般先用均匀权重跑通再考虑要不要加。提示频域损失的权重建议从 0.05 开始试逐步加到 0.2观察验证集上中高频段的预测误差变化。如果时域 MSE 明显变差说明权重太大了。4. 完整实操流程与关键环节4.1 数据准备与预处理我用的是一个公开的电力负荷数据集15 分钟粒度取连续 60 天的数据前 50 天训练后 10 天测试。输入窗口长度 96一天预测窗口长度 96预测下一天。预处理包括缺失值用线性插值补全异常值用 3-sigma 规则剔除后插值最后做 min-max 归一化到 [0,1]。归一化这一步要注意不要对整条序列做全局归一化而是对每个输入窗口单独归一化。因为不同天的负荷水平可能不同全局归一化会让模型学到绝对水平而不是相对模式。我试过全局归一化模型在负荷水平突变的那些天预测很差。窗口内归一化后预测结果再反归一化回原始尺度。数据集的划分也有讲究。时序数据不能随机划分必须按时间顺序切。我通常留出验证集做早停验证集取训练集最后 10% 的数据。测试集完全独立不参与任何训练和调参。4.2 模型搭建与参数配置模型搭建按前面说的架构来rfft 变换、频带切分、并行分支、逆变换、加权融合。我用 PyTorch 实现核心代码如下import torch import torch.nn as nn class FreqCycle(nn.Module): def __init__(self, seq_len, n_bands4, hidden_dim64): super().__init__() self.seq_len seq_len self.n_freq seq_len // 2 1 self.n_bands n_bands # 频带边界实际使用时根据能量计算后固定 self.boundaries self._get_boundaries() # 每个频带一个分支 self.branches nn.ModuleList() for i in range(n_bands): band_dim self.boundaries[i1] - self.boundaries[i] branch nn.Sequential( nn.Linear(band_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, band_dim) ) self.branches.append(branch) # 融合权重 self.fusion_weights nn.Parameter(torch.ones(n_bands)) # 分支缩放因子 self.scales nn.Parameter(torch.ones(n_bands)) def _get_boundaries(self): # 简化版等宽切分实际用能量自适应 step self.n_freq // self.n_bands bounds [i * step for i in range(self.n_bands)] bounds.append(self.n_freq) return bounds def forward(self, x): # x: (batch, seq_len) freq torch.fft.rfft(x, dim-1) # (batch, n_freq) outputs [] for i in range(self.n_bands): start, end self.boundaries[i], self.boundaries[i1] band_input freq[:, start:end] band_output self.branches[i](band_input) # 补零回全频段 full torch.zeros_like(freq) full[:, start:end] band_output # 逆变换 time_out torch.fft.irfft(full, nself.seq_len, dim-1) outputs.append(time_out * self.scales[i] * self.fusion_weights[i]) return sum(outputs)参数配置上hidden_dim 我设 64n_bands 设 4学习率 1e-3Adam 优化器batch size 32训练 200 epoch 配合早停。这些不是最优值但作为起点比较稳。你可以根据数据量调整 hidden_dim数据多就加大数据少就减小。4.3 训练过程与监控指标训练时除了看总 loss我强烈建议单独监控每个频带的预测误差。具体做法在验证集上把预测结果和真实结果都做 rfft按频带计算幅度误差打印出来。这样你能清楚看到中高频频带的误差有没有下降。如果低频误差降了但中高频没降说明模型没学到中高频可能需要调频带切分或者加频域损失。我训练时的典型曲线是前 20 epoch 低频误差快速下降中高频误差下降慢50 epoch 后中高频开始明显下降100 epoch 后各频带误差趋于平稳。如果 100 epoch 后中高频误差还很高基本可以判断架构或参数有问题。早停的 patience 设 20监控的是验证集总 loss。但要注意总 loss 可能被低频主导中高频改善在总 loss 上体现不明显。所以我有时候会自定义一个早停指标各频带误差的加权和权重按频带能量占比的倒数来设这样中高频的改善能被放大。4.4 预测结果的后处理模型输出的是归一化后的预测序列需要反归一化回原始尺度。反归一化用训练时保存的 min 和 max注意要用对应输入窗口的 min 和 max不是全局的。如果输入窗口归一化时用的是窗口内 min-max那反归一化也要用同一个窗口的 min-max。后处理还有一个可选步骤对预测结果做一次轻微的平滑去掉可能的高频噪声。但这一步要谨慎因为 FreqCycle 的目标就是保留中高频过度平滑会把有用的中高频也去掉。我一般不做平滑除非预测结果里出现了明显的不合理震荡比如相邻点跳变超过物理可能范围。5. 常见问题与排查技巧实录5.1 训练不收敛或 loss 震荡这是最常见的问题。我遇到过的原因有几个一是频带切分不合理某个频带只有一个频率分量分支输入维度是 1线性层退化成标量乘法梯度不稳定。解决办法是设最小频带宽度比如至少 3 个分量不够就合并到相邻频带。二是学习率太大频域分量的数值范围大梯度也大容易震荡。把学习率降到 1e-4 试试。三是分支缩放因子初始化不当前面说过用能量比例的平方根初始化。还有一个隐蔽的原因rfft 的输出是复数如果直接送进实数网络需要把实部和虚部分开处理或者取幅度和相位。FreqCycle 原文是分别处理实部和虚部我复现时也是这么做的。如果你直接把复数送进nn.LinearPyTorch 会报错或者行为异常。检查一下你的实现确保复数处理正确。5.2 中高频预测仍然很差如果训练收敛了但中高频预测还是不行排查顺序如下。先看频带切分是不是中高频频带太宽包含了太多分量分支学不过来试着把中高频切细一点多分几个频带。再看分支容量中高频分支的 hidden_dim 是不是太小低频分支和中高频分支可以用不同的 hidden_dim中高频用大一点的。然后看损失权重频域损失权重是不是太小加到 0.2 试试。最后看数据本身中高频成分是不是本来就是噪声没有可预测的模式对数据做自相关分析如果中高频成分的自相关性很弱那确实预测不了不是模型的问题。我遇到过一次折腾了半天模型最后发现是数据里中高频成分主要是测量噪声根本没有规律。所以先分析数据再调模型能省很多时间。5.3 预测结果出现异常尖峰或跳变有时候预测结果里会出现不合理的尖峰尤其是在序列边界附近。这通常是频域逆变换的边界效应导致的。rfft 假设信号是周期的如果输入序列首尾不连续逆变换后边界处会出现振荡。解决办法在输入序列两端做镜像延拓或者加窗减少边界不连续。我通常用镜像延拓把序列长度扩到原来的 1.5 倍预测完再截取中间部分。另一个原因是融合权重训练得太大某个频带的分量被过度放大。检查融合权重的值如果某个权重超过 2说明模型过度依赖那个频带可以加一个权重正则项惩罚过大的权重。5.4 常见问题速查表问题现象可能原因排查方法解决措施训练 loss 震荡学习率太大打印梯度范数降到 1e-4中高频误差不降频带切分太宽打印各频带误差切细中高频频带边界尖峰边界效应检查首尾连续性镜像延拓预测过于平滑融合权重偏向低频打印融合权重加频域损失复数处理报错未分离实虚部检查 rfft 输出实虚部分开处理过拟合分支太深对比训练验证误差减层或加 dropout提示排查时一次只改一个变量改完重新训练记录结果。同时改多个变量你分不清是哪个起了作用。6. 我踩过的坑和实测有效的调参经验6.1 频带数量不是越多越好我一开始觉得频带越多中高频补得越细效果应该越好。试了 8 个频带结果训练很慢而且中高频误差反而比 4 个频带时高。原因是频带多了之后每个频带的样本维度太小分支学不到东西而且频带之间的边界变得模糊融合权重也难学。后来固定在 4 到 5 个频带效果最好。我的经验是频带数量大致等于数据中可辨识的周期层数加 1。比如数据有日周期和周周期两层那就用 3 到 4 个频带。6.2 分支不要共享参数我试过让所有频带分支共享参数想着能减少参数量、防止过拟合。结果中高频预测明显变差。原因是不同频带的模式差异很大低频是平滑趋势中高频是快速振荡共享参数会让分支偏向低频模式中高频学不好。所以分支必须独立参数量大一点没关系时序预测任务通常数据量够。6.3 频域损失和时域损失的平衡频域损失权重我试过 0.01 到 1.0 的范围。太小0.01没效果和纯时域损失差不多太大1.0时域预测变差因为频域幅度损失不约束相位模型可能学出幅度对但相位错的预测时域波形就乱了。0.1 到 0.2 是比较好的区间。另外频域损失建议只加在幅度上不要加在相位上相位损失很难优化容易导致训练不稳定。6.4 输入窗口长度的选择输入窗口长度直接影响频率分辨率。窗口越长频率分辨率越高频带切分越细但计算量也越大而且太长的窗口可能包含过时的模式。我试过 48、96、192 三种长度。96一天效果最好48 频率分辨率不够中高频分不开192 计算量大而且引入了前天的模式对预测下一天帮助不大。如果你的数据周期不是一天按周期长度来设窗口一般取 2 到 3 个周期长度。6.5 用 LSTM 分支的注意事项如果你把分支换成 LSTM有几个坑要注意。一是 LSTM 的输入维度是频带内的分量数如果频带很窄比如只有 3 个分量LSTM 的隐藏状态维度不要设太大16 或 32 就够否则过拟合。二是 LSTM 的序列长度是频带内的分量数不是时间步数别搞混了。三是 LSTM 训练比卷积慢很多如果数据量大建议先用卷积跑通确认有效果再换 LSTM。我自己在数据量最大的那个数据集上用了 LSTM 分支hidden_dim 设 32效果比卷积分支好大概 5% 的 MSE 改善。但数据量小的数据集上LSTM 分支过拟合严重反而不如卷积。所以分支类型要根据数据量来选没有绝对的好坏。7. 这个框架还能怎么扩展FreqCycle 的思路其实可以往几个方向延伸。一个是把频带切分做成可学习的而不是预先固定。可以用一个轻量的网络预测切分边界或者用注意力机制让模型自己决定哪些频率分量归为一组。这样能适应频率成分随时间变化的数据。另一个是把 FreqCycle 和 Transformer 结合用频带分支的输出作为 Transformer 的 token让 Transformer 建模频带之间的交互。频带之间不是完全独立的低频和高频可能有耦合Transformer 能捕捉这种耦合。还有一个实用的扩展是 online 更新。FreqCycle 目前是离线训练、离线预测如果数据分布随时间漂移可以加一个滑动窗口的在线微调机制每隔一段时间用最新数据微调分支参数。我试过简单的在线微调对负荷预测这种分布缓慢变化的数据能提升大概 3% 到 5% 的预测精度。最后分享一个小技巧如果你不想从头实现 FreqCycle可以先用一个简单的两分支版本验证思路——一个分支处理低频比如前 1/4 频率分量一个分支处理中高频后 3/4其他结构不变。如果两分支版本比单分支有改善说明中高频补齐的思路对你的数据有效再上完整的多频带版本。这样试错成本低能快速判断方向对不对。
返回列表