CEEMDAN非平稳信号分解:从EMD模态混叠到参数配置与验收

发布时间:2026/10/28 23:52:30
CEEMDAN非平稳信号分解:从EMD模态混叠到参数配置与验收

简介:面向从事非线性、非平稳信号分析的研究生、算法工程师与科研人员,这套 CEEMDAN 实现可作为从 EMD、EEMD 过渡到自适应噪声完备集成经验模态分解的实践材料。其核心在于按信号特性动态加入噪声,经多次 EMD 迭代与 IMF 平均抑制模态混叠,并兼顾收敛速度与分解稳定性,常用于地震波解析、心电与脑电处理、金融序列分析及机械故障诊断等场景。压缩包共 5 个文件,以 4 个 .m 算法脚本为主体,另附 1 个 .mat 心电示例数据,整体约 32KB,体量轻便,便于直接放入 MATLAB 环境运行和对照。已有 5935 人学习下载。读者可借助脚本走通加噪、迭代分解、平均与结果分析流程,结合示例数据复现实验,观察不同 IMF 的振荡与幅值变化,并在此基础上修改参数、替换数据,形成自己的分解与验证思路。

1. 从一次分解失败说起:CEEMDAN 把 EMD 的模态混叠掰回来

一段 1 秒、采样率 1 kHz 的振动信号,肉眼看得出 50 Hz 的调幅成分和 120 Hz 的冲击串,可 EMD 跑完,第一阶 IMF 里两种东西搅在一起,第二阶又冒出本该在第一阶的高频毛刺——这就是模态混叠,EMD 最要命的地方。EEMD 的解法是往信号里灌白噪声、分解上百次再平均,混叠被摊开,代价是噪声残留和成倍的计算量。CEEMDAN 换了思路:白噪声不再一次性加在原始信号上,而是逐阶加进残差,每分解出一阶就立刻扣除,重构误差和噪声残留同时被压下去。做机械振动、声发射、心电、风速序列这类非平稳信号分解的人,如果已经被 EEMD 的参数瞎调折腾过几轮,CEEMDAN 值得进工具箱。

2. EMD 的地基:三次样条包络、筛分停止准则与端点延拓

2.1 分解目标是什么:本征模态函数的两个硬条件

EMD 不预设基函数,它把信号拆成一串本征模态函数(IMF)加一个单调残差。一个分量要算得上 IMF,得同时满足两条:极值点个数与过零点个数之差不超过 1;上包络和下包络的局部均值为零。第一条保证它像一条窄带振荡,第二条保证它关于零轴局部对称,不会带着趋势跑。

算法本身是个筛分循环:找出信号全部局部极大值和极小值,分别用三次样条插值出上包络 $e_{max}(t)$ 和下包络 $e_{min}(t)$,取均值 $m(t)=(e_{max}+e_{min})/2$,令 $h(t)=x(t)-m(t)$;判断 $h$ 是否满足 IMF 条件,不满足就把 $h$ 当成新信号再筛一遍;满足则记为一阶 IMF,用 $r_1=x-c_1$ 替换原信号,继续下一阶,直到残差只剩两个极值点或完全单调。

这个循环有两个地方最容易被忽略。一是停止准则,Huang 原文用连续两次筛分结果的归一化平方差 $SD=\sum[(h_{k-1}-h_k)^2/h_{k-1}^2]$ 卡在 0.2~0.3,实际工程里更常用固定筛分次数,因为 SD 阈值对结果的影响比想象中大。二是包络插值,三次样条在极值点之间穿过,边界外没有极值点约束,样条会自由外推,这就是端点效应的源头。

和傅里叶、小波比,EMD 的优势是基自适应:小波要选母函数和分解层数,EMD 让数据自己决定尺度;代价是分解结果不唯一,换一个库、换一组参数,IMF 的层数和波形都会变。这一点在后面做横向对比时非常关键,不能拿一次分解的结果当物理真值。

2.2 一份能跑的 EMD 基线:从造信号到 IMF 落袋

