简介:压缩感知理论允许信号以远低于奈奎斯特速率的条件采样,并通过重构算法恢复原始数据,正交匹配追踪(OMP)正是其中应用广泛的迭代重构方法。这一MATLAB实现包专门面向需要理解算法细节并完成代码落地的学习者、研究人员及信号处理方向工程师。压缩包体积约2KB,仅包含1个M文件,代码逻辑集中,便于快速阅读和直接运行。程序涵盖参数初始化、残差计算、最大相关原子检索、支撑集更新与迭代终止判断等关键环节,运行后可清晰观察稀疏信号的重构过程;修改稀疏度、测量矩阵或噪声条件,还能对比不同参数下的恢复效果,加深对算法稳定性的理解。该算法在图像压缩、无线通信、医学成像等领域均有应用,这份源码可作为改进算法或移植到实际工程的起点。资源已有159人浏览学习,适合正在学习压缩感知、信号处理或需要编写重构算法的读者作为入门参考。
1. 压缩感知里的OMP:为什么搜索恢复算法时总会挂上这个名字
当你手里只有 M 条观测数据,感知矩阵 Φ 又是 M×N 的满秩行向量,其中 N 远大于 M,常规最小二乘立刻会解出一个能量分散的密实向量,真正的 K 个非零分量一个都对不上。这是压缩感知里最常见的困境,也是 OMP(正交匹配追踪)最典型的用武之地。OMP 用“贪心选原子 + 正交投影”两步走:每一步挑一个与当前残差相关性最高的原子,再用最小二乘把已选原子的贡献整体扣除,反复迭代到稀疏度用完。只要感知矩阵的约束等距性不过分恶化,很小的观测数就能把信号恢复到工程可接受的误差。
在 MATLAB 社区里,OMP.m 这个文件名几乎成了压缩感知恢复算法的代名词。用 OMP.m.zip 这类打包文件搜到的内容,大概率就是一个主迭代函数加一两个示例脚本,再加上测量矩阵构造和误差评估的辅助代码。与其依赖某个封装版本,我更建议从函数体本身开始,把“选原子、更新支撑集、算残差”这条主线做清楚,新手能直接跑通,有经验的人也能在此基础上换成大矩阵或分块处理,而不被工具包束缚。
这篇内容面向已经有线性代数基础、想自己复现实验并调参数的人。我会从 OMP 的原理推导讲起,再给出一段可运行的实现,接着讨论图像压缩感知的分块重建,最后落到 OMP 的变体和验证技巧上。
2. OMP 的数学模型与正交化推导,以及和 MP 的本质差异
2.1 压缩感知问题中的 OMP 定位
设稀疏信号为 x∈R^N,其中最多 K 个非零位置,观测得到 y=Φx+n,Φ∈R^{M×N} 且 M<<N。要从欠定方程中恢复 x,最直接的做法是求解
min ‖x‖_0,约束 y=Φx。
这个 l0 问题是 NP 难的,枚举支撑集在 N 很大时完全不可行;l1 凸松弛(基追踪)能求解,但依赖凸优化器,在线处理和嵌入式场景里不够轻量。OMP 走的是完全不同的路线:它把支撑集估计当成一个贪心搜索问题,每次迭代只增加一个最有把握的原子,再通过正交投影把残差中已解释的部分彻底去掉,从而让下一次选原子时只看到没有被当前支撑集覆盖的方向。
下面这段 MATLAB 片段给出 OMP 一次迭代的数学骨架,实际实现会在下一章展开:
% OMP一次迭代的数学骨架,伪代码级 r = y; Omega = false(N, 1); % 支撑集指示向量 for iter = 1:K corr = abs(Phi' * r); % 所有原子与残差的内积 corr(Omega) = -inf; % 排除已入选原子 [~, j] = max(corr); % 相关性最强的原子索引 Omega(j) = true; Phi_s = Phi(:, Omega); alpha = Phi_s \ y; % 最小二乘系数 r = y - Phi_s * alpha; % 残差更新 end这段代码把 OMP 的四步全写清楚了:相关计算、支撑集更新、系数估计、残差更新。注意第 7 行用的是\运算,不是直接求逆,MATLAB 会按列满秩的最小二乘路径处理,数值上比显式pinv稳定一些。
2.2 正交投影推导:残差为什么要按已选原子空间扣除
OMP 的核心操作在第 8 行,对残差做正交投影更新。记第 k 次迭代的支撑集为 Λ_k,已选原子构成的子空间为 Φ_{Λ_k},其列空间上的正交投影矩阵为
P_{Λ_k} = Φ_{Λ_k}(Φ_{Λ_k}^T Φ_{Λ_k})⁻¹ Φ_{Λ_k}^T。
系数向量的最小二乘解是
α_k = (Φ_{Λ_k}^T Φ_{Λ_k})⁻¹ Φ_{Λ_k}^T y,
于是残差更新等价于
r_k = y − Φ_{Λ_k} α_k = (I − P_{Λ_k}) y。
这个形式的几何意义很直接:残差 r_k 永远落在已选原子列空间的正交补里。下次计算相关时,与已选原子方向相同的那部分信号分量不会再贡献相关性,因此不会重复选到同一个原子,也不会因为重复选择导致系数收敛缓慢。OMP 的这个性质,正是它相比最早 MP(匹配追踪)能在有限 K 步内稳定终止的根本原因。
2.3 MP 与 OMP 的差异:一次正交投影改变了什么
MP 的做法是每次只更新新增原子的系数,残差只与最新选中的原子正交,之前选过的原子和残差之间的内积可能再次变大,于是同一个原子会被反复选中多次,收敛到固定精度需要的迭代次数会明显上升。OMP 相反,每次都把所有已选原子重新做一次最小二乘,通过正交投影一次性修正全部系数,残差与整个支撑集正交。
| 对比维度 | MP | OMP |
|---|---|---|
| 系数更新方式 | 仅更新新增原子系数 | 每次迭代重估所有已选原子系数 |
| 残差正交范围 | 只与最近一次选的原子正交 | 与全部已选原子正交 |
| 迭代步数 | 可能远大于 K,需要阈值控制 | 通常 K 步内停止 |
| 单步计算量 | O(MN) | O(MN) 加一次最小二乘 |
| 数值稳定性 | 系数波动大 | 正交投影下更稳定 |
| 适用场景 | 稀疏编码、字典学习 | 压缩感知信号恢复 |
这个表格在实践中最重要的启示是:OMP 的收敛保障靠的是“正交”二字。如果实现时为了省时间把第 8 行的残差更新改成 y − Φ_{Λ_k}(Φ_{Λ_k}^T y) 这种不完整的计算,实质上就退化成了 MP,支撑集恢复正确率会明显下降。
2.4 复杂度、RIP 与测量数选择的现实边界
OMP 的计算代价主要由两步决定:每轮计算 Φ^T r 需要 O(MN),最小二乘解一次需要 O(Mk) 到 O(k²M) 不等。朴素实现跑 K 次迭代的总复杂度约为 O(KMN),感知矩阵规模一大,这个开销很快成为瓶颈,图像分块处理因此成为常见折中方案。
关于恢复条件,RIP 是理论层面的关键。若存在常数 δ_K 使得对任意 K 稀疏向量 v 都有
(1−δ_K)‖v‖₂² ≤ ‖Φv‖₂² ≤ (1+δ_K)‖v‖₂²,
则称 Φ 满足 K 阶 RIP,δ_K 越小说明测量越保真。OMP 的理论保证通常要求 δ_{K+1} 足够小,工程上更实用的判断是检查测量数 M 是否满足 M ≈ 2~4K·log(N/K),随机高斯矩阵在这个量级下通常能给出稳定恢复。低于这个范围时,OMP 偶尔也能成功,但支撑集选择会变得很敏感,不再建议依赖。
3. 用 MATLAB 写一个可运行的 OMP 最小实现并厘清核心参数
3.1 一个可直接落地的 OMP 函数
实际工程里我一般不会用重型工具箱,而是维护一个很小的函数,方便改成增量 QR 或者接入自定义停止条件。下面这个版本的思路是:维护布尔型支撑集指示向量,每次迭代把已选原子从候选中剔除,然后解最小二乘并更新残差。
function x_hat = omp_solve(y, Phi, K) % omp_solve - 正交匹配追踪的最小实现 % 输入: % y : M*1 观测向量 % Phi : M*N 测量矩阵,M << N % K : 稀疏度估计,即最多迭代次数 % 输出: % x_hat : N*1 恢复信号 [M, N] = size(Phi); r = y; % 初始残差 support = false(N, 1); % 支撑集标记 Phi_s = zeros(M, K); % 预分配已选原子矩阵 for iter = 1:K corr = abs(Phi' * r); % 所有原子与残差的内积 corr(support) = -inf; % 已入选原子不再参与竞争 [~, j] = max(corr); support(j) = true; Phi_s(:, iter) = Phi(:, j); active = Phi_s(:, 1:iter); alpha = active \ y; % 对已选原子做最小二乘拟合 r = y - active * alpha; % 正交投影后的残差 end x_hat = zeros(N, 1); x_hat(support) = alpha; end代码里有几个细节值得说明。corr(support) = -inf的作用不是置零,而是利用max跳过已选位置,避免同一原子被重复选择;如果改成corr(support)=0,在存在零相关原子时可能出现误判。第 14 行每次从 1:iter 截取活动矩阵,虽然没做内存复用,但胜在清晰,适合教学和小规模实验。若信号真实支撑集超过 K,或者观测有较强噪声,这个函数仍会强制跑满 K 步,需要在循环内记录残差范数并提前跳出,下面的小节会给出更稳健的做法。
3.2 核心参数表与稀疏度 K 的估计方法
用 OMP 之前,先要把几个关键参数的含义和工作范围定下来。
| 参数 | 含义 | 典型取值与说明 |
|---|---|---|
| K | 稀疏度估计,迭代上限 | 无先验时从 5~10 起步做扫描 |
| M | 观测维度 | 建议 M ≥ 2K·log(N/K) |
| Φ | 测量矩阵 | 高斯随机矩阵、伯努利矩阵,行归一化 |
| 停止阈值 ε | 残差范数阈值 | 无噪声场景设较小值,有噪声按 ‖n‖₂ 估计 |
| 支撑集大小约束 | 防止过拟合 | 避免 K 超过 M/2,否则理论保证失效 |
K 的估计是实际应用里绕不开的问题。自然信号很少是精确 K 稀疏的,更多是近似稀疏,比如图像经过 DCT 变换后系数从大到小衰减。常见做法是先解一个较大迭代次数的 OMP,记录每次迭代后的残差范数,观察残差下降曲线;曲线从陡降转为平缓的地方对应的迭代次数,就是可用的 K。另一种做法是按噪声水平反推:已知噪声 n 的能量约 ‖n‖₂,那么当残差范数降到 ‖n‖₂ 以下时,继续迭代多半是在拟合噪声,此时应停止。
3.3 一维稀疏恢复实验与支撑集正确率评估
下面这个脚本构造一个随机稀疏信号,用高斯矩阵测量,再用 OMP 恢复,并同时评估数值误差和支撑集命中率。
% OMP 一维稀疏恢复实验 rng(7); N = 512; M = 128; K = 20; x = zeros(N, 1); p = randperm(N, K); x(p) = randn(K, 1); % 随机生成的 K 稀疏信号 Phi = randn(M, N) / sqrt(M); % 行归一化高斯矩阵 y = Phi * x; % 无噪声观测 x_hat = omp_solve(y, Phi, K); % 调用最小实现 true_support = find(abs(x) > 1e-12); est_support = find(abs(x_hat) > 1e-6); hit = length(intersect(true_support, est_support)); rel_err = norm(x_hat - x) / norm(x); fprintf('相对误差: %.3e\n', rel_err); fprintf('支撑集命中: %d/%d\n', hit, length(true_support));这里 M/N = 0.25,K = 20,满足 2K·log(N/K) ≈ 130 的水平,因此恢复通常能精确支撑集。如果减小 M 到 64,或者把 K 提到 40,支撑集命中率会明显下降,est_support里会出现落在真实位置之外的假原子。用find(abs(x_hat)>1e-6)而不是find(x_hat),是因为浮点误差会让理论上的零位置出现 1e-15 量级的小数。
3.4 K 被高估时的失效模式与残差阈值停止
一个常见失误是把 K 设得比真实稀疏度大很多。比如真实 K=10,却把参数设成 50,OMP 在选完 10 个真实原子后不会自动停止,而是继续挑残差里相关性最大的干扰原子,这些干扰原子在无噪声情况下通常对应数值噪声和浮点误差,在有噪声情况下直接变成过拟合。恢复结果虽然残差很小,但支撑集里混入了大量假位置。
避免办法是给循环加两个跳出条件:残差范数低于阈值,或前后两次残差下降率小于某个比例。将上一节的循环改为while iter <= K && norm(r) > tol,每次更新残差后判断一次。阈值 tol 的选择应参考观测噪声的范数,而不是一个固定的小数;在无噪声实验里可以设为 1e-6 或更小,在有噪声场景下按 ‖n‖₂×0.8 设置更为稳妥。
4. 图像压缩感知的分块 OMP 重建:块尺寸、测量率与伪影控制
4.1 分块测量为什么成为 OMP 图像重建的默认做法
把 OMP 直接用到整幅图像上会遇到两个障碍。假设图像有 256×256 像素,展成一维后有 65536 维,感知矩阵 Φ 变成 M×65536,M 哪怕只取 30% 也有接近两万行,一次 Φ^T r 的计算就要几分钟,MATLAB 里更是直接卡在内存分配上。分块处理的动机就是把高维问题拆成若干个低维独立子问题:图像切成不重叠的 8×8 或 16×16 小块,每块展成 P 维,共享同一个 P×P 感知矩阵,逐块做测量和恢复。
分块还有一层实际好处:自然图像的局部结构相关性高,小块内部的稀疏表示往往比整幅图像更容易满足。工程上常见的块大小是 8、16、32,块越小单次计算越轻,但块之间的相关性被切断,重建后容易出现可见的块状伪影;块越大恢复质量越好,计算开销也成倍上升。
4.2 分块 OMP 重建的 MATLAB 代码骨架
下面代码假设图像已经被划分成 num_blocks 个互不重叠的块,每块观测向量按列存入 y_all。
function img_rec = block_omp_recon(y_all, Phi, blk, img_sz, K) % block_omp_recon - 分块OMP图像重建 % y_all : (M*num_blocks) * 1,所有块的观测向量按块顺序排列 % Phi : M * (blk*blk),共享感知矩阵 % blk : 块大小,如8 % img_sz: 原始图像大小,[h, w] % K : 每块稀疏度 h = img_sz(1); w = img_sz(2); num_h = h / blk; num_w = w / blk; img_rec = zeros(h, w); idx = 1; for ii = 1:num_h for jj = 1:num_w yb = y_all(:, idx); xb = omp_solve(yb, Phi, K); img_rec((ii-1)*blk+1 : ii*blk, ... (jj-1)*blk+1 : jj*blk) = reshape(xb, blk, blk); idx = idx + 1; end end end这个函数本身没有做任何测量操作,只负责把观测向量按块顺序还原成图像。实际测量时也需要按同样的块顺序调用,比如对每个块先reshape成列向量,再计算Phi * x_col。所有块共享同一个 Φ 是允许的,因为每块内容不同,观测到的 y 也不同,不会因为矩阵重复而引入额外问题。
4.3 块大小与测量率对重建质量的影响
块大小不仅影响计算量,还直接决定稀疏表示的有效性。以 DCT 域稀疏为例,8×8 块展开 64 个系数,常见做法是保留 3~10 个大系数,对应 K 取 5 上下;16×16 块有 256 个系数,K 可以取到 20 左右,但测量率不变时,每块观测数 M 也要随之增加。
| 块大小 | 展开维度 P | 50% 采样时的 M | 常见 K 范围 | 重建伪影 |
|---|---|---|---|---|
| 8×8 | 64 | 32 | 3~8 | 块边界较明显 |
| 16×16 | 256 | 128 | 8~20 | 块效应减轻 |
| 32×32 | 1024 | 512 | 20~40 | 更平滑,但耗时成倍 |
测量率同样影响支撑集选择。测量率过低时,每块只有十几个观测值而待选原子有上百个,OMP 很难稳定区分真实原子和干扰原子。工程经验是先用 25% 测量率跑一遍,观察残差是否在 K 步内降到目标阈值;下降缓慢就提高测量率或缩小块尺寸,不要只加大 K。
4.4 图像重建中容易被误解的两个问题
第一,不要认为测量率越低越好。低于理论界时,OMP 的支撑集恢复不再有保证,图像看起来会像叠加了一层结构化的散粒噪声,这种噪声不是调 K 能解决的,只能增加 M 或改用带平滑约束的重建算法。
第二,K 不是图像的真实稀疏度,而是“你想保留多少重要系数”的工程参数。自然图像的 DCT 系数是无限长的尾巴,K 设得越小,高频细节丢失越多,图像越平滑;K 设得过大,恢复结果会在平坦区域出现颗粒状伪影。观察每块的残差范数分布能帮助判断:如果多数块的残差在一个量级,只是少数边缘块残差特別大,说明 K 对大多数块是合适的,边缘块可以单独提高 K。
5. OMP 家族的派别与残差曲线验证技巧
5.1 OMP 的几个派别:OOMP、gOMP 与稀疏度扩展
OMP 本身是一个框架,围绕“选原子 + 正交化”可以衍生出多个派别。OOMP(正交优化匹配追踪)在每次迭代时不仅考虑当前残差与单个原子的相关性,还会对候选原子做一次最小二乘预评估,选能使残差下降最大的原子入支撑集;它比 OMP 多几十微秒的预计算,但在小测量数场景下支撑集选择更准。gOMP 则相反,每轮直接选 L 个相关性最高的原子一起入支撑集,把迭代次数压缩到 K/L,适合大规模支撑集快速搜索,代价是中间可能出现冗余原子需要后续剔除。CoSaMP 更进一步,融合了多轮支撑集合并和裁剪,理论上对带噪信号更鲁棒,但实现复杂度高出一个量级。
这些派别没有绝对优劣。信号足够稀疏、M 不大的情况下,OMP 仍是解释性最好、最容易调试的基准算法;需要考虑实时性或者支撑集特别大时,gOMP 的批量选择思路更值得优先尝试。
5.2 用残差下降曲线做恢复验证
我每次实验都会顺带记录残差范数,而不是只看最终误差。改进的 OMP 函数里加一行res_history(iter) = norm(r);,之后用semilogy(res_history)绘图。正常的收敛曲线应当是平滑下降,最后趋于平缓;如果曲线在中途出现上升或者长时间平台,说明要么支撑集选错,要么 K 超出了可恢复范围。把这段曲线和最终的hit指标放在一起看,比单独看一个误差数字更有诊断价值。
5.3 大矩阵下的提速技巧:预计算 Gram 矩阵与增量 QR
当 N 到几万规模时,每次迭代重新计算 Phi'*r 的代价会超过选原子本身。一个标准优化是预计算 Gram 矩阵 G = Phi'*Phi 和投影向量 u = Phi'*y,然后利用
Phi'*r = u − (Phi'*Phi) * alpha_active,
将相关计算从 O(MN) 降到 O(NK)。代价是把感知矩阵的内存占用从 O(MN) 换成了 O(N²),N 超过 5 万时反而得不偿失。另一个更通用的做法是在选原子时保持增量 QR 分解,每次新增一列只需要做一次 Givens 旋转,残差和系数都能在 O(MK) 内更新,省去反复调用\。跑大规模实验时,先用普通 OMP 绘制稀疏度-误差曲线确定 K 的大致区间,再针对该区间做上述优化,比盲目把参数调大要有用得多。
本文还有配套的精品资源,点击获取