☰
ESMD分解Python实战:极值点对称模态分解原理、代码与信号处理应用
2026/10/17 5:31:27 网站建设 项目流程

ESMD分解这名字,在信号处理圈子里已经不算新鲜了,但每次聊到EMD家族时它都处在一种“好像听过、又没人细讲”的位置。简单说,ESMD就是极值点对称模态分解:它和EMD一样,要把复杂信号拆成一串相对简单、有物理意义的模态分量,但它的均值线不是靠上下包络平均求出来的,而是拿相邻极值点的中点插出来。这篇文章不背理论包袱,直接上代码,用一段能跑的Python脚本把ESMD分解全过程跑一遍,再拿几类实测信号验证效果,最后把容易踩的坑摊开讲。

这篇适合谁:手里有振动、心电、脑电、气象或金融时间序列,想做非平稳信号拆解和时频分析,又不想依赖黑箱工具箱的人。我的目标是,你读完之后能完成三件事:第一,看懂ESMD分解的核心逻辑;第二,把代码复制到本地立刻跑出分解图;第三,知道参数怎么调、结果怎么读,而不是只看到一张热热闹闹的图。后面所有内容都围绕这份几十行的演示代码展开。

1. ESMD和经典EMD的差别,其实就藏在“均值线”里

1.1 经典EMD为什么要先构造上下包络

经典EMD的思路很直白:一个复杂振荡可以理解为一串相对简单的振荡叠加。要从信号里摘出一层振荡,必须先估计这条振荡的“中心线”。信号围绕中心线上下摆动,减掉中心线之后,振荡才真的关于零线对称。问题是中心线怎么估计?EMD给出的答案是:用极大值构造上包络,用极小值构造下包络,两条包络一平均,就当作中心线。

听起来很合理,实际上手就常常恼人。三次样条插值要求曲线处处光滑连续,但它在端点附近经常出现过冲。包络线一旦在端点附近甩出个大弯,平均值就跟着跑偏,筛出来的模态两端明显失真。这也解释了为什么几乎每个用EMD的人都要跟端点效应搏斗。设想一个不对称的三角波:上冲快、下跌缓,上包络贴着尖峰,下包络贴着平台,平均出来的中心线往往偏向平台一侧;而相邻极值点的中点,才是每个波动周期里更接近真实重心的位置。这个例子基本能说明ESMD想解决的问题。

1.2 ESMD的极值点对称,不是一个口号

ESMD换了一条思路:既然目标只是找到局部中心,何必先画两条包络再求平均?每个极大值和紧接着的极小值之间,天然有一个对称中心;如果把相邻极值点的中点全部找出来,再把这些中点插值成一条光滑曲线,这条曲线就是中心线的估计。反复用信号减掉这条中心线,直到中点曲线相对信号小到可以忽略,第一个模态就出来了。对剩余残差继续重复相同流程,就能得到一串模态和最后的趋势项。

“极值点对称”四个字的核心,就是这里的相邻极值点取中点。它绕开了上下包络各自的插值误差,只用一条插值曲线工作。这条曲线比包络平均更贴近局部振荡中心,尤其当信号幅度本身不对称的时候,效果会更明显。ESMD的标准实现里还有一些细节,例如在筛选结束后对残余趋势做自适应全局均值修正,但这不影响理解核心筛选循环;我下面给出的演示代码,就是这个循环的直白版本。

1.3 一上一下两包络,为什么不如单条中点线

从误差来源的角度看,EMD的上、下包络分别对极大值和极小值做插值,各自都可能过冲;平均之后,两个方向的误差不一定互相抵消,反而可能叠加。ESMD只对中点序列插值,极值点的位置和幅值是精确已知的样本,插值只有一次,误差来源更单一。另一个容易被忽略的点是计算效率:EMD每轮筛选要做两次包络插值,ESMD只要做一次。极值点数量庞大时,差距会被放大。现代机器上这点时间倒无所谓,但批量处理长序列时,少一次插值不只是快,还少一次引入偏差的机会。

2. 暴力演示:一套能跑通的ESMD核心代码

2.1 完整脚本

老规矩,先把代码完整贴出来。依赖只有NumPy、SciPy和Matplotlib,环境是Python 3.8以上即可,不需要任何专用信号处理工具箱。代码分两部分:一部分是ESMD核心函数,另一部分是演示信号与出图。

