news 2026/10/20 7:03:37

扩散后验采样DPS:用扩散模型破解计算成像逆问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
扩散后验采样DPS:用扩散模型破解计算成像逆问题

计算成像这个方向这几年确实被扩散模型带起了一波大节奏。过去我们解决成像逆问题,翻来覆去就是正则项、稀疏先验、学习型去噪器那一套,效果有上限,遇到强病态问题时解出来总是不够干净。这一讲要聊的扩散后验采样(DPS),算是把扩散模型当生成先验直接扔进逆问题求解框架里的一种代表性做法。理解它之后,你会发现相位恢复、超分辨率、加速磁共振成像这类问题,写法突然变得很统一,求解过程也变得有点像在采样,而不是在优化。

这篇内容我尽量不讲空话,直接拆解DPS的原理动机、公式推导的关键步骤、代码落地时的工程细节,以及我自己实际调参时踩过的一堆坑。适合正在做计算成像算法、图像重建,或者对扩散模型做条件生成感兴趣的读者。

1. 逆问题、先验与扩散模型:这一讲到底在解决什么

1.1 计算成像逆问题的统一框架

任何成像系统都可以抽象成一个正问题:

y = A(x) + n

x是潜在的真实图像,y是观测到的测量数据,A是系统对应的物理测量算子,n是噪声。相位恢复里A是傅里叶变换后取模,超分辨率里A是下采样加模糊,压缩感知里A是随机测量矩阵,去模糊里A就是卷积核。所谓逆问题,就是从y反推x。

问题在于,A要么是欠定的,要么是病态的。换句话说,可观测到的信息量远小于图像本身的信息量,直接做逆变换或者在最小二乘意义下求解,结果会被噪声放得面目全非。这时候就需要引入先验,也就是我们脑中对“正常图像长什么样”的假设。

经典的解法是写成变分模型:

min_x || y - A(x) ||^2 + λ R(x)

R(x)是正则项。TV正则假设图像分片光滑,稀疏正则假设图像在某个字典下稀疏。假设对不对,决定了解的质量高不高。实际工作中你会发现,这些手工先验的表达能力有限,能保住边缘,但恢复不了复杂的纹理,更别提细节和语义之间的关联。

1.2 从正则先验到生成先验

生成先验的思路是:既然我们已经有了大规模图像数据,能不能直接用神经网络学到的分布当作先验?GAN先验训一个生成器G,希望x满足x = G(z),然后通过优化潜在变量z去匹配测量。这个方法能产生很逼真的人脸,但对z的优化是非凸的,容易卡在不好的潜在编码上,而且GAN的训练脆弱,覆盖的数据模式有限。

标准化流能把潜在变量和图像之间做成可逆映射,从而算出精确似然,但网络结构约束多,表达能力也打了折扣。

扩散模型的登场改变了不少事情。它不直接建模x的显式密度函数,而是通过一个逐步加噪和逐步去噪的过程,拟合出数据的得分函数,也就是∇log p(x)。这个得分函数直接给出了朝向数据高概率区域的梯度方向,天然适合用来做“先验修正”。

1.3 扩散模型为什么特别适合当先验

先简单回忆扩散模型在做什么。前向过程把一张干净图像x0一步步加噪,直到变成纯高斯噪声xT:

q(x_t | x_0) = N(x_t; sqrt(α_bar_t) x0, (1 - α_bar_t) I)

α_bar_t是随t从1衰减到0的系数。反向过程训练一个噪声预测网络ε_θ(x_t, t),去预测每一步加的噪声。训练完成后,从随机噪声出发迭代去噪,就能生成新图像。

这跟成像逆问题的关联有三点很妙:

第一,扩散模型本身就在做“去噪恢复”,它的迭代结构天然匹配逆问题求解的迭代过程。第二,扩散模型能给出中间状态x_t对应的清晰图像估计,这一点在后面做条件引导时非常关键。第三,它已经在大规模图像上预训练好了,我们不需要为每一种成像系统重新训练一个模型,而是把测量算子A作为条件塞进去,这在实际使用中省了太多功夫。

