Forward Diffusion Process

最近 midjourney 用的比较多,很好奇,这背后的技术大概是怎样的。好了,那就去做一些了解吧(补充注:后来我才发现这个坑有多大…)。

除了大语言模型之外,另一个非常活跃的领域就是图片处理技术,例如文生图(Text-to-Image)、多模态模型等。除了在原有的 CNN 技术架构上,也出现了很多新的突破。虽然同样使用的是深度神经网络,但图像生成技术与大语言模型技术(LLM)是非常不同的。其模型架构不同 ,底层的数学、物理原理也非常不一样。在对大语言模型有一个框架性的了解 之后,现在打算开一个新的“坑”,当然,也就没有打算爬出来,可能就“浅尝辄止”做个最为基础的了解。一般学东西,都是从 “Why/How/What” 的顺序,但在尝试了一下,无法理解“Why”后,于是就打算绕道,看看 How 和 What 了。那么,“What”打算就从“Foward Diffusion Process”开始吧。

整个文生图(Text-to-Image)的架构是比较复杂的,其中较为“简单”、基础 的一步即位“Forward Diffusion Process”,本文从“Denoising Diffusion Probabilistic Models”论文中为例,说明这个过程。

1. 概述:“FDP” 是一个加随机噪声的过程

很多地方都会说:“Forward Diffusion Process” 就是一个添加随机噪声的过程。这话对、也不对。首先,“FDP” 确实是一个添加随机噪声的过程,但是这个随机噪声添加得非常有“讲究”。我们就从“添加随机噪声”和“讲究”两个角度去介绍。

“Forward Diffusion Process” 过程的数据是用于训练神经网络(通常是一个U-Net架构)的参数的,从而最终实现其逆过程(即“Reverse Diffusion Process”),而生成图片。

1.1 添加随机噪声

“FDP” 做如下的事情:

  • 先读取一张图片(这里使用的一张博客图片)
  • 将图片的每个像素点取值,随机“迭代加上”一个随机值 \(N(0,1) \)

例如,我们使用论文 DDPM 中的设定对如下图片添加噪声,就可以观察到如下过程:

更为详细的:

  • 1. 先读取一张图片,并将其 RGB 通道的数据读取出来
  • 2. 将像素值从 [0, 255] 的整数缩放到 [0.0, 1.0] 的浮点数
  • 3. 每个通道每个点随机“迭代加上”一个随机值,按照 \(N(0,1) \) 分布生成该随机值

2. “线性”添加噪声

Denoising Diffusion Probabilistic Models 论文中使用了“线性调度”的方式添加噪声。即添加噪声的强度“线性”的逐渐增强,这里的“线性”是指增加的噪声的“方差”线性增加。

先用更加形式化的数学语言描述上述的噪声添加,即:

$$ x_t = A x_{t-1} + B \epsilon \quad \text{where} \quad \epsilon \sim \mathcal{N}(0,1) $$

2.1 调整权重系数

直觉上,可以这样理解,在添加噪声的过程中,刚开始是清晰图片,所以噪声添加的较少,而后,随着图片变得模糊,也逐步增加了添加噪声的强度(方差)。

论文中 “线性调度” 模式做了如下设计:

$$
x_t = \sqrt{1 – \beta_t} x_{t-1} + \sqrt{\beta_t} \epsilon, \quad \epsilon \sim \mathcal{N}(0, \mathbf{I}) \tag{1}
$$

这里的 \(\beta_t \) 是一个随着时间序列推进逐渐增大的值,从而在迭代过程中(或者说这个马尔科夫链中),逐步增加噪声在图片中的影响。论文中,\(\beta_t \) 是一个线性变换的序列,从 \(10^{-4} \),通过1000步迭代,增加到\(0.02 \)。

2.2 计算的“简化”

上述的“公式(1)”是一个迭代计算的“数列”(或者说是“马尔科夫链”),在实际的计算中,会经常使用如下的“通项公式”计算上述的迭代“数列”:

$$
x_t = \sqrt{\bar{\alpha}_t} x_0 + \sqrt{1-\bar{\alpha}_t}\epsilon \quad \epsilon \sim \mathcal{N}(0,1) \\
\text{Where} \quad \alpha_t := 1-\beta_t ,\, \bar{\alpha}_t = \prod_{s=1}^{t}\alpha_s \tag{2}
$$

关于详细的如何从“公式(1)”严格的推导到上述表达式(2),参考本文小结“4.5 “调度公式”的推导”。

上述的表达式,在论文中出现的形式则是:

$$
q(x_t|x_0) = \mathcal{N}(x_t; \sqrt{\bar{\alpha}}x_0,(1-\bar{\alpha_t}\mathbf{I}) ) \tag{3}
$$

这里的“公式(2)、(3)”所表达的意思是等价的。简单的说明:

  • (a) \(\mathcal{N}(x; \mu \, , \sigma^2) \) 表示正态分布的随机变量 \(x \),均值为\(\mu \) 方差为 \(\sigma^2 \)
  • (b) 上述的表达式中的 \(I \) 表示单位矩阵。这是因为公式中的 \(x_t \) 是一个表示所有像素值的向量(例如,128×128的向量,即可能有16384个随机变量),\(I \) 表示协方差矩阵是一个对角矩阵,即所有随机变量都是完全独立的。

3. 代码实现

完整的代码参考:Forward-Diffusion-Process.ipynb.ipynb

3.1 读取并预处理图片

该函数输入原始图片、迭代次数、初始噪声,即 (x_0, t,noise) 。原始图片在读取后,需要做几个处理:

  • 统一 Resize 到 128 x 128 像素来出来
  • 按 RGB 三通道转成一个 1 x 3 x 128 x 128 的张量/数组
  • 像素值从 [0, 255] 的整数缩放到 [0.0, 1.0] 的浮点数

对应代码:

# 3. 加载测试图片
url = "https://www.orczhou.com/wp-content/uploads/2025/12/IMG_1710-scaled.jpg"
img = Image.open(requests.get(url, stream=True).raw).convert("RGB")
transform = transforms.Compose([transforms.Resize((128, 128)), transforms.ToTensor()])
x_0 = transform(img).unsqueeze(0) # 变为 (1, 3, 128, 128)

3.2 生成随机噪声

生成一个随机按 \(N(0,1) \) 分布的对象,与上述图片相同,即:1 x 3 x 128 x 128

noise = torch.randn_like(x_0) # 采样纯噪声 epsilon

3.3 原始图片叠加噪声

噪声的叠加并不是简单的直接相加( \(x_0 + \text{noise} \) ),而是一个迭代式的,并考虑原始图片影响的方式(线性采样考虑):

$$
\begin{aligned}
x_t &= \sqrt{1 – \beta_t} x_{t-1} + \sqrt{\beta_t} \epsilon, \quad \epsilon \sim \mathcal{N}(0, \mathbf{I}) \\[0.5em]
x_t &= \sqrt{\bar{\alpha}} x_0 + \sqrt{1-\bar{\alpha}}\epsilon
\end{aligned}
$$

4. 公式(1)的说明

我们再来看看 “公式(1)” 的设计:

$$
x_t = \sqrt{1 – \beta_t} x_{t-1} + \sqrt{\beta_t} \epsilon, \quad \epsilon \sim \mathcal{N}(0, \mathbf{I}) \tag{1}
$$

为什么不用最为直观、自然的 \(1-\beta_t \) 与 \(\beta_t \) 作为上述表达式中的系数,而是使用了他们的平方根?

这个“设计”思路的根源是因为:高斯分布乘以一个常数后,其方差则为该常数的平方再乘以原来的方差。即: \(X \sim \mathcal{N}(\mu,\sigma^2) \),那么 \(aX \sim \mathcal{N}(a\mu,a^2\sigma^2) \)。

整体上,考虑是希望在传播过程中,方差不要偏离太大。并且随着时间的推进,最终的数值是一个标准正态分布的,如果来看推迟到出来的“公式3”:

$$
q(x_t|x_0) = \mathcal{N}(x_t; \sqrt{\bar{\alpha}}x_0,(1-\bar{\alpha_t})\mathbf{I}) \tag{3}
$$

可以看到在这样的“设计”(即使用“根号”)下,最终迭代的\(x_t \) 的均值是 \(\sqrt{\bar{\alpha}}x_0 \),方差为 \(1-\bar{\alpha_t} \),随着\(t \)的增加,就逐步趋向于 \(\mathcal{N}(0,1) \)了。

5. 一些数学公式与推导

Diffusion 相关的数学基础还是非常、非常复杂的,而这里的公式推导看起来虽然有点复杂,但可能是整个Diffusion模型的数学基础中最为简单的部分了。这里,勉强祭出右边的图片。

我们这里还是来做一些尝试吧。

5.1 Forward Diffusion Process 公式

$$x_t = \sqrt{1 – \beta_t} x_{t-1} + \sqrt{\beta_t} \epsilon, \quad \epsilon \sim \mathcal{N}(0, \mathbf{I}) \tag{1} $$

