☰
EMD与样本熵:非平稳信号特征提取的Python实现指南
2026/10/16 4:10:55 网站建设 项目流程

1. 序列信号分析的现实问题:为什么非平稳信号不能直接上频谱

1.1 平稳性假设是传统方法最大的坎

做信号分析的人早晚都会遇到同一个问题:手里的数据根本不平滑,甚至谈不上平稳。所谓平稳,通俗讲就是信号在整个采集时间内统计特性基本不变——均值不变、方差不变、频率成分不变。但真实世界里的信号,几乎没有一个是这种性格的:脑电会随意识状态漂移,心电上面叠加着呼吸基线,轴承振动会随着转速和负载变化,风速、股价更是从头到尾都在变。

如果你在这种情况下直接做快速傅里叶变换,得到的是整段信号的平均频谱,里面既有突变瞬间的高频毛刺,又有缓慢趋势。更麻烦的是,FFT把时间信息彻底抹掉了——你不知道某个频率成分是出现在前半段还是后半段,也不知道它是持续存在还是闪了一下就消失。对非平稳信号来说,传统频域方法给出的结果往往是一锅粥,频段之间互相泄漏,幅值谱根本没法准确反映真实的振荡能量分布。

这不是说FFT没用,它依然是频谱分析的基石,只是它假设信号是无限周期、全局平稳的。用这个假设去套分段线性、时变幅值的现实信号,天然会失真。所以工程实践里大家才会不断寻找自适应的方法来替代固定基函数的分解思路。

1.2 EMD的出场逻辑:不靠基函数的自适应分解

小波变换能解决一部分时间-频率定位问题,但它有个绕不过去的坎:你得先选基函数(比如哪种小波母函数),还得选分解层数。基函数选错了,分解结果就会把信号本身的结构扭曲掉——这等于在分析之前就预设了信号长什么样,自相矛盾。

经验模态分解的价值就在这:它是一种完全数据驱动、自适应的分解方法,不需要预先定义任何基函数。上世纪90年代末被正式提出后,这个方法在非平稳、非线性信号处理里迅速火起来,原因只有一个——它真的能靠信号自身的极值分布,把隐藏在原始波形里的不同尺度的振荡一层层剥出来,每一层叫一个固有模态函数。

你可以把EMD理解成剥洋葱:原始信号是整颗洋葱,每一层皮代表一个振荡尺度,剥出来的每一片都有比较纯粹的局部特征,剩下的洋葱心就是趋势项。对脑电、心电、振动这类成分混杂的信号,EMD能在没有任何先验信息的情况下做到这一点,所以它天然适合作为特征提取的前置步骤。

1.3 样本熵在整套流程里扮演什么角色

分完解只是第一步。如果你只把分解后的IMF画出来看,你没法回答一个更本质的问题:某个IMF里的振荡到底有多规律?是接近周期性的稳定波动,还是接近随机的杂乱波动?这时就需要熵类指标出场。

样本熵是在近似熵基础上改进的一个复杂度统计量,用来度量序列产生新信息的速率。样本熵值越低,代表序列越规则、越可预测;样本熵值越高,代表序列越随机、越复杂。它最大的好处是对数据长度要求没有近似熵那么苛刻,而且不包含自身匹配的误差,结果一致性明显更好。

把EMD和样本熵组合起来,是近年信号特征工程里很经典的一条流水线:先用EMD把信号按尺度拆开,再用样本熵对每一层IMF单独打分,最终得到一个代表"各振荡尺度复杂程度"的特征向量。这个向量可以拿去训练分类器、做聚类、做健康指数,用途非常广。我自己在实际项目里用这个方法处理过机械振动数据和生理信号,效果比直接对原始序列求熵值要好很多——原因也很好理解:原始序列把多个尺度的复杂度混在一起算,彼此掩盖,分开算之后信息才真正显露出来。

2. EMD分解程序实现:从筛分逻辑到可运行代码

2.1 固有模态函数的定义和筛分迭代过程

要上手写EMD代码,得先搞清楚它筛分的两个核心判据,因为后续所有代码都是围绕这两个条件设计的。

