news 2026/10/23 9:34:15

CEEMD源码解析:互补经验模态分解原理、参数调优与应用实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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_iter=50): prev_h = signal.copy() for _ in range(max_sift_iter): upper = _envelope(prev_h, kind='max') # 由极大值拟合三次样条 lower = _envelope(prev_h, kind='min') # 由极小值拟合三次样条 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, size=len(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]), axis=0)

参数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): # 残差变成单调趋势,不能再视为可分解的振荡信号 break

eps是一个相对值,常见做法是用原始信号的均值绝对偏差乘一个系数,避免不同量纲的数据都套死同一个阈值。第二个单调判断非常重要,因为自然趋势项往往就是单调递增或递减的,如果代码继续拆分,就会把趋势项切成好几段人为的振荡,分解结果看着更丰富,实际上已经没有物理意义了。

最后一层残差一般会被保留为趋势项,而不是丢弃。在做信号处理演示时,它往往就是数据里那个缓慢漂移的基线。你后续做二次开发时,输出里最好同时保留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 pandas

pandas不是源码必须的依赖,但我建议装上,因为后面读 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_ensemble=50, nstd=0.1, max_imf=8) 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', header=None) # 假设单列数据 raw = df.iloc[:, 0].dropna().values.astype(float) x = raw - np.mean(raw) # 去均值,防止直流成分干扰筛分 x = x / np.max(np.abs(x)) # 归一化,让 nstd 的取值更有意义 ceemd = CEEMD(n_ensemble=50, nstd=0.15, max_imf=8) 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, linewidth=0.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, fs=1024, n_ensemble=50, nstd=0.15): df = pd.read_csv(data_path, header=None) raw = df.iloc[:, 0].dropna().values.astype(float) x = raw - np.mean(raw) x = x / np.max(np.abs(x)) ceemd = CEEMD(n_ensemble=n_ensemble, nstd=nstd, max_imf=8) imfs = ceemd.decompose(x) reconstruct = np.sum(imfs, axis=0) 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 家族的每一个旋钮。真把参数行为摸透之后,它就不再是那个需要靠运气调参的黑匣子了。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 9:32:24

计算机网络2核心解析:从IP路由到TCP可靠传输与故障排查

我做过不少网络相关的项目&#xff0c;真正把我从“交换机能通”带到“网络是门系统工程”这个认知阶段的&#xff0c;是一门后来补修的进阶课程&#xff0c;课程名叫“计算机网络2”。如果你以为它只是在《计算机网络》基础上多讲几章&#xff0c;那就低估了。这门课或者说这个…

作者头像 李华
网站建设 2026/10/23 9:31:43

LUN Masking与Storage Partition:SAN存储隔离实战解析

简介&#xff1a;面向IBM DS系列存储系统管理员与运维工程师的实用操作文档&#xff0c;系统讲解存储管理软件的核心模块&#xff0c;包括存储分区、LUN屏蔽、存储虚拟化和存储资源池管理&#xff0c;尤其适合需要独立调试DS4000或DS4800存储设备的读者。资源包内仅有一个doc文…

作者头像 李华
网站建设 2026/10/23 9:30:56

Python人脸识别系统实战:从dlib环境搭建到摄像头实时识别

去年帮朋友搞一个课堂点到工具&#xff0c;需求很简单&#xff1a;摄像头拍到一张脸&#xff0c;系统能自动判断是不是名单里的人。真正动手才发现&#xff0c;一套完整的Python人脸识别系统从环境搭建到实时识别&#xff0c;中间每一步都有讲究&#xff0c;尤其是dlib编译、特…

作者头像 李华
网站建设 2026/10/23 9:29:55

用 supervisor 管好你的后台进程:从硅基聊天室看进程管理实战

说实话&#xff0c;第一次做“硅基聊天室”——一个让大家挤在同一个网页里&#xff0c;对着同一个大模型接口提问、实时看到流式回复的小型聊天服务——我压根没把进程管理当回事。刚开始我就用 nohup ... & 挂一个 Web 接口进程&#xff0c;觉得能跑就行。可日子久了问…

作者头像 李华
网站建设 2026/10/23 9:22:18

小店吧台空间受限下的自助点餐机部署指南

很多开小店的朋友都有过这样的纠结&#xff1a;看着门口排起的长队&#xff0c;心里着急想提升效率&#xff0c;可一低头看看自己那巴掌大的吧台&#xff0c;又犯了难。奶茶店、快餐店这类小微业态&#xff0c;每一寸空间都是“黄金地段”&#xff0c;放个大设备吧&#xff0c;…

作者头像 李华
网站建设 2026/10/23 9:20:01

AI安全的地基,刚刚被重新浇了一遍

⚠️ 说明&#xff1a;本文基于公开报道整理&#xff0c;涉及企业与政策信息以官方发布为准。10月20日&#xff0c;河北雄安&#xff0c;一块牌子被揭开。 国家数据集团有限公司&#xff0c;正式成立。 当天A股数据要素板块一片涨停&#xff0c;市场反应很直接——又一支国家队…

作者头像 李华