这个公式本身已经有一定的复杂度了,要搞清楚大概需要关注如下点:

  • 这里的 \(\epsilon \) 是什么意思
  • \(\beta_t \) 的计算设计
  • 从“迭代公式(1)”到“通项公式(2)”

5.2 唬人的 \(\epsilon \)

多维变量的概率已经忘得差不多了…,好在这里是一些完全“独立”的随机变量,还比较好理解。我们先看看这里的: \(\epsilon \sim \mathcal{N}(0, \mathbf{I}) \)。

第一次看到这些个符号的时候,也是被“怔”了一下的,仔细一看还好。

首先,这里公式中的 \(x_t \) 是一张图片所有的像素信息,例如,如果是一张 128×128 的图片,那么所有的像素信息则是一个长度为 1 x 3 x 128 x 128 的向量。即,\(x_t \) 是一个 3 x 128 x 128 的向量。

对应的,这里的 \(\epsilon \) 也是一个这样的向量(例如, 3 x 128 x 128 的向量,而不是数学中常见的表示一个很小的值),可以这样理解,这个向量的每一个取值都是一个随机变量(例如一共 3 x 128 x 128 个随机变量),每一个随机变量都是独立的,即协方差矩阵为单位矩阵(这里的 \(\mathbf{I} \)),并且每个随机变量符合标准正态分布,即 \(\mathcal{N}(0, 1) \)。

再回头看看原公式,是不是简单了很多:

$$x_t = \sqrt{1 – \beta_t} x_{t-1} + \sqrt{\beta_t} \epsilon, \quad \epsilon \sim \mathcal{N}(0, \mathbf{I}) $$

5.3 线性调度

再来看公式中的 \(\beta_{t} \):

$$x_t = \sqrt{1 – \beta_t} x_{t-1} + \sqrt{\beta_t} \epsilon, \quad \epsilon \sim \mathcal{N}(0, \mathbf{I}) $$

在原始的 Denoising Diffusion Probabilistic Models 论文中,取值如下:\( \beta_1 = 10^{-4} ,\, \beta_{T} = 0.02 \),即均匀线性的在1000次噪声添加中,\(\beta \)均匀的从\(0.0001 \) 增长到 \(0.02 \),即:

$$ \beta_1 = 0.0001, \beta_2 = 0.0001199, \beta_3 = 0.0001398 , … , \beta_{1000} = 0.02 $$

在 Python 中就是如下代码:

T = 1000  # 总步数
betas = torch.linspace(0.0001, 0.02, T) # 线性调度:噪声方差逐渐增大

5.4 “线性调度”的真实计算式

但是,在实际的运算不会用上面的公式。而是,使用了这个版本的推导,从而更加高效,更加直觉,同时,看起来更加“抽象”,即在论文中的如下公式:

$$ q(x_t|x_0) = \mathcal{N}(x_t; \sqrt{\bar{\alpha}}x_0,(1-\bar{\alpha_t}\mathbf{I}) ) \tag{3}$$

这个版本看起来就很“唬人” ,但理解了其意思还是感觉比较“简洁”的,再理解之后,就觉得也还比较“简单”。

本质上,这公式是前面“公式(1)”的推导与快速计算版本,该公式提供了一个无需迭代计算,而直接根据\(x_0 \) 计算 \(x_t \) 的方法。其中的 \(\alpha_t := 1-\beta_t ,\, \bar{\alpha}_t = \prod_{s=1}^{t}\alpha_s \) 。

5.5 “调度公式”的推导

这里来尝试做一下“公式(1)”到“公式(2)”的推导,尝试理解以下研究者们这部分工作(也可以参考这里:Diffusion Models: A Mathematical Introduction的第14页)。

$$
\begin{aligned}
x_t &= \sqrt{1 – \beta_t} x_{t-1} + \sqrt{\beta_t} \epsilon \\[0.5em]
x_{t-1} &= \sqrt{1 – \beta_{t-1}} x_{t-2} + \sqrt{\beta_{t-1}} \epsilon \\[0.5em]
x_{t-2} &= \sqrt{1 – \beta_{t-2}} x_{t-3} + \sqrt{\beta_{t-2}} \epsilon \\[0.5em]
\quad &\vdots & \\[0.5em]
x_{1} &= \sqrt{1 – \beta_{1}} x_{0} + \sqrt{\beta_{1}} \epsilon \\[0.5em]
\end{aligned}
$$

