ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

EMD分解MATLAB程序:IMF筛分、端点效应与故障诊断

EMD分解MATLAB程序:IMF筛分、端点效应与故障诊断

简介:面向信号处理学习者与科研人员的EMD经验模态分解MATLAB实现,可用于非线性、非平稳信号的分解、IMF提取及希尔伯特变换分析,适合振动分析、故障诊断、心电与地震波等场景的入门复现与算法学习。压缩包共44个文件,以38个m脚本为核心,另含5个asv备份和1个fig图形文件,整体约90KB,体积轻便,便于直接查看分解流程、包络线构建、迭代停止条件与结果展示。已有1316人学习,说明其在EMD程序复现方面具有一定参考价值。读者可据此理解从信号预处理、上下包络线插值、IMF筛选到趋势项输出的完整思路,并对照复杂谐波、间谐波、暂降与中断等示例,掌握参数调整和结果判读方法,为后续HHT分析打基础。

1. 拆开一段振动信号:EMD分解MATLAB程序能解决什么

某开发者拿到一段轴承外圈的振动数据,采样率两万赫兹,时域上冲击被背景噪声盖住,FFT 谱上边带糊成一团,看不出特征频率。他把这段信号喂进一套 EMD分解MATLAB程序,先分解再对前两阶 IMF 做包络谱,冲击周期一下就清楚了。真正的转折点不在"分解"本身,而在于他终于能把一个非平稳信号拆成若干个近似单频的分量,再逐个下功夫。

但 EMD 不是"一键出特征"的灵丹妙药。同一套代码,端点延拓少写几行、SD 阈值随手设成 0.05、输入用行向量而不是列向量,分解结果就是端点发散、IMF 飞边。这套 EMD分解MATLAB程序适合做旋转机械故障诊断、脑电与肌电分析、地震波处理、语音基频提取的工程人员,也适合刚接触时频分析、想读懂"筛分"到底在筛选什么的学生。它解决的核心问题是:把一个多分量、频率随时间变化的信号,自适应地拆成有限个本征模态函数和一个残差,不需要你预先指定基函数。

2. 程序骨架:主函数、筛分函数与文件是怎么组织的

一套能用的 EMD 程序,文件数量不会太多,但职责必须分清。常见做法是把"整体控制"和"单次筛分"拆开,主函数循环剥出每一阶 IMF,筛分函数只负责把一个分量反复减去上下包络均值,直到满足 IMF 条件。这样拆的好处是:调停止准则时只动一个文件,换端点延拓策略时也只动一个文件,不会牵一发动全身。

2.1 目录结构与文件职责

下面这套文件组织是我在多个项目里反复用过的版本,函数名按 MATLAB 的命名习惯来,放到 path 里就能直接调用。

文件名职责被谁调用
emd_decompose.m主入口,控制 IMF 阶数与外层循环用户脚本
sift_once.m单次筛分,返回一个候选分量emd_decompose.m
find_extrema.m定位局部极大值、极小值索引sift_once.m
envelope_spline.m三次样条拟合上下包络sift_once.m
extend_signal.m端点延拓,返回加长后的信号与偏移量emd_decompose.m
stop_criteria.mSD 判据与 IMF 判定sift_once.m
demo_signal.m演示脚本,合成信号并画 IMF用户手动运行

这套划分有个隐含约定:所有函数只处理列向量,行向量在入口处统一转置。血泪经验是,MATLAB 里spline对行、列向量的返回维度跟着输入走,一旦某一步混用,后面sum(h.^2)会得到标量而不是逐点结果,SD 判据直接失效,分解就停在第一层不动了。

2.2 主流程:一个信号从进入到出 IMFs

主函数的结构可以写得很直白:延拓、初始化残差、外层循环、每轮调筛分、剥掉已得分量、判断残差是否还能继续分解。下面这段是主流程的骨架代码。

