CEEMDAN算法实战:从EMD到CEEMDAN的信号分解与故障诊断

发布时间:2026/10/25 15:17:49
CEEMDAN算法实战:从EMD到CEEMDAN的信号分解与故障诊断

简介:这份资源聚焦CEEMDAN算法,即完全自适应噪声集合经验模态分解,是EMD与EEMD的进一步改进方案,面向从事非线性、非平稳信号处理的研究生、工程师与科研人员,可用于地震波解析、心电与脑电分析、金融时序波动识别及机械故障诊断等场景。压缩包共5个文件,以4个m脚本和1个mat数据文件为主,脚本分别对应EMD、EEMD与CEEMDAN的分解实现及ICASSP2011示例,mat文件提供心电信号样本,整体约32KB,轻量便于快速上手复现。目前已有5933人学习下载,说明该实现具备一定参考价值。读者可借助脚本理解自适应噪声的添加方式、迭代分解与平均流程,对比三种算法在模态混合与收敛速度上的差异,并直接替换数据验证分解效果,为后续特征提取与信号分析提供可运行的基础代码。

1. 从模态混叠到 CEEMDAN:这份算法资源到底解决了什么

如果你处理过轴承振动、心电、地震波或者风速这类非平稳信号,大概率被 EMD 的模态混叠折磨过。原始 EMD 在信号里混入一点噪声或间歇成分,本征模态函数就会断裂、重叠,分解出来的 IMF 物理意义直接崩掉。EEMD 靠加白噪声集合平均来压制这个问题,但代价是计算量大、重构误差里残留噪声,而且不同次分解的 IMF 数量对不齐。CEEMDAN 就是在 EEMD 基础上继续改进的版本,通过自适应加入特定噪声、逐阶计算残差,把模态混叠和重构误差同时压下去,还让 IMF 的阶数更稳定。这份资源围绕 EMD、EEMD、CEEMDAN 三代算法的递进关系展开,适合做信号分解、故障诊断、时序特征提取的工程师,也适合正在选分解算法做论文复现的人。它不只是一个函数调用,而是把三代算法的差异、参数影响和落地细节讲清楚,让你知道什么时候该用哪一代、参数怎么设、结果怎么验证。

2. EMD、EEMD、CEEMDAN 的递进逻辑:为什么不是越新越好

2.1 三代算法的核心差异与选型理由

EMD 的出发点是任何复杂信号都能拆成有限个本征模态函数,每个 IMF 满足两个条件:极值点数量和过零点数量相差不超过一个,且上下包络均值为零。它的实现靠筛分过程,反复找极值、拟合上下包络、减去均值,直到满足 IMF 条件。问题在于,当信号里存在间歇性高频成分或噪声时,极值点分布会跳变,同一个时间尺度上的成分被拆到不同 IMF 里,这就是模态混叠。

EEMD 的思路很直接:既然混叠来自极值点不稳定,那就往原始信号里加不同 realization 的白噪声,噪声把极值点分布“抹匀”,每个尺度上的成分在多次分解中自动对齐到同一阶 IMF,最后对集合做平均。白噪声在平均过程中互相抵消,理论上残留量随集合数增大而趋近于零。但 EEMD 有两个硬伤:一是加噪声后每次分解得到的 IMF 数量可能不同,需要对齐处理;二是重构信号时残留噪声虽然小,但在高精度场景下仍不可忽略。

CEEMDAN 的改进点在于不再对原始信号加噪声后直接分解,而是每一阶都加入自适应噪声,并且用上一阶的残差来驱动下一阶的分解。具体说,第一阶 IMF 通过对加噪信号集合平均得到,然后从原始信号里减去这个 IMF 得到第一阶残差;第二阶 IMF 通过对残差继续加噪分解得到,依此类推。这样每一阶的噪声都是独立控制的,重构误差可以精确到接近零,而且 IMF 阶数在不同集合间更一致。代价是计算量比 EEMD 略高,因为每阶都要做集合分解。

选型上,如果信号干净、长度短、只做粗略趋势提取,EMD 够用;如果模态混叠明显但能接受轻微残留噪声,EEMD 是性价比选择;如果要做精确重构、多分量分离或者后续做希尔伯特谱分析,CEEMDAN 更稳。常见做法是先用 CEEMDAN 跑一遍看 IMF 数量和频谱分布,再决定是否降级到 EEMD 省算力。