我个人的理解是,扩散模型相当于把“图像先验”这份知识打包成了一个黑盒函数,你给它一个带噪图,它告诉你哪个方向是更接近真实图像的方向。DPS的核心,就是把这个黑盒函数接到贝叶斯后验采样的框架里。

2. DPS的建模范式:后验梯度的可计算近似

2.1 条件采样的贝叶斯拆分

目标不再是无条件生成图像,而是从后验分布p(x | y)中采样。贝叶斯公式给出:

log p(x | y) = log p(x) + log p(y | x) - log p(y)

其中log p(y)与x无关,可以忽略。于是条件采样只需要知道两件事:数据先验的得分∇log p(x),以及测量似然项的梯度∇log p(y | x)。

无条件扩散模型能提供第一项的梯度,即噪声预测网络输出的噪声直接对应得分函数:

∇_{x_t} log p_t(x_t) ≈ -ε_θ(x_t, t) / sqrt(1 - α_bar_t)

但第二项有问题。p(y | x)是关于干净图像x的似然,可采样过程是在带噪状态x_t上进行的。我们需要的是p(y | x_t)的梯度,而x_t是加了大量噪声的中间状态,与y之间的关系隔着一条前向扩散链,直接计算几乎不可能。

这里插一句,为什么不能直接以x_t作为自变量写p(y | x_t) = N(A(x_t), σ²I)?因为x_t是带噪的,不是图像本身,把测量算子作用在x_t上毫无物理意义。DPS的关键一步就是绕过这个问题。

2.2 用去噪估计近似中间状态

DPS的核心近似是:先用扩散模型当前对x_t的预测,得到干净图像的估计:

x̂_0 = (x_t - sqrt(1 - α_bar_t) * ε_θ(x_t, t)) / sqrt(α_bar_t)

这个x̂_0可以理解为从x_t这个带噪状态恢复出来的“当前置信的干净图像”。然后用它来近似p(y | x_t):

p(y | x_t) ≈ N(y; A(x̂_0), σ_y² I)

于是似然梯度变为:

∇_{x_t} log p(y | x_t) ≈ - (1 / σ_y²) * ∇_{x_t} || y - A(x̂_0) ||²

这个梯度能通过反向传播计算出,因为x̂_0是ε_θ输出的函数,而ε_θ是神经网络,所以从x_t到损失值构成了一个可微的计算图。算法每次采样步,除了做无条件去噪之外,还要额外回传一次梯度,更新方向被拉向满足测量约束的区域。

我在第一次看到这个近似时,第一反应是“这也太粗暴了吧”。但实践下来,它真的能work,而且效果不错。背后的原因后面讲。

2.3 这种近似为什么管用

理解这个近似可以从两个角度。

一是从得分函数的语义角度看。扩散模型在迭代早期主要恢复低频结构和轮廓,晚期才填充高频细节。在中间状态x_t,x̂_0代表的是在当前噪声水平下对真实x0的最优估计,平均意义上是接近真实后验均值的。用这个估计去计算似然,相当于把测量算子A作用在一个“可信的中间结果”上,这个中间结果已经包含了生成先验的信息,因此梯度方向既服从测量,也服从图像流形。

二是从泰勒展开的角度看。如果x̂_0是x0的无偏估计(在MMSE意义下近似成立),那么损失||y - A(x̂_0)||²相对x_t的梯度,其实是对真实后验得分的一个一阶近似。严格分析需要假设网络误差有界,实际中只要扩散模型训得够好,误差项带来的干扰就会被后续的去噪步骤逐步吸收。

我在实验中感受到,DPS最明显的优点是稳定。它没有像GAN先验那样需要精心设计潜在空间的初始化,也不像一些投影方法那样在每次采样后硬切到测量一致性集合,产生振铃伪影。它只是用软性的梯度引导,每一步都温温柔柔地把解拉向合理方向。

3. 从公式到代码:DPS采样器的工程实现