上手最快的路径是 Python 的 PyEMD(安装包名EMD-signal,导入名PyEMD),它把 EMD、EEMD、CEEMDAN 放在同一个 API 风格下,便于横向对比。先装依赖:

pip install EMD-signal numpy scipy matplotlib

然后造一段成分已知的仿真信号,这一步不是多余的:只有知道原始成分长什么样,后面才能判断哪个 IMF 对上了哪个物理量。

import numpy as np import matplotlib.pyplot as plt from PyEMD import EMD # 1) 造仿真信号:50Hz 调幅 + 120Hz 冲击串 + 4Hz 低频 + 白噪声 fs, N = 1000, 1000 t = np.linspace(0.0, 1.0, N, endpoint=False) am = 1.0 * np.sin(2 * np.pi * 50 * t) * (1 + 0.5 * np.cos(2 * np.pi * 5 * t)) impulse = 0.6 * np.exp(-30 * (t % 0.1)) * np.sin(2 * np.pi * 120 * t) trend = 0.8 * np.sin(2 * np.pi * 4 * t) rng = np.random.RandomState(0) x = am + impulse + trend + 0.05 * rng.randn(N) # 2) 配置 EMD:固定筛分次数 + 边界延拓点数,保证可复现 emd = EMD() emd.FIXE = 10 # 每个 IMF 固定筛 10 次,替代 SD 阈值 emd.FIXE_H = 0 # 与 FIXE 二选一,同时设置时 FIXE 优先 emd.nbsym = 2 # 两端各取 2 个极值点做镜像延拓 emd.MAX_ITERATION = 1000 # 单阶筛分上限,防止死循环 # 3) 分解,并把 IMF 与残差拆开 IMFs = emd(x) # 最后一行是残差 imfs, res = emd.get_imfs_and_residue() print("IMF 层数:", imfs.shape[0]) # 4) 完备性校验:ΣIMF + 残差 应无损还原原信号 recon = imfs.sum(axis=0) + res print("重构最大绝对误差:", np.max(np.abs(recon - x))) # 5) 逐层画图,肉眼检查混叠 n = imfs.shape[0] fig, axes = plt.subplots(n + 2, 1, figsize=(9, 1.5 * (n + 2)), sharex=True) axes[0].plot(t, x); axes[0].set_ylabel("raw") for i in range(n): axes[i + 1].plot(t, imfs[i]); axes[i + 1].set_ylabel(f"IMF{i+1}") axes[-1].plot(t, res); axes[-1].set_ylabel("res") axes[-1].set_xlabel("time / s") plt.tight_layout(); plt.show()

代码里有三处值得说清。FIXE=10把停止条件从"满足 IMF 判据"改成"筛满 10 次就收",好处是同一份数据在任何机器上跑出来的 IMF 完全一致,坏处是筛过头会让 IMF 变成纯调幅波、丢掉部分幅值调制信息,10 次是个折中起点,信号高频成分重的时候可以降到 6~8 次。nbsym=2控制边界延拓取几个极值点,数值越大两端越平缓,但会引入人为的低频摆动。

emd(x)返回的二维数组最后一行是残差,直接imfs[-1]当 IMF 画图是新手最常犯的错,get_imfs_and_residue()就是为拆开这两者准备的。完备性校验那一行必须每次都跑,它是后面所有对比的前提——如果 ΣIMF 加残差都还原不了原信号,说明中途有异常,别急着解释结果。

跑完通常会看到 6 到 9 层 IMF,第一层是 120 Hz 冲击串和一部分 50 Hz 分量搅在一起,第二层是 50 Hz 调幅,后面几层是低频和趋势。第一层和第二层边界模糊,就是模态混叠的直接证据。

2.3 端点效应怎么压:nbsym、镜像延拓与截尾

端点效应是 EMD 落地时绕不开的第一个坑。表现很典型:IMF 两端出现远大于中段的摆动,层数越多越明显,而且随着筛分进行,这种摆动会从两端向中间扩散,污染整段信号。根因是样条插值在边界外没有任何极值点约束,只能按最后两个极值点的趋势外推,外推方向一错就是一条大尾巴。

