做过几年现场测试的人大概都遇到过这种场面:两路传感器同步采回来的数据,各自做FFT,幅值谱都挺像回事,峰值也能对上,可一旦被问"这两路信号到底谁先谁后、是不是同一个振源传过来的、中间那段传递路径的延迟是多少",单通道谱就彻底哑火了。单通道FFT本质上是把每路信号拆成自己的频率成分清单,它不携带任何"两路之间"的信息。互功率谱密度(CPSD,Cross Power Spectral Density)就是专门补这块空白的工具:它在频域里同时保留幅值和相位关系,配合相干分析还能告诉你"这个频点上算出来的结果到底可不可信"。幅值告诉你两个信号的频率成分有多"同步",相位告诉你它们之间的时间错位,相干函数则像一块仪表盘,标出哪些频段的结论是有统计支撑的、哪些纯粹是噪声凑出来的数。
这篇内容适合三类人看:一是刚接触振动、声学、控制系统辨识,被要求"做一下互谱"但不知道参数怎么定的同学;二是已经在用scipy.signal.csd但只会看幅值曲线、相位图一出来就头大的开发者;三是想把两路信号之间关系讲清楚、需要一套可复现流程的从业者。下面的内容从定义式里的每个符号讲起,一路走到能直接跑通的Python代码,中间会把分段长度、重叠率、去趋势这些"看起来随便填"的参数为什么这么填说透,也会把我自己踩过的相位缠绕、传感器极性反接、相干虚高这几类坑摊开讲。
1. 单通道FFT看不见的那部分信息
1.1 自谱只描述"自己跟自己"的关系
先把概念边界划清楚。一路信号做FFT得到复频谱 $X(f)$,取模平方再按段平均,得到的是自功率谱密度(Auto PSD,$P_{xx}(f)$)。它的物理含义很直白:单位频率带宽内,这路信号携带了多少功率。自谱是一个实数、非负的量,单调地告诉你"这个频率上有多少能量",仅此而已。
自谱丢掉了什么?丢掉了相位。而在很多工程问题里,相位才是主角。举个具体的:一台设备上两个测点,一个装在轴承座,一个装在远离激振源的机脚。如果只看自谱,两边都在 120 Hz 附近有峰,你无法判断是振源从轴承座一路传到机脚(存在传播时延),还是两台独立的设备各自在 120 Hz 上振动。前者意味着你要去查结构传递路径,后者意味着你要去查另一台设备——结论方向完全相反。
提示:自谱的模平方操作是不可逆的,相位信息在被平方的那一刻就没了。想找回两路之间的相位关系,唯一的办法是回到复频谱层面做共轭相乘,也就是 CPSD。
1.2 两路信号之间关系的三个核心问句
一旦把问题限定在"两路信号之间",需要回答的其实就三件事,而这三件事恰好对应 CPSD 能给出的三样东西。
第一件是同源性:这两路信号里,某个频段的能量是不是来自同一个源头。对应的是幅值 $|P_{xy}(f)|$——两个复频谱在频域做相关,同源成分会因为相位锁定而在平均过程中互相增强,非同源成分因为相位随机而互相抵消。
第二件是时序关系:如果同源,那么信号在到达两个测点之间有先后。对应的是相位 $\angle P_{xy}(f)$——在纯时延的情况下,相位随频率呈严格的线性关系,斜率就是时延量。
第三件是可信度:上面那两个数,在这个频点上到底靠不靠得住。对应的是相干函数$\gamma^2(f)$,取值 0 到 1,它衡量的是"这一路的输出里有多少比例能被那一路线性解释"。
这三样东西是一套组合拳,单独看任何一个都容易得出错误结论。我见过不少报告只贴一条幅值曲线就下判断,结果在低相干频段上把噪声当成有效信号,后面排查方向直接跑偏。
1.3 为什么是"互"而不是"偏"
有人会问,既然要看两路关系,为什么不直接做偏相干、偏相干分析或者多输入模型?这里需要说明一点:CPSD 是双变量、线性、二阶统计量意义上的关系刻画。它假设两个信号之间是线性时不变的联系。对于单输入单输出的传递路径辨识,CPSD 配合自谱就能得到频响函数的估计 $H(f) = P_{xy}(f) / P_{xx}(f)$,这是最经典、最稳妥的路径。
多输入场景下确实需要偏相干来剥离输入之间的相关性,但那是建立在两两 CPSD 矩阵之上的二次加工。换句话说,CPSD 矩阵是所有这些高级分析的原材料。把原材料算对、参数定好、相干检查过,后面的工作才有意义。直接从偏相干开始讲,反而容易让基础不牢的人把错误归因到算法上。
所以我的建议是:不管后面要做什么,先把两两之间的 CPSD、自谱、相干这三样算准,画出来看一遍,确认相干在关键频段是高的,再去谈更复杂的模型。这一步省不得。
2. 定义式拆解:共轭、期望与相位基准
2.1 从互相关函数到频域的桥
CPSD 的严格定义来自维纳-辛钦定理在双变量情形的推广。两个广义平稳随机过程 $x(t)$ 和 $y(t)$,它们的互相关函数是
$$R_{xy}(\tau) = E[x(t),y(t+\tau)]$$
对这个互相关函数做傅里叶变换,得到的就是互功率谱密度:
$$P_{xy}(f) = \int_{-\infty}^{+\infty} R_{xy}(\tau), e^{-j2\pi f\tau}, d\tau$$
这个定义本身是干净的,但落到实际算的时候没人真的去积这个无穷积分。工程上采用的是它的频域等价形式:
$$P_{xy}(f) = E\left[ X^*(f), Y(f) \right]$$
其中 $X(f)$、$Y(f)$ 分别是两路信号的傅里叶变换,$*$ 表示复共轭,$E[\cdot]$ 表示系综平均。这个式子是所有实现的基础,逐符号拆开看:
- $X^*(f)$ 是 $x$ 的频谱取共轭。共轭的作用是把 $x$ 的相位取反,这样相乘之后留下来的相位差是 $\phi_y - \phi_x$,即以 $x$ 为参考的 $y$ 的相对相位。
- $Y(f)$ 不加共轭,代表"被比较"的那一路。
- $E[\cdot]$ 是关键。单次快拍的 $X^*Y$ 是一个随机量,方差极大,只有对多个独立快拍取平均,非同源成分才会被压下去,得到稳定的估计。
这里有个容易搞混的点:共轭加在哪一路,决定了相位的符号约定。如果写成 $E[Y^*(f) X(f)]$,得到的相位是 $\phi_x - \phi_y$,符号整体反过来。很多开源库和商业软件在这一点上并不统一,跨工具对比曲线时如果发现相位差了个负号,先别怀疑数据,去翻文档确认共轭位置。我自己就吃过这个亏:同一批数据在两个工具里算出的时延一正一负,查了两天才发现是约定差异,数据本身没问题。
2.2 期望项在有限数据下怎么落地
理论上 $E[\cdot]$ 是系综平均,实际只有一段有限长的记录。这就带来了 CPSD 估计的核心矛盾:方差与分辨率不可兼得。
最朴素的做法叫周期图法:整段数据只做一次 FFT,一次 $X^*Y$ 就算完。问题是单个周期图的方差极大,估计值的标准差和它本身的量级相当,也就是说你算出来的谱在真值附近剧烈抖动,这种抖动不会随着采样率提高而消失,只会随着记录长度增加缓慢改善。这在工程上基本不可用。
改进路线有三条,思路各不相同:
第一条是分段平均(Welch 法的核心):把长记录切成若干段,每段各自做 FFT 得到 $X_i^* Y_i$,然后对所有段求平均。段数越多,方差越小,方差大约按 $1/K$ 下降($K$ 为段数)。代价是每段变短,频率分辨率 $\Delta f = f_s / N_{seg}$ 变粗。
第二条是频域平滑:不分段,整段做一次 FFT,然后在频率轴上用一个滑动窗对相邻频点做加权平均。这种做法保留了细密的频率栅格,但相邻频点被强制相关,等效分辨率同样下降。
第三条是多窗谱估计(如 Slepian 序列),用一组正交窗分别做 FFT 再加权平均,能在方差和泄漏之间取得比单窗更好的平衡,代价是实现复杂、计算量大。
日常工程里,Welch 分段平均是默认选项,scipy.signal.csd走的就是这条路。原因很实际:它对非平稳性有一定容忍度(每段内近似平稳就行),参数直观(段长和重叠率),实现成熟。下面所有讨论都基于这个方法。
2.3 三种估计方法的关键参数对照
为了把取舍说清楚,列个表对比一下。这里的数值是常见工程配置,不是硬性规定,具体要结合你的采样率和关心的频段来定。
| 方法 | 关键参数 | 频率分辨率 | 方差水平 | 适用场景 |
|---|---|---|---|---|
| 单周期图 | 无 | $f_s/N$(最细) | 极大,不可用 | 仅作教学演示 |
| Welch 分段平均 | 段长 $N_{seg}$、重叠率 | $f_s/N_{seg}$ | 随段数 $K$ 按 $1/K$ 降 | 绝大多数工程场景 |
| 频域平滑 | 平滑窗宽 $M$ | $\approx M \cdot f_s/N$ | 随 $M$ 下降 | 记录短、不想丢频率栅格 |
| 多窗谱 | 窗数、时宽积 | 介于两者之间 | 优于单窗 | 短记录、强非平稳 |
有一点必须强调:频率分辨率不是采样率决定的,是段长决定的。很多人以为采样率 10 kHz 就能分辨 1 Hz 的差异,实际上如果段长只有 1024 点,分辨率就是 9.77 Hz,两个相隔 5 Hz 的成分根本分不开。这个认知偏差在现场排查里非常常见,表现为"明明是两个峰,谱上只有一个胖峰"。
3. 用Python把两路信号跑通CPSD
3.1 先造一个有标准答案的合成信号
在真实数据上调试算法是很痛苦的事,因为你不知道正确答案。我的习惯是先构造一个解析上完全已知的合成信号,把整条链路跑通、验证数值对得上,再去处理真实数据。这样一旦出问题,能立刻判断是代码错了还是数据本身就这样。
合成信号的设计要点:包含几个明确的单频成分(方便检查幅值峰位置)、一个已知的时延(方便检查相位斜率)、一路独立噪声(方便检查相干的下沉)。下面这段代码可以直接复制运行。
import numpy as np from scipy import signal import matplotlib.pyplot as plt fs = 2000.0 # 采样率 2 kHz T = 20.0 # 记录长度 20 s t = np.arange(0, T, 1.0/fs) # 源信号:三个单频成分 f1, f2, f3 = 80.0, 230.0, 470.0 src = (1.0*np.sin(2*np.pi*f1*t) + 0.6*np.sin(2*np.pi*f2*t + 0.7) + 0.3*np.sin(2*np.pi*f3*t + 1.9)) # 通道x:源 + 噪声 # 通道y:源经过 4 ms 纯时延 + 独立噪声 delay = 0.004 lag_samples = int(round(delay * fs)) # 8 个采样点 src_delayed = np.concatenate([np.zeros(lag_samples), src[:-lag_samples]]) rng = np.random.default_rng(20240501) x = src + 0.50 * rng.standard_normal(t.size) y = 0.8 * src_delayed + 0.80 * rng.standard_normal(t.size)这里故意让 $y$ 的源成分幅度是 0.8 倍、噪声更重,模拟真实场景里测点离源更远、信噪比更差的情况。时延取了 8 个采样点,正好是整数,方便先用互相关粗验,再和 CPSD 相位斜率互相印证。
注意:真实数据里时延一般不是整数个采样点,
np.interp或频域相移可以做亚采样点延迟。合成信号阶段用整数点是为了先排除插值误差的干扰,把变量控制住。
3.2scipy.signal.csd的每个参数到底在管什么
核心调用就一行,但每个参数都值得说清楚,因为默认值不一定适合你的场景。
nper = 2048 nover = nper // 2 # 50% 重叠 win = 'hann' detr = 'constant' f, Pxx = signal.welch(x, fs, window=win, nperseg=nper, noverlap=nover, detrend=detr, scaling='density') f, Pyy = signal.welch(y, fs, window=win, nperseg=nper, noverlap=nover, detrend=detr, scaling='density') f, Pxy = signal.csd(x, y, fs, window=win, nperseg=nper, noverlap=nover, detrend=detr, scaling='density')nperseg:每段点数,直接决定频率分辨率。这里 2048 点、采样率 2000 Hz,分辨率是 0.977 Hz,能干净地分开 230 Hz 和 470 Hz。如果关心的是 1 Hz 量级的窄带特征,段长得拉到上万点,那就得先确认记录长度够不够切出足够多的段。noverlap:相邻段的重叠点数。50% 是通用起点,理由在第 6 节展开。重叠不改变频率分辨率,只影响段数和方差。window:窗函数。Hann 窗是振动噪声类信号的稳妥选择,主瓣宽度适中、旁瓣衰减 -31 dB 左右,能压住强成分对邻近弱成分的泄漏。如果信号里有明显的冲击成分,改用'flattop'保幅值精度,但频率分辨会变差。detrend:去趋势方式。'constant'只去均值,'linear'去掉线性趋势。有传感器零点漂移时必须用'linear',否则低频段会被一个巨大的直流斜坡污染,CPSD 在低频的信噪比会崩掉。scaling:'density'给出功率谱密度(单位 量纲²/Hz),'spectrum'给出功率谱(单位 量纲²)。做 CPSD 和后续传递函数估计时用'density'更自然,因为频响函数的量纲才对得上。
还有一个隐含约束:调用welch和csd时,三者的nperseg、noverlap、window、detrend必须完全一致,否则算出来的相干函数会在数学上不成立,可能大于 1。这是个典型的静默错误——代码不报错,图也能画,但数值是错的。
3.3 幅值、相位、相干一次算齐
有了 $P_{xy}$、$P_{xx}$、$P_{yy}$,剩下三样东西都是直接派生:
mag = np.abs(Pxy) # 互谱幅值 phase = np.angle(Pxy) # 互谱相位,弧度 coh = np.abs(Pxy)**2 / (Pxx * Pyy) # 幅值平方相干 # 或者直接用现成函数,省得自己写 f2_, coh2 = signal.coherence(x, y, fs, window=win, nperseg=nper, noverlap=nover, detrend=detr)signal.coherence内部就是按上式算的,两种写法结果应当一致(浮点误差范围内),我一般会两个都跑一遍做交叉验证——如果对不上,说明某处参数没对齐。
相干函数还有个细节值得留意:它本质上是归一化后的互谱模平方,所以理论上限是 1。但在实际计算里,如果你把welch和csd的参数搞混了,或者自己对 $P_{xy}$ 做了插值、重采样,相干就可能出现大于 1 的数值。看到相干超过 1,不用怀疑人生,直接去查参数一致性。
3.4 把结果画成能读懂的三联图
单独画一条曲线没什么信息量,我的习惯是幅值、相位、相干三张图上下对齐、共用同一根频率轴,这样能一眼看出"这个峰对应的相干是多少、相位在这个频段是不是线性的"。
fig, axes = plt.subplots(3, 1, figsize=(10, 9), sharex=True) axes[0].semilogy(f, mag, lw=1.2) axes[0].set_ylabel('|Pxy| (unit^2/Hz)') axes[0].set_title('Cross Power Spectral Density') axes[0].grid(alpha=0.3) axes[1].plot(f, np.rad2deg(phase), lw=0.9) axes[1].set_ylabel('Phase (deg)') axes[1].set_ylim(-200, 200) axes[1].grid(alpha=0.3) axes[2].plot(f, coh, lw=1.2, color='tab:green') axes[2].axhline(0.8, ls='--', lw=0.9, color='gray') axes[2].set_ylabel('Coherence') axes[2].set_xlabel('Frequency (Hz)') axes[2].set_ylim(0, 1.05) axes[2].grid(alpha=0.3) plt.xlim(0, 600) plt.tight_layout()跑完这段,你应该能在 80、230、470 Hz 三个位置看到幅值峰,相干在同样位置接近 1,其余频段相干掉到 0.05 附近(这个下限和段数有关,下面细说)。相位曲线在有效频段内应该是分段折线——因为相位在 $[-\pi, \pi]$ 之间循环,超过就跳。这个"看起来乱七八糟"的锯齿,正是第 4 节要处理的对象。
4. 幅值与相位读图:几个反直觉的细节
4.1 幅值的量纲和"看起来对不对"
$|P_{xy}|$ 的量纲是两个信号量纲的乘积除以赫兹。传感器都是加速度时是 $(m/s^2)^2/Hz$,一个速度、一个加速度时是 $(m/s)(m/s^2)/Hz$。这个量纲本身没有特别直观的物理意义,它不像自谱那样能直接解释成"功率"。
所以不要拿互谱幅值的绝对数值去和自谱比大小,容易得出错误结论。真正有意义的用法有两种:
第一种是看形状:峰值出现在哪些频率、峰宽是多少、有没有边带。这部分和自谱的峰形应当高度一致,如果互谱上出现了自谱上没有的峰,那基本可以判定是两路噪声在这个频点上偶然相关,去查相干就能确认。
第二种是归一化之后比较:把 $|P_{xy}|^2 / (P_{xx} P_{yy})$ 算出来,就是相干。所以互谱幅值的信息其实已经完整地编码进相干里了,单独看幅值的边际价值有限。
真正用得最多的是传递函数估计:$H(f) = P_{xy}(f)/P_{xx}(f)$。这个量是有明确物理意义的(频响函数),单位是输出/输入。注意分子分母的顺序——如果写成 $P_{yx}/P_{xx}$,得到的是反过来的传函,方向搞反了整条路径分析都是错的。
4.2 相位在低相干频段没有讨论价值
这是我认为最需要强调、也最容易被忽略的一点。相位是一个有方向的量,在 $[-\pi, \pi]$ 里循环,噪声在两个随机相位上相乘,算出来的相位差在整圈上均匀分布。也就是说,在相干接近 0 的频段,相位曲线看起来是满幅随机跳动的,它不携带任何信息。
问题在于,这张图照样能画出来,照样好看,如果不看相干图,很容易从中"看出"某种规律。我见过一份报告,作者在相干只有 0.1 的频段上对相位做线性拟合,硬推出一个时延值,还据此下了"存在传播路径"的结论。这种结论完全站不住脚。
我的做法是:画相位图的时候,把相干低于阈值的频段直接涂灰或者干脆不画。阈值取多少?经验值是 0.8 以上算高相干,0.5 到 0.8 是勉强可用、需要谨慎,0.5 以下基本不可用。当然这跟段数有关,段数少的时候相干估计本身偏差就大,后面的表里会展开。
4.3 相位解缠绕与时延反推
如果两路信号之间确实是纯时延关系 $y(t) = x(t - \tau)$,那么它们的频谱满足 $Y(f) = X(f) e^{-j2\pi f \tau}$,于是
$$P_{xy}(f) = E[X^*(f) Y(f)] = P_{xx}(f), e^{-j2\pi f\tau}$$
相位就是 $\angle P_{xy}(f) = -2\pi f \tau$,对频率是严格线性的,斜率是 $-2\pi\tau$,过原点。这就是从相位斜率反推时延的全部原理。
实现上要处理两件事:解缠绕和频段筛选。
phase_unwrap = np.unwrap(np.angle(Pxy)) # 只在相干高、且避开直流附近和奈奎斯特附近的频段拟合 mask = (coh > 0.85) & (f > 20) & (f < 800) omega = 2 * np.pi * f[mask] p_fit = phase_unwrap[mask] A = np.vstack([omega, np.ones_like(omega)]).T (slope, intercept), *_ = np.linalg.lstsq(A, p_fit, rcond=None) tau_est = -slope print(f"估计时延: {tau_est*1e3:.3f} ms (真值 4.000 ms)") print(f"拟合截距: {intercept:.4f} rad")np.unwrap的作用是把 $-\pi$ 到 $\pi$ 之间的跳跃补回来,还原成连续曲线。但它对噪声很敏感——如果在低相干频段出现了虚假跳变,unwrap会把这个错误一路传播下去,后面所有频点的相位都平移了 $2\pi$。所以更稳的顺序是:先按相干筛频段,再在筛选后的连续区间上解缠绕,或者干脆在拟合前把明显离群的点剔掉。
至于截距,理论上纯时延应该接近 0。如果拟合出来截距明显不为零(比如超过 0.5 rad),通常意味着两路之间存在额外的相位偏移,可能是传感器极性不同、也可能是滤波器群延迟不一致。这个量本身也是有用的诊断信息,别直接扔掉。
4.4 极性和符号约定:一个高频踩坑点
相位曲线在有效频段上如果整体平移了 180°,最常见的物理原因是两个传感器极性接反了,或者其中一路的电荷放大器设置了反相。这个现象在加速度计现场布置里非常常见,尤其是临时加装测点时。
判断方法很简单:正常情况下的相位斜率应该和互相关算出来的时延符号一致。如果相位整体偏 180° 而斜率形态没变,那就是极性反了,把它当成"信号反相"处理即可——在计算前给其中一路乘个 -1,或者接受这个 180° 并在解读时扣除。
提示:极性反接和真实的反相振动是两个完全不同的问题,前者是测量链路问题,后者是物理现象。区分方法是多看几个测点:如果只有这一个测点反相,多半是接线;如果一组测点都反相,那可能是物理上的反相模态。
5. 相干函数:判断结论能不能落地
5.1 相干不是"相关系数",它的下限和段数挂钩
相干函数定义为
$$\gamma^2_{xy}(f) = \frac{|P_{xy}(f)|^2}{P_{xx}(f), P_{yy}(f)}$$
取值 0 到 1。1 表示 $y$ 在该频点上完全可以由 $x$ 线性预测,0 表示完全无关。听起来像个相关系数,但它的统计性质完全不同,最容易误解的一点是:即使两路信号完全独立,算出来的相干也不会是 0。
原因在于估计的不确定性。用 $K$ 段数据平均,在真实相干为 0 的频点上,估计出的相干服从一个偏向正值的分布,其期望大约在 $1/K$ 量级。也就是说:
- 段数 $K = 10$,独立信号上的相干底噪约 0.10
- 段数 $K = 50$,底噪约 0.02
- 段数 $K = 4$,底噪高达 0.25
这个数值在实操中至关重要。如果你只有 4 段数据,看到相干 0.3,那和噪声底噪没区别,不能当有效信号。反过来,段数多了以后相干曲线整体会"变干净",不是信号变好了,是估计变稳了。
很多文献给的经验判据是相干大于 0.8 或 0.9 才算有效,这个阈值隐含了段数足够多的前提。段数少于 8 的时候,我一般不会用相干做硬性筛选,而是先把段数加上去再说。
5.2 相干掉下去的四种典型成因
相干低了,不代表"两路信号无关",它可能来自四种完全不同的机制,排查方向各不相同。
第一种是真的无关。两路信号来自不同的物理源,没有共同的激励。这种情况相干在所有频段都接近底噪,很好识别。
第二种是噪声污染。信号本身相关,但其中一路或两路叠加了较强的独立噪声。特征是:相干在有信号的频段高、在其他频段低,形成一个"峰谷结构"。这种情况下提高信噪比(换更好的传感器、缩短线缆、加屏蔽)能直接改善相干。公式上也能看出来:如果 $y = h \cdot x + n$,那么相干约等于 $|h|^2 P_{xx} / (|h|^2 P_{xx} + P_{nn})$,噪声功率 $P_{nn}$ 越大相干越低。
第三种是非线性。两路信号之间确实有因果关系,但关系不是线性的,比如存在削波、死区、间隙。相干函数只度量线性相关,非线性成分会被算成"不相关",表现为相干中等偏低但又不是底噪。这种情况需要用互双谱、或先做线性化处理。
第四种是时变或非平稳。系统参数在记录期间发生了变化,或者两路信号的时间对齐在漂移。分段平均的前提是每段统计特性一致,一旦不满足,不同段之间的相位关系互相抵消,相干就被拉低。特征是增加段数反而让相干下降——如果观察到这个现象,基本可以锁定是这个问题。
5.3 用相干做频段筛选与加权
相干最实用的用途不是"看一眼判断好坏",而是作为筛选器嵌进计算流程里。三个具体用法:
用法一:给传函估计加权。标准的 $H_1$ 估计 $H = P_{xy}/P_{xx}$ 在低相干频段偏差很大。一个常见的改进是只在相干高于阈值的频段输出结果,其余频段标记为无效。做频响函数拟合(比如模态参数识别)时,这一步能显著减少伪模态。
用法二:指导时延估计的频段选择。前面 4.3 节的代码已经用了——只在相干高的频段做相位斜率拟合。这个操作对结果的影响非常大,我在一批实测数据上对比过:不加筛选直接全频段拟合,时延估计误差能到 30% 以上;加上相干筛选后误差降到 3% 以内。
用法三:多测点之间的传递路径排序。有一组测点时,可以两两算相干,构建一个相干矩阵,相干高的那些对说明能量在这个路径上流动更直接。这个方法粗糙但快,适合在正式建模之前做一轮快速的路径筛查。
下面这个表把常见现象和对应排查方向归了一下,实际排查时可以直接对照:
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 全频段相干接近 $1/K$ | 两路确实无关 | 先确认测点位置是否合理,检查数据通道是否接错 |
| 有信号频段高、其余低 | 独立噪声污染 | 提高信噪比、检查接地与屏蔽、缩短信号线 |
| 相干中等(0.3~0.6)且平坦 | 存在非线性或时变 | 检查是否有削波、间隙、参数漂移;缩短分析窗 |
| 段数增加后相干反而下降 | 非平稳 | 缩短记录长度、分段做时频分析 |
| 相干大于 1 | 参数不一致 | 核对 welch 与 csd 的窗、段长、重叠是否完全一致 |
6. 参数调优与实战里的那些坑
6.1 段长怎么定:从需求倒推而不是从经验拍
段长nperseg是这套流程里最关键的参数,它同时决定了频率分辨率和段数(进而决定相干估计的稳定性)。我的定法是从你关心的最小频率间隔倒推:
$$\Delta f = \frac{f_s}{N_{seg}} \le \Delta f_{target}$$
比如要分辨 230 Hz 和 240 Hz 两个成分,$\Delta f_{target}$ 取 5 Hz,采样率 2000 Hz,则 $N_{seg} \ge 400$。考虑到窗函数会加宽主瓣(Hann 窗的主瓣半宽约 $1.5 \Delta f$),实际要让 $N_{seg}$ 再大 1.5 到 2 倍,取 1024 或 2048 更稳。
定完段长,再算段数:记录长度 $T = 20$ s、采样率 2000 Hz 共 40000 点,段长 2048、重叠 50%,则段数约
$$K \approx \frac{N - N_{seg}}{N_{seg} - N_{overlap}} + 1 = \frac{40000 - 2048}{1024} + 1 \approx 38$$
38 段对应的相干底噪约 0.026,这个水平足够做有效筛选了。如果算出段数少于 8,就要在分辨率和稳定性之间做取舍——这时我会优先保证段数,宁可牺牲一点频率分辨率,因为记数太少导致的相干偏差会让整个结论都不可靠。
6.2 重叠率:50% 不是随便定的
50% 重叠是个行业默认值,背后的理由有两个。一是补偿窗函数的信息损失:加窗会在段两端把数据压到接近 0,重叠一半能让被压掉的样本在相邻段里重新获得完整权重。二是控制相关性:相邻段重叠太多,各段之间高度相关,平均的效果就打折扣了,方差的下降不再是严格的 $1/K$。
常用的组合是 Hann 窗配 50%、Kaiser 窗配 60%~75%。重叠率超过 75% 一般没有额外收益,只是白算。低于 50% 比如 0 重叠,算得快但方差偏大,而且在窗函数两端的信息损失没人补。
还有一个实操细节:重叠点数必须是整数。noverlap用nperseg // 2是安全的,用int(0.5 * nperseg)也可以,但如果从前端传来的是浮点比例,记得先转整型,否则 scipy 会报错或者静默截断。
6.3 去趋势和去均值:低频段的隐形杀手
这两件事经常被当成小事跳过,实际影响很大。传感器零点漂移、电荷放大器的温度漂移、ADC 的直流偏置,都会在极低频注入巨大的能量。如果不去趋势,自谱在 0 Hz 附近会有一个高出几十 dB 的尖峰,而 CPSD 在这个区域的数值会被这个直流成分主导。
具体表现是:相位在低频段呈现完全无规律的跳变,unwrap之后变成一条陡峭的直线,然后时延估计出一个荒唐的大数值。我第一次遇到时以为是数据有问题,后来发现就是没去趋势。
处理方式:detrend='linear'能去均值加线性趋势,覆盖大多数漂移场景。如果漂移明显是非线性的(比如温度急剧变化期间采集的数据),可以在预处理阶段用高通滤波(比如 0.5 Hz 的 Butterworth)先处理一遍,再做谱分析。注意滤波必须是零相位的(filtfilt而非lfilter),否则滤波器自身的相位延迟会叠加到时延估计上,把结果带偏。
提示:如果先做了滤波再做 CSPD,两路信号必须用完全相同的滤波器参数和相同的
filtfilt处理,任何一路的参数差异都会在两路之间引入虚假的相位差。
6.4 时间对齐与缺失样本
真实采集系统里两路信号的起始时刻可能差几个采样点,甚至中间有丢包。这个偏差会直接叠加到时延估计上,而且很难从谱图上分辨出来——它和真实的传播时延在数学形式上完全一样。
我的处理步骤是固定的:先用互相关做一个粗对齐。
from scipy.signal import correlate, correlation_lags xc = correlate(x - x.mean(), y - y.mean(), mode='full') lags = correlation_lags(x.size, y.size, mode='full') coarse_lag = lags[np.argmax(np.abs(xc))] print(f"互相关粗估时延: {coarse_lag / fs * 1e3:.3f} ms")这个粗估值精度受采样率限制(这里是 0.5 ms),但足够判断"是不是有整体错位"。如果粗估出来的数值远大于物理上可能的传播时延,那就说明是采集系统的时间基准问题,需要先在数据层面做对齐(scipy.signal.resample_poly或者插值),再进 CPSD 流程。
对齐之后再用 CPSD 相位斜率做精细估计,两者应当一致。如果差很多,说明存在频散(不同频率的传播速度不同),那就要按频段分段估计时延,而不是强行用一个线性模型拟合全频段。
6.5 一份可以直接抄的检查清单
把上面这些串起来,我在每次跑 CPSD 之前会过一遍这个清单,能挡掉绝大多数低级错误:
- 两路信号的采样率是否严格相同(包括时钟源是否同源)?
- 记录长度是否足够切出至少 8 段?如果不足,是先降分辨率还是先接受更大的相干偏差?
welch与csd的window、nperseg、noverlap、detrend是否逐项一致?- 是否做了去趋势?低频有没有异常的直流尖峰?
- 相干在关心的频段是否显著高于 $1/K$ 的底噪?
- 相位图是否只在相干高的频段上做解读?
- 时延估计的符号是否和互相关粗估一致?
- 有无传感器极性反接的可能(相位整体偏移 180°)?
这八条里,第 3 条和第 5 条是我见过出错频率最高的。前者是静默错误,代码不报错但结果全错;后者是被忽略的统计前提,很多人拿着 4 段数据算出来的相干当圣旨。
6.6 记录长度不够时的补救思路
最后说一个现实约束:现场采集经常没法采很久,可能只有几秒钟的数据。这种情况下段数和分辨率必然打架,我的处理顺序是:
先降低对频率分辨率的要求。如果只关心某个宽频带(比如 100 到 500 Hz 的整体能量),那把段长压到 256 甚至 128,换来几十段数据,相干估计会稳很多,代价是频率栅格变粗,但在这个需求下无所谓。
如果分辨率不能降,那就接受较大的相干偏差,但要相应提高判断阈值。段数只有 6 的时候,相干底噪约 0.17,这时候我把有效判据从 0.8 提到 0.9,宁可漏掉一些真实的相关成分,也不要把噪声当成信号。
再不行,还可以考虑把记录分段做短时分析,看相干随时间的变化趋势,而不是追求一个全局平均的结果。短时相干能反映系统状态的漂移,对诊断时变问题反而更有价值。这个方法我在处理启停过程的数据时用得比较多,全局平均会把启停瞬态完全抹平。
实测下来,这套流程在振动、声学、控制系统辨识这几类场景里都跑得通,核心就是三件事:参数对齐、相干把关、相位只在可信频段解读。我自己最常犯的错不是算法理解不到位,而是急着看结果、跳过了相干检查这一步——每次跳过去,后面都要花更多时间回头返工。