DDPM (Denoising Diffusion Probabilistic Models) Introduction
Diffusion models 的成功并非源于某个直观的理论证明说明”这种方法一定更好”。事实上,它的优越性主要是通过实验 发现的——研究者们尝试了这种方法,发现它在生成质量和多样性上表现出色。
有此可以得一窥泛AI本身就是一个工程和理论相互结合的一个领域,你在工程上能够反复迭代够快,你就可以test你的这些理论模型到底会不会work。你再去反过去用概率学来研究那些模型的原理,让它更加的make sense。
Diffusion models 受热力学 启发,定义一个马尔可夫链逐步向数据添加噪声。
前向过程 (Forward Process) : 逐步向原始图像 添加高斯噪声,经过 步后得到纯噪声
反向过程 (Reverse Process) : 训练神经网络 学习去噪,从纯噪声 逐步恢复出原始图像
Forward process 给定
Adding noise ,at time , we add a noise sampled from gaussian to generate latents to :
Single step transition:
重参数化技巧 : 我们引入重参,它可以直接从 跳到任意时间步 ,无需逐步计算中间步骤。给定 和 ,我们有:
公式 :
我们可以对他进行重参数化化简:
推导 (参考 Lilian Weng 的博客 ):
从单步转移公式开始,我们可以用重参数化改写:
现在递推,将 代入:
使用高斯合并,两个独立的高斯噪声 和 相加后仍是高斯分布 。
因此对于后面一项我们可以化简方差项:
其中 。
所以:
引入定义 : , ,则:
然后持续展开递推到 :
Intuition :
: 保留原始信号的比例(随 递减)
: 添加噪声的比例(随 递增)
通常 ,所以 ,噪声逐渐增强,不过这个可以自定义,很多实现都有自己的加噪scheduling算法。
Code 1. Beta Schedule 定义 (gaussian_diffusion.py:18-42 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def get_named_beta_schedule (schedule_name, num_diffusion_timesteps ): """获取预定义的 beta 调度""" if schedule_name == "linear" : scale = 1000 / num_diffusion_timesteps beta_start = scale * 0.0001 beta_end = scale * 0.02 return np.linspace(beta_start, beta_end, num_diffusion_timesteps, dtype=np.float64) elif schedule_name == "cosine" : return betas_for_alpha_bar( num_diffusion_timesteps, lambda t: math.cos((t + 0.008 ) / 1.008 * math.pi / 2 ) ** 2 , )
2. 系数预计算 (gaussian_diffusion.py:140-151 )
1 2 3 4 5 6 7 8 alphas = 1.0 - betas self .alphas_cumprod = np.cumprod(alphas, axis=0 ) self .alphas_cumprod_prev = np.append(1.0 , self .alphas_cumprod[:-1 ]) self .sqrt_alphas_cumprod = np.sqrt(self .alphas_cumprod) self .sqrt_one_minus_alphas_cumprod = np.sqrt(1.0 - self .alphas_cumprod)
3. 前向加噪 q_sample (gaussian_diffusion.py:188-206 )
1 2 3 4 5 6 7 8 9 10 11 12 def q_sample (self, x_start, t, noise=None ): """ 前向扩散:从 q(x_t | x_0) 采样 实现公式: x_t = √ᾱ_t · x_0 + √(1-ᾱ_t) · ε """ if noise is None : noise = th.randn_like(x_start) return ( _extract_into_tensor(self .sqrt_alphas_cumprod, t, x_start.shape) * x_start + _extract_into_tensor(self .sqrt_one_minus_alphas_cumprod, t, x_start.shape) * noise )
4. 辅助函数:提取系数 (gaussian_diffusion.py:828-841 )
1 2 3 4 5 6 7 8 9 def _extract_into_tensor (arr, timesteps, broadcast_shape ): """ 从 1-D 数组中根据 timesteps 索引提取值,并广播到目标形状 例如:arr=[a0,a1,...,aT], timesteps=[3,5,2] → 提取 [a3,a5,a2] 并扩展维度 """ res = th.from_numpy(arr).to(device=timesteps.device)[timesteps].float () while len (res.shape) < len (broadcast_shape): res = res[..., None ] return res.expand(broadcast_shape)
公式对应 :
论文符号
代码变量
alphas_cumprod[t]
sqrt_alphas_cumprod[t]
sqrt_one_minus_alphas_cumprod[t]
x_start
noise
返回值
反向去噪 Intuition 数学原理表明,当 足够小时,每一步加噪声的逆操作也满足正态分布 :
这是一个关键的理论基础——它告诉我们反向过程的函数形式 是已知的(高斯分布),我们只需要学习它的参数 (均值 和方差 )。
如果我们知道精确的反向分布 ,就可以:
从纯噪声采样
逐步反向运行,得到 的样本
现在我们来用神经网络 来近似(均值 和方差 )。)
公式 (3): 神经网络建模反向过程 符号解释 :
符号
含义
由参数 的神经网络建模的分布
要生成的更清晰图像
当前噪声图像(条件)
网络预测的均值 (去噪后的期望值)
网络预测的协方差 (不确定性)
训练好之后怎么采样 :
我们可以把diffusion的前项abstract成一个编码器,和后向abstract成一个解码器。
就会发现diffusion就是一个大号的VAE。
后验分布的贝叶斯推导 直觉上我们需要让真实的后验分布( )和我们预测的分布 对齐(也就是让他们之间的散度变小),我们来简化后验分布( 的公式。
直接计算 需要整个数据分布,但如果我们知道 ,就可以用贝叶斯定理 :
由于马尔可夫性质, ,我们已知:
反向过程的真实后验 当我们知道 和 时,可以用贝叶斯定理计算反向一步的精确分布 ,我们得到后验方差和后验均值,此处省略推到,具体看附录。
均值的重参数化:预测噪声 问题 : 后验均值 依赖 ,但推理时我们没有 。
解决方案 : 从前向过程的重参数化公式出发,反推 :
解出 :
将这个代入后验均值 :
化简后得到均值的噪声形式 :
Intuition :
神经网络只需预测噪声
均值
这样就不需要显式预测 了
训练目标:预测噪声 神经网络学习预测添加的噪声 ,训练损失为:
其中:
训练流程 :
从训练集采样
随机采样时间步
采样噪声
计算
网络预测
计算损失 ,反向传播
推理流程 :
从纯噪声开始
对 :
返回
对应代码 预测噪声并恢复 x_0 gaussian_diffusion.py:328-349:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def _predict_xstart_from_eps (self, x_t, t, eps ): assert x_t.shape == eps.shape return ( _extract_into_tensor(self .sqrt_recip_alphas_cumprod, t, x_t.shape) * x_t - _extract_into_tensor(self .sqrt_recipm1_alphas_cumprod, t, x_t.shape) * eps ) def _predict_eps_from_xstart (self, x_t, t, pred_xstart ): return ( _extract_into_tensor(self .sqrt_recip_alphas_cumprod, t, x_t.shape) * x_t - pred_xstart ) / _extract_into_tensor(self .sqrt_recipm1_alphas_cumprod, t, x_t.shape)
其中预计算了 gaussian_diffusion.py:150-151:1 2 3 4 5 self .sqrt_recip_alphas_cumprod = np.sqrt(1.0 / self .alphas_cumprod)self .sqrt_recipm1_alphas_cumprod = np.sqrt(1.0 / self .alphas_cumprod - 1 )
训练损失 gaussian_diffusion.py:677-750:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 def training_losses (self, model, x_start, t, model_kwargs=None , noise=None ): if model_kwargs is None : model_kwargs = {} if noise is None : noise = th.randn_like(x_start) x_t = self .q_sample(x_start, t, noise=noise) model_output = model(x_t, self ._scale_timesteps(t), **model_kwargs) target = { ModelMeanType.PREVIOUS_X: self .q_posterior_mean_variance( x_start=x_start, x_t=x_t, t=t )[0 ], ModelMeanType.START_X: x_start, ModelMeanType.EPSILON: noise, }[self .model_mean_type] terms["mse" ] = mean_flat((target - model_output) ** 2 )
采样过程 gaussian_diffusion.py:356-387:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def p_sample (self, model, x, t, clip_denoised=True , denoised_fn=None , model_kwargs=None ): out = self .p_mean_variance( model, x, t, clip_denoised=clip_denoised, denoised_fn=denoised_fn, model_kwargs=model_kwargs, ) noise = th.randn_like(x) nonzero_mask = ( (t != 0 ).float ().view(-1 , *([1 ] * (len (x.shape) - 1 ))) ) sample = out["mean" ] + nonzero_mask * th.exp(0.5 * out["log_variance" ]) * noise return {"sample" : sample, "pred_xstart" : out["pred_xstart" ]}
系数预计算 gaussian_diffusion.py:153-169:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 self .posterior_variance = ( betas * (1.0 - self .alphas_cumprod_prev) / (1.0 - self .alphas_cumprod) ) self .posterior_log_variance_clipped = np.log( np.append(self .posterior_variance[1 ], self .posterior_variance[1 :]) ) self .posterior_mean_coef1 = ( betas * np.sqrt(self .alphas_cumprod_prev) / (1.0 - self .alphas_cumprod) ) self .posterior_mean_coef2 = ( (1.0 - self .alphas_cumprod_prev) * np.sqrt(alphas) / (1.0 - self .alphas_cumprod) )
计算后验分布 gaussian_diffusion.py:208-230:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def q_posterior_mean_variance (self, x_start, x_t, t ): """ Compute the mean and variance of the diffusion posterior: q(x_{t-1} | x_t, x_0) """ posterior_mean = ( _extract_into_tensor(self .posterior_mean_coef1, t, x_t.shape) * x_start + _extract_into_tensor(self .posterior_mean_coef2, t, x_t.shape) * x_t ) posterior_variance = _extract_into_tensor(self .posterior_variance, t, x_t.shape) posterior_log_variance_clipped = _extract_into_tensor( self .posterior_log_variance_clipped, t, x_t.shape ) return posterior_mean, posterior_variance, posterior_log_variance_clipped
使用后验分布 gaussian_diffusion.py:232-326:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 def p_mean_variance (self, model, x, t, clip_denoised=True , denoised_fn=None , model_kwargs=None ): model_output = model(x, self ._scale_timesteps(t), **model_kwargs) if self .model_mean_type == ModelMeanType.START_X: pred_xstart = process_xstart(model_output) else : pred_xstart = process_xstart( self ._predict_xstart_from_eps(x_t=x, t=t, eps=model_output) ) if clip_denoised: pred_xstart = pred_xstart.clamp(-1 , 1 ) model_mean, _, _ = self .q_posterior_mean_variance( x_start=pred_xstart, x_t=x, t=t ) return { "mean" : model_mean, "variance" : model_variance, "log_variance" : model_log_variance, "pred_xstart" : pred_xstart, }
公式对应表
论文符号
代码变量
含义
alphas_cumprod[t]
累积信号保留率
alphas_cumprod_prev[t]
前一步的累积信号保留率
betas[t]
前向噪声调度
alphas[t]
sqrt_alphas_cumprod[t]
信号系数
sqrt_one_minus_alphas_cumprod[t]
噪声系数
sqrt_recip_alphas_cumprod[t]
恢复 的系数
sqrt_recipm1_alphas_cumprod[t]
噪声恢复系数
posterior_variance[t]
后验方差 (公式 10)
posterior_mean
后验均值 (公式 11)
$q(x_{t-1} \
x_t, x_0)$
q_posterior_mean_variance()
后验分布 (公式 12)
model_output (when model_mean_type=EPSILON)
网络预测的噪声
的预测
pred_xstart
从噪声恢复的
优化目标 以上介绍了前向过程、反向过程以及训练时预测噪声的损失函数,直觉上我们能够基本确定让噪声接近对方几乎就是对的,但我们还没有数学上的解释。
扩散概率模型名字之所以有”概率”二字,是因为这个模型是在描述一个系统的概率。准确来说,扩散模型是在描述反向过程生成出某一项数据的概率。也就是说,扩散模型 是一个有着可训练参数 的模型,它描述了反向过程生成出数据 的概率。 满足 ,其中 就是我们熟悉的反向过程,只不过它是以概率计算的形式表达:
上一节里见到的优化目标,是让去噪声操作 和加噪声操作的逆操作 尽可能相似。然而,这个描述并不确切。扩散模型原本的目标,是最大化 这个概率,其中 是来自训练集的数据。换个角度说,给定一个训练集的数据 ,经过前向过程和反向过程,扩散模型要让复原出 的概率尽可能大。这也是我们在本文开头认识 VAE 时见到的优化目标。
变分下界(VLB) 最大化 ,一般会写成最小化其负对数值,即最小化 。使用和 VAE 类似的变分推理,可以把优化目标转换成优化一个叫做变分下界 (variational lower bound, VLB)的量。它最终可以写成:
这里的 表示分布 P 和 Q 之间的 KL 散度。如果 , 都是正态分布,则它们的 KL 散度可以由一个简单的公式给出。
其中,第一项 和可学习参数 无关(因为可学习参数只描述了每一步去噪声操作,也就是只描述了 ),可以不去管它。那么这个优化目标就由两部分组成:
最小化 :表示的是最大化每一个去噪声操作和加噪声逆操作的相似度
最小化 :就是已知 时,让最后复原原图 概率更高
KL 散度项的简化 我们来看第一部分是怎么计算的。首先回顾一下正态分布之间的 KL 散度公式。设一维正态分布 , 的公式如下:
则:
而对于 ,根据前文的分析,我们知道,待求方差 可以直接由计算得到:
两个正态分布方差的比值是常量。所以,在计算 KL 散度时,不用管方差那一项了,只需要均值那一项:
由根据之前的均值公式:
这一部分的优化目标可以化简成:
DDPM 论文指出,如果把前面的系数全部丢掉的话,模型的效果更好。最终,我们就能得一个非常简单的优化目标:
这就是我们上一节见到的优化目标。
重构项 还优化目标里还有 这一项。它的形式为:
只管后面有 的那一项(注意, ):
由于 ,代入后化简:
这和那些 KL 散度项 时的形式相同,我们可以用相同的方式简化优化目标,只保留 。这样,损失函数的形式全都是 了。
附录:后验分布 的数学推导 目标 :从贝叶斯公式出发,推导出后验均值 和后验方差 。
Step 1: 贝叶斯公式 三个已知的高斯分布:
Step 2: 写出概率密度的指数部分 高斯分布 的概率密度正比于 。
后验分布的指数部分为(忽略归一化常数):
Step 3: 展开并整理成关于 的二次型 展开括号内的表达式,只保留与 相关的项:
整理成标准形式 :
由于 ,分子化简:
所以:
Step 4: 得到后验方差 对于高斯分布, 的系数给出方差:
Step 5: 得到后验均值 高斯分布的均值 :
代入 :
最终结果 其中:
reference
https://lilianweng.github.io/posts/2021-07-11-diffusion-models/
https://zhuanlan.zhihu.com/p/638442430
https://www.zhihu.com/question/658056360