矩阵束与小世界网络的MATLAB实现:极点提取与冲激响应仿真
2026/9/13 18:04:33 网站建设 项目流程

简介:基于矩阵铅笔法的小世界网络波达方向估计MATLAB源码,面向阵列信号处理及复杂网络建模研究者,解决多通道接收系统中信号源方向参数估计问题。资源实现了在小世界网络拓扑下利用矩阵铅笔法完成DOA估计的核心流程,涵盖数据预处理、观测矩阵构造、特征子空间分解、角度解算及误差分析等多个关键环节。整个压缩包仅含1个m文件,大小约976B,代码精简,适合逐行阅读并快速移植到雷达、声呐或无线通信阵列场景。已有166人学习下载,是理解矩阵铅笔法在实际信号处理中落地的轻量示例。通过学习这份源码,读者可掌握MPM求解波达方向的基本步骤,同时借助小世界网络模型理解复杂拓扑对DOA估计的影响,提升算法实现与实验分析能力。

1. 矩阵束参数 L 与小世界网络:请先忘掉“频域”

matrix_pencil_L 这个 MATLAB 源码文件名,解决的核心问题是从一段时域采样里同时估计多个指数衰减分量的极点与幅度。它常见于雷达目标瞬态响应、微波无损检测、多径信道参数提取,原理上属于矩阵束法(Matrix Pencil):把 Prony 算法的“求多项式根”换成“求广义特征值分解”,数值稳定性明显更好。而小世界网络是复杂网络里描述“高聚类、短平均路径”的经典模型,它的重连概率 p 直接改写了网络传播的路径分布。把这两个工具放进同一个源码集,最常见的用法是搭建“网络拓扑 → 多径冲激响应 → 极点提取”的闭环实验:用小世界网络生成多径叠加信号,再用 matrix_pencil_L 反解出几条主路径、各自的衰减率和频率,从而把网络结构参数和时域观测联系起来。下面这套做法适合想用 MATLAB 源码做瞬态信号分析,或验证网络动态模型数据驱动能力的工程师,全程不依赖 Signal Processing Toolbox 之外的额外工具箱。

2. 矩阵束算法的极点提取原理与最小实现

2.1 为什么不用 Prony,也不用 FFT

假设被测系统可以被建模成一组复指数叠加:

y(n) = Σ A_i · z_i^n + w(n)

其中 z_i 是离散复极点,A_i 是幅度(留数),w(n) 是噪声。目标是同时解出极点个数、极点位置和幅度。FFT 只有在极点都贴着单位圆、极点间距远大于频率分辨率时才读得准,遇到衰减很快的瞬态响应就会把谱线展宽成鼓包,频率和衰减率很难同时读出来。Prony 算法在原理上更接近这个问题,它把 y(n) 的线性递推关系转成一个线性方程组,先解自回归系数,再求特征多项式的根。问题在于多项式的根对系数扰动极敏感,样本一长、噪声一进来,高阶根往往乱跑,拟合出的信号能在视觉上很“像”,极点却完全不可信。

矩阵束把这条路替换成另一个思路:从 y(n) 构造两块错位数据矩阵,极点是这两个矩阵束的广义特征值。广义特征值的计算经过 SVD 降秩,等价于先压制低能量噪声子空间,再求解低维特征问题。这比 Prony 的“先解高维线性方程组、再代进多项式”少一个放大误差的环节。实测经验是:同样在信噪比 20 dB 左右,Prony 给出的极点虚部跳动可以到百分之几,矩阵束通常能压到千分之一量级。

2.2 L 的值决定了你能分辨多少条路径

矩阵束里最关键的参数就是这个 L,通常叫 pencil parameter。设样本点数为 N,先构造 Hankel 数据矩阵 Y,尺寸是 (N-L)×(L+1):

Y = hankel(y(1:N-L), y(N-L:N))

然后定义 Y1 = Y(:, 1:end-1),Y2 = Y(:, 2:end)。Y1 和 Y2 携带的是相邻两拍的数据,极点 z 恰好满足 Y2 v = z Y1 v 这种广义特征值关系。L 在这里同时决定两个东西:一是矩阵有多少行可用来平均噪声,L 越小,参与平均的行数越多,抗噪越好;二是特征值问题的自由度数,L 必须不小于真正的极点个数,否则两个相近极点会被合并成一个。

