简介:面向统计信号处理学习者与科研人员的广义最大似然比检验(GLRT)MATLAB仿真资源,聚焦弱信号检测与噪声背景下异常判断问题,适合正在学习假设检验、需要动手验证理论的本科高年级或研究生。压缩包仅3KB,包含7个功能明确的m脚本,分别覆盖仿真数据生成、似然函数与似然比计算、临界值求解、检测概率与虚警率评估等关键环节,并基于连续波信号模型进行设置,便于对照理论逐步复现。已有452人学习,代码结构清晰、注释简明,可帮助读者快速理解GLRT的决策流程,观察不同参数对检测性能的影响,并为实际雷达或通信场景中的检测问题提供可修改的仿真框架。同时,资源围绕一个典型算例完整呈现从建立假设、计算似然比到统计判决的整个过程,适合作为课程设计或论文复现的参考。
1. 看到这串文件名先别急着解压:MLE、GLRT、似然比检验说的是同一件事
看到这串文件名,做统计信号处理的人多半会心一笑。MLE、GLRT、Statistical test、似然比检验——四个词指向同一个技术方向:用最大似然估计构造广义似然比检验,回答"信号到底存不存在、模型该不该换"。这个方向的实际价值很直接:只要观测能被某个概率分布描述,想在两个假设之间做选择,似然比检验就是最通用的框架之一;参数未知时,GLRT先用MLE把未知量估出来再比,代价是渐近分布只在样本量足够时才准确。这篇笔记按"原理→代码→参数→踩坑→验证"展开,适合正在做信号检测、模型对比,或需要证明"差异显著"的从业者。新手跟第3章能跑通最小例子,熟手直接看第4、5章的参数与坑位。
2. 似然比检验的原理:MLE是地基,GLRT是处理未知参数的升级版
2.1 最大似然估计:为什么似然比检验离不开它
回到最原始的问题。拿到一组观测 $x_1, x_2, \dots, x_n$,假设它们独立同分布,服从带参数 $\theta$ 的分布 $f(x;\theta)$。似然函数写成:
$$L(\theta) = \prod_{i=1}^{n} f(x_i; \theta)$$
最大似然估计就是找出让 $L(\theta)$ 最大的那个 $\hat{\theta}$。这个概念看起来只是在做参数估计,但它同时给统计假设检验提供了一把尺子:如果某个假设能让数据出现的概率最大,那这个假设就是相对合理的。于是检验问题被转化成比较"受约束的假设"和"不受约束的假设"各自对数据的解释能力,而解释能力的度量就是似然函数值本身。
似然比检验的统计量写成:
$$\Lambda(x) = \frac{\max_{\theta \in \Theta_0} L(\theta)}{\max_{\theta \in \Theta} L(\theta)}$$
分子是零假设约束下的最大似然值,分母是全局最大似然值。因为 $\Theta_0$ 是 $\Theta$ 的子集,分子永远不超过分母,所以 $\Lambda$ 落在 $[0,1]$ 区间内。$\Lambda$ 越接近1,说明零假设下数据也被解释得不错,倾向于接受 $H_0$;$\Lambda$ 明显小于1,说明零假设太勉强,应该拒绝。这里"嵌套模型"是整个方法的基石——如果两个假设不是嵌套关系,$\Lambda$ 可能大于1,整个框架立刻失效。
实际计算时没人直接用 $\Lambda$,原因在 Wilks 定理里。这个定理说的是:在 $H_0$ 成立、样本量趋于无穷时,$-2\ln\Lambda$ 渐近服从自由度为 $d$ 的卡方分布,其中 $d = \dim(\Theta_1) - \dim(\Theta_0)$。它的工程价值极大:你不需要为每个具体问题做蒙特卡洛仿真找门限,直接用卡方分布的分位数就能判决,用scipy.stats.chi2.ppf和sf就能拿到门限和 p 值。代价是它是渐近结论,样本量不够大、参数落在边界、模型不满足正则条件时,卡方近似都会偏离,这留给第5章细说。
2.2 GLRT的定义:把未知参数替换成MLE之后,统计量发生了什么变化
Neyman-Pearson 引理给出了最优检验,前提是两个假设下的概率密度完全已知,包括所有参数值。现实里这个前提几乎不成立——你多半只大概知道噪声方差,信号幅度未知;或者方差也要估计。广义似然比检验的做法很直白:未知参数就用 MLE 估出来,然后当作已知参数代入似然比:
$$t(x) = \frac{f(x; \hat{\theta}_1)}{f(x; \hat{\theta}_0)}$$
注意 $\hat{\theta}_0$ 是受 $H_0$ 约束的 MLE,$\hat{\theta}_1$ 是无约束的 MLE,两者不是一回事,别在代码里省掉其中一个。直觉上,因为估计过程"偷看"了数据,GLRT 会比理想似然比检验更激进,倾向于拒绝 $H_0$。这个偏差在渐近意义下会消失,这是 Wilks 定理保证的,但有限样本下必须靠仿真校准。
以检测高斯白噪声中的直流信号为例,接收模型是 $x_i = \mu + w_i$,其中 $w_i \sim \mathcal{N}(0, \sigma^2)$。检验目标是 $H_0: \mu = 0$ 对 $H_1: \mu eq 0$。当方差已知时,$\mu$ 的 MLE 就是样本均值 $\bar{x}$,这是高斯似然求导后得到的闭式解,不需要迭代优化。把 MLE 代入似然比并整理,$-2\ln\Lambda$ 化简成:
$$t(x) = \frac{n\bar{x}^2}{\sigma^2}$$
这个形式有清晰的物理含义:样本均值偏离零假设的程度,除以噪声方差,再乘上样本量。偏离越大、数据量越多,越怀疑信号存在。它还说明了一件事——在高斯加性噪声模型下,GLRT 最终等价于能量检测器或均值检测器,统计量是样本均值的函数,不是更复杂的东西。如果只关心信号方向,比如只检测正的直流偏移,就把判决改成单边:不仅要 $t(x)$ 超过门限,还要求 $\bar{x} > 0$。
2.3 自由度与模型嵌套:这个细节决定了p值对不对
自由度 $d$ 是两个参数空间维度之差,不是"被检验参数的个数"。检验两个独立样本组的均值是否相等时,$H_1$ 的参数空间是 $(\mu_1, \mu_2, \sigma^2)$,维度3;$H_0$ 下 $\mu_1 = \mu_2 = \mu$,参数空间是 $(\mu, \sigma^2)$,维度2;自由度是 $3-2=1$。如果你凭直觉认为"两个均值就是两个参数,自由度应该是2",那 p 值就会系统性偏大,检验变得过于保守,本来显著的差异可能被判成不显著。
嵌套性是另一个高频出错点。嵌套的意思是 $H_0$ 的参数空间是 $H_1$ 参数空间的子集。检验 $\mu=0$ 对 $\mu eq 0$ 是嵌套的;检验"观测服从正态分布"对"观测服从指数分布"就不是嵌套的,两者不能做似然比检验。非嵌套模型在工程上更常用的比较工具是 AIC、BIC 这类信息准则,它们的推导出发点不同,不能混用。还有一类边界问题:比如 $H_0: \mu \ge 0$ 而真实参数恰好落在边界 $\mu=0$ 上,此时卡方近似的收敛速度会明显变慢,实际虚警率比名义水平偏大,这在第5章的 5.4 节有对应的处理方法。
3. 用GLRT检测高斯噪声中的直流信号:最小可运行的Python实现
3.1 先写清楚假设,再写代码
写统计检验代码前,第一件事是用注释把三件事写清楚:数据模型、零假设与备择假设、哪些参数已知。这个习惯能省掉大量返工。本文的场景如下:数据模型是 $x_i = \mu + w_i$,$i = 1, \dots, n$,$w_i$ 独立同分布服从 $\mathcal{N}(0, \sigma^2)$;检验目标是 $H_0: \mu = 0$ 对 $H_1: \mu eq 0$;先假定方差 $\sigma^2$ 已知。
为什么先假定方差已知?因为这能让 GLRT 统计量有解析形式,并且在 $H_0$ 下精确服从 $\chi^2_1$ 分布。你可以用蒙特卡洛仿真验证代码写对了没有。如果一上来就假定方差未知,统计量的精确分布变成 $F(1, n-1)$,代码跑出的结果对不对就很难判断——到底是理论错了还是实现错了?先建立一个可信基线,再逐步放开假设,这是工程上更稳的推进方式。
实际计算时,$H_1$ 下 $\mu$ 的 MLE 就是样本均值 $\bar{x} = \frac{1}{n}\sum_{i=1}^{n}x_i$,这是高斯似然对 $\mu$ 求导得到的闭式解。有人会用scipy.optimize.minimize数值最大化似然函数,结果和np.mean(x)一致但慢了几十倍。高斯模型这类可解析求解的场景,直接用闭式解,节省时间也避免数值问题。
3.2 核心代码:GLRT统计量、p值与判决
import numpy as np from scipy import stats def glrt_dc_detect(x, sigma2, alpha=0.05): """ 高斯白噪声中检测未知直流信号的GLRT。 参数 ---- x : array_like 观测序列 sigma2 : float 噪声方差(已知) alpha : float 显著性水平,即允许的虚警概率 返回 ---- t_stat : float GLRT统计量 n * mean^2 / sigma2 p_value : float 零假设下出现当前或更极端统计量的概率 reject : bool True 表示拒绝 H0,认为存在直流信号 """ x = np.asarray(x, dtype=float) n = x.size # H1下 mu 的MLE:高斯似然的闭式解是样本均值 mu_hat = np.mean(x) # GLRT统计量:-2*ln(L0/L1) 化简后的解析形式 # 不直接计算两个似然函数再相除,避开数值下溢 t_stat = n * mu_hat**2 / sigma2 # 卡方分布自由度1;双边检验 mu != 0 p_value = stats.chi2.sf(t_stat, df=1) threshold = stats.chi2.ppf(1 - alpha, df=1) return t_stat, p_value, t_stat > threshold这里的三个设计选择值得说明。第一,统计量用化简式而不是原始似然比定义:当 $n$ 超过几十,直接用prod(f(x_i))计算似然函数乘积会下溢成0,取对数再相减同样会损失精度,化简式完全绕开这个问题。第二,chi2.sf算右尾概率,正好对应 p 值的定义——零假设下出现当前或更极端统计量的概率;门限用ppf(1-alpha, df=1)取上侧分位数,两侧逻辑一致。第三,函数返回统计量、p 值、判决结果三个值,后续做 Monte Carlo 仿真时直接取用,不需要重复计算。
3.3 Monte Carlo验证:5千次试验看虚警率
验证代码正确性的最好方法不是反复核对理论推导,而是做仿真。思路很简单:在 $H_0$ 下生成大量数据集,每次调用glrt_dc_detect,统计误报比例,看它是否接近 $\alpha$。
def estimate_false_alarm(n=50, sigma2=1.0, alpha=0.05, n_trials=5000, seed=42): rng = np.random.default_rng(seed) false_alarms = 0 for _ in range(n_trials): x = rng.normal(0.0, np.sqrt(sigma2), n) _, _, reject = glrt_dc_detect(x, sigma2, alpha) false_alarms += int(reject) return false_alarms / n_trials for a in [0.01, 0.05, 0.1]: fa = estimate_false_alarm(alpha=a) print(f"alpha={a:.2f}, 经验虚警率={fa:.4f}")几个参数值得细说。n_trials=5000是最低可接受的仿真次数:经验频率的标准误差约为 $\sqrt{p(1-p)/N}$,在 $p=0.05$ 时约0.003,足够判断虚警率是否显著偏离理论值。seed=42不是随便选的,固定随机种子才能让结果可复现,同事跑出来的数字和你的一致。如果两次运行结果不同,不是代码有 bug,而是没有固定种子。rng.normal走 NumPy 1.17 之后推荐的default_rng接口,不要再用旧的全局np.random.seed加np.random.normal混着写。
运行后你会看到经验虚警率在理论值附近小幅波动。如果偏差超过0.01,第一步检查自由度是否写错;第二步检查统计量是不是忘了乘 $n$;第三步检查H0下的数据生成有没有混入非零均值。这三个低级错误几乎覆盖了所有"虚警率对不上"的场景。
提示:把这段虚警率自检代码保留在你的工具库里。以后每次改动统计量、换数据生成方式、调自由度,都先跑一遍。这是检验代码回归测试的核心,比任何代码审查都可靠。
3.4 检测概率仿真:扫描信噪比画性能曲线
虚警率验证通过只说明零假设下没有失控,还要看备择假设下能不能检测出来。固定虚警率,检测概率与信噪比之间的关系,就是这个检验的性能标尺。
def monte_carlo_pd(snr_db, n=50, sigma2=1.0, alpha=0.05, n_trials=5000, seed=0): """ 给定信噪比(dB)下的检测概率。 snr_db = 10*log10(mu^2 / sigma2) """ rng = np.random.default_rng(seed) mu = np.sqrt(sigma2 * 10**(snr_db / 10.0)) detects = 0 for _ in range(n_trials): x = rng.normal(mu, np.sqrt(sigma2), n) _, _, reject = glrt_dc_detect(x, sigma2, alpha=alpha) detects += int(reject) return detects / n_trials for snr in [-10, -5, 0, 5, 10]: pd = monte_carlo_pd(snr) print(f"SNR={snr:3d} dB, P_d={pd:.3f}")信噪比定义成 $10\log_{10}(\mu^2/\sigma^2)$,单位是 dB。负10dB 时信号功率是噪声功率的十分之一,检测概率应该接近 $\alpha$,基本靠猜;0dB 时均值幅度等于噪声标准差,检测概率明显上升;10dB 以上几乎必然检测到。如果曲线不符合这个趋势,多半是数据生成或统计量构造有错。常见错误是把 $\mu$ 生成成 $\sqrt{\sigma^2 \cdot 10^{snr/10}}$ 时忘记外层开根号,或者把 $\sigma^2$ 错当 $\sigma$ 用。这个函数本身也可复用:换场景时只需要改数据生成那一行,性能评估框架不用动。
4. 参数怎么设:显著性水平、自由度与样本量的工程选择
4.1 显著性水平不要照抄0.05:从应用场景倒推门限
教科书里 0.05 几乎成了默认值,实际工程里这个数值要由应用承担的成本决定。学术论文里 0.05 用于控制错误发现率尚可接受;雷达检测、故障告警、医学诊断这类场景,一次虚警的代价很高,虚警率指标可能要求 $10^{-5}$ 甚至更低。此时用卡方分布理论分位数作为门限是否还可靠?只要统计量在 $H_0$ 下确实服从卡方分布,分位数本身就是精确的,不会因为显著性水平变小而失效。真正的风险来自样本量不足:尾部偏差在小 $\alpha$ 下会被放大,小样本加小 $\alpha$ 的场景必须用经验门限。
经验门限的做法是:在 $H_0$ 下生成 $N$ 个数据集,算出 $N$ 个统计量 $t_1, \dots, t_N$,排序后取 $(1-\alpha)$ 分位数作为门限。这个门限不依赖任何渐近近似,代价是需要几千到几万次仿真。我通常把理论卡方门限和经验门限一起打出来对比,若偏差超过20%,就采用经验门限。对需要反复使用的检验场景,把经验门限预计算好存下来,运行时查表,性能完全可接受。
4.2 自由度怎么数才不出错:维度差方法
自由度计算的唯一可靠方法是维度差:$\dim(\Theta_1) - \dim(\Theta_0)$。以下是四种常见模型的具体数值:
| 检验场景 | H1参数空间与维度 | H0参数空间与维度 | 自由度 |
|---|---|---|---|
| 直流信号 mu=0,方差已知 | mu,维度1 | 无参数,维度0 | 1 |
| 直流信号 mu=0,方差未知 | mu, sigma2,维度2 | sigma2,维度1 | 1 |
| 两组均值相等,方差未知 | mu1, mu2, sigma2,维度3 | mu, sigma2,维度2 | 1 |
| 线性回归全模型 vs 去掉2个系数 | 原模型 p+1 个参数 | 减元模型 p-1 个参数 | 2 |
注意第二行和第三行的自由度都是1。方差未知时不要以为"参数变多了所以自由度变大"——两个假设下方差都要估计,这个维度被抵消了。自由度真正变化的场景是 $H_1$ 比 $H_0$ 多估计了若干个系数,比如回归模型里比较"包含两个额外自变量"与"不包含"的情况,自由度才是2。写代码前把两边的参数空间列出来,把维度差的数字写在函数注释里,这是成本最低的防错手段。
4.3 样本量与精确分布:何时不能靠卡方近似
Wilks 定理是渐近结果,样本量多少才算足够大没有严格边界。以方差未知的直流检测为例,GLRT 统计量在 $H_0$ 下精确服从 $F(1, n-1)$ 分布。$n=20$ 时 $F(1,19)$ 的0.95分位数约为4.38,而 $\chi^2_1$ 的0.95分位数是3.84——用卡方门限意味着实际显著性水平高于名义值,虚警偏多。$n=50$ 时约4.03,差距缩到5%;$n=100$ 时约3.94,差距约2.6%。
工程上我的习惯是:$n \ge 100$ 才放心用卡方近似;$n$ 在20到100之间尽量用精确分布,查scipy.stats.f.ppf(1-alpha, 1, n-1)做门限;$n < 20$ 时 GLRT 的小样本性质不稳定,考虑置换检验这类重抽样方法。置换检验对分布假设要求低,但每次检验需要几千次重抽样,计算量大,适合离线分析,不适合在线实时检测。这里不展开实现细节,但要记住它是一个可靠的备选项。
4.4 模型假设的适用前提:写代码前就要想清楚
最后一个参数类问题不是数值而是模型设定。GLRT 适用前提包括:样本独立同分布或至少似然函数可写;参数空间光滑;真实参数不在边界上;模型嵌套。如果你的数据是时间序列且强相关,或者备择假设的参数空间不光滑,再调参数也救不回来。正确的做法是换检验框架,比如基于秩的非参数检验,或专门处理序列相关性的似然比修正。判断一个检验方法是否适用,优先级高于调整参数——参数调得再好,模型设错了也是白做。
5. 避坑:似然比检验最常见的5个翻车现场
5.1 似然比大于1,取对数直接报错
现象:代码算出 $\Lambda > 1$,np.log得到正数,p 值变成负的或毫无意义。原因几乎只有两种:一是两个模型不是嵌套关系,分母不是全局最大化;二是代码里分子分母写反了。解决:先确认 $H_0$ 的参数空间确实是 $H_1$ 的子集,再检查L0和L1的赋值顺序。我习惯在函数 docstring 里把分子分母的含义写死,并在关键位置加一行assert lambda_ <= 1 + 1e-9,一旦违反立刻暴露问题,不等到后面算出奇怪数字才回头查。
5.2 p值整体偏移,经验虚警率对不上显著性水平
现象:$H_0$ 下仿真,经验虚警率稳定在0.08而不是0.05。原因:自由度写错、统计量公式漏了 $n$、或用了错误的方差值。排查顺序按三步走。第一步打印统计量的均值:$H_0$ 下 $\chi^2_1$ 的均值是1,如果均值明显偏离,统计量本身就有问题。第二步核对仿真代码里的数据生成是否真的满足 $H_0$。第三步检查chi2.sf(t, df)里df是不是维度差而非参数个数。按这个顺序排查,基本十分钟内可以定位。我一直保留一个 $H_0$ 仿真的最小脚本,任何检验代码改动后都先跑一遍,让回归测试替我把关。
5.3 对数似然全是-inf,统计量变成NaN
现象:计算对数似然时出现RuntimeWarning: divide by zero,输出是nan或无穷大。原因:直接连乘密度函数再取对数,样本量一大就下溢;或者密度函数在某个点取到0,对数变成负无穷。解决:所有似然计算一律使用对数似然并逐样本累加,禁止np.prod。对高斯模型直接用scipy.stats.norm.logpdf(x, loc, scale).sum(),由库函数处理尾部细节。如果必须自己写密度函数,用np.log时要对参数范围做保护,比如方差下限加一个eps=1e-12。这个坑在做非高斯模型时尤其常见,指数族之外的概率密度很容易在某个点取0。
5.4 真实虚警率比理论值偏大,重复多次都一样
现象:$H_0$ 下经验虚警率稳定高于 $\alpha$,并且和样本量关系不大。原因:参数位于参数空间边界。典型场景是单边检验 $H_0: \mu \ge 0$ 里真实 $\mu=0$ 正好落在边界上,或方差检验中 $H_0: \sigma^2=0$。边界破坏了 Wilks 定理所需的正则条件,卡方近似不成立。解决:改用仿真校准门限,或者用专门处理边界问题的混合卡方分布。工程上如果只是要一个判决结论,做 $10^5$ 次仿真标定经验门限,代码简单,结论可靠。别试图用更大的样本量硬撑——在某些边界场景下,样本量再大也救不回卡方近似。
5.5 换了随机种子,检测概率结果差很多
现象:seed=0时检测概率 $P_d=0.65$,seed=1时 $P_d=0.71$,不知道该信谁。原因:仿真次数太少,估计标准差太大。$P_d \approx 0.68$ 时,1000 次试验的标准差约0.015,但统计量接近门限时,单次试验结果对种子更敏感,实际抖动会更大。解决:试验次数提到5000以上,固定种子,并把不同种子下的结果都打出来做一致性检查,应该在 ±0.02 范围内波动。如果项目对性能曲线精度要求高,额外保存原始统计量列表而不是只存一个均值。固定种子不是为了好看,是为了让结论可复现——否则同事复现你的结果时会对不上,最后只能花时间排查一个不存在的问题。
6. 收尾:与你的实际场景对接,以及三个必须做的自检
6.1 拿到类似的项目包先看什么
如果你手上的压缩包是统计检验代码的常见形态,先别急着改代码,按三个层次读。第一层跑通 demo 脚本,复现它输出的数字;第二层看核心函数签名返回什么、假定哪些参数已知,判断能不能直接用你的数据喂进去;第三层找到数据生成与假设设定的注释,确认与你的场景一致。spellcw5这类后缀通常是作者或版本的标识,不影响使用。按正常流程解压即可,代码包的完整性和安全性检查是另一件事,这里不展开。
6.2 三个自检方法,改动任何假设后都跑一遍
第一个自检:$H_0$ 下经验虚警率与名义 $\alpha$ 一致。第二个自检:$H_1$ 下固定信噪比,检测概率随样本量 $n$ 增大单调上升。第三个自检:统计量的经验直方图与理论卡方密度叠加后形状吻合,直观确认渐近逼近成立。这三个自检加起来不到50行,能拦下绝大多数翻车。我自己的习惯是把自检写成独立脚本,不混进业务代码,每次改完假设和参数单独重跑。
最后分享一个长期习惯:每写完一组检验代码,把数据生成、假设设定、自由度计算三件事单独抽成模板。下次换场景先回答三个问题——方差已知还是未知、单边还是双边、模型是否嵌套——回答完这三个问题,检验代码的框架基本不用动。这套流程帮我处理过几十个检测与对比任务,多数统计错误都源于这三个问题没想清就动手写。希望帮到你。
本文还有配套的精品资源,点击获取