所以:

$$
\begin{aligned}
x_t &= \sqrt{1 – \beta_t} (\sqrt{1 – \beta_{t-1}} x_{t-2} + \sqrt{\beta_{t-1}} \epsilon) + \sqrt{\beta_t} \epsilon \\[0.5em]
&= ( \sqrt{1 – \beta_t}\sqrt{1 – \beta_{t-1}} )x_{t-2} + ( \sqrt{1 – \beta_t}\sqrt{\beta_{t-1}} )\epsilon + \sqrt{\beta_t} \epsilon
\end{aligned} \tag{4}
$$

接下去看看上面等式的后面两部分:

$$ ( \sqrt{1 – \beta_t}\sqrt{\beta_{t-1}} )\epsilon + \sqrt{\beta_t} \epsilon \tag{5}$$

这里并不是简单的加法,而是两个独立概率分布的加法:如果概率基础还在的话,就有如下的公式,两个独立的高斯分布(例如:\(X \sim \mathcal{N} (\mu_X ,\sigma^2_X ) \quad Y \sim \mathcal{N} (\mu_Y ,\sigma^2_Y ) \))的随机变量之和,其结果依旧是高斯分布,并且均值依据是两个均值的和、方差也是两个方差的和( \(X+Y \sim \mathcal{N} (\mu_X + \mu_Y ,\sigma^2_X + \sigma^2_Y ) \) )。

注意上面的表示 \(\sqrt{\beta_t}\epsilon \) 中,\(\sqrt{\beta_t} \) 是标准差,方差即 \(\beta_t \)。

所以上面“公式(5)”两个分布的和,依旧是高斯分布,且均值依旧是 0,方差则为 \((1-\beta_t)\beta_{t-1}+\beta_t \),即有了如下看似错误的,但是却是正确的推导:

$$
( \sqrt{1 – \beta_t}\sqrt{\beta_{t-1}} )\epsilon + \sqrt{\beta_t} \epsilon = \sqrt{(1-\beta_t)\beta_{t-1}+\beta_t} \epsilon
$$

其实上面并不是一个一般意义的“等式”,而是表达了如下的含义:

$$
( \sqrt{1 – \beta_t}\sqrt{\beta_{t-1}} )\epsilon + \sqrt{\beta_t} \epsilon \sim \mathcal{N}(0,(1-\beta_t)\beta_{t-1}+\beta_t)
$$

有了这里的理解,就以继续上面公式(4)的推导就非常容易有如下的结论了:

$$
\begin{aligned}
x_t &= \sqrt{1 – \beta_t} (\sqrt{1 – \beta_{t-1}} x_{t-2} + \sqrt{\beta_{t-1}} \epsilon) + \sqrt{\beta_t} \epsilon \\[0.5em]
&= ( \sqrt{1 – \beta_t}\sqrt{1 – \beta_{t-1}} )x_{t-2} + ( \sqrt{1 – \beta_t}\sqrt{\beta_{t-1}} )\epsilon + \sqrt{\beta_t} \epsilon \\[0.5em]
&= \sqrt{\alpha_t\alpha_{t-1}}x_{t-2} + \sqrt{1-\alpha_t\alpha_{t-1}} \epsilon \\[0.5em]
&\vdots \\[0.5em]
&=\sqrt{\alpha_t\alpha_{t-1}\cdots\alpha_1}x_{0} + \sqrt{1-\alpha_t\alpha_{t-1}\cdots\alpha_{1}} \epsilon \\[0.5em]
&= \sqrt{\bar{\alpha_t}}x_0 + \sqrt{1-\bar{\alpha_t}} \epsilon \\[0.5em]
\text{where}\, \alpha_t &:= 1-\beta_t ,\, \bar{\alpha_t} = \prod_{s=1}^{t}\alpha_s
\end{aligned}
$$

即有了最终的公式:

$$
x_t = \sqrt{\bar{\alpha_t}}x_0 + \sqrt{1-\bar{\alpha_t}} \epsilon \\
\text{where}\, \alpha_t := 1-\beta_t ,\, \bar{\alpha_t} = \prod_{s=1}^{t}\alpha_s
$$

5.6 在Python中的实现

了解了上面这些,再看这些代码就很简单了:

T = 1000  # 总步数
betas = torch.linspace(0.0001, 0.02, T) # 线性调度:噪声方差逐渐增大

# 计算中间变量
alphas = 1. - betas
alphas_cumprod = torch.cumprod(alphas, axis=0) # 对应公式中的 alpha_bar