一个信号片段要被称为固有模态函数,必须满足两个条件。第一个条件:极值点的数量(极大值加极小值)与过零点的数量相等,或者最多相差一个。这个条件保证IMF在当地是窄带的、像一个比较干净的振荡波形,而不是乱七八糟的宽带信号。第二个条件:由局部极大值拟合出的上包络和由局部极小值拟合出的下包络,两者在任意时刻的平均值都等于零。也就是说,这条波形在时间轴上基本是"对称"的,没有偏置。第二个条件在数学上很难严格做到,实际实现里通常用一个阈值来判断包络均值是不是已经够接近零。

筛分的过程是一个循环迭代:第一步,找到原始信号的全体局部极大值点和局部极小值点;第二步,用三次样条插值把极大值点连成上包络线,把极小值点连成下包络线;第三步,计算上下包络的均值线;第四步,用信号减去这条均值线,得到一个更接近对称的候选序列;第五步,检查这个候选序列是否满足IMF的条件,如果满足,它就是第一个IMF;如果不满足,就把这个候选序列当作新的原始信号,回到第一步继续循环。

整个EMD算法就是不断执行这个内层筛分过程,每筛出一个IMF就从残差信号里减掉,再对剩下的残差继续筛,直到残差变成单调趋势或者幅值小到可以忽略。程序实现的核心逻辑就是两层循环:外层循环负责控制出几个IMF,内层循环负责筛出一个IMF所需的反复迭代。

2.2 环境准备与最小可运行示例

在Python生态里,最常用的EMD工具包是PyEMD,底层实现是近年活跃维护的,专门用来做经验模态分解和它的各种变体。安装只需要一条命令:

pip install EMD-signal

注意安装的是EMD-signal这个包名,但代码导入的时候用的是from PyEMD import EMD。常有人在这里搞混,装完之后导入失败,大概率是包名和模块名没对上。另外这个包依赖NumPy、SciPy和Matplotlib,建议先确认这些基础库都已经装好。

下面给一个最小可运行的分解示例。我生成了一段包含三个不同频率正弦成分的合成信号,外加一点白噪声,模拟一个典型的"多尺度嵌套信号":

import numpy as np from PyEMD import EMD import matplotlib.pyplot as plt # 生成测试信号:三个频率叠加 + 高斯白噪声 fs = 1000 # 采样率 1000 Hz t = np.linspace(0, 1, fs) # 1 秒时长 s = (np.sin(2 * np.pi * 5 * t) + # 5 Hz 低频振荡 0.6 * np.sin(2 * np.pi * 20 * t) + # 20 Hz 中频振荡 0.3 * np.sin(2 * np.pi * 80 * t) + # 80 Hz 高频振荡 0.05 * np.random.randn(len(t))) # 噪声 # 执行 EMD 分解 emd = EMD() imfs = emd(s) # 新版也支持 emd.emd(s) 写法 # 绘图查看结果 plt.figure(figsize=(10, 8)) plt.subplot(len(imfs), 1, 1) plt.plot(t, s) plt.title("Original Signal") for i, imf in enumerate(imfs): plt.subplot(len(imfs), 1, i + 2) plt.plot(t, imf) plt.ylabel(f"IMF{i+1}") plt.xlabel("Time (s)") plt.tight_layout() plt.savefig("emd_result.png", dpi=150) plt.show()

跑完之后,如果分解正常,你会看到图从上到下依次是原始信号、IMF1、IMF2……一直到残差。高频振荡通常会先出来,然后是一层比一层低的频率成分,最后的残差是一条平滑的趋势线。

有个细节值得说明:PyEMD里emd(s)默认会自动完成完整的分解,直到残差无法再分解为止。但不同版本的API略有差异,有些版本需要显式调用emd.emd(s),有些版本直接调用对象本身就行,我在不同环境里两种写法都碰到过。最稳妥的做法是安装之后先打印一下版本信息,或者在编辑器里看一下对象有没有emd方法,避免在兼容性上卡半天。

2.3 分解结果怎么判读:别只看图

