news 2026/10/20 14:45:23

从傅立叶到Koopman:长期时序预测的谱方法实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从傅立叶到Koopman:长期时序预测的谱方法实践

有人在评论区问我:“长期时间序列预测到底怎么才能不飘?”这个问题我几乎每个月都会遇到。我的回答通常只有一个:先别急着上神经网络,回头看看频域和谱方法。这篇要聊的就是从傅立叶变换到Koopman算子这条路径,以及一条能用几十行Python代码落地的预测方案。整篇文章面向两类人:一类是刚接触时序预测、想搞懂底层原理的新手,另一类是已经被LSTM和Transformer折腾得够呛、想换一条更可控路线的从业者。我会从最基础的傅立叶视角讲起,逐步推到Koopman算子的高维线性化思想,然后把求解过程、代码实现、实测结果和踩坑经验全部摊开讲。

先说明一点,我讲的是一种“谱方法”思路,而不是某个现成的模型。谱方法的核心是:把复杂的时间演化拆解成一组固定模式,每个模式有自己的频率或增长率,预测未来就是把这组模式分别外推再叠加。傅立叶变换做的是这件事,Koopman算子做的也是这件事,区别在于基函数的选取和适用范围完全不同。

1. 长期预测的真正难点:先回答“为什么傅立叶不够用”

很多人一开始都会对“谱方法”有个天然疑问:谱方法不就是傅立叶吗,把信号拆成正弦余弦叠一叠,外推几步就完事了?如果时间序列真的是平稳周期信号,这么做确实够了。比如一段由几个固定频率组成的正弦波,用FFT找到主频和相位,重建未来一百步都不会有明显偏差。问题在于,真实场景里的时间序列几乎不存在这种理想状态——信号频率会漂移、振幅会随时间变化、不同分量之间还会相互耦合。

我用一个简单例子说明。假设有一条序列由两个频率相近的正弦波叠加而成,其中一个频率在中间某个时刻开始缓慢偏移。傅立叶变换有“不确定性”这个特点,它只能告诉你整个时间窗内存在哪些频率成分,却无法表达“某个频率在某个时刻发生了怎样的变化”。于是外推两步还行,外推二十步后相位就开始错位,五十步以后预测曲线和真实曲线完全对不上。再叠加一点非线性耦合,比如某个分量对另一个分量的振幅产生调制,傅立叶框架就更吃力了。

更本质的原因是,傅立叶变换假设系统是线性的、时不变的,也就是说各频率成分独立演化,互不干扰。长期预测真正难的地方恰恰在于非线性动力学——状态之间相互影响,未来的变化方式依赖当前状态本身。你用固定基函数去拟合一条由非线性系统产生的轨迹,就像用一把刻度不变的尺子去量一张不断伸缩的地图,短距离误差不大,长距离必然失控。

这里需要引入“谱方法”的另一个视角。我们想要的其实是一组“自带动力学”的基函数:每一个基函数在系统演化下都只是被乘上一个常数,也就是特征值和特征函数的关系。如果这种基函数存在,预测就变成了对每个基函数做几何级数外推,既不依赖当前窗口的具体形态,也不会因为微小噪声导致轨迹很快发散。Koopman算子就是建设这种基函数的严格数学框架。

2. Koopman算子在做什么:把非线性动力学变成线性演化

Koopman算子的基本思想可以从一个离散动力系统说起。假设状态变量是x,它按照某个映射F演化:x_{t+1}=F(x_t)。F可以是完全非线性的,没关系。现在考虑一个观测函数g,把状态x映射成一个复数或实数,也就是我们能测到的量。Koopman算子K作用于观测函数g,定义是:

(Kg)(x) = g(F(x))

这个式子看起来像在循环定义,但它蕴含了一个非常重要的视角转换:K本身是一个线性算子,因为它只对函数做“输入替换”运算。无论F多么非线性,K作为一个函数空间上的算子始终是线性的。换句话说,非线性动力学被“搬到了”一个高维函数空间里,在那个空间里演化关系反而变成了线性映射。

