6.3 扩散模型(一)DDPM原理

1.生成这一摊事,最后怎么就落到扩散模型头上了

说起来,生成模型这几年的风向变了好几回。早些年大家折腾GAN(生成对抗网络),它的思路挺巧,一边训一个生成器造假,一边训一个判别器打假,两边互相逼着长进,后来StyleGAN把人脸生成推到了吓人的精度。再后来VAE(变分自编码器)又火了一阵,它把编码和重参数化玩得漂亮,配一个KL散度做正则(KL散度我们在1.4 节详细聊过,它衡量两个分布差得有多远),整出一个连续可插值的隐空间。可惜GAN训练不稳,VAE画出来的东西偏糊,两家各有各的难处。那时候大家用GAN做图像生成,常常要调参调到深夜还稳不下来。

扩散模型冒出来其实是2020年的事。DDPM这篇论文一出来,大家发现这套路画出来的图又清晰、训练又稳,慢慢地就成了图像生成的主流。再后来Latent Diffusion把扩散搬到隐空间里做,配上前一阵4.10 节讲的CLIP做文本条件,就成了大家熟知的Stable Diffusion。如今扩散模型早已不只是画图玩具,电影分镜、广告插画、乃至药物分子设计,到处都能见到它落地的影子。说到底,扩散模型能成主流,靠的是两条腿走路,一是效果确实好,二是训练确实稳。

我们今天就先把DDPM这件事的原理讲清楚,下一篇再去聊加速采样那些事。

2.核心就两件事:前向加噪,反向去噪

扩散模型的核心,说穿了就是两个过程。一个叫前向过程(forward process),做的事情是往图里加噪声。另一个叫反向过程(reverse process),做的事情是从噪声里把图还原回来。这两个过程名字对称,做的事恰好相反。

我们一个一个看。

前向过程是预先定好的,不用学。它的思路朴素得可爱,给一张干净的图,往里加一点点高斯噪声,得到一张稍微花一点的图,再往这张花的图上加一点点噪声,得到一张更花的图,这样一步一步加下去。只要步数够多,最后这张图就被噪声彻底淹掉了,变成一张纯粹的随机噪声,肉眼看上去和电视没信号时的雪花点一模一样。

这个过程为什么是预先定好的呢,因为加噪声的规则是一组固定的公式。我们设总步数为 TTTT 就是前向过程一共要走多少步,通常取 10001000。再设一个单调下降的序列 β1,β2,,βT\beta_1,\beta_2,\ldots,\beta_T,每一项 βt\beta_t 都是一个介于 0011 之间的小正数,叫作噪声调度(noise schedule),βt\beta_t 控制第 tt 步加的噪声有多大。这些 βt\beta_t 在训练之前就已经定死,不参与梯度更新。

给定初始的干净图 x0x_0x0x_0 就是第 00 步那张还没动过的原图,下标 00 表示步数),第 tt 步加噪之后得到的那张图记作 xtx_t。每一步的加噪规则是这样的:

xt=1βtxt1+βtϵt,ϵtN(0,I)x_t=\sqrt{1-\beta_t}\,x_{t-1}+\sqrt{\beta_t}\,\epsilon_t,\quad \epsilon_t\sim\mathcal{N}(0,\mathbf{I})

我们一项一项拆。等号右边的 xt1x_{t-1} 是上一步那张图,βt\beta_t 就是上面说的噪声调度。ϵt\epsilon_t 是这一步加进去的高斯噪声,ϵ\epsilon 这个希腊字母我们全文都用来表示噪声,下标 tt 说明它是第 tt 步的那一份。N(0,I)\mathcal{N}(0,\mathbf{I}) 是标准正态分布,意思就是它的均值是 00、方差是 11I\mathbf{I} 是单位矩阵,对角线全是 11、其余位置全是 00)。前面那两个根号是给定的固定系数,用来控制上一步的图和这一步的噪声各占多少分量。一句话总结,每一步都是把上一步的图稍微缩小一点点,再补上一点点噪声。

这里有个特别漂亮的性质,叫作闭式跳跃加噪。它说的是,给定原图 x0x_0,我们其实不用真的一步一步加,可以直接跳到任意中间步 tt。我们再设两个记号,αt=1βt\alpha_t=1-\beta_tαˉt=α1α2αt\bar{\alpha}_t=\alpha_1\alpha_2\cdots\alpha_t,也就是前 ttα\alpha 全部乘在一起(α\alphaαˉ\bar\alpha 都是中间量,记下来方便后面写公式)。那么:

xt=αˉtx0+1αˉtϵ,ϵN(0,I)x_t=\sqrt{\bar{\alpha}_t}\,x_0+\sqrt{1-\bar{\alpha}_t}\,\epsilon,\quad \epsilon\sim\mathcal{N}(0,\mathbf{I})

这个闭式是怎么推出来的。 关键用到一个高斯分布的性质:两个独立高斯 N(0,σ12I)\mathcal N(0,\sigma_1^2\mathbf I)N(0,σ22I)\mathcal N(0,\sigma_2^2\mathbf I) 相加,还是高斯,方差是 σ12+σ22\sigma_1^2+\sigma_2^2。我们从单步公式 xt=αtxt1+1αtϵtx_t=\sqrt{\alpha_t}\,x_{t-1}+\sqrt{1-\alpha_t}\,\epsilon_t 出发,把 xt1x_{t-1} 再展开一代:xt1=αt1xt2+1αt1ϵt1x_{t-1}=\sqrt{\alpha_{t-1}}\,x_{t-2}+\sqrt{1-\alpha_{t-1}}\,\epsilon_{t-1},代进去:

xt=αtαt1xt2+αt(1αt1)ϵt1+1αtϵt两个独立高斯之和x_t=\sqrt{\alpha_t\alpha_{t-1}}\,x_{t-2}+\underbrace{\sqrt{\alpha_t(1-\alpha_{t-1})}\,\epsilon_{t-1}+\sqrt{1-\alpha_t}\,\epsilon_t}_{\text{两个独立高斯之和}}

后面两个噪声项都是独立的标准正态乘个系数,合起来方差是 αt(1αt1)+(1αt)=1αtαt1\alpha_t(1-\alpha_{t-1})+(1-\alpha_t)=1-\alpha_t\alpha_{t-1},所以合并成 1αtαt1ϵˉ\sqrt{1-\alpha_t\alpha_{t-1}}\,\bar\epsilonϵˉ\bar\epsilon 仍是标准正态)。于是 xt=αtαt1xt2+1αtαt1ϵˉx_t=\sqrt{\alpha_t\alpha_{t-1}}\,x_{t-2}+\sqrt{1-\alpha_t\alpha_{t-1}}\,\bar\epsilon。这样一层层往回展开到 x0x_0,前面的系数连乘就是 αˉt=α1αt\bar\alpha_t=\alpha_1\cdots\alpha_t,后面噪声的方差凑成 1αˉt1-\bar\alpha_t,闭式就出来了。这个性质是扩散模型能高效训练的基础——训练时每张图随机挑一个 tt,一步就能算出 xtx_t,不用真跑 tt 步前向。

这里 ϵ\epsilon 仍然是标准正态噪声,只不过这一次是从头到尾只采样一次的累计噪声。这个公式特别重要,训练时我们要随便挑一个步数 tt、随便采一份噪声 ϵ\epsilon,就能立刻拿到对应的 xtx_t,不需要把前 t1t-1 步全跑一遍。我记得小张第一次看代码的时候,半天没反应过来这个跳跃是怎么回事,盯着这行公式看了小半个钟头才连连点头。

到这一步,前向过程就讲完了,确实不用学。

3.反向去噪,这才到了要学的部分

前向是顺着往下坡走,加噪声很容易。反向过程则要逆着往回走,把噪声一点点扒掉,还原出一张干净的图,这件事就没那么轻松了。

我们想要的是反向过程的每一步,给定当前这张加噪图 xtx_t,能猜出来上一步那张稍微干净的图 xt1x_{t-1} 长什么样。从概率上讲,前向过程的每一步 q(xtxt1)q(x_t\mid x_{t-1}) 是高斯分布,前向好算。可反向过程的后验 q(xt1xt)q(x_{t-1}\mid x_t)(给定当前图倒推上一步图的概率)就麻烦了,它没有解析形式。换句话说,反向这一步没法用解析公式直接算出来,得另想办法。

数学上有个技巧,是用一个神经网络去近似这个反向的后验分布。我们把这个神经网络记作 pθp_\theta,下标 θ\theta 表示它内部的参数(就是那些权重,要靠训练学出来)。pθ(xt1xt)p_\theta(x_{t-1}\mid x_t) 表示在参数 θ\theta 控制下、给定 xtx_t 时预测 xt1x_{t-1} 的概率分布。这个 pθp_\theta 通常用一个U-Net来实现,U-Net是那种先把图一层层压缩、再一层层还原回去、中间用跳接把细节补回来的网络结构,特别适合像素级的预测任务。