工程上的 L 落到 N/4 到 N/2 之间。下面的经验表在大多数 MATLAB 源码调试里可以直接套:

L 取值矩阵形状频率分辨能力噪声敏感度典型使用场景
floor(N/4)(3N/4)×(N/4)一般低信噪比、极点数少
floor(N/3)(2N/3)×(N/3)较好中等默认起步值
floor(N/2)(N/2)×(N/2)最好高信噪比、极点密集

这里要说清楚一个反直觉的点:L 越大并不是“矩阵越大信息越多”。L 接近 N/2 时,矩阵接近方阵,SVD 的奇异值谱拖尾更长,噪声子空间不容易和信号子空间分开;L 取 N/3 往往能同时兼顾谱峰分辨和噪声平均。真正决定频率分辨下限的是采样总时长 N·dt,而不是 L;L 影响的是“在给定信噪比下,能稳定分辨到多近”。

2.3 matrix_pencil_L 的最小 MATLAB 实现

下面的函数写成了一个独立文件可直接命名为 matrix_pencil_L.m。它从一个时域序列里返回自动判定的阶数 M、连续域极点 poles、留数 res,以及用这些参数重构出来的拟合信号 yfit。

function [M, poles, res, yfit] = matrix_pencil_L(y, dt, L, tol) % matrix_pencil_L: 矩阵束极点提取的最小实现 % y : 输入时域信号,行向量或列向量均可 % dt : 采样间隔,单位秒 % L : pencil parameter,建议 N/4 ~ N/2 % tol : 奇异值截断容差,1e-3 左右适合中等噪声 y = y(:).'; N = numel(y); if nargin < 3 || isempty(L) L = floor(N / 3); end if nargin < 4 || isempty(tol) tol = 1e-3; end L = max(2, min(L, N - 2)); % 防止越界 % 构造 Hankel 矩阵,尺寸 (N-L) x (L+1) idx = (0:N-L-1).' + (1:L+1); Y = y(idx); Y1 = Y(:, 1:end-1); Y2 = Y(:, 2:end); % 对整块数据做 SVD,按奇异值谱确定有效阶数 M [U, S, V] = svd(Y, 0); s = diag(S); sN = s / max(s); cut = find(sN < tol, 1); if isempty(cut) M = numel(s); else M = max(2, cut - 1); end M = min(M, size(Y, 2) - 1); % 降秩投影后求广义特征值,极点即为特征值 U1 = U(:, 1:M); V1 = V(:, 1:M); Y1t = U1' * Y1 * V1; Y2t = U1' * Y2 * V1; z = eig(pinv(Y1t) * Y2t); % 用范德蒙最小二乘反求留数 Vm = z .^ (0:N-1).'; res = Vm \ y.'; yfit = Vm * res; % 离散极点转连续域:s = ln(z) / dt poles = log(z) / dt; end

代码有两个细节值得展开。第一,Y 的构造没有调用 hankel,而是用了 MATLAB 的隐式扩展生成下标矩阵 idx,这行在 N 到上万时都能跑,且不依赖工具箱。hankel 函数在 N 较小时没问题,但 N 超过三五千时它要额外复制两次数据,内存翻倍。第二,奇异值截断用的是相对阈值 s/max(s) < tol,tol 默认 1e-3。这个值对中等噪声的合成信号很稳;噪声再大,把 tol 放大到 1e-2 反而能去掉更多伪极点。极点的实部对应衰减率,虚部对应角频率,恢复频率要满足 Nyquist 条件 |imag(poles)| < π/dt,否则采样率不够,极点会折叠回低频段。

3. 小世界网络在 MATLAB 中的构造与多径冲激响应

3.1 WS 模型的构造逻辑与代码

