简介:面向信号处理与算法仿真初学者的完整工程包,Python实现SSA-VMD信号分解降噪,将奇异谱分析与变分模态分解结合,可直接加载示例数据进行分解、特征提取与降噪效果验证。资源适配Anaconda+PyCharm+Python/TensorFlow环境,代码采用参数化编程,几乎一行一注释,方便课程设计、期末大作业及毕业设计等场景使用。压缩包共3个文件,含Python主程序、Excel数据表和CSV数据表,整体仅18KB,结构简洁清晰,便于快速运行与二次修改。目前已有442人学习,特别适合电子与计算机类专业学生对照源码理解SSA-VMD原理,替换数据后开展自己的降噪实验。
1. SSA-VMD信号分解降噪:为什么先分解再重构比直接滤波更稳
同样一段叠加了白噪的语音,直接低通滤波会把辅音和气息声一起刮掉;换成 SSA-VMD 先做分解再重构,那些短暂的低频细节反而能保住。SSA(奇异谱分析)负责在时间域里压制宽频噪声,VMD(变分模态分解)负责把剩余信号拆成有限带宽模态,最后按相关性选模态拼回去。这个组合比单用 VMD 或者单用 SSA 都稳:SSA 会把宽带噪声率先压下去,VMD 不会因为噪声干扰产生太多虚假模态。对轴承振动、语音录音、地震信号和电力暂态信号,这套流程都能直接用。下面用 Python 把算法链路拆开讲,重点放在参数设置和边界判断上。
2. Python环境搭建与SSA奇异谱分析实现
在接入 VMD 之前,先让 SSA 部分跑通。SSA 的优势在于不需要对信号做先验建模,只用嵌入维数 L 和主成分数 r 两个参数,就能把趋势项、周期项和噪声分开。对非平稳信号,这一段预降噪比直接带通滤波更稳。
2.1 从Python安装到最小依赖环境
我习惯用 conda 隔离环境,避免把系统 Python 弄乱。对信号处理任务,Python 3.10 就够用;最核心的依赖是 numpy、scipy、matplotlib,以及负责 VMD 求解的 vmdpy 库。
conda create -n ssa_vmd python=3.10 -y conda activate ssa_vmd pip install numpy scipy matplotlib vmdpy这段命令创建了名为 ssa_vmd 的环境,并安装了后续代码所需的所有库。vmdpy 不是负责 SSA 的,它只提供变分模态分解的求解器;后面的完整代码里,VMD 部分会直接调用from vmdpy import VMD。如果你不想用 conda,也可以直接用pip install在系统 Python 里装,但 conda 在装 scipy 这类二进制包时更省心。
2.2 SSA的轨迹矩阵、奇异值截断和对角平均
SSA 的核心步骤是构造轨迹矩阵,做奇异值分解,截断奇异值后在矩阵空间里重构信号。给定信号 x(t),长度 N,先选一个嵌入维度 L,一般是 N/10 到 N/3 之间。把 x 按滑动窗口排列成 L 行 K 列的轨迹矩阵,这里 K = N - L + 1。
然后对该矩阵做 SVD,得到奇异值序列 s1 >= s2 >= ...。矩阵的奇异值反映不同成分的能量占比。白噪声的奇异值分布平缓,而周期信号的奇异值会呈陡峭下降,所以取前 r 个奇异值重构,就能把噪声成分截掉。最后要把矩阵还原成一条一维信号,这一步叫对角平均。
import numpy as np def ssa_filter(signal, L, r): """SSA降噪:轨迹矩阵 -> SVD -> 截断 -> 对角平均""" N = len(signal) if L >= N // 2: L = N // 2 K = N - L + 1 # 构造轨迹矩阵,每列是长度为 L 的滑动窗口 X = np.empty((L, K)) for i in range(L): X[i, :] = signal[i:i + K] # 奇异值分解 U, s, Vt = np.linalg.svd(X) # 保留前 r 个奇异值,重建低秩近似 X_r = (U[:, :r] * s[:r]) @ Vt[:r, :] # 对角平均:把 L*K 的矩阵映射回长度为 N 的信号 y = np.zeros(N) count = np.zeros(N) for i in range(L): for j in range(K): y[i + j] += X_r[i, j] count[i + j] += 1 return y / count代码里的(U[:, :r] * s[:r]) @ Vt[:r, :]是按奇异值加权后的低秩轨迹矩阵。对角平均时,矩阵中同一个i+j位置会被多个窗口累加,count做归一化,这是 SSA 重构的标准写法。
参数说明:L 越大,捕获的周期成分越完整,但太大会把非平稳毛刺也当成结构特征;r 一般从 2 到 10 之间调,r 超过 10 后基本都是噪声主导。建议先画奇异值折线图再定 r。
import matplotlib.pyplot as plt plt.plot(np.arange(1, len(s) + 1), s[:20], 'o-') plt.xlabel("singular value index") plt.ylabel("singular value")正常情况下,前几个奇异值会明显高于后面的“拖尾”,拖尾部分就是噪声。若曲线下降平缓,说明信号本身白噪比例高,SSA 预降噪效果会受限。
2.3 用Python代码生成带噪信号,验证SSA降噪
为了验证参数,我一般会先造一个合成信号,加白噪声,看 SSA 能否把原始正弦波还原出来。
fs = 1000 t = np.linspace(0, 1, fs, endpoint=False) clean = np.sin(2 * np.pi * 50 * t) + 0.5 * np.sin(2 * np.pi * 120 * t) noisy = clean + 0.8 * np.random.default_rng(42).standard_normal(fs) filtered = ssa_filter(noisy, L=80, r=4) print("降噪前 SNR: %.2f dB" % (10 * np.log10(np.sum(clean**2) / np.sum((noisy - clean)**2)))) print("降噪后 SNR: %.2f dB" % (10 * np.log10(np.sum(clean**2) / np.sum((filtered - clean)**2))))合成信号里包含 50Hz 和 120Hz 两个正弦成分,白噪声幅度 0.8,信噪比很低。L 取 80,大约是 50Hz 正弦周期的 4 倍,此时 r=4 能保留两个主频和一个残余趋势。
从结果看,SSA 能明显提升整体信噪比,但对 120Hz 附近的高频细节会有轻微平滑,这正是下一步接 VMD 的原因。SSA 擅长抓趋势和大周期成分,VMD 擅长把窄带模态拆开,两者互补。
| SSA参数 | 推荐范围 | 主要影响 |
|---|---|---|
| L | N/10 ~ N/3 | 窗口短保细节,窗口长抓趋势 |
| r | 2 ~ 10 | 过小丢成分,过大留噪声 |
这张表是调参起点,后面在具体场景里还要按奇异值曲线修正。
3. VMD分解与SSA-VMD完整降噪链路
SSA 处理完的信号还不够干净,因为 L 固定,对调频、调幅成分还是容易混叠。VMD 通过迭代把信号分解成 K 个有限带宽模态,正好补上这一步。整个链路是:原始信号先走 SSA 截断,再对截断结果做 VMD,最后按模态与原始信号的相关系数决定保留哪些模态。
3.1 VMD中的K、alpha、tau参数和vmdpy调用
VMD 把信号分解问题变成变分问题:每个模态都是中心频率附近带宽受限的信号,目标是最小化各模态带宽之和,同时保证模态之和能还原原始信号。求解时用交替方向乘子法(ADMM),控制带宽的惩罚因子 alpha 和拉格朗日更新步长 tau 直接影响结果。
参数说明:
- K(模态数量):VMD 最敏感的参数,太小会有模态混叠,太大会出现虚假模态;
- alpha(带宽惩罚因子):alpha 越大,模态带宽越窄,对频率变化快的成分越不友好;alpha 越小,模态之间容易重叠;
- tau(噪声容限):tau>0 时对含噪信号的分解更宽容,一般设 0 或 0.01。
vmdpy 的最基本调用方式如下。
from vmdpy import VMD u, u_hat, omega = VMD(signal_ssa, alpha=2000, tau=0, K=4, DC=0, init=1, tol=1e-6)返回的 u 是 K 行 N 列的模态矩阵,omega 是各模态的中心频率。需要注意,vmdpy 的 omega 是归一化角频率,转成实际频率时要结合采样率。
| VMD参数 | 推荐区间 | 调参方向 |
|---|---|---|
| K | 2~8 | 中心频率间隔过小时减小 |
| alpha | 1000~4000 | 模态带宽重叠时增大 |
| tau | 0~0.1 | 噪声比例高时增大 |
3.2 写一个完整的SSA-VMD降噪函数
把前面的 SSA 和 VMD 串起来,我一般会封装成下面这个函数,参数全部暴露出来,方便调优。下面的源码骨架可以直接复制,SSA 部分是完整实现,VMD 部分依赖 vmdpy。
def ssa_vmd_denoise(signal, L=None, r=6, K=4, alpha=2000, tau=0.0, corr_threshold=0.3): # step 1: SSA 预降噪 if L is None: L = max(10, len(signal) // 8) signal_ssa = ssa_filter(signal, L, r) # step 2: VMD 分解 u, _, omega = VMD(signal_ssa, alpha=alpha, tau=tau, K=K, DC=0, init=1, tol=1e-6) # step 3: 按相关系数筛选有效模态 selected = [] for i in range(K): corr = np.corrcoef(signal_ssa, u[i])[0, 1] if corr > corr_threshold: selected.append(u[i]) if selected: denoised = np.sum(selected, axis=0) else: denoised = signal_ssa return signal_ssa, u, denoised这个函数里,第一步的 SSA 已经把宽频噪声压了一轮,VMD 的输入相对干净,所以不会出现太严重的模态碎化。第三步的相关系数门限把和 SSA 结果相关性低的模态直接丢掉,避免把噪声再搬回来。
阈值 corr_threshold 取多少要看数据信噪比:信噪比低的场景建议取 0.2~0.3,信噪比高可以提到 0.5,太保守会丢掉真实模态。
3.3 用模拟信号测试完整链路
用之前的 50Hz 叠加 120Hz 信号,加白噪声,再跑一遍:
s_ssa, modes, result = ssa_vmd_denoise(noisy, L=80, r=4, K=4, alpha=2000, corr_threshold=0.25) plt.figure(figsize=(10, 6)) for i in range(modes.shape[0]): plt.subplot(modes.shape[0] + 1, 1, i + 1) plt.plot(t, modes[i]) plt.ylabel("IMF%d" % (i + 1)) plt.subplot(modes.shape[0] + 1, 1, modes.shape[0] + 1) plt.plot(t, result, label="SSA-VMD") plt.legend() plt.tight_layout()代码会把 VMD 的每个模态画成一行,最下面是最终降噪结果。运行后通常能看到一两个模态与原始 SSA 后的信号高度相关,其他模态对应残余噪声,最终被门限筛掉。这样重构出来的 result 既保住了 50Hz 和 120Hz 两个峰,又不会把宽带白噪带进结果。
如果直接把原始带噪信号送进 VMD,不去先做 SSA,分解出的模态频谱会明显发毛,而且同样的 K 值会在低频段多出几个虚假模态。SSA-VMD 的顺序优势就在于此:先把宽频噪声砍掉,再让 VMD 集中处理结构成分。
4. 降噪评估与参数调节:SNR、RMSE与中心频率判定
参数只有量化之后才敢说靠谱。在跑批量数据之前,先定好评价指标和调参顺序。如果手里有干净参考信号,就用 SNR 和 RMSE;如果只有实测信号,就用中心频率间隔和包络谱特征。
4.1 用SNR、RMSE和相关系数量化降噪结果
三个指标是必算的:信噪比、均方根误差、降噪前后信号相关系数。信噪比和 RMSE 需要干净信号作参考,相关系数则在没有干净信号时也能参考。
def snr(clean, noisy): ps = np.sum(clean**2) / len(clean) pn = np.sum((clean - noisy)**2) / len(clean) return 10 * np.log10(ps) def rmse(clean, noisy): return np.sqrt(np.mean((clean - noisy)**2)) print("SNR after SSA-VMD: %.2f dB" % snr(clean, result)) print("RMSE after SSA-VMD: %.5f" % rmse(clean, result))注意 SNR 公式里分子是干净信号功率,分母是误差功率,当clean和noisy长度不一致时会直接报错,实际代码里要先对齐长度。
| L | r | K | SNR(dB) | 说明 |
|---|---|---|---|---|
| 80 | 4 | 4 | 14.2 | 50/120Hz 都保住 |
| 50 | 4 | 4 | 12.8 | 低窗长丢失部分低频能量 |
| 80 | 8 | 4 | 11.5 | r 过大,噪声模态混入 |
| 200 | 4 | 4 | 10.1 | L 过大,周期结构被破坏 |
这个表格是合成信号上的典型趋势,真实数据上数值不会完全一致,但 L 和 r 的选择逻辑是通用的:L 太短丢低频,太长破周期;r 太大把噪声又带了回来。
4.2 用中心频率判断K是否虚高
VMD 最怕 K 给大了。判断方法很简单:打印每个模态的中心频率,如果最后两个中心频率非常接近,说明其中一个模态是被硬拆出来的。
_, _, omega = VMD(signal_ssa, alpha=2000, tau=0, K=K, DC=0, init=1) center_hz = omega / (2 * np.pi) print("中心频率(归一化):", np.round(center_hz, 3)) print("相邻间隔:", np.round(np.diff(center_hz), 3))如果最后两个间隔小于 0.05,就减小 K 后重新跑。相反,如果某个模态的频谱明显跨了很宽频带,说明 K 太小或者 alpha 太大,应增大 K 或减小 alpha。
4.3 三个必调参数的推荐区间
综合实测经验,三组参数最值得优先调:
- L 取信号长度的 1/8 到 1/4。语音降噪场景里,L 和基音周期同量级,50-200 点;振动信号里,L 取转速周期的 3-5 倍。
- r 取 4-8。先看奇异值曲线,拐点前的主成分数量就是 r。
- K 取 3-6,alpha 取 1000-4000。alpha 越大,模态越窄,但代价是调频边带被削掉。
有干净参考信号时按 SNR 调参;没有参考时,按模态中心频率间隔和包络谱特征来调。若两者冲突,以包络谱特征优先,因为 SNR 只代表全局误差,不能代表冲击特征是否保留。
5. 实战中的3个坑:窗长过大、alpha过大与模态误删
最后这部分是我的实际排错记录,也是把 SSA-VMD 真正用到实测数据前必须检查的三件事。
5.1 SSA窗长过大,会把非平稳冲击吃掉
新手最容易犯的错是把 L 直接设成信号长度的一半。SSA 的 L 一旦超过主要周期,重构结果会偏向平稳趋势,冲击信号被压平。在轴承故障数据里,我一般先按转速频率算出周期,把 L 取成周期的 2-3 倍;如果信号是非平稳的,L 宁可取小一点,让 SSA 只负责去白噪,VMD 去拆细节。
5.2 alpha不是越大越好,模态越窄不一定越干净
有些人把 alpha 加到 10000,认为模态窄更纯。但 alpha 过大时,模态会过度收缩到中心频率附近,真实信号里的边带被当作噪声丢掉。判断 alpha 是否过大,就看降噪信号包络谱里特征频率的边带还在不在。我一般先在 alpha=2000 附近跑一版,再看模态频谱的 -3dB 带宽;若边带被切,就往下调到 1000 左右。
5.3 用包络谱验证降噪后的冲击特征
与平滑趋势类信号不同,故障冲击不是看时域幅值,而是看包络谱特征频率。对降噪结果做 Hilbert 解调,再取包络的 FFT,如果特征频率及其谐波清晰可见,说明 SSA-VMD 把冲击信息保留了下来。
from scipy.signal import hilbert envelope = np.abs(hilbert(result)) env_spec = np.abs(np.fft.rfft(envelope)) freqs = np.fft.rfftfreq(len(result), 1 / fs)如果特征频点幅值明显高于邻域,就说明降噪有效。这个方法在语音降噪里同样可用,只不过关注的是共振峰而不是故障频率;如果共振峰位置被削平,就该回头调小 alpha 或增大 K。
本文还有配套的精品资源,点击获取