function [imf, res, info] = emd_decompose(x, opt) % EMD_DECOMPOSE 经验模态分解主入口 % 输入: % x - 列向量,原始信号 % opt - 参数结构体,字段见 2.3 节 % 输出: % imf - n x k 矩阵,每列一阶 IMF % res - 列向量,最终残差 % info - 结构体,记录每阶的筛分次数与 SD 终值 x = x(:); % 强制列向量,避免维度混用 N = numel(x); t = (0:N-1)'; % 采样点索引,也可换成真实时间轴 % 端点延拓,n_add 为左右各延拓的点数 [x_ext, n_add] = extend_signal(x, opt.n_ext); res = x_ext; % 残差初值为延拓后的信号 imf = zeros(N, opt.max_imf); % 预分配,防止循环内扩容 info.niter = zeros(opt.max_imf, 1); info.sd = zeros(opt.max_imf, 1); k = 0; while k < opt.max_imf [idx_max, idx_min] = find_extrema(res); % 极值点总数不足 3 个,说明剩下的已是趋势项 if numel(idx_max) + numel(idx_min) < 3 break; end [h, niter, sd_end] = sift_once(res, t, opt); if isempty(h) break; end k = k + 1; imf(:, k) = h; info.niter(k) = niter; info.sd(k) = sd_end; res = res - h; % 剥掉当前 IMF end imf = imf(:, 1:k); % 去掉延拓段,只保留原信号长度对应的部分 imf = imf(n_add+1 : n_add+N, :); res = res(n_add+1 : n_add+N); end

逐段说明这段代码的意图。x = x(:)是防御性写法,后面所有运算都建立在列向量前提下。extend_signal返回延拓后的信号和左侧新增点数n_add,剥完 IMF 之后要按这个偏移量裁回原始长度,否则输出的 IMF 比输入长一截,画图时时间轴对不上,这是新手最容易忽略的一步。

imf预分配成N x max_imf,循环里只填前k列,最后用imf(:, 1:k)截断。如果不预分配而是每轮imf = [imf, h],信号长度上万、阶数十几阶时,内存拷贝的耗时会明显上升。info结构体记录每阶的筛分次数和终值 SD,排查"某一阶是不是筛过头了"时非常有用。

res - h这一步是 EMD 的核心递推:每得到一阶 IMF 就从残差里减掉,剩下的继续分解。终止条件有两个:IMF 阶数达到max_imf,或残差极值点不足 3 个。后者对应"残差已是单调趋势或常数",再分下去也没有物理意义。

提示:t用采样点索引还是真实时间轴,只影响样条插值的自变量尺度,不影响分解结果的形状。但如果采样间隔不均匀,必须传真实时间轴,否则包络拟合会偏。

2.3 参数接口与调用方式

参数全部塞进一个结构体,调用时只改需要改的字段,其余走默认值,这是 MATLAB 里比较省心的写法。

字段默认值作用调参建议
max_imf8最多分解几阶 IMF按信号复杂度设,一般 5~10
sd_th0.2筛分停止的 SD 阈值0.2~0.3,太小筛过头,太大分量不纯
max_iter15单阶最大筛分次数防止死循环,10~20
n_extround(0.1*N)左右各延拓点数取信号长度 10%~20%
interp'spline'包络插值方式三次样条最常用,'pchip'可抑制过冲

调用方式很轻:

opt = struct('max_imf', 6, 'sd_th', 0.2, 'max_iter', 12, 'n_ext', 200); [imf, res, info] = emd_decompose(x, opt); t = (0:numel(x)-1) / fs; figure; for i = 1:size(imf, 2) subplot(size(imf,2)+1, 1, i); plot(t, imf(:, i)); ylabel(sprintf('IMF%d', i)); end subplot(size(imf,2)+1, 1, size(imf,2)+1); plot(t, res); ylabel('Res');

n_ext最好和采样率挂钩:采样率高、信号周期多,延拓点数可以取大一些;采样率低、只有几百个点,延拓太多反而把样条外推的不稳定引入内部。我一般先用默认值跑一遍看端点,再决定要不要加。