import numpy as np from scipy.interpolate import PchipInterpolator from scipy.signal import argrelextrema import matplotlib.pyplot as plt def find_extrema(x): # 找一阶极值点,并强制纳入首尾端点 max_idx = argrelextrema(x, np.greater, order=1)[0] min_idx = argrelextrema(x, np.less, order=1)[0] idx = np.unique(np.concatenate([max_idx, min_idx])) if len(idx) == 0: return np.array([], dtype=int), np.array([]) if idx[0] != 0: idx = np.concatenate([[0], idx]) if idx[-1] != len(x) - 1: idx = np.concatenate([idx, [len(x) - 1]]) return idx, x[idx] def esmd(y, M=6, K=200, rtol=1e-7): y = np.asarray(y, dtype=float) n = len(y) modes = [] residue = y.copy() for _ in range(M): h = residue.copy() for _ in range(K): p, v = find_extrema(h) if len(p) < 4: # 极值点太少,说明快变成趋势项了 break # 相邻极值点取中点,即“极值点对称” mid_p = (p[:-1] + p[1:]) / 2.0 mid_v = (v[:-1] + v[1:]) / 2.0 # 用PCHIP插值得到中心线 L = PchipInterpolator(mid_p, mid_v, extrapolate=True)(np.arange(n)) h = h - L scale = np.max(np.abs(h)) + 1e-12 if np.max(np.abs(L)) <= rtol * scale: break p, v = find_extrema(h) if len(p) < 4: break modes.append(h) residue = residue - h return modes, residue

2.2 演示信号与运行

经典的验证信号用多频正弦叠加:5Hz低频、40Hz中频、120Hz高频,三个分量幅度递减。这样能直观看出模态拆得对不对。

fs = 1000 t = np.arange(fs) / fs x = (np.sin(2 * np.pi * 5 * t) + 0.6 * np.sin(2 * np.pi * 40 * t) + 0.3 * np.sin(2 * np.pi * 120 * t)) modes, trend = esmd(x) fig, axes = plt.subplots(len(modes) + 2, 1, figsize=(11, 2.2 * (len(modes) + 2))) axes[0].plot(t, x) axes[0].set_title("original") for i, mode in enumerate(modes): axes[i + 1].plot(t, mode) axes[i + 1].set_title(f"mode {i + 1}") axes[-1].plot(t, trend) axes[-1].set_title("trend") plt.tight_layout() plt.show()

跑完你会看到,代码输出的模态数量不是严格等于3,而可能是4个或5个。这是ESMD的正常现象:多出来的模态振幅通常极小,频谱上找不到清晰的尖峰,基本可以当噪声或冗余分量扔掉。这种“宁多勿缺”的筛选逻辑,在处理真实信号时更稳妥,因为它不会因为强制预设模态数而忽略掉某些物理分量。

2.3 怎么把代码接到自己的数据上

换成自己的数据只需要注意两点。第一,输入是一维数组,如果是二维或多通道数据,请逐通道单独调用esmd;第二,采样率不会影响算法本身,但会影响你对模态频率的解读。建议在分解前先把数据归一化到[-1,1]区间,顺手把均值去掉,这样rtol的设置比较稳定,不会因为信号量级不同而忽严忽松。

如果是震动信号或心电这类带明显漂移的数据,运行后看看trend曲线的形状,那基本就是你要提取的趋势项。我在实际项目里最常用的套路是:把trend的均值也算出来,如果trend只是接近常数的直流偏移,这在实际指标分析里通常意味着传感器零漂或基线漂移,可以单独记录,不参与模态解释。

3. 拆开逐行看:这几十行代码的动作与心思

3.1 找极值点:为什么强行把首尾端点塞进去

argrelextrema这个函数干的事很简单:对每个内部点,比较它和左右邻居的大小关系,如果同时大于两边就是局部极大,同时小于两边就是局部极小。它的问题在于天然忽略端点,而插值曲线又需要覆盖整个采样区间,所以我在find_extrema里强制把首尾点加进极值序列。这是演示代码里最“暴力”的处理,代价是端点可能被误认为极值点,但这总比中心线在两端突然没有参照、插值外推到天际要好。很多精细的EMD实现会在这里做镜像延拓或者多项式外推,但演示版本追求的是让你先把整体流程跑通。

还要注意一个参数:order=1表示只和相邻1个点比较。如果信号噪声大,会找出一堆跨度极窄的假极值点,这些点会让中点序列变得密集,插值曲线跟随噪声抖动。想把假极值点滤掉,可以把order调到2或3,代价是计算量增加、对一些小幅值真实转折不敏感。信号光滑时用order=1完全够。

3.2 中点插值:为什么用PCHIP而不是三次样条

我把相邻极值点的位置取平均得到mid_p,幅值取平均得到mid_v,这组点完全代表了相邻极值之间的对称中心。对它们插值时,我刻意选了PCHIP(分段三次Hermite插值),而不是EMD里最常用的三次样条。原因很简单:三次样条在全局光滑的同时可能产生过冲,在极值点密度不均的位置会甩出明显高于局部幅值的尖峰;PCHIP则保证插值曲线在数据点之间不会超出相邻点的幅值范围,对“局部中心线”这种角色来说更不容易制造假振荡。

L = PchipInterpolator(mid_p, mid_v, extrapolate=True)(np.arange(n))