2.2 用 Python 复现三代算法:从 EMD 到 CEEMDAN 的代码骨架

下面这段代码用 PyEMD 库演示三代算法的调用差异。PyEMD 是常见的 EMD 系列实现,CEEMDAN 在PyEMD.CEEMDAN里。先装依赖:

pip install EMD-signal numpy matplotlib

然后跑一个合成信号,包含两个不同频率分量加一点噪声:

import numpy as np from PyEMD import EMD, EEMD, CEEMDAN import matplotlib.pyplot as plt # 构造合成信号:低频趋势 + 中频振荡 + 高频噪声 t = np.linspace(0, 1, 1000) signal = (np.sin(2 * np.pi * 5 * t) # 5 Hz 分量 + 0.5 * np.sin(2 * np.pi * 20 * t) # 20 Hz 分量 + 0.1 * np.random.randn(len(t))) # 噪声 # EMD 分解 emd = EMD() imfs_emd = emd.emd(signal) # EEMD 分解: trials 控制集合数,noise_width 控制噪声幅度 eemd = EEMD() eemd.trials = 100 # 集合次数,越大越稳但越慢 eemd.noise_width = 0.2 # 噪声标准差相对信号幅度的比例 imfs_eemd = eemd.eemd(signal) # CEEMDAN 分解: epsilon 控制每阶噪声幅度,trials 控制集合数 ceemdan = CEEMDAN() ceemdan.trials = 100 ceemdan.epsilon = 0.2 imfs_ceemdan = ceemdan.ceemdan(signal) # 打印各算法得到的 IMF 数量 print("EMD IMFs:", imfs_emd.shape[0]) print("EEMD IMFs:", imfs_eemd.shape[0]) print("CEEMDAN IMFs:", imfs_ceemdan.shape[0])

这段代码里,trials是集合次数,EEMD 和 CEEMDAN 都靠它做平均,设太小残留噪声大,设太大跑得慢,一般 100 到 200 之间够用。noise_width是 EEMD 的噪声幅度,通常取信号标准差的 0.1 到 0.3 倍,太大引入伪分量,太小压不住混叠。CEEMDAN 的epsilon类似,但它是逐阶控制的,第一阶可以稍大,后面阶可以小一点。跑完看 IMF 数量,CEEMDAN 一般比 EEMD 少一阶,因为重构更干净。

2.3 参数怎么设:集合数、噪声幅度与 IMF 阶数的取舍

参数设置是这类算法最容易翻车的地方。集合数trials直接决定噪声抵消程度,理论上残留噪声标准差按1/sqrt(trials)衰减。设 50 时残留还比较明显,设 200 基本干净,设 500 以上收益递减但耗时线性增长。我一般先跑 100 看结果,如果 IMF 波形毛刺多再往上加。

噪声幅度noise_width或epsilon更敏感。设太小,极值点分布没被抹匀,模态混叠还在;设太大,噪声本身会分解出伪 IMF,污染低频分量。经验值是信号标准差的 0.2 倍左右,但具体要看信号信噪比。如果原始信号本身噪声就大,可以降到 0.1;如果信号很干净但混叠严重,可以升到 0.3。

IMF 阶数不用手动设,算法自适应决定,但可以通过max_imf限制最大阶数,防止分解出太多无意义的高频分量。常见做法是分解后看每个 IMF 的频谱,如果某一阶能量极低且频谱平坦,基本是噪声,可以丢弃。重构时只取前几阶能量占比高的 IMF,能进一步降噪。

提示:EEMD 和 CEEMDAN 的结果有随机性,每次跑 IMF 可能略有差异。做对比实验时固定随机种子,或者多跑几次取平均。

3. 把 CEEMDAN 用到实际信号上:从分解到重构的完整链路

3.1 读取真实信号并做预处理

实际场景里信号往往带趋势项和异常点,直接丢给 CEEMDAN 会分解出奇怪的 IMF。常见做法是先做去趋势和归一化。下面以一段模拟的轴承振动信号为例,实际用时替换成你的数据加载逻辑:

import numpy as np from scipy.signal import detrend from PyEMD import CEEMDAN # 假设 signal 是从 CSV 读入的一维振动信号 # signal = np.loadtxt("vibration.csv", delimiter=",") # 去线性趋势 signal_detrended = detrend(signal) # 归一化到零均值单位方差,避免噪声幅度参数依赖原始量纲 signal_norm = (signal_detrended - np.mean(signal_detrended)) / np.std(signal_detrended) # CEEMDAN 分解 ceemdan = CEEMDAN() ceemdan.trials = 150 ceemdan.epsilon = 0.2 imfs = ceemdan.ceemdan(signal_norm) print("IMF 矩阵形状:", imfs.shape) # (阶数, 样本数)

去趋势用detrend去掉线性项,归一化让epsilon的参数意义稳定。如果不归一化,信号幅度是几千,epsilon=0.2就完全不起作用。这一步很多人忽略,结果分解出来全是噪声。

3.2 用频谱和能量占比筛选有效 IMF

分解完不是所有 IMF 都有用。高频阶往往是噪声,低频阶可能是趋势。筛选方法有两种:看频谱峰值,或者算能量占比。下面代码计算每个 IMF 的主频和能量占比:

from scipy.fft import rfft, rfftfreq n = len(signal_norm) fs = 1000 # 采样率,按实际数据改 freqs = rfftfreq(n, d=1/fs) for i, imf in enumerate(imfs): spectrum = np.abs(rfft(imf)) dominant_freq = freqs[np.argmax(spectrum)] energy_ratio = np.sum(imf**2) / np.sum(signal_norm**2) print(f"IMF {i+1}: 主频 {dominant_freq:.2f} Hz, 能量占比 {energy_ratio:.4f}")

主频接近工频或故障特征频率的 IMF 保留,能量占比低于 0.01 的基本是噪声。常见做法是保留前 3 到 5 阶能量占比高的 IMF 做重构,剩下的丢弃。重构就是把保留的 IMF 直接相加:

# 假设保留前 4 阶 imfs_kept = imfs[:4] signal_reconstructed = np.sum(imfs_kept, axis=0) # 计算重构误差 reconstruction_error = np.mean((signal_norm - signal_reconstructed)**2) print("重构均方误差:", reconstruction_error)

CEEMDAN 的重构误差通常能到 1e-6 量级,EEMD 在 1e-3 左右,EMD 如果模态混叠严重可能到 1e-2。这个指标可以直接用来判断分解质量。

3.3 希尔伯特谱与边际谱的快速画法

分解完常要做时频分析,希尔伯特谱是标配。下面代码对每个 IMF 做希尔伯特变换,得到瞬时频率和瞬时幅度,再画时频图:

from scipy.signal import hilbert import matplotlib.pyplot as plt plt.figure(figsize=(10, 6)) for i, imf in enumerate(imfs[:5]): # 只画前 5 阶 analytic = hilbert(imf) amplitude = np.abs(analytic) phase = np.unwrap(np.angle(analytic)) inst_freq = np.diff(phase) / (2 * np.pi) * fs inst_freq = np.append(inst_freq, inst_freq[-1]) plt.scatter(t[1:], inst_freq[1:], c=amplitude[1:], s=1, cmap='jet', alpha=0.5) plt.colorbar(label='幅度') plt.xlabel('时间 (s)') plt.ylabel('瞬时频率 (Hz)') plt.title('CEEMDAN 希尔伯特谱') plt.show()

瞬时频率出现负值说明该 IMF 不满足窄带条件,可能是分解不充分或噪声残留。常见处理是丢弃负频率点,或者回到参数调整那一步加大trials。边际谱就是把希尔伯特谱对时间积分,看频率能量分布,适合做故障特征频率提取。

注意:希尔伯特变换要求 IMF 是窄带信号,如果 IMF 里混了多个尺度,瞬时频率会剧烈跳变。CEEMDAN 的 IMF 通常比 EEMD 更窄带,但也不能保证每阶都合格。

4. 避坑与排查:CEEMDAN 落地时最容易翻车的五个点

4.1 现象:分解结果每次跑都不一样,IMF 数量跳变

原因:EEMD 和 CEEMDAN 内部有随机噪声生成,没有固定种子时每次 realization 不同。解决:在调用前设np.random.seed(42),或者用库提供的seed参数。如果 IMF 数量仍然跳变,说明trials太小,加到 200 以上。