3. 核心算法拆解:筛分循环、极值点与三次样条包络

EMD 的数学描述只有一句话:不断减去上下包络的均值,直到剩下的分量满足 IMF 条件。但把这句话翻译成代码,有三处细节决定成败——极值点怎么找、包络怎么插、什么时候停。

3.1 筛分循环:一个分量是怎么被"筛"出来的

筛分函数的输入是当前残差,输出是一个候选 IMF。每轮先找极值点,拟合上下包络,算均值,再相减,然后用 SD 判据决定是否继续。

function [h, niter, sd_end] = sift_once(x, t, opt) % SIFT_ONCE 单次筛分,从 x 中提取一阶 IMF % 输入: % x - 当前残差信号(列向量,已延拓) % t - 与 x 等长的时间轴 % opt - 参数结构体 % 输出: % h - 候选 IMF;失败时返回空 % niter - 实际筛分次数 % sd_end - 最后一次迭代的 SD 值 h = x; sd_end = inf; for niter = 1:opt.max_iter [idx_max, idx_min] = find_extrema(h); % 极值点太少,无法拟合包络 if numel(idx_max) < 2 || numel(idx_min) < 2 h = []; return; end [e_max, e_min] = envelope_spline(t, h, idx_max, idx_min, opt.interp); m = (e_max + e_min) / 2; % 上下包络均值 h_new = h - m; % 减去均值 h_new = h_new(:); % SD 判据:两次筛分结果的相对能量差 sd = sum((h - h_new).^2) / (sum(h.^2) + eps); sd_end = sd; h = h_new; if sd < opt.sd_th break; end end end

h = x之后进入 for 循环,每一轮都重新找极值、重新拟合包络,这是因为减去均值后极值点位置会变,包络必须跟着更新。numel(idx_max) < 2的判断放在循环内而不是循环外,是因为筛分几轮之后可能出现极值点退化的极端情况,提前返回空值比硬插值更安全。

sd的计算用的是相邻两次结果的能量比,分母加eps防止全零信号除零。这个判据来自经典的筛分停止准则,物理含义是"这一轮减掉的均值相对于当前分量已经足够小"。它比单纯限制迭代次数更合理,因为不同信号的收敛速度差别很大。

3.2 极值点查找与三次样条包络

找极值点看起来简单,实际有两个坑:平顶段会出现连续相等的点,diff(sign(diff(x)))在平顶处得到 0,真正的极值点会被漏掉或重复计数;信号首尾附近的极值点如果离端点太近,样条外推会剧烈发散。

function [idx_max, idx_min] = find_extrema(x) % FIND_EXTREMA 定位局部极大值与极小值索引 % 采用差分符号变化判断,平顶段做合并处理 x = x(:); d = diff(x); s = sign(d); % 极大值:diff 由正变负 idx_max = find(s(1:end-1) > 0 & s(2:end) < 0) + 1; % 极小值:diff 由负变正 idx_min = find(s(1:end-1) < 0 & s(2:end) > 0) + 1; % 平顶段处理:若存在连续相等点,取中点作为极值位置 idx_max = unique(idx_max); idx_min = unique(idx_min); % 去掉紧邻端点的极值点,避免样条外推发散 margin = 2; idx_max = idx_max(idx_max > margin & idx_max < numel(x) - margin + 1); idx_min = idx_min(idx_min > margin & idx_min < numel(x) - margin + 1); end

find(s(1:end-1) > 0 & s(2:end) < 0) + 1这一行是核心:s是差分符号序列,前一点为正、后一点为负,说明中间那个点就是极大值,所以索引要加 1。这样写比diff(sign(diff(x)))更直观,也避免了二阶差分在平顶处的符号跳变。

