简介:这是关于变分模态分解(VMD)的MATLAB实现压缩包,面向需要处理非线性、非平稳实测离散信号的科研人员与工程师。VMD能够将原始信号自适应分解为多个具有不同频率特性的模态分量,相比傅里叶变换与小波变换更擅长捕捉瞬态与局部特征,适用于噪声抑制、信号恢复、特征提取等场景。包内仅含1个m脚本,整体体积约2KB,结构精简,用户可将自己的离散信号数据导入后调用VMD算法,并通过调整中心频率、迭代次数等参数获得分解结果,适合具备基础MATLAB编程能力的学习者快速上手。已有229人浏览学习,验证了该工具在实测信号分解中的实用价值。代码源自实际工程应用,具备直接运行与二次开发的参考意义,有助于理解VMD原理并迁移至自身数据中。
1. 变分模态分解不是玄学:实测信号分解为什么选 VMD
设备振动数据到手,随便做一版谱分析,低频频段总有几个谁也说不清的鼓包;包络分析做出来,该出的故障频率没出来,不该出的边带倒是排得整整齐齐。干过信号处理的人大概率都经历过这一幕。在处理多次实测轴承振动和齿轮箱噪声后,我把 VMD 作为拆解非平稳信号的主要手段,把一段原始离散信号按频段拆成若干条本征模态,每条模态各自对应一个窄带成分。VMD 尤其适合真实数据:噪声大、工况波动快、没有先验特征频率。这份资源提供的是 VMD.m 的 MATLAB 实现,可以直接载入实测信号做分解并拿到模态分量,省去从论文公式自己写代码的中间步骤。顺便说一句,搜索 VMD 时一半结果是 Intel 硬盘驱动,先停一下,这里说的 VMD 是变分模态分解,不是 RAID 驱动。
2. VMD 的核心逻辑:从反复筛 IMF 的递归,到变分约束下的全局寻优
2.1 EMD 的固有毛病:误差会顺着递归一路传到最后一个模态
经验模态分解(EMD)的思路是逐条提取 IMF:提取完一条,把剩余信号接着提下一条,直到残差单调为止。这种递归结构的致命问题是误差没有回头修正的机会。第一层要是提得不干净,后面所有模态都会继承这个残差;端点处极值延拓的细微差别,能一路扭曲到信号中部。学术上管这叫模态混叠、端点效应,实际干活时我看到的就是第一个 IMF 带着好几个频率成分,后面的 IMF 每个都长得差不多,根本没法解释物理意义。
VMD 换了个完全不同的思路,把“分解”变成了一个带约束的优化问题,全局求解。全部模态一次性解出来,第一个模态提“歪”了不会把误差单向传给后续模态,这是 VMD 相对 EMD 最本质的改进。它由 Dragomiretskiy 和 Zosso 在 2014 年前后提出来,属于完全自适应的分解方法,不需要像小波变换那样预先选基函数,也不像 EMD 那样靠极值包络递归。
对实测信号来说,EMD 的另一个问题是模态数量不受控。实测数据里噪声成分多,EMD 容易产出大量物理意义不明的 IMF,光靠肉眼挑模态就很费劲。VMD 允许使用者显式指定模态数 K,输出严格对应 K 条带限分量,你要几个就给几个,剩下的全算残差。这一点在工程场景里特别有用,因为故障特征往往就集中在少数频带。
2.2 VMD 把“分解”翻译成了带约束的最优化问题
VMD 的数学描述不复杂,核心假设是:每个模态 u_k 都是调幅调频信号,它的带宽是有限的,我们希望所有模态的带宽之和最小,同时所有模态相加后能完整重构原始信号。写成约束变分问题就是:在满足 f = Σu_k 的约束下,最小化各模态解调信号的梯度范数平方和。
这个最优化问题不能直接手解,VMD 用增广拉格朗日函数把它转成无约束问题,再用交替方向乘子法迭代求解。每次迭代里,先固定中心频率更新模态,再固定模态更新中心频率,最后更新拉格朗日乘子。迭代收敛后输出的就是一组带宽受限的模态 u_k 和它们各自的中心频率 ω_k。
这套机制带来的直接好处是:每个模态天然带一个窄带约束,相当于一个自适应的带通滤波器组。中心频率是优化出来的,不是靠人拍脑袋指定的;带宽由惩罚参数 alpha 控制,而非由采样率或滤波阶数决定。对离散实测信号,它工作在数字采样域,输入是一列离散时间序列,输出是 K×N 的模态矩阵,处理流程和 EMD 一样简单,却在数学框架上规避了递归误差传播。
VMD.m 实现的输入参数有七个:signal 是原始信号列向量,alpha 是带宽惩罚参数,tau 是噪声容限,K 是模态个数,DC 表示是否单独提取直流分量,init 是中心频率初始化方式,tol 是迭代收敛容差。理解了这七个参数,基本上就理解了 VMD 的全部控制旋钮。
2.3 为什么它适合离散实测信号
实测信号和仿真信号最大的差别在于:仿真信号你知道里面有几个成分,实测信号你不知道。现场采集的振动数据里,可能有转频及其谐波、轴承故障冲击、齿轮啮合频率、电磁干扰、随机噪声,它们频带互相交叠,幅度差异能到几个数量级。用固定滤波器去分,你得先知道特征频率在哪;用 EMD 去分,模态混叠让你分不清哪条是信号哪条是噪声。
VMD 的实践价值在于它把“不知道特征频率在哪”这个难题转化成“大概有几个频带”这个更容易回答的问题。即使你对 K 的估计不准,后续还可以根据中心频率序列修正。而且 VMD 内嵌维纳滤波结构,等价于每个模态都过了个自适应滤波器,对噪声有一定的抑制能力。处理强噪实测数据时,VMD 分解得到的模态一般比直接滤波结果更干净,因为它是全局优化的,不会像滑动窗滤波那样产生相位畸变。
作为离散模态分解工具,VMD.m 处理的输入输出全部是离散序列。你只需要把实测波形整成一列,设置好 K、alpha、tau 就能跑。接下来我用一份实际的振动信号走一遍完整流程,包含脚本、参数含义和输出整理。
3. 用 VMD.m 分解一段实测信号:从数据载入到模态输出
3.1 确认工作目录和主脚本结构
这个压缩包里核心文件是 VMD.m,一个标准 MATLAB 函数文件,不需要安装额外的工具箱,直接把文件放到当前工作目录即可调用。函数原型固定为:
function [u, u_hat, omega] = VMD(signal, alpha, tau, K, DC, init, tol)建议的工作目录结构是:原始数据文件(比如 vibration_signal.txt)、主脚本 run_vmd_example.m、VMD.m 三个文件放在同一层。主脚本负责读数据、设置参数、调用 VMD、画图保存,VMD.m 保持原样不修改。把调用脚本和函数分开的好处是,后续调参时不用动函数内部逻辑,只在脚本里改几个变量就行。
signal 必须是列向量,这是最常见的一个翻车点。MATLAB 读文本数据时,如果原始文件里每一行是一个采样点,读进来是列向量;如果每一行包含时间和幅值两列,取第二列之后依然是列向量,可以直接用。但万一数据文件里横着排的,load 出来是行向量,不转置直接传进 VMD,轻则报维度错误,重则分解结果完全错乱。所以脚本里我都会强制加一句 signal = signal(:),保证万无一失。
3.2 调用 VMD.m 的完整示例脚本
下面这段脚本以处理实测轴承振动信号为例。实际使用时把文件名、采样率和参数按自己的数据改掉就行。
% run_vmd_example.m % 注意:VMD.m 需与数据文件放在同一目录 clear; clc; close all; % ---- 1. 载入实测信号 ---- data = load('vibration_signal.txt'); % 假设是两列:[时间 振动] signal = data(:, 2); % 取振动幅值列 signal = signal(:); % 强制转成列向量,VMD 内部按列处理 fs = 25600; % 采样率,按实际采集参数修改 N = length(signal); t = (0:N-1) / fs; % ---- 2. 预处理与 VMD 参数 ---- signal = signal - mean(signal); % 去直流,否则低频大分量会压制冲击成分 alpha = 2000; % 带宽惩罚参数,第4章会细说怎么调 tau = 0; % 实测信号含噪时设 0,信号很干净才设 0.1 K = 4; % 模态数,先用频域峰值个数估计 DC = 0; % 不单独分解直流分量 init = 1; % 1:全部模态中心频率初始化为 0 tol = 1e-7; % 收敛容差,1e-7 一般够用 % ---- 3. 执行 VMD ---- [u, u_hat, omega] = VMD(signal, alpha, tau, K, DC, init, tol); % u 是 K×N 矩阵,每一行是一个模态的时域波形 % omega 是 K×1 向量,存的是各模态的归一化中心频率(范围 0~pi) % ---- 4. 时域波形 ---- figure; for k = 1:K subplot(K, 1, k); plot(t, u(k, :), 'LineWidth', 0.8); ylabel(['IMF', num2str(k)]); xlim([0 0.5]); % 只看前 0.5 秒,便于观察冲击 end xlabel('时间 (s)'); sgtitle('VMD 各模态时域波形'); % ---- 5. 频域波形 ---- figure; for k = 1:K subplot(K, 1, k); X = abs(fft(u(k, :))); % 对每个模态单独做 FFT,不依赖 u_hat f = (0:N/2-1) * fs / N; plot(f, X(1:N/2)); ylabel(['IMF', num2str(k)]); xlim([0 fs/2]); end xlabel('频率 (Hz)'); sgtitle('VMD 各模态频谱');这段脚本里我做了一个重要取舍:频域展示部分没有直接使用 VMD 返回的 u_hat,而是对每个模态重新做了一次 FFT。原因是不同版本的 VMD.m 函数里 u_hat 的排列方式不一定相同,有的是单边谱,有的是完整谱,还有的行列方向和 u 相反,直接取段很容易出错。自己用 fft 重新算一遍,虽然多几行代码,但排列方式完全确定,不会莫名其妙画出来一条空图。
参数设置方面,alpha=2000、K=4 是轴承振动数据比较常用的起点。如果你的信号里冲击特征很弱,K 可以先给 2,跑完看中心频率再往上加;tau 在实测强噪环境下必须给 0,给非零值会把噪声保留进模态,这个坑在第 5 章会专门讲。DC 参数这里设 0,因为我们已经在预处理阶段把均值减掉了,不需要 VMD 再单独拆一条直流模态。
3.3 输出整理:保存模态并校验重构残差
VMD 分解完不是终点,拿到 u 之后第一件事是检查重构误差。VMD 的约束是所有模态相加等于原始信号,但这个约束是迭代逼近的,不是精确相等。残差大小直接告诉你算法收没收敛,也帮你判断 K 够不够。
% ---- 6. 重构校验与保存 ---- residual = signal - sum(u, 1); % sum(u,1) 把 K 个模态叠加为 1 行 rms_res = rms(residual); % 计算残差的有效值 fprintf('重构残差 RMS = %.6e\n', rms_res); % 残差远大于噪声均方根,说明 K 设小了,还有成分没被分解出来 save('vmd_result.mat', 'u', 'omega', 'residual');稳定收敛的正常情况下,残差 RMS 应该在原信号 RMS 的 1% 以下。如果你发现残差明显偏大,甚至和某个模态的幅度一个量级,基本可以断定 K 给少了,信号里还有没拆干净的能量,这时需要回到参数设置环节调整。保存成 MAT 文件是为后续包络谱分析和特征提取做准备,避免每次都重新跑一遍 VMD。
到这里,你已经能完整跑通 VMD.m 并拿到模态结果了。但能跑通和能分解好完全是两回事,K 和 alpha 直接决定分解结果是物理可解释的模态还是数学上成立的假模态,下一章展开讲这两个参数到底怎么定。
4. 模态数和惩罚参数怎么调:K 与 alpha 的选择逻辑
4.1 一张参数表说明七个输入参数的定位和尺度
VMD 的七个参数不是等权的,真正需要反复调的是 K、alpha、tau 三个,其余四个基本能固定下来。我把常用取值整理成一张表,方便对照设置:
| 参数 | 含义 | 常用范围 | 拿不准时的起点 |
|---|---|---|---|
| K | 期望分解出的模态数 | 2~8 | 先按频谱主峰数量+1 |
| alpha | 模态带宽惩罚参数 | 500~5000 | 2000 |
| tau | 噪声容限 | 0 或 0.1~0.3 | 实测含噪信号直接给 0 |
| DC | 是否单独分解直流分量 | 0 或 1 | 已去均值就填 0 |
| init | 中心频率初始化方式 | 1 或 2 | 1 |
| tol | 迭代收敛容差 | 1e-9~1e-6 | 1e-7 |
这张表里最容易被忽视的是 init。init=1 表示所有模态中心频率初始化为 0,收敛路径稳定但可能陷入局部最优;init=2 表示在频域均匀初始化,适合信号频率成分分布很散的情况。跑出来的模态波形如果明显不合理,可以切换 init 重跑一次对比,这一步成本极低,但经常能解决“看起来不收敛”的问题。
4.2 K 怎么定:用中心频率序列判断欠分解与过分解
K 是 VMD 里最难给准的参数。K 给小了,多个不同频带的成分会被压进同一个模态,频谱图上出现多个鼓包挤在一起;K 给大了,某个真实成分会被拆成两条外观几乎相同但幅度随机分配的伪模态。这两种情况靠肉眼观察时域波形很难分辨,因为波形太像了。
我一般用中心频率序列来判断。VMD 输出的 omega 是 K 个模态各自的中心频率,按频带分离原则,正确的分解结果里 omega 应该严格递增且两两之间有明显间距。如果两个 omega 靠得特别近,基本可以断定发生了过分解。用下面这段循环脚本可以快速扫描 K:
% K 值扫描脚本:观察中心频率序列变化 for K_test = 3:7 [~, ~, omega_test] = VMD(signal, 2000, 0, K_test, 0, 1, 1e-7); fprintf('K = %d, omega = [%s]\n', K_test, num2str(omega_test', '%.3f ')); end判断逻辑很简单:K=3 时 omega 如果是 [0.10, 0.45, 1.20],K=4 时变成 [0.10, 0.44, 0.46, 1.20],说明多出来的第 3 条模态是凭空分裂的伪模态,真正的 K 应该定在 3 或 4 之间再进一步验证。反过来,如果 K=3 时第一条模态的频谱里有两个明显的峰值,说明欠分解,K 需要加 1。
经验做法是先对原始信号做一次功率谱估计,数一下明显谱峰的数量,K 初始值设成峰数+1。多出的这一个模态用来吸收底座噪声和不规则残余成分,比直接设成峰数更稳。跑完再按中心频率序列去验证。
这里有一个单位换算的坑:VMD 返回的 omega 是归一化角频率,范围在 0~π 之间,不是自然频率。想换算成实际频率 Hz,要用公式 f = omega × fs / (2π)。打印中心频率时如果发现数值超出 π,先怀疑信号输入维度或预处理环节出了问题,不要急着调 K。
4.3 alpha 怎么调:从频谱重叠和冲击保留两个角度判断
alpha 控制的是每个模态的带宽约束强度。alpha 越小,约束越松,模态带宽越大,不同模态之间频谱容易重叠;alpha 越大,约束越紧,模态带宽越窄,模态越接近纯正弦。问题是纯正弦往往丢掉信号里的冲击细节,而我们做故障诊断恰恰要靠冲击成分找到故障频率。
判断 alpha 是否合适,第一看相邻模态频谱有没有明显交叠。如果两条模态的频谱在 -20dB 处就叠在一起,说明 alpha 太小了。第二看关键模态的时域波形是否保留了冲击脉冲的窄尖特征。如果第 1 条模态从冲击波形变成了正弦波,说明 alpha 过大,把冲击细节削平了。
调 alpha 时不宜小步挪,我习惯从 500 到 5000 按 500 步长粗扫一轮,用分组循环逐个算重构残差和第一模态的包络谱峭度。包络谱峭度最大的一组,通常对应冲击保留最完整的情况。这个准则在滚动轴承故障数据上已经验证过多次,比单纯看频谱重叠要灵敏。
实际工程里还有一种偷懒但有效的做法:alpha 固定 2000,K 固定按频谱峰数+1,先跑通看结果,再根据“模态内频谱是否多峰”和“模态间频谱是否重叠”这两个信号做定向微调。大部分实测数据在 alpha=2000 附近就能得到可用的分解,真正需要大范围扫参的是信噪比极低的场合。
tau 的设置逻辑相对简单:实测信号普遍含噪,tau 给 0,让 VMD 不去刻意拟合噪声通道;仿真信号干净时,tau 给 0.1~0.3 可以加速收敛。核心原则是宁可让噪声残留进残差,也不让噪声污染模态。
5. VMD 实战排查:把不收敛、伪模态和端点振荡逐个摁住
5.1 现象:脚本运行半天,MATLAB 状态栏一直显示忙
这是 VMD 使用中最常见的卡死场景。现象很直接:主脚本停在 VMD 调用那一行,MATLAB 左下角显示“正忙”,几分钟不出结果。
原因通常有两个:一是 alpha 过大同时信号噪声偏强,迭代在目标函数上震荡,迟迟压不到 tol 以下;二是 init=1 的中心频率初值离真实值太远,收敛需要上百次迭代。解决方法是先把 tol 从 1e-7 放宽到 1e-6,alpha 降回 2000 附近,然后将 init 改成 2,让中心频率从均匀分布初始值出发。这三招里通常第二招就见效了。改完再不行,检查信号里是不是包含 NaN,NaN 会让迭代永远无法收敛。
5.2 现象:分解结果里出现两条波形几乎相同的模态
这属于典型的过分解伪模态。从时域看,两条模态波形高度相似,只是幅度各有大小;从中心频率看,omega 序列里有两个数值差不到 0.02 的频率项。
原因很简单:K 设大了。算法为了满足“必须输出 K 条”的约束,把一条物理成分的功率拆给了两条数学模态。处理方式不是去分析哪条模态更干净,而是直接减小 K。先打印 omega,确认是第几个模态在重复,然后把 K 减到重复出现之前的值。如果减 K 后发现残差显著增大,说明是欠分解而非过分解,这时候 K 反而要加。
5.3 现象:每条模态的两端都剧烈振荡,中间段正常
这是传导和 VMD 都逃不掉的端点效应,只是 VMD 的表现形式不同。现象是模态开头几十个点和结尾几十个点出现大幅波动,频率明显高于中间正常段,中间部分的波形倒是干净的。
原因是信号起点和终点不连续,变分约束在外推边界条件时失真,边界附近的解被拉偏。解决思路是镜像延拓:先让信号首尾平滑,再切掉延拓段。我一般按数据长度的 5% 做延拓,代码如下:
% 镜像延拓后分解,再截取中间段 N_ext = round(N * 0.05); signal_ext = [flipud(signal(1:N_ext)); signal; flipud(signal(end-N_ext+1:end))]; [u_ext, ~, omega_ext] = VMD(signal_ext, alpha, tau, K, DC, init, tol); u = u_ext(:, N_ext+1:N_ext+N); % 截掉延拓段,保留原始长度注意延拓后 VMD 输出的 u 矩阵列数比原来多了 2×N_ext,截取时前半段和后半段都要去掉。镜像延拓解决的是“边界振荡”而非“边界泄漏”,它能保证分解结果的首尾 5% 可用,但不会让中间段变化太多。实测信号如果只关心中间 80% 的时段,也可以不延拓,直接在分析时把首尾各 10% 剔除。
5.4 现象:分解后的模态里噪声残留明显,有效信号不突出
现象是某个模态的时域波形看起来还是毛茸茸的,冲击特征被噪声包住,频谱底抬高,包络谱上底盘噪声很高。
最常见原因是 tau 设成了非零值。VMD 的 tau 参数控制拉格朗日乘子的更新步长,非零 tau 会把部分噪声保留进模态,导致模态信噪比不升反降。实测强噪环境下的处理方式是 tau=0,让噪声尽量落进残差而不是模态。如果你已经设了 tau=0 还是噪声大,那多半是 alpha 太小导致模态带宽过宽,把旁边的噪声频带也包进来了,这时候调大 alpha 看看频谱底有没有降下来。
5.5 现象:报错信息里有“Data length mismatch”或维度不匹配
这类报错 90% 出在 signal 的行列方向上。VMD 要求输入是列向量,传进去一个行向量,内部运算时维度就全乱了。解决办法是在调用前强制转换:signal = signal(:)。另外数据文件里如果混入 NaN 或 Inf,也会导致中间计算数组维度异常。检查办法是加一行判断:
if any(isnan(signal)) || any(isinf(signal)) error('输入信号包含 NaN 或 Inf,请先清洗数据'); end清洗实测数据时我一般把 NaN 段直接删掉,而不是插值填充,因为插值会引入低频假成分,干扰后续模态的物理解释。
6. 把 VMD 用进故障诊断流程:模态包络谱与重构残差校验
VMD 分解只是前半程,模态到底对应什么物理成分,需要用包络谱来校验。以滚动轴承内圈故障为例,故障冲击会在第 1 条模态里形成间隔稳定的瞬态脉冲,把这条模态做希尔伯特变换求包络,再对包络做功率谱,就能看到故障特征频率及其倍频。代码很简短:
% 对第 1 条模态做包络谱分析 env = abs(hilbert(u(1, :))); env = env - mean(env); % 去直流 [pxx, f] = periodogram(env, hann(length(env)), [], fs); plot(f, pxx); xlim([0 2000]); xlabel('频率 (Hz)'); ylabel('包络谱幅值');如果包络谱上在某个特征频率处出现明显峰值,说明这条模态确实捕获了故障冲击成分,物理意义成立。反之,如果包络谱一片平坦,说明该模态大概率是伪模态或不含瞬态特征的分量,需要回看 K 和 alpha 的设置。
我处理实测信号时还有一个固定动作:对比 VMD 模态与原始信号的时域冲击点。把原始信号里的冲击时刻找出来,再检查这些时刻是否出现在第 1 条模态的时间轴上。如果冲击时刻对得上,说明分解保留了时序关系;对不上,说明分解过程发生了相位失真,这条结果就不适合做时频定位。这一步花不了十秒钟,却能避开大量后期解释上的麻烦。
重构残差校验放到整个流程的出口再跑一次也不迟。残差 RMS 在信号 RMS 的 1% 以下,并且残差频谱里没有明显的窄带峰,说明 VMD 已经把主要成分拆完了,剩下的纯噪声可以放心丢弃。残差里只要出现窄带峰,就说明还有成分没拆干净,K 还得加。
从那以后,我每次拿到一段新的实测信号都强制走一遍这个流程:先扫 K 看中心频率序列,再定 alpha 看频谱重叠,跑完看残差和包络谱,最后才敢把分解结果用在报告里。VMD 是个好工具,但好工具也得按步骤来。希望这篇文章能帮你把 VMD.m 用起来,少走几趟弯路。
本文还有配套的精品资源,点击获取