halo 的技术博客

返回

你拿到一段价格序列,想抽出「长期趋势」做择时中轴、把周期成分单独拿出来做轮动、或者把高频噪声剥掉看结构。前面我们聊过 EMD(经验模态分解)——它不预设基函数,靠局部极值反复筛出 IMF。但 EMD 有个老毛病:模态混叠(一个 IMF 里混进多个频率、或者一个频率被拆到两个 IMF),而且停止准则相当主观。

结论先放这:变分模态分解 VMD(Dragomiretskiy & Zosso 2014)在 EMD 之后,换了一套更「讲道理」的数学框架。 它不再靠筛,而是把分解写成一个约束变分问题:找到 K 个模态,每个模态各自「窄带」、围绕一个自估计的中心频率,并且 K 个模态加起来精确还原信号。求解用 ADMM(交替方向乘子法),频率域里有闭式更新。

在我们合成的价格(慢饱和趋势 + 周期 120 + 周期 37 + 噪声)上,VMD 趋势相对真实趋势的 RMSE≈3.19,与 HP 滤波的 2.34 同量级、远优于 MA(80) 的 12.99;它同时把隐藏的周期 37.5 与 120 直接估了出来(注入值正是 37 与 120)。 诚实地说:VMD 不是无脑更优——模态数 K 和带宽参数 alpha 要拍、端点仍有轻微效应、频域太密时周期会混叠,后文五类陷阱要一一拆穿,但它确实是「把频谱当成可估计的量」那一类分解里最干净的一个。

VMD 把价格拆成 K 个窄带模态,最低频那块就是趋势


1. EMD 的痛点:为什么需要 VMD#

EMD 的核心循环是「筛」(sifting):反复减上下包络均值,直到一个分量满足 IMF 两条。问题在于:

  • 模态混叠:如果信号里有两个频率靠得近,或者存在中断(jump),筛的过程会把它们搅到一起,一个 IMF 里同时出现两种尺度。
  • 停止准则主观:什么时候算「筛够了」?靠一个阈值或固定迭代次数,不同选择给出不同分解。
  • 端点效应:包络靠插值,头尾没数据,容易甩起来。

VMD 的思路是先把「什么是好的分解」写成一个数学目标,再让优化器去找。它假设每个模态都是一个调幅-调频信号,围绕某个中心频率 ωₖ 窄带振动。于是问题变成:

找 K 个模态 uₖ(t) 和它们的中心频率 ωₖ,使得:

  1. 各模态加起来等于原信号 f(t);
  2. 每个模态的带宽尽可能小(即它真的很「窄带」)。

2. 目标函数:把「窄带」量化成一个数#

VMD 的优化目标是这个(这是我们整篇文章的数学核心,务必读一遍):

min{uk},{ωk}{k=1Kt[(δ(t)+jπt)uk(t)]ejωkt22}s.t.k=1Kuk(t)=f(t)\min_{\{u_k\},\{\omega_k\}} \left\{ \sum_{k=1}^{K} \left\| \partial_t \Big[ \big( \delta(t) + \frac{j}{\pi t} \big) * u_k(t) \Big] e^{-j\omega_k t} \right\|_2^2 \right\} \quad \text{s.t.} \quad \sum_{k=1}^{K} u_k(t) = f(t)

别被符号吓到,逐块拆:

  • uk(t)u_k(t) 是第 k 个模态(时域),ωk\omega_k 是它的中心频率(角频率)。
  • δ(t)+jπt\delta(t) + \frac{j}{\pi t}解析信号(Hilbert 变换)的核——它把实信号变成「只有正频率」的复信号,这样「围绕 ωₖ 振动」才有定义。
  • 乘上 ejωkte^{-j\omega_k t} 再做 Hilbert,相当于把模态平移到基带(中心频率搬到 0)。
  • 外面那个 t\partial_t(对时间求导)再取 L2 范数,度量的是「基带信号的带宽」——带宽越小,说明这个模态越集中在 ωₖ 附近,越「窄带」。

所以整句话翻译过来就是:让每个模态搬到自己基带后都尽量「平」(带宽小),同时 K 个模态拼起来等于原信号。 约束 uk=f\sum u_k = f 保证信息不丢。


3. ADMM 求解:频率域里的闭式更新#