光说线性还不够,真正有用的是谱分解。如果存在函数φ和一个复数λ,使得:

(Kφ)(x) = λφ(x)

那么φ就是K的特征函数,λ是特征值。沿着这个关系迭代下去,会得到一个非常干净的演化规律:

φ(x_t) = λ^t φ(x_0)

也就是说,这个特征函数在演化中的变化是纯粹的几何缩放。如果我们能找到一个观测函数g的展开式,把它表示成一系列特征函数φ_i的线性组合:

g(x_t) = Σ c_i λ_i^t φ_i(x_0)

那么整个预测问题就变成了对每个特征值λ_i求幂再加权求和。长期行为由哪些特征值的模大于1、哪些小于1直接决定。模接近1的部分会持续存在,模小于1的部分会逐渐衰减,模大于1的部分则主导长期增长。这跟傅立叶分析里的“频谱”结构几乎一模一样,只不过这里的“频率”是系数λ在复平面上的角度,而“增长率”则是λ的模。

从这个角度就能理解Koopman为什么适合长期预测。传统数值积分是每步都在当前状态下作用非线性映射,误差会沿着轨迹不断累积,非线性项还会放大误差。Koopman方法等于预先算好了整个系统的“全景地图”——那些特征函数和特征值。预测时不需要逐步重新计算非线性映射,只需要对每个模式做一个幂运算,然后叠加。这是一种从“走一步算一步”到“直接看全局趋势”的转变。

它和傅立叶的关系也可以说得很直白:当系统退化为线性时不变系统时,Koopman特征函数恰好就是傅立叶基函数,K算子作用于复指数后得到的特征值就是对应的频响因子。所以Koopman并不是抛弃傅立叶,而是把傅立叶放进了一个更一般的框架里,让它能处理非线性系统。标题里的“从傅立叶到库普曼”,其实就是这条推广路径。

3. 把理论变成可计算:延迟嵌入、特征映射与DMD的核心逻辑

理论很漂亮,但直接求解Koopman算子等于求无限维函数空间的谱分解,几乎不可能精确实现。工程上通用的做法是找一个有限维近似,这就要用到动态模态分解(DMD)及其扩展形式。

先来整理DMD的思路。假设我们有一串观测数据x_1, x_2, ..., x_{n+1},把前n个数据排成矩阵X,后n个排成矩阵Y:

X = [x_1, x_2, ..., x_n] Y = [x_2, x_3, ..., x_{n+1}]

如果数据在低维空间里近似满足x_{t+1}=A x_t,那就可以通过最小二乘估计出矩阵A:

A ≈ Y X^+

这里X^+是X的伪逆。这个A就是Koopman算子在原始观测空间上的一个有限维近似。它的特征向量和特征值就对应近似Koopman模式。这套方法在流体力学领域用得非常多,处理高维系统时还会先做奇异值分解降维再求A。

但处理非线性系统时,直接在原始空间求A往往不够。真实系统可能需要无穷多个维度才能线性化,原始观测空间的线性映射当然拟合不好。解决办法是“升维”——把状态x映射到更高维的“特征空间”,在那个空间里做线性拟合。这就叫扩展DMD。

升维具体怎么做?一个直接做法是构建字典函数,比如多项式基、径向基函数或者三角基:

Φ(x) = [1, x, x^2, x^3, ...]

然后同样构造数据矩阵,只不过这次用的是字典函数作用后的值:

K ≈ Φ(Y) Φ(X)^+

如果特征空间足够丰富,K就能更准确地逼近真实Koopman算子。多项式函数常用来处理弱非线性系统,因为很多平滑非线性在多项式空间里能被展开成有限项。径向基函数则更适合处理局部结构较强的系统。

这里有一个隐性的维度灾难问题。特征空间维度越高,矩阵规模就越大,而且还会过拟合,尤其当样本量不够时。实际中有两个常用技巧。第一个是加正则化,把伪逆换成岭回归形式:

K = Φ(Y) Φ(X)^T (Φ(X) Φ(X)^T + εI)^{-1}

正则项ε能压制那些主要由噪声驱动的伪模式。第二个是延迟嵌入。如果你的观测量是一维时间序列,没有显式的多维状态,那就用最近d个观测值组成一个窗口,把这个窗口当作当前状态:

s_t = [y_{t-d+1}, y_{t-d+2}, ..., y_t]

这个操作的理论依据是Takens嵌入定理:在很一般的条件下,延迟坐标构成的空间和真实状态空间是拓扑等价的。实践上,延迟嵌入加Koopman的组合对一维时间序列特别有效,因为窗口长度d给了算法记忆能力,相当于在构造状态时就已经引入了低维流形的信息。

还要说清楚一件事:谱方法算出的特征值在复平面上如何分布,直接决定了预测靠不靠谱。模明显大于1的特征值如果数量多,说明系统存在不稳定模式,长期预测的置信度就要打折。模明显小于1的模式则只影响短期过渡,长期可以忽略。真正难处理的是那些模刚好约等于1的特征值,它们对应系统的持久振荡或混沌行为,也是长期预测误差的主要来源。

4. 用Python实现一个最小可用的Koopman预测器

理论讲再多,不如跑一次代码。我这里的实现刻意保持简单,只依赖numpy,没有引入任何复杂的库,目标是让人看一遍就能自己复现。

先构造测试数据。我选了一个二维非线性映射,再通过非线性观测函数投影成一维时间序列,这样既保留非线性特征,又说清楚了“观测空间”和“状态空间”的区别。

import numpy as np def generate_series(n=3000): z = np.zeros((n, 2)) z[0] = [1.0, 0.5] for i in range(n - 1): x, y = z[i] z[i + 1] = [ 0.8 * x + 0.2 * y + 0.05 * x * y, 0.1 * x + 0.9 * y - 0.05 * x * y ] # 非线性观测:第一个分量加第二个分量的平方 obs = z[:, 0] + 0.5 * z[:, 1] ** 2 return obs

这里的动力系统带有交叉项x*y,属于弱非线性的情况,非常适合用多项式特征空间来处理。观测函数引入了二次非线性,所以原始观测序列不能直接用线性模型拟合得很好。

接下来实现Koopman预测器。核心步骤是延迟嵌入、多项式特征映射、岭回归估计转移矩阵、谱分解和递归预测。

class KoopmanPredictor: def __init__(self, window=10, degree=2, reg=1e-3): self.window = window self.degree = degree self.reg = reg self.K = None self.eigvals = None def embed(self, series): # 将一维序列转成延迟嵌入矩阵 n = len(series) - self.window X = np.zeros((n, self.window)) for i in range(n): X[i] = series[i:i + self.window] return X def lift(self, X): # 多项式特征映射:常数项 + 一次项 + 二次项 n, d = X.shape cols = [np.ones((n, 1))] for j in range(d): cols.append(X[:, j:j + 1]) for j in range(d): for k in range(j, d): cols.append((X[:, j] * X[:, k])[:, None]) return np.hstack(cols) def fit(self, series): S = self.embed(series) S_next = self.embed(series[1:]) Phi = self.lift(S) Psi = self.lift(S_next) # 岭回归求解 Koopman 矩阵 A = Phi.T @ Phi + self.reg * np.eye(Phi.shape[1]) B = Phi.T @ Psi self.K = np.linalg.solve(A, B) self.eigvals = np.linalg.eigvals(self.K) return self def predict(self, history, steps): # history 长度必须 >= window preds = [] state = history[-self.window:].copy() for _ in range(steps): phi = self.lift(state.reshape(1, -1)) next_phi = phi @ self.K # 从特征向量的前 window 个状态坐标里读出下一时刻窗口 next_state = next_phi[0, 1:1 + self.window] pred = next_state[-1] preds.append(pred) state = np.r_[state[1:], pred] return np.array(preds)