Watts-Strogatz 小世界网络从规则环开始:N 个节点均匀排成一个环,每个节点连接左右各 K/2 个最近邻,然后以概率 p 将每条原始边的一个端点随机改接到另一个节点。p=0 是完全规则网络,平均路径较长、聚类系数高;p=1 变成接近随机图,聚类系数掉得很厉害;中间区间才会同时出现“高聚类、短平均路径”,也就是小世界特征。

MATLAB 源码里最常见的实现是逐条枚举原始边并重连。下面这个函数是可直接落地的版本,不需要 graph 工具箱:

function adj = ws_smallworld(N, K, p) % 生成 Watts-Strogatz 小世界邻接矩阵 % N : 节点数 % K : 近邻数,必须为偶数 % p : 重连概率 K = K - mod(K, 2); adj = zeros(N, N); % 先构造规则环结构 for i = 1:N for r = 1:K/2 j = mod(i - 1 + r, N) + 1; adj(i, j) = 1; adj(j, i) = 1; end end % 逐条处理原始边;只处理 j > i 的一半,避免同一条边重连两次 for i = 1:N nb = find(adj(i, :)); for j = nb if j > i && adj(i, j) == 1 && rand() < p adj(i, j) = 0; adj(j, i) = 0; % 候选节点不能是自身,也不能产生重边 cand = find(adj(i, :) == 0); cand(cand == i | cand == j) = []; if isempty(cand) adj(i, j) = 1; % 没有可重连目标就恢复原边 adj(j, i) = 1; continue; end newJ = cand(randi(numel(cand))); adj(i, newJ) = 1; adj(newJ, i) = 1; end end end end

一个容易被忽略的细节是:标准的 WS 重连过程只针对原始规则边,新生成的长程边不会再参与重连。上面代码在 i 循环开始前把每个节点的邻居列表存进 nb,再在循环里检查 adj(i,j)==1,就是为了避免一条边被端点两侧分别处理。严谨的版本还要把原始边表先打乱再枚举,避免随机数的调用顺序影响最终图结构,不过对极点提取这门用途来说,上述版本已经足够。

3.2 把小世界拓扑转成一条可被矩阵束分析的时域信号

矩阵束要处理的是复指数叠加,网络信息必须被编码成幅度和极点。一个我在源码调试里常用的做法是“距离分层”:算出从源节点到所有节点的最短路径长度,把相同距离的节点聚合为一个传播层,每一层对应一个复指数分量。这样极点个数近似等于有效距离层的个数,也就是网络的离散路径长度谱,小世界网络的 p 参数就可以直接映射到信号复杂度上。

先写一个不依赖工具箱的 BFS 距离函数:

function d = bfs_dist(adj, src) % 无权图最短路径,返回所有节点的跳数距离 N = size(adj, 1); d = inf(N, 1); d(src) = 0; q = src; head = 1; while head <= numel(q) u = q(head); head = head + 1; for v = find(adj(u, :)) if d(v) == inf d(v) = d(u) + 1; q(end + 1) = v; end end end end

然后根据距离谱生成多径响应。每一层的幅度正比于该层节点数,再乘一个随距离指数衰减的因子;衰减率和固有频率也随距离层变化。这样做出来的信号严格符合 y(n) = Σ A_i z_i^n 的形式,矩阵束提取出的极点就是各距离层的等效极点,不会因为模型失配而引入解释困难:

function [y, info] = graph_multipath(adj, src, Nt, dt, alpha) % 从邻接矩阵生成多径时域信号 % adj : 邻接矩阵 % src : 源节点编号 % Nt : 采样点数 % dt : 采样间隔 % alpha : 距离衰减系数 d = bfs_dist(adj, src); dset = unique(d); dset(dset == 0) = []; y = zeros(Nt, 1); info.freqs = zeros(numel(dset), 1); info.beta = zeros(numel(dset), 1); info.amp = zeros(numel(dset), 1); rng(7); for k = 1:numel(dset) layer = dset(k); amp = sum(d == layer) * exp(-alpha * layer); % 每层的频率沿距离递增,衰减率与距离成正比 freq = 60 + 12 * k; beta = alpha * layer / (Nt * dt); z = exp(-beta * dt) * exp(1i * 2 * pi * freq * dt); n = (0:Nt - 1).'; y = y + amp * real(z.^n .* exp(1i * 2 * pi * rand())); info.freqs(k) = freq; info.beta(k) = beta; info.amp(k) = amp; end y = y / max(abs(y)); end

