欠定方程里藏着哪几个非零项:OMP 稀疏恢复侦探笔记

测量数少于未知数时,普通方程求解没有唯一答案;稀疏先验却能把嫌疑范围迅速缩小。本文以侦探推理方式拆解正交匹配追踪 OMP 的相关性筛选、最小二乘回归和残差更新,并给出不依赖第三方库的 Python 实现。

案发现场:方程比未知数少

四维信号只有两个位置非零,我们却只拿到三次线性测量。方程数量少于未知数,直接消元会留下无穷多解;但"只有两个非零项"是一条强线索。OMP 的做法像逐轮锁定嫌疑列:寻找与当前残差最相关的字典原子,把它加入支撑集,再让所有已选原子共同解释测量。

第一轮排查相关性

残差是当前模型尚未解释的证据。对每一列字典向量计算与残差的内积,绝对值越大,说明该列最能解释剩余方向。选中一列后不能只减去它的一次投影,因为新列与旧列可能不正交;必须在整个支撑集上重新求最小二乘,让系数共同调整。更新后的残差与已选列张成空间正交,这也是"正交匹配追踪"名称的来源。

为何必须重新做最小二乘

目标是 y=Ax,且 x 至多有 s 个非零项。第 t 轮计算 c=A^T r,选择未入集合且 |c_j| 最大的索引 j。令 S 加入 j,求 x_S=argmin ||A_S x_S-y||_2,再令 r=y-A_S x_S。当残差范数低于容差或已选择 s 列时停止。示例通过正规方程 A_S^T A_S x=A_S^T y 和高斯消元求解,适合小型教学数据;实际数值计算更应使用 QR 或 SVD,避免病态矩阵把舍入误差放大。

字典矩阵按行存储。transpose、matVec、dot 和 solve 构成最小线性代数工具箱。每轮先扫描所有列相关性,跳过已选索引;leastSquares 构造正规方程并调用带主元选择的高斯消元。最终把支撑集系数写回完整向量。测试信号为 2,0,-1,0,测量由程序现场生成,恢复结果的第一和第三项应接近真值,其余项接近零。

完整侦查程序

python 复制代码
import math


def transpose(a):
    return [list(col) for col in zip(*a)]


def dot(a, b):
    return sum(x * y for x, y in zip(a, b))


def mat_vec(a, x):
    return [dot(row, x) for row in a]


def solve(a, b):
    n = len(b)
    aug = [row[:] + [rhs] for row, rhs in zip(a, b)]
    for col in range(n):
        pivot = max(range(col, n), key=lambda r: abs(aug[r][col]))
        if abs(aug[pivot][col]) < 1e-12:
            raise ValueError("singular least-squares system")
        aug[col], aug[pivot] = aug[pivot], aug[col]
        scale = aug[col][col]
        aug[col] = [v / scale for v in aug[col]]
        for row in range(n):
            if row == col:
                continue
            factor = aug[row][col]
            aug[row] = [x - factor * y for x, y in zip(aug[row], aug[col])]
    return [aug[i][-1] for i in range(n)]


def least_squares(a, y, support):
    cols = [[a[r][c] for r in range(len(a))] for c in support]
    gram = [[dot(ci, cj) for cj in cols] for ci in cols]
    rhs = [dot(col, y) for col in cols]
    return solve(gram, rhs)


def omp(a, y, sparsity, tolerance=1e-10):
    n = len(a[0])
    if not 1 <= sparsity <= n:
        raise ValueError("invalid sparsity")
    residual = y[:]
    support = []
    coeff = []
    columns = transpose(a)
    for _ in range(sparsity):
        candidates = [(abs(dot(col, residual)), j) for j, col in enumerate(columns)
                      if j not in support]
        _, chosen = max(candidates)
        support.append(chosen)
        coeff = least_squares(a, y, support)
        estimate = [sum(a[r][c] * v for c, v in zip(support, coeff)) for r in range(len(a))]
        residual = [target - pred for target, pred in zip(y, estimate)]
        if math.sqrt(dot(residual, residual)) <= tolerance:
            break
    result = [0.0] * n
    for index, value in zip(support, coeff):
        result[index] = value
    return result, support


if __name__ == "__main__":
    matrix = [[1, 0, 1, 0], [0, 1, 1, 0], [1, 1, 0, 1]]
    truth = [2.0, 0.0, -1.0, 0.0]
    measurement = mat_vec(matrix, truth)
    recovered, support = omp(matrix, measurement, 2)
    assert set(support) == {0, 2}
    assert all(abs(a - b) < 1e-8 for a, b in zip(recovered, truth))
    print(recovered, support)
    print("omp tests passed")

每轮要花多少代价:复杂度分析

设 A 为 m×n,稀疏度 s。每轮相关性扫描 O(mn),第 t 轮最小二乘用当前朴素实现约 O(mt²+t³),总成本主要为 O(smn+s⁴),状态空间 O(mn+s²)。使用增量 QR 可降低重复求解成本,但会增加实现复杂度。

证据不足与重复嫌疑:边界条件

  • s 必须在一到 n 之间。
  • 零测量应直接得到全零向量或在残差阈值处停止。
  • 两列完全相同会导致支撑不可辨识。
  • 正规方程奇异时必须报告失败,不能除以接近零的主元。
  • 容差过小会追逐浮点噪声,过大又会提前停止。

常见误判记录:常见错误

  • 选中原子后只固定旧系数,不重新最小二乘。
  • 用带符号最大相关性而不是绝对值。
  • 允许同一索引被重复加入支撑集。
  • 把恢复误差全归因于算法,忽略字典列归一化和条件数。

重放案件的测试:可复制的测试用例