直接解这个带约束的变分问题很难,VMD 引入二次惩罚项 + 拉格朗日乘子(就是 ADMM 的标准套路),把约束松成目标的一部分:

L=带宽项+αf(t)uk(t)22+λ(t),f(t)uk(t)\mathcal{L} = \text{带宽项} + \alpha \left\| f(t) - \sum u_k(t) \right\|_2^2 + \langle \lambda(t),\, f(t) - \sum u_k(t) \rangle

其中 α\alpha保真度权重(也可以理解成「允许模态有多宽」的逆:α 越大,每个模态被压得越窄)。

关键的工程红利是:在频率域里,每一步都有闭式解。对第 k 个模态:

u^k(ω)=f^(ω)iku^i(ω)+λ^(ω)21+2α(ωωk)2\hat{u}_k(\omega) = \frac{\hat{f}(\omega) - \sum_{i\neq k} \hat{u}_i(\omega) + \frac{\hat{\lambda}(\omega)}{2}}{1 + 2\alpha(\omega - \omega_k)^2}

看分母:1+2α(ωωk)21 + 2\alpha(\omega-\omega_k)^2——这就是一个以 ωₖ 为中心的带通滤波器形状。ω 离 ωₖ 越远,分母越大,这一项被压得越狠。于是每个模态自动「只捡自己频率附近的那块能量」。

而中心频率的更新更简单、也更直观:

ωk=0ωu^k(ω)2dω0u^k(ω)2dω\omega_k = \frac{\int_0^\infty \omega\, |\hat{u}_k(\omega)|^2\, d\omega}{\int_0^\infty |\hat{u}_k(\omega)|^2\, d\omega}

这就是模态功率谱的重心(一阶矩)——物理量上就是「这个模态的频谱质心」。

实现注记:自己从零写 VMD 的 ADMM 很容易在频率网格归一化上踩坑(α 的量纲依赖 ω 的定义域),数值上也不稳。这里直接用权威 vmdpy 包保证模态精确相加回原信号(重建误差 < 0.3)。代码里对输入做单位方差归一化,是因为 VMD 的 α 是对「单位方差信号」调出来的经验值。


4. 看分解结果:模态分层长什么样#

我们把合成价格(慢饱和趋势 + 周期 120 + 周期 37 + 噪声)做 K=5 的 VMD。

同一价格的 VMD 模态分层:趋势在最底、周期在中、高频噪声在最顶

从下往上看(按主导频率从低到高):

  • Mode 1(最底,趋势):平滑的慢饱和 S 形,就是我们要的趋势。
  • Mode 2(周期 ≈37):干净的正弦,主导周期约 37 步,对应我们注入的短周期。
  • Mode 3(周期 ≈120):主导周期约 120 步,对应长周期。
  • Mode 4 / 5(最顶,高频):振幅很小(std≈0.08),基本是噪声和残差。

注意一个和 EMD 的关键区别:VMD 的模态是「按中心频率排序、各自窄带」的,不存在模态混叠——一个周期不会同时出现在两个模态里。

怎么知道每个模态的主导周期?直接对每个模态做 FFT 取峰值,干净利落:

def dom_period(mode, n):
    """用 FFT 峰值求模态主导周期(排除直流)。"""
    sp = np.abs(np.fft.fft(mode))
    freqs = np.fft.fftfreq(n)
    sp[0] = 0                              # 丢掉直流
    dom = freqs[np.argmax(sp)]
    return np.inf if abs(dom) < 1e-9 else abs(1.0 / dom)

# 在干净信号上:Mode2 主导周期≈37.5,Mode3 主导周期≈120.0
# 注入值正是 37 和 120 —— VMD 把隐藏的周期「算」了出来
python

5. 和基准比:VMD 趋势到底准不准#

我们用三个基准对比趋势提取质量:HP 滤波(λ=1600)、长期 MA(80)、短期 MA(20)。

VMD 趋势 vs HP / 移动平均:端点稳、低频模态干净

在我们的合成数据上,相对真实趋势的 RMSE:

方法趋势 RMSE
VMD(K=5)3.19
HP 滤波2.34
长期 MA(80)12.99
短期 MA(20)7.52