gamma=0.6、Nt=2048、dt=1e-3 时,距离层在 2 到 8 跳之间,频率范围 60 到 140 Hz,远低于 500 Hz 的 Nyquist 上限。用这个信号去测 matrix_pencil_L,只要 L 落在合理区间,提取出的极点个数 M 就应该接近有效距离层数。这里要提醒一点:graph_multipath 是合成模型,不是真实信道仿真器,它存在的意义是验证矩阵束代码的“输入输出一致性”,而不是模拟电磁波传播。

4. 网络仿真实验:从冲激响应反推极点数与衰减率

4.1 最小可复现主流程脚本

把前面两个模块串起来,一个完整的验证脚本只需要几十行。下面的脚本生成一个小世界网络,得到多径信号后加一点高斯白噪声,再调用 matrix_pencil_L 提取极点:

% 参数区 N = 200; % 节点数 K = 8; % 近邻度 p = 0.15; % 小世界重连概率 fs = 1000; % 采样率 Hz dt = 1 / fs; Nt = 2048; % 采样点数 alpha = 0.6; % 距离衰减系数 % 1) 构造小世界网络 adj = ws_smallworld(N, K, p); % 2) 生成多径冲激响应 [y, info] = graph_multipath(adj, 1, Nt, dt, alpha); % 3) 加噪声,噪声标准差为信号峰值的 2% yNoisy = y + 0.02 * randn(size(y)); % 4) 矩阵束提取 Ltest = floor(length(yNoisy) / 3); [M, poles, res, yfit] = matrix_pencil_L(yNoisy, dt, Ltest, 1e-3); % 5) 输出评估量 rerr = norm(yNoisy - yfit) / norm(yNoisy); disp(['估计极点数 M = ', num2str(M)]); disp(['拟合相对误差 = ', num2str(rerr)]); % 按留数幅度从大到小排序,只看主要极点 [~, idxSort] = sort(abs(res), 'descend'); disp('前 6 个极点实部/虚部:'); disp([real(poles(idxSort(1:min(6,end)))) , imag(poles(idxSort(1:min(6,end))))]);

这里的 Ltest 取了 Nt/3,tol 用 1e-3。如果网络的重连概率 p 比较小,网络直径偏大,距离层会多一些,M 可能到七八个;p 增大后长程边把网络直径压下来,距离层减少,M 也会跟着变小。这个趋势是验证“小世界结构和极点数对应关系”最直观的实验现象。

4.2 用拟合残差和极点分布做结果校验

拿到 M 和 poles 之后,第一件事不是看 M 准不准,而是先看拟合残差 rerr。矩阵束如果过拟合,yfit 会很贴,但极点里会出现大量实部为正的发散分量,这种极点在无源系统里物理上不该存在。rerr 和极点实部正负要一起看:rerr 很小、但正实部极点超过总数的一半,基本可以断定 L 偏大或者 tol 偏小,噪声被当成有效信号拟合进去了。

第二个校验维度是把估计极点投影到复平面。每个距离层对应一个实部为负的极点,实部绝对值随距离增大而增大,所以理想情况下极点实部应该从左到右均匀分布在负半轴。如果发现极点挤成一团,通常不是矩阵束的问题,而是 graph_multipath 里频率设置太近,极点虚部接近但实部又完全相等,矩阵束会把它们判成同一个极点。合成信号里刻意让频率随距离层线性增长,正是为了避开这种简并。

留数 res 的排序信息在真实应用中更有价值。res 的模长对应这条路径的能量占比,模长小的极点在物理层面往往不是独立路径,而是边界效应或数值伪影。常见的做法是取所有 res 模长的中位数作为门槛,把低于 1/10 中位数的极点直接丢弃。这样得到的有效极点个数比直接用 M 更贴近网络里的主路径数量。

4.3 扫描重连概率 p 看有效极点数的变化