有几个实现细节需要特别说明。lift函数里第一列是常数,第二段到第window+1列是原始延迟状态。这样设计是为了在预测后能把特征向量中的状态分量提取出来。拟合时用岭回归而不用普通伪逆,一个小正则能显著减少噪声模式下特征值模长超过1的情况。预测阶段没有重新投影到训练集特征空间,而是直接取特征向量对应位置,好处是速度快,坏处是如果特征向量分解不精确,误差会逐渐积累,这一点后面会看到。

补充一个和傅立叶方法的直接对比。下面这个函数用FFT找出信号中最强的几个频率分量,然后外推未来:

def fft_predict(series, horizon, top_k=5): n = len(series) spec = np.fft.rfft(series) freqs = np.fft.rfftfreq(n) idx = np.argsort(np.abs(spec))[::-1][:top_k] ts = np.arange(n, n + horizon) pred = np.zeros(horizon) for i in idx: amp = np.abs(spec[i]) / n phase = np.angle(spec[i]) freq = freqs[i] pred += 2 * amp * np.cos(2 * np.pi * freq * ts + phase) return pred

这个朴素FFT预测器在纯周期信号上几乎完美,但在我们的非线性观测信号上会怎样,留到下一节实测对比里说。

5. 实测结果与调参心得:什么参数在决定预测质量

我用前面生成的序列做了一次完整的对比实验。序列共3000个点,前2500个点作为训练,后500个点用于测试。预测任务要求从第2500个点开始,往后推100步。

下面是一段可运行的评估代码:

np.random.seed(7) series = generate_series(3000) train, test = series[:2500], series[2500:] model = KoopmanPredictor(window=10, degree=2, reg=1e-3) model.fit(train) horizon = 100 history = train[-model.window:] pred = model.predict(history, horizon) fft_pred = fft_predict(series[:2500], horizon, top_k=8) mae_koop = np.mean(np.abs(pred - test[:horizon])) mae_fft = np.mean(np.abs(fft_pred - test[:horizon])) print("Koopman MAE:", round(mae_koop, 4)) print("FFT MAE:", round(mae_fft, 4))

实测下来,在多项式特征映射加上延迟嵌入的配置下,前20步预测基本能贴合真实曲线,100步的平均绝对误差明显低于朴素FFT外推。这个结果不意外,因为测试数据本身带有非线性耦合,FFT外推在相位和幅值上都会逐渐失衡,而Koopman方法通过特征值λ的幂次演化,在增长和衰减模式上保持了长期趋势的结构。

需要提醒的是,Koopman方法的预测效果高度依赖几个参数,我分别说下感受。

第一个是window长度。窗口太短,比如3到5,状态信息不足,特征值谱往往会出现好几对模接近1的退化模式,预测前期还行,中后期会开始震荡。窗口太长,比如30以上,虽然记忆容量大,但特征空间维度飙升,在数据量不够的时候过拟合严重,而且计算量增长很快。我多次试下来,window在8到15之间是这个数据和这个观测结构下的甜点区间。这个区间并不是普适的,它取决于系统本身的状态维度和观测信号的复杂程度,一般来说可以选择滑动窗口重构的典型嵌入维度,或者直接比较不同窗口在验证集上的表现。

第二个是degree。degree=1就等于做普通的线性DMD,此时多项式特征空间不包含交叉项,二次非线性观测无法被准确表达,预测误差会明显上升。degree=2能覆盖交叉项和平方项,通常就够了。degree>=3在短序列上更容易导致特征空间的稀疏性和过拟合,而且矩阵条件数会变大,由正则化生成的伪模式反而干扰长期外推。所以除非明确知道系统有三次非线性,否则degree=2永远是首选。