这里手动加上了extrapolate=True,让首尾小段也能被线性外推覆盖。很多初学者栽在这里:如果插值器默认不向外延伸,返回的数组在两端会出现缺失值,后面一系列计算全被带偏。我用的SciPy版本里默认是外推的,但还是建议显式写清楚,避免环境差异带来莫名其妙的问题。

3.3 筛选停止条件:一个容易翻车的阈值问题

ESMD的筛选可以理解为反复“剥中心线”:每剥一次,剩余信号h的中线就越平,直到中点曲线L的振幅相对剩余信号可忽略,这个模态就算定型。代码里用的是:

if np.max(np.abs(L)) <= rtol * scale: break

scale取当前剩余信号的最大绝对值加一个极小量,防止除零。这里要特别提醒:rtol不是越大越好。如果设成1e-4,筛选会过早结束,第一个模态里还残留着明显的拍频结构;如果设成1e-10,筛选次数会爆炸增加,而且信号的高频细节会被反复磨平,模态幅度也被压低。我建议从1e-7起步,根据你的信号长度在1e-6到1e-8之间微调。信号长度越长,越需要更小的rtol。

4. 拿真实信号试一把,模态到底干不干净

4.1 多频正弦叠加:分得利落吗

先看2.2节那个5Hz、40Hz、120Hz叠加的信号。运行后,第一模态的频谱尖峰大概率落在120Hz附近,第二模态落在40Hz附近,第三模态落在5Hz附近,trend大约是一条零线。这符合经验:ESMD和EMD一样,倾向于先剥离最高频分量,再逐层往下。你可以用下面几行代码快速验证每个模态的主频:

from numpy.fft import rfft, rfftfreq freqs = rfftfreq(fs, d=1 / fs) for i, mode in enumerate(modes[:4]): spec = np.abs(rfft(mode)) peak = freqs[np.argmax(spec)] print(f"mode {i+1}: peak = {peak:.1f} Hz")

如果我们把信号换成40Hz和41Hz两个极近的频率,ESMD单通道往往就分不开了,两个模态会交替出现能量混叠,每个模态都说不上纯粹。这时候想拆频带,VMD反而更合适。所以我的结论是:ESMD适合频率拉开比较大的情况,不适合做密集频带的精细分离。

4.2 调幅信号:包络形状管不管用

接下来试一个调幅加低频混合的信号:

x = ((1 + 0.5 * np.cos(2 * np.pi * 3 * t)) * np.cos(2 * np.pi * 40 * t) + 0.4 * np.sin(2 * np.pi * 5 * t))

分解之后,你大概率会看到一个模态的主频在40Hz附近,而且这个模态的幅值包络呈现3Hz的缓慢起伏。想验证包络是否还原,用Hilbert变换取包络即可:

from scipy.signal import hilbert env = np.abs(hilbert(modes[1]))

这是ESMD相对EMD一个比较顺手的地方:因为中心线基于极值点对称构造,它对载波和调制包络的分离往往比经典EMD更平滑,包络不会出现严重的锯齿。当然这不代表它一定优于EMD,当调制深度很高、包络与载波频率接近时,两种方法都会费劲,最好先用频谱摸清分量结构再决定方法。

4.3 信噪比不高时,第一个模态是什么

最后看一个贴近实际的工况:5Hz正弦加白噪声,噪声标准差0.5,信号幅度1,信噪比大约6dB。

rng = np.random.default_rng(42) x_noisy = np.sin(2 * np.pi * 5 * t) + 0.5 * rng.standard_normal(fs)

运行后注意:第一个模态不是干净的5Hz正弦,而是一串高频噪声主导的细节,5Hz分量要到后面几个模态才逐渐浮现。这不是bug,ESMD本来就不是滤波器,它按极值密度逐层拆解,高密度极值的噪声天然占优势。强噪声环境下不要指望前几个模态直接对应物理分量,更合理的做法是:把分解出的若干模态里与目标频带对应的一段加权重建,或者先做带通滤波再分解。

5. 调参、端点、插值方式:ESMD实战里的三个命门

5.1 最大模态数M往上调的意义不大

esmd函数里M默认等于6,很多人喜欢把它调到20甚至更高。实际经验是:一段平稳信号能分解出有意义模态的数量非常有限,超过8个之后基本都在“打磨”残差和噪声。最直观的判断方式是看趋势:当残差轨迹已经明显单调,或者极值点数目少于4个时,继续分解就没有物理意义。参数M只是保险丝,不是目标。你真正应该关心的是每段模态的频谱是否有独立主峰。如果两个相邻模态的频谱主峰重叠严重,那是筛分不彻底,不是模态数量不够。

5.2 筛选上限K和rtol是一对搭档

