====== 显示图片 ====== 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 # 导入 imageio 库,用于读取图片文件 mpl.rcParams['figure.figsize'] = (12, 8) # 设置 Matplotlib 图表默认尺寸为 12x8 英寸 img = torch.FloatTensor(imageio.imread('imgs/hills_2.png')/255) # 读取图片,像素值归一化到[0,1],转为 PyTorch 浮点张量 print(type(img)) plt.imshow(img) # 使用 Matplotlib 显示图片 plt.show() 将上述文件保存为1.py,执行后将hills_2.png显示出来。 {{.:pasted:20260311-135159.png?720}} RGB一共有三个通道 R:Red(红色) G:Green(绿色) B:Blue(蓝色) 其取值范围为[0,255] 例如: 黑色:(0,0,0) 白色:(255,255,255) 红色:(255,0,0) 蓝色:(0,0,255) 如果图像尺寸是 𝐻×𝑊 常见 tensor 形状: PyTorch:(3,𝐻,𝑊) TensorFlow:(𝐻,𝑊,3) ====== 对称处理 ====== 为了让数据和高斯噪声处在同一个零中心对称空间里,要对像素值进行仿射变换: 原像素值取值范围为[0,255] 归一化(就是除以255),取值范围为[0,1] 现在想它取值范围为[-1,1] 变换形式为$y=ax+b$ 当$x=0$时,$y=-1$,当$x=1$时,$y=1$ $y=2x-1$,$x=\frac{1}{2}y+\frac{1}{2}$ 仿射变换矩阵为: $$ \begin{bmatrix} y \\ 1 \end{bmatrix} = \begin{bmatrix} 2 & -1 \\ 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ 1 \end{bmatrix} $$ $$ \begin{bmatrix} x \\ 1 \end{bmatrix} = \begin{bmatrix} \frac{1}{2} & \frac{1}{2} \\ 0 & 1 \end{bmatrix} \begin{bmatrix} y \\ 1 \end{bmatrix} $$ 写成代码2.py 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 # 导入 imageio 库,用于读取图片文件 mpl.rcParams['figure.figsize'] = (12, 8) # 设置 Matplotlib 图表默认尺寸为 12x8 英寸 img = torch.FloatTensor(imageio.imread('imgs/hills_2.png')/255) # 读取图片,像素值归一化到[0,1],转为 PyTorch 浮点张量 print(type(img)) # 使用 Matplotlib 显示图片 def input_T(input): # [0,1] -> [-1,+1] return 2*input-1 def output_T(input): # [-1,+1] -> [0,1] return (input+1)/2 def show(input): plt.imshow(output_T(input).clip(0,1)) img_=input_T(img) plt.imshow(img_) #show(img_) plt.show() 这段代码改改玩,如果只进行变换,不进行逆变换 图片长这样 {{.:pasted:20260313-124519.png}} 可以看出图片的形状没有变 ====== 扩散过程 ====== 扩散过程是基于一个**方差调度表**构建的,这个调度表决定了在扩散过程的每一个时间步中加入噪声的强度。为此,我们定义如下几个量: * betas:$\beta_t$ * alphas:$\alpha_t = 1 - \beta_t$ * alphas_sqrt:$\sqrt{\alpha_t}$ * alphas_prod:$\bar{\alpha}_t = \prod_{i=0}^{t}\alpha_i$ * alphas_prod_sqrt:$\sqrt{\bar{\alpha}_t}$ 1. betas:$\beta_t$ 它表示**第 $t$ 步加入噪声的强度**,也可以理解为该步的“噪声方差比例”。 **直观理解:** * $\beta_t$ 越大,这一步加入的随机噪声越多 * $\beta_t$ 越小,这一步对原图破坏得越轻 所以,$\beta_t$ 控制的是: **当前这一步要往图像里掺多少噪声。** 2. alphas:$\alpha_t = 1 - \beta_t$ 它表示**第 $t$ 步保留下来的原始信号比例**。 **直观理解:** 因为每一步不是“全换成噪声”,而是: * 保留一部分原信号 * 再加入一部分新噪声 所以: * $\alpha_t$ 大,说明这一时刻保留原图信息更多 * $\alpha_t$ 小,说明这一时刻原图被削弱得更多 可以把它理解为: **这一轮之后,图像内容还能剩下多少。** 3. alphas_sqrt:$\sqrt{\alpha_t}$ 这个量是在实际加噪公式里直接使用的系数。 单步扩散通常写成: $$x_t = \sqrt{\alpha_t} \, x_{t-1} + \sqrt{1-\alpha_t} \, \epsilon_t$$ 其中: * $x_{t-1}$ 是上一步图像 * $x_t$ 是当前这一步加噪后的图像 * $\epsilon_t \sim \mathcal{N}(0, I)$ 是高斯噪声(符合[[概率论:随机变量及其分布#2.4.3 常见连续型分布|高斯分布]]的噪声) **直观理解:** $\sqrt{\alpha_t}$ 是**上一步图像在当前步中的保留权重**。 之所以取平方根,是因为扩散模型里控制的是[[概率论:数字特征#4.2 方差|方差]],而真正乘在样本上的系数是标准差,所以会出现平方根。 注:变量乘以标准差,才能得到对应的方差。如X=>3X,方差是原来的9倍。 4. alphas_prod:$\bar{\alpha}_t = \prod_{i=0}^{t}\alpha_i$ 它表示从第 $0$ 步到第 $t$ 步,**所有保留比例连乘之后的结果**。 **直观理解:** 如果每一步都保留一部分信号,那么经过很多步之后,原始图像 $x_0$ 还能剩多少,不是看某一步,而是看所有步骤累积起来的效果。 所以 $\bar{\alpha}_t$ 表示: **原始图像 $x_0$ 到第 $t$ 步时,整体还保留了多少比例的信号能量。** * 当 $t$ 很小时,$\bar{\alpha}_t$ 还比较大,说明原图信息还比较多 * 当 $t$ 很大时,$\bar{\alpha}_t$ 会变得很小,说明原图已经几乎被噪声淹没了 5. alphas_prod_sqrt:$\sqrt{\bar{\alpha}_t}$ 这个量是从原图 $x_0$ 直接生成第 $t$ 步噪声图 $x_t$ 时的关键系数。 扩散模型里一个非常重要的公式是: $$x_t = \sqrt{\bar{\alpha}_t} \, x_0 + \sqrt{1-\bar{\alpha}_t} \, \epsilon$$ **直观理解:** 这里: * $\sqrt{\bar{\alpha}_t}$ 控制**原始图像 $x_0$** 在 $x_t$ 中还占多少 * $\sqrt{1-\bar{\alpha}_t}$ 控制**累计噪声**占多少 所以 $\sqrt{\bar{\alpha}_t}$ 可以理解为: **从原图走到第 $t$ 步时,原图成分的缩放系数。** 扩散过程本质上就是不断重复下面这件事: - 保留一部分当前图像 - 加入一点高斯噪声 - 重复很多次,直到图像几乎变成纯噪声 其中: * $\beta_t$:这一轮加多少噪声 * $\alpha_t$:这一轮保留多少信号 * $\sqrt{\alpha_t}$:这一轮信号实际乘上的系数 * $\bar{\alpha}_t$:经过前 $t$ 步后,原图总共还剩多少 * $\sqrt{\bar{\alpha}_t}$:原图在第 $t$ 步中的实际缩放系数 最关键的两个公式 单步加噪 $$x_t = \sqrt{\alpha_t} \, x_{t-1} + \sqrt{1-\alpha_t} \, \epsilon_t$$ 表示从上一步 $x_{t-1}$ 走到当前步 $x_t$。 直接从原图采样到第 t 步 $$x_t = \sqrt{\bar{\alpha}_t} \, x_0 + \sqrt{1-\bar{\alpha}_t} \, \epsilon$$ 表示不用一步一步加,可以直接从原图 $x_0$ 得到任意时刻 $x_t$。 用代码表示为 num_timesteps=1000 betas=torch.linspace(1e-4,2e-2,num_timesteps) alphas=1-betas alphas_sqrt=alphas.sqrt() alphas_cumprod=torch.cumprod(alphas,0) alphas_cumprod_sqrt=alphas_cumprod.sqrt() 前向过程 $q$ 决定了扩散过程中后续各步是如何得到的,也就是如何对原始样本 $x_0$ 进行逐步扰动。 首先,我们给出描述这一过程的关键公式。 前向过程的单步基本形式: $q(x_t|x_{t-1}) := \mathcal{N}(x_t; \sqrt{1-\beta_t}x_{t-1}, \beta_t I) \tag{1}$ ====== 前向过程单步公式讲解 ====== 1. 公式整体是什么意思 $q(x_t|x_{t-1})$ 表示一个**条件概率分布**,读作: “在给定 $x_{t-1}$ 的条件下,$x_t$ 的分布”。 这里: * $q$:表示前向扩散过程 * $x_{t-1}$:第 $t-1$ 步的样本 * $x_t$:第 $t$ 步的样本 也就是说,这个式子定义了: **如何从上一步的样本 $x_{t-1}$ 随机生成当前步的样本 $x_t$。** 2. 符号 := 的含义 $q(x_t|x_{t-1}) := \mathcal{N}(\cdots)$ 这里的 $:=$ 表示: **“定义为”** 也就是说,不是在推导结论,而是在规定: **前向过程的单步转移分布就定义成右边这个高斯分布。** 右边的 $\mathcal{N}$ 是什么 $\mathcal{N}(x_t;\mu,\Sigma)$ 表示一个高斯分布,其中: * 第一个位置 $x_t$:表示随机变量 * 第二个位置 $\mu$:表示均值 * 第三个位置 $\Sigma$:表示协方差矩阵 所以: $\mathcal{N}(x_t;\sqrt{1-\beta_t}x_{t-1},\beta_t I)$ 表示: **$x_t$ 服从一个均值为 $\sqrt{1-\beta_t}x_{t-1}$、协方差为 $\beta_t I$ 的高斯分布。** 4. 公式中每个位置分别表示什么 公式为: $q(x_t|x_{t-1}) := \mathcal{N}(x_t;\sqrt{1-\beta_t}x_{t-1},\beta_t I)$ 可以按位置拆成三部分: 第 1 个位置:$x_t$ 这是高斯分布里的**随机变量**。 意思是: **我们要描述的是 $x_t$ 这个量的分布。** 也就是“当前步生成出来的样本是多少”。 第 2 个位置:$\sqrt{1-\beta_t}x_{t-1}$ 这是高斯分布的**均值**。 意思是: **$x_t$ 大致会围绕这个中心值波动。** 其中: * $x_{t-1}$:上一时刻的样本 * $\sqrt{1-\beta_t}$:对上一时刻样本的缩放系数 所以均值部分表示: **当前步会保留上一步的大部分信息,但会稍微缩小一点。** 如果 $\beta_t$ 很小,那么: * $1-\beta_t$ 很接近 $1$ * $\sqrt{1-\beta_t}$ 也很接近 $1$ 说明当前步和上一步会很接近,只是稍微被扰动一点。 第 3 个位置:$\beta_t I$ 这是高斯分布的**协方差矩阵**。 其中: * $\beta_t$:控制噪声强度 * $I$:单位矩阵 $\beta_t I$ 的意思是: * 每个维度上的噪声方差都是 $\beta_t$ * 不同维度之间相互独立 如果是在图像里理解,就是: **每个像素或每个特征维度都被加上独立的高斯噪声。** 5. 这个公式展开后是什么意思 这个分布形式可以等价地写成采样形式: $x_t = \sqrt{1-\beta_t}x_{t-1} + \sqrt{\beta_t}\,\epsilon_t$ 其中: $\epsilon_t \sim \mathcal{N}(0,I)$ 这就是这个公式最常见的“展开形式”。 从 $x_0$ 直接跳到 $x_t$ 的形式: $q(x_t|x_0) = \mathcal{N}(x_t; \sqrt{\bar{\alpha}_t}x_0, (1-\bar{\alpha}_t) I) \tag{2}$ 定义一个函数 `forward_step()` 下面我们定义一个函数 `forward_step()`,它可以让我们使用单步前向过程 $q(x_t|x_{t-1})$;同时再定义 `forward_jump()`,用于表示从 $x_0$ 直接到 $x_t$ 的过程 $q(x_t|x_0)$。 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 # 导入 imageio 库,用于读取图片文件 mpl.rcParams['figure.figsize'] = (12, 8) # 设置 Matplotlib 图表默认尺寸为 12x8 英寸 # 读取图片,像素值归一化到[0,1],转为 PyTorch 浮点张量 img = torch.FloatTensor(imageio.imread('imgs/hills_2.png')/255) print(type(img)) # 值域转换函数:将图像从 [0,1] 转换到 [-1,+1] def input_T(input): # [0,1] -> [-1,+1] return 2*input-1 # 值域转换函数:将图像从 [-1,+1] 转换到 [0,1] def output_T(input): # [-1,+1] -> [0,1] return (input+1)/2 # 显示图像函数,自动转换值域并裁剪到 [0,1] def show(input): plt.imshow(output_T(input).clip(0,1)) # ========== 扩散模型超参数设置 ========== T = 100 # 时间步总数 # 定义 beta 调度(线性调度) betas = torch.linspace(0.0001, 0.02, T) # 从 0.0001 线性增加到 0.02 # 计算 alpha 相关参数 alphas = 1 - betas # alpha_t = 1 - beta_t alphas_cumprod = torch.cumprod(alphas, dim=0) # 累积乘积:ᾱ_t = ∏(1-β_s) alphas_cumprod_sqrt = torch.sqrt(alphas_cumprod) # √ᾱ_t alphas_sqrt = torch.sqrt(alphas) # √α_t # ========== 加噪过程函数 ========== def forward_step(t, condition_img, return_noise=False): """ forward step: t-1 -> t 单步加噪:从前一时刻 t-1 加噪到当前时刻 t """ assert t >= 0 mean = alphas_sqrt[t] * condition_img std = betas[t].sqrt() # sampling from N if not return_noise: return mean + std * torch.randn_like(img) else: noise = torch.randn_like(img) return mean + std * noise, noise def forward_jump(t, condition_img, condition_idx=0, return_noise=False): """ forward jump: 0 -> t 直接跳转加噪:从初始时刻 0 直接加噪到时刻 t """ assert t >= 0 mean = alphas_cumprod_sqrt[t] * condition_img std = (1 - alphas_cumprod[t]).sqrt() # sampling from N if not return_noise: return mean + std * torch.randn_like(img) else: noise = torch.randn_like(img) return mean + std * noise, noise # ========== 测试加噪过程 ========== if __name__ == "__main__": # 将图像转换到 [-1, 1] 值域 img_ = input_T(img) # 测试 forward_jump:展示不同时间步的加噪效果 fig, axes = plt.subplots(2, 5, figsize=(15, 6)) # 显示原图 axes[0, 0].imshow(img) axes[0, 0].set_title('Original (t=0)') axes[0, 0].axis('off') # 显示不同时间步的加噪结果 timesteps = [10, 30, 50, 70, 90, 20, 40, 60, 80, 99] for idx, t in enumerate(timesteps): row = idx // 5 col = idx % 5 if row == 0 and col == 0: continue # 跳过原图位置 noisy_img = forward_jump(t, img_) axes[row, col].imshow(output_T(noisy_img).clip(0, 1)) axes[row, col].set_title(f't={t}') axes[row, col].axis('off') plt.tight_layout() plt.suptitle('Forward Diffusion Process (Jump)', y=1.02) plt.show() # 测试 forward_step:逐步加噪过程 fig, axes = plt.subplots(2, 5, figsize=(15, 6)) current_img = img_ for t in range(10): row = t // 5 col = t % 5 if t > 0: current_img = forward_step(t, current_img) axes[row, col].imshow(output_T(current_img).clip(0, 1)) axes[row, col].set_title(f'Step t={t}') axes[row, col].axis('off') plt.tight_layout() plt.suptitle('Forward Diffusion Process (Step by Step)', y=1.02) plt.show() print("加噪过程测试完成!") {{.:pasted:20260313-132147.png}} ====== 反向过程 ====== 反向过程 $p$ 的目的是:根据扩散链中的当前样本 $x_t$,去近似恢复前一步的样本 $x_{t-1}$。在实际情况下,这种近似 $p(x_{t-1}|x_t)$ 必须在不知道 $x_0$ 的前提下完成。 通常会使用一个带参数 $\theta$ 的可参数化预测模型,来估计 $p_\theta(x_{t-1}|x_t)$。 如果扩散的每一步足够小,那么反向过程也可以近似看作高斯分布: $$p_\theta(x_{t-1}|x_t) := \mathcal{N}(x_{t-1};\mu_\theta(x_t),\Sigma_\theta(x_t))\tag{3}$$ 在很多工作中,通常假设这个分布的方差不应强烈依赖于 $x_0$ 或 $x_t$,而应主要依赖于扩散过程所处的阶段 $t$。这一点可以从真实分布 $q(x_{t-1}|x_t, x_0)$ 中观察到,因为该分布的方差等于 $\tilde{\beta}_t$。 对 $\mu_\theta$ 的参数化 对于反向单步分布 $p_\theta(x_{t-1}|x_t)$ 的均值 $\mu_\theta$,至少有 3 种参数化方式: * 直接预测:由神经网络直接估计 $\mu_\theta$ * 通过 $x_0$ 参数化:由神经网络先估计 $x_0$ $$\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\tag{4}$$ * 通过从 $x_0$ 中减去噪声 $\epsilon$ 来参数化:由神经网络估计 $\epsilon$ $$x_0=\frac{1}{\sqrt{\bar{\alpha}_t}}(x_t-\sqrt{1-\bar{\alpha}_t}\epsilon)\tag{5}$$ 其中,最常用的方法是近似预测高斯噪声 $\epsilon$。 下面我们来看一个 $\epsilon$ 的示例可能长什么样: 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 # 导入 imageio 库,用于读取图片文件 mpl.rcParams['figure.figsize'] = (12, 8) # 设置 Matplotlib 图表默认尺寸 # 读取图片,像素值归一化到[0,1],转为 PyTorch 浮点张量 img = torch.FloatTensor(imageio.imread('imgs/hills_2.png') / 255) # 值域转换函数:将图像从 [0,1] 转换到 [-1,+1](扩散模型标准输入值域) def input_T(input): # [0,1] -> [-1,+1] return 2 * input - 1 # 值域转换函数:将图像从 [-1,+1] 转换到 [0,1](用于显示) def output_T(input): # [-1,+1] -> [0,1] return (input + 1) / 2 # 显示图像函数,自动转换值域并裁剪到 [0,1] def show(input): plt.imshow(output_T(input).clip(0, 1)) # ========== 扩散模型超参数设置 ========== T = 100 # 时间步总数 # 定义 beta 调度(线性调度) betas = torch.linspace(0.0001, 0.02, T) # 从 0.0001 线性增加到 0.02 # 计算 alpha 相关参数 alphas = 1 - betas # alpha_t = 1 - beta_t alphas_cumprod = torch.cumprod(alphas, dim=0) # 累积乘积:ᾱ_t = ∏(1-β_s) alphas_cumprod_sqrt = torch.sqrt(alphas_cumprod) # √ᾱ_t # 设置前向跳转的时间步 t_step = 50 def forward_jump(t, condition_img, condition_idx=0, return_noise=False): """ 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__ == "__main__": # 将图像转换到 [-1, 1] 值域 img_ = input_T(img) # 执行前向跳转加噪,获取加噪后的图像和噪声 x_t, noise = forward_jump(t_step, img_, return_noise=True) # 可视化结果 plt.subplot(1, 3, 1) show(img_) plt.title(r'$x_0$') plt.axis('off') plt.subplot(1, 3, 2) show(x_t) plt.title(r'$x_t$') plt.axis('off') plt.subplot(1, 3, 3) show(noise) plt.title(r'$\epsilon$') plt.axis('off') plt.suptitle(f'Forward Diffusion Process (t={t_step})', y=0.98) plt.tight_layout() plt.show() {{.:pasted:20260313-133231.png}} 如果噪声 $\epsilon$ 能够被正确预测,那么我们就可以利用公式 $x_0=\frac{1}{\sqrt{\bar{\alpha}_t}}(x_t-\sqrt{1-\bar{\alpha}_t}\epsilon)$来预测 $x_0$: 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 # 导入 imageio 库,用于读取图片文件 mpl.rcParams['figure.figsize'] = (12, 8) # 设置 Matplotlib 图表默认尺寸 # 读取图片,像素值归一化到[0,1],转为 PyTorch 浮点张量 img = torch.FloatTensor(imageio.imread('imgs/hills_2.png') / 255) # 值域转换函数:将图像从 [0,1] 转换到 [-1,+1](扩散模型标准输入值域) def input_T(input): # [0,1] -> [-1,+1] return 2 * input - 1 # 值域转换函数:将图像从 [-1,+1] 转换到 [0,1](用于显示) def output_T(input): # [-1,+1] -> [0,1] return (input + 1) / 2 # 显示图像函数,自动转换值域并裁剪到 [0,1] def show(input): plt.imshow(output_T(input).clip(0, 1)) # ========== 扩散模型超参数设置 ========== T = 100 # 时间步总数 # 定义 beta 调度(线性调度) betas = torch.linspace(0.0001, 0.02, T) # 从 0.0001 线性增加到 0.02 # 计算 alpha 相关参数 alphas = 1 - betas # alpha_t = 1 - beta_t alphas_cumprod = torch.cumprod(alphas, dim=0) # 累积乘积:ᾱ_t = ∏(1-β_s) alphas_cumprod_sqrt = torch.sqrt(alphas_cumprod) # √ᾱ_t # 设置前向跳转的时间步 t_step = 50 def forward_jump(t, condition_img, return_noise=False): """ 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__ == "__main__": # 将图像转换到 [-1, 1] 值域 img_ = input_T(img) # 执行前向跳转加噪,获取加噪后的图像和噪声 x_t, noise = forward_jump(t_step, img_, return_noise=True) # 根据 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, 3, 1) show(x_t) plt.title(r'$x_t$ ($\ell_1$: {:.3f})'.format(F.l1_loss(x_t, img_))) plt.axis('off') plt.subplot(1, 3, 2) show(x_0_pred) plt.title(r'$x_0$ prediction ($\ell_1$: {:.3f})'.format(F.l1_loss(x_0_pred, img_))) plt.axis('off') plt.subplot(1, 3, 3) show(img_) plt.title(r'$x_0$') plt.axis('off') plt.suptitle(f'x_0 Prediction from x_t (t={t_step})', y=0.98) plt.tight_layout() plt.show() {{.:pasted:20260313-134755.png}} 这里的含义是: * 第一幅图显示当前带噪样本 $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$ 步对应分布的均值: # 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$ 只是估计值,不是真实值 * 因此两者线性组合得到的均值仍然会带来额外误差 而前向过程中的均值通常只是对干净样本乘上一个标量,因此更精确。 下面的代码展示了这种比较: plt.subplot(1,3,1) show(x_t) plt.title('$x_t$ ($\ell_1$: {:.3f})'.format(F.l1_loss(x_t, img_))) plt.subplot(1,3,2) show(mean_pred) plt.title(r'$\tilde{\mu}_{t-1}$' + ' ($\ell_1$: {:.3f})'.format(F.l1_loss(mean_pred, img_))) plt.subplot(1,3,3) show(mean_gt) plt.title(r'$\mu_{t-1}$' + ' ($\ell_1$: {:.3f})'.format(F.l1_loss(mean_gt, img_))) {{.:pasted:20260313-135602.png}} 这三幅图分别表示: * 左图:当前带噪图像 $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,t), \tilde{\beta}_t I) \tag{7}$$ 这个定义表示: * 均值使用网络估计出来的 $\tilde{\mu}_\theta(x_t,t)$ * 方差使用 $\tilde{\beta}_t$ * 从而构造出反向过程中的高斯分布 重要说明 > 下面的实验应被看作一种模拟。 > 在实际训练和推理中,网络必须预测以下三者之一: > * $\epsilon$ > * $x_0$ > * $\tilde{\mu}_\theta$ > > 而这里的 $\epsilon$ 值只是直接代入使用的,并不是真实模型预测出来的结果。 `reverse_step` 函数 下面这个函数实现了一次反向扩散步骤: def reverse_step(epsilon, x_t, t_step, return_noise=False): # 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$ 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$ if t_step == 0: sample = x_0_pred noise = torch.zeros_like(x_0_pred) 当 `t_step == 0` 时,说明已经到达反向过程的最后一步,不需要再继续去噪。 因此: * 直接把 $x_0\_pred$ 当作最终结果 * 此时噪声设为全零 ---- 3. 如果 $t > 0$,先估计反向均值 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. 计算标准差 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, \tilde{\beta}_t I)$$ ---- 5. 从反向分布中采样 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}\,z,\quad z\sim\mathcal{N}(0,I)$$ ---- 6. 计算 `noise` 仅用于模拟分析 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. 是否返回噪声 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}$ 也就是说,它实现了扩散模型中的一次“去噪反推”。 {{.:pasted:20260313-135843.png}} {{.:pasted:20260313-135917.png}}