第三个是reg正则化强度。我试过reg=0,也就是严格最小二乘,此时协方差矩阵Phi^T Phi在特征维度较高时接近奇异,求逆后会产生一些模大于1的噪声特征值,预测会提前发散。reg=1e-3到1e-2能把噪声模式拉回单位圆以内,又不至于把真实动态压缩得太多。reg太大的话,所有特征值会朝圆心收缩,预测信号衰减过快,长期预测会趋向一个固定值,看起来“稳定”但实际偏差也很大。调正则项时建议直接看K矩阵的特征值分布,模长超过1的特征值里有无明显的物理模式,如果有就果断加大reg。

另外还有一个容易被忽视的因素:训练集长度。Koopman谱分解本质上是希望特征函数的演化规律在数据里被充分采样。训练长度若小于几百个点,很多中等模长的特征值估计不准,长期预测自然不稳。我这组实验里2500个点的训练集是够用的。

6. 特征值谱的阅读方法:预测前先判断能不能长期预测

Koopman方法最独特的优势之一,是它在给出预测结果的同时,还能告诉你这个系统的“脾气”。训练完成后,把K矩阵的特征值全部画出来,就能直观判断系统是否适合长期外推。

具体来说,特征值λ在复平面上有三种典型位置。模小于1的特征值对应耗散模式,它们会随时间衰减,只在过渡段起作用,对长期预测无关紧要。模大于1的特征值对应不稳定增长模式,如果它们不是表征系统真实的分岔行为而是噪声伪影,那么长期预测必然发散。模约等于1且辐角非零的特征值最为关键,它们对应持续振荡或者准周期行为,是最有“物理含义”的模式。

判断一组特征值是否可靠,有一个很实用的经验:用训练集的前半段做一次预测,再用后半段做预测,对比两者得到的特征值谱。两个谱高度一致,说明这些模式是数据中稳定存在的动力学结构,可以放心用于长期外推。如果谱差异巨大,说明你捕获的只是特定时期的瞬时状态,长期预测就没意义。这个方法类似时间序列中的稳定性校验,成本低,效果却很明显。

还有一类特殊系统要格外注意:混沌系统。混沌系统的Koopman谱有连续谱或者非常复杂的结构,有限维矩阵只能近似出部分模式,预测水平上限就是所谓的Lyapunov时间。在实测中,面对混沌系统,Koopman方法在短期上可能比若干基线方法好一些,但强行外推几十步后误差会迅速膨胀。这不是Koopman本身不好,而是混沌系统的可预测性本身有物理极限。我建议在预测超过某个关键指标前,先用特征值谱判断一下系统是否处于混沌区域,别让方法背不该背的锅。

7. 落坑记录与下一步扩展:这几件事我反复踩过

预测结束后,很多人喜欢直接把误差曲线拉出来看平均值,但谱方法建议再多看几样东西:特征值谱、各模式在预测中的权重、以及长期趋势的衰退曲线。

我踩过的第一个坑是特征向量的坐标提取。最开始做预测时,我没有在特征映射里保留原始状态坐标,而是单独训练了一个从特征空间到观测值的回归层。结果在测试集前期准确,后期某个模式占主导后回归层把观测推到了错误的量级。后来我改成在lift函数里直接包含一次项,让延迟状态本身就是特征向量的一部分,预测就稳定多了。这里的教训是:特征空间的构造必须包含你真正关心的问题量,否则信息在“翻译”阶段就会损失。

第二个坑是标准化。时间序列的均值方差如果很大,直接做多项式特征会导致数值溢出,矩阵条件数也会恶化。我做过一组对比实验,把序列缩放到零均值单位方差后,特征值谱变得干净很多。预测时记得对预测结果做反向缩放,别把尺度搞错了。这个方法对谱方法来说不是可选项,而是必需品。做序列标准化时,只能用训练集的统计量来缩放测试集,避免信息泄漏。

第三个坑是递推预测时窗口的更新方式。我上面代码里是把预测值直接拼进延迟窗口,这是最简单的做法。但它有个隐患:误差会通过窗口的端点污染下一个状态。想更稳健,可以在每次递推后把特征向量重新投影到训练集分布上,或者用多个预测值的加权平均来更新窗口。代价是实现复杂度上升,收益是在极度敏感的系统上能多撑几步。我个人刚开始做实验时不需要这个技巧,但当window变大、系统靠近临界态时必须考虑。

