☰
FreqCycle时频预测框架:解决时序预测中高频丢失的实战指南
2026/11/3 4:52:28 网站建设 项目流程

三年前第一次调时序预测模型,把我整崩溃的不是模型不收敛,而是收敛之后输出一条笔直的线。拿去和真实波形对比,趋势全对、均值全对,可那堆锯齿状的中高频波动几乎全被模型吃掉了。后来我意识到,这不是某个模型的锅,而是几乎所有“回归式训练”都对中高频天然不友好:误差先被低频填满,高频分量贡献的那点梯度根本排不上号。我做了一套叫 FreqCycle 的时频预测框架,思路就是反过来——既然模型学不会中高频,那就把它拆出来,显式地一个频点一个频点去补齐。这篇我会完整讲一遍这套框架的动机、设计和实现,也把调参踩过的坑一并交代,给正在和“预测太平滑”搏斗的朋友一个可直接落地的参考。

1. 先搞清楚痛点:为什么中高频预测这么难

做时频预测的朋友应该都有同感:低频随便预测都能八九不离十,中高频才是真正的分水岭。如果问题只是“资料不够多”或者“模型不够大”,那反而简单。实际上难点藏在数据表示和训练目标里,不换思路很难绕过去。

1.1 MSE 对高频的天然歧视

训练时序模型最常见的损失函数是 MSE 或 MAE,它是在时域逐点对齐的。假设一个信号由低频大振幅和高频小振幅叠加而成,MSE 按平方计算,大振幅对损失的贡献占绝对主导。训练时梯度也是平方加权的,模型自然会把绝大多数容量分配去拟合低频趋势,高频细节只能靠边站。

量化看更直观。一个周期内低频波幅为 5,高频波幅为 0.2,平方之后是 25 与 0.04,差距 600 多倍。这意味着,模型只要把低频拟合到 90% 的精度,收益就远远大于把高频拟合到 90% 的精度,梯度方向几乎完全被低频锁死。

我用过一个生活类比来理解这件事:请一位画家画人像,如果评分只看整体色块误差,他一定先涂脸蛋、衣服这些大色块,眉毛、睫毛这类精细高频线条即使画了,分数也没多大变化。不是画家不会画,是评分规则没有给精细结构留位置。所以第一步不是换更大的模型,而是换目标表示,让高频在损失函数里有话语权。这是 FreqCycle 的出发点。

1.2 相位才是真正的隐藏难点

就算把高频分量单独提出来,还有相位这座大山等着。幅度可以被插值、被平滑,但相位本质上是角度,而角度的难点在于周期性和卷绕。举个例子,350 度和 10 度这两个相位,在数值上相距 340 度,实际最小夹角只有 20 度。如果让网络直接回归角度,它要在 350 和 10 这个边界反复横跳,非常难学。

频率越高,相位旋转越快,建模难度进一步放大。一个高频分量,预测频率哪怕偏差千分之一,跑几个周期之后相位就完全对不上。波形幅度对了、相位错位,合成之后高频项还会互相抵消,最终输出又回归平滑。这解释了为什么很多模型高频幅度明明学出来了,预测曲线还是一副“没睡醒”的样子。

处理相位不能用直线思维。要用复数表示,或者预测相位增量,而不是直接预测一个绝对值角度。FreqCycle 里对相位的处理是我最想分享的部分,后面实现章节会展开讲。

1.3 主流模型为什么搞不定高频

我拿几个有代表性的方案横向压测过,包括 N-BEATS、Autoformer、TimesNet、Transformer/TCN,结论都不是“不能预测”,而是“高频表达不到位”。

模型高频处理方式为什么中高频不友好
N-BEATS用基函数展开趋势和季节基函数数量和阶数有限,高次谐波表达弱
Autoformer移动平均分解趋势+周期,自相关主导周期序列被当作整体,没有显式频点选择
TimesNet按周期将一维序列转二维卷积多周期混叠后高频纠缠,难以逐个补偿
Transformer/TCN全局注意力或卷积归一化和注意力偏向低频全局结构,高频被平滑

它们的共同点是:都把信号当作“整段”来学,高频分量混在目标里,模型看不到个别的频点。FreqCycle 的策略完全不同——不让模型一次性搞定全部频率,而是每次只处理残差里能量最大的一个窄带分量。这个微小的定位差异,对中高频的补齐效果影响极大。

2. FreqCycle 的整体设计与拆解思路