程序输出恢复向量、支撑索引和 omp tests passed。断言允许 1e-8 浮点误差,并确认支撑集合恰好为第零与第二列。还可以把测量改为全零检查提前终止,或复制一列字典制造奇异案例,验证实现会明确抛错而不是返回无穷值。

从教学样例到测量系统

真实测量中应先归一化字典列,否则范数较大的列仅凭尺度就获得更高相关性。噪声存在时,停止条件通常结合已知噪声能量,而不是固定稀疏度。需要同时记录残差范数曲线、每轮选中索引与最小二乘条件数;只保存最终系数,会失去定位误选发生在哪一轮的证据。

进一步复核

若两列高度相关,第一轮的微小噪声就可能改变选择。互相关矩阵是重要的案前资料,最大非对角元素越接近一,支撑恢复越不可靠。

正规方程会把条件数平方,因此本文实现只用于展示流程。工程代码应优先调用经过验证的线性代数库,并在异常时输出条件数或秩,而非静默返回大系数。

知道稀疏度并不等于必须跑满 s 轮。残差已经落入噪声水平后继续选择,只会把噪声解释成信号;停止规则应与测量误差模型一起确定。

第一轮排查相关性之后的专项复盘

列归一化决定嫌疑排序是否公平

相关性内积同时受方向和列范数影响。一列若整体放大十倍,即使与残差方向并不更接近,也可能因尺度占优首先被选中。标准做法是在追踪前把每列归一化,并在恢复后把系数换回原量纲。若零列存在,归一化必须明确拒绝。测量矩阵来自不同传感器时,还要记录每列的物理单位,否则所谓稀疏系数可能只是单位选择的产物。

残差曲线是一份过程证据

理想无噪声案例中,每轮加入正确原子后,残差范数应单调不增,并在选满真实支撑后接近零。若某轮残差几乎不降,可能选中了高度相关的替代列;若系数突然变得极大,可能是支撑子矩阵病态。程序可在每轮保存索引、相关性、系数、残差范数和条件数估计。这样最终恢复失败时,可以区分第一轮误选与后续数值求解失稳,而不是笼统地说 OMP 不适合。

与暴力小规模真值对照

在 n 很小时,可以枚举所有大小不超过 s 的支撑集,对每个集合做最小二乘,取残差最小者作为对照。暴力法不适合生产规模,却是验证贪婪实现的有力裁判。测试应包含正交字典、相关字典、含噪测量和真实稀疏度小于上限四组数据。若 OMP 与枚举解不同,不一定表示代码错误,但必须检查两者残差和支撑可辨识性,解释贪婪选择在哪一步失去全局最优。

重放案件的测试的验证矩阵

  • 验证矩阵 1:构造最小输入,把"s 必须在一到 n 之间。"设为通过契约;随后故意模拟"选中原子后只固定旧系数,不重新最小二乘。"。测试需要同时记录返回值、关键状态和终止位置,不能只凭程序没有异常就判定通过。这一项应单独运行,也应与前后正常操作组合,防止局部正确掩盖状态污染。
  • 验证矩阵 2:固定执行顺序,把"零测量应直接得到全零向量或在残差阈值处停止。"设为通过契约;随后故意模拟"用带符号最大相关性而不是绝对值。"。测试需要把期望结果写成独立断言,并在失败时打印触发分支所需的最短上下文。这一项应单独运行,也应与前后正常操作组合,防止局部正确掩盖状态污染。
  • 验证矩阵 3:放大数据规模,把"两列完全相同会导致支撑不可辨识。"设为通过契约;随后故意模拟"允许同一索引被重复加入支撑集。"。测试需要分别观察正确性与资源曲线,避免性能变化掩盖已经出现的语义偏差。这一项应单独运行,也应与前后正常操作组合,防止局部正确掩盖状态污染。
  • 验证矩阵 4:注入一次错误,把"正规方程奇异时必须报告失败,不能除以接近零的主元。"设为通过契约;随后故意模拟"把恢复误差全归因于算法,忽略字典列归一化和条件数。"。测试需要确认错误能被测试稳定捕获,再恢复实现验证用例不会产生偶然通过。这一项应单独运行,也应与前后正常操作组合,防止局部正确掩盖状态污染。
  • 验证矩阵 5:重放完整状态,把"容差过小会追逐浮点噪声,过大又会提前停止。"设为通过契约;随后故意模拟"选中原子后只固定旧系数,不重新最小二乘。"。测试需要使用相同输入重复运行,检查结果、排序规则和日志字段是否保持可复现。这一项应单独运行,也应与前后正常操作组合,防止局部正确掩盖状态污染。

结案报告:总结

OMP 的推理链只有三步:相关性找候选、支撑集上重估全部系数、用新残差继续。它用稀疏先验把欠定问题变得可解,但前提是字典列足够可区分,数值求解也足够稳定。

相关推荐
zjnlswd2 小时前
C#学习笔记4
笔记·学习·c#
青山是哪个青山3 小时前
LangChain 学习笔记(九):上下文与记忆
笔记·学习·langchain
Z5998178414 小时前
c#软件开发学习笔记--Modbus-TCP/UDP网口通讯
笔记·学习·c#
阿里巴巴首席技术官6 小时前
目标检测基础
笔记·yolo
TJHHH.6 小时前
SQL注入学习总结
数据库·笔记·sql·学习·注入
自小吃多6 小时前
Capture软件原理图添加元器件笔记
笔记·嵌入式硬件
TJHHH.7 小时前
文件包含漏洞
笔记·安全·文件包含
zjnlswd7 小时前
C# 学习笔记
笔记·学习
远离UE47 小时前
UE5 显存 虚拟内存 深入学习笔记
笔记·学习·ue5