再往下扩展,有两条路值得提一下。一条是在特征空间里显式做谱截断:对K矩阵做特征值分解后,人为把模长大于1且辐角混乱的模式去掉,再用于预测。另一条是用核方法替代显式特征映射,比如径向基核。核方法的好处是可以在一个无限维空间里隐式构造特征,而不需要显式计算每一个多项式项,对大窗口、高维状态尤其友好。缺点是失去了对特征空间的直观控制,调参难度更高,GPU显存也容易爆。如果你后续要处理真正的高维工业数据进行持续多步预测,值得在这两条路上分别做一次原型验证。

最后再说一点我的个人体会。谱方法在时间序列预测领域长期处于一个微妙的位置——理论界关注它的上限,工程界却常常因为实现不当而低估它。我见过很多人跑出来的模型效果不好,一查原因,要么是特征映射没构造好,要么是没加正则项,要么是训练集太短。把Koopman的思想用正确的方式落到代码里,你会发现它对弱非线性系统的长期趋势预测异常可靠,而且在可解释性上远超那些动不动就上千个参数的黑箱模型。希望这篇笔记能帮你少走几步我走过的弯路,也欢迎带着自己的数据来交流参数选择经验。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 14:43:25

REA逆向意图考古学:从代码解构到行为意图重建

1. 项目概述:REA 并非新工具,而是一次认知范式的迁移“比Jev更爆火!逆向灭霸 REA 一天登顶GitHub”——这个标题在技术圈刷屏时,我正调试一个嵌入式固件解析脚本。第一反应不是点开仓库看代码,而是抓起纸笔画了三遍流程…

作者头像 李华
网站建设 2026/10/20 14:42:38

别再熬夜改PPT!汇报答辩高效工具流全拆解

答辩前夜还在跟PPT较劲的人,我见得太多。我自己也是从那个阶段过来的:代码能写一千行,一到汇报演示就满脑子浆糊。尤其是程序员和学生党,两者表面身份不同,实际遇到的困难几乎一模一样——时间紧、内容多、审美心里没底…

作者头像 李华
网站建设 2026/10/20 14:41:52

文本驱动PPT生成:内容与样式分离的自动化工作流

上周三我赶一份季度复盘PPT,本来估摸着半小时收工,结果折腾到晚上十一点。最后逼我动手写 ppt-master 的,不是什么高大上的需求,而是这种换模板后全盘重排的窝火经历。倒不是内容难写,而是同样的内容换了一版模板之后&…

作者头像 李华
网站建设 2026/10/20 14:41:45

wrf-python安装与使用指南:从tar.gz到WRF后处理实战

简介:这份资源是wrf-python 1.1.3的官方发布压缩包,面向气象科研、环境模拟及数值预报领域的Python开发者,用于对WRF模式输出数据进行后处理、插值、诊断计算与可视化绘图。包体整体约1.3MB,共包含328个文件,其中以222…

作者头像 李华
网站建设 2026/10/20 14:41:29

Amazing5马丁EA源码拆解:回测避坑与参数调优指南

简介:在量化交易领域,策略源码的质量直接决定了实盘账户的生死。马丁格尔策略通过亏损后加倍加仓来摊平成本,依赖一次回调回补全部浮亏,因此看似胜率极高,实则暗藏爆仓风险。验证这类EA源码,不仅需要看懂MT…

作者头像 李华
网站建设 2026/10/20 14:40:35

CleanCode AI编程标准代码生成器:生成即规范,告别代码不敢改

1. 为什么“生成即规范”是个值得死磕的方向 写代码这件事,最怕的不是不会写,而是写完了之后没人敢动。我见过太多项目,功能跑得通,但命名像密码、函数长得像火车、注释停留在“TODO: 以后再说”。这种代码一旦交接,接…

作者头像 李华