VMD 略逊于 HP(这是合理的:HP 是「全局平滑」专门 tuned for trend,VMD 是把信号切成窄带、趋势只是其中一块),但远好于移动平均,而且:

  • 端点不漂:VMD 的模态是全局变分求出来的,不像 HP 把末点狠狠拽向最后观测。
  • 周期分离干净:MA 把周期和趋势搅在一起,VMD 把它们分到不同模态,你可以单独拿周期模态做轮动。
  • 频率可估计:MA 和 HP 都不告诉你「里面有什么周期」,VMD 直接给了。

6. VMD 的隐藏能力:把周期直接估出来#

VMD 不只是趋势提取器——它是个频谱估计器。因为中心频率 ωₖ 是优化出来的,我们等于在「无 FFT 先验」的情况下把信号里的周期找了出来。

VMD 把隐藏的周期直接估出来 + 趋势对噪声的鲁棒性

左图:注入周期 120 与 37,VMD 恢复出 120.0 与 37.5——几乎精确。右图:噪声从 0.02 加到 0.20(相对趋势标准差),趋势 RMSE 只从约 1.0 温和升到 3.2,对噪声相当鲁棒(因为窄带约束天然抗噪)。

# 抗噪性:噪声越大,VMD 趋势误差仅温和上升
noise_levels = [0.02, 0.05, 0.10, 0.20]
rmses = []
for sd in noise_levels:
    price = make_signal(n=600, seed=4, noise_sd=sd)
    tr = vmd_trend(price, alpha=2000, K=5)
    rmses.append(rmse(tr, true_trend))
# 结果: [1.0, 3.20, 3.20, 3.23]
python

7. 五类真实陷阱(不拆穿就是自欺)#

陷阱 1:模态数 K 敏感,且无法「自动」选。 K 太小,多个周期被压进一个模态(混叠回潮);K 太大,噪声被拆成假周期。没有理论给最优 K,只能靠「重构误差 + 业务解释」反复试。经验:先用 FFT 看有几个明显峰,K 取峰数 + 1(趋势)。

陷阱 2:带宽 α 是量纲相关的旋钮。 α 太小,模态变宽、互相串味;α 太大,模态被压得过窄、可能漏掉真实能量。α 的单位和「信号的频率网格 / 方差」绑定——换一份量纲不同的数据,α=2000 可能完全失效。务必对输入做单位方差归一化再调。

陷阱 3:端点效应仍在。 VMD 用周期延拓处理边界,若信号在端点有强趋势或跳变,首尾模态会轻微失真。对「最新一根 K 线」做实时趋势提取时要留缓冲,别直接用最后几步。

陷阱 4:频域太密时周期会混叠。 当真实周期接近 Nyquist(比如周期 < 4 步)或彼此频率太近(Δf 小于带宽),VMD 分不清,会合并或错配。高频交易里别指望 VMD 拆出 2-3 步的微周期。

陷阱 5:计算成本随 N·K·迭代 线性涨。 每次迭代要对 K 个模态各做一次 FFT/反 FFT,N=600、K=5、上千次迭代是秒级,但日线十年的数据(~2500 点)反复跑网格搜索 K/α 会很慢。生产里建议预计算、缓存,别在信号循环里硬编码大 K。


8. 小结:什么时候用 VMD#

VMD 适合:你要的是**「把信号切成几个窄带成分、并且想知道每个成分的中心频率」**——比如趋势 + 多周期轮动 + 去噪一站式解决,且想要比 EMD 更干净、无模态混叠的分解。

不适合:你只想要一条最快的趋势线(那 HP 或 MA 更直接);或者你要严格的非参数、不预设「窄带」假设(那 EMD/EEMD 更合适);或者你没法容忍 K/α 的手调成本。

一句话:VMD = 把「频谱」当成可估计的量的分解方法。它用变分 + ADMM 把 EMD 的主观筛过程,换成了有目标函数、有闭式更新的优化问题——代价是要拍 K 和 α,收益是窄带干净、频率自现、端点稳。

代码与数据完全可复现:本文所有图由 gen_vmd_decomposition.py 生成,VMD 调用权威 vmdpy 包,趋势 RMSE、恢复周期、抗噪曲线均直接打印在脚本输出里。

变分模态分解 VMD:把价格拆成一组「窄带模态」,最低频那块就是趋势
https://blog.halo26812.eu.org/blog/vmd-decomposition
Author halo
Published at 2026年7月21日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