共计 2815 个字符,预计需要花费 8 分钟才能阅读完成。
背景:为什么加噪是扩散模型的核心
扩散模型的训练可以看作是一个逐步破坏数据分布的过程,而加噪模块正是实现这一过程的关键组件。其数学本质是通过一系列高斯噪声的注入,将原始数据分布逐渐转化为一个简单的已知分布(通常是标准正态分布)。

前向过程的公式推导如下:
给定一个数据样本 x₀,我们定义一系列逐步加噪的版本 x₁, x₂,…,x_T,其中每一步的加噪过程可以表示为:
q(x_t|x_{t-1}) = N(x_t; √(1-β_t)x_{t-1}, β_tI)
这里 β_t 是噪声调度参数,控制着每一步添加的噪声量。通过马尔可夫链的性质,我们可以直接得到从 x₀到任意 x_t 的闭式解:
x_t = √(ᾱ_t)x_0 + √(1-ᾱ_t)ε
其中 ᾱt = ∏^t(1-β_s),ε ~ N(0,I)。这个推导展示了加噪过程如何将数据逐渐向噪声分布转变。
技术方案对比:两种加噪策略
在工程实现中,我们通常面临两种主要的加噪策略选择:
- 逐像素加噪(Pixel-space Noise)
- 优点:实现简单,计算开销低
-
缺点:对高频信息敏感,可能导致细节丢失
-
特征空间加噪(Latent-space Noise)
- 优点:更符合人类感知,保留语义信息
- 缺点:需要额外的编码器,增加模型复杂度
选择依据:
– 对于低分辨率图像生成(<256×256),逐像素加噪通常足够
– 高分辨率或需要精细控制的场景,推荐特征空间加噪
– 计算资源受限时,逐像素加噪是更实用的选择
核心代码实现
以下是一个完整的 PyTorch 加噪模块实现,包含噪声调度器和批量加噪功能:
from typing import Tuple
import torch
import torch.nn as nn
import math
class NoiseScheduler:
""" 可配置的噪声调度器
Args:
schedule_type: 调度策略,支持['linear', 'cosine']
timesteps: 总时间步数
beta_start: 初始 beta 值
beta_end: 终止 beta 值
"""
def __init__(self,
schedule_type: str = 'cosine',
timesteps: int = 1000,
beta_start: float = 1e-4,
beta_end: float = 0.02):
self.timesteps = timesteps
self.schedule_type = schedule_type
if schedule_type == 'linear':
self.betas = torch.linspace(beta_start, beta_end, timesteps)
elif schedule_type == 'cosine':
steps = torch.arange(timesteps + 1, dtype=torch.float32)
alpha_bar = torch.cos((steps / timesteps + 0.008) / 1.008 * math.pi / 2).pow(2)
alpha_bar = alpha_bar / alpha_bar[0]
betas = 1 - (alpha_bar[1:] / alpha_bar[:-1])
self.betas = torch.clip(betas, 0, 0.999)
else:
raise ValueError(f"Unknown schedule type: {schedule_type}")
self.alphas = 1. - self.betas
self.alpha_bars = torch.cumprod(self.alphas, dim=0)
def add_noise(self,
original_samples: torch.Tensor,
timesteps: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
""" 批量加噪方法
Args:
original_samples: 原始样本 [batch_size, *shape]
timesteps: 时间步 [batch_size]
Returns:
noisy_samples: 加噪后的样本
noise: 添加的噪声
"""
sqrt_alpha_bar = torch.sqrt(self.alpha_bars[timesteps])
sqrt_one_minus_alpha_bar = torch.sqrt(1. - self.alpha_bars[timesteps])
# 为每个样本生成随机噪声
noise = torch.randn_like(original_samples)
# 扩展维度以支持广播
sqrt_alpha_bar = sqrt_alpha_bar.view(-1, *([1]*(original_samples.dim()-1)))
sqrt_one_minus_alpha_bar = sqrt_one_minus_alpha_bar.view(-1, *([1]*(original_samples.dim()-1)))
noisy_samples = sqrt_alpha_bar * original_samples + sqrt_one_minus_alpha_bar * noise
return noisy_samples, noise
# 使用示例
scheduler = NoiseScheduler(schedule_type='cosine', timesteps=1000)
x0 = torch.randn(8, 3, 64, 64) # 8 张 64x64 的 RGB 图像
t = torch.randint(0, 1000, (8,)) # 为每张图像随机采样时间步
xt, noise = scheduler.add_noise(x0, t)
性能优化技巧
- 内存优化:
- 使用原地操作 (in-place) 减少内存分配
-
对 alpha_bar 等预计算量进行缓存
-
CUDA 优化:
- 使用融合内核 (fused kernel) 减少内存访问
-
对小型张量操作使用 torch.jit.script
-
分布式训练:
- 对噪声生成使用相同的随机种子保证一致性
- 采用梯度检查点减少显存占用
避坑指南
- 噪声衰减过快导致模式崩溃
- 现象:生成样本多样性低
-
解决:调整 beta_end 参数,使用更平缓的调度曲线
-
数值不稳定
- 现象:训练后期出现 NaN
-
解决:对 alpha_bar 添加小的 epsilon(如 1e-6)防止除零
-
训练效率低下
- 现象:GPU 利用率低
- 解决:增加批量大小,使用混合精度训练
开放性问题
当前的噪声调度策略大多是预设的固定曲线,但实际数据可能需要不同的噪声衰减模式。一个有趣的方向是设计自适应的噪声调度策略:
- 能否根据训练过程中模型的反馈动态调整噪声曲线?
- 不同类别的数据是否需要不同的调度策略?
- 如何设计一个可学习的调度器?
建议读者在 Colab 上实验不同的噪声曲线(线性、余弦、平方根等),观察对生成质量的影响。一个简单的实验框架:
- 固定随机种子生成一组样本
- 使用不同的调度策略训练相同架构的模型
- 定量比较 FID 分数和生成多样性