以及最后的 Forward Diffusion Process 计算:

    sqrt_alphas_cumprod_t = torch.sqrt(alphas_cumprod[t]) # 信号系数
    sqrt_one_minus_alphas_cumprod_t = torch.sqrt(1. - alphas_cumprod[t]) # 噪声系数

    # 核心公式实现
    return sqrt_alphas_cumprod_t * x_0 + sqrt_one_minus_alphas_cumprod_t * noise, noise

6. 微分方程角度的考虑

从随机微分方程的角度考虑 “DDPM” 模型是该模型发布之后的事情了,我们这里先看看这个随机微分方程的“漂移/Drift”部分与上述迭代式子的关系。

先不考虑“随机项”的增加,那么在设计时,希望随着时间步骤的迭代,变化的速率逐渐增加,即迁移变化慢,后期变化快,即考虑在微分方程的右侧增加一下 \(\beta(t) \),该函数随着时间增加而增加,最为常见的即为线性增长(对应于“线性调度”)。此外,该变化率应该与当前值有关,当前值越大,则变化率应该越大;并且期望最终迭代结果趋向于零(即均值最终为零的正态分布),就有最终的微分方程设计:

$$ \frac{dx}{dt} = -\frac{1}{2}\beta(t)x \tag{5} $$

说明:

  • 方程右侧的负号,表示总是朝着x取值相反的方向移动,即总是朝着原点方向移动
  • \(\beta(t)x \)则表达了上述的两个关于“变化率”大小的意图

该微分方程的迭代解就有如下的迭代表达式:

$$
\begin{aligned}
&x_{t} – x_{t-1} = -\frac{1}{2}\beta(t)x_{t-1} \\[0.5em]
&x_{t} = x_{t-1} -\frac{1}{2}\beta(t)x_{t-1} \\[0.5em]
&x_{t} = (1 -\frac{1}{2}\beta(t))x_{t-1} \\[0.5em]
\end{aligned}
$$

很神奇的是,在\(\beta \)很小的时候,根据泰勒展开有:

$$
\sqrt{1-\beta} = 1 – \frac{1}{2}\beta – \frac{1}{8}\beta^2 + \cdots
$$

最终就有:

$$
x_{t} = \sqrt{1-\beta_t}x_{t-1}
$$

这里就可以从“微分方程”的角度去理解上述的 stable diffusion 中 Forward Diffusion Process 中前半部分了。

7. 小结 FDP

在了解 What 中,也在慢慢理解 How 以及Why 。这里再次从宏观上概述 FDP 的过程,从而跳出上述的 What 细节,再次审视这个过程。

7.1 首先,为什么需要 “Forward Diffusion Process” ?

简单回答:给样本添加噪声,构建训练数据。

“Forward Diffusion Process” 过程的数据主要用于训练,对于一个给定的图片,逐步添加噪声,最后让其变成一张纯粹的、高斯分布的噪声。而这个过程的数据,则可以用于训练 U-Net 的神经网络,让该U-Net具备一个神奇的能力:即给出一张图片(带有噪声的),该 U-Net 可以预测出这张图片中有哪些是噪声。

7.2 “Forward Diffusion Process” 操作的数学计算

其核心公式如下:

$$
x_t = \sqrt{1 – \beta_t} x_{t-1} + \sqrt{\beta_t} \epsilon, \quad \epsilon \sim \mathcal{N}(0, \mathbf{I})
$$

经过推导,等价与如下公式(关于公式的推导:Forward Diffusion Process):

$$
x_t = \sqrt{\bar{\alpha_t}} x_0 + \sqrt{1-\bar{\alpha_t}}\epsilon \quad \epsilon \sim \mathcal{N}(0,1) \\
\text{Where} \quad \alpha_t := 1-\beta_t ,\, \bar{\alpha_t} = \prod_{s=1}^{t}\alpha_s \tag{a}
$$

在论文中可能看到的形式:

$$
q(x_t|x_0) = \mathcal{N}(x_t; \sqrt{\bar{\alpha}}x_0,(1-\bar{\alpha_t}\mathbf{I}) )
$$

对于一张照片实际做上述操作则有如下效果:

关于上述公式的一些重要特性:

  • 经过若干次迭代后,一张清晰的图片最终变成一张“随机”噪声图片,这里的“随机”是指的正态分布
  • 从“公式(a)”可以看到,逐步迭代和多步合并迭代有一样的效果。当然,为了获得训练数据,总是逐步迭代

Leave a Reply

Your email address will not be published. Required fields are marked *