差别
这里会显示出您选择的修订版和当前版本之间的差别。
| 两侧同时换到之前的修订记录 前一修订版 后一修订版 | 前一修订版 | ||
| 深度学习:扩散模型:class1 [2026/03/13 13:33] – 张叶安 | 深度学习:扩散模型:class1 [2026/04/01 13:15] (当前版本) – [前向过程单步公式讲解] 张叶安 | ||
|---|---|---|---|
| 行 1: | 行 1: | ||
| + | ====== 显示图片 ====== | ||
| + | |||
| < | < | ||
| import torch # 导入 PyTorch 库,用于张量操作 | import torch # 导入 PyTorch 库,用于张量操作 | ||
| 行 46: | 行 48: | ||
| TensorFlow:(𝐻, | TensorFlow:(𝐻, | ||
| + | |||
| + | ====== 对称处理 ====== | ||
| + | |||
| 为了让数据和高斯噪声处在同一个零中心对称空间里,要对像素值进行仿射变换: | 为了让数据和高斯噪声处在同一个零中心对称空间里,要对像素值进行仿射变换: | ||
| 行 54: | 行 59: | ||
| 现在想它取值范围为[-1, | 现在想它取值范围为[-1, | ||
| + | |||
| + | 变换形式为$y=ax+b$ | ||
| + | |||
| + | 当$x=0$时,$y=-1$, | ||
| + | |||
| + | $y=2x-1$, | ||
| 仿射变换矩阵为: | 仿射变换矩阵为: | ||
| 行 131: | 行 142: | ||
| 可以看出图片的形状没有变 | 可以看出图片的形状没有变 | ||
| + | ====== 扩散过程 ====== | ||
| 扩散过程是基于一个**方差调度表**构建的,这个调度表决定了在扩散过程的每一个时间步中加入噪声的强度。为此,我们定义如下几个量: | 扩散过程是基于一个**方差调度表**构建的,这个调度表决定了在扩散过程的每一个时间步中加入噪声的强度。为此,我们定义如下几个量: | ||
| - | * `betas`:$\beta_t$ | + | * betas:$\beta_t$ |
| - | * `alphas`:$\alpha_t = 1 - \beta_t$ | + | * alphas:$\alpha_t = 1 - \beta_t$ |
| - | * `alphas_sqrt`:$\sqrt{\alpha_t}$ | + | * alphas_sqrt:$\sqrt{\alpha_t}$ |
| - | * `alphas_prod`:$\bar{\alpha}_t = \prod_{i=0}^{t}\alpha_i$ | + | * alphas_prod:$\bar{\alpha}_t = \prod_{i=0}^{t}\alpha_i$ |
| - | * `alphas_prod_sqrt`:$\sqrt{\bar{\alpha}_t}$ | + | * alphas_prod_sqrt:$\sqrt{\bar{\alpha}_t}$ |
| 行 144: | 行 156: | ||
| - | 1. `betas`:$\beta_t$ | + | 1. betas:$\beta_t$ |
| 它表示**第 $t$ 步加入噪声的强度**,也可以理解为该步的“噪声方差比例”。 | 它表示**第 $t$ 步加入噪声的强度**,也可以理解为该步的“噪声方差比例”。 | ||
| 行 158: | 行 170: | ||
| - | 2. `alphas`:$\alpha_t = 1 - \beta_t$ | + | 2. alphas:$\alpha_t = 1 - \beta_t$ |
| 它表示**第 $t$ 步保留下来的原始信号比例**。 | 它表示**第 $t$ 步保留下来的原始信号比例**。 | ||
| 行 179: | 行 191: | ||
| - | 3. `alphas_sqrt`:$\sqrt{\alpha_t}$ | + | 3. alphas_sqrt:$\sqrt{\alpha_t}$ |
| 这个量是在实际加噪公式里直接使用的系数。 | 这个量是在实际加噪公式里直接使用的系数。 | ||
| 行 191: | 行 203: | ||
| * $x_{t-1}$ 是上一步图像 | * $x_{t-1}$ 是上一步图像 | ||
| * $x_t$ 是当前这一步加噪后的图像 | * $x_t$ 是当前这一步加噪后的图像 | ||
| - | * $\epsilon_t \sim \mathcal{N}(0, | + | * $\epsilon_t \sim \mathcal{N}(0, |
| **直观理解:** | **直观理解:** | ||
| 行 197: | 行 209: | ||
| $\sqrt{\alpha_t}$ 是**上一步图像在当前步中的保留权重**。 | $\sqrt{\alpha_t}$ 是**上一步图像在当前步中的保留权重**。 | ||
| - | 之所以取平方根,是因为扩散模型里控制的是**方差**,而真正乘在样本上的系数是标准差,所以会出现平方根。 | + | 之所以取平方根,是因为扩散模型里控制的是[[概率论: |
| + | 注:变量乘以标准差,才能得到对应的方差。如X=> | ||
| - | 4. `alphas_prod`:$\bar{\alpha}_t = \prod_{i=0}^{t}\alpha_i$ | + | |
| + | |||
| + | |||
| + | 4. alphas_prod:$\bar{\alpha}_t = \prod_{i=0}^{t}\alpha_i$ | ||
| 它表示从第 $0$ 步到第 $t$ 步,**所有保留比例连乘之后的结果**。 | 它表示从第 $0$ 步到第 $t$ 步,**所有保留比例连乘之后的结果**。 | ||
| 行 218: | 行 234: | ||
| - | 5. `alphas_prod_sqrt`:$\sqrt{\bar{\alpha}_t}$ | + | 5. alphas_prod_sqrt:$\sqrt{\bar{\alpha}_t}$ |
| 这个量是从原图 $x_0$ 直接生成第 $t$ 步噪声图 $x_t$ 时的关键系数。 | 这个量是从原图 $x_0$ 直接生成第 $t$ 步噪声图 $x_t$ 时的关键系数。 | ||
| 行 559: | 行 575: | ||
| - | 反向过程 | + | ====== |
| 反向过程 $p$ 的目的是:根据扩散链中的当前样本 $x_t$,去近似恢复前一步的样本 $x_{t-1}$。在实际情况下,这种近似 $p(x_{t-1}|x_t)$ 必须在不知道 $x_0$ 的前提下完成。 | 反向过程 $p$ 的目的是:根据扩散链中的当前样本 $x_t$,去近似恢复前一步的样本 $x_{t-1}$。在实际情况下,这种近似 $p(x_{t-1}|x_t)$ 必须在不知道 $x_0$ 的前提下完成。 | ||
| 行 683: | 行 700: | ||
| {{.: | {{.: | ||
| + | |||
| + | |||
| + | |||
| + | 如果噪声 $\epsilon$ 能够被正确预测,那么我们就可以利用公式 $x_0=\frac{1}{\sqrt{\bar{\alpha}_t}}(x_t-\sqrt{1-\bar{\alpha}_t}\epsilon)$来预测 $x_0$: | ||
| + | |||
| + | <code python> | ||
| + | import torch # 导入 PyTorch 库,用于张量操作 | ||
| + | import torch.nn.functional as F # 导入 PyTorch 的函数式 API | ||
| + | import numpy as np # 导入 NumPy 库,用于数值计算 | ||
| + | import matplotlib as mpl # 导入 Matplotlib 库的主模块 | ||
| + | import matplotlib.pyplot as plt # 导入 Matplotlib 的绘图模块 | ||
| + | import imageio.v2 as imageio | ||
| + | |||
| + | mpl.rcParams[' | ||
| + | |||
| + | # 读取图片,像素值归一化到[0, | ||
| + | img = torch.FloatTensor(imageio.imread(' | ||
| + | |||
| + | # 值域转换函数:将图像从 [0,1] 转换到 [-1, | ||
| + | def input_T(input): | ||
| + | # [0,1] -> [-1,+1] | ||
| + | return 2 * input - 1 | ||
| + | |||
| + | # 值域转换函数:将图像从 [-1,+1] 转换到 [0, | ||
| + | def output_T(input): | ||
| + | # [-1,+1] -> [0,1] | ||
| + | return (input + 1) / 2 | ||
| + | |||
| + | # 显示图像函数,自动转换值域并裁剪到 [0,1] | ||
| + | def show(input): | ||
| + | plt.imshow(output_T(input).clip(0, | ||
| + | |||
| + | |||
| + | # ========== 扩散模型超参数设置 ========== | ||
| + | T = 100 # 时间步总数 | ||
| + | |||
| + | # 定义 beta 调度(线性调度) | ||
| + | betas = torch.linspace(0.0001, | ||
| + | |||
| + | # 计算 alpha 相关参数 | ||
| + | alphas = 1 - betas # alpha_t = 1 - beta_t | ||
| + | alphas_cumprod = torch.cumprod(alphas, | ||
| + | alphas_cumprod_sqrt = torch.sqrt(alphas_cumprod) | ||
| + | |||
| + | # 设置前向跳转的时间步 | ||
| + | t_step = 50 | ||
| + | |||
| + | |||
| + | def forward_jump(t, | ||
| + | """ | ||
| + | forward jump: 0 -> t | ||
| + | 直接跳转加噪:从初始时刻 0 直接加噪到时刻 t | ||
| + | | ||
| + | 根据扩散模型公式:x_t = √ᾱ_t * x_0 + √(1-ᾱ_t) * ε | ||
| + | 其中 ε ~ N(0, I) | ||
| + | """ | ||
| + | assert t >= 0 | ||
| + | |||
| + | mean = alphas_cumprod_sqrt[t] * condition_img | ||
| + | std = (1 - alphas_cumprod[t]).sqrt() | ||
| + | |||
| + | # 从正态分布采样 | ||
| + | if not return_noise: | ||
| + | return mean + std * torch.randn_like(condition_img) | ||
| + | else: | ||
| + | noise = torch.randn_like(condition_img) | ||
| + | return mean + std * noise, noise | ||
| + | |||
| + | |||
| + | # ========== 主程序 ========== | ||
| + | if __name__ == " | ||
| + | # 将图像转换到 [-1, 1] 值域 | ||
| + | img_ = input_T(img) | ||
| + | |||
| + | # 执行前向跳转加噪,获取加噪后的图像和噪声 | ||
| + | x_t, noise = forward_jump(t_step, | ||
| + | |||
| + | # 根据 x_t 和噪声预测 x_0 | ||
| + | # 由 x_t = √ᾱ_t * x_0 + √(1-ᾱ_t) * ε 可得: | ||
| + | # x_0 = (x_t - √(1-ᾱ_t) * ε) / √ᾱ_t | ||
| + | x_0_pred = (x_t - (1 - alphas_cumprod[t_step]).sqrt() * noise) / alphas_cumprod_sqrt[t_step] | ||
| + | |||
| + | # 可视化结果:x_t(加噪图像)、x_0 预测值、原始 x_0 | ||
| + | plt.subplot(1, | ||
| + | show(x_t) | ||
| + | plt.title(r' | ||
| + | plt.axis(' | ||
| + | |||
| + | plt.subplot(1, | ||
| + | show(x_0_pred) | ||
| + | plt.title(r' | ||
| + | plt.axis(' | ||
| + | |||
| + | plt.subplot(1, | ||
| + | show(img_) | ||
| + | plt.title(r' | ||
| + | plt.axis(' | ||
| + | |||
| + | plt.suptitle(f' | ||
| + | plt.tight_layout() | ||
| + | plt.show() | ||
| + | |||
| + | </ | ||
| + | |||
| + | |||
| + | {{.: | ||
| + | |||
| + | 这里的含义是: | ||
| + | |||
| + | * 第一幅图显示当前带噪样本 $x_t$ | ||
| + | * 第二幅图显示根据预测噪声反推出的 $x_0$,记作 $x_0\_pred$ | ||
| + | * 第三幅图显示真实的干净样本 $x_0$ | ||
| + | |||
| + | 如果预测的噪声足够准确,那么 $x_0\_pred$ 就会接近真实的 $x_0$。 | ||
| + | |||
| + | |||
| + | |||
| + | 近似或已知 $x_0$ 后,可以进一步近似 $t-1$ 步的均值 | ||
| + | |||
| + | 一旦我们得到了对 $x_0$ 的近似(或者直接知道真实的 $x_0$),就可以利用公式 (4) 来近似第 $t-1$ 步对应分布的均值: | ||
| + | |||
| + | <code python> | ||
| + | # estimate mean | ||
| + | mean_pred = x_0_pred * (alphas_cumprod_sqrt[t_step-1] * betas[t_step]) / (1 - alphas_cumprod[t_step]) \ | ||
| + | + x_t * (alphas_sqrt[t_step] * (1 - alphas_cumprod[t_step-1])) / (1 - alphas_cumprod[t_step]) | ||
| + | |||
| + | # let's compare it to ground truth mean of the previous step (requires knowledge of x_0) | ||
| + | mean_gt = alphas_cumprod_sqrt[t_step-1] * img_ | ||
| + | </ | ||
| + | |||
| + | 其中: | ||
| + | |||
| + | * `mean_pred`:根据预测得到的 $x_0$ 和当前样本 $x_t$ 计算出的反向过程均值近似 | ||
| + | * `mean_gt`:真实的前一步均值,它需要知道真实的 $x_0$ | ||
| + | |||
| + | 这里的 `mean_gt` 本质上对应的是前向过程中从干净图像得到的“真实均值”。 | ||
| + | |||
| + | ---- | ||
| + | |||
| + | 为什么 `mean_pred` 的误差通常会更大 | ||
| + | |||
| + | 由于公式 (4) 中的反向过程均值估计 $\tilde{\mu}_\theta$,本质上是对带噪的 $x_t$ 和估计出来的 $x_0$ 做一种线性组合,因此它通常会比前向过程直接得到的均值误差更大。 | ||
| + | |||
| + | 原因是: | ||
| + | |||
| + | * $x_t$ 本身仍然含有噪声 | ||
| + | * $x_0\_pred$ 只是估计值,不是真实值 | ||
| + | * 因此两者线性组合得到的均值仍然会带来额外误差 | ||
| + | |||
| + | 而前向过程中的均值通常只是对干净样本乘上一个标量,因此更精确。 | ||
| + | |||
| + | 下面的代码展示了这种比较: | ||
| + | |||
| + | <code python> | ||
| + | plt.subplot(1, | ||
| + | show(x_t) | ||
| + | plt.title(' | ||
| + | |||
| + | plt.subplot(1, | ||
| + | show(mean_pred) | ||
| + | plt.title(r' | ||
| + | |||
| + | plt.subplot(1, | ||
| + | show(mean_gt) | ||
| + | plt.title(r' | ||
| + | </ | ||
| + | |||
| + | {{.: | ||
| + | |||
| + | 这三幅图分别表示: | ||
| + | |||
| + | * 左图:当前带噪图像 $x_t$ | ||
| + | * 中图:反向过程估计的均值 $\tilde{\mu}_{t-1}$ | ||
| + | * 右图:真实均值 $\mu_{t-1}$ | ||
| + | |||
| + | 比较它们的 $\ell_1$ 误差,就可以看出估计值和真实值之间的差别。 | ||
| + | |||
| + | ---- | ||
| + | |||
| + | 得到 `mean_pred` 之后,就可以定义前一步的分布 | ||
| + | |||
| + | 当我们获得了 `mean_pred`,也就是 $\tilde{\mu}_t$ 之后,就可以将前一步的条件分布定义为: | ||
| + | |||
| + | $$\tilde{\beta}_t = \beta_t \tag{6}$$ | ||
| + | |||
| + | $$p_\theta(x_{t-1}|x_t) := \mathcal{N}(x_{t-1}; | ||
| + | |||
| + | 这个定义表示: | ||
| + | |||
| + | * 均值使用网络估计出来的 $\tilde{\mu}_\theta(x_t, | ||
| + | * 方差使用 $\tilde{\beta}_t$ | ||
| + | * 从而构造出反向过程中的高斯分布 | ||
| + | |||
| + | |||
| + | |||
| + | 重要说明 | ||
| + | |||
| + | > 下面的实验应被看作一种模拟。 | ||
| + | > 在实际训练和推理中,网络必须预测以下三者之一: | ||
| + | > * $\epsilon$ | ||
| + | > * $x_0$ | ||
| + | > * $\tilde{\mu}_\theta$ | ||
| + | > | ||
| + | > 而这里的 $\epsilon$ 值只是直接代入使用的,并不是真实模型预测出来的结果。 | ||
| + | |||
| + | |||
| + | |||
| + | `reverse_step` 函数 | ||
| + | |||
| + | 下面这个函数实现了一次反向扩散步骤: | ||
| + | |||
| + | <code python> | ||
| + | def reverse_step(epsilon, | ||
| + | | ||
| + | # estimate x_0 based on epsilon | ||
| + | x_0_pred = (x_t - (1 - alphas_cumprod[t_step]).sqrt() * epsilon) / (alphas_cumprod_sqrt[t_step]) | ||
| + | |||
| + | if t_step == 0: | ||
| + | sample = x_0_pred | ||
| + | noise = torch.zeros_like(x_0_pred) | ||
| + | else: | ||
| + | # estimate mean | ||
| + | mean_pred = x_0_pred * (alphas_cumprod_sqrt[t_step-1] * betas[t_step]) / (1 - alphas_cumprod[t_step]) \ | ||
| + | + x_t * (alphas_sqrt[t_step] * (1 - alphas_cumprod[t_step-1])) / (1 - alphas_cumprod[t_step]) | ||
| + | |||
| + | # compute variance | ||
| + | beta_pred = betas[t_step].sqrt() if t_step != 0 else 0 | ||
| + | |||
| + | sample = mean_pred + beta_pred * torch.randn_like(x_t) | ||
| + | |||
| + | # this noise is only computed for simulation purposes (since x_0_pred is not known normally) | ||
| + | noise = (sample - x_0_pred * alphas_cumprod_sqrt[t_step-1]) / (1 - alphas_cumprod[t_step-1]).sqrt() | ||
| + | |||
| + | if return_noise: | ||
| + | return sample, noise | ||
| + | else: | ||
| + | return sample | ||
| + | </ | ||
| + | |||
| + | ---- | ||
| + | |||
| + | `reverse_step()` 的逻辑讲解 | ||
| + | |||
| + | 1. 根据 $\epsilon$ 先恢复 $x_0$ | ||
| + | |||
| + | <code python> | ||
| + | x_0_pred = (x_t - (1 - alphas_cumprod[t_step]).sqrt() * epsilon) / (alphas_cumprod_sqrt[t_step]) | ||
| + | </ | ||
| + | |||
| + | 这一行对应公式 (5): | ||
| + | |||
| + | $$x_0=\frac{1}{\sqrt{\bar{\alpha}_t}}(x_t-\sqrt{1-\bar{\alpha}_t}\epsilon)$$ | ||
| + | |||
| + | 作用是: | ||
| + | |||
| + | * 输入当前带噪样本 $x_t$ | ||
| + | * 输入预测得到的噪声 $\epsilon$ | ||
| + | * 反推出干净图像的估计值 $x_0$ | ||
| + | |||
| + | ---- | ||
| + | |||
| + | 2. 如果已经到达 $t=0$ | ||
| + | |||
| + | <code python> | ||
| + | if t_step == 0: | ||
| + | sample = x_0_pred | ||
| + | noise = torch.zeros_like(x_0_pred) | ||
| + | </ | ||
| + | |||
| + | 当 `t_step == 0` 时,说明已经到达反向过程的最后一步,不需要再继续去噪。 | ||
| + | |||
| + | 因此: | ||
| + | |||
| + | * 直接把 $x_0\_pred$ 当作最终结果 | ||
| + | * 此时噪声设为全零 | ||
| + | |||
| + | ---- | ||
| + | |||
| + | 3. 如果 $t > 0$,先估计反向均值 | ||
| + | |||
| + | <code python> | ||
| + | mean_pred = x_0_pred * (alphas_cumprod_sqrt[t_step-1] * betas[t_step]) / (1 - alphas_cumprod[t_step]) \ | ||
| + | + x_t * (alphas_sqrt[t_step] * (1 - alphas_cumprod[t_step-1])) / (1 - alphas_cumprod[t_step]) | ||
| + | </ | ||
| + | |||
| + | 这一行对应公式 (4): | ||
| + | |||
| + | $$\tilde{\mu}_\theta = \frac{\sqrt{\bar{\alpha}_{t-1}}\beta_t}{1-\bar{\alpha}_t}x_0 + \frac{\sqrt{\alpha_t}(1-\bar{\alpha}_{t-1})}{1-\bar{\alpha}_t}x_t$$ | ||
| + | |||
| + | 只不过代码里用的是预测值 `x_0_pred` 来代替真实的 $x_0$。 | ||
| + | |||
| + | 意思是: | ||
| + | |||
| + | * 综合当前带噪样本 $x_t$ | ||
| + | * 再结合恢复出的干净样本估计 $x_0\_pred$ | ||
| + | * 得到前一步分布的均值估计 `mean_pred` | ||
| + | |||
| + | ---- | ||
| + | |||
| + | 4. 计算标准差 | ||
| + | |||
| + | <code python> | ||
| + | beta_pred = betas[t_step].sqrt() if t_step != 0 else 0 | ||
| + | </ | ||
| + | |||
| + | 这里使用: | ||
| + | |||
| + | * 方差 $\tilde{\beta}_t = \beta_t$ | ||
| + | * 所以标准差为 $\sqrt{\beta_t}$ | ||
| + | |||
| + | 这是为了后面从高斯分布中采样: | ||
| + | |||
| + | $$x_{t-1} \sim \mathcal{N}(\tilde{\mu}_\theta, | ||
| + | |||
| + | ---- | ||
| + | |||
| + | 5. 从反向分布中采样 | ||
| + | |||
| + | <code python> | ||
| + | sample = mean_pred + beta_pred * torch.randn_like(x_t) | ||
| + | </ | ||
| + | |||
| + | 这一步就是从高斯分布中真正采样出 $x_{t-1}$。 | ||
| + | |||
| + | 也就是: | ||
| + | |||
| + | * 均值为 `mean_pred` | ||
| + | * 标准差为 `beta_pred` | ||
| + | * 加上一份标准高斯噪声 | ||
| + | |||
| + | 对应采样形式可以写成: | ||
| + | |||
| + | $$x_{t-1} = \tilde{\mu}_\theta + \sqrt{\tilde{\beta}_t}\, | ||
| + | |||
| + | ---- | ||
| + | |||
| + | 6. 计算 `noise` 仅用于模拟分析 | ||
| + | |||
| + | <code python> | ||
| + | noise = (sample - x_0_pred * alphas_cumprod_sqrt[t_step-1]) / (1 - alphas_cumprod[t_step-1]).sqrt() | ||
| + | </ | ||
| + | |||
| + | 这里的 `noise` 只是为了实验分析而额外算出来的。 | ||
| + | |||
| + | 因为在真实的反向扩散推理中: | ||
| + | |||
| + | * 我们并不知道真实的 $x_0$ | ||
| + | * 这里只是因为做实验,才利用 `x_0_pred` 反推一个噪声值 | ||
| + | |||
| + | 所以注释里才特别说明: | ||
| + | |||
| + | * 这个噪声只是为了模拟目的而计算 | ||
| + | * 并不是实际系统中能直接获得的量 | ||
| + | |||
| + | |||
| + | |||
| + | 7. 是否返回噪声 | ||
| + | |||
| + | <code python> | ||
| + | if return_noise: | ||
| + | return sample, noise | ||
| + | else: | ||
| + | return sample | ||
| + | </ | ||
| + | |||
| + | 如果 `return_noise=True`: | ||
| + | |||
| + | * 返回当前反向步骤采样结果 `sample` | ||
| + | * 同时返回额外计算的 `noise` | ||
| + | |||
| + | 否则: | ||
| + | |||
| + | * 只返回 `sample` | ||
| + | |||
| + | |||
| + | |||
| + | |||
| + | |||
| + | 这个 `reverse_step()` 函数做的事情是: | ||
| + | |||
| + | * 先根据预测噪声 $\epsilon$ 还原出 $x_0$ | ||
| + | * 再根据 $x_t$ 和 $x_0$ 的估计值计算反向分布均值 | ||
| + | * 然后从这个高斯分布中采样得到 $x_{t-1}$ | ||
| + | |||
| + | 也就是说,它实现了扩散模型中的一次“去噪反推”。 | ||
| + | |||
| + | |||
| + | {{.: | ||
| + | |||
| + | {{.: | ||