解决高频问题的方向确定后,接下来就是具体的框架设计。FreqCycle 不是推翻现有预测主干,而是在主干之外加一套显式的频域循环修正器。设计上有四个关键决策。

2.1 为什么在频域做多尺度分解

FreqCycle 第一步是把输入信号切成多个频率子带。为什么在频域拆而不是时域拆?两个理由:正交性和可逆性。时域里的滑动平均、差分这类操作很难把正交分量干净分开,而频域里不同频率点是内积正交的,用一个频域掩码乘上去,就能拿到干净的窄带子信号,再做逆变换回到时域,信息不丢。

多尺度体现在子带划分上。低频区域带宽窄、分辨率高,负责趋势和慢周期;越往高频,带宽按对数增大,负责快速变化的细节。划分方式不是拍脑袋定的:低频段一个频率点上的扰动,可能会影响整段波形的形态,所以需要细粒度;高频段本来能量就分散,按对数间隔切能保证每个子带都包含足够多的能量,不至于出现空带。

掩码不能用硬 0/1 截断。硬截断会在时域产生吉布斯振荡,重建信号出现一圈一圈的伪周期波纹。我用形状接近 sigmoid 的过渡带,相邻子带保留大约 10% 的重叠,重建时相加依然平滑。初期把掩码参数固定,只训练后端预测器,稳定后再放开学习,效果更好。

2.2 低频基座先行:给高频循环一个干净的起点

分解完成之后,最低频的 base 子带单独用一个小模型预测。base 能量最大,但变化平缓,线性层、MLP 或者两层 GRU 都能预测得很好。低频预测准了,等于给整个预测结果一个稳定的偏置和粗轮廓,后面每一步高频补丁都只是在这个轮廓上做修正。

为什么必须先低频后高频?因为循环补的是残差,必须从误差最大的子问题开始。如果一开始就处理高频,残差里还有大尺度趋势没被解释,频域能量几乎全压在低频端,峰值选择器会被低频带占领,循环很难跳到真正的高频缺口。先预测 base 后,残差的低频部分被抹平,峰值选择器才会老老实实挑出中高频的欠拟合部分。

我实测过顺序颠倒的结果:高频补丁会跑去拟合趋势残差,循环过程经常把补丁叠加成一个大曲率多项式,非常灾难。低频先行,后面的“高频显式补齐”才真正做到了字面意义。

2.3 循环补齐:把最大残差分量一块块塞进去

FreqCycle 的核心是一个循环过程,每一轮做三件事:

  1. 对当前残差做快速傅里叶变换,找出谱能量最高的未解释频点或极窄频带。
  2. 用一个轻量谱预测器,预测该频点在未来 H 步的幅度和相位演化,生成一条窄带时域信号。
  3. 把它叠加到预测结果上,重新计算残差。若残差总能量低于阈值,或循环次数达到上限,停止。

这个流程本质上是贪心的匹配追踪(Matching Pursuit):每次吃掉剩余能量里“最大的一块”。区别在于,匹配追踪用来重构历史,FreqCycle 用来预测未来。这样设计有三个直接好处:

  • 每一轮循环只学一个窄带分量,问题自由度极小,模型不容易过拟合。
  • 每一步都可检查。循环到第几轮、补了哪个频点、补了多少能量,都能可视化,排障非常直观。
  • 训练目标被逐步分解,不会出现“所有频率一起预测,误差相互抵消”的糊账。

可以类比拼乐高:先拼最大的底板,再看哪里缺得最多,一块一块补上去。整个过程目标清晰,最后剩下的小碎块属于噪声,可以选择不补。

2.4 叫 FreqCycle 的另一个原因是匹配追踪

名字里的 Cycle 不只是“循环”的意思,也是对循环机制的致敬。用更数学的语言描述,这个循环是在对预测目标谱做稀疏近似:把预测谱表示成若干窄带分量的叠加,每个循环识别并补齐一个基元。这个思路和稀疏编码、正交匹配追踪一脉相承。

FreqCycle 与标准稀疏编码的关键区别在于,标准稀疏编码的字典是固定的,而这里的窄带分量是模型根据输入历史动态生成出来的。我们的基元不需要提前定义,网络在训练过程中自己学会“什么样的历史谱结构对应什么样的未来窄带演化”。模型要的是发现频率之间的演化规律,而不是死记一组字典原子。

因此 FreqCycle 可以迁移到训练时没见过的频率上。只要谱峰对应的幅度、相位演化模式与训练数据相似,它就能补出来。

3. 核心实现细节与实操流程

