变分模态分解 VMD:把价格拆成一组「窄带模态」,最低频那块就是趋势
移动平均滞后、HP 滤波端点漂、EMD 模态混叠——变分模态分解(VMD, Dragomiretskiy & Zosso 2014)换个数学框架:把信号拆成 K 个「各自窄带、中心频率自估计」的模态,用变分+ADMM 求解。本文用 vmdpy 从零讲清目标函数、频率域闭式更新与 Python 实现,在「慢饱和趋势+双周期+噪声」合成价格上把趋势 RMSE 压到 3.19(HP 2.34、MA80 12.99),并把隐藏的周期 37 / 120 直接估出来,诚实对比并拆穿模态数 K 敏感、alpha 带宽、端点效应、频域混叠、计算成本五类真实陷阱(中阶)。
你拿到一段价格序列,想抽出「长期趋势」做择时中轴、把周期成分单独拿出来做轮动、或者把高频噪声剥掉看结构。前面我们聊过 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 要拍、端点仍有轻微效应、频域太密时周期会混叠,后文五类陷阱要一一拆穿,但它确实是「把频谱当成可估计的量」那一类分解里最干净的一个。

1. EMD 的痛点:为什么需要 VMD#
EMD 的核心循环是「筛」(sifting):反复减上下包络均值,直到一个分量满足 IMF 两条。问题在于:
- 模态混叠:如果信号里有两个频率靠得近,或者存在中断(jump),筛的过程会把它们搅到一起,一个 IMF 里同时出现两种尺度。
- 停止准则主观:什么时候算「筛够了」?靠一个阈值或固定迭代次数,不同选择给出不同分解。
- 端点效应:包络靠插值,头尾没数据,容易甩起来。
VMD 的思路是先把「什么是好的分解」写成一个数学目标,再让优化器去找。它假设每个模态都是一个调幅-调频信号,围绕某个中心频率 ωₖ 窄带振动。于是问题变成:
找 K 个模态 uₖ(t) 和它们的中心频率 ωₖ,使得:
- 各模态加起来等于原信号 f(t);
- 每个模态的带宽尽可能小(即它真的很「窄带」)。
2. 目标函数:把「窄带」量化成一个数#
VMD 的优化目标是这个(这是我们整篇文章的数学核心,务必读一遍):
别被符号吓到,逐块拆:
- 是第 k 个模态(时域), 是它的中心频率(角频率)。
- 是解析信号(Hilbert 变换)的核——它把实信号变成「只有正频率」的复信号,这样「围绕 ωₖ 振动」才有定义。
- 乘上 再做 Hilbert,相当于把模态平移到基带(中心频率搬到 0)。
- 外面那个 (对时间求导)再取 L2 范数,度量的是「基带信号的带宽」——带宽越小,说明这个模态越集中在 ωₖ 附近,越「窄带」。
所以整句话翻译过来就是:让每个模态搬到自己基带后都尽量「平」(带宽小),同时 K 个模态拼起来等于原信号。 约束 保证信息不丢。
3. ADMM 求解:频率域里的闭式更新#
直接解这个带约束的变分问题很难,VMD 引入二次惩罚项 + 拉格朗日乘子(就是 ADMM 的标准套路),把约束松成目标的一部分:
其中 是保真度权重(也可以理解成「允许模态有多宽」的逆:α 越大,每个模态被压得越窄)。
关键的工程红利是:在频率域里,每一步都有闭式解。对第 k 个模态:
看分母:——这就是一个以 ωₖ 为中心的带通滤波器形状。ω 离 ωₖ 越远,分母越大,这一项被压得越狠。于是每个模态自动「只捡自己频率附近的那块能量」。
而中心频率的更新更简单、也更直观:
这就是模态功率谱的重心(一阶矩)——物理量上就是「这个模态的频谱质心」。
import numpy as np
import vmdpy # 权威 VMD 实现(基于 Zosso 的 MATLAB 代码移植)
def vmd_decompose(price, alpha=2000.0, K=5, DC=1):
"""用 vmdpy 做 VMD。返回 (modes[K, N], 直流分量已还原到价格单位)。"""
f = np.asarray(price, float)
f_mean = f.mean()
f0 = f - f_mean
sstd = f0.std()
if sstd < 1e-9:
sstd = 1.0
f_n = f0 / sstd # vmdpy 期望输入近似单位方差
# 返回 (modes, 频谱, 中心频率轨迹);DC=1 让第 0 个模态固定为直流(趋势)
modes, _, omega_traj = vmdpy.VMD(f_n, alpha=alpha, tau=0.0, K=K,
DC=DC, init=1, tol=1e-7)
modes = modes * sstd # 缩放回价格单位
return modes, f_mean
def vmd_trend(price, alpha=2000.0, K=5):
modes, f_mean = vmd_decompose(price, alpha=alpha, K=K)
# DC=1 时第 0 个模态就是趋势(直流),加回均值
return modes[0] + f_meanpython实现注记:自己从零写 VMD 的 ADMM 很容易在频率网格归一化上踩坑(α 的量纲依赖 ω 的定义域),数值上也不稳。这里直接用权威
vmdpy包保证模态精确相加回原信号(重建误差 < 0.3)。代码里对输入做单位方差归一化,是因为 VMD 的 α 是对「单位方差信号」调出来的经验值。
4. 看分解结果:模态分层长什么样#
我们把合成价格(慢饱和趋势 + 周期 120 + 周期 37 + 噪声)做 K=5 的 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 把隐藏的周期「算」了出来python5. 和基准比:VMD 趋势到底准不准#
我们用三个基准对比趋势提取质量:HP 滤波(λ=1600)、长期 MA(80)、短期 MA(20)。

在我们的合成数据上,相对真实趋势的 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 先验」的情况下把信号里的周期找了出来。

左图:注入周期 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]python7. 五类真实陷阱(不拆穿就是自欺)#
陷阱 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、恢复周期、抗噪曲线均直接打印在脚本输出里。