这是本文档旧的修订版!
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显示出来。
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]
仿射变换矩阵为:
$$ \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()
这段代码改改玩,如果只进行变换,不进行逆变换
图片长这样
可以看出图片的形状没有变
扩散过程是基于一个方差调度表构建的,这个调度表决定了在扩散过程的每一个时间步中加入噪声的强度。为此,我们定义如下几个量:
- `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)$ 是高斯噪声
直观理解:
$\sqrt{\alpha_t}$ 是上一步图像在当前步中的保留权重。
之所以取平方根,是因为扩散模型里控制的是方差,而真正乘在样本上的系数是标准差,所以会出现平方根。
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("加噪过程测试完成!")
反向过程
反向过程 $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()