这一节从工程角度把 FreqCycle 落到代码。我会把需要计算、需要注意的参数选择过程写清楚,直接照着搭基本能跑通。

3.1 输入窗口和频率分辨率怎么匹配

做谱分析最容易踩的坑就是输入窗口长度。频率分辨率由 df = fs / N 决定,其中 fs 是采样率,N 是参与 FFT 的点数。假设采样率 100Hz,想稳定捕捉 20Hz 的中高频分量,一个周期只有 5 个点,输入窗口至少得包含 2 到 3 个完整周期,也就是 10 到 15 个点。如果还想同时分辨 1Hz 和 1.5Hz 的两个慢频,频率间隔 0.5Hz,必须让 N 达到 200 以上。

实际业务里,很多公开数据集习惯用 96 或 192 点作为输入。如果采样率偏高,这个窗口长度根本容不下完整的高频周期,高频信息在源头就被截断了,后面再强的算法也补不回来。FreqCycle 对输入长度有一个经验下限:至少是最大关注周期的 2 到 3 倍,同时满足目标最低频率分辨率。

我通常拿到数据的第一件事,是用 Welch 谱估计看一眼真实信号的频率分布,确定能量集中在哪几个频段,再倒推窗口长度。先看谱再定窗长,这条顺序不能反。

3.2 频域多尺度分解的代码落地

多尺度分解模块用 PyTorch 实现并不复杂。核心思路:对输入序列做 rfft,在频域乘上若干平滑掩码,再做 irfft 还原子带时域信号。

import torch import torch.nn as nn class MultiScaleFreqDecompose(nn.Module): def __init__(self, num_bands=6, overlap=0.1): super().__init__() self.num_bands = num_bands self.overlap = overlap def forward(self, x): # x: (B, L) 单变量输入 spec = torch.fft.rfft(x, dim=-1) # (B, FreqBin) freqs = torch.fft.rfftfreq(x.size(-1)) # 归一化频率 0~0.5 bands = [] # 从第一个非零频率开始,按对数间隔切分子带 edges = torch.logspace( torch.log10(torch.tensor(freqs[1])), torch.log10(torch.tensor(freqs[-1])), steps=self.num_bands + 1 ) for i in range(self.num_bands): mask = self._smooth_mask(freqs, edges[i], edges[i + 1]) band_spec = spec * mask.to(spec.device) bands.append(torch.fft.irfft(band_spec, n=x.size(-1), dim=-1)) return torch.stack(bands, dim=1) # (B, K, L) def _smooth_mask(self, freqs, lo, hi): width = (hi - lo) * (1 + self.overlap) return torch.sigmoid((freqs - lo) / width) * torch.sigmoid((hi - freqs) / width)

注意两个细节。rfft 输出的 Nyquist 频率只有一个点,对数间隔从 freqs[1] 开始,避开直流分量。掩码用 sigmoid 做过渡带,重叠取 10% 带宽,重建时各子带相加仍能还原原信号。验证做法:对同一段信号分别做全频 irfft 和分解后叠加,误差应该接近浮点精度。

3.3 频谱预测器:幅度、相位分开处理

多尺度分解只负责“看到”子带,真正预测未来要靠频谱预测器。以单频点为例,把历史序列等间隔切成若干帧,每帧做 rfft 提取该频点的复数值,得到一条复数时间序列。预测时把幅度和相位分开处理。

def predict_frequency(freq_history, horizon, amp_model, phase_model): # freq_history: (B, T, 2),由 rfft 的实部/虚部构成 amp = torch.sqrt(freq_history[..., 0] ** 2 + freq_history[..., 1] ** 2) phase = torch.atan2(freq_history[..., 1], freq_history[..., 0]) # 用相位增量而不是绝对相位 dphase = phase.diff(dim=-1) future_amp = amp_model(amp) # (B, H) future_dphase = phase_model(dphase) # (B, H) future_phase = phase[..., -1] + torch.cumsum(future_dphase, dim=-1) # 欧拉公式重建复数序列 future = future_amp * torch.exp(1j * future_phase) return future

幅度建模要注意尺度。幅度跨度往往很大,直接回归原始幅度,MSE 会被大数值低频带主导,所以我训练时把目标改成 log 幅度。输出层用 exp 还原,训练更稳定。

相位建模是 FreqCycle 的精髓。绝对相位在边界处有卷绕,模型很难学;相位增量基本在 [-π, π] 范围内,而且连续帧之间的差值通常很小,回归起来非常轻松。预测时把历史最后一个相位作为起点,累计增量得到未来相位。