margin = 2是我加的保护:离端点两个点以内的极值点不参与包络拟合。原因是三次样条在边界附近的曲率估计依赖邻近点,如果极值点紧贴端点,样条会为了穿过它而产生巨大的外推幅值,表现出来就是 IMF 首尾飞边。

包络函数的实现要短:

function [e_max, e_min] = envelope_spline(t, x, idx_max, idx_min, interp) % ENVELOPE_SPLINE 用极值点插值得到上下包络 % interp 可选 'spline'(三次样条)或 'pchip'(保形插值) e_max = interp1(t(idx_max), x(idx_max), t, interp, 'extrap'); e_min = interp1(t(idx_min), x(idx_min), t, interp, 'extrap'); e_max = e_max(:); e_min = e_min(:); end

spline插值在极值点稀疏时容易过冲,尤其是信号里有冲击成分时,包络会在冲击两侧鼓起来,把真实分量带偏。换成pchip保形插值能缓解,代价是包络不够光滑,筛分收敛慢一些。我的习惯是先用spline跑,如果 IMF 里出现明显不属于原信号的振荡,再换pchip对比。

3.3 停止准则:SD 阈值与 IMF 判定

IMF 的严格定义有两条:整个数据段内极值点数量与过零点数量之差不超过 1;任意时刻上下包络均值为零。代码里不可能严格满足第二条,所以用 SD 阈值近似。

判据阈值特点适用场景
SD 相对误差0.2~0.3计算简单,收敛可控大多数振动、语音信号
固定迭代次数8~15不依赖阈值,结果稳定批量处理、实时性要求高
极值点-过零点差≤1更接近 IMF 定义对分量纯度要求高

SD 设得太小,比如 0.05,筛分会把噪声也当成 IMF 的一部分反复剥离,同一阶分量被拆成两阶,后面所有 IMF 都跟着错位;设得太大,比如 0.5,第一轮减完均值就退出,分量里混着低频趋势,包络谱上什么都看不出来。0.2 是个折中值,多数论文和工程实现都落在这个量级。

3.4 残差与终止条件

外层循环的终止条件写在主函数里,但真正决定"还能不能继续分"的是残差本身的极值点数量。如果残差已经是单调函数,说明剩下的只是趋势项,再分解也只会得到没有物理意义的低频分量。我的处理是极值点总数小于 3 就停,同时在info里记录停在了第几阶,方便回头检查是不是max_imf设小了。

另一种情况是残差幅值已经降到原信号的千分之一以下,这时继续分解得到的是数值噪声。可以在循环里加一条if max(abs(res)) < 1e-3 * max(abs(x)), break; end,避免输出一堆幅值接近零的"假 IMF"。

4. 端点效应与模态混叠:两个必须处理的工程问题

EMD 被诟病最多的两点就是端点效应和模态混叠。前者是样条插值在信号两端没有约束,包络外推发散,误差从端点向内部传播;后者是信号里存在间断或噪声时,同一阶 IMF 里混进了差异很大的时间尺度。这两件事不处理,分解结果看着有模有样,拿去做包络谱就是错的。

4.1 端点效应:镜像延拓、极值延拓与剔除端点段

镜像延拓是最容易实现的方案:以端点为对称轴,把信号向外翻折一段,拟合包络后再裁掉。它的假设是端点附近的波形近似对称,对周期成分明显的振动信号效果不错。

function [x_ext, n_add] = extend_signal(x, n_ext) % EXTEND_SIGNAL 镜像延拓,缓解端点处样条外推发散 % 输入: % x - 原始列向量 % n_ext - 左右各延拓的点数 % 输出: % x_ext - 延拓后的信号 % n_add - 左侧新增点数,用于最后裁回原长度 x = x(:); n_ext = min(n_ext, numel(x) - 1); % 延拓长度不能超过信号长度 left = flipud(x(1:n_ext)); % 左端镜像 right = flipud(x(end-n_ext+1:end)); % 右端镜像 x_ext = [left; x; right]; n_add = n_ext; end

