D-AMP压缩感知恢复:从近似消息传递到深度去噪实践 📅 发布时间:2026/9/15 22:15:38 👁 浏览次数: 简介这是一份面向信号处理与压缩感知研究者的 MATLAB/Python 算法实现合集聚焦基于深度学习的图像与信号重建方法。包含 Learned D-AMP、D-AMP、D-VAMP、D-prGAMP 与 DnCNN 等主流算法的运行代码并附有基于 SURE 损失训练 DnCNN、Learned D-AMP 以及 Deep Image Prior U-net 的训练脚本适用于压缩感知成像、一维信号恢复、压缩相位检索等任务既可复现论文结果也可作为二次开发基线。压缩包共517个文件约81.27MB其中以258个 .m 脚本、26个 .py 文件为主同时包含19个 C 语言源文件、多个 mex 动态库、20个 PNG 图示及配套数据文件与说明文档便于在 MATLAB/Python 环境下直接运行或扩展。已有412人学习/下载适合具备一定深度学习与优化算法基础、希望快速上手 D-AMP 系列方法或将其应用于自身研究的高年级本科生、研究生与工程师。整体目录结构清晰从演示脚本到模型训练全覆盖可帮助读者高效搭建实验环境并深入理解算法原理。1. 为什么压缩感知恢复绕不开 D-AMP 这条线在压缩感知里从少于奈奎斯特准则要求的观测样本里重建高维信号过去十年最实用的两条路一条是迭代硬阈值另一条就是近似消息传递Approximate Message Passing, AMP。而 D-AMP 系列最大的不同是把「稀疏性」这个信号模型替换成「去噪器」你不需要知道信号在哪个字典下稀疏只要有一个好的高斯去噪器就能在测量矩阵满足一定条件时稳定恢复。标题里这个软件包把 D-AMP、D-VAMP、D-prGAMP 和 Learned D-AMP 以及 DnCNN 放到同一套代码里等于把散落在论文里的基线一次性收齐。对做压缩感知、相位恢复和图像重建的人来说这是比重新读三篇论文更快上手的对照实现。它解决两类问题一类是给固定测量矩阵下的 PSNR 收敛曲线做基线另一类是用来对比不同去噪先验对恢复质量的贡献。适合刚读完 AMP 基础公式、想在真实图像上验证效果的工程师和研究生。2. 从 D-AMP 到 D-VAMP消息传递背后的去噪桥接2.1 AMP 从稀疏先验到去噪先验的推广原始 AMP 算法来自 Donoho 等人对压缩感知的迭代阈值算法的分析核心是每一步用软阈值函数处理估计信号再通过 Onsager 校正项消除迭代干扰。D-AMP 的论文题目其实已经点破把软阈值换成任意一个具备良好高斯去噪能力的黑盒去噪器比如 BM3D、DnCNN就可以解决更一般的逆问题。代码里你经常会看到这样的循环# 伪代码示意 D-AMP 主循环 x_t x_prev A.T r_t # 信号估计更新 denoised denoiser(x_t, sigma_t) # 用当前噪声水平去噪 r_t y - A denoised # 残差更新 sigma_t np.sqrt(np.mean(r_t**2) sigma_prev**2) / np.sqrt(n)这段的逻辑是先让欠定方程的解落到测量值附近再用去噪器把信号拉回流形残差里剔除了 Onsager 项后噪声水平可以估计。参数A是测量矩阵y是观测值sigma_t是每一步估计出的残留噪声标准差它决定了去噪器的强度。新手最容易错的一点是看到denoiser是函数就直接用没有把sigma_t传给 DnCNN结果去噪强度恒定为初始值前面几步有效后面完全不收敛。D-AMP 的收敛性依赖于去噪器必须是接近 MMSE 的高斯去噪器。这就是为什么该软件包内置 DnCNN它本身就是针对高斯去噪训练的残差网络可以直接拿来当黑盒。你不需要重新训练只需要调用预训练权重传入噪声水平图即可。代码里通常用dncnn_denoiser包装一下内部把灰度图归一化到 0 到 1 之间再乘以sigma控制残差缩放。2.2 D-VAMP 和 D-prGAMP 的矩阵条件适配VAMPVector Approximate Message Passing解决的是 D-AMP 在一般测量矩阵下收敛窗口窄的问题。D-AMP 对矩阵要求近似右旋转不变如果测量矩阵是部分傅里叶或强相关的行D-AMP 的 Onsager 修正会失效残差方差估计会偏得离谱。D-VAMP 则在每次迭代里加了一个线性最小均方误差 (LMMSE) 步骤把矩阵的奇异值分布显式纳入计算。代码中 D-VAMP 的初始化通常要求传A和At两个算子而不是直接传矩阵因为要对任意线性算子计算A x和A.T r。D-prGAMP 则走的是广义近似消息传递 (GAMP) 的路线专门处理输出通道不是高斯的情况。比如你观测到的不是y Ax z而是y |Ax| z或者量化后的y sign(Ax)这时候 D-AMP 的残差更新就不适用了。D-prGAMP 把输出通道建模成概率分布在每次迭代里用期望值作为去噪器的输入。代码里需要额外指定观测似然函数通常是likelihood参数传入的是Gaussian、Quantized或Clipped等字符串再配一个y和噪声标准差。这里容易踩坑如果y是量化值且幅度远大于信号本身必须先做归一化否则 GAMP 的残差方差会发散。2.3 代码里denoiser参数怎么接打开这个软件包大概率会在utils或denoisers目录下看到统一的接口。常见做法是定义这样一个类class DnCNN: def __init__(self, model_path, devicecpu): # 加载预训练权重 self.net torch.load(model_path, map_locationdevice) self.net.eval() def __call__(self, x, sigma): # x 形状为 (B, 1, H, W)sigma 是标量或 (B,) noise_map torch.ones_like(x) * sigma / 255.0 # 输入拼接噪声图以匹配 DnCNN 的输入 out self.net(torch.cat([x, noise_map], dim1)) return torch.clamp(x - out, 0, 1)这里把sigma / 255.0是因为 DnCNN 在训练时噪声水平是按像素值 0 到 1 归一化后的标准差。很多复现失败就是因为直接传了原始像素域的标准差比如sigma25让网络认为噪声是 25而实际噪声是 0.1。注意最后的残差学习结构输出的是噪声图不是去噪后的清晰图所以要用x - out还原。torch.clamp是防止极端值影响下一步迭代。参数model_path指向的权重文件可能是.pth或.mat。如果是.mat多半是 MATLAB 训练后导出的读取时要注意维度顺序DnCNN 的卷积层权重是[输出通道, 输入通道, 高度, 宽度]MATLAB 导出的顺序常常反过来了需要用np.transpose调整。这个坑在后续章节我会再展开。3. 跑通包内最小示例数据、模型与 DnCNN 权重3.1 用 Python 调用 D-AMP 的解压入口先找一个能直接跑的入口通常是main.py或demo.py。最小调用方式一般包括加载图像、生成测量矩阵、调用恢复函数。下面是一段复现常见结构的代码import numpy as np from scipy.linalg import orth from utils import load_dncnn, comp_sensing from algorithms import damping img load_gray_image(data/set11/01.png, size128) m, n 2048, 128 * 128 A orth(np.random.randn(n, m)).T # 生成列归一化高斯矩阵 y A img.flatten() np.random.randn(m) * 0.005 denoiser load_dncnn(model_pathweights/dncnn.pth, devicecpu) rec damping(y, A, denoiser, max_iter50, sigma00.05, verboseTrue)注意orth(np.random.randn(n, m)).T生成的是一个 m 行 n 列的矩阵每一列归一化且相互正交。这样能保证A A.T接近单位阵这是 D-AMP 收敛的隐性条件。sigma0是初始噪声标准差我建议设成比真实噪声略大一点因为迭代第一步误差最大。damping函数内部会动态估计sigma所以这个值只是初始值不用精确。verboseTrue会打印每一轮 PSNR 和残差能量我第一次跑包时就是用这个输出判断收敛曲线的方向。3.2 DnCNN 去噪器与噪声水平图DnCNN 在这个包中的作用不是独立的图像去噪模块而是 D-AMP/D-VAMP 的插件。因此它的输入除了噪声图之外还有一张噪声水平图。噪声水平图可以是全图同一个值也可以是逐像素不同的估计。代码里最常见的是把sigma_t变成一个全图常数与含有噪声的图像在通道维拼接。DnCNN 的优势在于它的感受野比较大且残差学习容易训练所以去噪效果比 BM3D 更平滑但代价是 GPU 上的前向耗时更高。如果你在 CPU 上跑 128x128 图像每轮要 0.1 秒50 轮就需要 5 秒这个体感还好但如果图像是 512x512建议直接用 D-AMP 的batch_denoiser模式分块。一个需要注意的细节是DnCNN 在训练时用的是加性高斯白噪声对真实噪声的适应能力有限。如果你用这个包处理真实传感器噪声建议把sigma_t的下限设为 1e-3不要让它降到 0否则去噪器会在最后一轮退化成恒等映射。实际执行时很多包已经内置了sigma_min参数直接设成 0.005 即可。3.3 参数表迭代次数、步长、正则化下表汇总了我在调参时最常用的参数范围也是包内常见默认值的扩展参数常见默认值调参思路失败时看什么max_iter30-50先跑 30 轮看 PSNR 是否还在上升如果还在上升就加残差能量下降但 PSNR 不动说明去噪器太强sigma00.05设成观测噪声的 1.2-1.5 倍前 5 轮 PSNR 暴跌则初始值过大denoiser_strength1.0某些包允许调去噪器权重的缩放恢复结果过于平滑降低到 0.9 或 0.8tol1e-5用连续两次迭代残差之差定位达不到 tol 而退出检查verbose路径verboseFalse调试时打印每轮 PSNR、残差、sigma看不见中间值就无法判断发散点行内另一个容易忽略的参数是step_size或relaxation。D-VAMP 在两个去噪器之间有一个线性插值通常是 0.5表示去噪输出与上一次估计各取一半。我测试过当测量矩阵的条件数大于 10 时把relaxation降到 0.3 能明显稳定收敛代价是每轮增量变慢。D-prGAMP 里不存在这个参数但它多了一个channel_noise参数表示输出通道的噪声方差直接用np.var(y - A x_initial)初始化比较稳妥。4. 复现 PSNR 指标时最容易栽的 4 个坑4.1 测量矩阵归一化影响收敛方向在大多数 D-AMP 代码里测量矩阵 A 的生成方式看似自由实际上对结果影响极大。一个反例直接用np.random.randn(m, n)然后除以np.sqrt(m)这样矩阵各列范数约为 1但整体奇异值分布很宽导致残差方差估计偏向高估。你会看到 PSNR 前几轮上升然后突然塌陷到 10dB 以下。正确的做法是使用orth或scipy.linalg的hadamard生成正交基后再随机抽取若干行。如果坚持用高斯矩阵必须保证每列二范数等于 1并且A A.T的对角元远大于非对角元。如果你处理的是部分哈达玛矩阵D-VAMP 比 D-AMP 更合适因为 VAMP 不要求矩阵近似等距。我建议在包内先跑一次demo_dvamp.py它会输出测量矩阵的条件数如果条件数大于 10就不要再切回 D-AMP 了。矩阵归一化的另一方面是图像向量的缩放把图像像素值除以 255 还是保持 0 到 255直接影响 sigma 的量级。包内权重文件训练时使用的输入范围通常为 0 到 1所以我在所有 demo 脚本前都会强调先做归一化。4.2 噪声水平 σ 与真实噪声不一致D-AMP 系列最敏感的参数就是每一步的sigma_t。论文里这个值是通过残差能量递归估计的但实际代码在第一次迭代时如果初始sigma0设得太小去噪器会尝试去除并不存在的噪声导致信号被过度平滑。反过来如果sigma0太大去噪器几乎不起作用前几轮 PSNR 提升缓慢。我在复现时发现有一个稳定的技巧先用np.median(np.abs(y)) / 0.6745估计观测噪声的标准差再把这个值乘以 1.5 作为sigma0。这样能保证第一轮去噪强度略高后续估计值再逐渐回落。另外一个常见的坑是噪声水平图与图像尺寸不匹配。某些包的noise_map函数要求传入与图像相同尺寸的矩阵而有的只接受标量。如果你用批量去噪多个图像共享同一个sigma_t会导致去噪器对不同图像施加强度不同的噪声去除。正确做法是为每个样本生成一张(B, 1, H, W)的噪声图全部填充同一个sigma_t值。这一点在 DnCNN 的 forward 里如果写死torch.ones_like不会出错但换用torch.full必须指定 device否则 CPU/GPU 不一致会卡住。4.3 D-prGAMP 的非线性输出通道D-prGAMP 的一个重要特性是它可以直接处理量化观测。如果你从传感器获得的观测已经过 1-bit 量化那y的取值集合是{-c, c}。此时如果直接调用默认的Gaussian似然GAMP 算法会估计出一个非常小的残差方差并且输出值始终不匹配观测范围。包内通常提供一个likelihoodQuantized选项但你需要额外传入quant_step参数。这个参数不该用观测的绝对值范围去猜而应该用np.percentile(np.abs(y), 95)作为阈值。另一个与通道相关的是裁剪噪声例如观测值超过某个阈值就被截断这需要likelihoodClipped并把clip_lo和clip_hi传入。我自己的经验是 D-prGAMP 的收敛速度比 D-AMP 慢 2-3 倍因为它每次迭代需要计算输出通道期望的导数。如果脚本里没有显式调用torch.enable_grad可能导数返回全零算法会退化成固定点迭代PSNR 停在 8dB 左右。调试时可以在循环内打印grad_norm如果为 0说明通道模型没有建立梯度链路。4.4 内存和随机种子D-VAMP 在每轮迭代里会保留上一个估计值用于阻尼因此内存占用大约是 D-AMP 的 1.5 倍。当图像尺寸达到 256x256 时PyTorch 在 GPU 上运行 DnCNN 的前向计算会占用约 2GB 显存如果还打开了梯度检查显存会进一步翻倍。建议在调用去噪器时用torch.no_grad()包裹前向过程因为这个去噪器本身不需要训练梯度信息只会浪费显存。随机种子对结果的影响主要在测量矩阵的生成上。如果你不固定np.random.seed每次运行都会得到不同的矩阵和噪声导致 PSNR 曲线抖动。包内通常有--seed参数默认值建议设为 0 到 2 之间因为某些库在特定种子上会有边缘性的数值问题。表格对比一下不同算法的资源占用算法每轮去噪器调用次数额外内存占用适合矩阵类型D-AMP1无高斯、近似等距D-VAMP2保存上一步估计一般矩阵、部分傅里叶D-prGAMP1保存输出通道期望带非线性通道Learned D-AMP1需加载训练好的掩码与训练时相同分布5. 把 Learned D-AMP 改造成自己的恢复管线5.1 替换外部去噪器而不破循环Learned D-AMP 与 D-AMP 的唯一区别是每次迭代使用了不同的去噪器一个深度展开结构里的denoiser是训练出来的不是固定的 DnCNN。把这个包移植到自己的任务时最可行的改造是把denoiser换成你的图像先验网络比如基于 SwinIR 的复原网络。你不需要重写消息传递循环只要保持输入输出形状一致。关键点是网络输入需要同时接收噪声图和噪声水平图如果你的网络没有噪声图输入可以用一个简单的UNet把噪声水平和图像 concat 在一起。在替换后我把max_iter从 50 减小到 20因为更强的去噪器会在更少迭代内收敛。替换过程中要保证去噪器的输出范围在 0 到 1 之间如果最后的激活函数是tanh输出范围变为 -1 到 1你需要在外部加(out 1) / 2转换。否则残差更新和目标值量纲不匹配sigma_t估计会失真。另一个实用技巧是学习一个每步独立的步长参数具体做法是把阻尼系数relaxation从常数改成可学习的nn.Parameter在循环外定义优化器每个 epoch 只更新这个参数其他层冻结收敛速度会明显加快。5.2 结合快速采样和动量当测量数很少时即压缩比低于 0.1标准 D-AMP 需要 100 轮以上才能稳定。我常用的改造是在每轮之间加入动量x_t x_t momentum * (x_t - x_t_prev)。这个操作实现起来只有一行但能显著提升路径上的单调性。需要特别注意的是动量系数不要大于 0.9否则会在收敛点附近震荡。另外可以在每次迭代前对观测残差做一次投影切到小批量上适用于大规模测量矩阵。实际应用中把 D-AMP 用作图像插值任务时测量矩阵 A 可以定义成掩码采样矩阵。你需要为A和A.T分别写两个函数避免显式构建大矩阵。包内已经支持LinearOperator接口你只需实现matvec和rmatvec两个方法然后传给 D-AMP 循环。这种改造代码量不大但能让同样的算法处理非规则采样问题。改造后建议验证A.T A的对角元是否接近 1如果偏差超过 5%需要在残差更新里除以对角元作为归一化修正。5.3 验证修改正确性的最小闭环我在改完管线后不会直接跑完整数据集而是先用单张 64x64 的小图验证数值一致性。具体做法是固定一个随机种子连续跑两次算法这两次输入输出完全一致如果两次 PSNR 差值小于 1e-6说明代码没有随机扰动。这个检查能过滤掉大部分 device 不一致和数据未归一化的错误。然后我再逐步把图像尺寸调大对比 D-AMP 和 D-VAMP 的输出 PSNR 是否与论文趋势一致。如果 D-AMP 在部分傅里叶矩阵上发散但 D-VAMP 收敛那你的改造没有问题如果两者都发散就要回头检查测量矩阵的归一化条件。最后一步是验证去噪器输入的噪声水平图确实从高到低递减打印出前三个和后三个sigma_t的值。正常曲线应该呈现单调递减如果中途有反弹代表残差更新或 Onsager 修正计算有误。你可以把这段日志直接接到--verbose输出里形成自己的调试基线。本文还有配套的精品资源点击获取