很多新手分解完之后,看一眼图形觉得"好像挺合理"就算过了。但实际工程交付的时候,不能停留在"看着像",得有几条客观检查标准。

第一条,检查每个IMF是否真的满足那两个条件。PyEMD在筛分过程中本身就做了这个判断,但如果你对分解结果有疑虑,可以用包自带的check_imfs功能。标准做法是:对每个IMF数一下极值点个数和过零点个数,两者差值应该在1以内;再看上下包络均值线的幅值,应该远小于IMF本身的幅值。

第二条,看IMF的个数是否稳定。同一条信号,如果只是换了一个端点,或者加了几个采样点,最后分解出的IMF数量不应该突然从4个变成8个。如果数量剧烈跳变,多半是端点效应或噪声干扰在捣乱,后面第五节详细讲。

第三条,检查残差是否单调。如果残差还在明显振荡,说明分解没有完全,可以把采样点补长或者调整停止条件。PyEMD里有stop_threshold等参数供调整,但多数情况下默认值够用。

说到底,EMD分解判读的核心就一句话:分解结果要让你能一眼看出"哪一层对应哪个物理过程"。如果IMF之间还在互相纠缠,说明分解质量不及格,需要通过参数调整或改用后面的改进算法来解决。

3. 样本熵的程序实现:三个核心参数决定结果可信度

3.1 样本熵的数学逻辑:从"相似对"出发

样本熵的核心思想可以用一句话概括:它统计的是"长度为m的模板向量中,有多少对是相近的;把长度延长到m+1之后,相近的比例还剩下多少"。这个比例的衰减速度,就反映了序列的复杂度。衰减越慢,说明信号没什么新信息,越规律;衰减越快,说明信号频繁出现新变化,越随机。

具体流程是这样的。假设序列有N个点,取一个嵌入维数m,把序列切成N-m+1个长度为m的小窗口,称为模板向量。然后定义一个容差阈值r,任意两个模板向量逐点取差的绝对值,再取这些差值的最大值,如果这个最大差值小于r,就认为这两个模板是"匹配"的。统计所有不作自身匹配的模板对里,匹配对的数量,除以总对数,得到一个比例B。接着把模板长度加长到m+1,用同样的方式再算一遍匹配对数,除以总对数,得到比例A。最后样本熵定义为:

SampEn = -ln(A / B)

注意这里的关键是分母里的比值是A和B的比,不是单纯的对数计数。这个比值本质上是"在m维匹配的条件下,m+1维还继续匹配的条件概率"。如果继续匹配的概率低,说明序列复杂度高,熵值就大。

"不作自身匹配"是样本熵与近似熵最根本的区别。近似熵在统计匹配对数时把自己跟自己的匹配也算进去了,导致算出来的值系统性偏低,而且对数据长度很敏感,短序列结果非常不稳定。样本熵专门去掉自身匹配这一项,所以它更鲁棒,这也是它在工程实践里逐步取代近似熵的核心原因。

3.2 从零手写样本熵:代码与逐行解释

下面这段代码是我在项目里常用的实现,直接用NumPy向量化计算,没有依赖第三方熵值库,方便移植和改参数:

import numpy as np def sample_entropy(x, m=2, r=None, r_ratio=0.2): """ 计算一维序列的样本熵。 参数 ---------- x : 1D array_like 输入序列 m : int 嵌入维数,工程上常用 2 r : float, optional 容差阈值。如果为 None,则使用 r_ratio * std(x) r_ratio : float 容差阈值与序列标准差的比例 返回 ------ sampen : float 样本熵值;如果无法定义则返回 np.nan """ x = np.asarray(x, dtype=np.float64) N = len(x) if r is None: r = r_ratio * np.std(x) # 处理退化情况 if N < 2 * m + 1: return np.nan if np.std(x) < 1e-12: return 0.0 # 恒定序列,复杂度为 0 def _count_matches(m): # 构造 m 维模板向量,形状为 (n_vectors, m) n_vectors = N - m + 1 templates = np.lib.stride_tricks.sliding_window_view(x, m) # 计算所有模板对之间的 Chebyshev 距离(最大值范数) # 通过扩维广播一次算完,避免双重循环 expanded = templates[:, None, :] # (n, 1, m) diff = np.abs(expanded - templates[None, :, :]) # (n, n, m) dist = np.max(diff, axis=2) # (n, n) # 排除自身匹配:把对角线置为无穷大 np.fill_diagonal(dist, np.inf) # 统计匹配对数 matches = np.sum(dist < r) # 分母是总模板对数减去自身对数,即 n*(n-1) denom = n_vectors * (n_vectors - 1) return matches / denom B = _count_matches(m) A = _count_matches(m + 1) if B == 0 or A == 0: return np.nan return -np.log(A / B)