flipud把端点附近的一段反过来接在两边,n_ext用min限制不超过信号长度,否则短信号会延拓出比原信号还长的镜像段,样条反而更不稳定。这种简单镜像的缺点是端点处一阶导数不连续,包络在接缝处会有一个小折角。改进做法是"偶延拓":左端用2*x(1) - x(n_ext:-1:1),保证端点值连续,代价是多一次乘法。

除了镜像延拓,还有两种常见处理。极值延拓是在端点外补一个极值点,位置按最近两个极值点的间隔外推,幅值取端点值,适合极值点分布均匀的信号。多项式拟合是在端点附近拟合一段低阶曲线再外推,适合趋势明显的信号,但阶数选高了会引入额外振荡。工程上最省事的其实是第三种:不延拓,直接在最后裁掉首尾各 5%~10% 的数据,用中间段做分析。端点效应的影响范围大致就是几个极值点间隔,裁掉这一段,剩下的 IMF 是干净的。

4.2 模态混叠:从 EEMD 到 CEEMDAN 的思路

模态混叠的典型表现是:第一阶 IMF 里既有高频冲击,又有明显的低频起伏,频率成分不单一。根源是信号里存在间断点或强噪声,极值点分布不均匀,包络拟合把不同尺度的成分混在一起。

EEMD 的思路是加噪辅助:对原信号叠加多组不同实现的白噪声,各自做 EMD,再对同一阶 IMF 取平均。白噪声把间断点"抹平",让极值点分布均匀,平均之后噪声相互抵消。

function [imf_ens, res_ens] = eemd(x, NE, sigma, opt) % EEMD 集合经验模态分解 % 输入: % x - 原始信号 % NE - 集合次数,常用 100~200 % sigma - 噪声幅值系数,常用 0.2(相对信号标准差) % 输出: % imf_ens - 平均后的 IMF 矩阵 % res_ens - 平均残差 x = x(:); N = numel(x); sd_x = std(x); imf_all = []; for k = 1:NE w = sigma * sd_x * randn(N, 1); % 每组独立的白噪声 [imf_k, ~] = emd_decompose(x + w, opt); if isempty(imf_all) imf_all = zeros(N, size(imf_k, 2), NE); end % 阶数不一致时按最小阶数对齐 ncol = min(size(imf_k, 2), size(imf_all, 2)); imf_all(:, 1:ncol, k) = imf_k(:, 1:ncol); end imf_ens = mean(imf_all, 3); res_ens = x - sum(imf_ens, 2); end

sigma取 0.2 倍标准差是个经验值,噪声太小起不到均匀极值点的作用,太大则会引入原本不存在的分量。NE取 100 以上平均效果才稳定,代价是计算量翻百倍,一段一万点的信号跑几分钟很正常。如果只是做离线分析,这个代价可以接受;要做实时处理,就得换 CEEMDAN,它在每阶分解后自适应地加噪,集合次数可以降到几十次。

提示:EEMD 的平均结果里残留的噪声幅值约为sigma / sqrt(NE),NE 取 100、sigma 取 0.2 时残噪约 2%,对包络谱影响很小。如果发现 IMF 里还有高频毛刺,优先加大 NE 而不是加大 sigma。

4.3 分解层数与有效分量筛选

EMD 会把信号一直分到残差单调为止,但真正有物理意义的往往只有前几阶。判断哪几阶有用,常用三个指标:与原信号的相关系数、能量占比、以及瞬时频率是否落在感兴趣的频带内。

相关系数低于 0.1 的 IMF 基本可以认为是噪声或数值伪影;能量占比突然从百分之几十掉到百分之几的那一阶,通常是真实分量与噪声的分界。我的做法是先算相关系数,再把前几阶的包络谱画出来,看特征频率是否落在预期位置,两条都对上才认为这一阶可信。不要迷信"分解阶数越多越好",多余的阶数只会让后续分析多几个干扰项。

5. 常见问题排查:五类跑不出正确 IMF 的场景