到这里问题就变成了,怎么训这个U-Net。

4.训练目标:让模型预测噪声

DDPM最妙的一步,就在这里。直觉上你可能觉得,模型应该预测原图 x0x_0(毕竟我们最后想要的就是原图),可原作者偏偏挑了另一条路子,让模型去预测每一步加进去的那个噪声 ϵ\epsilon

为什么不预测原图呢,预测噪声的好处有这么几条。其一,原图里信息太多,像素之间的关联又复杂又多样,模型学起来吃力。噪声就简单多了,它就是一个标准正态分布,每一维独立、方差一致,是个特别干净的目标。其二,从数学上看,反向后验分布的均值里恰好就藏着这个 ϵ\epsilon,把 ϵ\epsilon 猜准了,均值也就准了,xt1x_{t-1} 自然就有指望。其三,预测噪声这件事和重参数化(VAE里那个把随机性从参数里挪到外部噪声上的技巧)思路一脉相承,训练的梯度好算。

那个简单的"猜噪声"损失,其实是从 ELBO(变分下界)一路化简推出来的。 这条推导我们交代一下,免得你觉得这个损失是凭空冒出来的。和 VAE 一样,扩散模型的目标也是最大化数据对数似然 logpθ(x0)\log p_\theta(x_0),但直接算不出来,于是用变分下界:logpθ(x0)Eq ⁣[logpθ(x0:T)q(x1:Tx0)]-\log p_\theta(x_0)\leq\mathbb E_q\!\left[-\log\frac{p_\theta(x_{0:T})}{q(x_{1:T}|x_0)}\right]。用前向马尔可夫链 qq 的结构展开、用贝叶斯公式把反向后验 q(xt1xt,x0)q(x_{t-1}|x_t,x_0) 凑出来,经过一番冗长的代数化简,ELBO 最终分解成 TT 项 KL 散度之和:

LELBO=t=1TDKL(q(xt1xt,x0)pθ(xt1xt))+常数\mathcal L_{\text{ELBO}}=\sum_{t=1}^{T}D_{KL}\bigl(q(x_{t-1}|x_t,x_0)\,\|\,p_\theta(x_{t-1}|x_t)\bigr)+\text{常数}

这里有个关键事实:前向后验 q(xt1xt,x0)q(x_{t-1}|x_t,x_0) 是可以算出闭式的,它是个高斯,均值是 μ~t=1αt(xtβt1αˉtϵ)\tilde\mu_t=\frac{1}{\sqrt{\alpha_t}}\Bigl(x_t-\frac{\beta_t}{\sqrt{1-\bar\alpha_t}}\epsilon\Bigr)ϵ\epsilon 就是用闭式跳跃公式反解出来的那一份噪声),方差也是固定的。而模型 pθp_\theta 也设成同方差的高斯,只让均值 μθ\mu_\theta 由 U-Net 学。于是每一步的 KL 就是两个同方差高斯均值之差的平方,把 μ~t\tilde\mu_tμθ\mu_\theta 代进去化简,那个 ϵ\epsilon 会露出来:

Ltβt22σt2αt(1αˉt)ϵϵ^θ(xt,t)2\mathcal L_t\propto\frac{\beta_t^2}{2\sigma_t^2\alpha_t(1-\bar\alpha_t)}\bigl\|\epsilon-\hat\epsilon_\theta(x_t,t)\bigr\|^2

DDPM 的关键简化就在这一步:原作者发现前面那一坨加权系数 βt22σt2αt(1αˉt)\frac{\beta_t^2}{2\sigma_t^2\alpha_t(1-\bar\alpha_t)} 对最终效果影响不大,干脆扔掉,把所有 tt 等权对待,损失就退化成下面那个干净极简的形式。所以那个"猜噪声"损失不是拍脑袋,而是变分下界去掉加权系数后的简化版

具体的训练步骤说起来不复杂。我们从训练集里随便抽一张原图 x0x_0,随便挑一个步数 tt,随便采一份标准正态噪声 ϵ\epsilon,用上面那个闭式公式直接算出 xtx_t。然后把 xtx_ttt 一起喂给U-Net,让它预测一份噪声 ϵ^\hat\epsilonϵ^\hat\epsilon 就是模型猜出来的噪声,那个小帽子符号表示预测值)。预测出来之后,和真值 ϵ\epsilon 做均方误差,作为损失:

L=Ex0,t,ϵ ⁣[ϵϵ^θ(xt,t)2]\mathcal{L}=\mathbb{E}_{x_0,t,\epsilon}\!\left[\big\|\epsilon-\hat\epsilon_\theta(x_t,t)\big\|^2\right]

我们一行行看。L\mathcal{L} 是损失函数,E\mathbb{E} 是期望符号,意思是对后面这三个随机变量(原图 x0x_0、步数 tt、噪声 ϵ\epsilon)所有可能的取值求平均。ϵ^θ(xt,t)\hat\epsilon_\theta(x_t,t) 表示参数为 θ\theta 的U-Net接收 xtx_ttt 后输出的预测噪声。中间那个 2\|\cdot\|^2 是欧几里得范数的平方,就是把两个向量每个分量做差、平方、再全部加起来。

就这么一个简单的损失,整个训练就跑起来了。每一次迭代都是上面这套流程,模型反复地猜噪声、对照真值、调整参数,慢慢地越猜越准。说起来,这么朴素的目标最后能画出那么漂亮的图,第一次看的人常常会觉得有些不可思议。

5.生成:从一块乱石里凿出形象

训练完之后,怎么真的生成一张新图呢。

做法是从一张纯粹的随机噪声出发,也就是先采一个标准正态的 xTx_T(这是第 TT 步、也就是最末那一步的图,纯噪声)。然后从 t=Tt=T 一步一步往回走,每一步都让U-Net根据当前的 xtx_ttt 预测出一份噪声 ϵ^\hat\epsilon,再按照固定的公式把这份噪声从图里减掉、再加回一点点随机扰动(防止走得太死板),得到上一步的 xt1x_{t-1}。我们就这样一直走到 t=0t=0,最后得到的 x0x_0 就是一张全新的图。

这套走法有个朴素的直觉比喻。我说像雕塑家从一块乱石里一点点凿出形象,每一锤下去都看不出什么,可连着凿上几百锤,脸的轮廓、衣褶的走向就慢慢浮出来了。我之前翻过一本讲雕塑的书,里面提到米开朗基罗说过类似的话,说石头里本来就藏着那尊雕像,他只是把多余的部分敲掉。这话拿来形容扩散模型的反向去噪,竟意外地贴切。也有人拿冲洗老照片来打比方,底片一开始一片模糊,慢慢显影液把影像一层层还原回来,思路也是通的。这两个例子都指向同一件事,就是生成这件事在这里被拆成了成百上千个微小步骤,每一步只做一点点修正,最后凑出来的效果反而特别扎实。

这套流程有个绕不开的麻烦,就是慢。生成一张图要走 TT 步,T=1000T=1000 时就是 10001000 次完整的U-Net前向,画一张图几秒钟起步,比GAN一次前向出图要慢得多。所以后面大家想了很多办法去加速,比如DDIM、DPM-Solver这些采样器,能把步数压到二三十步,下一篇我们会专门聊。这也是扩散模型在大规模落地时绕不过去的坎。

6.说两句闲话

到这里,DDPM的主干就讲完了。说白了就是这么几样东西,一个固定的前向加噪过程用来造训练样本,一个U-Net学着去预测每一步的噪声,一个反向采样的循环用来生成新图,外加一个干净到有点出人意料的均方误差损失。后面那些花活,包括Latent Diffusion把扩散搬到隐空间、Stable Diffusion把文本条件接进来、还有最近很火的用Transformer替换U-Net的DiT(Transformer我们在3.4 节讲过,是当前大多数大模型的底座),其实都是在这套骨架上做改良。

下一章我们去看采样加速这件事,看看怎么把那 10001000 步压下去,让一张图能在眨眼之间画完。

练习

Q1. DDPM的前向加噪过程为什么说"不用学"?闭式跳跃公式 xt=αˉtx0+1αˉtϵx_t=\sqrt{\bar\alpha_t}x_0+\sqrt{1-\bar\alpha_t}\epsilon 解决了什么问题?