这段代码里有两个值得注意的实现细节。第一,我用sliding_window_view来构造模板向量,它在NumPy 1.20及以上版本里可用,内存效率比逐条切片更高。如果你的NumPy版本比较老,改回列表推导式[x[i:i+m] for i in range(N-m+1)]也完全没问题,数据量不大的时候性能差距可以忽略。第二,距离矩阵的计算用了广播机制,时间复杂度是O(n²),n等于N-m+1。这个复杂度在短序列上没问题,但序列一长就会很慢,第五节会专门谈怎么优化。

跟第三方库对比验证也很简单。直接用pip install antropy,然后调用它内置的样本熵函数跑同一段数据,结果应该和上面的手写实现几乎一致。我自己测过,在序列长度1000、m=2、r=0.2倍标准差的情况下,两个实现的结果差异通常在小数点后第五位以内,足以证明手写逻辑的正确性。

3.3 参数选择实测心得:m、r、N怎么搭配

样本熵的三个核心参数,任何一个选不好,结果都会失真。工程上积累下来有几条经验值,但更重要的是理解背后的约束。

嵌入维数m,最常用的是2,少数场景下用3。m值越大,捕捉到的模式越精细,但需要的序列长度也越长。如果N不够,模板向量数量太少,匹配统计量方差巨大,熵值不可信。参考的底线是N至少要大于10的m次方再乘以10左右,也就是说m=2时N至少要几百,m=3时N至少要几千。信号不够长却硬要用大m,是新手最容易犯的错误。

容差阈值r,通常取原始序列标准差的0.1到0.25倍,最常用的是0.2倍。r取得太小,匹配对数趋近于零,A或B变成0,熵值直接没有定义;r取得太大,几乎所有模板都互相匹配,熵值趋近于0,区分度消失。这里有一个非常实际的问题:如果信号幅值波动很大,同一段数据的r固定在一个值上,分解出的不同IMF之间可比性就会出问题。所以实践中我几乎不用绝对阈值,而是用r_ratio * np.std(该序列)的方式,让每个IMF用自己的标准差做归一化。这样算出来的熵值反映的是"相对复杂度",更公平,也更容易跨样本比较。

序列长度N和数据长度对熵值的稳定性影响也很大。经验上,N在500以下时熵值波动明显,N在1000到5000之间时结果就比较稳定了。如果你的原始信号很长,也没必要一股脑全算,可以分段算然后取均值,既能降低波动,又能拿到统计特征,一举两得。

4. 整条流水线实操:从一份原始信号到特征向量

4.1 预处理:EMD对数据质量远比你想的敏感

很多人拿到原始信号直接丢进EMD,这是个大坑。EMD靠的是局部极值来构造包络,如果数据里有异常毛刺、直流偏置或趋势项,极值点会被污染,包络线跟着变形,分解结果就全错了。

预处理至少要做三件事。第一,去除趋势和直流偏置,否则残差项会吸收掉本来不该有的低频漂移,甚至让IMF1的包络不对称。第二,做带通滤波,把采集设备引入的高频噪声和工频干扰先滤掉一部分,但注意滤波不能做得太狠,否则会把有用信号的边缘信息削掉。第三,检查是否有多余的重复采样点或缺失值,这些都会让三次样条插值出问题,最好是先重采样到统一间隔。

我用一个简单的低通滤波加去均值示例来说明:

from scipy.signal import butter, filtfilt def preprocess_signal(s, fs, lowcut=0.5, highcut=100.0): """ 简易预处理:去除直流 -> 带通滤波 """ s = s - np.mean(s) # 设计带通滤波器 nyquist = 0.5 * fs low = lowcut / nyquist high = highcut / nyquist b, a = butter(2, [low, high], btype='band') # 零相位滤波,避免相位偏移 s_filtered = filtfilt(b, a, s) return s_filtered

这里特意用了filtfilt而不是lfilter,因为lfilter会引入相位延迟,对后续EMD的极值定位会造成定时偏差。零相位滤波能做到波形位置和原始信号完全对齐,代价是计算量翻倍,但对离线分析来说完全值得。

4.2 分段、分解、熵值计算的流水线设计

实际项目里很少直接对一整段长信号做EMD,原因有二:一是计算量太大,二是长信号的局部特征是会被平均掉的。更常见的做法是把信号切成固定长度的窗口,每个窗口独立做分解,再独立算熵值,最后拼成一个特征序列。

窗口长度怎么定?一个经验性选择是让窗口内包含至少10到20个最低频关心成分的周期。比如你要分析的最低频是1Hz,采样率256Hz,那窗口至少要有10秒,也就是2560个点。窗口太短,低频成分根本没法在EMD里形成完整的振荡,分解结果会碎得不像样。

分段完成后还有个绕不开的问题:EMD每次分解出的IMF数量不固定。有的窗口能分解出5个IMF,有的窗口只能分解出3个,这就导致特征矩阵的列数对不齐。处理这个问题的常见做法是只保留前K个IMF(比如前4个),多的截掉,少的补零,或者把缺失位置的熵值设为一个特殊标记,留给后续模型去处理。工程上选前4个IMF是够用的,因为后面那些低频IMF和残差往往代表趋势,跟健康状态特征的关系通常不大。

4.3 整段流水线的核心代码

下面是我在多个项目里用下来比较顺手的完整流程封装,输入一段原始信号,输出一个特征向量:

def emd_sampen_pipeline(signal, fs, m=2, r_ratio=0.2, n_imf=4, win_sec=10): """ 完整的 EMD + 样本熵特征提取流水线。 参数 ----- signal : 1D array 原始信号 fs : int 采样率 m : int 样本熵嵌入维数 r_ratio : float 容差比例,基于每个IMF的标准差 n_imf : int 保留的IMF数量 win_sec : float 滑动窗口秒数 返回 ------ feature_vec : ndarray 每个窗口的熵值特征向量,形状为 (num_windows, n_imf) """ win_len = int(win_sec * fs) step_len = int(win_len / 2) # 50% 重叠 emd = EMD(parallel=True) # 开启并行计算 features = [] for start in range(0, len(signal) - win_len + 1, step_len): win = signal[start : start + win_len] win = preprocess_signal(win, fs) # EMD 分解 imfs = emd(win) # 只保留前 n_imf 个 IMF,不足则补 NaN selected = np.full(n_imf, np.nan) for i in range(min(len(imfs) - 1, n_imf)): # 一般把残差排除 imf = imfs[i] r = r_ratio * np.std(imf) selected[i] = sample_entropy(imf, m=m, r=r) features.append(selected) return np.array(features)

这段代码有几点要特别说明。第一,EMD(parallel=True)是PyEMD提供的并行选项,多核机器上能明显加速;但如果你的机器CPU核心数不多,开启反而有线程调度开销,按实际环境取舍。第二,保留IMF时我刻意把最后一个残差项排除掉了,因为残差多数是趋势项,序列单调,样本熵算出来意义不大。第三,缺失位置用NaN填充,后续在特征矩阵阶段可以统一做丢弃或插值,不要在流水线中途擅自补零,因为补零会把"没有这个成分"错误地表达成"这个成分的熵值为0",语义上完全不对。

整个流水线跑完之后,你会得到一个形状为(窗口数, IMF数)的特征矩阵。每一行代表一个时间窗口,每一列代表某个振荡尺度下的样本熵值。这个矩阵可以直接喂给随机森林、支持向量机做分类,也可以作为时间序列特征去做聚类、做趋势监测。我在实际做轴承故障识别时,就是靠这几个熵值特征在二维散点图上把正常状态和故障状态完全分开了,效果远比直接对原始信号算统计量要好。