3.1 主干伪代码

讲完原理,我们直接看实现。我以PyTorch风格的伪代码展示,采样采用DDPM的逆过程,步数T设成100或200,已足够在很多任务上得到满意结果。代码的关键就是每一步多了一次梯度计算。

def dps_sample(model, A, y, sigma_y, T=200, zeta=1.0): # 初始化纯噪声 x_t = torch.randn(y.shape[0], 3, H, W) * sqrt_beta_T for t in reversed(range(1, T + 1)): t_batch = torch.full((batch,), t, dtype=torch.long) # 1. 无条件去噪预测 eps_pred = model(x_t, t_batch) x0_pred = (x_t - sqrt(1 - alpha_bar[t]) * eps_pred) / sqrt(alpha_bar[t]) x0_pred = x0_pred.clamp(-1, 1) # 2. 数据一致性梯度 x0_pred = x0_pred.detach().requires_grad_(True) loss = torch.norm(y - A(x0_pred), p=2) ** 2 / (sigma_y ** 2) grad = torch.autograd.grad(loss, x0_pred)[0] # 3. 更新采样:DDPM均值 + 引导梯度 mean_coef = 1 / sqrt(alpha_t) noise_coef = beta_t / sqrt(1 - alpha_bar[t]) mean = (x_t - noise_coef * eps_pred) / sqrt(alpha_t) sigma_t = sqrt(beta_t) z = torch.randn_like(x_t) x_next = mean + zeta * grad + sigma_t * z x_t = x_next return x_t

这里我有一处实现上的偷懒,也有意识的调整:梯度是相对于x0_pred算的,再直接加到x_{t-1}更新上。严格实现可以把梯度一路回传到x_t,但实际测试中,前者的效果已经能看,而且省了很大一部分显存开销。如果你想跟原始论文的写法保持一致,把梯度的计算图挂到x_t上即可。

3.2 关键参数:步长、噪声尺度、采样步数

DPS里最重要的参数是zeta,也就是引导梯度前面的缩放系数。它的量级直接影响结果质量。假如zeta太小,测量信息完全压不过扩散模型的先验,结果就等于一次无条件生成,根本不保证与y吻合。假如zeta太大,梯度会把x_t从图像流形上推出去,产生强烈的伪影甚至发散。

经验上,zeta取1 / ||A||²的倍数比较稳。对于超分辨率,下采样算子A的谱范数通常是1,所以zeta可以直接从0.1附近开始试。对于相位恢复,A是傅里叶模算子,能量有损失,需要重新估计||A||²,我一般用随机向量做几次前向传播估算。

另一个关键参数是sigma_y,代表测量噪声水平的假设。它不能设得太小,否则数据一致性项过于尖锐,梯度方差很大,采样的随机性被破坏,结果会出现环路状伪影。我的习惯是先估算真实噪声水平,再稍微放大一点,这样反而让重建更平滑。

采样步数的取舍也很有讲究。原始DDPM要跑1000步,DPS每步都带一次反向传播,跑满1000步计算量非常大。实践中我发现200步已经足够,如果任务比较简单,比如图像修复,甚至可以缩到50步。更激进的做法是配DDIM类采样器,但我提醒一句,DPS的梯度引导是和DDPM的随机噪声步配套设计的,强行在DDIM确定性采样上套用DPS梯度,需要把随机噪声项调成0并仔细调整zeta,否则效果会打折扣。

3.3 工程细节:显存、精度与加速

DPS在显存上的消耗比普通采样高一大截,因为要在每一步都对网络做前向传播并反向传播梯度。我的实测数据是:单卡24G显存跑512分辨率、200步、batch为1,勉强能跑,但每一步都要花大约0.4秒,一个样例需要一分半。如果你要跑高分辨率的相位恢复,建议不要等到每一步最后才释放计算图,而是用梯度检查点把中间激活丢掉,只保留输出层的梯度。