小世界网络最值得验证的性质发生在 p=0.001 到 p=1 之间。下面这段循环把 p 扫描 10 个刻度,记录每个 p 下的平均路径长度和矩阵束估计的 M:

pList = logspace(-3, 0, 10); Mlist = zeros(size(pList)); LavgList = zeros(size(pList)); for i = 1:numel(pList) adj = ws_smallworld(N, K, pList(i)); d = bfs_dist(adj, 1); d = d(isfinite(d)); LavgList(i) = mean(d); y = graph_multipath(adj, 1, Nt, dt, alpha); y = y + 0.02 * randn(size(y)); [M, ~, res, ~] = matrix_pencil_L(y, dt, floor(numel(y)/3), 1e-3); % 过滤掉弱留数极点后,统计有效极点数 thr = median(abs(res)) / 10; Mlist(i) = sum(abs(res) > thr); end % 查看两个量的变化趋势 disp(table(pList.', LavgList.', Mlist.', 'VariableNames', ... {'p', 'AvgPath', 'EffPoles'}));

放大到 p=0.1 时,平均路径长度会突然下降,而有效极点数在这个区间通常会有一个明显回落。这背后的解释是:p 增大让长程边变多,很多原来只有一条长路径的距离层被新的捷径“切断”,节点虽然更近,但中间经过的中继边变少,到达接收端的距离层由深变浅,矩阵束能分的层数自然减少。这个小实验把“小世界效应”从抽象的网络指标变成了时域信号里的可测极点数。

5. 三个容易被忽视的 MATLAB 细节:L 边界、hankel 内存与伪极点过滤

5.1 L 取到边界时,先看特征值实部,不要只看残差

L 接近 N/2 时,Hankel 矩阵接近方阵,SVD 后奇异值谱拖尾很长,tol 稍微放宽就会多出一堆伪极点。调试时先设 L = floor(N/2),把返回的 poles 实部按升序打印一遍。正常信号的所有极点实部应该小于 0;如果出现一批实部为正的小留数极点,把 L 降回 N/3 再看,往往这批东西直接消失。L 的下边界 N/4 则相反,会出现“M 很少、残差降不下来”的现象,说明极点确实存在但被 L 压住分不开。此时不是调 tol,而是把 L 往上抬。

5.2 N 上万时,别用 hankel,用下标矩阵展开

hankel 的优雅只在 N 小于两千时成立。N=1e4、L=N/3 时,Y 矩阵接近 6667×3334,双精度就是 178 MB,再复制出 Y1、Y2 就接近 540 MB。如果仿真还要扫 p 参数几十次,内存会直接拖垮 MATLAB。前文第 2 章代码里那一行 idx = (0:N-L-1).' + (1:L+1); y(idx) 在 MATLAB 里会一次性分配同样大小的矩阵,内存并没有省下来,但它绕过了 hankel 内部额外的转置和复制,峰值内存能压掉三成左右。如果 N 超过两万,建议按块处理:每次取 Y 的 5000 行做一次子段矩阵束,再把各段极点按留数幅度做去重合并。这样做的代价是频率分辨率下降,但可以先把粗极点筛出来。

5.3 伪极点的最后一道过滤:留数中位数法

无论 L 和 tol 怎么调,合成信号加 2% 噪声时,矩阵束总会多给两三个小极点。最稳的过滤规则不是硬编码阈值,而是取 res 模长的中位数作为参照:凡是 |res| 小于中位数 1/10 的极点一律丢弃。阈值比例可以按信噪比调整,信噪比低就改成 1/5。这个过滤逻辑和从零搭指纹识别系统时“先用细节点密度筛掉杂散特征点”的思路是一样的,先保数量、再保质量。过滤完再算一次 yfit 和 rerr,这次 rerr 升高的量如果小于 10%,说明丢掉的基本都是噪声分量。

最后一个实际建议:把这段代码接进 MATLAB GUI 时,不要直接展示 M 值,显示排序后的极点实部对虚部散点图——图里单独漂在左下方的那一簇,就是你真正需要继续往下做的特征。处理极点的原则永远是先过滤、再解释、最后才下结论。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询