4.2 现象:低频 IMF 出现明显毛刺,重构信号失真

原因:epsilon或noise_width设太大,噪声分解出的伪分量混进了低频阶。解决:把噪声幅度降到信号标准差的 0.1 倍,同时增大trials补偿噪声抵消效果。如果还不行,检查信号是否归一化,未归一化时噪声幅度参数会失效。

4.3 现象:分解跑得极慢,几分钟出不来结果

原因:trials设了几百上千,信号长度又长,每次集合分解都是 O(N log N) 起步。解决:先降采样到 1000 点以内试参数,确定后再用原始长度跑。或者用max_imf限制阶数,减少后续筛分次数。CEEMDAN 比 EEMD 慢,如果只是做趋势提取,EEMD 够用。

4.4 现象:IMF 主频和预期对不上,故障特征频率消失

原因:信号预处理时用了错误的滤波或去趋势,把特征频率滤掉了。解决:检查detrend是否去掉了有用的低频分量,检查采样率fs是否设对,频谱轴算错会导致主频读数偏移。常见做法是先用原始信号画频谱确认特征频率存在,再跑分解。

4.5 现象:重构误差很大,CEEMDAN 还不如 EEMD

原因:CEEMDAN 的epsilon逐阶控制,如果第一阶设太大,后续残差里噪声残留反而更多。解决:把epsilon降到 0.1 以下,或者改用CEEMDAN的beta参数控制每阶噪声比例。另外检查是否把所有 IMF 都用来重构,丢弃噪声阶后误差应该显著下降。

5. 进阶技巧:用 CEEMDAN 做多分量分离与参数自动寻优

5.1 用重构误差和 IMF 正交性做参数自动搜索

手动调trials和epsilon很费时间,可以写个小循环自动搜。指标用重构均方误差和 IMF 正交性指数,后者衡量各阶 IMF 之间是否泄漏:

def orthogonality_index(imfs): n_imfs = imfs.shape[0] total_energy = np.sum(imfs**2) cross_energy = 0 for i in range(n_imfs): for j in range(i+1, n_imfs): cross_energy += np.sum(imfs[i] * imfs[j]) return np.abs(cross_energy) / total_energy best_score = np.inf best_params = {} for trials in [50, 100, 150, 200]: for eps in [0.1, 0.2, 0.3]: ceemdan = CEEMDAN() ceemdan.trials = trials ceemdan.epsilon = eps imfs = ceemdan.ceemdan(signal_norm) recon = np.sum(imfs, axis=0) mse = np.mean((signal_norm - recon)**2) oi = orthogonality_index(imfs) score = mse + 0.1 * oi # 加权组合,权重按场景调 if score < best_score: best_score = score best_params = {'trials': trials, 'epsilon': eps} best_imfs = imfs print("最优参数:", best_params) print("最优得分:", best_score)

正交性指数越小越好,理想情况接近零。加权系数 0.1 是经验值,如果更看重重构精度就调小,更看重分量分离就调大。这个搜索跑一遍大概几分钟,比盲调靠谱。

5.2 把 CEEMDAN 嵌入故障诊断流水线的注意点

实际故障诊断里,CEEMDAN 通常只是特征提取的第一步,后面接包络谱或者机器学习分类器。几个经验:第一,分解前务必确认采样率足够,轴承故障特征频率往往在几千赫兹,采样率低于 10 kHz 可能根本采不到;第二,保留的 IMF 阶数不要固定,按能量占比动态选,不同工况下有效阶数会变;第三,重构信号做包络谱时,用希尔伯特变换取包络再 FFT,比直接对 IMF 做 FFT 更能突出冲击成分。

我自己的习惯是每次换数据集先跑一遍 CEEMDAN 看 IMF 数量和主频分布,确认分解合理再往下走。有一次偷懒跳过这步,直接拿 EEMD 的结果做分类,准确率比 CEEMDAN 低了 8 个点,回头查才发现模态混叠把故障特征频率拆到了两阶 IMF 里。从那以后我每次换信号都强制走一遍分解质量检查,看重构误差和正交性指数,不达标就调参数重跑。希望这些能帮到你,少走点弯路。

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