工程上有四种处理,按成本从低到高排:

第一种,调nbsym。PyEMD 默认取两端各 2 个极值点做镜像延拓,把边界外的包络补出来。信号两端极值点密集时,加到 3 或 5 效果更稳;如果信号两端本身很平缓、极值点稀疏,加大 nbsym 反而会人为造出一个假的极值点,这时候别动它。

第二种,预处理去趋势。端点效应的严重程度和信号在边界处的斜率强相关,先减掉一条拟合的低阶多项式或者做一次高通,让两端尽量贴近零轴,飞边能小一半。

第三种,镜像拼接。在信号首尾各接一段自身镜像,分解完再把镜像段丢掉。这是 MATLAB 系工具箱里常见的做法,代价是代码要多写十几行,好处是对长信号几乎无副作用。

第四种,最省事也最粗暴:分解后直接截掉首尾各 5% 的样本,只看中段。做离线分析时我个人最常用这一招,因为端点效应本质上是边界信息不足,无论怎么延拓都只是缓解。但要记住,截尾意味着有效样本变短,后面做统计指标时长度要统一,否则对比出来的数字没有意义。

还有一个容易混淆的现象:某段信号里极值点少于两个,三次样条根本插不出来,PyEMD 会退化成直线连接并给 warning,分解出来的 IMF 那一段会变成一条斜线。这不是参数问题,是信号本身在该尺度上没有振荡信息,正确做法是先做带通滤波把该尺度提出来,或者接受这一层没有物理含义。

3. EEMD 的补救:白噪声集成与两个旋钮的联动

3.1 白噪声为什么能把混叠摊开

模态混叠的本质是:信号里某个尺度的极值点分布不均匀,筛分时样条插值被局部主导成分带偏,导致不同尺度的成分被塞进同一个 IMF。EEMD 的思路是往信号里加一组幅值相同、实现不同的高斯白噪声,然后对每次加噪后的信号做完整 EMD,最后把所有试验的对应阶 IMF 取平均。

为什么这样能拆开?白噪声的能量在整个频带上均匀分布,相当于给每一个尺度都提供了一个"探针"。加了噪声之后,原本落在同一 IMF 尺度上的两种成分,在不同试验里会被随机分配到相邻的不同 IMF 中;集成平均时,噪声项因为零均值被压下去,两种成分各自在自己出现概率最高的那一层沉淀下来。可以把它理解成一组覆盖不同尺度的二进滤波器组,噪声负责把每个尺度的能量"顶"出来。

代价也很明确。有限次试验下,噪声不会被完全平均掉,每个 IMF 里都残留一小部分随机成分,这叫残噪;同时,噪声的加入会改变 IMF 的层数——同一信号跑两次,如果随机种子不同,层数可能差一层。这就是为什么 EEMD 的结果必须配合固定随机种子使用,否则后面的特征提取、分类模型全都在追一个会飘的目标。

从落地角度看,EEMD 适合这样的场景:信号长度在几百到几千点、成分尺度差异明显、对分解结果的物理纯净度要求高于对速度的要求。如果信号是几十万点的连续采集数据,EEMD 的集成次数乘以样本长度会让耗时迅速失控,这时候要么降采样,要么分段处理,要么直接考虑 CEEMDAN 减少每阶的计算浪费。

3.2 noise_width 与 trials:怎么配才不白跑

EEMD 只有两个真正需要调的参数:noise_width(加入噪声的标准差相对信号标准差的倍数)和trials(集成次数)。这两个参数是联动的,单独调一个基本没有意义。

noise_width太小,噪声提供的探针强度不够,混叠改善有限,分解结果和普通 EMD 差别不大;太大,噪声本身会成为主导尺度,IMF 层数明显增多,残噪也跟着上升。Wu 和 Huang 给出的经验值是 0.2,这个值在多数振动、声学信号上确实是个稳妥起点。trials的经验规律是:噪声幅值每降低一个数量级,集成次数要提高两个数量级才能保持同样的平均精度,所以把noise_width从 0.2 降到 0.02,只把trials提到 200 是远远不够的,残噪会明显变大。

