CEEMD源码解析:互补经验模态分解原理、参数调优与应用实战 简介CEEMD源代码.zip提供了一套完整的完全集合经验模态分解算法实现面向信号处理、故障诊断、生物医学数据分析等方向的研究者与工程师。资源共5个文件包含4个M脚本和1个MAT数据文件分别用于算法主函数、示例调用、测试数据等环节压缩包整体只有12KB结构紧凑、易于研读。目前已有982人学习下载特别适合希望通过源代码理解CEEMD内部机制并动手实践的用户。算法在经典EMD基础上引入多次白噪声扰动通过集合平均获得更稳定的IMF分量有效缓解模式混合与虚假极值问题配套的心电信号数据和ICASSP2011示例可以帮助读者快速运行验证直观观察sifting筛选过程、残余分量以及希尔伯特谱输出。深入学习这份代码后读者既能掌握CEEMD从参数设置、噪声生成到集成分解的完整流程也能根据实际应用调整噪声幅值与迭代次数为非线性非平稳数据分析及后续研究提供实用起点。1. 为什么我把这个源码包拆了两遍CEEMD 到底能解决什么前阵子需要处理一批实测振动数据老办法用 EMD 拆第一个固有模态函数的时候高频冲击和背景振荡直接混在同一个分量里怎么都分不干净换成 EEMD 之后模式混叠被压住了但每个 IMF 上面又留下了一层明显的白噪声毛刺。真正让我停下来把整套逻辑过了一遍的是 CEEMD 这个看起来只改了一小步的方案它把正负噪声成对注入再做集成平均最后得到的 IMF 既保住了 EEMD 的抗混叠能力又不会残留噪声痕迹。这份 CEEMD 源代码正好把从白噪声生成、EMD 筛分到分量输出的完整链路都摊开了适合处理非线性、非平稳信号的开发者尤其是振动分析、脑电和心率这类需要分离趋势项与振荡分量的场景。下面按“源码结构 → 核心算法 → 跑通 → 避坑 → 改造”的顺序讲完照做就能跑起来。2. 源码结构与技术选型解压 CEEMD 包后先看这几张牌把压缩包解压之后如果一上来就读主文件很容易被一堆嵌套循环和数组维度换算绕晕。这类数学实现的工程封装一般都做得很薄核心不在面向对象设计而在算法流程本身。所以我习惯先看文件组织和依赖再跑一次自带的示例最后才回过头读主类这样理解成本会低很多。2.1 为什么不直接用 EMD 或 EEMD先捋一下算法演进方便你判断这份源码的侧重点。纯 EMD 最大的优势是不需要任何先验参数直接靠信号自身的极值分布去筛出固有模态函数但对实测信号来说它有个老毛病叫模式混叠信号里如果夹着间歇性的小幅冲击同一个 IMF 里就可能同时包含不同时间尺度的振荡物理含义一下就模糊了。EEMD 的解决办法是在每次分解之前往原信号里加白噪声利用白噪声频谱均匀的特性去扰动极值点的局部分布让不同尺度的成分在多次试验中被“打散”到对应的分量里最后做集成平均把噪声影响压回去。思路很聪明代价是平均完之后每个 IMF 里还是会残留一层加性噪声信噪比敏感的场景会很别扭。CEEMD 的思路比 EEMD 更对称在一次试验里同时构造一组正、负白噪声副本对x noise和x - noise分别做完整分解再把两边的结果合在一起取平均。正负噪声在集成中的贡献方向相反理想的平均操作能互相抵消大半所以最终分量的白噪声残留明显更小。这份源码不是简单封装了一个单一函数而是把这条对称链路拆成了几个模块底层处理单次 EMD 筛分上层负责正负噪声配对和整套集成流程。理解这个分层之后后面改参数、换数据就不会只是在黑盒外面瞎试。2.2 解压后的目录结构与各文件职责ZIP 解压后通常能看到这样一个核心文件集合实际命名可能略有出入但职责基本是这几类CEEMD源代码/ ├─ ceemd.py # 互补集成分解主类参数入口 ├─ emd.py # 单次筛分实现极值提取、包络求取 ├─ utils.py # 三次样条包络、停止准则、边界扩展 ├─ example.py # 仿真信号演示直接生成 IMF 对比图 ├─ requirements.txt # numpy / scipy / matplotlib 依赖清单 └─ README.md # 调用方式与参数表ceemd.py是门面你只需要跟它打交道它内部会循环调用emd.py里的单次筛分函数。utils.py承担的是最底层的数学细节包括上下包络的构造和平移的停止准则这两个点也是后续最容易产生边界发散问题的地方。example.py适合当第一个入口跑通了再替换成自己的数据。技术栈上这份实现依赖的只有三样numpy做数组运算和随机数生成scipy.interpolate做三次样条包络matplotlib做结果可视化。没有框架依赖也不需要外部编译器这是它比很多工程框架友好得多的地方。整个代码的架构可以看成三层最底层是包络与筛分中间层是一次完整的单份分解最上层是正负噪声集合的批处理平均。每一层之间都是纯函数调用输入输出都是二维数组这让后期做二次开发时替换某个环节变得很容易。3. 核心模块拆解一个 IMF 从噪声注入到停止条件的完整链路这章我把源码里最关键的几条路径单独拎出来讲。读懂这一层你就不是在“调用工具”而是在“控制算法”。3.1 单轮 EMD 筛分包络、均值与分量判决EMD 的筛分过程可以理解成反复做减法找到信号的局部极大值点拟合出上包络再找到局部极小值点拟合出下包络取上下包络的平均值把它从当前信号里减掉剩下的部分如果满足两条规则就认为它是一个合格的 IMF。先看单次筛分的骨架# emd.py单轮 EMD 筛分核心精简 def _sift_once(signal, max_sift_iter50): prev_h signal.copy() for _ in range(max_sift_iter): upper _envelope(prev_h, kindmax) # 由极大值拟合三次样条 lower _envelope(prev_h, kindmin) # 由极小值拟合三次样条 mean_env (upper lower) / 2.0 h prev_h - mean_env # 从信号中扣掉均值包络 if _is_imf(h, prev_h): return h prev_h h return h_is_imf判断的是两条准则一是全序列里过零点的数量和极值点数量至多相差 1二是上下包络均值在数值上接近 0。源码里常见做法是用一个标准差阈值来放松条件比如连续两次筛分的差值平方和小于原始信号能量的一定比例就提前结束这样能省掉大量无意义迭代。这里有个容易误读的参数max_sift_iter不是分解层数而是单层筛分的最大迭代次数。它不是越大越好筛分次数太多会把信号里的振幅信息磨平出现过平滑。实际工程里我一般保持默认只在分量不光滑时把它从 50 提到 100不要一上来就猛加。3.2 正负噪声配对CEEMD 的互补集成单次 EMD 的代码并不是 CEEMD 的全部亮点上下两层组合起来才是。源码在生成噪声时不只有一个随机序列而是同时生成符号相反的两组# ceemd.py互补集成主循环核心精简 rng np.random.default_rng(42) # 固定随机种子保证可复现 std_x np.std(x) for k in range(n_ensemble): noise rng.normal(0, nstd * std_x, sizelen(x)) imf_plus[k] emd_sift(x noise, max_imf, sift_tol) imf_minus[k] emd_sift(x - noise, max_imf, sift_tol) imfs np.mean(np.vstack([imf_plus, imf_minus]), axis0)参数nstd是噪声标准差相对于原始信号标准差的比例一般取 0.02 到 0.5 之间太小起不到扰动极值分布的作用太大又会让噪声本身成为分解主体所以源码里默认给一个中间值换数据时最需要动的就是它。n_ensemble是正负噪声的配对数也就是说实际分解次数是它的两倍。正负配对的意义要放在平均这一步理解单看x noise的分解结果噪声贡献是正的单看x - noise贡献是负的。把两组结果叠加取均值理想情况下噪声贡献相互抵消真实信号的结构被保留这也是互补集成的核心动机。如果你拿到源码后想把它的残留噪声压得更低可以改成先对每个副本各自保留残差项再做整体平均比直接平均 IMF 更稳一点。3.3 停止条件与残差趋势判断每个 IMF 被筛出来之后源码会重新算一次残差并判断要不要继续往下一层拆。停止逻辑通常落在下面这段# ceemd.py主循环尾部判断 residual residual - imf if np.std(residual) eps: # 残差能量已经小到可以忽略 break if np.all(np.diff(residual) 0) or np.all(np.diff(residual) 0): # 残差变成单调趋势不能再视为可分解的振荡信号 breakeps是一个相对值常见做法是用原始信号的均值绝对偏差乘一个系数避免不同量纲的数据都套死同一个阈值。第二个单调判断非常重要因为自然趋势项往往就是单调递增或递减的如果代码继续拆分就会把趋势项切成好几段人为的振荡分解结果看着更丰富实际上已经没有物理意义了。最后一层残差一般会被保留为趋势项而不是丢弃。在做信号处理演示时它往往就是数据里那个缓慢漂移的基线。你后续做二次开发时输出里最好同时保留imfs和residual否则重建信号时总会少一块能量。4. 跑通指南把这套 CEEMD 源码接到你自己的数据上这一章的目的是让你在半小时内看到第一张 IMF 分解图。我按“环境 → 自带示例 → 替换数据”的顺序写每一步都给出可执行代码。4.1 环境准备与依赖安装先确保本机 Python 版本在 3.8 以上然后建一个独立虚拟环境避免把全局环境弄乱python -m venv .venv source .venv/bin/activate # Windows 下用 .venv\Scripts\activate pip install -r requirements.txt如果 ZIP 里没有 requirements.txt直接安装下面这套也能跑pip install numpy scipy matplotlib pandaspandas不是源码必须的依赖但我建议装上因为后面读 CSV 数据时它最省事。装完之后先不急着替换数据在example.py所在的目录里直接运行python example.py脚本会生成一组由正弦波、趋势项和随机噪声叠加而成的仿真信号然后输出一张每个 IMF 单独占一行的分解图。如果这张图能顺利弹出来说明环境已经通了接下来才需要碰你自己的数据。4.2 用 example.py 验证默认信号example.py内部逻辑一般分四步构造时间轴、生成仿真信号、调用CEEMD.decompose、逐行绘制 IMF。打开它的源码你会看到类似这样的调用方式# example.py演示脚本核心片段 import numpy as np from ceemd import CEEMD fs 1024 t np.linspace(0, 1, fs) x np.sin(2 * np.pi * 10 * t) 0.5 * np.sin(2 * np.pi * 60 * t) x x 0.3 * t 0.05 * np.random.randn(fs) ceemd CEEMD(n_ensemble50, nstd0.1, max_imf8) imfs ceemd.decompose(x)这个例子里三个参数分别代表n_ensemble控制正负噪声配对数nstd控制噪声强度max_imf限制最多拆几层。前两个影响分解质量最后一个影响输出行数新手最容易在这里多填或者少填。对照输出图看前几个 IMF 应该是高频振荡靠后的分量是低频趋势每一行从上到下频率递减这是判断分解是否正常的直观标准。如果某一层出现明显的大幅扭曲而原信号没有这个特征优先怀疑nstd设置不合适。4.3 把仿真信号换成 CSV 实测数据替换数据时最忌讳直接拿原始信号往里塞。代码里加入去均值和去趋势两步能避免首轮 IMF 被直流偏置污染# demo_replace_signal.py替换 CSV 数据的调用示例 import numpy as np import pandas as pd from ceemd import CEEMD df pd.read_csv(signal.csv, headerNone) # 假设单列数据 raw df.iloc[:, 0].dropna().values.astype(float) x raw - np.mean(raw) # 去均值防止直流成分干扰筛分 x x / np.max(np.abs(x)) # 归一化让 nstd 的取值更有意义 ceemd CEEMD(n_ensemble50, nstd0.15, max_imf8) imfs ceemd.decompose(x)如果你的数据本身带有明显线性漂移先做scipy.signal.detrend再进 CEEMD否则趋势项会占用掉一个本可拆出振荡成分的维度。实测数据往往有采样率这个概念但decompose管的只是“数值序列”本身采样率只影响你后续画频率轴时怎么换算。拿到imfs之后画图时给每层加一个垂直偏移比挤在一张图里直观得多import matplotlib.pyplot as plt for i in range(imfs.shape[0]): plt.plot(imfs[i] (i 1) * 2, linewidth0.8) plt.xlabel(Sample Index) plt.ylabel(IMF Offset) plt.show()参数表可以这样定参数取值范围建议说明n_ensemble30–100太小不稳定太大性能开销成倍上涨nstd0.02–0.3数据干净取低值噪声大取高值max_imf6–10超过log2(len(x))后容易过分解跑完这一步分解结果已经落在这个流程的终端接下来要处理的就是工程层面的坏味道了。5. 避坑与排查EMD 系列最容易翻车的几个现场EMD 家族的项目代码本身不难跑难的是结果一出来你就知道它坏在哪。这一章我把高频翻车的几个现场按“现象 → 原因 → 解决”写清楚都是从实际数据集上踩过的坑里提炼的。5.1 首轮 IMF 毛刺明显不是算法 bug而是 nstd 和均值现象分解出的第一个 IMF 波形像一把碎草完全没有清晰振荡原信号看起来却光滑得多。 原因一类情况是信号没去均值直流成分在下包络里制造了虚假极值另一类是nstd设得过大白噪声强度盖过了信号本身的振荡结构。 解决先把x减去均值再归一化然后把nstd按 0.05、0.1、0.15 三档做小步扫描。如果你发现毛刺随着nstd变小而明显改善基本就是噪声注入比例的问题如果改善不明显回去确认极值提取的代码里有没有把端点处理成极值点。5.2 两端发散包络外推过冲现象IMF 图的两端出现幅度很大的喇叭口中间段正常尤其趋势项的尾端翘得很奇怪。 原因三次样条在信号端点附近没有足够的极值点约束插值曲线会以很陡的角度外推导致上下包络发散。 解决在求包络前给序列两端做反射扩展也就是把信号首尾镜像拼接一段再求极值等包络算完再切回原长度。这个操作在utils.py里一般会有实现你只需要检查decompose入口前有没有调用它。如果源码没做先用scipy.signal.argrelmax观测两端极值分布确认后再决定扩展长度。5.3 换数据之后结果不可复现随机种子与集成次数现象同一份数据连续跑两次IMF 波形看起来不完全一样数值对不上。 原因CEE MD 依赖随机噪声如果源码里没有固定随机数种子每次试验的噪声副本都不同或者n_ensemble设得太低平均抵消效应不够结果重现性就差。 解决在生成随机数那一行前面显式加一句np.random.seed(0)固定种子之后再把n_ensemble提到至少 50。注意 50 是配对数实际跑的是 100 次分解耗时翻倍但稳定性好很多。种子固定和参数固定要一起做否则出图确实会漂。5.4 IMF 数量过多导致趋势被切碎需要限制 max_imf现象分解出来 8 个 IMF但后面几个分量在肉眼看来都是同一条曲线的不同切片没有独立物理意义。 原因残差已经进入单调段代码里的单调判断没有生效或者max_imf设到了明显大于合理层数的值。 解决先看残差序列是不是单调如果单调就强制 break如果源码没做判断自己加入前面 3.3 那段逻辑。经验上分量层数不要超过np.log2(len(x))这个参考值超出之后多出来的基本都是数值分解的“副产品”。把max_imf降下来输出更稳健解释起来也更自信。5.5 分解后的 IMF 无法重建原始信号多半是残差没取全现象把imfs按行相加之后和原始信号的差值有明显偏移低频部分尤其对不上。 原因部分实现把最后一层残差丢掉了或者decompose返回值里把残差单独放在一个零行上你画图时只取了 IMF 部分。 解决先检查返回值里imfs.shape[0]是否包含趋势项。常见做法是让decompose完整输出全部分量最后一个分量就是残差如果源码把残差分离了重建时就手动加回去。这一步不解决后面做重构误差评估时你会平白无故多出几个百分点的误差。6. 二次开发思路把源码改造成一个批处理信号分析入口源码跑通只是第一步真正让它产生复用价值的是把它包成一个批处理入口让批量 CSV 可以连续吃进去统一输出分解结果和重建误差。我一般会在这个项目里加一个薄薄的调度函数# rework_process.py把 CEEMD 封装成批处理函数 import numpy as np import pandas as pd from ceemd import CEEMD def process_signal(data_path, fs1024, n_ensemble50, nstd0.15): df pd.read_csv(data_path, headerNone) raw df.iloc[:, 0].dropna().values.astype(float) x raw - np.mean(raw) x x / np.max(np.abs(x)) ceemd CEEMD(n_ensemblen_ensemble, nstdnstd, max_imf8) imfs ceemd.decompose(x) reconstruct np.sum(imfs, axis0) rms_error np.sqrt(np.mean((reconstruct - x) ** 2)) return imfs, reconstruct, rms_error这里我把rms_error作为回归验证阈值换数据时先跑一次记下基线值再来调节nstd如果误差显著变大说明分解开始丢失真实结构参数就该回退。这个习惯帮我避开很多“看着图很漂亮实际上重建全失真”的错觉。改造的下一步是加一个结果导出函数把每层 IMF 的方差占比算出来作为特征向量喂给下游分类模型。注意不要直接把整段 IMF 丢给模型先算能量占比和瞬时频率特征才稳定。从那以后我每次换一个新的数据源都强制先扫一遍nstd从 0.01 到 0.3 的敏感性曲线再决定正式参数分档扫描已经变成了拆分这类实测信号的第一道工序。这套源码最大的价值不在于跑通一个 Demo而在于让你可以直视 EMD 家族的每一个旋钮。真把参数行为摸透之后它就不再是那个需要靠运气调参的黑匣子了。希望帮到你。本文还有配套的精品资源点击获取