简介:CEEMDAN算法资源包,面向信号处理、机械故障诊断、生物医学与金融时序分析等研究人员,解决EMD/EEMD在非线性非平稳信号分解中的模态混叠问题。作为EMD和EEMD的进一步改进,CEEMDAN通过引入与信号相位相关的自适应噪声,在多次迭代中不断优化IMF提取过程,并借助平均策略提升分解稳定性,从而更精确地分离不同频率成分,对心电、地震波等复杂信号的振荡频率与振幅变化分析尤为适用。压缩包共5个文件,以MATLAB脚本为主,辅以一个.mat测试数据文件,整体仅32KB,包含CEEMDAN、EEMD、EMD三套实现代码、国际会议ICASSP示例演示以及心电信号测试数据,代码结构清晰,直接运行即可快速掌握调用流程与参数设置,便于对照改进效果。已有5930人学习下载,适合具备一定MATLAB基础、希望将CEEMDAN应用于实际复杂信号分析的研究生与工程师。
1. CEEMDAN:EEMD 和 EMD 的改进终点,信号分解选它最省心
做振动信号分析、轴承故障诊断的人,大概率经历过被 EMD 模态混叠支配的阶段:一段实测信号拆出来的 IMF 里粘着两三个频率成分,怎么筛都分不干净;换成 EEMD 加白噪声,模态分开了,但每次跑出来的 IMF 数量对不上,残差还带着噪声尾巴。CEEMDAN(完全自适应噪声集合经验模态分解)就是冲这两个痛点来的——它在每一级残差上加入由 EMD 分解得到的自适应噪声,逐层逼近真实模态,既保留集合平均的抗混叠能力,又保证分解完备、结果稳定。这篇笔记从 EMD 讲起,把 EEMD、CEEMDAN 的差异说透,再落到 Python 代码、参数调优和避坑记录,适合拿实测信号做分解的从业者。
2. 从 EMD 到 CEEMDAN:三次迭代分别解决了哪三个问题
EMD 是 Huang 在 1998 年提出的经验模态分解,CEEMDAN 是沿着“EMD → EEMD → CEEMDAN”这条线改出来的。要理解 CEEMDAN 为什么这么设计,得先把前两代算法的“病”看清楚。这一章不堆公式,只讲清楚每个版本做了什么、留下了什么问题。
2.1 EMD 的筛分过程与模态混叠:Hilbert-Huang 的第一道坎
EMD 的核心假设是:任何复杂信号都能拆成若干个本征模态函数(IMF)加一个残差。IMF 要满足两个条件:一是过零点数和极值点数相等或最多差 1;二是上下包络的均值处处为 0。
筛分过程是这样的:先找出信号的所有局部极大值和极小值,用三次样条分别拟合出上包络和下包络,取两者平均得到 m1,用 x(t) - m1 得到 h1;如果 h1 不满足 IMF 条件,就重复这套操作,直到满足为止,得到第一个 IMF,再对残差继续筛。这套迭代叫 sifting,是整个 EMD 家族的地基。
问题出在哪儿?当信号里两个频率成分靠得比较近时,包络拟合出来的均值会把它们“捏”在一起,筛出来的一个 IMF 里同时带着两个模态,这就是模态混叠。混叠的直接后果是,后续做 Hilbert 变换算瞬时频率的时候,会出现负频率或频率跳变。
还有一个衍生问题:端点附近没有极值点,包络只能靠样条外推,外推结果容易过冲,导致 IMF 两端大幅摆动,这叫端点效应。我第一次拿 EMD 跑齿轮箱振动数据,第一个 IMF 的包络谱里既有啮合频率又有转频边带,怎么解释都牵强,后来才明白这是模态混叠,不是信号本身的问题。后来做多了才发现,EMD 这套东西边界情况极多,参数稍微不合适,出来的模态就没法从物理上解释,这是它最大的使用门槛。
2.2 EEMD 的白噪声平均法:为什么加了噪声反而能分离模态
Wu 和 Huang 在 2009 年提出 EEMD,思路相当反直觉:往信号里加白噪声,做 EMD,重复 N 次,再把结果平均。白噪声的频谱均匀铺满整个频带,它的加入会把不同尺度的信号成分“顶”到各自的参考尺度上,让原本粘在一起的模态被强制分开;而白噪声本身是随机成分,多次平均后相互抵消。
但 EEMD 有两个先天毛病。第一,平均后残余的白噪声无法完全消除,高频段尤其明显,每个 IMF 里都带着一层噪声底。第二,每次加噪声后都做完整的 EMD,N 次分解得到的 IMF 数量可能不一样,平均时只能按序号对齐,一旦某次分解多出一个 IMF,后面的全错位,这就是所谓的“分解不完备”。
计算量也不乐观,每次试验都是一次完整筛分,100 次试验就是 100 遍 EMD。我跑一组 10 万点的数据,EEMD 一次要一分多钟,来回调参数的时间比算数据的时间还长。还有一个容易被忽略的问题:EEMD 的集合平均只保证统计意义上的稳定,单次试验之间的差异非常大,如果你只跑几十次试验,结果基本不可复现。
用一个不严谨但好记的说法:EEMD 是“先把噪声撒进整个信号,再靠平均把噪声捞出来”,捞不干净是常态。
2.3 CEEMDAN 的自适应噪声:把“先加后减”改成“逐级逼近”
CEEMDAN 是 Torres 等人在 2011 年提出的,全称 Complete Ensemble Empirical Mode Decomposition with Adaptive Noise。它和 EEMD 最核心的差别有两点。
第一,噪声不是一次性加在原始信号上,而是加在每一级残差上,幅值随残差能量自适应变化。第二,加进去的不是纯白噪声,而是先对白噪声做一次 EMD,取出它的第一个 IMF(记作 E1(w^i)),再按系数加到残差上。这样做的好处是,噪声分量的尺度与信号当前残差处于同一水平,不会出现噪声尺度与信号尺度不匹配导致的虚假模态。
完整流程可以概括成四步:对 x + ε0·w^i 做 EMD,得到 N 组第一个 IMF,平均后得到 IMF1;算残差 r1 = x - IMF1;对 r1 + ε1·E1(w^i) 做 EMD,平均得到 IMF2;如此逐级推进,直到残差不再满足分解条件。
“逐级”这个设计决定了 CEEMDAN 的分解是完备的——所有 IMF 加回来就是原始信号,不存在 EEMD 里“尾部对不齐”的问题。这也是工程上选它最多的原因:分解结果可以直接拿去做重构验证,不用怀疑中间丢没丢信息。实测下来,同一段信号 CEEMDAN 多跑几次,IMF 数量稳定,首层模态也不会像 EEMD 那样频繁混叠,这才是它能替代前两代的底气。
3. 用 Python 落地 CEEMDAN:PyEMD 安装、调用与参数对照
Python 生态里做 CEEMDAN 最常用的是 PyEMD 库,底层依赖 numpy 和 scipy,一个库同时实现了 EMD、EEMD、CEEMDAN 三个类。这一章直接讲怎么装、怎么调、参数怎么设。
3.1 环境准备与 PyEMD 安装
安装命令很简单,包名注意别搞混:PyPI 上叫 EMD-signal,导入名是 PyEMD。
pip install EMD-signal装完验证一下能不能正常导入三个核心类:
python -c "from PyEMD import EMD, EEMD, CEEMDAN; print('ok')"逻辑说明:PyEMD 的 CEEMDAN 类内部会持有自己的 EMD 实例来做噪声分解和残差筛分,所以导入时 EMD 一定要在。如果是在 conda 环境里装,建议先把 numpy、scipy 用 conda 装好,再 pip 装 EMD-signal,能少踩不少编译依赖的坑。
参数说明:PyEMD 版本迭代比较快,不同小版本之间的 API 有差异,老版本里 CEEMDAN 要调用 ceemdan.ceemdan(sig) 方法,新版本直接用 ceemdan(sig) 就行。代码里建议用可调用对象的形式,兼容性更好,也能少写一层方法名。
3.2 三段代码对照:同一个信号,三种算法各解一次
用一个合成的混合信号做对照:5 Hz 正弦 + 20 Hz 正弦,再加少量随机噪声,模拟振动信号里“低频转频 + 高频冲击分量”的简化形态。
import numpy as np from PyEMD import EMD, EEMD, CEEMDAN # 构造测试信号:1 秒采样 1000 点,含 5Hz 和 20Hz 两个分量 t = np.linspace(0, 1, 1000) sig = np.sin(2 * np.pi * 5 * t) + 0.5 * np.sin(2 * np.pi * 20 * t) sig = sig + 0.05 * np.random.randn(t.size) # 加一点采样底噪 # EMD:直接筛分,不设任何集合参数 emd = EMD() imfs_emd = emd(sig) # EEMD:200 次试验,噪声幅值为信号标准差的 0.1 倍 eemd = EEMD(trials=200, noise_width=0.1) imfs_eemd = eemd(sig) # CEEMDAN:200 次试验,自适应噪声系数 0.075 ceemdan = CEEMDAN(trials=200, epsilon=0.075) imfs_ceemdan = ceemdan(sig) print("EMD IMF 数量:", imfs_emd.shape[0]) print("EEMD IMF 数量:", imfs_eemd.shape[0]) print("CEEMDAN IMF 数量:", imfs_ceemdan.shape[0])逻辑说明:三段调用的结构完全一样——实例化、把信号传进去、拿回一个二维数组,每行是一个 IMF,按频率从高到低排列。差异全部体现在构造参数上。EEMD 的 noise_width 是白噪声幅值相对信号标准差的倍数;CEEMDAN 的 epsilon 是每级自适应噪声的幅值系数。跑完打印 IMF 数量,通常结果是 EMD 最少,EEMD 和 CEEMDAN 差不多,但 CEEMDAN 多跑几次数量稳定,EEMD 偶发多一个或少一个。
参数说明:trials 是集合试验次数,我一般先用 100 快速看趋势,定稿实验提到 300 到 500。噪声幅值的经验区间是信号标准差的 0.01 到 0.2 倍,信号信噪比越低,噪声系数要越小,否则真实模态会被噪声淹没,这属于分解里的玄学区间,后面避坑章节会细说。
3.3 参数怎么选:trials、epsilon 与 max_imfs 的实用取值
把三个算法最常用的参数收敛到一张表里,方便对照:
| 参数 | 适用算法 | 作用 | 经验取值 |
|---|---|---|---|
| trials | EEMD / CEEMDAN | 集合平均次数 | 100~500 |
| noise_width | EEMD | 白噪声幅值 / 信号标准差 | 0.05~0.2 |
| epsilon | CEEMDAN | 每级自适应噪声系数 | 0.01~0.15 |
| max_imfs | 三者通用 | 最大 IMF 数量 | 6~12 |
提示:epsilon 和 noise_width 语义不同,不要把一个算法的取值直接套到另一个上。EEMD 的 0.2 放到 CEEMDAN 里通常会明显过噪。
选参逻辑我一般这么走:先用 EMD 快速拆一遍,看信号大概能出几个有效模态,确定 max_imfs 的下限;然后用 CEEMDAN(trials=100, epsilon=0.05) 跑一遍,看 IMF 数量稳不稳定;再逐步加大 epsilon,观察新增的 IMF 是真实成分还是噪声碎片——如果出现大量贴近零点的小幅振荡,说明噪声系数过头了,往回退一档。max_imfs 建议一开始就设好,比等它自动拆到底再截断要省事得多,也能避免把残余噪声硬拆成伪模态。
熟手可能已经注意到,PyEMD 的 CEEMDAN 还暴露了 ext_EMD 参数,允许你把一个自定义的 EMD 实例传进去,精细控制内部的筛分迭代次数和样条参数。这个在避坑章节会展开讲。
4. 实战:把轴承振动信号拆成 IMF 并锁定故障频率
上一章讲的是跑通,这一章讲的是跑对。实测数据和合成信号差别很大,从预处理到分解再到包络谱分析,每一步都有固定的动作要领。
4.1 数据准备:去均值、去趋势、降采样三步预处理
实测振动信号里有传感器偏置导致的直流分量,有转频带来的低频趋势,还有采集系统的高频底噪。直接喂给 CEEMDAN 不是不行,但会多拆出几个没有物理意义的低频 IMF,浪费计算量还干扰判断。
from scipy.io import loadmat from scipy.signal import detrend, resample import numpy as np # 假设你手里的数据是 .mat 格式,键名为 'vibration' data = loadmat('bearing_data.mat')['vibration'].flatten() fs = 25600 # 原始采样率,单位 Hz # 1. 去均值:消除直流偏置 data = data - np.mean(data) # 2. 去趋势:用线性拟合去掉缓慢变化的趋势项 data = detrend(data, type='linear') # 3. 降采样:从 25.6kHz 降到 2560Hz,保留故障特征频带的裕量 step = 10 data = resample(data, int(len(data) / step)) fs_new = fs // step print("预处理后数据长度:", len(data), "新采样率:", fs_new)逻辑说明:去均值是必须的,CEEMDAN 的包络拟合基于极值,直流分量会让包络整体平移,第一个 IMF 里多出近似直流的成分。detrend 去掉线性趋势,防止把趋势当成低频频段拆出来。resample 在包络谱分析前做,降采样前要估算故障特征频率的最高可能值,留出 5~10 倍裕量。
参数说明:fs 和降采样倍数要根据自己的设备改。一般原则是降采样后的采样率不低于目标频率带宽的 2.56 倍,这是振动分析里常用的频率分辨率经验值。比如轴承内圈故障特征频率算出来大约 200 Hz,降到 2560 Hz 就足够看清它的边带结构。
4.2 CEEMDAN 分解与质量判断:三条判据看着选
预处理完直接分解,然后立刻做质量检查,不要等画完图才发现分解是废的。
from PyEMD import CEEMDAN ceemdan = CEEMDAN(trials=200, epsilon=0.08) imfs = ceemdan(data, max_imfs=10) # 重构相对误差:CEEMDAN 完备性最直接的检验 residual = data - np.sum(imfs, axis=0) rel_err = np.sqrt(np.mean(residual**2)) / np.std(data) print("重构相对误差:", round(rel_err, 6))逻辑说明:CEEMDAN 完备性意味着残差应当接近零。rel_err 如果超过 1%,先检查 trials 是否太小,或者 max_imfs 被截断导致信号没拆完。我一般把相对误差小于 0.5% 作为分解可信的门槛,超过这个值就回去调参,不带着侥幸往下走。
判断分解质量我主要看三件事:一是上面这个重构误差;二是每个 IMF 的频谱是否落在互不重叠的频带;三是 IMF 数量是否稳定——同一段数据多跑几次,数量变化超过 1 个就要警惕。第一次跑出来的结果只是“能看”,三次重跑都稳定才叫“能用”。
4.3 从 IMF 到瞬时频率:Hilbert 变换与包络谱定位故障频率
拆出 IMF 之后,最常用的下一步是包络谱分析:对每个 IMF 做 Hilbert 变换得到解析信号,取模得到包络,再对包络做 FFT,得到包络谱。轴承故障的特征频率(外圈 BPFO、内圈 BPFI)在包络谱里会以尖峰形式出现。
from scipy.signal import hilbert import numpy as np # 对第一个 IMF 做包络谱分析 imf = imfs[0] analytic = hilbert(imf) envelope = np.abs(analytic) env_fft = np.abs(np.fft.rfft(envelope)) freqs = np.fft.rfftfreq(len(envelope), d=1/fs_new) # 跳过前 5 个频率点,找到最大峰值 peak_idx = np.argmax(env_fft[5:]) + 5 print("包络谱峰值频率:", round(freqs[peak_idx], 1), "Hz")逻辑说明:hilbert 函数直接返回解析信号,np.abs 取模就是包络。包络谱的峰值频率对应的是调制频率,也就是故障冲击的重复频率。注意 freq 轴用的是降采样后的采样率 fs_new,别用回原采样率,否则频率全错位,这个坑我踩过一次之后每次都会检查一遍。
参数说明:峰值搜索前先跳过前 5 个频率点,是为了滤掉包络谱里的直流和极低频成分。实际项目中,把 BPFO/BPFI 计算值和峰值做对比,误差在 ±2% 以内就可以认为锁定故障。如果峰值落在转频附近而不是特征频率附近,多半是分解不彻底,转频成分混到了这个 IMF 里。
5. 避坑手记:CEEMDAN 的五个高频翻车现场
CEEMDAN 不是开箱即用的工具,参数、边界、噪声每一项都能让结果翻车。下面这五条是从实际项目里攒出来的记录,每一条都按现象、原因、解决的顺序写清楚。
5.1 分解结果不稳定:同一段信号两次跑完全不一样
现象:同一段数据,连续跑两次 CEEMDAN,IMF 数量和波形都有差异,第一个 IMF 尤其明显,重跑一次对不上上一次的模态。
原因:CEEMDAN 本质是随机过程,每轮加的白噪声不同。trials 不够时,平均结果没有收敛到稳定值;另一个隐藏原因是 epsilon 设置过大,噪声分量在平均后没有完全抵消。
解决:先把 trials 提到 300 以上,确认稳定性;再逐步降低 epsilon。规律是噪声幅值越大,需要的 trials 越多,两者近似线性关系。我常用的组合是 trials=300、epsilon=0.05 到 0.08,跑 10 万点数据大约 30 秒,可以接受。
5.2 端点飞翼:首尾两端飘得不像话
现象:分解出的 IMF 在数据开头和结尾出现大幅度摆动,幅度甚至超过信号本身,中间段看起来正常。
原因:包络拟合用的是三次样条,端点附近没有极值点做约束,样条外推会产生过冲,这就是端点效应。CEEMDAN 虽然改进了噪声加入方式,但底层的 EMD 筛分同样继承了这个毛病。
解决:常见做法是端点延拓——在分解前给信号两端各延伸一段镜像数据,分解完再裁掉。PyEMD 的 EMD 类支持通过传 ext_EMD 实例进去控制内部行为,你可以构造一个做端点延拓的 EMD 传给 CEEMDAN,这样噪声分解和残差筛分都走同一套延拓逻辑。更省事的替代方案是分段处理,把长信号切成有重叠的段分别分解,只取中间部分。从那以后我每次跑长信号前都先看一眼端点,再决定要不要延拓。
5.3 过分解:把噪声也拆成了 IMF
现象:分解出的 IMF 数量特别多,最后几个 IMF 波形杂乱,频谱铺满整个频带,看不出任何集中峰。
原因:max_imfs 没设上限,CEEMDAN 会把残差一路拆到底,直到残差没有两个极值为止。这个过程中噪声不具备完整模态结构,但筛分过程仍会把它“硬拆”成若干伪 IMF。
解决:先跑一次快速分解,看真实模态集中在前面几个 IMF,然后设置 max_imfs 截断。判断截断位置的方法是:计算每个 IMF 与原始信号的互相关系数,从某个位置开始系数都低于 0.1,后面基本就是噪声碎片。我一般把 max_imfs 设为 8 到 10,既能覆盖常见故障信号,又不会拆出太多垃圾模态。
5.4 直接把 EEMD 的噪声参数套给 CEEMDAN
现象:把 EEMD 的 noise_width=0.2 直接换成 CEEMDAN(epsilon=0.2),分解结果多出大量异常尖峰,重构误差反而变大。
原因:两个参数的语义不一样。EEMD 的 noise_width 是加在原始信号上的白噪声幅值相对标准差的比例;CEEMDAN 的 epsilon 是加在残差上的自适应噪声系数,它还要经过 EMD 分解出 E1(w^i),实际作用到信号的噪声能量远小于 epsilon 表面值。直接套用同一个值,相当于放大了噪声能量。
解决:CEEMDAN 的 epsilon 通常取 EEMD noise_width 的 1/2 到 1/3,再结合重构误差和包络谱质量回调。我一般从 0.05 起步,每次加 0.025,看结果变化再定。这个参数直接影响分解质量,值得多花两轮试验去定,比后面返工划算。
5.5 分解完只盯 IMF 不看残差
现象:IMF 看起来漂亮,但拿 IMF 重构信号,误差大得离谱,或者残差里还藏着明显的周期性成分。
原因:CEEMDAN 的完备性是有前提的——trials 足够、噪声参数合适、max_imfs 没有在模态没拆完时截断。任何一环没满足,残差里都会有结构信息。
解决:每次分解后强制检查 residual = data - np.sum(imfs, axis=0),算它的能量占比和频谱。如果残差里还有明显单频峰,说明欠分解,需要降低 epsilon 或增大 max_imfs。这个检查我固定写进每个项目的处理函数里,跑完自动打印,眼睛看不到的问题让数字说话。
6. 残差检查与模态重构:验证 CEEMDAN 结果是否可信的固定动作
CEEMDAN 跟 EMD、EEMD 最大的不同是完备性,这是它的卖点,也是验证它结果最方便的入口。我把它拆成三个固定检查动作:重构误差、残差频谱、模态间相关系数。
import numpy as np def check_ceemdan(signal, imfs, fs_new): # 1. 重构误差 residual = signal - np.sum(imfs, axis=0) rel_err = np.sqrt(np.mean(residual**2)) / np.std(signal) # 2. 残差频谱:找残余单频峰 spec = np.abs(np.fft.rfft(residual)) freqs = np.fft.rfftfreq(len(residual), d=1/fs_new) peak_idx = np.argmax(spec[5:]) + 5 peak_freq = freqs[peak_idx] peak_energy = spec[peak_idx] / np.sum(spec) # 3. 模态间相关系数矩阵 n = imfs.shape[0] corr = np.corrcoef(imfs) max_off_diag = np.abs(corr - np.eye(n)).max() print(f"重构误差: {rel_err:.4%}") print(f"残差主峰频率: {peak_freq:.1f} Hz, 能量占比: {peak_energy:.4%}") print(f"模态间最大相关系数: {max_off_diag:.3f}") return rel_err, peak_freq, max_off_diag逻辑说明:重构误差衡量完备性,超过 0.5% 说明分解没做干净;残差主峰频率如果落在信号的特征频率附近,说明有真实模态被漏拆,这时候要回退参数;模态间最大相关系数超过 0.3,说明两个 IMF 之间严重重叠,分解并不独立。三个指标分别卡住完备性、完整性和正交性,比单看一张分解图靠谱得多。
这个固定动作省掉过我一次大麻烦:之前跑一组齿轮箱数据,CEEMDAN 拆出来的前三个 IMF 看着很像样,结果残差里还杵着一个 23 Hz 的峰——那是齿面磨损故障的真实特征频率,被参数没调好的分解漏掉了,如果直接拿前几个 IMF 去做包络谱,这一处故障就彻底丢了。从那以后,我每次跑完 CEEMDAN 都强制走一遍这三连检查,确认残差干净了才敢拿 IMF 去做后续分析。希望帮到你。
本文还有配套的精品资源,点击获取