下面这张表是我在一批长度 1024 点、成分已知的仿真信号上反复试出来的起步配置,实际项目里建议按它先跑一轮,再根据 IMF 的纯净度和耗时微调:

noise_widthtrials适用场景观察到的现象
0.05~0.1100高频分量主导、信噪比本身较高混叠改善有限,残噪低,层数稳定
0.2100~200通用默认,机械振动、心电、语音分离度与残噪平衡最好,层数偶有 ±1 波动
0.3~0.4200 以上多尺度混合、低频弱分量容易被淹没分离更彻底,但 IMF 层数增加、末端飞边变重

有两点容易踩:一是trials不是越大越好,超过某个点之后残噪的下降已经小于层数波动带来的不确定性,纯属烧 CPU;二是噪声幅值应该相对信号标准差来定,如果先对信号做了归一化,noise_width的物理含义就变了,换数据集时必须重新确认。

3.3 EEMD 代码落地:随机种子、并行与残差校验

PyEMD 的 EEMD 接口和 EMD 保持了一致,切换成本很低:

from PyEMD import EEMD # 1) 配置:集成 100 次,噪声幅值 0.2 倍标准差 eemd = EEMD(trials=100, noise_width=0.2, parallel=False) # 2) 固定随机种子,保证同一份数据每次结果一致 eemd.noise_seed(42) # 3) 分解,t 可选传入,不传则按样本序号当时间轴 IMFs_e = eemd(x) imfs_e, res_e = eemd.get_imfs_and_residue() print("EEMD 层数:", imfs_e.shape[0]) # 4) 完备性校验:集成平均后仍然应该无损重构 recon_e = imfs_e.sum(axis=0) + res_e print("EEMD 重构最大绝对误差:", np.max(np.abs(recon_e - x)))

noise_seed(42)这一行是整段代码里最重要的。EEMD 内部的噪声是随机生成的,不固定种子的话,每次运行得到的 IMF 层数和波形都有细微差别,做论文复现或者产线对比时会非常难受。parallel=True能开多进程加速,但依赖pathos包,没装的话会在初始化时直接报错,装完再开;单机跑几百点的短信号,开并行的收益还不如它带来的进程开销。

关于重构误差我要多说一句,这是个反直觉的点:EEMD 的重构误差在数学上同样接近机器精度,因为每一次加噪试验的分解都是完备的,线性平均之后完备性依然成立。所以拿重构误差来判断"EEMD 比 EMD 好多少"是无效的,它只能用来验证代码有没有写错。真正体现差异的是每个 IMF 内部的噪声残留和层数稳定性,这两点得靠和已知成分做相关分析才能看出来,第 4 章会给具体指标。

4. CEEMDAN 的自适应噪声:逐阶取残差再补噪声

4.1 噪声注入位置换了,重构误差跟着降

EEMD 的问题可以归结为一句话:噪声是在最外层一次性加进去的,集成平均之后总有一部分没被抵消掉,残留在每一阶 IMF 里;而且每次试验都要做一次完整 EMD,计算量按 trials 倍数膨胀。

CEEMDAN(自适应噪声完备集成经验模态分解)把噪声的注入位置挪到了每一阶的残差上,流程是这样递推的:先对 $x(t)+\varepsilon_0 w^i(t)$ 做 EMD,只取第一阶 IMF,对 $i=1..I$ 求平均得到 $IMF_1$;算残差 $r_1=x-IMF_1$;再对 $r_1+\varepsilon_1 E_1(w^i)$ 做 EMD,取第一阶 IMF 平均得到 $IMF_2$;继续算 $r_2=r_1-IMF_2$,如此逐阶往下。其中 $E_1(\cdot)$ 表示取 EMD 分解的第一阶 IMF,$\varepsilon_k$ 是第 k 阶的噪声系数。