预测出的复数可以直接经过逆傅里叶变换生成窄带时域信号,能量严格限制在目标频点附近,不会污染别的频段。这也是它比直接预测整段未来序列更可控的原因。

3.4 损失组合与分阶段训练

损失函数直接决定 FreqCycle 能否收敛。我试过只加时域 MSE,结果高频还是不够;只加幅度谱损失,波形相位又乱。最后沉淀下来的损失组合如下:

L = w_time * MSE(y_pred, y_true) + w_amp * || log(|F(y_pred)|+1) - log(|F(y_true)|+1) ||_1 + w_phase * angular_distance(F(y_pred), F(y_true)) + w_res * || |F(y_true - y_pred)| ||_1

其中 F 是预测段的 FFT。时域 MSE 保证波形骨架正确;幅度谱损失用 log 缩放,让高频段也有同等的梯度贡献;相位损失只统计能量超过阈值的频点,避免噪声主导;残差谱损失激励循环去填补剩余能量。初始权重按 1 : 0.5 : 0.3 : 0.2 起步,跑一轮看各分量量级再微调。

分阶段训练强烈推荐,直接端到端联合训练容易让梯度混乱。

第一阶段:冻结循环模块,只训练低频 base 预测器。第二阶段:固定 base 和分解掩码,单独训练第一轮循环块。第三阶段:所有循环块联合微调几个 epoch,学习率降到正常值的十分之一。

分阶段的好处是让每个模块都先在自己能处理的残差上找到梯度信号,再逐步放开。我在实验中发现,跳过这个步骤会导致高频循环块在早期根本收不到有效梯度,白白训练好几轮。

4. 调参实录:那些坑和它们的解法

任何框架真正落地时都会遇到文档里不写的坑。下面这些是我在 FreqCycle 上实际踩过的,按踩坑频率从高到低排序。

4.1 频谱泄漏让人想砸键盘

最典型的翻车现象是高频项越补越乱:预测波形里出现一串像梳齿一样的高频振荡,频率还不是信号中的任何真实成分。查下来是有限长序列 FFT 的频谱泄漏。能量从真实频点泄漏到旁边,峰值检测器把泄漏峰当成真实分量,补进去一堆假频率。

解法有三条。第一,做 FFT 前乘 Hann 窗,主瓣宽一点没关系,FreqCycle 只需要定位峰值。第二,在子带掩码里保留过渡带,让泄漏的能量落在掩码边缘,不会误判为强分量。第三,如果目标只关心基频和若干谐波,可以先对谱做一次平滑,比如用 3-bin 中值滤波,再去挑峰。

这里有个容易忽略的细节:乘窗后的 IFFT 重建历史,必须把窗函数的增益补偿回来,否则叠加出的波形幅度会偏小。这也是为什么我在多尺度分解里用频域掩码做基座,窗函数只用在循环块内部的谱估计上。

4.2 相位卷绕:高频波形来回跳

另一个高频问题:训练损失降得挺好,但预测出来的高频波形每隔几步就突然反向翻转,像信号在发抖。原因还是相位卷绕。直接回归角度的模型,目标值偶尔从正接近 179 度跳成负接近 179 度,网络在边界上反复横跳。

处理方式我总结成三条:

  • 绝对相位永远不直接当回归目标。
  • 相位增量也要做截断。如果相邻两帧的相位差绝对值大于 π,就把差值映射到 [-π, π] 区间,避免模型学出“绕远路”的相位路径。
  • 训练时相位损失用复数空间的角距离,而不是角度差平方。

理解了相位是圆周上的变量而不是直线上的变量,这一块才算真正跨过去。如果采样率足够高,连续帧的相位差本来就很小,出现大差值往往意味着窗口切分不对,回去检查 3.1 里的窗长。

4.3 高频能量太低,损失函数根本看不到

有段时间中高频子带的损失权重已经开到 0.5,训练很多轮,高频依旧接近零。看日志才发现,高频子带损失占总损失只有 0.1%。问题还是出在尺度上:虽然加了子带损失,权重依然被大数低频压着。

解决有两条路。一是对幅度谱做 log 缩放,把高频幅度从 0.02 放大到 log 尺度上,与低频的差距被大幅压缩。二是对每个子带的幅度谱损失除以该子带的历史平均能量,相当于在每个频带内部做归一化。这样每个频带在训练时都有同等的更新话语权。

这两个方案比单纯调大 w_amp 更本质。调大权重只是放大了一个子带,log 缩放则把所有子带拉到了可比量级。