前向加噪的规则是一组固定公式(噪声调度 β1,,βT\beta_1,\dots,\beta_T 在训练前就定死、不参与梯度更新),所以不用学。闭式跳跃公式解决的是"不用一步一步加噪"的问题:给定原图 x0x_0,不用真跑 tt 步前向,直接挑任意中间步 tt、采一次累计噪声 ϵ\epsilon 就能一步算出 xtx_t。训练时每张图随机挑一个 tt、采一份 ϵ\epsilon 立刻拿到 xtx_t,这是扩散模型能高效训练的基础。它由"两个独立高斯相加还是高斯、方差相加"这条性质一层层往回展开得到。

Q2. DDPM让U-Net预测的是噪声 ϵ\epsilon 而不是原图 x0x_0,这么做有哪些好处?

好处有三条。其一,原图信息太多、像素关联又复杂又多样,模型学起来吃力;噪声是标准正态分布,每维独立、方差一致,是个特别干净的目标。其二,从数学上看,反向后验分布的均值里恰好藏着 ϵ\epsilon,把 ϵ\epsilon 猜准了均值就准、xt1x_{t-1} 自然有指望。其三,预测噪声和VAE的重参数化思路一脉相承(把随机性挪到外部),训练梯度好算。所以损失就是简单的 L=E[ϵϵ^θ(xt,t)2]\mathcal{L}=\mathbb{E}[\|\epsilon-\hat\epsilon_\theta(x_t,t)\|^2],朴素却有效。

Q3. 易错点:DDPM的反向后验 q(xt1xt)q(x_{t-1}\mid x_t) 没有解析形式,那训练时损失里的 ϵ\epsilon 是怎么和"预测噪声"对上的?

关键在于条件后验 q(xt1xt,x0)q(x_{t-1}\mid x_t,x_0)(多给了 x0x_0 这个条件)是有闭式的,它是个高斯,均值是 μ~t=1αt(xtβt1αˉtϵ)\tilde\mu_t=\frac{1}{\sqrt{\alpha_t}}(x_t-\frac{\beta_t}{\sqrt{1-\bar\alpha_t}}\epsilon),方差固定。模型 pθp_\theta 也设成同方差高斯、只让U-Net学均值 μθ\mu_\theta。于是从ELBO分解出的每一步KL就是两个同方差高斯均值之差的平方,把 μ~t\tilde\mu_tμθ\mu_\theta 代入化简,那个用闭式跳跃反解出的 ϵ\epsilon 就露出来了,损失正比于 ϵϵ^θ(xt,t)2\|\epsilon-\hat\epsilon_\theta(x_t,t)\|^2。DDPM的关键简化是把前面的加权系数扔掉、所有 tt 等权对待。所以"预测噪声"不是拍脑袋,是变分下界去掉加权后的简化版。

Q4.(面试题) 请说明 DDPM 的前向加噪、反向去噪、训练损失三者的关系,并解释为什么 DDPM 训练稳但生成慢。

三者关系:前向加噪是固定过程(噪声调度 βt\beta_t 预先定死),把干净图 x0x_0 一步步加噪到纯噪声 xTx_T,靠闭式跳跃 xt=αˉtx0+1αˉtϵx_t=\sqrt{\bar\alpha_t}x_0+\sqrt{1-\bar\alpha_t}\epsilon 一步算出任意中间步;反向去噪是要学的部分,用一个U-Net pθp_\theta 近似反向后验 q(xt1xt)q(x_{t-1}\mid x_t),从纯噪声 xTx_T 一路走到 x0x_0;训练损失就是让U-Net预测每一步加进去的噪声 ϵ\epsilon,做均方误差 L=Ex0,t,ϵ[ϵϵ^θ(xt,t)2]\mathcal{L}=\mathbb{E}_{x_0,t,\epsilon}[\|\epsilon-\hat\epsilon_\theta(x_t,t)\|^2],每次迭代随机抽 x0x_0、挑 tt、采 ϵ\epsilon、算 xtx_t、预测、对照真值调参。训练稳的原因:损失是从变分下界(ELBO)化简来的、有明确理论根,目标(预测标准正态噪声)干净简单,不像GAN那样两个网络对抗博弈、容易模式崩溃,所以训练曲线平稳。生成慢的原因:生成时要从纯噪声 xTx_T 一步步反向走到 x0x_0T=1000T=1000 就要跑1000次完整的U-Net前向,画一张图几秒起步,远不如GAN一次前向出图。所以后来才有DDIM、DPM-Solver这些采样器把步数压到二三十步来加速,这是扩散模型大规模落地绕不过去的坎。

相关标签
深度学习生成模型扩散模型DDPM