这个改动带来两个直接好处。第一,每一阶的噪声只影响当前这一阶的提取,不会像 EEMD 那样在平均后留下全局残噪,所以重构误差更小、IMF 更纯净。第二,每阶分解只需要提取第一阶 IMF 就停,不需要把整个信号分解到底,单次 EMD 的成本大幅下降;虽然后面每一阶都要重跑 trials 次,但总耗时和 EEMD 处于同一量级,长信号上反而更省。

代价是 CEEMDAN 多了一个参数 $\varepsilon_k$,而且这个系数在不同实现里的归一化方式不一样,这是最容易翻车的地方,第 5 章会专门讲。

4.2 三参数落代码:trials、epsilon 与 max_imf

PyEMD 的 CEEMDAN 接口如下,参数名和 EEMD 有区别,别照抄:

from PyEMD import CEEMDAN # 1) trials 与 EEMD 含义一致;epsilon 是噪声系数,注意默认值很小 ceemdan = CEEMDAN(trials=100, epsilon=0.2, parallel=False) ceemdan.noise_seed(42) # 2) max_imf 限制最大层数,-1 表示由算法自动判断 IMFs_c = ceemdan(x, max_imf=10) imfs_c, res_c = ceemdan.get_imfs_and_residue() print("CEEMDAN 层数:", imfs_c.shape[0]) # 3) 完备性校验 recon_c = imfs_c.sum(axis=0) + res_c print("CEEMDAN 重构最大绝对误差:", np.max(np.abs(recon_c - x)))

三个参数的作用分别是:trials控制每阶的集成次数,50~100 是常规区间,信号短、成分简单时 50 就够;epsilon控制噪声相对残差标准差的倍数,PyEMD 默认值偏小,如果你发现分解出来的层数比 EEMD 少好几层、后面几阶 IMF 幅度趋近于零,第一反应应该是把它调到 0.1~0.2 再试;max_imf是安全阀,仿真信号里我一般设成 8~10,防止残差里剩下的数值噪声被继续拆成无意义的 IMF。

需要提醒的是,不同来源的 CEEMDAN 实现里epsilon的物理含义并不统一:有的实现把它定义成相对原始信号标准差的倍数,有的定义成相对当前残差标准差的倍数,还有的在加噪前先对噪声模态做了单位方差归一化。换库的时候不要直接搬参数,先用一段成分已知的仿真信号验证一下分解层数和 IMF 顺序,再决定用哪个值。

4.3 三个验收指标:重构误差、正交性 IOI、相邻 IMF 相关

分解完必须验收,否则后面所有的特征工程都建在沙子上。我一般算三个指标,各有分工。

import numpy as np def completeness(x, imfs, res): """完备性:重构信号与原信号的最大绝对误差,应该接近机器精度""" recon = imfs.sum(axis=0) + res return float(np.max(np.abs(recon - x))) def orthogonality(imfs): """正交性指标 IOI:任意两阶 IMF 的内积和 / 原信号能量,越小越正交""" e = np.sum(imfs.sum(axis=0) ** 2) num = 0.0 for i in range(imfs.shape[0]): for j in range(imfs.shape[0]): if i != j: num += np.sum(imfs[i] * imfs[j]) return float(num / e) def match_score(imfs, comp): """分离度:每个已知成分与最相关 IMF 的相关系数绝对值""" scores = [abs(np.corrcoef(imf, comp)[0, 1]) for imf in imfs] return float(np.max(scores)), int(np.argmax(scores)) + 1

completeness是门槛项,低于 1e-10 才算代码没问题,它不参与方法优劣比较——三种方法在这一项上都会很好看。orthogonality才是区分项,混叠越严重,不同 IMF 之间共享的成分越多,IOI 越大;EMD 通常在 0.05 上下,EEMD 和 CEEMDAN 能压到 0.01 量级,具体数值随信号而变,只看相对大小。

match_score是仿真信号才有的特权:拿每一阶 IMF 和已知的 50 Hz 调幅、120 Hz 冲击、4 Hz 低频分别算相关系数,

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