文章摘要
从 GAN 到扩散模型,掌握图像生成的两大范式
1生成模型概述:从 VAE 到 Diffusion
生成模型(Generative Models)的目标是学习数据分布 P_data(x),并从中采样生成新的、与训练数据相似但不完全相同的样本。与判别模型(分类、检测)不同,生成模型需要「创造」而非「识别」,这使其成为 AI 最具挑战也最迷人的方向之一。主流生成模型经历了三个时代的演进:第一代是基于似然的模型,如变分自编码器(VAE)和归一化流(Normalizing Flows),它们通过最大化数据似然下界(ELBO)或可逆变换来学习分布,生成质量受限于模型假设;第二代是对抗生成网络(GAN),通过生成器和判别器的博弈达到纳什均衡,生成的图像质量极高但训练不稳定、模式崩溃问题突出;第三代是扩散模型(Diffusion Models),通过逐步加噪和去噪的马尔可夫链学习数据分布,训练稳定、生成质量超越 GAN,成为当前图像生成的主流范式。理解生成模型的本质,关键在于回答一个问题:我们如何在高维连续空间中,从随机噪声映射到有语义意义的数据点?
"""生成模型三大范式对比"""
from abc import ABC, abstractmethod
import torch
import torch.nn as nn
class GenerativeModel(ABC):
"""生成模型抽象基类"""
@abstractmethod
def sample(self, n: int) -> torch.Tensor:
"""从模型采样 n 个样本"""
pass
@abstractmethod
def likelihood(self, x: torch.Tensor) -> torch.Tensor:
"""计算样本对数似然(部分模型可计算)"""
pass
@abstractmethod
def encode(self, x: torch.Tensor) -> torch.Tensor:
"""编码到潜在空间(如适用)"""
pass
class VAE(GenerativeModel):
"""变分自编码器:显式密度模型"""
def __init__(self, latent_dim: int = 128):
self.latent_dim = latent_dim
# 编码器 q(z|x) 和解码器 p(x|z)
self.encoder = nn.Sequential(
nn.Conv2d(3, 64, 4, 2, 1), nn.ReLU(),
nn.Conv2d(64, 128, 4, 2, 1), nn.ReLU(),
nn.Flatten(),
)
self.fc_mu = nn.Linear(128 * 8 * 8, latent_dim)
self.fc_logvar = nn.Linear(128 * 8 * 8, latent_dim)
self.decoder = nn.Sequential(
nn.Linear(latent_dim, 128 * 8 * 8),
nn.Unflatten(1, (128, 8, 8)),
nn.ConvTranspose2d(128, 64, 4, 2, 1), nn.ReLU(),
nn.ConvTranspose2d(64, 3, 4, 2, 1), nn.Sigmoid(),
)
def reparameterize(self, mu: torch.Tensor, logvar: torch.Tensor) -> torch.Tensor:
"""重参数化技巧:z = mu + sigma * eps"""
std = torch.exp(0.5 * logvar)
eps = torch.randn_like(std)
return mu + eps * std
def sample(self, n: int) -> torch.Tensor:
z = torch.randn(n, self.latent_dim)
return self.decoder(z)
def likelihood(self, x: torch.Tensor) -> torch.Tensor:
mu, logvar = self._encode(x)
recon = self.decoder(self.reparameterize(mu, logvar))
# 重构损失(高斯假设下的负对数似然)
return -nn.MSELoss(reduction='none')(recon, x).sum(dim=[1, 2, 3])"""生成模型能力矩阵"""
model_comparison = {
"VAE": {
"显式密度": "是(可计算 ELBO)",
"采样速度": "极快(一次前向传播)",
"图像质量": "中等(模糊倾向)",
"训练稳定性": "非常稳定",
"模式覆盖": "好(不易模式崩溃)",
"典型应用": "表示学习、异常检测",
},
"GAN": {
"显式密度": "否(隐式模型)",
"采样速度": "快(一次前向传播)",
"图像质量": "极高(清晰锐利)",
"训练稳定性": "不稳定(需精心设计)",
"模式覆盖": "差(易模式崩溃)",
"典型应用": "超分辨率、风格迁移",
},
"Diffusion": {
"显式密度": "近似(变分下界)",
"采样速度": "慢(多步迭代去噪)",
"图像质量": "极高(超越 GAN)",
"训练稳定性": "稳定(MLE 目标)",
"模式覆盖": "极好",
"典型应用": "文本生成图像、视频生成",
},
"Flow": {
"显式密度": "是(精确似然)",
"采样速度": "中等(可逆网络)",
"图像质量": "中高",
"训练稳定性": "稳定",
"模式覆盖": "好",
"典型应用": "密度估计、可逆编辑",
},
}
print(f"{'模型':<12} | {'质量':<6} | {'速度':<6} | {'稳定性':<8}")
print("-" * 40)
for name, props in model_comparison.items():
print(f"{name:<12} | {props['图像质量']:<14} | {props['采样速度']:<8} | {props['训练稳定性']}")| 模型类型 | 密度估计 | 采样效率 | 图像质量 | 训练难度 | 代表工作 |
|---|---|---|---|---|---|
VAE | 显式(ELBO) | 快 | 中等 | 低 | Kingma et al. 2014 |
GAN | 隐式 | 快 | 高 | 高 | Goodfellow et al. 2014 |
Flow | 显式(精确) | 中 | 中高 | 中 | Dinh et al. 2017 |
Diffusion | 近似(VLB) | 慢 | 极高 | 低 | Ho et al. 2020 |
Score-based | 隐式(Score) | 中-慢 | 极高 | 低 | Song et al. 2021 |
Autoregressive | 显式(分解) | 慢 | 高 | 中 | van den Oord et al. 2017 |
⚠️ 常见踩坑
不要盲目追求 GAN 的生成质量——GAN 的训练需要大量调参经验(学习率、架构、正则化),对新手极不友好
2StyleGAN 架构详解:Style Space 与 AdaIN
StyleGAN(2018)是 NVIDIA 提出的一种革命性 GAN 架构,它彻底重构了传统 GAN 的生成器设计。传统 GAN 将潜在向量 z 直接输入全连接层生成图像,导致潜在空间高度纠缠——改变一个维度可能同时影响多个视觉属性。StyleGAN 的核心创新有三点:第一,引入映射网络(Mapping Network),将输入的潜在向量 z 通过 8 层全连接网络映射到中间潜在空间 W,解耦了不同语义属性;第二,采用自适应实例归一化(AdaIN),将 W 空间中的风格向量注入到生成器的每一层,实现了对不同尺度视觉特征的精细控制——浅层控制姿势性别,中层控制发型,深层控制颜色纹理;第三,引入噪声注入,在每一层添加独立的随机噪声图,控制随机细节,使生成图像更加真实自然。StyleGAN2(2020)进一步改进了架构,移除了权重解调中的 artifacts(水滴状伪影)。StyleGAN3(2021)解决了纹理粘连问题(aliasing),通过等变信号处理实现了真正的平移等变性。
import torch
import torch.nn as nn
import math
class MappingNetwork(nn.Module):
"""StyleGAN 映射网络:z -> w"""
def __init__(self, z_dim: int = 512, w_dim: int = 512, num_layers: int = 8):
super().__init__()
layers = []
for _ in range(num_layers):
layers.extend([
nn.Linear(z_dim if _ == 0 else w_dim, w_dim),
nn.LeakyReLU(0.2, inplace=True),
])
self.mapping = nn.Sequential(*layers)
# 初始化:缩小方差
for layer in self.mapping:
if isinstance(layer, nn.Linear):
fan_in = layer.in_features
layer.weight.data *= 1.0 / math.sqrt(fan_in)
def forward(self, z: torch.Tensor) -> torch.Tensor:
"""归一化输入 + 映射"""
z = z / torch.linalg.norm(z, dim=-1, keepdim=True) # 归一化
return self.mapping(z)
class AdaIN(nn.Module):
"""自适应实例归一化"""
def __init__(self, channels: int, w_dim: int = 512):
super().__init__()
self.norm = nn.InstanceNorm2d(channels, affine=False)
self.style_scale = nn.Linear(w_dim, channels)
self.style_bias = nn.Linear(w_dim, channels)
# 初始化:输出接近 1 和 0
nn.init.zeros_(self.style_scale.weight)
nn.init.ones_(self.style_scale.bias)
nn.init.zeros_(self.style_bias.weight)
nn.init.zeros_(self.style_bias.bias)
def forward(self, x: torch.Tensor, w: torch.Tensor) -> torch.Tensor:
"""AdaIN: norm(x) * scale(w) + bias(w)"""
normalized = self.norm(x)
scale = self.style_scale(w).view(x.size(0), -1, 1, 1)
bias = self.style_bias(w).view(x.size(0), -1, 1, 1)
return normalized * scale + biasclass StyleGAN2Generator(nn.Module):
"""StyleGAN2 生成器简化版"""
def __init__(self, z_dim=512, w_dim=512, img_size=128):
super().__init__()
self.mapping = MappingNetwork(z_dim, w_dim)
self.w_dim = w_dim
# 计算合成网络层数(从 4x4 到 img_size)
self.num_layers = int(math.log2(img_size)) * 2 - 2
# 常数输入 4x4
self.const_input = nn.Parameter(torch.randn(1, 512, 4, 4))
# 每层的 AdaIN + 卷积
self.synthesis = nn.ModuleList()
channels = 512
for i in range(self.num_layers):
if i > 0 and i % 2 == 0:
channels = max(channels // 2, 64)
self.synthesis.extend([
nn.Conv2d(channels, channels, 3, padding=1),
AdaIN(channels, w_dim),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(channels, channels, 3, padding=1),
AdaIN(channels, w_dim),
nn.LeakyReLU(0.2, inplace=True),
nn.Upsample(scale_factor=2) if i < self.num_layers - 2 else nn.Identity(),
])
self.to_rgb = nn.Conv2d(channels, 3, 1)
def forward(self, z: torch.Tensor, truncation: float = 0.7) -> torch.Tensor:
w = self.mapping(z)
# 截断技巧(Truncation Trick):向均值收缩,提高质量、降低多样性
w_avg = torch.zeros(1, self.w_dim, device=z.device) # 预计算的 w 均值
w = w_avg + truncation * (w - w_avg)
x = self.const_input.expand(z.size(0), -1, -1, -1)
for layer in self.synthesis:
if isinstance(layer, AdaIN):
x = layer(x, w)
else:
x = layer(x)
return torch.tanh(self.to_rgb(x))
def style_mixing(self, z1: torch.Tensor, z2: torch.Tensor,
cutoff_layer: int = 4) -> torch.Tensor:
"""风格混合:z1 提供 coarse 风格,z2 提供 fine 风格"""
w1 = self.mapping(z1)
w2 = self.mapping(z2)
# 在 cutoff_layer 处切换风格
w_mixed = torch.cat([w1[:, :cutoff_layer], w2[:, cutoff_layer:]], dim=1)
return self._forward_with_w(w_mixed)| StyleGAN 版本 | 年份 | 核心改进 | 分辨率 | 主要突破 |
|---|---|---|---|---|
StyleGAN | 2018 | Mapping Network + AdaIN + 噪声注入 | 1024x1024 | 解耦风格控制 |
StyleGAN2 | 2020 | 移除 AdaIN 的 IN 步骤 + 权重归一化 | 1024x1024 | 消除水滴伪影 |
StyleGAN3 | 2021 | 等变卷积 + 抗混叠滤波 | 1024x1024 | 纹理平移等变性 |
StyleGAN-XL | 2022 | 大规模训练 + 架构缩放 | 1024x1024 | ImageNet-1K SOTA |
StyleGAN3-T | 2021 | 平移等变版本 | 1024x1024 | 消除纹理粘连 |
💡 一句话理解
StyleGAN 的截断技巧(truncation trick)是提升生成质量的关键——truncation_psi 设 0.7 适合高质量生成,设 1.0 适合探索潜在空间多样性
⚠️ 常见踩坑
StyleGAN 只适合生成固定分辨率的图像(如人脸),不适合可变分辨率或条件生成任务——它的架构是专门为高质量人脸生成设计的
3Diffusion 模型基础:前向过程与反向过程
扩散模型的核心思想极其优雅:前向过程逐步加噪到高斯分布,反向过程学习从噪声去噪还原。前向过程是一个固定参数的马尔可夫链:每一步根据预设的方差调度(Variance Schedule)向数据添加少量高斯噪声。经过 T 步(通常 1000 步)后,数据完全变为高斯噪声。关键在于——由于每一步只添加少量噪声,前向过程任意时刻 t 的分布可以直接计算,不需要逐步模拟,公式为 q(x_t|x_0) = N(x_t; sqrt(alpha_bar_t) * x_0, (1-alpha_bar_t) * I)。反向过程是学习一个参数化的马尔可夫链 p_theta(x_{t-1}|x_t),用神经网络预测每一步的去噪方向。DDPM 将反向过程的均值预测转化为噪声预测——网络只需预测添加的噪声 epsilon,这大大简化了训练目标。训练损失简化为预测噪声与真实噪声之间的 MSE 损失:L = E_{t,x_0,epsilon}[||epsilon - epsilon_theta(x_t, t)||^2]。这个简洁的损失函数是扩散模型能够在大规模数据集上成功训练的关键。
import torch
import torch.nn as nn
import math
class LinearNoiseScheduler:
"""线性噪声调度器"""
def __init__(self, timesteps: int = 1000, beta_start: float = 1e-4,
beta_end: float = 0.02):
self.timesteps = timesteps
self.betas = torch.linspace(beta_start, beta_end, timesteps)
self.alphas = 1.0 - self.betas
self.alpha_bars = torch.cumprod(self.alphas, dim=0)
def add_noise(self, x0: torch.Tensor, t: torch.Tensor) -> tuple:
"""前向加噪:一步得到 x_t(不需要逐步模拟)"""
noise = torch.randn_like(x0)
alpha_bar = self.alpha_bars[t].view(-1, 1, 1, 1)
x_t = torch.sqrt(alpha_bar) * x0 + torch.sqrt(1 - alpha_bar) * noise
return x_t, noise
def get_alpha_bar(self, t: torch.Tensor) -> torch.Tensor:
return self.alpha_bars[t].view(-1, 1, 1, 1)
class SimpleUNet(nn.Module):
"""简化版 U-Net(扩散模型的去噪网络)"""
def __init__(self, in_channels: int = 3, base_channels: int = 64):
super().__init__()
self.time_embed = nn.Sequential(
nn.Linear(1, base_channels * 4),
nn.SiLU(),
nn.Linear(base_channels * 4, base_channels * 4),
)
# 编码器
self.enc1 = nn.Conv2d(in_channels, base_channels, 3, padding=1)
self.enc2 = nn.Conv2d(base_channels, base_channels * 2, 3, padding=1)
self.enc3 = nn.Conv2d(base_channels * 2, base_channels * 4, 3, padding=1)
# 解码器
self.dec3 = nn.ConvTranspose2d(base_channels * 4, base_channels * 2, 3, padding=1)
self.dec2 = nn.ConvTranspose2d(base_channels * 2, base_channels, 3, padding=1)
self.dec1 = nn.ConvTranspose2d(base_channels, in_channels, 3, padding=1)
self.pool = nn.MaxPool2d(2)
self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)
def forward(self, x: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
t_emb = self.time_embed(t.float().unsqueeze(-1)).unsqueeze(-1).unsqueeze(-1)
e1 = torch.relu(self.enc1(x) + t_emb)
e2 = torch.relu(self.enc2(self.pool(e1)) + t_emb)
e3 = torch.relu(self.enc3(self.pool(e2)) + t_emb)
d3 = torch.relu(self.dec3(e3) + e2 + t_emb)
d2 = torch.relu(self.dec2(self.up(d3)) + e1 + t_emb)
d1 = self.dec1(self.up(d2))
return d1 # 预测的噪声"""DDPM 训练与采样循环"""
import torch
from torch.utils.data import DataLoader
class DDPMSampler:
"""DDPM 采样器"""
def __init__(self, scheduler: LinearNoiseScheduler, model: nn.Module):
self.scheduler = scheduler
self.model = model
@torch.no_grad()
def sample(self, n_samples: int, shape: tuple, device: str = 'cpu') -> torch.Tensor:
"""从纯噪声逐步去噪生成样本"""
x = torch.randn(n_samples, *shape, device=device)
for t in reversed(range(self.scheduler.timesteps)):
t_batch = torch.full((n_samples,), t, device=device, dtype=torch.long)
# 预测噪声
predicted_noise = self.model(x, t_batch)
# 提取调度参数
alpha = self.scheduler.alphas[t]
alpha_bar = self.scheduler.alpha_bars[t]
beta = self.scheduler.betas[t]
# 反向步(DDPM 更新公式)
mean = (1.0 / torch.sqrt(alpha)) * (
x - (beta / torch.sqrt(1 - alpha_bar)) * predicted_noise
)
variance = beta
# 除了最后一步,都添加噪声
if t > 0:
noise = torch.randn_like(x)
x = mean + torch.sqrt(variance) * noise
else:
x = mean
return torch.clamp(x, -1.0, 1.0)
# 训练循环示例
def train_diffusion(model, dataloader, scheduler, epochs=100):
optimizer = torch.optim.AdamW(model.parameters(), lr=2e-4)
for epoch in range(epochs):
for x0 in dataloader:
x0 = x0.cuda()
# 随机采样时间步
t = torch.randint(0, scheduler.timesteps, (x0.size(0),), device='cuda')
# 前向加噪(一步到位)
x_t, noise = scheduler.add_noise(x0, t)
# 预测噪声
predicted_noise = model(x_t, t)
# MSE 损失
loss = nn.MSELoss()(predicted_noise, noise)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch % 10 == 0:
print(f"Epoch {epoch}, Loss: {loss.item():.6f}")| 扩散模型组件 | 作用 | 数学描述 | 可学习? |
|---|---|---|---|
前向过程 q(x_t|x_{t-1}) | 逐步添加高斯噪声 | N(sqrt(1-beta)x_{t-1}, betaI) | 否(固定调度) |
前向闭式 q(x_t|x_0) | 一步计算任意 t 的分布 | N(sqrt(alpha_bar_t)*x_0, (1-alpha_bar_t)*I) | 否 |
反向过程 p(x_{t-1}|x_t) | 学习去噪 | N(mu_theta, sigma^2) | 是 |
噪声预测网络 epsilon_theta | 预测添加的噪声 | CNN/UNet 输出 | 是 |
时间嵌入 t | 告知当前噪声级别 | 正弦位置编码 / MLP | 是 |
方差调度 beta_t | 控制每步加噪量 | 线性 / 余弦 / 二次 | 否(预设) |
💡 一句话理解
训练扩散模型时,余弦噪声调度(cosine schedule)比线性调度能产生更高质量的样本——因为它在低噪声区域(接近 x_0)分配了更多时间步
⚠️ 常见踩坑
扩散模型的推理速度是最大瓶颈——标准 DDPM 需要 1000 步去噪,生成一张图可能需要数秒,不直接用于实时场景
4DDIM 加速采样:从马尔可夫到非马尔可夫
DDIM(2020)是扩散模型加速采样的里程碑工作。DDPM 的采样必须逐步进行,因为每一步 x_{t-1} 依赖于 x_t 和预测的噪声——这是一个马尔可夫链。DDIM 的核心洞察是:扩散模型的训练目标只约束了边缘分布,没有约束条件分布——这意味着存在无数个具有相同边缘分布但不同条件分布的扩散过程。DDIM 选择了一个确定性(非马尔可夫)的反向过程:给定 x_t 和预测的 x_0,x_{t-1} 被完全确定(不添加随机噪声),这使得反向过程变成了一个常微分方程(ODE)的离散化。确定性采样的关键优势在于:由于过程是确定性的,可以跳过中间步骤——从 1000 步直接跳到 100 步甚至更少。100 步 DDIM 采样的图像质量几乎与 1000 步 DDPM 相当,而速度提升 10 倍。DDIM 还引入了 eta 参数在确定性和随机性之间插值。DDIM 的另一个重要特性是「编码」能力——由于采样是确定性的,可以从真实图像反向编码到潜在空间,为图像编辑提供了可能。
class DDIMSampler:
"""DDIM 确定性采样器"""
def __init__(self, scheduler: LinearNoiseScheduler, model: nn.Module):
self.scheduler = scheduler
self.model = model
@torch.no_grad()
def sample(self, n_samples: int, shape: tuple, num_steps: int = 50,
eta: float = 0.0, device: str = 'cpu') -> torch.Tensor:
"""
DDIM 采样(支持加速)
num_steps: 采样步数(可以远小于 scheduler.timesteps)
eta: 随机性系数,0=确定性(DDIM),1=随机(DDPM)
"""
# 选择子序列(均匀间隔的时间步)
timesteps = torch.linspace(
self.scheduler.timesteps - 1, 0, num_steps, dtype=torch.long
)
timesteps_next = torch.cat([timesteps[1:], torch.tensor([-1])])
x = torch.randn(n_samples, *shape, device=device)
for i, (t, t_next) in enumerate(zip(timesteps, timesteps_next)):
t_batch = torch.full((n_samples,), t, device=device, dtype=torch.long)
# 预测噪声
eps_pred = self.model(x, t_batch)
# 从 x_t 和 eps_pred 恢复 x_0 预测
alpha_bar_t = self.scheduler.alpha_bars[t]
alpha_bar_next = self.scheduler.alpha_bars[t_next] if t_next >= 0 else torch.ones_like(alpha_bar_t)
x0_pred = (x - torch.sqrt(1 - alpha_bar_t) * eps_pred) / torch.sqrt(alpha_bar_t)
x0_pred = torch.clamp(x0_pred, -1.0, 1.0) # 裁剪防止数值溢出
if t_next < 0:
# 最后一步:直接返回 x0_pred
x = x0_pred
else:
# 方向指向 x0 的分量
dir_xt = torch.sqrt(1 - alpha_bar_next - (eta ** 2) * (
(1 - alpha_bar_t) / alpha_bar_t
)) * eps_pred
# 随机分量(当 eta > 0 时)
sigma = eta * torch.sqrt((1 - alpha_bar_t) / alpha_bar_t
* (1 - alpha_bar_next) / (1 - alpha_bar_t))
noise = torch.randn_like(x) if eta > 0 else 0
x = torch.sqrt(alpha_bar_next) * x0_pred + dir_xt + sigma * noise
return torch.clamp(x, -1.0, 1.0)
# 速度对比
for steps in [1000, 500, 100, 50, 25]:
print(f" DDIM {steps:>4} 步: {'~' * max(1, steps // 10)} ({1000/steps:.0f}x 加速)")"""DDIM 图像编码与编辑"""
class DDIMInversion:
"""DDIM 反向编码:从图像找到对应的初始噪声"""
def __init__(self, scheduler: LinearNoiseScheduler, model: nn.Module):
self.scheduler = scheduler
self.model = model
@torch.no_grad()
def invert(self, x0: torch.Tensor, num_steps: int = 50) -> torch.Tensor:
"""将真实图像 x0 编码为初始噪声 x_T"""
timesteps = torch.linspace(0, self.scheduler.timesteps - 1, num_steps, dtype=torch.long)
timesteps_next = torch.cat([timesteps[:1] - 1, timesteps[:-1]])
x = x0.clone()
for t, t_next in zip(timesteps, timesteps_next):
t_batch = torch.full((x0.size(0),), t, device=x0.device, dtype=torch.long)
eps_pred = self.model(x, t_batch)
alpha_bar_t = self.scheduler.alpha_bars[t]
alpha_bar_next = self.scheduler.alpha_bars[t_next] if t_next >= 0 else torch.zeros_like(alpha_bar_t)
# 反向 DDIM 步
x0_pred = (x - torch.sqrt(1 - alpha_bar_t) * eps_pred) / torch.sqrt(alpha_bar_t)
dir_xt = torch.sqrt(1 - alpha_bar_next) * eps_pred
x = torch.sqrt(alpha_bar_next) * x0_pred + dir_xt
return x
class ImageEditor:
"""基于 DDIM 的图像编辑器"""
def __init__(self, sampler: DDIMSampler, inverter: DDIMInversion):
self.sampler = sampler
self.inverter = inverter
def edit(self, image: torch.Tensor, edit_fn, num_steps: int = 50) -> torch.Tensor:
"""
编辑图像:编码 → 编辑潜在空间 → 解码
edit_fn: 对初始噪声 x_T 进行操作的函数
"""
# 1. 编码到噪声空间
x_T = self.inverter.invert(image, num_steps)
# 2. 编辑噪声
x_T_edited = edit_fn(x_T)
# 3. 从编辑后的噪声重新采样
# (注意:这里需要修改 sampler 从指定 x_T 开始)
return self.sampler.sample_from_xt(x_T_edited, num_steps)| 采样方法 | 步数 | FID (CelebA) | 每图耗时 | 确定性 | 可编码? |
|---|---|---|---|---|---|
DDPM | 1000 | 3.17 | ~15s | 否 | 否 |
DDPM | 100 | 4.16 | ~1.5s | 否 | 否 |
DDIM | 100 | 3.29 | ~1.5s | 是 | 是 |
DDIM | 50 | 3.58 | ~0.75s | 是 | 是 |
DDIM | 25 | 4.67 | ~0.38s | 是 | 是 |
DDIM | 10 | 8.45 | ~0.15s | 是 | 是 |
💡 一句话理解
DDIM 的 eta=0 确定性采样不仅速度快,还支持图像编辑——先编码再修改潜在空间再解码,是图像编辑的实用方案
⚠️ 常见踩坑
DDIM 加速采样的步数不能无限减少——当步数低于 10 时,生成质量急剧下降,此时应考虑更高级的采样器(如 DPM-Solver)
5评估指标:FID 与 Inception Score
评估生成模型的质量是生成式 AI 中最大的挑战之一——不像分类任务有明确的准确率指标,生成模型需要从多个维度衡量。Inception Score(IS,2016)是第一个广泛使用的自动评估指标,它利用预训练的 Inception-v3 网络对生成图像进行分类,从两个维度评分:清晰度和多样性。IS 的局限在于它只关注分类语义多样性,不评估图像的真实感——一张模糊但分类明确的图可能获得很高的 IS。FID(Fréchet Inception Distance,2017)是目前最主流的评估指标。它计算真实图像和生成图像在 Inception-v3 中间层特征空间中的 Fréchet 距离。FID 越低表示生成质量越好(0 表示完全一致),它同时评估了生成质量和多样性,且与人类主观评价高度相关。但 FID 也有局限:对样本量敏感(需要至少 10000 张图)、计算成本高、Inception 特征空间不一定覆盖所有图像类型。此外,Precision 和 Recall 指标被提出用于分别评估生成的「质量」和「多样性」——Precision 衡量生成样本中有多少是「真实」的(落在真实数据流形内),Recall 衡量真实数据流形中有多少被生成模型覆盖了。
import torch
import numpy as np
from scipy import linalg
from torchvision.models import inception_v3, Inception_V3_Weights
import torch.nn.functional as F
class FIDEvaluator:
"""Fréchet Inception Distance 评估器"""
def __init__(self, device: str = 'cuda'):
self.device = device
# 使用 Inception-v3 的 pool3 层(2048 维特征)
weights = Inception_V3_Weights.IMAGENET1K_V1
model = inception_v3(weights=weights, transform_input=False)
model.fc = nn.Identity() # 去掉分类头
model.aux_logits = False
self.model = model.eval().to(device)
@torch.no_grad()
def extract_features(self, images: torch.Tensor) -> np.ndarray:
"""提取 Inception 特征"""
# images: [N, 3, 299, 299], 范围 [0, 1]
images = images.to(self.device)
features = self.model(images) # [N, 2048]
return features.cpu().numpy()
@staticmethod
def calculate_fid(mu1: np.ndarray, sigma1: np.ndarray,
mu2: np.ndarray, sigma2: np.ndarray) -> float:
"""计算 Fréchet 距离"""
diff = mu1 - mu2
covmean, _ = linalg.sqrtm(sigma1 @ sigma2, disp=False)
if np.iscomplexobj(covmean):
covmean = covmean.real
fid = diff.dot(diff) + np.trace(sigma1 + sigma2 - 2 * covmean)
return max(0, fid) # FID 不应为负
def evaluate(self, real_images: torch.Tensor,
fake_images: torch.Tensor) -> float:
"""评估真实集和生成集的 FID"""
real_feats = self.extract_features(real_images)
fake_feats = self.extract_features(fake_images)
mu_real, sigma_real = real_feats.mean(0), np.cov(real_feats, rowvar=False)
mu_fake, sigma_fake = fake_feats.mean(0), np.cov(fake_feats, rowvar=False)
return self.calculate_fid(mu_real, sigma_real, mu_fake, sigma_fake)"""Precision & Recall 评估"""
class PrecisionRecallEvaluator:
"""评估生成质量(Precision)和多样性(Recall)"""
def __init__(self, device: str = 'cuda', k: int = 5):
self.device = device
self.k = k # 最近邻数量
@torch.no_grad()
def extract_features(self, images: torch.Tensor) -> np.ndarray:
"""提取特征(简化为 Flattened 图像或预训练特征)"""
return images.view(images.size(0), -1).cpu().numpy()
def compute_precision_recall(self, real_feats: np.ndarray,
fake_feats: np.ndarray) -> tuple:
"""
Precision: 生成样本中落在真实数据流形内的比例
Recall: 真实数据流形中被生成样本覆盖的比例
"""
# 计算真实特征的 k-NN 半径
from sklearn.neighbors import NearestNeighbors
# 真实数据流形边界(用 k-NN 半径估计)
nn_real = NearestNeighbors(n_neighbors=self.k).fit(real_feats)
dists_real, _ = nn_real.kneighbors(real_feats)
radii_real = dists_real[:, -1] # 每个真实样本的 k-NN 半径
# 生成数据流形边界
nn_fake = NearestNeighbors(n_neighbors=self.k).fit(fake_feats)
dists_fake, _ = nn_fake.kneighbors(fake_feats)
radii_fake = dists_fake[:, -1]
# Precision: 每个生成样本是否在真实流形内
dists_fake_to_real, _ = nn_real.kneighbors(fake_feats)
precision = np.mean(dists_fake_to_real[:, 0] < radii_real.min())
# Recall: 每个真实样本是否在生成了流形内
dists_real_to_fake, _ = nn_fake.kneighbors(real_feats)
recall = np.mean(dists_real_to_fake[:, 0] < radii_fake.min())
return float(precision), float(recall)
# 典型 FID 参考值
fid_benchmarks = {
"StyleGAN2 (FFHQ 1024x1024)": 2.84,
"StyleGAN3 (FFHQ 1024x1024)": 2.43,
"Guided Diffusion (ImageNet 256x256)": 2.07,
"DALL-E 2": 10.39,
"Stable Diffusion v1.4": 9.63,
"Stable Diffusion v2.1": 7.71,
"SDXL 1.0": 6.64,
}
for model, fid in sorted(fid_benchmarks.items(), key=lambda x: x[1]):
print(f" {model:<45} FID: {fid:.2f}")| 评估指标 | 评估维度 | 值范围 | 越?越好 | 计算成本 | 局限性 |
|---|---|---|---|---|---|
IS | 清晰度 + 多样性 | [1, ∞) | 越高越好 | 低 | 不评估真实感 |
FID | 分布距离 | [0, ∞) | 越低越好 | 中 | 依赖 Inception 特征 |
KID | MMD 距离(无偏) | [-1, 1] | 越低越好 | 中 | 需要大样本 |
Precision | 生成质量 | [0, 1] | 越高越好 | 中 | 只评估质量 |
Recall | 覆盖多样性 | [0, 1] | 越高越好 | 中 | 只评估多样性 |
CLIP Score | 图文一致性 | [0, ∞) | 越高越好 | 低 | 仅条件生成 |
💡 一句话理解
6GAN vs Diffusion:两大范式全面对比
GAN 和 Diffusion 代表了图像生成的两条截然不同的技术路线。GAN 的核心哲学是「对抗」——通过生成器和判别器的零和博弈,让生成器学会产生判别器无法区分的假样本。这种对抗性训练的产物是「一次前向传播即可生成」的极致效率,但也带来了模式崩溃、训练不稳定、评估困难等顽疾。Diffusion 的核心哲学是「渐进」——通过马尔可夫链逐步加噪和去噪,将复杂的生成分解为许多简单步骤。每个步骤只需要学习「减少一点噪声」,这远比「一次性从噪声变成图像」容易。代价是采样需要数百到数千步迭代。从实践角度,两者的差异体现在:训练方面,Diffusion 的训练目标是一个简单的 MSE 损失,几乎不需要调参;GAN 需要精心设计学习率、正则化、架构。质量方面,Diffusion 已经全面超越 GAN——在大规模数据集上 FID 显著更低,且没有模式崩溃问题。效率方面,GAN 生成一张图只需 1-10ms,Diffusion 即使经过加速也需要 100ms-数秒。应用场景的选择取决于需求:实时应用(视频游戏、AR)仍然依赖 GAN;对质量要求极高的场景(艺术创作、设计)选择 Diffusion;移动端部署目前 GAN 仍有优势。值得注意的是,两大范式正在融合——GAN 启发的对抗损失被引入 Diffusion 训练,Diffusion 的渐进思想也被用于改进 GAN。
"""Classifier-Free Guidance (CFG):Diffusion 的条件控制"""
class CFGDiffusionSampler:
"""带分类器自由引导的扩散模型采样器"""
def __init__(self, model: nn.Module, scheduler: LinearNoiseScheduler):
self.model = model # 联合训练的条件/无条件模型
self.scheduler = scheduler
@torch.no_grad()
def sample_cfg(self, n: int, shape: tuple, cond: torch.Tensor,
uncond: torch.Tensor, guidance_scale: float = 7.5,
num_steps: int = 50) -> torch.Tensor:
"""
CFG 采样:同时运行条件和无条件前向传播
guidance_scale: 越大 → 越忠于条件但质量可能下降
典型值: 3-10(Stable Diffusion 默认 7.5)
"""
timesteps = torch.linspace(
self.scheduler.timesteps - 1, 0, num_steps, dtype=torch.long
)
timesteps_next = torch.cat([timesteps[1:], torch.tensor([-1])])
x = torch.randn(n, *shape)
for t, t_next in zip(timesteps, timesteps_next):
t_batch = torch.full((n,), t, dtype=torch.long)
# 条件预测
eps_cond = self.model(x, t_batch, cond)
# 无条件预测
eps_uncond = self.model(x, t_batch, uncond)
# CFG: 引导方向 = 无条件 + scale * (条件 - 无条件)
eps_guided = eps_uncond + guidance_scale * (eps_cond - eps_uncond)
# 使用 DDIM 更新
alpha_bar_t = self.scheduler.alpha_bars[t]
alpha_bar_next = self.scheduler.alpha_bars[t_next] if t_next >= 0 else torch.ones_like(alpha_bar_t)
x0_pred = (x - torch.sqrt(1 - alpha_bar_t) * eps_guided) / torch.sqrt(alpha_bar_t)
x0_pred = torch.clamp(x0_pred, -1.0, 1.0)
if t_next >= 0:
dir_xt = torch.sqrt(1 - alpha_bar_next) * eps_guided
x = torch.sqrt(alpha_bar_next) * x0_pred + dir_xt
else:
x = x0_pred
return torch.clamp(x, -1.0, 1.0)
# CFG scale 的影响
for scale in [1.0, 3.0, 5.0, 7.5, 10.0, 15.0]:
quality = "低" if scale < 3 else "中" if scale < 7 else "高" if scale < 12 else "过饱和"
diversity = "高" if scale < 5 else "中" if scale < 10 else "低"
print(f" CFG={scale:>4.1f}: 质量={quality:<6} 多样性={diversity}")"""GAN vs Diffusion 决策树"""
def choose_generative_model(requirements: dict) -> str:
"""根据需求选择生成模型"""
if requirements.get("real_time", False):
return "StyleGAN2/3 (GAN) —— 需要实时生成"
if requirements.get("max_quality", False):
if requirements.get("text_conditioned", False):
return "Stable Diffusion / DALL-E 3 (Diffusion)"
return "Guided Diffusion (Diffusion)"
if requirements.get("mobile_deploy", False):
return "MobileStyleGAN (轻量 GAN) —— 端侧部署"
if requirements.get("image_editing", False):
return "DDIM Inversion + Diffusion —— 支持编码编辑"
if requirements.get("high_diversity", False):
return "Diffusion —— 更好的模式覆盖"
if requirements.get("training_ease", False):
return "Diffusion —— 训练稳定,几乎无需调参"
if requirements.get("deterministic_output", False):
return "DDIM (eta=0) —— 确定性采样"
return "StyleGAN3 (GAN) —— 均衡选择"
# 典型场景决策
scenarios = [
{"name": "AI 绘画 App", "requirements": {"max_quality": True, "text_conditioned": True}},
{"name": "游戏 NPC 头像", "requirements": {"real_time": True, "max_quality": False}},
{"name": "移动端滤镜", "requirements": {"mobile_deploy": True}},
{"name": "产品图修改", "requirements": {"image_editing": True}},
{"name": "数据集增强", "requirements": {"high_diversity": True}},
]
for scenario in scenarios:
choice = choose_generative_model(scenario["requirements"])
print(f" {scenario['name']:<15} → {choice}")| 对比维度 | GAN (StyleGAN3) | Diffusion (SDXL) | 胜出方 |
|---|---|---|---|
训练稳定性 | 不稳定,需大量调参 | 稳定,MSE 损失 | Diffusion |
图像质量 (FID) | FID ~2.4 (FFHQ) | FID ~3-10 (视模型) | GAN (人脸) / Diffusion (通用) |
生成速度 | 1-10ms/张 | 100ms-5s/张 | GAN |
模式覆盖 | 中等(有模式崩溃) | 极好 | Diffusion |
条件生成 | 支持(条件 GAN) | CFG 机制,灵活 | Diffusion |
图像编辑 | 困难(单向映射) | DDIM 编码+编辑 | Diffusion |
移动端部署 | 成熟(模型小) | 困难(模型大+多步) | GAN |
训练数据需求 | 中等 | 大规模数据效果好 | GAN (小数据) |
理论基础 | 博弈论(纳什均衡) | 非平衡热力学 | 平局 |
社区生态 | 成熟 | 爆发增长 | Diffusion |
💡 一句话理解
实际项目中不要陷入「二选一」的思维陷阱——可以组合使用:用 Diffusion 做粗生成,用 GAN 做精修(Refinement),结合两者优势
⚠️ 常见踩坑
CFG scale 不是越大越好——超过 15 会导致图像过饱和、颜色失真、出现 artifacts,Stable Diffusion 的推荐范围是 5-10
7PyTorch 实战:训练简单扩散模型
理论终需落地为代码。本节从零实现一个完整的扩散模型训练流程,包括:噪声调度器、U-Net 去噪网络、训练循环、采样器,以及可视化生成结果。虽然这是一个简化版本(在 MNIST 数据集上训练),但涵盖了扩散模型的所有核心组件。训练扩散模型的关键细节:时间嵌入必须足够丰富——正弦位置编码或 learnable embedding 都能工作,但前者在训练数据范围外的时间步有更好的泛化性;U-Net 中的跳跃连接至关重要——它们让网络在去噪时保留输入的结构信息;EMA(指数移动平均)权重在采样时使用,可以显著提升生成质量——EMA 权重是训练过程中所有历史权重的平滑平均,相当于一个隐式的模型集成;学习率预热(Warmup)有助于训练初期的稳定性。
"""完整扩散模型训练 Pipeline"""
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
from torchvision.utils import save_image
import math
class SinusoidalTimeEmbedding(nn.Module):
"""正弦位置编码时间嵌入"""
def __init__(self, dim: int):
super().__init__()
self.dim = dim
def forward(self, t: torch.Tensor) -> torch.Tensor:
half_dim = self.dim // 2
embeddings = math.log(10000.0) / (half_dim - 1)
embeddings = torch.exp(torch.arange(half_dim, device=t.device) * -embeddings)
embeddings = t.float().unsqueeze(-1) * embeddings.unsqueeze(0)
embeddings = torch.cat([embeddings.sin(), embeddings.cos()], dim=-1)
return embeddings
class SimpleDiffusionModel(nn.Module):
"""简化扩散模型 U-Net"""
def __init__(self, img_size: int = 28, channels: int = 1, hidden_dim: int = 128):
super().__init__()
self.time_embed = SinusoidalTimeEmbedding(hidden_dim * 4)
self.time_mlp = nn.Sequential(
nn.Linear(hidden_dim * 4, hidden_dim * 4),
nn.SiLU(),
nn.Linear(hidden_dim * 4, hidden_dim * 4),
)
# 编码器
self.down1 = nn.Conv2d(channels, hidden_dim, 3, padding=1)
self.down2 = nn.Conv2d(hidden_dim, hidden_dim * 2, 4, 2, 1)
self.down3 = nn.Conv2d(hidden_dim * 2, hidden_dim * 4, 4, 2, 1)
# 中间层
self.mid = nn.Sequential(
nn.Conv2d(hidden_dim * 4, hidden_dim * 4, 3, padding=1),
nn.SiLU(),
nn.Conv2d(hidden_dim * 4, hidden_dim * 4, 3, padding=1),
)
# 解码器
self.up1 = nn.ConvTranspose2d(hidden_dim * 4, hidden_dim * 2, 4, 2, 1)
self.up2 = nn.ConvTranspose2d(hidden_dim * 2, hidden_dim, 4, 2, 1)
self.up3 = nn.Conv2d(hidden_dim, channels, 3, padding=1)
def forward(self, x: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
t_emb = self.time_mlp(self.time_embed(t))
t_emb = t_emb.unsqueeze(-1).unsqueeze(-1)
d1 = F.silu(self.down1(x) + t_emb)
d2 = F.silu(self.down2(d1) + t_emb)
d3 = F.silu(self.down3(d2) + t_emb)
m = self.mid(d3)
u1 = F.silu(self.up1(m) + d2 + t_emb)
u2 = F.silu(self.up2(u1) + d1 + t_emb)
return self.up3(u2)"""训练与采样主循环"""
def train_diffusion_mnist(epochs: int = 50, batch_size: int = 128):
"""在 MNIST 上训练扩散模型"""
device = 'cuda' if torch.cuda.is_available() else 'cpu'
scheduler = LinearNoiseScheduler(timesteps=1000)
model = SimpleDiffusionModel().to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
# EMA 权重(用于采样)
ema_model = SimpleDiffusionModel().to(device)
ema_model.load_state_dict(model.state_dict())
ema_decay = 0.995
# 数据
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,)), # 归一化到 [-1, 1]
])
dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
for epoch in range(epochs):
model.train()
total_loss = 0
for x0, _ in loader:
x0 = x0.to(device)
t = torch.randint(0, scheduler.timesteps, (x0.size(0),), device=device)
# 前向加噪
x_t, noise = scheduler.add_noise(x0, t)
# 预测噪声
pred_noise = model(x_t, t)
loss = F.mse_loss(pred_noise, noise)
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
# EMA 更新
for ema_p, p in zip(ema_model.parameters(), model.parameters()):
ema_p.data = ema_decay * ema_p.data + (1 - ema_decay) * p.data
total_loss += loss.item()
print(f"Epoch {epoch+1}/{epochs}, Loss: {total_loss/len(loader):.6f}")
# 每个 epoch 采样一张图看看进展
if (epoch + 1) % 5 == 0:
sampler = DDIMSampler(scheduler, ema_model)
samples = sampler.sample(16, (1, 28, 28), num_steps=50, device=device)
save_image((samples + 1) / 2, f'samples_epoch_{epoch+1}.png', nrow=4)
torch.save(ema_model.state_dict(), 'diffusion_mnist_ema.pt')
print("训练完成!模型已保存。")
# 推理:加载训练好的模型生成图像
def generate_samples(model_path: str = 'diffusion_mnist_ema.pt', n: int = 16):
device = 'cuda' if torch.cuda.is_available() else 'cpu'
scheduler = LinearNoiseScheduler(timesteps=1000)
model = SimpleDiffusionModel().to(device)
model.load_state_dict(torch.load(model_path, map_location=device))
sampler = DDIMSampler(scheduler, model)
samples = sampler.sample(n, (1, 28, 28), num_steps=50, device=device)
save_image((samples + 1) / 2, 'generated.png', nrow=4)
print(f"已生成 {n} 张样本 → generated.png")| 训练超参数 | 推荐值 | 说明 |
|---|---|---|
学习率 | 2e-4 ~ 3e-4 | AdamW,比 Adam 更稳定 |
Batch Size | 64 ~ 256 | 越大越稳定,受显存限制 |
时间步 T | 1000 | 训练用 1000,推理可用更少 |
梯度裁剪 | 1.0 | 防止梯度爆炸 |
EMA Decay | 0.995 ~ 0.9999 | 采样时使用 EMA 权重 |
Warmup | 5000 步 | 学习率从 0 线性增加到目标值 |
噪声调度 | 余弦 | 比线性调度生成质量更高 |
优化器 | AdamW | 权重衰减 0.01 |
Epoch | 50 ~ 200 | MNIST 50 足够,ImageNet 需要更多 |
⚠️ 常见踩坑
不要在扩散模型训练中用太大的学习率(>1e-3)——虽然 MSE 损失看起来稳定,但生成质量会显著下降,因为模型会学到退化解(预测零噪声)
8扩展到视频生成:从图像到时空扩散
图像生成模型的成熟自然推动了向视频生成的扩展。视频生成的核心挑战在于时间一致性——不仅是每一帧要好看,帧与帧之间还要连贯流畅。
视频生成的本质是将扩散模型从二维(空间)扩展到三维(空间 + 时间)。具体来说,输入从单张图像 [B, C, H, W] 变为视频片段 [B, C, T, H, W],其中 T 是帧数。最简单的扩展方式是将 2D 卷积替换为 3D 卷积——3D 卷积核同时在空间和时间维度上滑动,天然地捕捉时空特征。但这带来了巨大的计算成本:一个 2 秒(60 帧)的视频需要的计算量是单帧图像的 60 倍。
Sora 采用的 DiT(Diffusion Transformer)架构提供了一条更高效的路线:将视频帧切分为时空 Patch(Spacetime Patches),然后将每个 Patch 视为一个 Token,送入 Transformer 处理。这种方式的优势在于 Transformer 的自注意力机制天然支持长程依赖——无论是空间上的远距离像素还是时间上的远距离帧,都可以在同一层中建模。
视频生成的关键技术挑战包括:时间一致性(相邻帧之间的平滑过渡)、长视频生成(60 秒以上的连贯性)、条件控制(文本→视频的精确引导)、以及物理规律的模拟(重力、流体、碰撞)。前三个问题已有较好方案,但第四个——物理规律模拟——正是世界模型(World Model)的领域。
世界模型代表了视频生成的下一个范式升级:从「生成看起来像真的视频」到「模拟真实物理世界的运行」。2026 年 NVIDIA 发布的 SANA-WM(2.6B 参数开源世界模型)标志着这一方向的重大进展——它不是生成像素级视频,而是在潜在空间中预测物理状态的演变。这意味着世界模型可以回答「如果我这样做会发生什么」这类因果问题,而不仅仅是「下一帧看起来像什么」。
视频生成模型(如 Sora)和世界模型(如 SANA-WM)的关系可以理解为:视频生成模型负责渲染——将抽象状态变为可视画面;世界模型负责推理——在状态空间中进行规划和预测。两者的结合才是完整的「理解并生成视频」系统。
import torch
import torch.nn as nn
class SpacetimePatchEmbed(nn.Module):
"""Sora 风格的时空 Patch 嵌入"""
def __init__(self, patch_size: int = 16, in_chans: int = 3,
embed_dim: int = 768, temporal_patch: int = 2):
super().__init__()
# 3D 卷积:同时在时间和空间维度上切块
self.proj = nn.Conv3d(
in_chans, embed_dim,
kernel_size=(temporal_patch, patch_size, patch_size),
stride=(temporal_patch, patch_size, patch_size)
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""[B, C, T, H, W] → [B, N, D]"""
x = self.proj(x) # [B, D, T', H', W']
x = x.flatten(2).transpose(1, 2) # [B, N, D]
return x
class VideoDiffusionTransformer(nn.Module):
"""视频扩散 Transformer 简化版"""
def __init__(self, num_frames: int = 16, img_size: int = 256,
patch_size: int = 16, embed_dim: int = 768, depth: int = 12):
super().__init__()
self.patch_embed = SpacetimePatchEmbed(
patch_size=patch_size, embed_dim=embed_dim, temporal_patch=2
)
# 时间位置编码
num_patches = (num_frames // 2) * (img_size // patch_size) ** 2
self.pos_embed = nn.Parameter(torch.randn(1, num_patches, embed_dim))
self.time_embed = nn.Sequential(
nn.Linear(1, embed_dim * 4), nn.SiLU(),
nn.Linear(embed_dim * 4, embed_dim),
)
self.blocks = nn.ModuleList([
nn.TransformerEncoderLayer(
d_model=embed_dim, nhead=12,
dim_feedforward=embed_dim * 4, batch_first=True
) for _ in range(depth)
])
self.norm = nn.LayerNorm(embed_dim)
self.head = nn.Linear(embed_dim, patch_size ** 2 * 3 * 2)
def forward(self, x: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
x = self.patch_embed(x) + self.pos_embed
t_emb = self.time_embed(t.float().unsqueeze(-1)).unsqueeze(1)
x = x + t_emb
for block in self.blocks:
x = block(x)
x = self.norm(x)
return self.head(x)| 方法 | 时间建模 | 计算成本 | 视频长度 | 代表工作 |
|---|---|---|---|---|
3D 卷积扩展 | 3D 卷积核 | 极高 | 短(2-4 秒) | Video Diffusion Models |
时空 Patch (DiT) | 自注意力 | 高 | 中长(60 秒) | Sora |
帧间自回归 | 逐帧生成 | 中 | 理论上无限 | Phenaki, VideoPoet |
世界模型 (WM) | 潜在状态预测 | 中 | 取决于规划深度 | SANA-WM, Genie |
💡 一句话理解
视频生成的最佳入门路径:先掌握单图扩散模型 → 理解时间一致性的挑战 → 学习 DiT 的时空 Patch 设计 → 最后探索世界模型。不要跳过基础直接做视频。
⚠️ 常见踩坑
视频生成的计算成本远超图像——训练一个视频扩散模型需要的 GPU 小时是图像的数十到数百倍。在有限资源下,建议先用低分辨率(64x64x8)做原型验证。
9更新于 2026-05-17:视频生成与世界模型新进展
2026 年 5 月的生成式 AI 领域迎来了重大突破:NVIDIA 发布 SANA-WM(2.6B 参数开源世界模型),将世界模型从闭源实验推向开源社区。
SANA-WM 的核心贡献是证明了开源社区同样可以构建具备物理理解能力的 AI 系统。与 Sora 等闭源视频生成模型不同,SANA-WM 专注于在潜在空间中预测状态演变,而非像素级重建。这种设计使其在机器人仿真、强化学习等需要理解物理规律的场景中具有独特优势。
与此同时,视频生成模型也在持续进化:从 Sora 的 60 秒视频到新一代模型的更长时间窗口,从单纯的视觉逼真到对物理规律的初步模拟。两者的融合趋势明显——视频生成模型开始加入世界模型的预测能力,世界模型开始结合生成模型的渲染能力。
对于生成式 AI 的研究者和从业者来说,世界模型代表了从「生成」到「理解」的范式转变。它不再满足于做出看起来像真的画面,而是追求真正理解物理世界如何运作。这一转变将深刻影响 AI 在科学发现、机器人控制、自动驾驶等领域的应用前景。
💡 一句话理解
关注 NVIDIA SANA-WM 等开源世界模型项目的更新。这一领域正处于爆发期,几个月内的进展可能超过过去几年的总和。
⚠️ 常见踩坑
世界模型仍处于早期阶段。不要将其预测结果用于安全关键决策——当前的预测误差和幻觉问题尚未完全解决。
🎯 相关面试题
巩固本篇知识点,备战 AI 岗位面试。
- 初级场景查看详解 →
如何用 AI 给图片自动打标签 / 做图像识别功能?
简单用云视觉 API 或多模态大模型直接问图,定制需标注微调,注意类别定义与置信度阈值。
- 初级编码查看详解 →
手撕代码:实现两个边界框的 IoU
求两框交集面积与并集面积之比,关键是用 max(0,...) 钳制无重叠时的负宽高。
- 中级编码高频查看详解 →
手撕代码:实现非极大值抑制(NMS)
按分数排序,反复取最高分框并抑制与其 IoU 超阈值的框,去除冗余检测。
- 高级概念查看详解 →
Anchor-Free 目标检测(如 CenterNet/FCOS)相比 Anchor-Based 有何优势?
Anchor-Free 用关键点/中心+回归直接预测框,免去锚框设计与正负样本失衡,更简洁、超参更少。