做信号分析这些年,频谱估计这道坎算是我踩得比较深的。早期做振动监测的时候,FFT一跑,两条频率差不到一个频点宽的谱线直接糊到一起,换窗函数、加长数据都试过,效果始终一般。后来把目光转向“现代频谱估计”,才真正摸到把叠加信号拆开的门道。所谓现代频谱估计,核心思路是抛弃“直接做傅里叶变换”的老路子,转而利用信号本身的统计模型,把频率估计当成参数估计来做。它特别适合短数据、低信噪比、频率间隔小于瑞利限的场景,也被广泛用在雷达测速、声呐定位、生物医学信号分析和机械故障诊断里。这篇博文,我想把几种主流方法的思路、原理、实操要点一次讲清楚,顺便把我在实际工程里踩过的坑都交代一下,希望能帮你少走弯路。
1. 为什么经典频谱估计会触到天花板
1.1 瑞利限究竟卡住了谁
经典频谱估计的典型代表是周期图法,核心操作就是给有限长数据加窗后做FFT。它的频谱分辨率直接取决于数据长度:频率分辨率约为 fs/N,也就是采样率除以参与变换的数据点数。这个数值有个专门的名字,叫瑞利限。两个频率分量之间的间隔只要小于这个值,在频谱上就会变成一个宽包络,怎么调窗函数都无法彻底分开。
举个例子,采样率 fs=1000Hz,取 N=256 点数据,频率分辨率大约 3.9Hz,这时候两个相距不到 4Hz 的正弦分量在周期图里必然混叠。更麻烦的是,为了抑制旁瓣泄漏,我们加大汉宁窗、布莱克曼窗,但这等于变相展宽了主瓣,进一步压缩了可分辨的频率间隔。窗函数选择本质上是在主瓣宽度和旁瓣衰减之间做折中,可无论怎么折中,物理极限摆在那里:经典方法的分辨率由数据长度决定,而数据长度在很多场景下并不由你说了算。
实际工程里,我见过太多人试图通过“加零”来提高分辨率。这里要澄清一下:FFT补零只能让频谱曲线更平滑,看起来更“精细”,它并不能突破瑞利限。补零相当于插值,没有给算法增加任何新的信息量,两个靠得很近的峰照样糊在一起,只是糊得更圆润了而已。理解了这一点,你才明白为什么需要另一套思路。
1.2 现代频谱估计的破局点
现代频谱估计并没有跟瑞利限硬碰硬,而是换了一个角度:既然信号在频域上表现为若干峰值,那我干脆不直接做傅里叶变换,而是先为信号建立一个数学模型,再通过估计模型参数来间接推出频率信息。这么做最大的好处是,频率分辨率不再直接受数据长度限制,而主要取决于信噪比、模型阶数以及参数估计算法的质量。
大致可以分为两条技术路线。一条是参数化谱估计,比如AR模型谱估计,假设信号由一个全极点线性系统激励白噪声产生,我们估算系统极点位置,极点的相角就对应频率分量。另一条是子空间方法,比如MUSIC和ESPRIT,利用信号自相关矩阵在特征分解后形成的信号子空间与噪声子空间之间的正交性来求解频率。两种路线各有千秋,但共同点都是靠线性代数和统计估计工具,把“看频谱”变成“解数学问题”。
理解了这个转变,你再看后面几个具体方法,就不会觉得它们是孤立的算法,而是同一思想在不同约束条件下的不同表现形式。
2. 四种主流现代方法的核心思路
2.1 AR模型谱估计:参数化思路的典型代表
AR模型谱估计的基本假设是:当前信号样本可以由过去 p 个样本的线性组合加上一个白噪声激励来表示,也就是全极点模型。写成公式就是:
x[n] = -a1x[n-1] - a2x[n-2] - ... - ap*x[n-p] + w[n]
这里的系数 a1...ap 是AR参数,p 是模型阶数。如果信号里有几个正弦分量,AR模型的极点就会落在对应频率位置附近,极点的相角指示频率值。估计出AR参数之后,就可以通过模型传递函数计算功率谱,谱峰位置就是我们要找的频率。
AR参数的求法至少有三条路:尤尔-沃克方程、莱文森-德宾递推和Burg算法。我实际用得最多的是Burg算法,因为它对短数据序列表现特别好,尤其是信噪比不太高的时候,谱估计的结果比直接解尤尔-沃克方程更稳定。Burg算法的核心是使前向和后向预测误差功率之和最小化,边递推边更新反射系数,最后再换成AR参数。
AR谱估计最大的优点是用很小的数据量就能得到较尖锐的谱峰,适合极短数据序列场景。但它有个天生的缺点:对阶数选取非常敏感。p 选小了,频谱过于平滑,两个靠近的频率根本分不开;p 选大了,可能出现一大堆虚假峰,谱图毛刺丛生。实际使用中,我一般用AIC准则或者MDL准则先估一个候选阶数,再结合信号物理特性人工微调。阶数这个参数,可以说是AR方法的命门。
2.2 MUSIC方法:子空间正交性的优雅应用
MUSIC方法全称是多重信号分类,它把观测信号建模为若干复正弦(或平面波)叠加噪声的形式。假设有 K 个频率分量,对 M 个阵元(或 M 次快拍构造的自相关矩阵)做特征分解,会得到 M 个特征值。其中较大的 K 个特征值对应的特征向量张成信号子空间,较小的 M-K 个特征值对应的特征向量张成噪声子空间。
关键性质来了:信号方向矢量与噪声子空间是正交的。也就是说,如果我们扫描整个频率范围,把每个候选频率对应的方向矢量投影到噪声子空间上,那么当扫描频率正好等于真实信号频率时,投影长度会趋近于零。MUSIC谱的定义就是投影长度的倒数,真实频率位置会形成尖锐的峰值。
这里要注意,MUSIC谱不是传统意义上的功率谱,它没有物理幅度概念,叫“伪谱”更准确。它的值只代表该频率点存在信号的可能性高低,不能用来还原每个频率分量的真实幅度。如果项目里必须知道各分量的幅度,还得在频率估计结束后,再用最小二乘单独拟合幅度。
MUSIC的另一个前提是必须准确知道信号个数 K。K 估少了,漏掉真实信号;K 估多了,噪声子空间被污染,伪峰会变多。工程上一般通过特征值的分布拐点、信息论准则来估计K。说句实在话,这个问题在低信噪比下并不容易,后面的常见问题章节里我会专门展开。
2.3 ESPRIT方法:免搜索的旋转不变思路
MUSIC需要对整个频带做扫描搜索,计算量不小,而且扫描网格的粗细直接影响分辨精度。ESPRIT方法则完全绕开了扫描,利用了一个漂亮的几何性质:对于均匀线阵或者等间隔采样数据,把数据分成两个结构相同的子阵,两个子阵之间存在一个固定的相位差,这个相位差正好就是从频率信息。
具体说,对两个子阵的数据分别做自相关估计,然后拼接成一个大矩阵,再做广义特征值分解。广义特征值的幅角就对应着信号的频率。整个过程不需要穷举式扫描,频率是直接“算”出来的,所以计算效率高,特别适合实时处理或需要持续跟踪频率变化的场合。
ESPRIT的代价在于对阵列结构或者数据划分方式有要求。它要求两个子阵的响应具有旋转不变性,均匀线阵天然满足,非均匀阵或者随机稀疏采样就得先做阵列校准。在时间序列的频谱估计里,常见做法就是把等间隔采样的数据切成两个时移相同的子序列来构造旋转不变性,这样等效于构造了一个“时间平移阵列”。实测下来,ESPRIT的频率估计精度通常会略高于MUSIC,尤其在快拍数有限的时候,因为它的参数估计步骤更少,传递误差的机会也少一些。
2.4 Capon方法:空域滤波的视角
Capon方法又叫最小方差无失真响应,它的出发点跟子空间方法完全不同。它设计一个空域滤波器,让信号在某个关注频率方向上的增益固定为1,同时使滤波器输出总功率最小。这等价于一个约束最优化问题,解出来之后,输出功率关于频率的表达式就是Capon谱。
这个方法相当于把阵列的“自由度”集中起来压制干扰,所以在干扰多、其他信号源强的场景下,Capon比普通波束成形和周期图法表现得更好。相比MUSIC,Capon谱的峰没那么尖锐,但它保留了一定的幅度信息,能量估计相对可信。计算上需要对自相关矩阵求逆,矩阵维数高时开销也不低,而且要求自相关矩阵满秩,否则逆矩阵不存在。实际计算时一般加一个小的对角加载项来稳定求逆。
四种方法放一起看,选型的逻辑就比较清楚了。我整理了下面这个表,方便对照:
| 方法 | 理论基础 | 分辨率 | 计算开销 | 典型场景 |
|---|---|---|---|---|
| AR/Burg | 全极点模型参数估计 | 高(受阶数影响) | 低 | 短数据、平滑谱估计 |
| Capon/MVDR | 最小方差无失真滤波 | 中高 | 中(矩阵求逆) | 强干扰下的功率估计 |
| MUSIC | 信号/噪声子空间正交 | 很高 | 高(频域扫描) | 高精度频率估计、测向 |
| ESPRIT | 子阵旋转不变性 | 很高 | 中 | 实时频率跟踪、测向 |
3. 实操复现:用仿真实例跑通整个流程
3.1 先构造一个“经典法必失败”的信号
为了把现代方法的价值体现出来,我设计了一个模拟信号:采样率1000Hz,数据长度256点,包含两个余弦分量,频率分别为200Hz和205Hz,幅度比3比1,外加高斯白噪声,信噪比约10dB。按照瑞利限估算,频率分辨率约为3.9Hz,两个分量只差5Hz,理论上勉强可分,但加上幅度差和噪声后,经典法实际会糊得厉害。
下面是生成信号的代码:
import numpy as np fs = 1000 # 采样率 N = 256 # 数据长度 t = np.arange(N) / fs f1, f2 = 200.0, 205.0 A1, A2 = 3.0, 1.0 rng = np.random.default_rng(42) x = (A1 * np.cos(2 * np.pi * f1 * t) + A2 * np.cos(2 * np.pi * f2 * t)) snr_db = 10 power_x = np.mean(x**2) noise_power = power_x / (10**(snr_db / 10)) x = x + rng.normal(0, np.sqrt(noise_power), N)这里故意把小幅度分量放在离大幅度分量很近的位置,还原了工程里最常见的麻烦:强信号旁边藏着一个弱信号。通用做法里,我会建议先把数据去均值,避免直流偏置干扰低频谱峰;幅度差异大的时候,可以考虑对数据做归一化。不过在这个例子里,主要矛盾在于频率间隔,所以保持原始幅度关系更直观。
3.2 经典法基线:先看传统手段的极限
用周期图和Welch法分别处理这个信号,作为参照系。Welch法把数据分段加窗再平均,能降低方差,但分辨能力进一步下降。
import matplotlib.pyplot as plt # 周期图 X = np.fft.fft(x * np.hanning(N)) freqs = np.fft.fftfreq(N, 1/fs) plt.plot(freqs[:N//2], 20*np.log10(np.abs(X[:N//2]))) # Welch法 from scipy.signal import welch f_w, P_w = welch(x, fs, nperseg=64) plt.plot(f_w, 10*np.log10(P_w))我这里只用了一个频点附近的观察。周期图在195Hz到210Hz之间会呈现一个不对称的宽峰,主峰明显偏向200Hz,205Hz分量只能表现为主峰旁边的小肩包,几乎无法确认是独立谱线。Welch法因为进一步缩短了有效段长,两个分量基本就完全融合成一个峰了。这算是经典方法在这个参数组合下的正常表现,不是代码问题,是原理限制。
3.3 Burg算法实现AR谱估计
在Python里可以用自带的谱估计接口,也可以手工写Burg递推。手工写有助于理解算法内部发生了什么,这里给出一个精简实现:
def burg(x, p): N = len(x) a = np.zeros(p + 1) a[0] = 1 ef = x.astype(float).copy() eb = x.astype(float).copy() rho = np.mean(np.abs(x)**2) for i in range(p): num = -2 * np.dot(ef[i+1:], np.conj(eb[i:-1])) den = np.sum(np.abs(ef[i+1:])**2) + np.sum(np.abs(eb[i:-1])**2) + 1e-12 k = num / den a_new = a.copy() for j in range(1, i+2): a_new[j] = a[j] + k * np.conj(a[i-j+1]) a = a_new ef_new = ef[i+1:] + k * eb[i:-1] eb_new = eb[i:-1] + np.conj(k) * ef[i+1:] ef[i+1:] = ef_new eb[i:-1] = eb_new rho = rho * (1 - np.abs(k)**2) return a, rho p = 8 a, rho = burg(x, p) w_ar, h = plt.freqz(np.sqrt(rho), a, worN=4096, fs=fs)阶数 p 我取了8。为什么是8而不是其他值?先用AIC准则对 p 从1到20做一轮计算,AIC最小值落在6附近,但为了给谱峰留一点余量,我在实际操作里会把 p 微调到8,同时观察谱图不出现明显虚假峰。这段代码跑出来的AR谱会在200Hz和205Hz附近各出现一个峰。由于AR模型是全极点结构,峰形比周期图尖锐得多,两个分量基本能分开,但205Hz处的峰高被压得比较低,毕竟幅度只有强信号的1/3。
3.4 MUSIC方法实操
MUSIC方法的第一步是构造自相关矩阵。在时间序列分析中,通常把数据分成若干重叠快拍,对每个快拍求自相关,再取平均。快拍数太少,自相关矩阵秩亏;快拍太多,每个快拍长度变短,分辨能力下降。这个平衡很微妙。
def corr_matrix(x, m, n_snap): # m: 矩阵维数,n_snap: 快拍数 l = len(x) R = np.zeros((m, m), dtype=complex) step = l // n_snap for i in range(n_snap): seg = x[i*step : i*step + m] R += np.outer(seg, np.conj(seg)) return R / n_snap m = 20 R = corr_matrix(x, m, 8) evals, evecs = np.linalg.eigh(R) K = 2 # 假设已知两个信号 noise_sub = evecs[:, :-K]噪声子空间取最后 m-K 个特征向量。接着建立方向矢量并扫描频率范围:
n_music = 4096 f_scan = np.linspace(150, 250, n_music) pmusic = np.zeros(n_music) for i, f in enumerate(f_scan): a_vec = np.exp(1j * 2 * np.pi * f * np.arange(m) / fs) proj = noise_sub.conj().T @ a_vec pmusic[i] = 1 / np.abs(proj) ** 2 plt.plot(f_scan, 20*np.log10(pmusic / np.max(pmusic)))MUSIC谱在200Hz和205Hz处各出现一个非常尖锐的峰,而且两峰分离清晰,幅度谱里弱信号被强信号掩盖的问题在这里完全不存在。注意MUSIC谱的两个峰高度虽然不完全一样,但不代表幅度比,它只反映方向矢量与噪声子空间的正交程度。这也是我前面强调的,MUSIC适合找频率位置,不适合恢复幅度。
3.5 ESPRIT方法实操
ESPRIT的矩阵构造稍微复杂一点。先将观测数据切成两个子序列,每个子序列长度相同,第二个子序列相对第一个整体时移1个采样点。对两个子序列分别构造自相关矩阵,然后求解广义特征值问题。
def esprit_freq(x, m, K): N = len(x) x1 = x[:N-1] x2 = x[1:] R1 = corr_matrix(x1, m, 8) R2 = corr_matrix(x2, m, 8) # 广义特征值分解 evals = np.linalg.eigvals(np.linalg.pinv(R1) @ R2) # 取最靠近单位圆的K个特征值 idx = np.argsort(np.abs(1 - np.abs(evals)))[:K] phi = np.angle(evals[idx]) freqs = np.sort(np.abs(phi * fs / (2 * np.pi))) return freqs f_est = esprit_freq(x, 20, 2) print(f_est)我跑下来的结果是200.1Hz和204.8Hz,误差大约0.1到0.2Hz。这个精度在短数据条件下已经相当不错。需要注意的点是,广义特征值分解要求 R1 可逆,所以 m 不能大于构造 R1 时使用的有效数据长度。实际操作中我一般对 R1 做对角加载,即 R1 + λI,λ取一个小值如1e-6,保证数值稳定性。
3.6 参数的选取经验
这四个方法跑下来,你会发现参数选择几乎决定了算法的成败。快拍数太少,自相关矩阵秩亏;快拍数太多,频率分辨率下降。矩阵维数 m 到底取多少?经验上取信号个数的8到20倍,不能超过单个快拍的长度。如果完全不知道信号个数,就用特征值的分布判断:特征值从大到小排列,在真实信号个数之后通常会出现一个明显下降的平台。下面的代码可以辅助判断:
evals, _ = np.linalg.eigh(R) evals = evals[::-1] # 降序 ratio = evals[:-1] / evals[1:] # 找最大比值处对应的索引,通常就是信号个数 K_est = np.argmax(ratio) + 1这个方法在信噪比正常时很好用,但低信噪比下拐点不明显,就得结合MDL或者AIC来辅助判断。用矩阵维数 m 做搜索时,扫描点数也不能太粗,我一般至少取4096个点,保证MUSIC谱的峰定位足够精细。
4. 常见问题与排查技巧实录
4.1 信号个数估计不准导致子空间划分错误
这是子空间方法最常见的翻车原因。K 估少了,噪声子空间里混入信号成分,谱峰位置偏移甚至漏检;K 估多了,信号子空间里混入噪声特征向量,MUSIC谱出现大量假峰。我在一次多振源监测中遇到过类似问题:频谱里明明只有两个主导频率,因为特征值拐点不明显,我按特征值比值估出了4个信号,结果MUSIC谱在真实信号旁边多出一排细碎尖峰。
排查思路是先看自相关矩阵的特征值分布图,如果存在明显的“陡崖”形拐点,按拐点取K即可。没有明显拐点时,用MDL准则,或者对着频谱图直接数明显峰的数量。我个人的习惯是:先用Burg方法粗跑一遍,看谱峰数量,再把这个数量作为子空间方法的K输入。前面章节提到的AIC估计、特征值比值都是辅助,人工确认最后一道关不该省。
4.2 MUSIC谱出现伪峰
伪峰的来源不止K估计错误一种。数据里的直流偏置、非平稳趋势、坏点都会在伪谱里形成虚假尖峰。这种情况下,首先要做数据预处理:去均值、去趋势、剔除异常值。如果信号中包含很强的干扰分量,功率差异太大时,自相关矩阵的动态范围会拉大,数值精度不够也会产生伪峰。
另一个隐蔽问题是空间平滑缺失。如果多个信号是相干的,比如同一个源经过多径到达阵列,信号子空间的秩会降低,MUSIC的谱峰会变钝甚至消失。工程上最简单的补救是使用前向-后向空间平滑:把子阵划分,对前向和反向共轭子阵分别求自相关再取平均,可以恢复信号子空间的秩。代价是有效孔径缩小,但相干信号场景下值得。
4.3 ESPRIT结果跳变和相位缠绕
ESPRIT特征值的相位是频率信息。如果特征值模长偏离1太多,往往是自相关矩阵构造不当或者数据噪声过大。广义特征值分布杂乱时,筛选最靠近单位圆的K个特征值这个步骤就会失灵。我遇到过的最典型的问题,是频率接近0或接近奈奎斯特频率时,相位角接近边界,轻微的噪声扰动就让相位从正跳到负,估计结果剧烈跳动。
解决方法是先对数据做带通滤波,把关注频段限制在奈奎斯特频率的中段,避开相位边界。另外可以尝试把多个时滞差构造多个广义特征值方程,对估计结果取中位数而不是平均值,能明显抑制跳变。配对问题也常出现在阵列测向场景:两个子阵分开估计时,特征向量顺序可能不一致,需要用特征向量的相关系数矩阵来匹配。时间序列的ESPRIT在单变量情况下没有配对问题,但一旦扩展到多通道同步采集,就要考虑这个。
4.4 低信噪比下的整体性能下降
现代方法在信噪比降到0dB以下时会急剧恶化,这一点跟经典法是难兄难弟。但现代法有个额外的优势:可以通过增加快拍数、累积统计量来改善。我的经验是,快拍累积对MUSIC的帮助最大,AR模型次之,ESPRIT对信噪比变化的反应相对温和。处理低信噪比信号时,先做窄带滤波,把带外噪声先压掉,再进现代法,效果提升明显。
比如说,如果目标频率大致在200Hz附近,我先用带通滤波器把频带限制在180Hz到220Hz内。这样信号周围的噪声底整体下降,等价于局部信噪比提升,特征值分布更清晰,K估计也更稳定。这个方法看似简单,但在工业数据里往往比换任何高级算法都有效。
4.5 现代法不是万能的
需要泼一盆冷水:现代频谱估计有很多前提条件。噪声必须近似白噪声,AR模型对有色噪声敏感;信号要平稳,快变信号的参数估计误差很大;模型要匹配,如果你的信号本质是宽带信号而不是若干离散谱线,硬套子空间方法只会得到一堆无意义的峰。
我遇到过把MUSIC用在宽带冲击信号上的案例,结果伪谱里全是虚假峰,最后老老实实切回Welch法看包络。正确做法是先用经典法看数据的大致形态,判断信号属于线谱型还是宽带型,再决定要不要上现代方法。现代法不是来取代经典法的,它只是在特定假设下提供更高分辨率的工具。
5. 工程选型的思考与几条压箱底的经验
5.1 从项目需求反推方法
选型的时候,我一般按这几个约束条件对号入座:
- 数据量极少(几十个点),又必须分辨出相近频率,优先AR/Burg,它的参数模型特性让短数据也能出谱峰。
- 频率必须实时跟踪,计算量敏感,但允许阵列结构约束,优先ESPRIT,免扫描的优势在实时场景里非常明显。
- 强干扰背景下还要估计功率值,Capon比MUSIC靠谱,因为它的谱输出拥有功率含义。
- 追求最高分辨率、离线处理、有大把算力,选MUSIC并把扫描点拉满。
- 信号相干、多径严重,加空间平滑后的MUSIC或ESPRIT,比AR更稳。
5.2 我踩过坑之后养成的习惯
第一,永远先画一张经典频谱图兜底。即使你非常确定要用现代方法,也先跑一次Welch或者周期图。这一步不费什么事,却能帮你确认信号大致形态、估算信号个数,还能验证预处理有没有做干净。
第二,数据结构别直接喂算法。去掉均值、去趋势、剔除坏点、带通预滤波,这几步做完,后面所有方法的稳定性都会上一个台阶。很多所谓“算法失效”的案例,根源其实是数据质量。
第三,写代码时保持矩阵条件数的敏感度。自相关矩阵求逆、特征分解都受条件数影响。看到计算结果剧烈振荡时,别急着调算法的超参数,先检查 R 的条件数,该加对角加载就加。
第四,结果别只看图。谱峰看着分开了是一回事,量化误差指标是另一回事。我习惯把估计频率跟真实频率偏差的均方根误差算出来,在同一组参数下重复跑50次蒙特卡洛,再做方法对比。否则你很难判断到底是算法好还是运气好。
5.3 可继续深入的方向
现代频谱估计这套思路不止于单通道频率估计。二维MUSIC可以直接扩展到方位角和俯仰角联合估计,ESPRIT在分布式阵列里能做高精度定位,压缩感知类的稀疏重构方法处理非均匀采样频谱也有不错的表现。如果你所在的项目涉及多个传感器同步采集,把时间序列数据组织成多通道数据矩阵时,前面讲的所有子空间方法都能平滑迁移过去。
我个人最深的体会是:频谱估计领域不缺牛逼算法,缺的是对问题本质的理解。你不清楚信号模型是否匹配、噪声是否白噪声、信源数到底有多少,任何漂亮算法都会给你一套完全不可信的答案。反过来,把这些前提搞扎实,哪怕用最简单的Burg,也能解决绝大多数工程问题。希望这篇文章能帮你在面对频谱糊成一团时,多几把顺手的工具。