数值精度方面,我踩过一个很隐蔽的坑。扩散模型的预训练权重很多是用FP16混合精度训练的,而DPS的梯度计算要求对x_t求二阶路径,半精度下的梯度噪声会显著放大。我建议采样阶段统一用FP32,虽然慢一些,但稳定很多。如果是自用的模型权重,可以先把FP16转成FP32再加载。

加速方面有条捷径:前T*k个采样步只做无条件更新不做梯度引导,后面再开启DPS梯度。原因在于早期x_t几乎全是噪声,x0_pred也很糊,这时算出来的数据一致性梯度没有意义,还容易把采样方向带歪。我通常在200步里,前30步只做去噪,第31步开始加引导,效果比全程引导更稳,伪影更少。

4. 计算成像里的三类典型任务怎么套用DPS

4.1 相位恢复

相位恢复是DPS早期论文里效果惊艳的任务之一。它的观测模型是:

y = |F(x)|

其中F是傅里叶变换,取模操作丢掉了相位信息。这个问题的病态程度极高,传统交替投影方法容易陷入局部极小,尤其是缺少支撑约束的时候。DPS处理起来,只需把A定义为“傅里叶变换取模”的前向算子,数据一致性损失就写成:

|| y - |F(x0_pred)| ||²

这里要注意一点,取模操作在x0_pred穿过0的时候梯度是不连续的,所以实现上加一个极小的epsilon防止NaN。我在复现时还发现,梯度更新前的x0_pred最好先做一次clip到[-1,1]的操作,否则取模后的尺度可能跑飞。

实测结果,DPS在低采样率下能恢复出清晰图像,纹理细节和真实的相位恢复结果在视觉上一眼能分辨。这背后的原因还是生成先验太强了,它知道真实的自然图像大致长什么样,因此能在相位完全缺失的情况下脑补出合理结果。

4.2 超分辨率与去模糊

超分辨率任务里,A是模糊核加下采样。DPS的做法非常简单:前向算子A就是一个下采样函数,数据一致性损失是:

|| y - Downsample(x0_pred) ||²

去模糊时把模糊核卷积放进A里。这两个任务是我日常调试DPS用得最多的场景,因为它们可以直接用ImageNet上预训练的扩散模型,不需要重新训练,改一下A的写法就能适用不同任务。

印象很深的一次实验是对一张低分辨率人脸做4倍超分。传统深度学习方法,比如预训练的ESPCN或SRGAN,会把五官恢复得“很像”,但细节可能错误,比如眼睛形状偏了、发丝糊成一片。DPS给出的结果在整体结构上非常合理,而且在边缘处有过曝的倾向,但视觉真实感更高。代价是计算量高出几个数量级,单张图在消费级显卡上需要跑几十秒到几分钟。

4.3 压缩感知式加速采集

压缩感知成像更符合DPS发挥的场合。观测模型可以写成:

y = M * F(x)

其中M是采样掩码,F是傅里叶变换。加速磁共振成像、射电干涉成像、太赫兹成像都可以归到这个框架。A的数学形式是一个线性算子,但实现上要注意它可能是复数域运算,PyTorch的自动求导对复数支持没问题,但梯度更新时要注意取实部,因为图像x0_pred在绝大多数数据集里是实数域的。

这类任务尤其考验sigma_y的设置。因为实际采集过程中噪声并不严格高斯,而且掩码的采样率极低,如果sigma_y设小了,DPS会疯狂往数据一致性方向拽,最终结果出现块状伪影,表面看起来噪声被压住了,但结构完全失真。我的经验是,欠采样率是4倍时,sigma_y设为0.1;欠采样率是8倍时,设成0.15到0.2,效果更符合视觉直觉。

这里的通用心法简单概括:越病态的逆问题,越要给梯度引导留余地,同时越要依赖生成先验去填充缺失信息,两者需要平衡。

5. 我踩过的坑:DPS调试经验实录

5.1 结果发糊、边缘丢失

这是我第一次上手DPS时最常见的问题,跑出来的图看起来“好像知道是张人脸,但放大了全是浆糊”。检查优先级依次是:zeta是否太小,扩散步数是否太少,x0_pred是否在梯度计算前被clip。