EMD 代码不长,但出错时的表现很隐蔽:不报错、不崩溃,只是输出的 IMF 不对。下面这几类是我调试时反复遇到的,按"现象、原因、解决"记下来,能省不少时间。

5.1 只分解出一阶,或者直接返回原信号

现象:调用emd_decompose后,imf只有一列,res几乎等于输入信号,info.niter显示第一阶只筛了一两次就退出。

原因通常有三个。输入是行向量,find_extrema里diff沿错误维度做差分,极值点索引全乱;信号本身极值点太少,比如只有几十个采样点的缓变信号,第一轮就触发numel(idx_max) < 2返回空;sd_th设得过大,第一轮减完均值就满足退出条件。

解决:在入口处强制x = x(:),并在find_extrema开头也加一次,双重保险;检查信号长度,少于 100 个点时 EMD 本身就不适用,换其他方法;把sd_th降到 0.2 以下重跑,观察info.sd数组是不是每阶都在阈值附近收敛。如果info.niter全是 1,基本可以确定是阈值问题。

5.2 IMF 首尾飞边严重,幅值远超原信号

现象:画出来的 IMF 在两端出现大幅振荡,幅值是信号本身的几倍甚至几十倍,中间段却正常。

原因:样条插值在端点外推时没有约束,如果端点附近的极值点离边界太近,三次样条的曲率估计会失控。另一个常被忽略的原因是n_ext设得比信号长度还大,镜像段把样条带偏。

解决:把n_ext限制在信号长度的 10%~20%,并在find_extrema里用margin剔除紧邻端点的极值点;或者干脆改用pchip插值,它的保形特性可以抑制过冲;最稳妥的办法是分析时裁掉首尾各 5% 的数据,只保留中间段做包络谱。三种方法可以叠加使用,我一般先裁端点,再调延拓长度。

5.3 报错 "Not enough extrema" 或样条插值报索引越界

现象:运行到某一阶时 MATLAB 抛出错误,提示极值点不足,或者interp1报 "The grid vectors must be strictly monotonically increasing"。

原因:t或极值点索引里出现重复值。比如信号有平顶段,find返回的索引虽然做了unique,但相邻极值点可能落在同一个采样点上;或者手动构造的t没有严格单调,采样间隔不均匀时尤其容易出问题。

解决:在find_extrema里对索引做unique之后,再检查相邻索引差是否大于 0,差值等于 0 的合并掉;t在入口处用t = (0:N-1)'重建,不要直接拿外部传入的时间向量,除非确认它严格单调。如果必须用真实时间轴,先做t = cumsum([0; diff(t)])强制单调化。

5.4 模态混叠明显,同一阶 IMF 里频率成分不单一

现象:第一阶 IMF 的包络谱上出现两个相距很远的频率峰,或者时域上高频冲击叠在低频起伏上。

原因:信号里有间断点、脉冲噪声,或者两个分量的频率比接近 1,极值点分布不均匀,包络拟合把不同尺度混在一起。这是 EMD 的固有缺陷,不是代码写错了。

解决:先做一次简单滤波或中值滤波去掉脉冲噪声,再分解;如果信号本身包含间断,改用 EEMD,NE取 100~200,sigma取 0.2 倍标准差;分解后如果某一阶仍然混叠,可以单独对这一阶再做一次 EMD,相当于二次筛分。实测下来,EEMD 能解决大部分混叠,代价是计算时间。

5.5 运行慢,长信号要跑好几分钟

现象:一万点以上的信号,单次 EMD 就要几十秒,EEMD 更是等到怀疑人生。

原因:筛分循环里每轮都重新spline插值整段信号,而样条的计算量与点数成正比;循环内如果还有动态数组扩容,内存拷贝的开销会叠加。MATLAB 的interp1默认走分段线性以外的路径时,对大规模数据并不快。

