从VAE到DDPM:图解扩散模型如何革新图像生成
从VAE到DDPM:图解扩散模型如何革新图像生成
在图像生成的演进长河中,我们见证了从变分自编码器(VAE)的隐变量探索,到生成对抗网络(GAN)的对抗博弈,再到如今以去噪扩散概率模型(DDPM)为代表的扩散模型引领的范式转移。对于许多算法工程师而言,理解这一技术脉络,不仅仅是掌握一个新工具,更是洞察生成式AI底层逻辑如何被重塑的关键。想象一下,在团队技术分享会上,如何向同事清晰地解释,为什么一个看似“笨拙”的、需要成百上千步迭代去噪的过程,最终却能生成比GAN更稳定、更多样、细节更丰富的图像?这背后,是概率建模思想的一次深刻回归与超越。
传统生成模型往往致力于学习一个从简单分布(如高斯噪声)到复杂数据分布(如图像)的直接映射。VAE通过编码-解码结构引入隐空间,追求重建与正则化的平衡;GAN则通过判别器与生成器的博弈,试图直接逼近数据分布。然而,它们各有掣肘:VAE生成图像可能模糊,GAN则饱受模式崩溃和训练不稳定的困扰。扩散模型另辟蹊径,它不追求“一步登天”,而是将生成过程分解为一系列微小的、可逆的步骤,模拟一个物理上合理的扩散与逆扩散过程。这种“渐进式精修”的思想,不仅带来了惊人的生成质量,更在数学上具备了优雅的可解释性和训练稳定性。
本文旨在为你拆解这一技术跃迁的核心。我们将抛开复杂的公式堆砌,通过信息图式的思维,对比VAE、GAN与DDPM在架构哲学上的根本差异,深入剖析DDPM中马尔可夫链的设计、噪声调度策略的奥秘,以及它如何通过学习“去噪”这一看似简单的任务,最终实现“无中生有”的创造性生成。无论你是希望将扩散模型应用于实际项目,还是单纯想理清其技术脉络,这篇文章都将为你提供一个清晰、深入且操作性强的视角。
1. 传统生成模型的局限与扩散模型的哲学转向
在深入DDPM的细节之前,我们有必要回顾一下它所试图超越的范式。理解旧框架的“天花板”,才能更好地欣赏新范式的“突破口”。
变分自编码器(VAE) 的核心思想是压缩与重建。它将输入图像编码到一个低维的、连续的隐空间(通常假设服从标准高斯分布),再从这个隐变量解码回图像。其优化目标是一个折衷:一方面要最小化重建误差,让输出尽可能像输入;另一方面要约束隐变量分布接近标准正态(通过KL散度),以保证隐空间的规整性和生成能力。
注意:VAE的生成过程是一次性的映射。从隐变量
z到图像x,通过解码器网络一步完成。这导致它难以建模非常复杂的数据分布,生成结果常常丢失高频细节,显得平滑甚至模糊。
下面是一个简化的VAE训练损失函数的核心部分,它清晰地体现了这种权衡:
# 伪代码示意VAE损失
reconstruction_loss = F.mse_loss(decoder(z), x) # 重建损失
kl_divergence = -0.5 * torch.sum(1 + log_var - mu.pow(2) - log_var.exp()) # KL散度
total_loss = reconstruction_loss + beta * kl_divergence # β-VAE,β是权衡系数
生成对抗网络(GAN) 则采用了完全不同的博弈论思路。它引入一个判别器(D)来区分真实图像和生成图像,而生成器(G)的目标是生成足以“欺骗”判别器的图像。理想状态下,这个博弈会达到纳什均衡,生成器学到的分布无限接近真实数据分布。
| 特性 | VAE | GAN (原始) |
|---|---|---|
| 训练稳定性 | 高,有明确的损失函数 | 低,容易模式崩溃或训练发散 |
| 生成清晰度 | 较低,倾向于模糊 | 高,能产生锐利细节 |
| 隐空间性质 | 连续、结构化,易于插值 | 可能不连续,插值结果不可控 |
| 多样性 | 较好,但可能牺牲保真度 | 可能陷入模式崩溃,多样性差 |
尽管GAN在图像清晰度上取得了突破,但其训练如同走钢丝。判别器和生成器的能力必须精心平衡,任何一方的过强都会导致训练崩溃。此外,GAN缺乏一个显式的、易于评估的概率模型,其生成过程更像一个“黑箱”。
扩散模型的哲学正是在此背景下诞生。它放弃了“直接生成”的执念,转而模拟一个渐进式的物理过程:如何将一张清晰的图像,通过逐步添加噪声,变成纯粹的无结构噪声(前向过程);再学习如何将这个过程逆向进行,从噪声中一步步恢复出图像(反向过程)。这个想法的精妙之处在于:
- 将复杂问题分解:学习一个复杂的生成分布,被转化为学习一系列相对简单的去噪分布。
- 训练目标稳定:每一步的反向过程都被构造成一个预测噪声的回归任务,损失函数是平滑的均方误差(MSE),避免了GAN的对抗性不稳定。
- 理论基础坚实:基于马尔可夫链和变分推断,整个框架有严格的概率论解释。
2. DDPM核心机制图解:前向扩散与反向生成
DDPM的整个过程可以比喻为一部电影的倒放。正向播放时(前向扩散),一幅画被不断泼上墨点,直到完全变成一团随机墨渍。而我们需要训练的模型,就是学会如何将这部电影倒放(反向生成),从一团墨渍中,一步步还原出最初的画作。
2.1 前向扩散过程:确定性的加噪
前向过程是一个固定的、无需学习的马尔可夫链。在每一步t,我们向当前数据x_{t-1}添加一小部分高斯噪声,得到x_t。这个过程可以用一个简单的公式定义:
x_t = √(α_t) * x_{t-1} + √(1 - α_t) * ε_t,其中 ε_t ~ N(0, I)
这里,α_t是一个介于0和1之间的值,称为噪声调度系数。它决定了在步骤t保留多少原始信号,以及添加多少新噪声。√(α_t)是保留信号的权重,√(1 - α_t)是添加噪声的权重。关键设计在于,这些权重的平方和为1,这保证了每一步输出x_t的方差保持稳定(假设输入已标准化)。
由于每一步都是线性的高斯加噪,并且是马尔可夫过程(下一步只依赖于当前步),我们可以通过数学推导,将任意步骤t的x_t直接表示为原始图像x_0和一系列噪声的线性组合:
x_t = √(ᾱ_t) * x_0 + √(1 - ᾱ_t) * ε
其中 ᾱ_t = ∏_{i=1}^{t} α_i,ε ~ N(0, I)。这个性质极其重要!它意味着我们不需要真的迭代1000步来模拟第500步的状态,而是可以直接通过x_0和一个随机噪声ε计算出来。这大大加快了训练时的数据采样效率。
在典型的DDPM设置中,α_t被安排为从接近1(如0.9999)缓慢衰减到接近0(如0.98)的序列。经过足够多的步数T(通常是1000步),ᾱ_T会趋近于0,此时x_T ≈ ε,几乎完全变成了标准高斯噪声。下图概括了这一过程:
[原始图像 x_0] --(加噪 step 1)--> [x_1] --(加噪 step 2)--> [x_2] --> ... --> [x_T ≈ 纯噪声]
|(√ᾱ_1衰减) + 噪声 |(√ᾱ_2衰减) + 更多噪声 |(ᾱ_T ≈ 0)
2.2 反向生成过程:学习去噪
如果前向过程是已知的、确定性的“破坏”,那么反向过程就是我们需要学习的“重建”。从纯噪声x_T开始,我们希望能一步步“猜”出前一步稍微干净一点的图像x_{T-1},并最终得到x_0。
理论上,反向过程的每一步p(x_{t-1} | x_t)也是一个高斯分布,但其均值和方差依赖于x_t以及我们想要求得的x_0。这形成了一个“先有鸡还是先有蛋”的循环:为了从x_t得到x_{t-1},我们需要知道x_0;但x_0正是我们最终想生成的。
DDPM的巧妙解决方案是:不直接预测x_0或x_{t-1},而是预测在前向过程中添加到x_{t-1}上,从而得到x_t的那个噪声ε。为什么可行?回顾前向过程的直接计算公式:
x_t = √(ᾱ_t) * x_0 + √(1 - ᾱ_t) * ε
如果我们有一个模型ε_θ(x_t, t)能够预测出这个ε,那么我们就可以解出x_0的估计值:
估计的 x_0 = (x_t - √(1 - ᾱ_t) * ε_θ(x_t, t)) / √(ᾱ_t)
一旦有了x_0的估计,再结合x_t和已知的噪声调度参数α_t, ᾱ_t,就可以计算出反向高斯分布的均值和方差,进而采样得到x_{t-1}。这个过程可以逐步迭代,从t=T一直进行到t=1。
因此,DDPM的训练目标变得异常简洁和稳定:训练一个神经网络(通常是U-Net),输入是任意噪声程度t下的噪声图像x_t,输出是预测的噪声ε_θ,使其与真实用于生成x_t的噪声ε的均方误差最小。
# DDPM训练步骤的简化伪代码
def train_step(model, x_0, noise_scheduler):
# 1. 随机采样一个时间步t
t = torch.randint(0, T, (batch_size,))
# 2. 根据公式,直接从x_0生成加噪后的x_t
sqrt_alpha_bar_t = noise_scheduler.get_sqrt_alpha_bar(t) # √ᾱ_t
sqrt_one_minus_alpha_bar_t = noise_scheduler.get_sqrt_one_minus_alpha_bar(t) # √(1-ᾱ_t)
noise = torch.randn_like(x_0) # 真实噪声 ε
x_t = sqrt_alpha_bar_t * x_0 + sqrt_one_minus_alpha_bar_t * noise
# 3. 模型预测噪声
predicted_noise = model(x_t, t) # 模型输入:噪声图x_t和时间步t
# 4. 计算简单的MSE损失
loss = F.mse_loss(predicted_noise, noise)
return loss
这种将复杂的生成问题,转化为预测一个简单高斯噪声的任务,是DDPM成功的关键。它避免了GAN中判别器与生成器的动态博弈,也避免了VAE中需要平衡重建与正则化的难题。
3. 架构差异深度对比:DDPM为何效果更胜一筹?
理解了基本流程后,我们可以从多个维度,将DDPM与VAE、GAN进行更深入的对比,从而揭示其性能优势的根源。
3.1 目标函数的稳定性与梯度质量
- VAE:优化的是证据下界(ELBO),包含重建损失和KL散度。两者需要权衡(如β-VAE),调整不当会导致生成质量下降。梯度来源于明确的损失函数,稳定但可能陷入模糊解的局部最优。
- GAN:优化的是一个极小极大博弈目标。梯度来源于判别器提供的“批评”,这种对抗性梯度可能非常不稳定、稀疏甚至消失,导致训练震荡和模式崩溃。
- DDPM:优化的是一个简单的去噪分数匹配目标(预测噪声的MSE)。梯度直接、平滑且信息丰富。模型在每一个噪声级别
t上都接受训练,相当于在数据空间的许多不同噪声版本上进行了平滑,这有助于学习到更稳健的数据流形。
3.2 生成过程的可控性与可解释性
- VAE/GAN的一次性生成:从隐变量
z到图像x是单步非线性变换。我们很难精确控制z空间中某个方向对应图像中的何种变化(尽管VAE的隐空间相对可控)。生成过程像一个“跳跃”,缺乏中间状态。 - DDPM的渐进式生成:生成是从
x_T到x_0的多步迭代过程。这带来了几个巨大优势:- 中间可视化:我们可以观察图像从噪声逐渐清晰化的全过程,这本身具有很好的可解释性和调试价值。
- 条件控制:可以在任何中间步骤
t引入额外的条件信息(如类别标签、文本描述),引导生成方向。著名的Stable Diffusion就是在潜在空间进行扩散,并结合了文本编码器的条件输入。 - 编辑能力:基于扩散的图像编辑技术(如SDEdit、Prompt-to-Prompt)通过混合不同步骤的噪声或注意力图,实现了对生成内容的精细操控,这是单步生成模型难以做到的。
3.3 对数据分布的覆盖与模式多样性
- GAN的模式崩溃:判别器可能会“封杀”某些生成模式,导致生成器只产生少数几种相似的样本,多样性严重不足。
- VAE的模糊平均:为了最小化重建损失,解码器可能倾向于输出所有可能输出的“平均”,导致图像模糊,丢失细节和多样性。
- DDPM的似然建模:DDPM本质上是在建模数据的概率密度函数。其训练目标(分数匹配)与最大化数据似然密切相关。这意味着它更倾向于覆盖所有训练数据模式,而不是像GAN那样可能丢弃一些“难生成”的模式。因此,扩散模型通常表现出更好的多样性和分布覆盖度。
为了更直观地对比,我们用一个表格总结三者在关键特性上的差异:
| 特性维度 | VAE | GAN | DDPM |
|---|---|---|---|
| 训练稳定性 | 高 | 低 | 高 |
| 生成清晰度 | 低(模糊) | 高 | 极高 |
| 模式覆盖/多样性 | 较好 | 差(易崩溃) | 极好 |
| 可解释性 | 中等(隐空间) | 低 | 高(渐进过程) |
| 条件生成便利性 | 容易 | 中等(需cGAN) | 非常容易 |
| 计算成本 | 低(单步) | 中等(单步) | 高(多步迭代) |
| 推理速度 | 快 | 快 | 慢 |
从表格可以看出,DDPM几乎在所有生成质量相关的指标上都占优,其代价是高昂的计算成本和慢速的推理(需要多次网络前向传播)。这也正是当前扩散模型研究的一个核心优化方向。
4. 噪声调度与采样器:影响生成质量与速度的关键杠杆
在DDPM框架中,除了U-Net模型本身,噪声调度和采样算法是另外两个至关重要的组件,它们直接决定了生成图像的质量、多样性和速度。
4.1 噪声调度:设计加噪的“节奏”
噪声调度定义了前向过程中α_t(或等价的β_t = 1 - α_t)如何随时间t变化。它控制了信息被破坏的速率。
- 线性调度:原始DDPM论文使用的方案,
β_t从很小的值(如1e-4)线性增长到较大的值(如0.02)。这意味着早期加噪很慢,保留大部分信息;后期加噪快,迅速走向纯噪声。 - 余弦调度:后续改进(如Improved DDPM)提出使用基于余弦函数的调度。它在过程开始和结束时变化平缓,在中间阶段变化更快。这种调度通常能产生更好的生成效果,因为它更平滑地处理了信号与噪声的权衡。
不同的调度策略,会导致ᾱ_t(累计乘积)曲线形状不同,从而影响模型在不同噪声水平下的学习难度和最终生成效果。选择或设计调度策略,是调优扩散模型的第一步。
# 两种常见噪声调度函数的示例(伪代码)
def linear_beta_schedule(timesteps, start=1e-4, end=0.02):
return torch.linspace(start, end, timesteps)
def cosine_beta_schedule(timesteps, s=0.008):
steps = timesteps + 1
x = torch.linspace(0, timesteps, steps)
alphas_cumprod = torch.cos(((x / timesteps) + s) / (1 + s) * math.pi * 0.5) ** 2
alphas_cumprod = alphas_cumprod / alphas_cumprod[0]
betas = 1 - (alphas_cumprod[1:] / alphas_cumprod[:-1])
return torch.clip(betas, 0, 0.999)
4.2 采样器:加速反向生成的艺术
原始DDPM采样需要完整迭代T步(如1000步),这非常缓慢。研究人员提出了多种采样器来加速这一过程,其核心思想是:用更少的步数,近似模拟多步反向过程。
- DDIM (Denoising Diffusion Implicit Models):这是最重要的加速采样方法之一。它发现DDPM的生成过程具有确定性的一面。通过将随机噪声
ε的影响固定(或部分固定),DDIM可以将采样过程转化为一个确定性映射。这意味着:- 可以用远少于
T的步数(如50步、20步)进行采样。 - 隐变量具备了良好的插值特性,因为过程是确定性的,从一个隐码
x_T出发,总是生成相同的图像。这为图像编辑和插值提供了便利。
# DDIM采样步骤的核心思想(简化) # 已知当前步x_t和预测的噪声ε_θ,直接跳步估计x_{t-δt} pred_x0 = (x_t - sqrt_one_minus_alpha_bar_t * ε_θ) / sqrt_alpha_bar_t # 使用一个公式,结合pred_x0和x_t,直接计算x_{t-δt},跳过了中间若干步 x_prev = sqrt_alpha_bar_prev * pred_x0 + sqrt_one_minus_alpha_bar_prev * ε_θ - 可以用远少于
- PLMS (Pseudo Linear Multi-step method)、DPM-Solver等:这些是更先进的数值求解器,将扩散方程视为一个微分方程,利用高阶方法(如龙格-库塔法)来求解,能以极少的步数(10-20步)获得高质量样本。
选择采样器时,需要在速度、质量和确定性之间做权衡。DDIM在速度和质量间取得了很好的平衡,并且是确定性的;而一些更快的采样器(如DPM-Solver++)可能在某些步骤数下质量略有波动。
4.3 引导技术:控制生成内容
基础的DDPM从纯噪声生成图像,结果是随机的。为了控制生成内容(如根据文本“一只戴着礼帽的猫”生成图像),需要引入引导。
- 分类器引导:训练一个额外的分类器
p(y|x_t, t),在采样时,用分类器关于x_t的梯度来调整反向过程的均值,使其朝向指定类别y移动。这种方法效果好但需要额外训练分类器。 - 无分类器引导:目前的主流方法。在训练时,随机以一定概率将条件信息(如文本)置为空(
null)。在采样时,计算有条件预测和无条件预测的差值,将这个差值放大一定倍数(引导尺度)后,用来调整噪声预测。
引导尺度是一个超参数,越大则生成图像越贴合条件,但可能牺牲多样性和图像自然度。找到合适的尺度是获得理想结果的关键。# 无分类器引导采样伪代码 # model(x_t, t, cond) 是条件预测,model(x_t, t, null) 是无条件预测 eps_cond = model(x_t, t, text_embedding) # 有条件噪声预测 eps_uncond = model(x_t, t, empty_embedding) # 无条件噪声预测 # 用引导尺度(guidance_scale)放大条件与无条件预测的差异 eps = eps_uncond + guidance_scale * (eps_cond - eps_uncond) # 使用调整后的eps进行下一步采样
5. 从理论到实践:构建与训练一个简易DDPM
理解了所有原理后,让我们动手勾勒一个简易DDPM的实现框架,将理论落地。这里我们聚焦于核心组件,省略一些工程细节。
5.1 构建噪声调度器
首先,我们需要一个模块来管理所有与时间步t相关的噪声参数。
import torch
import math
class NoiseScheduler:
def __init__(self, timesteps=1000, beta_start=1e-4, beta_end=0.02, schedule_type='linear'):
self.timesteps = timesteps
if schedule_type == 'linear':
self.betas = torch.linspace(beta_start, beta_end, timesteps)
elif schedule_type == 'cosine':
# 使用简化版的余弦调度
steps = timesteps + 1
x = torch.linspace(0, timesteps, steps)
alphas_cumprod = torch.cos((x / timesteps + 0.008) / 1.008 * math.pi * 0.5) ** 2
alphas_cumprod = alphas_cumprod / alphas_cumprod[0]
betas = 1 - (alphas_cumprod[1:] / alphas_cumprod[:-1])
self.betas = torch.clip(betas, 0, 0.999)
else:
raise ValueError(f"Unsupported schedule type: {schedule_type}")
self.alphas = 1. - self.betas
self.alphas_cumprod = torch.cumprod(self.alphas, dim=0) # ᾱ_t
self.sqrt_alphas_cumprod = torch.sqrt(self.alphas_cumprod) # √ᾱ_t
self.sqrt_one_minus_alphas_cumprod = torch.sqrt(1. - self.alphas_cumprod) # √(1-ᾱ_t)
# 为反向过程计算一些后验参数(用于DDPM采样)
alphas_cumprod_prev = torch.cat([torch.tensor([1.0]), self.alphas_cumprod[:-1]])
self.posterior_variance = self.betas * (1. - alphas_cumprod_prev) / (1. - self.alphas_cumprod)
self.posterior_mean_coef1 = self.betas * torch.sqrt(alphas_cumprod_prev) / (1. - self.alphas_cumprod)
self.posterior_mean_coef2 = (1. - alphas_cumprod_prev) * torch.sqrt(self.alphas) / (1. - self.alphas_cumprod)
def add_noise(self, x_start, timesteps, noise):
"""根据公式直接计算加噪后的x_t"""
sqrt_alpha_prod = self.sqrt_alphas_cumprod[timesteps].reshape(-1, 1, 1, 1)
sqrt_one_minus_alpha_prod = self.sqrt_one_minus_alphas_cumprod[timesteps].reshape(-1, 1, 1, 1)
return sqrt_alpha_prod * x_start + sqrt_one_minus_alpha_prod * noise
5.2 设计U-Net模型
U-Net是DDPM预测噪声ε_θ的核心网络。它需要接收两个输入:噪声图像x_t和时间步t的嵌入。
import torch.nn as nn
import torch.nn.functional as F
class SinusoidalPositionEmbeddings(nn.Module):
"""将时间步t转换为向量表示"""
def __init__(self, dim):
super().__init__()
self.dim = dim
def forward(self, time):
device = time.device
half_dim = self.dim // 2
embeddings = math.log(10000) / (half_dim - 1)
embeddings = torch.exp(torch.arange(half_dim, device=device) * -embeddings)
embeddings = time[:, None] * embeddings[None, :]
embeddings = torch.cat((embeddings.sin(), embeddings.cos()), dim=-1)
return embeddings
# 这里仅示意一个极简的Block,实际U-Net包含下采样、上采样、残差连接和注意力层等
class SimpleUNet(nn.Module):
def __init__(self, in_channels=3, time_emb_dim=128):
super().__init__()
self.time_mlp = nn.Sequential(
SinusoidalPositionEmbeddings(time_emb_dim),
nn.Linear(time_emb_dim, time_emb_dim),
nn.ReLU()
)
# 假设有一个初始卷积层和一个最终输出层
self.init_conv = nn.Conv2d(in_channels + time_emb_dim, 64, kernel_size=3, padding=1)
self.out_conv = nn.Conv2d(64, in_channels, kernel_size=3, padding=1)
# ... 中间应包含完整的U-Net下采样和上采样结构
def forward(self, x, timesteps):
# x: 噪声图像 [B, C, H, W]
# timesteps: 时间步 [B, ]
t_emb = self.time_mlp(timesteps) # [B, time_emb_dim]
# 将时间嵌入扩展到空间维度并拼接到输入
t_emb = t_emb[:, :, None, None].repeat(1, 1, x.shape[2], x.shape[3])
x = torch.cat([x, t_emb], dim=1)
x = self.init_conv(x)
# ... 经过U-Net主干网络
x = self.out_conv(x) # 预测的噪声 [B, C, H, W]
return x
5.3 训练循环与采样循环
有了调度器和模型,训练和采样的逻辑就非常清晰了。
def train_loop(model, dataloader, noise_scheduler, optimizer, device, epochs):
model.train()
for epoch in range(epochs):
for batch in dataloader:
x_0 = batch.to(device) # 干净图像
optimizer.zero_grad()
# 1. 随机采样时间步和噪声
t = torch.randint(0, noise_scheduler.timesteps, (x_0.shape[0],), device=device).long()
noise = torch.randn_like(x_0)
# 2. 根据时间步t,为x_0加噪,得到x_t
x_t = noise_scheduler.add_noise(x_0, t, noise)
# 3. 模型预测噪声
predicted_noise = model(x_t, t)
# 4. 计算损失并反向传播
loss = F.mse_loss(predicted_noise, noise)
loss.backward()
optimizer.step()
print(f"Epoch {epoch}, Loss: {loss.item():.4f}")
def ddpm_sample(model, noise_scheduler, image_size, batch_size=1, device='cuda'):
"""使用原始DDPM算法采样"""
model.eval()
with torch.no_grad():
# 从纯噪声开始
x_t = torch.randn((batch_size, 3, image_size, image_size), device=device)
for i in reversed(range(noise_scheduler.timesteps)):
t = torch.full((batch_size,), i, device=device, dtype=torch.long)
# 预测噪声
eps_theta = model(x_t, t)
# 计算后验均值和方差
sqrt_alpha_t = torch.sqrt(noise_scheduler.alphas[t])[:, None, None, None]
sqrt_one_minus_alpha_bar_t = noise_scheduler.sqrt_one_minus_alphas_cumprod[t][:, None, None, None]
sqrt_recip_alpha_t = 1. / sqrt_alpha_t
# 估计x_0
pred_x0 = sqrt_recip_alpha_t * (x_t - sqrt_one_minus_alpha_bar_t * eps_theta)
# 计算后验方差和均值系数
posterior_variance = noise_scheduler.posterior_variance[t][:, None, None, None]
coef1 = noise_scheduler.posterior_mean_coef1[t][:, None, None, None]
coef2 = noise_scheduler.posterior_mean_coef2[t][:, None, None, None]
# 计算均值
mean = coef1 * pred_x0 + coef2 * x_t
if i > 0:
noise = torch.randn_like(x_t)
else:
noise = 0
# 采样x_{t-1}
x_t = mean + torch.sqrt(posterior_variance) * noise
# 最终x_t即为生成的图像x_0
generated_images = torch.clip(x_t, -1, 1)
return generated_images
在实际项目中,你会使用更成熟的U-Net实现(如来自diffusers库),并集成DDIM等更快的采样器。但上述代码骨架清晰地揭示了DDPM从训练到推理的完整逻辑闭环:通过预测噪声来学习逆扩散过程。
从VAE的概率隐空间到GAN的对抗博弈,再到DDPM的渐进式去噪,图像生成技术的发展脉络体现着对“如何建模复杂数据分布”这一核心问题的不断深化理解。扩散模型的成功并非偶然,它将一个高难度的生成问题,分解为数百个可管理的去噪子问题,用稳定的回归损失替代了对抗性博弈,用明确的概率推导支撑了整个框架。虽然其多步迭代的特性带来了计算开销,但随之而来的却是前所未有的生成质量、稳定性和可控性。如今,结合潜在空间扩散、蒸馏加速、无分类器引导等技术的现代扩散模型(如Stable Diffusion、DALL-E 3),正在不断突破效率瓶颈,拓展应用边界。对于算法工程师而言,掌握扩散模型不仅意味着多了一件强大的生成工具,更意味着理解了一种新的、基于概率和迭代的生成范式,这种范式正在重新定义AIGC的可能性。
更多推荐
所有评论(0)