如果zeta已经调到较大值仍然发糊,那大概率是采样步数不够。DPS的生成过程本质上是扩散采样,步数太少会让细节没有足够时间去演化。我在128分辨率下测试,50步和200步的差距用肉眼看非常明显。另外,在梯度计算前把x0_pred的数值范围clip到[-1,1]非常重要,如果不clip,偶尔一次异常大值会打断整个采样的稳定性,采样结果会出现一条亮带。

5.2 梯度爆炸与NaN出现

梯度爆炸是DPS在高分辨率任务里的常客。一个典型现象是,前面几步采样的x_t还正常,某一步开始loss的值跳到1e8以上,然后整个图像变成雪花屏。

排查思路有一个顺序:第一步看A的实现,如果A包含插值或采样操作,确认梯度能否正确回传,试试用torch.autograd.gradcheck做一个最小化校验。第二步看sigma_y,过小会放大梯度,我建议先设成0.2以上跑通基线。第三步看zeta,过大的话直接在采样中段就爆了。

我自己遇到的一个隐蔽问题出在模糊核上。去模糊任务里A是一个固定卷积核,如果卷积核参数化不当,边界做了填充,梯度在边缘处被放大了几个量级。解决办法很简单,把卷积的padding模式从reflect换成zero,并让输入图像尺寸比卷积核大一圈,减小边界效应。

5.3 分辨率不匹配

预训练扩散模型通常固定在某个分辨率,比如256。当你需要重建1024分辨率的图像时,直接把预训练模型套上去会崩。常见做法是先用DPS在256尺度采样出一个低分结果,再用经典方法上采样并做一个后处理小修,或者用分块的DPS采样,把图像切块,每个块独立采样,然后重叠区域做平均融合。

分块采样要注意块之间的重叠不能太小,不然接缝明显。我用的是重叠16个像素,配合一个线性加权羽化,效果比硬拼好很多。为了减少分块数量,也可以先跑一个全局的低分辨率DPS,把所得结果作为初始x_t的初始化值,再进入分块精修。

5.4 计算时间过长,没法忍受

DPS慢是绕不开的,但有几招能显著提速。

第一招,用小步数预热加梯度检查点,前面已经提过。第二招,用前向过程把x_T初始化成靠近数据的带噪版本,而不是纯白噪声。具体做法是:传入一张快速重建的结果x_fast,按照扩散前向公式加噪到某个中间时刻t0,把x_{t0}当作采样的起点,这样可以跳过前面几十步的纯探索阶段。第三招,在计算数据一致性梯度时,把x0_pred的梯度先回传到x_t,再更新,速度差异不大,但显存省了很多。

实测下来,三步组合拳能把单张512分辨率超分任务从三分钟压到四十秒左右,视觉质量几乎没有退化。对于原型验证来说,这个增益非常值得做。

6. 下一步可以往哪走

6.1 硬件与算力层面的现实建议

DPS不挑显卡,但挑显存。我建议至少要一块12G显存的卡,跑256分辨率轻轻松松,512分辨率有点吃紧,1024基本只能分块处理。如果只是做学术验证,不需要执着于高分辨率,128或256分辨率上的结论完全可以支撑一篇技术报告。

另外,由于每一步都涉及反向传播,单精度前向时间反而比混合精度更稳定,我用下来的经验是别为了那一点速度去牺牲稳定性,尤其是在网络没有针对半精度做校准的情况下。

6.2 给入门者的路线建议

如果你准备从零开始掌握这套方法,我的建议是从两个最小复现开始。第一个是超分辨率,用现成预训练扩散模型,A写一个简单的下采样函数,数据用公开数据集,跑通之后你会对DPS的整体流程有肌肉记忆。第二个是相位恢复,试着理解取模算子带来的梯度不连续问题,这个任务的难度刚好够你踩一遍数值稳定性的坑。

