简介:这份资源面向信号处理、机器学习与后端开发方向的Python学习者,提供变分模态分解(VMD)的可运行代码与配套测试用例,帮助解决非平稳信号难以拆解、频率成分不易提取的问题。压缩包共2个文件,包含1个py脚本与1个txt说明,整体约1KB,其中py文件承载VMD分解与验证逻辑,txt则记录依赖库的安装方式,便于快速搭起实验环境。资源围绕模态个数K、迭代次数与尺度因子α等参数展开,测试用例会对不同输入信号执行分解,并检查模态频谱特性与正交性,读者可借此理解初始化、迭代优化到分解输出的完整流程。目前已有5452人学习下载,适合希望把VMD用于声音分析、故障诊断、金融数据预处理或时间序列特征提取的开发者,通过对照代码与测试结果,掌握参数选择与分解效果评估的排错思路。
1. 变分模态分解到底解决了什么:从一段抖动信号说起
手里有一段轴承振动信号,采样率 12.8 kHz,转频大约 30 Hz,但频谱上 200 Hz 到 2 kHz 之间糊成一团。你想把故障特征那条边频带单独拎出来看,FFT 只能告诉你「有哪些频率成分」,没法告诉你「这些成分各自长什么样、随时间怎么变」。经验模态分解(EMD)能拆,但模态混叠和端点效应经常让结果没法复现——同一段信号换个采样点,IMF 数量就变了。
变分模态分解(VMD)就是冲着这个痛点来的。它把「分解」写成一个带约束的变分问题:假设信号由 K 个中心频率为 ω_k 的窄带模态叠加而成,每个模态围绕自己的中心频率尽量紧凑,所有模态加起来要能重构原信号。求解用交替方向乘子法(ADMM),每轮更新模态、中心频率和拉格朗日乘子,收敛后一次性输出 K 个模态。相比 EMD 的递归筛分,VMD 是整体求解,模态混叠明显更少,而且给定 K 和惩罚因子 α 之后结果完全可复现。
这篇笔记面向的是想在自己项目里跑通 VMD 的 Python 工程师:做旋转机械故障诊断的、做生理信号(EEG/PPG)预处理的、做金融或气象时序拆分的,都会遇到「把一段非平稳信号拆成若干有物理意义的子序列」这个需求。下面从环境、最小可运行代码、参数怎么调、测试用例怎么写,一路讲到避坑和进阶验证,代码都能直接抄。
2. 环境准备与最小可运行 VMD:从 numpy 到第一个模态
2.1 依赖选型:为什么优先用 vmdpy 而不是自己撸 ADMM
VMD 的原始实现是 MATLAB 的VMD.m,Python 生态里最常被引用的是vmdpy这个包,它把论文里的 ADMM 迭代逐行翻译过来,接口就一个VMD(signal, alpha, tau, K, DC, init, tol)。自己从零写 ADMM 不是不行,但希尔伯特变换、频域镜像对称、收敛判据这几处细节很容易写错,调试成本远高于直接读一份成熟实现。
我一般会这样配环境,Python 3.8 到 3.11 都验证过:
python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install numpy scipy matplotlib vmdpynumpy负责数组运算,scipy用来做希尔伯特变换和生成测试信号,matplotlib画分解结果。vmdpy本身依赖很轻,装完就能用。如果你在 Windows 上遇到msvcp140.dll找不到,那是 Visual C++ 运行库缺失,装一下微软官方的 VC++ Redistributable 即可,跟 VMD 本身无关。
提示:不要用
pip install vmd,那个包是另一个作者的同名项目,接口和vmdpy不一样,搜教程时容易混。
2.2 最小可运行代码:合成信号分解
先造一段已知成分的信号,这样分解结果对不对一眼能看出来。下面这段代码生成三个不同频率的分量叠加,再跑 VMD:
import numpy as np import matplotlib.pyplot as plt from vmdpy import VMD # 采样参数 fs = 1000 # 采样率 Hz T = 1.0 # 信号时长 s t = np.arange(0, T, 1/fs) # 时间轴 # 合成信号:三个分量 + 少量噪声 f1, f2, f3 = 30, 80, 150 # 三个分量的频率 sig = (np.cos(2*np.pi*f1*t) + 0.6*np.cos(2*np.pi*f2*t) + 0.3*np.cos(2*np.pi*f3*t) + 0.05*np.random.randn(len(t))) # VMD 参数 alpha = 2000 # 带宽约束,越大模态越窄 tau = 0 # 噪声容限,0 表示无噪声假设 K = 3 # 模态数,这里我们知道有 3 个分量 DC = 0 # 是否包含直流分量 init = 1 # 中心频率初始化方式,1 表示均匀分布 tol = 1e-7 # 收敛容差 u, u_hat, omega = VMD(sig, alpha, tau, K, DC, init, tol) # u: shape (K, N) 各模态时域波形 # u_hat: shape (K, N) 各模态频谱 # omega: shape (K, n_iter) 中心频率迭代轨迹 print("最终中心频率 (Hz):", omega[:, -1] * fs)跑完你会看到omega[:, -1] * fs输出接近[30, 80, 150],说明三个分量被正确分离。u的每一行对应一个模态,画出来就是三条干净的正弦波。
逻辑说明:VMD内部先把信号做 FFT 转到频域,初始化 K 个中心频率(init=1时在 0 到 0.5 之间均匀铺开),然后进入 ADMM 循环——每轮对每个模态更新频域表达式、更新中心频率、更新拉格朗日乘子,直到相邻两次迭代的模态变化小于tol。alpha控制的是频域二次惩罚项的权重,它决定了每个模态的带宽:alpha越大,模态越窄,越不容易混叠,但太大可能把一个宽频分量切碎。
参数说明:tau=0适合干净信号;如果信号噪声大,可以设tau=0.1到0.3让重构不那么严格。DC=1会把 0 Hz 附近的趋势项单独作为一个模态,处理带直流偏置的传感器信号时有用。init=0是全部从 0 开始,init=2是随机初始化,一般用1最稳。
2.3 怎么确认分解没跑偏:三个检查动作
跑完不要只看图好看就完事,我习惯做三个检查。第一,把 K 个模态加起来和原信号对比,残差应该接近噪声水平:
recon = u.sum(axis=0) residual = sig - recon print("重构残差 RMS:", np.sqrt(np.mean(residual**2))) print("原信号 RMS:", np.sqrt(np.mean(sig**2)))残差 RMS 应该远小于原信号 RMS,如果接近甚至超过,说明 K 或 alpha 设得不合理。第二,看中心频率有没有两个模态挤在一起,如果两个 ω 几乎相等,说明 K 给多了。第三,看omega的迭代轨迹是否收敛,如果最后几轮还在大幅摆动,把tol放宽或者增加迭代上限。
3. 参数怎么调:K 和 alpha 的实操边界
3.1 K 的确定:从过分解到欠分解的观察法
K 是 VMD 里最需要经验的参数。K 太小,多个分量被塞进一个模态;K 太大,一个分量被拆成几个,还会多出虚假模态。常见做法是先设一个偏大的 K,观察中心频率分布,如果出现两个 ω 非常接近(比如相差不到 5 Hz),就把 K 减一,重复直到所有 ω 都明显分开。
下面这段代码可以批量跑不同 K 值,把中心频率打出来对比:
for K_test in [2, 3, 4, 5, 6]: u_t, _, omega_t = VMD(sig, alpha=2000, tau=0, K=K_test, DC=0, init=1, tol=1e-7) freqs = np.sort(omega_t[:, -1] * fs) print(f"K={K_test}: 中心频率 = {np.round(freqs, 1)}")你会看到 K=3 时输出[30, 80, 150]附近三个值;K=4 时可能多出一个在 200 以上的虚假模态,或者把 150 拆成两个;K=2 时 80 和 150 会被合并。这个方法对合成信号很直观,对实测信号同样适用——中心频率的物理意义就是你要找的特征频带。
注意:如果信号里有明显的转频谐波系列(比如 1x、2x、3x 转频),K 至少要覆盖到你关心的最高阶次,否则高阶谐波会被吞掉。
3.2 alpha 的影响:带宽与模态纯度的权衡
alpha的默认值 2000 对大多数机械振动信号够用,但它不是万能的。alpha越小,模态带宽越宽,能容纳频率漂移大的分量,但相邻模态容易重叠;alpha越大,模态越窄,分离更干净,但可能把一个本身有带宽的分量切成碎片。
我一般这样试:先固定 K,把 alpha 从 500 到 5000 扫一遍,看重构残差和模态频谱的集中程度。下面是一个简单的扫描脚本:
for alpha_test in [500, 1000, 2000, 3000, 5000]: u_a, _, omega_a = VMD(sig, alpha=alpha_test, tau=0, K=3, DC=0, init=1, tol=1e-7) recon_a = u_a.sum(axis=0) rms = np.sqrt(np.mean((sig - recon_a)**2)) print(f"alpha={alpha_test}: 残差 RMS = {rms:.4f}, " f"中心频率 = {np.round(np.sort(omega_a[:, -1]*fs), 1)}")经验上,残差 RMS 随 alpha 增大先降后升,最低点附近就是比较合适的值。如果 alpha 太小,残差可能不大但模态混叠严重;alpha 太大,残差会明显上升,因为窄带模态没法覆盖原信号的全部能量。
3.3 噪声信号下的 tau 和 init 设置
实测信号几乎都有噪声,tau=0的严格重构假设会让模态里混进噪声。把tau设成 0.1 到 0.3,相当于允许重构有一定松弛,模态会更平滑。init在噪声大时建议用1(均匀初始化),不要用2(随机),因为随机初始化在噪声下容易收敛到局部最优,同一段信号跑两次结果不一样,这在做批量测试时是灾难。
# 含噪信号推荐配置 u_noisy, _, omega_noisy = VMD(sig_noisy, alpha=2000, tau=0.2, K=3, DC=0, init=1, tol=1e-7)如果信号有直流偏置(比如加速度传感器输出有零点漂移),把DC=1,VMD 会单独分出一个 0 Hz 附近的模态,避免它污染低频模态。
4. 测试用例怎么写:从合成信号到断言
4.1 合成信号测试:已知频率的回归验证
写测试用例的核心思路是:造一个你知道答案的信号,跑 VMD,断言输出符合预期。用pytest组织,下面是一个最小测试文件:
import numpy as np import pytest from vmdpy import VMD def make_signal(freqs, fs=1000, T=1.0, noise=0.0, seed=0): """生成多分量余弦叠加信号""" rng = np.random.default_rng(seed) t = np.arange(0, T, 1/fs) sig = sum(np.cos(2*np.pi*f*t) for f in freqs) if noise > 0: sig = sig + noise * rng.standard_normal(len(t)) return t, sig def test_vmd_separates_three_tones(): """三个单频分量应被正确分离""" freqs = [30, 80, 150] _, sig = make_signal(freqs, noise=0.0) u, _, omega = VMD(sig, alpha=2000, tau=0, K=3, DC=0, init=1, tol=1e-7) detected = np.sort(omega[:, -1] * 1000) # 允许 2 Hz 误差 assert np.allclose(detected, freqs, atol=2.0), \ f"中心频率不匹配: {detected}" def test_vmd_reconstruction_error(): """重构残差应小于原信号能量的 5%""" _, sig = make_signal([30, 80, 150], noise=0.01) u, _, _ = VMD(sig, alpha=2000, tau=0, K=3, DC=0, init=1, tol=1e-7) recon = u.sum(axis=0) err = np.sqrt(np.mean((sig - recon)**2)) assert err < 0.05 * np.sqrt(np.mean(sig**2)), \ f"重构残差过大: {err}"第一个测试验证频率分离能力,第二个测试验证重构质量。atol=2.0是给数值误差留的余量,实际跑下来误差通常在 0.5 Hz 以内。噪声测试里noise=0.01模拟轻微噪声,残差阈值设 5% 是经验值,噪声更大时要相应放宽。
4.2 边界用例:短信号、单频、直流分量
除了正常用例,边界情况必须覆盖,否则上线后遇到短数据段就翻车。下面三个用例分别测短信号、纯单频和带直流偏置:
def test_vmd_short_signal(): """信号长度小于 100 点时不应崩溃""" _, sig = make_signal([50], fs=1000, T=0.05) # 只有 50 个点 u, _, _ = VMD(sig, alpha=2000, tau=0, K=2, DC=0, init=1, tol=1e-7) assert u.shape == (2, 50) def test_vmd_single_tone(): """单频信号用 K=1 应能完整重构""" _, sig = make_signal([100], noise=0.0) u, _, omega = VMD(sig, alpha=2000, tau=0, K=1, DC=0, init=1, tol=1e-7) assert abs(omega[0, -1] * 1000 - 100) < 2.0 def test_vmd_dc_component(): """带直流偏置时 DC=1 应分离出 0 Hz 模态""" _, sig = make_signal([50], noise=0.0) sig = sig + 2.0 # 加直流偏置 u, _, omega = VMD(sig, alpha=2000, tau=0, K=2, DC=1, init=1, tol=1e-7) freqs = np.sort(omega[:, -1] * 1000) assert freqs[0] < 5.0, f"未分离出直流模态: {freqs}"短信号用例里T=0.05只有 50 个采样点,VMD 内部 FFT 仍然能跑,但模态数不能超过信号本身能分辨的频率数,所以 K 设 2 而不是 3。直流用例断言最低中心频率小于 5 Hz,因为DC=1时直流模态的 ω 会收敛到接近 0。
4.3 参数化测试:批量覆盖 K 和 alpha 组合
用pytest.mark.parametrize可以把 K 和 alpha 的组合一次性覆盖,避免手写重复用例:
@pytest.mark.parametrize("K,alpha", [ (2, 1000), (3, 2000), (3, 3000), (4, 2000), ]) def test_vmd_param_grid(K, alpha): """不同参数组合下重构残差都应可控""" _, sig = make_signal([30, 80, 150], noise=0.01) u, _, _ = VMD(sig, alpha=alpha, tau=0, K=K, DC=0, init=1, tol=1e-7) recon = u.sum(axis=0) err = np.sqrt(np.mean((sig - recon)**2)) assert err < 0.1 * np.sqrt(np.mean(sig**2))这个用例不检查频率是否精确匹配(因为 K=2 或 K=4 时频率数不对),只检查重构残差在合理范围。参数化测试的价值在于,当你改了 VMD 的调用封装或升级依赖版本时,能快速发现哪些参数组合退化了。
5. 避坑与排查:VMD 跑不通时先看这几条
5.1 模态数 K 给大了,中心频率挤在一起
现象:跑完发现两个模态的中心频率几乎一样,或者某个模态的时域波形接近零。原因:K 超过了信号实际包含的窄带分量数,多出来的模态没有能量可分配,只能挤在已有频率附近或者随机漂移。解决:把 K 减一重跑,直到所有中心频率两两间隔大于你关心的最小频率分辨率。如果减到 K=1 还有问题,检查信号本身是不是太短或太噪。
5.2 alpha 太小导致模态混叠,看起来像没分解
现象:分解出来的几个模态波形长得差不多,频谱上互相重叠。原因:alpha太小,频域惩罚不够,每个模态的带宽很宽,ADMM 收敛后模态之间没有真正分开。解决:把 alpha 提高一个数量级试试,比如从 500 提到 2000 或 5000。但注意 alpha 太大又会把宽频分量切碎,需要配合 K 一起调。
5.3 信号长度不是 2 的幂,FFT 性能下降
现象:长信号(比如 10 万点以上)跑 VMD 特别慢。原因:vmdpy内部用numpy.fft.fft,对非 2 的幂长度会走混合基算法,比 2 的幂慢不少。解决:如果实时性要求高,把信号截断或补零到最近的 2 的幂长度再跑。补零会引入边缘效应,截断会丢数据,我一般优先截断到 2 的幂,因为 VMD 对端点本来就不太敏感。
5.4 噪声大时 tau=0 导致模态毛刺多
现象:实测信号分解出来的模态波形上有很多高频毛刺,看起来不平滑。原因:tau=0假设重构无噪声,ADMM 会把噪声也分配到模态里。解决:把tau设成 0.1 到 0.3,给重构留松弛量。如果毛刺还在,先对信号做一次低通滤波再跑 VMD,但滤波会改变相位,做故障诊断时要谨慎。
5.5 同一段信号两次运行结果不一致
现象:同样的输入和参数,两次跑出来的模态顺序或波形有差异。原因:init=2随机初始化,或者tol设得太松导致收敛到不同局部最优。解决:把init固定为1,tol收紧到1e-7或更小。如果还不行,检查 numpy 随机种子有没有在别处被改动。做批量测试时,可复现性比单次结果好看更重要。
6. 进阶验证:用包络谱确认模态的物理意义
分解出模态只是第一步,真正要回答的是「这个模态对应什么物理部件」。做旋转机械诊断时,我习惯对每个模态做希尔伯特包络谱,看故障特征频率有没有被分离出来。下面这段代码对每个模态算包络谱:
from scipy.signal import hilbert def envelope_spectrum(modal, fs): """计算模态的包络谱""" analytic = hilbert(modal) env = np.abs(analytic) env = env - env.mean() # 去直流 spec = np.abs(np.fft.rfft(env)) freqs = np.fft.rfftfreq(len(env), 1/fs) return freqs, spec # 假设 u 是 VMD 输出的模态矩阵 for i, modal in enumerate(u): freqs, spec = envelope_spectrum(modal, fs) peak_idx = np.argmax(spec[1:]) + 1 # 跳过 0 Hz print(f"模态 {i}: 包络谱峰值 = {freqs[peak_idx]:.1f} Hz")如果某个模态的包络谱峰值出现在轴承外圈故障特征频率(比如转频的 3.5 倍)附近,那这个模态就是故障相关的。这一步能把「分解好看」变成「分解有用」。
验证 VMD 参数是否合理,还有一个我常用的技巧:把重构信号和原信号做相干分析,看哪些频带的重构误差大。如果误差集中在某个频带,说明那个频带的模态没分好,需要调整 K 或 alpha。相干函数用scipy.signal.coherence算,代码很短:
from scipy.signal import coherence f_coh, cxy = coherence(sig, recon, fs=fs, nperseg=256) bad_bands = f_coh[cxy < 0.9] print("相干性低于 0.9 的频带:", bad_bands[:10])相干性低于 0.9 的频带就是重构没覆盖好的地方,对着这些频率去调参数,比盲目扫 K 和 alpha 高效得多。
最后说个我自己的习惯:每次调 VMD 参数,我都会把 K、alpha、tau 和对应的中心频率、重构残差记在一个 CSV 里,跑几十组之后回头看,能很快找到适合当前信号类型的参数区间。VMD 不是设一次就一劳永逸的东西,不同工况、不同传感器、不同采样率下最优参数都会漂,留一份参数日志比记住某个「万能值」靠谱得多。希望帮到你。
本文还有配套的精品资源,点击获取