
1. 从一张糊图说起为什么成像逆问题需要生成先验如果你拍过夜景、做过显微重建或者处理过遥感图像大概率遇到过这种场景手里的观测数据少得可怜噪声还大但你要反推出一张干净、细节丰富的高质量图像。这就是典型的成像逆问题——已知退化过程模糊核、降采样矩阵、编码掩膜等要从观测值反解原始信号。传统解法无非两类。一类是解析法比如维纳滤波、Tikhonov正则化思路是把逆问题写成最小二乘加一个手工设计的正则项求解快但先验太弱结果往往偏平滑、丢纹理。另一类是迭代法比如TV正则、稀疏编码靠人工设计的稀疏先验去约束解空间效果比解析法好但先验表达能力有限遇到复杂自然图像就力不从心。问题的根子在于逆问题本身是病态的观测数量远小于未知量解不唯一。你必须注入额外的先验知识才能挑出那个“合理”的解。而手工设计的先验无论是全变分、小波稀疏还是字典学习本质上都是对自然图像统计规律的一种粗糙近似。它们能描述“图像应该是分段平滑的”或者“图像在小波域是稀疏的”但描述不了“人脸应该有两只眼睛、一个鼻子”这种高层语义结构。扩散模型的出现改变了这个局面。它在大规模图像数据上训练学到的生成先验极其强大——你给它一个纯噪声它能一步步去噪生成一张逼真的图。这意味着它隐式地掌握了自然图像的深层分布。那么一个自然的想法就冒出来了能不能把这个生成先验拿来解逆问题让扩散模型在去噪的过程中同时受到观测数据的约束最终生成一个既逼真又符合观测的解这就是**扩散后验采样Diffusion Posterior Sampling简称DPS**要解决的核心问题。DPS的核心贡献在于它给出了一种优雅的方式把扩散模型的去噪过程和逆问题的数据保真项结合起来不需要针对每个逆问题重新训练模型也不需要知道退化算子的显式形式就能实现高质量的图像重建。这篇文章适合谁看如果你正在做计算成像、图像恢复、医学影像重建或者对扩散模型的应用感兴趣想搞清楚“生成先验怎么跟逆问题结合”这件事那接下来的内容应该对你有用。我会从设计思路、核心原理、实操细节到常见问题把DPS这套方法拆开讲清楚。2. DPS的整体设计思路为什么是“后验采样”而不是“投影”2.1 逆问题的贝叶斯视角先把问题形式化。成像逆问题可以写成$$y \mathcal{A}(x) n$$其中 $x$ 是真实图像$\mathcal{A}$ 是已知的退化算子比如模糊、降采样、掩膜编码$n$ 是噪声$y$ 是观测。我们的目标是从 $y$ 恢复 $x$。从贝叶斯角度看我们想要的是后验分布 $p(x|y)$。有了后验就能取最大后验估计MAP或者后验均值作为重建结果。根据贝叶斯公式$$p(x|y) \propto p(y|x) \cdot p(x)$$这里 $p(y|x)$ 是似然项描述观测数据对解的约束$p(x)$ 是先验项描述我们对自然图像的认知。传统方法的问题就出在 $p(x)$ 上——手工设计的先验太弱。扩散模型恰好提供了一个强大的 $p(x)$。它通过前向加噪过程把图像逐步破坏成噪声再训练一个网络学习反向去噪过程。训练完成后这个反向过程就隐式地定义了 $p(x)$ 的采样器。2.2 为什么不能简单地在扩散采样中做投影最朴素的想法是在扩散模型的每一步去噪之后把结果往观测数据上“投影”一下比如做一次数据一致性修正。这种做法在早期工作中确实出现过但问题很明显。扩散采样是一个逐步去噪的过程每一步的输出都带有随机性而且中间步骤的图像并不是最终解它只是去噪轨迹上的一个中间状态。如果你在中间步骤强行做投影相当于用观测数据去“纠正”一个还没成型的中间结果很容易破坏扩散模型本身的采样动力学导致最终结果要么不真实要么不满足观测。更关键的是投影操作通常假设退化算子是可逆的或者容易求伪逆的。对于复杂的退化比如非线性编码、未知噪声水平投影根本没法直接做。2.3 DPS的核心洞察用Tweedie公式搭桥DPS的巧妙之处在于它不去强行投影而是利用扩散模型去噪器与后验得分之间的数学联系把数据保真项自然地融入到采样过程中。具体来说扩散模型在每一步去噪时实际上是在估计当前噪声水平下的干净图像。这个估计可以通过Tweedie公式与得分函数联系起来。而得分函数又恰好是后验分布梯度的组成部分。DPS利用这个关系推导出一个近似的后验得分表达式从而在每一步采样中同时考虑生成先验和观测数据。这样做的好处是不需要重新训练模型不需要知道退化算子的显式形式只需要能计算 $\mathcal{A}(\hat{x})$ 和它的梯度而且适用于各种线性甚至非线性逆问题。一套框架多个任务通用这是DPS最吸引人的地方。3. 核心原理拆解DPS到底在每一步做了什么3.1 扩散模型的前向与反向过程回顾先快速过一遍扩散模型的基础。前向过程是一个马尔可夫链逐步向图像加高斯噪声$$x_t \sqrt{\bar{\alpha}_t} x_0 \sqrt{1 - \bar{\alpha}_t} \epsilon, \quad \epsilon \sim \mathcal{N}(0, I)$$其中 $\bar{\alpha}_t$ 是累积噪声系数$t$ 从0到T。当T足够大时$x_T$ 近似为标准高斯噪声。反向过程则是从 $x_T$ 出发逐步去噪得到 $x_0$。训练时网络 $\epsilon_\theta(x_t, t)$ 学习预测当前噪声。采样时利用这个预测噪声来估计干净图像$$\hat{x}_0 \frac{x_t - \sqrt{1 - \bar{\alpha}t} \epsilon\theta(x_t, t)}{\sqrt{\bar{\alpha}_t}}$$这个 $\hat{x}_0$ 就是扩散模型对当前步骤干净图像的估计也是DPS中用来计算数据保真项的关键量。3.2 后验得分的分解我们真正想要的是后验分布 $p(x_t|y)$ 的得分函数 $\nabla_{x_t} \log p(x_t|y)$。根据贝叶斯公式$$\nabla_{x_t} \log p(x_t|y) \nabla_{x_t} \log p(x_t) \nabla_{x_t} \log p(y|x_t)$$第一项是先验得分扩散模型已经学到了。第二项是似然得分描述观测数据对当前噪声图像的影响。难点在于 $p(y|x_t)$ 的计算。因为 $x_t$ 是带噪的中间状态而观测 $y$ 是针对干净图像 $x_0$ 的。DPS做了一个近似用扩散模型估计的 $\hat{x}_0$ 来代替 $x_0$然后假设$$p(y|x_t) \approx p(y|\hat{x}_0)$$在噪声水平较低时这个近似是合理的。进一步假设观测噪声是高斯噪声那么$$\log p(y|\hat{x}_0) \propto -|y - \mathcal{A}(\hat{x}_0)|^2 / 2\sigma^2$$于是似然得分就可以近似为$$\nabla_{x_t} \log p(y|x_t) \approx -\frac{1}{\sigma^2} \nabla_{x_t} |y - \mathcal{A}(\hat{x}_0)|^2$$这个梯度可以通过链式法则计算因为 $\hat{x}_0$ 是 $x_t$ 的函数。3.3 采样更新公式把先验得分和似然得分合起来DPS的采样更新可以写成$$x_{t-1} \text{DDIM_step}(x_t, \epsilon_\theta) - \zeta_t \nabla_{x_t} |y - \mathcal{A}(\hat{x}_0)|^2$$其中 $\zeta_t$ 是一个步长系数控制数据保真项的强度。DDIM_step是标准的扩散采样步骤负责去噪后面那一项则是数据一致性修正把采样轨迹往满足观测的方向拉。这个公式看起来简单但里面有几个关键细节需要处理好否则效果会大打折扣。3.4 步长系数的选择$\zeta_t$ 的选择直接影响重建质量。如果太大数据保真项会主导采样过程生成结果可能不真实如果太小观测约束不够重建结果可能偏离真实解。DPS原文给出的建议是$$\zeta_t \frac{\zeta}{|\nabla_{x_t} |y - \mathcal{A}(\hat{x}_0)|^2|}$$也就是用梯度的范数做归一化让每一步的修正幅度相对稳定。$\zeta$ 是一个全局超参数通常需要根据具体任务调。经验上$\zeta$ 在0.1到1.0之间比较常见噪声大的任务可以适当调大。注意这个归一化很重要。如果不做归一化梯度范数在不同步骤可能差几个数量级导致采样过程不稳定。4. 实操过程从零实现一个DPS采样器4.1 环境准备与依赖我平时用PyTorch做这类实验扩散模型部分可以直接用预训练的DDPM或DDIM模型。以下是一个最小化的依赖列表pip install torch torchvision numpy scipy如果你要用现成的扩散模型权重可以加载OpenAI的guided-diffusion或者HuggingFace的diffusers库。我这里为了讲清楚原理会写一个简化版的DDIM采样器方便你理解每一步在做什么。4.2 退化算子的定义不同的逆问题对应不同的 $\mathcal{A}$。我整理了几个常见任务的算子实现方式任务退化算子 $\mathcal{A}$备注图像去模糊卷积高斯核/运动核需要padding保持尺寸超分辨率降采样矩阵可用avg_pool或bicubic图像修复掩膜乘法缺失区域置零压缩感知随机投影矩阵需要固定随机种子相位恢复傅里叶幅值非线性需特殊处理以超分辨率为例退化算子可以写成import torch import torch.nn.functional as F def downsample_operator(x, scale_factor4): return F.avg_pool2d(x, kernel_sizescale_factor, stridescale_factor)对应的梯度计算需要用到自动微分后面会讲。4.3 核心采样循环下面是DPS采样循环的伪代码我加了详细注释def dps_sample(model, y, A, A_grad, num_steps1000, zeta0.3): model: 预训练扩散模型输入(x_t, t)输出预测噪声 y: 观测数据 A: 退化算子 A_grad: 退化算子的梯度函数 num_steps: 采样步数 zeta: 数据保真项步长 # 初始化从纯噪声开始 x torch.randn_like(y) # 注意这里需要根据任务调整尺寸 for t in reversed(range(num_steps)): t_tensor torch.full((x.shape[0],), t, devicex.device) # 第一步扩散模型预测噪声 with torch.no_grad(): eps_pred model(x, t_tensor) # 第二步估计干净图像 alpha_bar_t get_alpha_bar(t) x0_hat (x - torch.sqrt(1 - alpha_bar_t) * eps_pred) / torch.sqrt(alpha_bar_t) # 第三步计算数据保真项梯度 x0_hat x0_hat.detach().requires_grad_(True) residual y - A(x0_hat) loss torch.sum(residual ** 2) grad torch.autograd.grad(loss, x0_hat)[0] # 第四步DDIM去噪步骤 x ddim_step(x, eps_pred, t) # 第五步数据一致性修正 grad_norm torch.norm(grad) 1e-8 x x - zeta * grad / grad_norm # 可选对x做clip防止数值溢出 x torch.clamp(x, -1, 1) return x这段代码里几个关键点值得展开说。关于x0_hat的detach和requires_grad我们需要对x0_hat求梯度但不想让梯度回传到扩散模型参数上所以先detach再开requires_grad。这样计算图只保留x0_hat到loss的部分节省显存。关于DDIM_step这是标准的DDIM更新根据当前x和预测噪声计算下一步的x。具体实现可以参考DDIM原文核心是用确定性采样替代DDPM的随机采样步数可以大幅减少。关于梯度归一化我用了L2范数归一化这是DPS原文的做法。你也可以尝试其他归一化方式比如用梯度的标准差效果可能因任务而异。4.4 参数选择与调优经验跑通代码只是第一步真正影响效果的是参数调优。我整理了几个关键参数的经验值参数推荐范围影响调优建议采样步数100-1000步数越多越精细但越慢先用100步快速验证再增加到500zeta0.1-1.0越大数据保真越强噪声大时调大噪声小时调小噪声水平根据任务影响似然近似精度可估计或作为超参调初始噪声随机影响结果多样性固定种子可复现我实测下来zeta这个参数最敏感。在超分辨率任务上zeta0.3左右比较稳在去模糊任务上zeta0.5效果更好。建议你先固定其他参数单独扫zeta画一条PSNR随zeta变化的曲线找到峰值。实操心得如果重建结果出现明显的伪影或者不自然纹理通常是zeta太小数据保真不够如果结果过于平滑、丢失细节可能是zeta太大生成先验被压制了。5. 常见问题与排查技巧实录5.1 重建结果全是噪声或完全偏离这是最常见的问题通常有几个原因。原因一退化算子实现有误。检查A(x)的输入输出尺寸是否匹配卷积的padding是否正确降采样的scale是否对得上。我踩过一次坑avg_pool的kernel_size和stride设成不一样导致尺寸对不上梯度计算全乱。原因二alpha_bar的调度不对。扩散模型的噪声调度必须和训练时一致。如果你用的是预训练模型一定要用模型对应的alpha_bar序列不能自己随便设。原因三zeta太大导致发散。先把zeta调到0.01试试如果结果正常了说明是步长问题。然后再慢慢往上加。5.2 重建结果模糊、细节丢失这种情况通常是数据保真项太强把生成先验压制了。可以尝试减小zeta增加采样步数检查噪声水平估计是否偏大噪声水平越大似然项权重越小但DPS的近似可能不准另一个可能的原因是扩散模型本身的能力不够。如果你用的是在CIFAR-10上训练的模型去重建ImageNet图像效果肯定好不了。生成先验的覆盖范围要和任务匹配。5.3 采样速度太慢DPS的主要计算开销在两块扩散模型前向和梯度计算。扩散模型前向没法省但梯度计算可以优化。用DDIM替代DDPM步数从1000降到100梯度计算时只对x0_hat求导不要对整个计算图求导如果退化算子是线性的可以解析计算梯度不用自动微分我实测过在单张RTX 3090上256x256图像、100步DDIM、超分辨率任务一次重建大约需要15秒。如果降到50步可以压到8秒左右质量损失不大。5.4 不同任务的效果差异DPS不是万能的不同逆问题上的表现差异很大。根据我的经验任务DPS效果注意事项超分辨率很好降采样算子简单梯度稳定去模糊好需要处理边界效应图像修复很好掩膜算子简单适合DPS压缩感知中等随机投影矩阵条件数大需要调参相位恢复较差非线性强似然近似误差大相位恢复这类非线性强的任务DPS的近似假设不太成立效果会打折扣。这时候可能需要考虑更复杂的后验采样方法或者针对具体任务做改进。5.5 常见问题速查表现象可能原因排查步骤解决方法结果全噪声算子错误/调度错误检查A(x)尺寸和alpha_bar修正算子对齐调度结果模糊zeta太大扫zeta找峰值减小zeta结果不真实zeta太小增大zeta增大zeta或增加步数采样发散梯度爆炸检查梯度范数加归一化减小zeta速度太慢步数太多换DDIM减少步数特定任务效果差近似不成立分析退化算子非线性程度考虑其他方法6. 几个值得深挖的扩展方向DPS打开了一扇门但远不是终点。我在实际项目中遇到过几个值得继续探索的方向这里分享出来供参考。第一个方向是改进似然近似。DPS用 $\hat{x}_0$ 代替 $x_0$ 来计算似然这在噪声水平低时还行但在早期采样步骤噪声很大时误差明显。有些后续工作尝试用更精确的近似比如考虑 $\hat{x}_0$ 的不确定性或者用多个采样估计。如果你在做高噪声任务这块值得花时间。第二个方向是自适应步长。目前zeta是全局固定的但不同步骤对数据保真项的需求不一样。早期步骤应该更依赖先验后期步骤应该更依赖观测。设计一个随t变化的zeta调度可能比固定值效果更好。我试过简单的线性衰减有一定提升但还没系统调优。第三个方向是结合任务特定先验。DPS用的是通用图像先验如果你做的是医学影像、遥感图像或者特定类型的自然图像可以在扩散模型微调时注入领域知识或者在采样时加入任务特定的约束。比如医学图像重建中可以加入解剖结构的约束。第四个方向是加速采样。100步DDIM还是慢对于实时应用不够。可以尝试蒸馏、一致性模型或者少步采样技术。不过加速往往会损失一些质量需要权衡。第五个方向是理论分析。DPS的近似到底在什么条件下成立误差上界是多少这些问题目前还没有完整的理论回答。如果你偏理论方向这是一个有价值的问题。7. 我在实际项目中的几点体会最后分享几个踩坑之后总结的经验都是文档里不会写的。关于噪声水平的估计。DPS假设观测噪声是已知的但实际中往往不知道。我通常的做法是先用一个小网络估计噪声水平或者把噪声水平也当作超参数用验证集调。如果噪声水平设得偏大重建结果会偏平滑设得偏小会保留更多噪声。这个参数对最终效果影响很大值得多花时间。关于初始化的选择。标准DPS从纯高斯噪声开始采样。但在某些任务中从观测的某种粗估计开始比如用传统方法先做一个初始重建然后加噪到某个中间步骤可以加速收敛效果也更好。这个技巧在超分辨率任务上特别有用。关于batch内的多样性。如果你一次重建多张图像每张从不同的随机噪声开始可以得到多个不同的解。这些解的均值通常比单个解更稳定方差则可以用来评估不确定性。这个特性在医学影像中很有价值医生可以看到重建的置信区间。关于计算资源的规划。DPS的计算量大约是普通扩散采样的1.5到2倍因为多了梯度计算。如果你要处理大批量数据显存和时间的预算要提前算好。我的经验是256x256图像、100步采样batch size4大约需要8GB显存。512x512的话batch size1就需要10GB以上。关于代码调试。DPS的调试比普通扩散模型麻烦因为涉及梯度计算和算子实现。我的建议是先用一个极简的退化算子比如恒等映射验证采样器本身是否正确。如果恒等映射下能重建出原图说明采样器没问题再逐步增加算子复杂度。这个自顶向下的调试策略帮我省了很多时间。这个方向后续还可以这样扩展把DPS和变分推断结合起来用变分分布去近似后验可能比单点估计更稳定或者把DPS用到视频重建中利用时间维度的先验再或者探索DPS在三维重建、计算摄影中的应用。生成先验和逆问题的结合才刚刚开始后面还有很多有意思的问题值得做。