简介:源信号数目估计是信号处理、无线通信与音频分析中的经典难题。针对这一场景,一份MATLAB实现源码完整封装了AIC、IAIC、MDL、IMDL和MEVARC五种代表性准则,覆盖信息论与最小期望方差等不同思路,并配有主程序与复合矩阵生成等辅助脚本,可直接运行、调整参数并对比不同信噪比条件下的估计准确率曲线,也支持对自定义数据集进行估计与结果可视化。压缩包共8个文件,核心为7个.m脚本文件,另含1个.asv自动保存文件,整体仅5KB,轻量易用。目前已有183人学习下载,适合信号处理课程实验、算法研究或工程排错参考。借助这套源码,使用者既能理解各类准则在模型复杂度与数据拟合度之间的权衡逻辑,也能快速评估五种算法在强弱噪声环境下的适应能力,为实际应用中的源数目筛选与信噪比适应性分析提供了一手实验工具。
1. 源数目估计不是数一数:MDL 为什么判不准,MEVARC 在补什么
拿到一包 source-number-estimation 代码,你会以为它做的事很简单:阵列收到信号,数一数里面有几个辐射源。但当你真的布好 8 阵元均匀线阵,混进三个不相关的信号,把 MDL 判据跑出来的结果一看,2 个、3 个、4 个来回跳,和信噪比强相关,这时候才会意识到,源数目估计是阵列信号处理里最容易被低估的一步。它做错了,后面 MUSIC、ESPRIT、波束形成全部跟着翻车。
源数目估计(source number estimation)在雷达、声学麦克风阵列、通信感知里承担同样的角色:它是 DOA 估计的前置硬决策。最常用的无参方法是基于信息论准则的 MDL(Minimum Description Length,最小描述长度),而标题里的 MEVARC 是特征值方差比这条技术路线,它不像 MDL 给一个全局判据,而是通过特征值分布的统计差异逐级判定信号个数。信噪比估计则夹在两套方法之间——MDL 的惩罚项结构决定了它在低信噪比下漏检,而 MEVARC 的阈值又直接依赖信噪比估计的稳定性。这篇就把 MDL、MEVARC 和信噪比估计三者的关系拆开,从理论公式讲到可直接复现的最小代码,再把一定会踩的坑列清楚。
2. 先立模型再谈决策:MDL 判据的推导和它与 AIC 的本质差别
2.1 从阵列模型到特征值谱:MDL 到底在拿什么做比较
设均匀线阵有 M 个阵元,同时收到 P 个远场窄带信号,第 t 次快拍的接收向量写为:
x(t) = A(θ)·s(t) + n(t)
A(θ) 是 M×P 的导向矢量矩阵,s(t) 是信号复包络,n(t) 是复高斯白噪声,功率为 σ²。这个模型是后面所有判据的地基。MDL 判据对象不是原始数据 x(t),而是样本协方差矩阵:
R̂ = (1/N) Σ_{t=1}^{N} x(t)xᴴ(t)
N 是快拍数。对 R̂ 做特征值分解,得到按降序排列的 λ₁ ≥ λ₂ ≥ … ≥ λ_M。如果真实源数为 P,那么前 P 个特征值受信号功率影响,明显大于剩余 M−P 个全由噪声决定的特征值,后者理论上都在 σ² 附近。MDL 做的事情就是:猜一个候选源数 k,按照“前 k 个是信号子空间、后 M−k 个是噪声子空间”的假设重新拟合观测数据的概率分布,然后看这个假设能不能解释得好。解释得越好,似然越大,但模型越复杂,惩罚项越重。
这里最关键的一点是,MDL 从来没有显式估计信噪比,但它隐式地在比较特征值之间的差距。信噪比高,前 P 个特征值和噪声特征值的差距大,判据区分明显;信噪比低到噪声子空间的特征值都挤成一团,MDL 就分不出来了。所以标题里的“信噪比估计”才会和源数目估计强耦合——不是用信噪比做输入,而是源数目估计的可靠性本身就由信噪比决定。
2.2 MDL 与 AIC:同一个惩罚项,一个用 lnN 一个用常数,后果完全不同
MDL 和 AIC 的公式形式几乎一样,只差了惩罚项的系数。对候选源数 k,AIC 的代价是:
AIC(k) = −2·log L(k) + 2·k·(2M−k)
MDL 的代价是:
MDL(k) = −log L(k) + 0.5·k·(2M−k)·ln N
log L(k) 是假设有 k 个源时观测数据的对数似然值,具体形式由特征值乘积与均值表示。两边的第一项都随 k 增大而下降,惩罚项都随 k 增大而上升,取全局最小的 k 作为估计源数。差别在于惩罚强度:AIC 的惩罚项是常数 2 乘自由度,不随 N 变化;MDL 的惩罚项里多了一个 ln N,快拍数越大,惩罚越重。
这个差别的后果是理论级的:AIC 在快拍数趋于无穷时仍有过估计概率,而 MDL 是相合的——N 越大,估计越准。工程层面,N 在几百到几千时,MDL 对过估计的压制已经明显强于 AIC。我自己的习惯是:只要快拍数超过 200,就优先用 MDL;AIC 只在快拍极少、又不允许漏检的场景下考虑,比如某些单帧处理的声学定位。要注意,MDL 这里的“log”在多数实现里是自然对数,如果代码里用的是以 10 为底的 log10,惩罚项会整体缩小约 2.3 倍,结果会明显偏向过估计,这是移植 MATLAB 代码到 Python 时最容易翻车的一处。
3. 用 Python 复现 MDL 源数目估计:核心代码与参数变化
3.1 最小可跑的 MDL 估计函数
下面是 MDL 源数目估计的最小实现版本,输入是阵列接收数据矩阵 X,形状 M×N,返回估计的源数目。建议先单独跑通这段,再接到你自己的 MUSIC 或 ESPRIT 前面。
import numpy as np def mdl_source_number(X): """ MDL 源数目估计 X : ndarray, shape (M, N) M 个阵元,N 个快拍 返回: 估计的源数目 k_hat """ M, N = X.shape # 样本协方差矩阵,注意除以 N 而不是 N-1 R = (X @ X.conj().T) / N # 特征值分解,按降序排列 eigvals = np.linalg.eigvalsh(R)[::-1] eigvals = np.real(eigvals) mdl_vals = np.zeros(M) for k in range(M): # 噪声子空间特征值:索引 k 到 M-1 noise_ev = eigvals[k:] # 对数似然项:用几何均值与算术均值的比值表示拟合质量 avg = np.mean(noise_ev) geo = np.prod(noise_ev) ** (1.0 / len(noise_ev)) # 比值越接近 1,说明噪声子空间越“白” likelihood = N * (len(noise_ev) * np.log(avg) - np.log(geo)) # MDL 惩罚项,0.5 系数来自 Rissanen 的经典推导 penalty = 0.5 * k * (2 * M - k) * np.log(N) mdl_vals[k] = likelihood + penalty k_hat = int(np.argmin(mdl_vals)) return k_hat这段代码里的 likelihood 项是推导后的简化形式,它不直接算复高斯概率密度,而是用“噪声子空间特征值是否集中在一处”来等价衡量拟合质量。几何均值与算术均值的比值越接近 1,说明这 M−k 个特征值越聚集,越像噪声。argmin 取全体候选源数里代价最小的 k。候选范围是 0 到 M,也就是说 MDL 允许判“一个源都没有”,这在纯噪声帧里会表现为 k_hat=0,这是合理输出,不是 bug。
一个需要留意的参数是np.log的自然对数基准。如果是从 MATLAB 复刻过来的工程,原代码可能用了log即自然对数,没有差异,但如果有人写了log10,MDL 的惩罚项会轻得多。检查方法很简单:在固定 M 和 N 下,过估计频次突然变高,就先检查这个。
3.2 信噪比估计与源数目估计的耦合设置
MDL 本身不需要显式信噪比作为输入,但落到工程里,信噪比估计仍然要做,原因是:MDL 输出的源数会抖动,后续 DOA 估计需要知道当前帧的信噪比来决定是否信任这个源数。常见做法是在得到 k_hat 之后,用噪声子空间特征值均值去估计噪声功率,再反推信号功率,从而得到阵列输出信噪比。
def estimate_snr_by_subspace(X, k_hat): """ 基于源数目估计结果的信噪比估计 思路:后 M-k 个特征值的均值近似噪声功率,总功率减噪声功率得信号功率 """ M, N = X.shape R = (X @ X.conj().T) / N eigvals = np.real(np.linalg.eigvalsh(R)[::-1]) # 用 MDL 判过的噪声子空间估噪声功率 noise_ev = eigvals[k_hat:] noise_power = np.mean(noise_ev) # 总接收功率 = 信号功率 + 噪声功率,反推信号功率 total_power = np.trace(R) / M signal_power = total_power - noise_power snr_db = 10.0 * np.log10(signal_power / noise_power) return snr_db, noise_power这段代码演示了把源数目估计和信噪比估计串起来的标准流程:先用 MDL 给定源数,再按k_hat 切割特征值子空间。这个流程的微妙之处在于,信噪比估计的准确性反过来受源数误差影响——MDL 漏判一个源,噪声子空间里就混进一个信号特征值,噪声功率会被高估,导致信噪比低估。所以不要盲目相信这个信噪比值,它更适合作为帧质量的相对指标,而不是绝对测量。在工程里我一般会加一个时间平滑:连续多帧的源数和 SNR 各做一个中值滤波,源数抖动幅度从 ±2 降到接近于 0,这对后续 DOA 跟踪非常有帮助。
4. MEVARC 与蒙特卡洛验证:低信噪比下哪里翻车,阈值该怎么给
4.1 MEVARC 的特征值方差比:统计量构造和阈值选择
MEVARC 这一系方法的思路和 MDL 完全不同。MDL 走的是似然与惩罚的道路,而 MEVARC 走的是“特征值方差比”的道路:在纯噪声条件下,M 个特征值的方差应该和噪声功率、快拍数存在确定关系;一旦混入信号,前几个特征值的离差明显变大。于是可以通过逐级比较特征值之间的比值来判定源数。
一个经典的构造方式是从最大特征值开始,依次比较,第 k 个特征值与剩余特征值均值的比值超过阈值就判为信号:
def mevarc_source_number(eigvals, threshold=3.0): """ MEVARC 式特征值方差比源数目估计 eigvals : 已降序排列的特征值 threshold : 比值阈值,典型经验范围 2.0 ~ 4.0 """ eigvals = np.asarray(eigvals) M = len(eigvals) k_hat = 0 for k in range(M - 1): signal_ev = eigvals[k] noise_ev = eigvals[k + 1:] # 当前特征值与剩余噪声特征值均值的比值 ratio = signal_ev / np.mean(noise_ev) # 比值高,说明该特征值显著离开噪声平台,判为信号 if ratio > threshold: k_hat = k + 1 else: # 一旦掉到阈值以下,后面的特征值都视作噪声 break return k_hat这个代码的判定逻辑是顺序扫描:从最大的特征值开始,逐个和后面剩下的特征值比。比值超过阈值就继续扫描,一旦某个特征值比不出来,后面的全部判为噪声子空间,循环终止。阈值是关键参数,它既不像 MDL 那样有信息论推导,也不像某些检测方法有 CFAR 曲线可查。实践里,阵元数 M=8、快拍 N=100 左右,2.5 到 3.5 是常用区间;M 更大时阈值可以适当降低,因为噪声特征值均值更稳定。如果阈值设成 2 以下,低信噪比时会疯狂过估计;设到 5 以上,又会在中等信噪比下漏掉弱信号。MEVARC 的优势是计算量比 MDL 小,并且可以配合噪声功率估计直接控制误判方向,代价就是阈值调起来带点玄学味道。
4.2 蒙特卡洛验证:成功率曲线怎么画才可信
判断 MDL 和 MEVARC 谁更合适,不能靠单次运行的结果说话,要做蒙特卡洛实验。基本设置是固定阵元数、快拍数、真实源数和方向,扫描信噪比从低到高,每个信噪比点重复几百次独立实验,统计正确估计的比例。下面这段代码给出一个最小验证框架:
def monte_carlo_detection(m=8, n=100, p_true=3, snr_range=(-10, 10, 5), trials=200): """ 蒙特卡洛验证源数目估计的成功率 m : 阵元数 n : 快拍数 p_true : 真实源数 snr_range : (起始, 终止, 点数) trials : 每个信噪比点的独立重复次数 """ rng = np.random.default_rng(42) snr_list = np.linspace(snr_range[0], snr_range[1], snr_range[2]) angles = np.array([-30.0, 5.0, 40.0])[:p_true] * np.pi / 180.0 # 导向矢量:等距线阵,阵元间距半波长 def steering(theta): return np.exp(-1j * 2 * np.pi * 0.5 * np.arange(m) * np.sin(theta)) A = np.column_stack([steering(theta) for theta in angles]) # 每个信噪比点的成功率曲线 for snr_db in snr_list: success = 0.0 snr_linear = 10 ** (snr_db / 10.0) for _ in range(trials): # 随机信号:复高斯,功率归一 s = (rng.standard_normal((p_true, n)) + 1j * rng.standard_normal((p_true, n))) / np.sqrt(2.0) # 按信噪比分配噪声功率 noise_power = 1.0 / snr_linear sigma = np.sqrt(0.5 * noise_power) n = (sigma * (rng.standard_normal((m, n)) + 1j * rng.standard_normal((m, n)))) # 接收数据 = 信号混合 + 噪声 X = A @ s + n # 用样例里封装的 MDL 估计 k_hat = mdl_source_number(X) if k_hat == p_true: success += 1.0 print(f"SNR={snr_db:5.1f} dB success_rate={success / trials * 100:5.1f}%")这里信号复包络的功率做了归一化,sin(θ) 每根导向矢量增益为 1,所以真实信号功率为 1 时,噪声功率取 1/snr_linear 刚好让阵列输入信噪比等于目标值。打印结果就能看到一条成功率曲线,在低信噪比区间曲线会快速跌落,那个拐点就是这套参数下的检测门限。此段代码演示的只是 MDL 的验证,MEVARC 只需把mdl_source_number(X)换成用特征值分解后调用mevarc_source_number即可。
蒙特卡洛里最容易被忽略的是随机种子。每个信噪比点用同一组随机种子,结论才可对比;如果每次运行都重新生成不同的随机数,曲线会带着较大的抖动,很难判断算法差异到底是真实差距还是随机波动。我一般在算法对比实验里统一固定seed=42,并在每个信噪比点之间改变数据生成方式但不改变种子,这样成功率曲线的形状会更平滑。
5. 源数目估计必踩的四个坑:现象、原因与解法
5.1 低信噪比下 MDL 漏检,结果看起来像“少了一个源”
现象:真实有 3 个源,其中 2 个功率相当,1 个比它们低 8 dB,在信噪比 0 dB 左右连续跑了 50 帧,MDL 一直判 2。
原因:MDL 的惩罚项和似然项存在固有的保守偏置,弱信号的特征值会落到噪声平台上,统计上无法和噪声特征值区分。这本质上是信息论准则在有限样本下的检测门限问题。
解决:把弱源信噪比作为先验指标,如果期望检测的弱源功率比强源低 8 dB 以上,就需要提高阵元数或快拍数。具体做法是把 N 从 100 提到 400,MDL 的 lnN 惩罚项会变重,同时似然项对噪声特征值的压缩能力增强,漏检概率显著下降。如果 N 无法增加,考虑 MEVARC 并手动把阈值调低到 2.2 左右,用牺牲少量过估计概率换取弱源检测率。
5.2 高信噪比下 MDL 反而过估计,特征值扩散不收敛
现象:信噪比升到 15 dB 以上,MDL 偶尔判出 M−1 个源,也就是说几乎把所有阵元都当成了信号。
原因:高信噪比时信号特征值非常大,剩余噪声特征值在数值上被压低,几何均值与算术均值的比值出现数值误差,特征值扩散被误判为信号。另一个常见来源是样本协方差矩阵求逆时产生数值上不可忽略的微小特征值。
解决:在特征值分解之后加一个数值下限保护,把所有低于最大特征值 1e−8 倍的特征值替换成一个微小正数,避免 likelihood 项里出现 log 0 或除以零。这个坑在 16 位浮点运算或 GPU 混合精度下尤其严重,我在把单精度和双精度结果对比时发现过 5% 以上的估计差值,必须统一用双精度跑 MDL 的似然项。
5.3 相干信号导致 MDL 系统性低估源数,怎么处理都只有 1 个
现象:两个信号由同一发射机经多径到达阵列,通道间完全相干,MDL 把两个源判成 1 个。
原因:相干信号使 R̂ 中对应的两个特征值不再分别对应两个源,信号子空间的秩被压缩到小于真实源数。MDL 无法从特征值上区分这两个相干信号。
解决:先做空间平滑或前后向平滑再送 MDL。典型做法是把 M 阵元阵列分成若干子阵,把子阵协方差取平均,恢复协方差矩阵的秩。平滑后源数目估计才能回到正确数量级。如果系统是均匀线阵,前后向平滑可以用一行代码完成:R_smooth = 0.5 * (R + np.fliplr(np.flipud(R))),但要记住 M 会缩水到平滑子阵长度。
5.4 快拍数过短,协方差矩阵病态,判据在同一帧里反复跳变
现象:快拍数 N=32,阵元多到 M=16,MDL 结果在 0 到 6 之间跳动,明显不合理。
原因:样本协方差矩阵在 N<M 时是奇异的,特征值分解后噪声子空间的特征值并不是平稳接近 σ²,而是被严重压低。MDL 的似然比值会失真,源数估计变成随机数。
解决:先判断 N/M 是否大于 2,小于这个比值就不要硬跑 MDL。工程替代是对阵列做降维,比如把 16 阵元分成 4 个 4 阵元的子阵分别估计,再做投票合并。另一个办法是对快拍数据做对角加载:R_loaded = R + beta * np.eye(M),加载量 beta 取 R 迹的 1/100 附近,能把病态特征值推离零平面,让 MDL 的输出稳定。但加载量不宜过大,否则会掩盖真实信号特征值的差异。
6. 复用与验证:让源数目估计在工程里稳定工作的一个习惯
无论 MDL 还是 MEVARC,想在真实系统里长期稳定工作,单靠算法本身是不够的,还要有完整的验证与参数管理习惯。我的做法是每个项目里都会保留一个“算法体检”脚本:固定一组已知源数和信噪比,连续跑多次蒙特卡洛,画出成功率随信噪比变化的曲线,同时把估计错误的具体分布打出来——是偏漏检还是偏过估计,这对后续调参的指向性非常明确。
MEVARC 的阈值不要固定在代码深处,应该做成可调节参数,在系统联调时用一个文本配置项传进去。MDL 的惩罚项系数 0.5 也不要轻易改动,只有在快拍数非常小、需要人为压低漏检率时,才会把它降到 0.4 附近换取更多的候选源数。每次调整后都要重跑一遍蒙特卡洛,确认新的成功率曲线没有把误检转移到更危险的区间。我在交付过的几个声学阵列项目里,最终稳定版本都不是单一算法,而是 MDL 和 MEVARC 的组合:先用 MDL 给出基准值,再用 MEVARC 在低信噪比段做修正,最后用一个滑窗中值对时间轴上的估计结果做平滑。这个组合的好处是,MDL 的保守性和 MEVARC 的敏感性互补,源数估计的连续性明显提高,DOA 跟踪的断点大幅减少。后来凡是新接手的项目,我都会先在浮点精度的 Python 仿真里把这两套算法的边界摸清楚,再迁移到嵌入式实现——信噪比越低,越不能用凭空设定的阈值硬扛,这类经验都是从一次次翻车里换来的。希望帮到你。
本文还有配套的精品资源,点击获取