5. 实测中反复踩到的坑与对应避坑方案

5.1 端点效应:包络线在首尾处最容易失控

EMD有个天生缺陷:三次样条插值包络时,信号两端没有极值点约束,包络线会在端点处大幅发散,产生所谓的端点效应。这会导致分解出的IMF在首尾两端明显摆动异常,严重时甚至会把分解结果带偏。

我一开始没专门处理这个问题,结果就是每次分段窗口的边界处,IMF1的幅值比真实信号还大好几倍,整个特征向量都被污染了。后来用了一个非常朴素但有效的办法:在分段时给每个窗口两端各延伸一小段数据(比如加原始窗口10%的长度),分解完之后把延伸的部分砍掉,只保留中间段的IMF。这个"先延伸、后切除"的思路实现简单,效果立竿见影。

PyEMD本身也提供了端点处理的选项,比如extrema_detection参数可以调整极值检测方式。但说实话,用数据延伸法最省心,不依赖算法内部实现,换成任何版本都不会失效。

5.2 模态混叠:一个IMF里掺着两个尺度的信号

模态混叠指的是某一个IMF里混入了不同时间尺度的振荡成分,表现为IMF的幅值在不同段落之间突然跳变,或者频率成分来回切换。典型的触发场景是信号里存在间歇性的小幅冲击——比如振动监测数据里偶尔出现的锤击脉冲,它会让包络线被极值点带偏,导致筛分过程把一个纯净频率切成两段,混到相邻的IMF里去。

模态混叠对熵值特征的影响非常大,因为熵值对波形的"规律程度"很敏感,混叠后的IMF算出来的熵值既反映不了这个尺度本身的复杂度,也没有统一规律,特征在样本间会剧烈抖动。排查的时候最直观的方法是看IMF的瞬时频率曲线,如果频率在一段内跳变超过两倍,基本可以断定混叠了。

治理模态混叠的正规手段是改用集合经验模态分解(EEMD)。它的思路很有意思:给原始信号加上多次不同的白噪声,每一次都做一个EMD分解,最后把所有结果取平均——白噪声会打散原来的间歇性脉冲,让多尺度成分均匀分布,而被分解的真实模态在多次平均后又能完整体现出来。付出的代价是计算量成倍增加,但特征稳定性的提升完全值得,后面第六节会展开讲。

5.3 样本熵返回0或inf:多半是参数边界问题

手写样本熵代码的时候,最烦的异常是返回值突然变成inf或0。这通常对应两种边界情况:匹配对数A或B为0,或者序列本身是恒定值。

匹配对数为0的原因可能有几种:r取值太小,信号噪声又比较干净,导致没有任何两个模板向量的距离小于r;序列太短,模板对数量少,本来就很难匹配上;序列本身有大量重复值,模板变化微弱。碰到这种情况,我的排查顺序是:先看标准差是否接近0,如果是,直接返回0并写进注释说明;如果不是,那就放大r值,从0.1倍标准差往上试,找到一个结果稳定的区间。

另外一个不容易察觉的坑是:死板地用固定r值去做多组信号的熵值对比。不同组的信号幅值差异很大时,同一个r对高幅值组太严格(匹配少),对低幅值组太宽松(匹配多),熵值就没有可比性。我在实际处理多个传感器数据时就吃过大亏,输入数据里两种状态幅值差了三倍,用固定r算出的熵值几乎无法区分状态。改用r_ratio * std的自适应方案后,问题立刻解决。

5.4 计算耗时:O(n²)复杂度不是闹着玩的

样本熵的双重匹配计算是O(n²)复杂度。单看一个窗口可能没什么感觉,但一旦你像我一样要处理几十个传感器、每天上百万个数据点,这个成本就会爆炸。我在一次批量离线计算任务里,一小时的原始数据跑了快三小时才出结果,当场就想优化。