4.4 循环次数的调法

FreqCycle 的循环上限 K 不是越大越好。K 太大,模型会把噪声当成分量逐个补齐,过拟合特征非常明显:训练集惊艳、测试集摔惨。K 太小,中高频又补不干净。

我的策略是:

  • 训练阶段设一个较大的上限,比如 12,但每轮循环都记录残差能量的下降幅度。
  • 在验证集上画一条“循环序号对残差能量”的曲线,找到拐点。拐点后残差下降幅度小于 5% 的循环直接不要。
  • 推理阶段用阈值早停,而不是固定 K。阈值可以设为初始残差能量的 3% 到 5%,或者设为绝对能量阈值。

信号本身越干净,循环越少,推理计算越省。还有一个优化小技巧:如果某轮选中的频点与上一轮选中的频点相邻,就把它们合并成一个窄带再做预测,减少循环次数,也避免多次修正同一个峰值。

4.5 常见问题速查表

把高频训练常见的几个问题整理成表格,排查时直接对号入座。

现象可能原因解决方式
预测过于平滑,高频微弱时域损失主导,高频能量被低估加 log 幅度谱损失,按子带能量归一化
高频阶段出现梳状振荡傅里叶频谱泄漏加 Hann 窗,谱平滑后再挑峰
高频波形来回跳相位卷绕用相位增量建模,角距离损失
训练 loss 下降但验证集漂移很大循环次数过多,拟合噪声残差能量阈值早停,限制 K 上限
子带重构出现周期性波纹硬边界掩码,过渡带太窄加 sigmoid 过渡带和 10%~15% 重叠
推理时某频点相位缓慢漂移窗口内频率估计有偏滑动窗口重估频率中心,或改用 STFT

5. 扩展与迁移:FreqCycle 并不只有一种用法

这套框架做完后,我发现它的核心思想可以迁移到不少相关场景,不局限在最初的数据集上。

5.1 与 Transformer 结合

FreqCycle 的循环块可以替换成 Transformer 里的编码器。做法是把每个频点的复数历史当作 token,用注意力机制建模频点之间的依赖,尤其是谐波关系。基波和第 2、3 次谐波之间存在很强的耦合,注意力可以直接看到它们之间的关联。

这样做的好处是可以用固定循环次数,改为让 Transformer 一次性输出所有频点的未来演化,再经过 FreqCycle 的残差门控来决定每个频点补多少。实际效果上,我在一个公开数据集上比纯时域 Transformer 的中高频段误差降低了约 4%(MAPE),主要集中在高频段。

5.2 多变量与多通道扩展

多通道数据不需要每个通道单独跑一遍 FreqCycle。如果通道间共享同一个物理过程,比如同一台设备的振动传感器,那么它们的高频频率位置往往一致,只是幅度和相位不同。可以把所有通道拼在一起做一次 FFT,用频域聚类选出共同的频点,再让每个通道走各自的幅度相位预测器。这样频率选择是共享的,总参数量不会翻倍。

如果通道间完全无关,就各走各的 FreqCycle,最后用一个线性层融合。实战中我会先计算通道间的相干性(coherence),再决定共享还是分开,避免凭感觉拍脑袋。

5.3 非平稳场景:用 STFT 做局部谱

整段傅里叶变换不适合频率随时变化的信号,比如转速变化下的振动数据。这时把 FreqCycle 放到短时傅里叶变换(STFT)上:历史窗口滑动得到时频图,峰值检测在时频图上做,相位预测也变成对时频局部连续性的预测。

和整段版本相比,STFT 版能捕捉到频率漂移路径,预测高频时可以延续当前的漂移趋势。代价是计算量变大,窗口移动步长还要和预测粒度匹配。我对旋转机械振动信号做过糙实验,STFT 版比整段版预测误差低 20% 左右,主要赢在中高频段的相位连续性上。

这套框架做完之后,我最大的体会是:预测模型不是不够强,而是经常把力气花错了地方。MSE 天然偏心低频,不在目标表示上做干预,再强的模型也会往平滑方向走。FreqCycle 做的事其实很朴素——把目标切成频率上的小块,在残差里找到最大的一块,一块一块补上去。真正让我意外的是,这种简单机制对中高频的提升,比我替换更强的主干网络都明显。如果你正被“预测太平滑”折磨,建议先做一次频谱分析,看看模型到底丢掉了哪一段能量,再决定怎么修。希望这篇记录能帮你少走几个月的弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询