简介:针对信号处理中常用功率谱估计方法的对比与实现,这份资源以单个MATLAB脚本的形式,覆盖BT法(相关函数法)、周期图法、Bartlett法、Welch法与AR模型功率谱估计,并包含周期图函数封装与BT法推导过程,适合数字信号处理学习者、科研入门者及需要快速验证算法的工程师。压缩包仅含1个.m文件,整体约1KB,结构简单,便于直接运行与修改;脚本内置示例数据和完整计算流程,可直观观察不同方法的谱线差异与方差特性。已有602人浏览学习,适合在课程实验或课题预研中作为算法模板。通过学习该脚本,可快速掌握各类方法的实现思路、参数选取原则及其适用场景,理解从自相关函数到功率谱密度的推导脉络,并借助代码对比长序列与短序列、平稳与非平稳信号下的估计效果,为后续深入分析工作奠定基础。
1. 几种常用功率谱估计法能帮你解决什么:从振动信号到脑电都能用的谱分析工具
拿到一个叫“几种常用功率谱估计法.zip”的工程包,解压后通常是几个脚本和一份说明,里面实现了周期图(periodogram)、BT(Blackman-Tukey)法、Welch法等经典谱估计方法。标题里的“periodogram函数”和“功率谱BT推导”点出了重点:直接法和基于自相关截断的间接法。功率谱估计能告诉你信号在哪些频率上能量集中,比如振动故障特征频率、心电的呼吸频率、环境噪声的主频带。适合刚接触频谱分析的学生、要做特征提取的算法工程师,以及需要快速评估信号质量的硬件调试人员。我顺着这个压缩包的常见内容,把几种方法的原理、代码、参数调整和踩坑记录讲清楚,让你拿到类似代码时能改得动、跑得通。
2. 功率谱估计的理论起点:维纳-辛钦定理与BT推导
2.1 自相关函数与功率谱的数学关系
平稳随机信号通常不是能量有限信号,所以不能直接对单个样本做傅里叶变换再取模平方来得到“能量谱”。我们需要的是功率谱密度,也就是单位频率上的平均功率。维纳-辛钦定理给出了漂亮的桥梁:对平稳随机过程,功率谱密度 P(f) 是自相关函数 r[m] 的傅里叶变换,即 P(f) = Σ_{m=-∞}^{∞} r[m] e^{-j2πfm}。这个关系是所有经典谱估计方法的基础。
实际工程中,我们只有有限长的观测数据 x[0], x[1], …, x[N-1] 和有限长的期望近似。于是问题变成:如何从这 N 点数据里估计出足够接近真值的 r[m],再通过变换得到 P(f)。
自相关估计有两种常见形式。有偏估计 r_hat[m] = (1/N) Σ_{n=0}^{N-m-1} x[n]x[n+m] 的均值是 r[m](1 - |m|/N),因此是渐近无偏的,且当 N 远大于 m 时偏差很小。无偏估计 r_hat[m] = (1/(N-m)) Σ … 则完全无偏,但延迟越大,参与平均的样本越少,方差越大。在做功率谱估计时,我基本只用有偏估计,因为后续加窗或分段平均已经能抑制方差,而有偏估计能保证自相关矩阵的正定性在一定程度上更好。
还有一个容易忽略的对称性:对实信号,r[-m] = r[m]。在代码里构造自相关序列时必须保证这一点,否则后续 FFT 出来的谱会带有明显虚部,甚至出现负功率。
2.2 periodogram函数:直接法为什么简单又为什么方差大
周期图法直接定义如下:P_per(f) = (1/N) |Σ_{n=0}^{N-1} x[n] e^{-j2πfn}|²。换句话说,把数据看成能量有限的确定性信号,先做离散时间傅里叶变换,再取模平方并除以 N。现实中用 FFT 计算,就是np.abs(np.fft.fft(x, nfft))**2 / N。scipy.signal 里封装好了的periodogram函数,除了采样率 fs 外,还提供 window、nfft、detrend、scaling 等参数。
周期图的优点是计算效率高,整段数据一次 FFT 就出结果。但它的方差非常大。从统计性质看,对于白噪声输入,周期图在某一个频点的值近似为指数分布,其标准差等于该频点的期望值。这意味着无论把 N 取得多大,周期图都不会在某个频点附近收敛,它只是让曲线上的“毛刺”越来越密集,但起伏幅度始终和谱值本身在一个量级。所以单次周期图看起来“脏”,不是数据有问题,而是估计器本身的特性。
很多人误以为多补零就能让周期图变平滑,其实补零只是增加了频域的采样点密度,让曲线看起来更细腻,并没有改变每个频点上的统计起伏。同样,加窗可以降低频谱泄漏,但不能降低方差。要降低方差,只能用后面要讲的平滑、平均或分段处理。
2.3 BT法推导过程:对自相关截断加窗的谱估计
BT法(Blackman-Tukey法)从维纳-辛钦定理出发:先估计自相关 r_hat[m],然后截取有限的一段(|m| ≤ M-1),乘以窗函数 w[m],最后做傅里叶变换,得到 P_BT(f) = Σ_{m=-(M-1)}^{M-1} w[m] r_hat[m] e^{-j2πfm}。
推导的关键是把它变换成周期图的卷积形式。把 r_hat[m] 的表达式代入,交换求和顺序,可以得到 P_BT(f) = ∫_{-1/2}^{1/2} P_per(θ) U(f-θ) dθ,其中 U(f) 是窗函数 w[m] 的频谱。这个式子说明:BT 谱本质上是周期图在频域与窗谱 U(f) 做卷积,也就是对周期图做了一次平滑。
这个推导直接解释了 BT 法的两个特性。第一,方差降低了,因为卷积把邻近频点的随机波动平均掉了;第二,分辨率变差了,因为窗谱主瓣的宽度决定了平滑的带宽。M 越小,窗越短,平滑越强,方差越低,但分辨率越差。这就是经典谱估计中的“分辨率-方差”跷跷板。
实际实现时要注意两点。一是自相关序列要构造成对称的,从 r[-M] 到 r[M]。二是窗函数也必须是对称的,并且长度和自相关序列一致。如果窗取矩形窗,旁瓣会很高,导致强峰附近出现“涟漪”;取 hann 或 Bartlett 窗则旁瓣低,平滑效果更干净。M 的选择同样关键,我一般取 N/4 或 N/8,若 M 太接近 N/2,自相关尾部方差太大,谱会出现明显抖动。
3. 在Python里复现periodogram与BT法:可运行的代码与参数设置
3.1 用scipy.signal.periodogram快速出图
先给一个最小可跑的周期图代码:
import numpy as np from scipy.signal import periodogram import matplotlib.pyplot as plt fs = 1000 # 采样率 1000 Hz N = 1024 # 数据长度 t = np.arange(N) / fs # 合成信号:50Hz + 120Hz 正弦 + 白噪声 x = (np.sin(2*np.pi*50*t) + 0.5*np.sin(2*np.pi*120*t) + 0.1*np.random.randn(N)) # 调用 periodogram 直接法 f, Pxx = periodogram(x, fs=fs, window='hann', nfft=1024, detrend='constant', scaling='density') plt.semilogy(f, Pxx) plt.xlabel('Frequency (Hz)') plt.ylabel(r'Power Spectral Density (V$^2$/Hz)') plt.grid(True, which='both') plt.show()这段代码里,periodogram内部会先对 x 做去均值和加窗,再做 FFT。window='hann'是常用的窗函数,能压低正弦谱线的旁瓣泄漏;detrend='constant'会把整个序列的均值减掉,避免零频处出现巨大的直流尖峰;nfft=1024指定 FFT 点数,如果 nfft 大于 len(x) 则自动补零;scaling='density'返回功率谱密度,单位是 V²/Hz,对正弦峰进行积分可以得到信号的均方幅值。
如果换成scaling='spectrum',返回的是各频点上的功率,单位是 V²,此时正弦峰的高度直接对应正弦幅度平方的一半。两种标度在对比不同采样率的信号时要特别小心,密度谱需要除以采样率,不是同一个量纲。
3.2 手写BT法:自相关估计、窗函数与FFT
BT 法的核心是把自相关估计出来,截断加窗,再做 FFT。下面是完整的手写实现:
def bt_spectrum(x, fs=1.0, lag=128, window='hann', nfft=4096): N = len(x) # 1. 用相关函数库计算有偏自相关估计 r_full = np.correlate(x, x, mode='full') / N # 从 full 结果的中间取正延迟部分 r[0]...r[N-1] r = r_full[N-1:] # 2. 截取到 lag 个延迟(0 到 lag) M = min(lag, N-1) r_m = r[:M+1] # 3. 构造对称自相关序列 r[-M]...r[0]...r[M] r_rev = r_m[:0:-1] # 从最右边到第 1 个(不含第 0 个) r_sym = np.concatenate((r_rev, r_m)) # 4. 构造对称窗,长度 2*M+1 if window == 'hann': w_sym = np.hanning(2*M+1) elif window == 'bartlett': w_sym = np.bartlett(2*M+1) else: w_sym = np.ones(2*M+1) r_win = r_sym * w_sym # 5. 补零后做 FFT,除以 fs 得到密度 spectrum = np.fft.fft(r_win, n=nfft) / fs f = np.fft.fftfreq(nfft, 1/fs) half = nfft // 2 return f[:half], np.real(spectrum[:half])这里有几个关键点。
第一,np.correlate(x, x, mode='full')得到的序列长度为 2N-1,索引 N-1 对应延迟 0。取r_full[N-1:]就得到 r[0], r[1], …, r[N-1]。因为信号是实信号,自相关偶对称,所以正延迟部分已经包含了全部信息。
第二,r_m[:0:-1]与r_m拼接后,得到从负延迟到正延迟的完整对称序列。比如 r_m=[r0, r1, r2],则 r_rev=[r2, r1],拼接得到 [r2, r1, r0, r1, r2],对应延迟 [-2, -1, 0, 1, 2]。
第三,窗函数长度必须与 r_sym 的长度一致,即 2M+1。hann 窗和 Bartlett 窗都满足偶对称。如果用了不对称的窗,谱会出现虚部,此时要把窗换成对称的。
第四,FFT 后取实部是合理的,因为理想情况下功率谱是实数。如果发现虚部很大,先检查 r_sym 和 w_sym 是否对称。另外除以 fs 是为了把 FFT 得到的“自相关谱”换算成功率谱密度。
3.3 关键参数:fs、nfft、window、detrend怎么调
| 参数 | periodogram | BT法 | 影响 |
|---|---|---|---|
| fs | 采样率,单位 Hz | 同左 | 决定横轴频率范围,并参与密度归一化 |
| nfft | FFT 点数,可大于 N | FFT 点数,通常大于窗长 | 只影响谱线密度,不提高分辨率 |
| window | hann/boxcar 等 | 加在自相关上的窗 | 控制泄漏、平滑度与分辨率 |
| detrend | 'constant' 或 False | 在计算自相关前先去做趋势 | 避免直流泄漏 |
| lag | 无 | 截取自相关的长度 | 越小越平滑,分辨率越差 |
fs最重要的是统一单位。如果 fs=1000,一个实际的 50Hz 正弦会出现在横轴 50 的位置;如果忘记写 fs,默认 fs=1,横轴变成归一化频率,50Hz 的正弦会出现在 0.05 处,容易误判。
nfft补零会让曲线更密,但不会让两个靠近的峰分开。补零相当于在频域做 sinc 插值,看起来峰形更完整,实际上分辨率受限于原始数据长度或窗长。想验证这一点,可以把 nfft 从 1024 改成 4096,观察正弦峰是否变瘦——它的半宽不会变化,只是曲线上的采样点变多。
detrend在工程里容易被忽略。振动传感器或生理信号往往有基线漂移,如果不去趋势,FFT 的低频段会出现非常大的能量,把真实低频成分淹没。我习惯在计算前用scipy.signal.detrend(x, type='linear')手动处理,而不只依赖 detrend='constant',因为线性漂移只有用 'linear' 才能去掉。注意 Welch 法里,detrend 是逐段做的,不能只对整段做一次。
4. 常见功率谱估计方法横向对比:周期图、BT、Welch与参数化方法
4.1 从分辨率和方差看不同方法的定位
经典谱估计的四个主力:周期图、BT法、Welch法、参数化方法。它们各自的数学性质决定了适用场景。
| 方法 | 原理 | 方差 | 分辨率 | 典型用途 |
|---|---|---|---|---|
| 周期图 | 数据直接 FFT 后取模平方 | 高,不随 N 减小 | 约 fs/N | 快速查看,数据平稳且段长 |
| BT法 | 自相关加窗后 FFT | 低,随 lag 减小而降低 | 约 fs/(2*lag) | 平滑谱线,突出主峰 |
| Welch | 分段加窗,各段周期图平均 | 低,随段数增加而降低 | 取决于段长 | 一般性谱分析首选 |
| AR/参数化 | 拟合 AR 模型,用模型系数求谱 | 低但受阶数影响 | 可高于经典法 | 短数据、高分辨率需求 |
周期图的方差问题前面已说明。BT 法通过自相关加窗实现平滑,等价于对周期图做卷积,M 越小方差越低,但主瓣越宽。Welch 法是分段平均的周期图,把 N 点数据切成若干段,每段加窗做周期图,再对所有段取平均。段数 L 越多,方差越低,但每段长度 M 越短,分辨率越差。Welch 法中重叠率可以控制,常用 50% 重叠,这样一来在段数增加的同时,不会浪费太多数据。
AR 模型法则完全不同:它先假设信号可以由一个 p 阶自回归模型 x[n] = -Σ_{k=1}^p a[k]x[n-k] + w[n] 刻画,然后估计系数 a[k] 和激励白噪声方差,再用 P_AR(f)=σ²/|1+Σ_{k=1}^p a[k]e^{-j2πfk}|² 计算谱。AR 法在数据短且频率接近时有优势,因为它的分辨率不受窗长限制,而受模型阶数和信噪比影响。但阶数选不好会产生伪峰或谱线分裂。
4.2 用合成信号做对比的脚本演示
下面这段代码对比了周期图和 Welch 法对相近频率的分辨能力。生成 50Hz 和 52Hz 两个正弦,数据长度 0.5 秒,采样率 200Hz,N=100 点。理论频率间隔 2Hz,而周期图的分辨率约 fs/N=2Hz,刚好到临界。
import numpy as np from scipy.signal import periodogram, welch fs = 200 N = 100 t = np.arange(N) / fs x = np.sin(2*np.pi*50*t) + np.sin(2*np.pi*52*t) f1, P1 = periodogram(x, fs=fs, window='boxcar', nfft=1024) f2, P2 = welch(x, fs=fs, window='hann', nperseg=50, noverlap=25, nfft=1024) # P1 在 50 和 52 附近会有两个肩或一个宽峰 # P2 由于每段只有 50 点,分辨率 4Hz,两个峰基本融合运行后会发现,周期图补零到 1024 点后,谱线很密,但 50 和 52 的位置只是让主峰两侧出现凹凸,未必是两个清晰峰;Welch 因为 nperseg=50,段长为 50 点,分辨率只有 4Hz,两个峰彻底合并成一个宽峰。这说明靠补零和减小段长都换不来真正的分辨率。想分这两个峰,唯一办法是把采样时长加长到至少 0.5 秒以上。
再看一个场景:信号是 80Hz 正弦加很大的宽带噪声。周期图会看到大量毛刺,主峰时隐时现;用 Welch 分段平均后,噪声被平均掉,80Hz 尖峰变得稳定。这展示了 Welch 在低信噪比下的优势。
4.3 选型建议:什么场景用什么方法
工程上我给出的默认答案是:数据足够长(几千点以上),直接用 Welch,nperseg取 256 或 512 的幂,重叠率 50%,窗用 hann。这样既能控制方差,又能保证不错的频率分辨率。
数据很短(例如只有几十到一百点)时,Welch 分段后每段太短,分辨率严重恶化。这时我会上周期图加补零,虽然方差大,但能看到大概的主峰位置。想进一步追求分辨率,可以用 AR 模型,但要花时间定阶。定阶方法常用 AIC 或 FPE 准则;我习惯手动扫一遍阶数 4 到 30,观察主峰频率是否稳定,如果出现多个尖峰在阶数变化时乱跳,说明模型阶数可能过高或信噪比太低。
BT 法适合对谱形要求平滑的场景。比如需要从谱中提取主峰频率,而不关心细小起伏,用 BT 法把 lag 调到 N/8,会得到一条干净曲线。计算量也比 Welch 小得多,适合嵌入式平台。
5. 功率谱估计的常见坑与排查:从zip包解压到频谱泄漏的实战记录
5.1 zip伪加密导致解压失败
现象:下载的“几种常用功率谱估计法.zip”解压时弹出需要密码,但文件明明没有加密。
原因:zip伪加密,即压缩包文件头中的“加密标志位”被错误置为 1,实际数据并未加密。这个现象常见于某些论坛附件或自动打包工具。
解决:先尝试用 7-Zip 解压,它能忽略伪加密标志直接读取。如果 7-Zip 也不行,用十六进制编辑器打开 zip,定位到压缩文件头中通用位标记字段,把第 0 位从 1 改为 0,保存后再解压。改之前备份一份原文件。这是文件工具问题,解开后不影响后续代码。
5.2 频率分辨率不达标:补零和真实窗长的区别
现象:periodogram 里设置 nfft=8192,出来曲线非常细腻,但 49Hz 和 50Hz 两个峰依然叠在一起。
原因:补零只是增加 FFT 的网格密度,没有增加有效数据长度,也没有改变窗函数的真实主瓣宽度。分辨两个相近频率的关键是数据长度(或 Welch 的 nperseg),而不是 nfft。
解决:提升分辨率必须增加真实观测时间。如果数据已经固定,只能换 AR 等方法;或者接受峰融合的事实,不要把目标频率设得太近。补零的主要作用是便于 FFT 的高效实现,以及让峰形更光滑,它不能凭空带来真实分辨率。
5.3 周期图方差大:分段平均为什么有效
现象:同一信号重复采集几次,用 periodogram 得到的谱形差异很大,峰值位置和幅度都在跳。
原因:周期图是单次样本的估计,每个频点方差与谱值同量级,且不随 N 增大而收敛。
解决:用 Welch 分段平均,把 N 点数据分成多段,每段做周期图再平均。方差大约随段数增加而下降,但每段长度变短,分辨率降低。常见做法是重叠 50%,这样段数增加时数据利用率高。如果数据本身是非平稳的,分段平均会导致谱被“涂抹”,这时候要先检查平稳性,而不是盲目平均。
5.4 自相关函数估计偏置:BT法谱出现负值怎么办
现象:手写 BT 法得到 P_BT 在某些频点是负数,画 log 谱时这些点变成缺口。
原因:有偏自相关估计的尾部方差大,加上矩形窗谱的负旁瓣,卷积后会出现负功率。负值的绝对幅度通常很小,但会破坏对数显示。
解决:首先换用 hann 或 Bartlett 窗,减少窗谱的负旁瓣;其次适当增大 lag,让自相关截取范围更接近周期图,降低平滑引起的负值;还要确认窗对称、自相关构造正确。最后如果个别点仍是负值,可以做np.maximum(P, 0)钳位,但这只是显示兜底,真正要改的是参数选择。
5.5 detrend陷阱:直流分量没去干净导致零频尖峰
现象:谱图低频端出现一个巨大的尖峰,其他频率成分完全被淹没,放大后也看不到。
原因:信号均值不为零,零频能量极高,加上窗函数旁瓣泄漏,把邻近频点都盖住了。如果信号还有线性趋势,低频泄漏会更严重。
解决:在用 periodogram 或计算自相关之前,先调用scipy.signal.detrend(x, type='constant')或type='linear'。注意 Welch 法的 detrend 参数是逐段处理的,如果你对整个序列去了一次趋势,段间仍可能有均值变化,最好在 welch 调用里显式设置 detrend='constant'。这个细节很多人踩过:手动去趋势和函数内部分段去趋势并不能相互替代。
6. 把功率谱估计用得更稳:两个交叉验证技巧和一组常用模板
6.1 用已知理论谱校准算法
拿到一个新的谱估计函数,我的第一件事是用已知理论谱校准。生成一个 AR(1) 过程 x[n] = 0.9 x[n-1] + w[n],其理论功率谱为 P(f) = σ² / |1 - 0.9 e^{-j2πf}|²。把估计谱和理论谱画在同一张图上,如果主峰位置对但幅度整体偏低,通常是归一化问题;如果高频处翘起,多半是 fs 没有传对。这个习惯能帮我快速排除八成实现错误。
6.2 用分段稳定性判断结果可信度
对同一段数据,用 Welch 做两个不同 nperseg 的估计,比如 256 和 512。如果主要峰的位置在两种设置下都稳定,说明结果是可信的;如果峰的个数或位置变化很大,说明实际谱中可能没有这个峰,或者数据非平稳、窗长覆盖率不足。这个交叉验证不需要额外数据处理,只是多跑一次,但能在汇报结果前发现很多问题。
6.3 一个可复用的分析模板
def safe_spectrum(x, fs, method='welch', nperseg=256, lag=128): from scipy.signal import periodogram, welch, detrend x = detrend(x, type='constant') if method == 'periodogram': return periodogram(x, fs=fs, window='hann', nfft=2048) elif method == 'welch': return welch(x, fs=fs, window='hann', nperseg=nperseg, noverlap=nperseg//2, nfft=2048) elif method == 'bt': return bt_spectrum(x, fs=fs, lag=lag, window='hann') else: raise ValueError('unknown method')这个模板把去趋势前置,保证所有方法输入的是同一份干净信号。使用时先用 welch 看整体,再用 periodogram 检查细节,必要时用 bt 平滑。我早期做振动分析时,直接对长序列调 periodogram,看到一堆毛刺还以为是轴承故障特征,后来用 Welch 一平均才发现大部分是随机噪声。从那以后我的默认流程永远是:先去趋势、再分段平均、最后交叉验证。希望这些方法能帮你少走弯路,也希望你在自己的信号上跑通后,能对每一条谱线都有底气解释它的来源。希望帮到你。
本文还有配套的精品资源,点击获取