优化的手段有几个。第一,用NumPy矩阵运算替代Python双层循环,同样是O(n²),但速度能快一到两个数量级,这就是3.2节代码采用广播方式的原因。第二,可以用multiprocessing并行处理不同窗口的熵值计算,按照CPU核心数平分任务。第三,如果序列特别长,可以在不损失信息的前提下对序列做降采样,但注意降采样率不能高于目标频带最高频率的两倍,否则信号细节会丢。这三个手段叠加,我后来把同样的批量任务压缩到了十几分钟,完全可接受。

6. 从EMD到CEEMDAN与多尺度样本熵的进阶路线

6.1 为什么很多项目最终改用CEEMDAN

上一节提到,EEMD用"加多次白噪声再取平均"来压制模态混叠,效果不错,但它有个新问题:加了噪声之后分解出的IMF不再严格满足IMF条件,而且多次平均后不同分解之间的结果不能对齐,数值上会产生细微偏差。集合经验模态分解的改进版——完全自适应噪声集合经验模态分解,即CEEMDAN,在每个分解阶段独立地添加自适应噪声,逐级剥离固有模态,使得分解结果具有完备性和更好的重构性。

在PyEMD里切换到CEEMDAN是极其简单的:

from PyEMD import CEEMDAN ceemdan = CEEMDAN() imfs = ceemdan(s)

只需要把EMD替换成CEEMDAN,其余调用方式几乎不变。代价是计算时间显著增加,因为内部要做多次EMD分解。我的实践经验是:如果数据长度在几千点以下,直接用CEEMDAN;如果数据极长,优先用EEMD并通过设置噪声次数来控制耗时。这个取舍没有绝对标准,按你的业务时延需求来定。

6.2 多尺度样本熵:另一个维度的复杂度刻画

EMD加样本熵的思路是"按尺度拆开再分别度量复杂度",多尺度样本熵(MSE)则是完全不同的路线:它利用粗粒化操作,把原始序列按不同因子压缩成多个时间尺度,然后对每个尺度计算样本熵,观察熵值序列的走势。

粗粒化的做法是把序列按尺度因子k平均分成很多段长度等于k的块,每个块取平均,得到一个长度为N/k的新序列。k增大,序列越来越平滑,越来越"懒"。健康状态正常的系统,在各尺度上熵值保持平稳;而故障、病理状态下,熵值往往出现断崖式下降。

MSE和EMD熵特征并不是二选一的关系,我经常把两者拼起来用:把EMD分解后的前几阶IMF分别算样本熵,再对原始序列做多尺度样本熵,最终的特征向量同时包含"分尺度的复杂度"和"跨尺度的复杂度演化"两类信息。这样虽然特征维度高了些,但用于状态分类的准确率通常比单一方法高不少。

6.3 我实际用下来最有价值的两个场景

说两个亲测有效的场景,供读者判断这套方法是否值得深入。

第一个是机械轴承振动信号的状态识别。正常轴承的振动是近似周期性的滚珠通过频率,以及少量随机扰动,分解出的IMF熵值偏低;轴承出现局部故障时,冲击成分会在多个IMF里扩散,熵值特征会发生非常明显的跳变。我曾经用EMD加样本熵的组合,加上一个简单的随机森林分类器,在三种轴承状态上做到了95%以上的准确率,关键优势是特征不需要人工设计,完全由数据自适应生成。

第二个是脑电信号的特征分析。脑电节律的复杂度变化和人的意识状态密切相关,直接把原始脑电算熵值很容易被肌电伪迹干扰,先用EMD把不同节律波段分离,再分别计算样本熵,能明显提高特征的稳定性。这个方向在认知状态评估、疲劳检测等领域都有很多应用空间。

从我个人的经验来说,这套方法最值钱的地方是它把"不知道信号里有什么"的困境,转化成了"每个振荡尺度各有多少规律性"的可量化描述。你不需要提前知道信号里有哪些频率分量、哪些分量更重要,EMD帮你完成尺度分离,样本熵帮你把每个尺度压缩成一个可比性的数字,剩下的建模工作就变得非常轻量。如果要从头做一个信号特征提取方案,我大概率还是会先从这个组合入手,等它不够用了再考虑更复杂的深度模型。

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

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

立即咨询