在这之后,你可以换着不同的A算子玩:随机掩码、不同模糊核、多帧融合,基本都能用同一套代码框架接上。DPS的迁移性非常好,这是它最吸引我的一点。

6.3 这条路的局限与展望

DPS目前最大的短板还是计算开销。每一步都要回传一次梯度,导致它在实时成像系统里几乎用不了。另一个短板是它对测量算子A的可微性要求比较高,现实中的成像系统有些部件并不是严格可微的,比如一些光学元件的硬非线性,这限制了落地场景。

但换个角度看,这个框架的价值不只是“能解逆问题”,而是打开了一个思路:传统正则先验可以换成生成模型的得分函数,贝叶斯后验采样可以替代约束优化。后续衍生的改进方案,有的在梯度上做修正来减少每步计算量,有的用潜在空间扩散模型降低维度,有的结合条件生成模型减少对随机噪声的依赖,这些方向本质上都在与DPS的同一套知识体系对话。

我个人的体会是,看DPS的论文,第一遍觉得近似太粗糙,第二遍才意识到粗糙背后的洞察:在扩散过程的早期,精确解本来就不存在,只需要一个“别偏离太远”的引导就行。当我把每个采样步的中间结果可视化出来,看着一张张图像从噪声中逐渐显形,并且每一步都越来越服从测量约束的时候,确实有那种“原来生成模型还能这么用”的感慨。这套方法目前依然是我处理新成像问题时的默认起跑线。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 7:03:09

Vue2+SpringBoot+Nacos上K8s:StatefulSet部署与避坑实录

简介:面向在阿里云Kubernetes中搭建前后端分离应用的开发与运维人员,这份资源提供了Vue2NginxSpring Boot 2.5Nacos 2.0.3的整套部署参考。包内共16个文件,核心为8个YAML编排文件,另含2个Shell部署脚本、2个Dockerfile、1个SQL初始…

作者头像 李华
网站建设 2026/10/20 7:02:01

深入理解synchronized:从内存可见性到JVM锁升级机制

写了这么多年代码,也面试过不少人,我发现大部分人对synchronized的理解其实停留在"给方法加个锁就不会有并发问题"这个层面。它确实能挡住并发问题,但如果你不清楚它挡的到底是什么,迟早会在某个诡异的内存可见性 bug 面…

作者头像 李华
网站建设 2026/10/20 7:01:59

基于WinPcap的数据包分析器源码拆解:从抓包到协议解析

简介:这份资源是华中科技大学2018年计算机网络安全实验的完整源码,面向正在学习网络安全、网络协议分析或准备相关课程实验的高校学生与开发者,帮助理解数据包抓取、协议解析以及ARP欺骗实现中间人攻击的核心原理。压缩包共27个文件&#xff…

作者头像 李华
网站建设 2026/10/20 7:00:23

离线环境部署nginx:rpm集合方案与依赖避坑全解析

简介:面向Linux服务器运维与部署场景,这套离线安装包专门解决无网络或网络受限环境下nginx快速部署的难题,涵盖gcc与nginx所需的rpm依赖集合,适合需在内网服务器、数据中心离线安装nginx的技术人员使用。压缩包共4个文件&#xff…

作者头像 李华
网站建设 2026/10/20 7:00:13

基于Python的博客系统开题答辩全攻略:从技术选型到实战问答

开题答辩前一天晚上,很多人会把PPT改了第四遍,把可能被问的问题列在一张纸上,凌晨一点还在对着空气练开场白。这种状态我太熟悉了。但“基于Python的博客系统”这种看起来常见的题目,反而更容易在开题答辩时被问住——因为评委默认…

作者头像 李华
网站建设 2026/10/20 6:58:42

华三IRF堆叠配置实战:从设备规划到MAD防脑裂

1. 项目概览:华三堆叠到底在解决什么问题干网络这一行,迟早会碰到堆叠这两个字。我最早接触华三的堆叠,是在一个市级数据机房的项目里,客户要求核心网络做到"一台设备挂了不影响业务"。当时摆在我面前的无非三条路&…

作者头像 李华