K是单模态最多执行的筛选次数,rtol是提前停止的阈值。两者配合时有个常见误区:把K设得特别大,比如10000,然后觉得rtol设小也没事。但ESMD每轮筛选都会让剩余信号更平滑,K过大会把模态打磨得过于规整,反而丢失转折处的物理特征。我通常让K=200到500,如果内层循环经常因为达到K而break(而不是因为rtol收敛),就说明数据噪声太强,先做预处理,而不是继续加大K。

5.3 端点延拓:比“塞端点”更稳的做法

演示代码里直接把首尾加进极值序列,这种做法在信号两端有明显趋势时会让端点附近模态变形。更稳的做法是镜像延拓:把信号两端各一段向外翻转,延长假想信号,再统一找极值点。比如前端延拓:

def extend_signal(x, m=16): head = 2 * x[0] - x[m:0:-1] tail = 2 * x[-1] - x[-2:-m-2:-1] return np.concatenate([head, x, tail])

寻极值后,把得到的极值坐标减掉m个点的偏移,再截取有效区间。这样可以显著减少端点漂移,代价是代码多出一大截。我的态度是:初筛用演示版,确定研究方法后立刻换镜像延拓版;否则结果重复性会很差。

5.4 插值法可以换成样条,但要让结果说明问题

如果你确实想用三次样条,也可以,把PchipInterpolator换成CubicSpline即可。但换完之后请务必对比同一段信号的端点和峰值位置:样条过冲导致的假模态是ESMD里很隐蔽的坑,能量不大却能在时频谱上形成低频伪影。我个人的习惯是默认PCHIP,特殊需求才换样条。

6. 和EMD、EEMD、VMD放一起比较,ESMD的位置在哪

6.1 一张表看清差异

方法中心线构造模态生成方式典型优势典型劣势
EMD上下包络平均反复筛滤,直到IMF经典、理论基础完整端点效应、模态混叠
EEMD多次加噪声后EMD平均集成平均抑制混叠抗模态混叠较强计算量大、模态不严格正交
VMD变分优化频带约束解最优化问题频带分离干净需预设模态数、惩罚因子敏感
ESMD极值点中点插值单线筛滤逻辑直观、参数少、计算量较低强噪声下模态解释要谨慎

6.2 频率接近时的表现差异

如果两个分量频率接近,比如40Hz和43Hz,ESMD会挣扎,EMD也容易混叠,EEMD靠大量平均能稍微改善,VMD只要把频带设对就能干净分离。反过来,如果你根本不知道信号里有几个分量、频率带宽多少,ESMD反而友好:它自动剥模态,你只要事后看频谱找出主峰。VMD目标函数里硬约束太多,参数没有先验时比较容易过拟合。

6.3 什么情况别选ESMD

信号里存在明显冲击成分(比如齿轮断齿的振动信号),这类尖峰会让极值点中点插值产生大幅伪振荡,ESMD分解出的一堆模态都很难物理解释,不如直接用VMD再加冲击特征提取。数据长度太短(少于约500个采样点)也不用ESMD,极值点数不够,中点序列太稀疏,趋势项会被严重拖歪。最后提醒一句:ESMD做出来的模态不严格正交,用Hilbert谱做时频分析前,最好先做一次质量评估,确认模态之间没有明显的能量泄漏。

7. 实际操作里ESMD最值得养成的三个习惯

先讲第一个习惯:分解之前,永远先看一眼数据的功率谱。ESMD在多少程度上成功,取决于分量频率是否拉开。频谱上主要分量之间隔得开,ESMD就能给你整洁的模态;隔得太近,再怎么调rtol也白搭。

第二个习惯是保存每次分解的M、K、rtol这组参数。真实数据不是一次性跑通就好,改天换了数据段还要复现结果,参数不改,结果才能对比。我的做法是把这组参数写进结果文件名的后缀,比如esmd_M6_K200_1e-7.npz,避免回头找不到当时的设置。

第三个习惯是不要迷信趋势项。ESMD里最后的残差被解释为趋势,但这个趋势受参数影响很大。一个稳健的分析流程是:改变M(比如5、6、7),观察趋势项形状是否稳定。如果M变化时趋势项剧烈变化,说明信号本身没有稳定趋势,这时候强行把趋势项写进报告就容易误导人。

我实际用下来,ESMD最大的价值不在于替代EMD或VMD,而是它能用最短的代码画出最接近物理直觉的分解图。面对一段完全陌生的信号,我通常先上ESMD看全貌,再根据模态特性决定下一步用滤波、希尔伯特谱还是换别的分解方法。这个“先用ESMD探路”的定位,在我处理过的震动监测和生物电信号里帮了不少忙。如果你手里的数据是非平稳、多分量的,建议先跑一遍这篇的演示代码,看看分解出的模态是否跟你的物理直觉对得上。对得上,再沿着EDM家族继续深挖也不迟。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询