解决:把spline换成自己写的三对角求解,只对极值点区间做插值,中间段用线性插值近似,速度能快几倍;EEMD 的集合循环用parfor并行,前提是每组的随机噪声独立;如果只是看趋势,把信号降采样到两千点以内再分解,频率成分不会丢,因为 EMD 关心的是极值点相对间隔而不是绝对采样率。降采样前记得先做抗混叠滤波,否则高频折叠进来会让分解结果更乱。

6. 二次开发落地:把 EMD 接进自己的信号链路

这套代码拿回去,第一件事不是改算法,而是把它包成一个符合自己项目规范的函数。我见过太多人直接把emd_decompose.m拖进工程,参数散落在脚本各处,换一段信号就要改五六个地方,最后自己都忘了当时设的什么值。

6.1 包装成带默认参数的工程函数

比较省心的做法是用inputParser或arguments块把参数收拢,默认值写死,调用时只传信号和两三个关键参数。

function [imf, res, feat] = emd_feature(x, fs, varargin) % EMD_FEATURE 面向业务的封装:分解 + 包络谱特征提取 % 输入: % x - 原始信号 % fs - 采样率 % varargin - 名值对,可覆盖 max_imf / sd_th / n_ext % 输出: % imf - IMF 矩阵 % res - 残差 % feat - 结构体,含前 3 阶 IMF 的包络谱峰值频率 p = inputParser; p.addParameter('max_imf', 6); p.addParameter('sd_th', 0.2); p.addParameter('n_ext', round(0.1*numel(x))); p.parse(varargin{:}); opt = p.Results; [imf, res] = emd_decompose(x(:), opt); feat.peak_freq = zeros(1, min(3, size(imf,2))); for i = 1:numel(feat.peak_freq) env = abs(hilbert(imf(:, i))); % 包络 env = env - mean(env); N = 2^nextpow2(numel(env)); P = abs(fft(env, N)) / N; f = (0:N-1) * fs / N; [~, idx] = max(P(1:floor(N/2))); feat.peak_freq(i) = f(idx); end end

这个封装把"分解"和"取特征"串成一步,业务脚本里只写[imf, res, feat] = emd_feature(x, fs)就够了。inputParser的好处是参数名自解释,新同事接手时不用翻注释就知道能调什么。hilbert取包络那一步要注意先减均值,否则 FFT 的零频分量会盖过真实峰值。

6.2 用合成信号验证改造是否破坏了原逻辑

改动算法之后,最怕的是"看起来能跑,其实结果偏了"。我的习惯是准备一段合成信号做回归测试:两个正弦叠加一个冲击,频率和出现时刻都已知,分解出来的 IMF 阶数、主频、冲击位置应该和预期对得上。

fs = 2000; t = (0:1/fs:2-1/fs)'; x = 0.8*sin(2*pi*30*t) + 0.5*sin(2*pi*120*t); x(800:810) = x(800:810) + 1.5; % 人为加一个冲击 [imf, res, info] = emd_decompose(x, struct('max_imf',4,'sd_th',0.2,'n_ext',200)); fprintf('分解阶数: %d\n', size(imf,2)); fprintf('第一阶主频: %.1f Hz\n', ...);

验证时看三个数:分解阶数是否符合预期(两个正弦加冲击,通常出 3~4 阶)、第一阶 IMF 的主频是否接近 120 Hz、冲击位置对应的样点是否在 800 附近。任何一项偏了,说明刚才的改动动了筛分逻辑。回归脚本不用长,但每次改sift_once或find_extrema都要跑一遍。

还有一个容易被忽略的验证点:把imf各列和res加起来,应该等于原始信号(裁掉延拓段之后),误差在1e-10量级。这条恒等式是 EMD 的分解完备性,如果加起来对不上,说明某处裁剪偏移量算错了,或者res - h那一步写成了res = h。

从那以后我每次改 EMD 相关代码,都强制先跑一遍合成信号回归,再看真实数据的包络谱,两条都对上才敢往工程里合。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进