05 - 生成模型轨道 - 创造的艺术 🎨
欢迎来到生成模型的世界!这条轨道探索如何让机器创造新内容——图像、音乐、文本…
核心问题: 如何从噪声或潜在空间生成逼真的数据?
🎯 本轨道内容
| 算法 | 核心思想 | 优势 | 劣势 | 难度 |
|---|---|---|---|---|
| VAE | 编码→潜在空间→解码 | 可解释性,平滑插值 | 生成模糊 | ⭐⭐⭐ |
| GAN | 生成器vs判别器对抗 | 生成清晰 | 训练不稳定 | ⭐⭐⭐ |
| Diffusion | 逐步去噪 | 质量最高,稳定 | 生成慢 | ⭐⭐⭐⭐ |
学习路径: VAE(理解潜在空间) → GAN(理解对抗训练) → Diffusion(理解扩散过程)
🔮 算法一: VAE - 变分自编码器
什么是VAE?
VAE (Variational Auto-Encoder, Kingma & Welling, 2013) 学习数据的压缩表示,并能从这个表示重建数据。
结构:
数据x → Encoder → 潜在变量z ~ N(μ,σ) → Decoder → 重建x'
关键创新: Reparameterization Trick (重参数化技巧)
传统采样: z ~ N(μ,σ) (不可微!)
重参数化: z = μ + σ·ε, 其中 ε ~ N(0,1) (可微!)
对话1: VAE和普通AutoEncoder有什么区别?
🤔 学生: AutoEncoder也是Encoder+Decoder,VAE的”变分”体现在哪里?
💡 导师: 关键区别: VAE的潜在空间是概率分布,不是确定的点!
普通AutoEncoder:
数据x → Encoder → z(确定值) → Decoder → x'
Loss = ||x - x'||² (重建误差)
问题:
1. 潜在空间不连续 — 两个点z1,z2之间可能是"垃圾"
2. 不能生成 — 随机采样z可能解码出无意义图像
VAE:
数据x → Encoder → μ,σ → z=μ+σ·ε → Decoder → x'
Loss = 重建误差 + KL(q(z|x) || p(z))
强制:
1. q(z|x)接近标准正态分布N(0,I)
2. 潜在空间连续、平滑 — 任意采样z都能解码
直观理解:
AutoEncoder学到的潜在空间:
[数字0] → z=0.1
[数字1] → z=0.5
[数字2] → z=2.3 (跳跃!)
采样 z=1.5 → ??? (未定义)
VAE学到的潜在空间:
[数字0] → μ=0.0, σ=0.1
[数字1] → μ=1.0, σ=0.1
[数字2] → μ=2.0, σ=0.1
采样 z=1.5 → 类似0和1的混合体 (有效!)
KL散度的作用:
KL(q(z|x) || N(0,I)) 惩罚:
- μ离0太远 (中心化)
- σ过大或过小 (标准化)
结果: 所有数据的z分布重叠在N(0,I)附近
→ 平滑的潜在空间
✨ 关键洞察: VAE用KL散度”强制”潜在空间规整——代价是重建质量稍差,收益是可以随意采样生成新数据。
对话2: 重参数化技巧为什么重要?
🤔 学生: 为什么 z=μ+σ·ε 就能让采样可微?
💡 导师: 因为把随机性分离到ε,梯度只流经μ和σ!
问题: 直接采样z~N(μ,σ)不可微
# 不可微的采样
z = sample_from_normal(μ, σ)
# 反向传播时:
∂Loss/∂μ = ??? (采样操作没有梯度!)解决: 重参数化
# 可微的采样
ε = sample_from_normal(0, 1) # 固定分布,不依赖参数
z = μ + σ * ε
# 反向传播:
∂z/∂μ = 1
∂z/∂σ = ε
∂Loss/∂μ = ∂Loss/∂z · ∂z/∂μ = ∂Loss/∂z (梯度流通!)
∂Loss/∂σ = ∂Loss/∂z · ε图示:
直接采样(不可微):
μ,σ → [采样黑盒] → z → Decoder → x' → Loss
↑
梯度断裂!
重参数化(可微):
μ,σ → [μ + σ·ε] ← ε ~ N(0,1) (外部噪声)
↓
z → Decoder → x' → Loss
↑
梯度流通!
数学证明:
期望梯度等价性:
E[∂Loss/∂μ | z~N(μ,σ)] = E[∂Loss/∂μ | z=μ+σ·ε, ε~N(0,1)]
但后者可以用链式法则计算!
✨ 关键洞察: 重参数化是个”梯度路由技巧”——把不可微操作(随机采样)移到计算图外,只留可微操作(加法、乘法)在图内。
代码片段: VAE核心
class VAE:
def __init__(self, input_dim=784, latent_dim=20, hidden_dim=400):
# Encoder
self.encoder_fc1 = nn.Linear(input_dim, hidden_dim)
self.encoder_mu = nn.Linear(hidden_dim, latent_dim) # 均值头
self.encoder_logvar = nn.Linear(hidden_dim, latent_dim) # log方差头
# Decoder
self.decoder_fc1 = nn.Linear(latent_dim, hidden_dim)
self.decoder_fc2 = nn.Linear(hidden_dim, input_dim)
def encode(self, x):
"""编码器: x → μ, log(σ²)"""
h = F.relu(self.encoder_fc1(x))
mu = self.encoder_mu(h)
logvar = self.encoder_logvar(h) # 用log(σ²)而不是σ,确保正数
return mu, logvar
def reparameterize(self, mu, logvar):
"""重参数化技巧: z = μ + σ·ε"""
std = torch.exp(0.5 * logvar) # σ = sqrt(σ²) = exp(0.5·log(σ²))
eps = torch.randn_like(std) # ε ~ N(0,1)
z = mu + std * eps # z ~ N(μ,σ)
return z
def decode(self, z):
"""解码器: z → x'"""
h = F.relu(self.decoder_fc1(z))
x_recon = torch.sigmoid(self.decoder_fc2(h)) # [0,1]像素
return x_recon
def forward(self, x):
mu, logvar = self.encode(x)
z = self.reparameterize(mu, logvar)
x_recon = self.decode(z)
return x_recon, mu, logvar
def vae_loss(x, x_recon, mu, logvar):
"""VAE损失 = 重建误差 + KL散度"""
# 重建误差(Binary Cross-Entropy for pixel values)
recon_loss = F.binary_cross_entropy(x_recon, x, reduction='sum')
# KL散度: KL(N(μ,σ) || N(0,1))
# 解析解: -0.5 * sum(1 + log(σ²) - μ² - σ²)
kl_loss = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
return recon_loss + kl_loss
# 训练循环
for epoch in range(epochs):
for batch_x in dataloader:
x_recon, mu, logvar = vae(batch_x)
loss = vae_loss(batch_x, x_recon, mu, logvar)
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 生成新样本
z = torch.randn(100, latent_dim) # 从N(0,1)采样
x_generated = vae.decode(z)关键点:
- 两个输出头: μ和logvar(不是直接σ)
- KL散度解析解: 避免采样估计,训练稳定
- 生成: 直接从N(0,I)采样z,解码即可
⚔️ 算法二: GAN - 生成对抗网络
什么是GAN?
GAN (Generative Adversarial Network, Goodfellow et al., 2014) 通过对抗博弈训练生成器。
两个玩家:
- 生成器G: 噪声z → 假数据x_fake (骗过判别器)
- 判别器D: 真数据/假数据 → 真假概率 (识破生成器)
Minimax博弈:
min_G max_D E[log D(x_real)] + E[log(1 - D(G(z)))]
翻译: G想让D(G(z))→1(假数据被当真),D想让D(x_real)→1,D(G(z))→0
对话3: GAN的训练为什么不稳定?
🤔 学生: GAN听起来很聪明,为什么说训练困难?
💡 导师: 因为两个网络相互追赶,容易失衡!
问题1: Mode Collapse (模式坍缩)
数据分布有10种模式(比如10个数字)
生成器发现: 生成"7"总能骗过判别器
策略: 只生成"7" → 100%骗过D!
结果: 多样性丢失,只会生成单一模式
为什么D不抵抗?
- 当G只生成”7”,D学会识破”7” → 惩罚G
- G切换到生成”3” → 骗过D → D学会识破”3”
- G又切换到”7”…
- 循环振荡,永远不收敛到生成全部模式
问题2: 梯度消失
判别器太强 → D(G(z)) ≈ 0(完全识破假数据)
生成器损失: -log D(G(z)) ≈ -log(0) = +∞
但梯度: ∂(-log D(G(z)))/∂G ≈ 0 (D饱和,G收不到信号!)
G停止学习,训练崩溃。
问题3: 非收敛动力学
标准优化: Loss(θ) 有全局最小值 → 梯度下降收敛
GAN "优化": 两个玩家互相博弈 → Nash均衡难达到
类比: 两人石头剪刀布
- 一方总出石头 → 另一方出布
- 发现后改出剪刀 → 对方改石头
- 无穷循环!
解决方案:
- Non-saturating GAN: 改G的损失为 -log D(G(z)) → log D(G(z)) (反转)
- Wasserstein GAN: 用Earth Mover距离替代JS散度
- Spectral Normalization: 约束D的Lipschitz常数
- Tricks: label smoothing, feature matching, minibatch discrimination
- StyleGAN: 渐进式训练 + 自适应实例归一化
✨ 关键洞察: GAN是个”动态博弈”而非”静态优化”——稳定性来自精心设计的损失函数和训练技巧,不是算法本身。
对话4: GAN vs VAE,什么时候用谁?
🤔 学生: GAN生成更清晰,为什么还要用VAE?
💡 导师: 因为它们优化目标不同!
| 维度 | VAE | GAN |
|---|---|---|
| 优化目标 | 最大化似然P(x) | 最小化真假分布距离 |
| 生成质量 | 模糊(Gaussian假设) | 清晰(对抗逼真) |
| 训练稳定性 | 稳定(凸优化) | 不稳定(博弈) |
| 多样性 | 高(覆盖所有模式) | 低(易mode collapse) |
| 潜在空间 | 有(Encoder) | 无(单向生成) |
| 生成速度 | 快(1次前向) | 快(1次前向) |
| 应用 | 异常检测、插值 | 图像合成、超分辨率 |
VAE适合:
- 需要潜在空间(数据压缩、特征提取)
- 需要稳定训练(工业界)
- 需要密度估计(P(x)有意义)
GAN适合:
- 追求生成质量(艺术创作、超分)
- 不需要Encoder(只生成,不编码)
- 有足够资源调参(学术研究)
最新趋势: 混合模型
- VAE-GAN: VAE的Decoder加GAN判别器 → 清晰度提升
- α-GAN: GAN的生成器加VAE的Encoder → 潜在空间可用
✨ 关键洞察: VAE和GAN是”不同哲学”——VAE是贝叶斯推断(最大化似然),GAN是博弈论(极小化极大)。没有绝对优劣,只有任务适配。
代码片段: GAN核心
class Generator(nn.Module):
def __init__(self, noise_dim=100, output_dim=784):
super().__init__()
self.fc = nn.Sequential(
nn.Linear(noise_dim, 256),
nn.ReLU(),
nn.Linear(256, 512),
nn.ReLU(),
nn.Linear(512, output_dim),
nn.Tanh() # 输出[-1,1]
)
def forward(self, z):
return self.fc(z)
class Discriminator(nn.Module):
def __init__(self, input_dim=784):
super().__init__()
self.fc = nn.Sequential(
nn.Linear(input_dim, 512),
nn.LeakyReLU(0.2),
nn.Linear(512, 256),
nn.LeakyReLU(0.2),
nn.Linear(256, 1),
nn.Sigmoid() # 输出[0,1]概率
)
def forward(self, x):
return self.fc(x)
# 训练循环
G = Generator()
D = Discriminator()
opt_G = torch.optim.Adam(G.parameters(), lr=0.0002, betas=(0.5, 0.999))
opt_D = torch.optim.Adam(D.parameters(), lr=0.0002, betas=(0.5, 0.999))
for epoch in range(epochs):
for real_data in dataloader:
batch_size = real_data.size(0)
# === 训练判别器D ===
# 真数据
real_labels = torch.ones(batch_size, 1)
real_pred = D(real_data)
d_loss_real = F.binary_cross_entropy(real_pred, real_labels)
# 假数据
z = torch.randn(batch_size, noise_dim)
fake_data = G(z).detach() # 不更新G!
fake_labels = torch.zeros(batch_size, 1)
fake_pred = D(fake_data)
d_loss_fake = F.binary_cross_entropy(fake_pred, fake_labels)
# D总损失
d_loss = d_loss_real + d_loss_fake
opt_D.zero_grad()
d_loss.backward()
opt_D.step()
# === 训练生成器G ===
z = torch.randn(batch_size, noise_dim)
fake_data = G(z)
fake_pred = D(fake_data) # 不detach,G要通过D反向传播!
# G的目标: 让D(G(z))→1 (骗过D)
g_loss = F.binary_cross_entropy(fake_pred, real_labels)
opt_G.zero_grad()
g_loss.backward()
opt_G.step()
# 生成新样本
z = torch.randn(64, noise_dim)
fake_images = G(z)关键点:
- 交替训练: 每步先更新D,再更新G
- detach: 训练D时,G不更新(梯度阻断)
- 标签反转: G训练时用real_labels(骗D)
🌊 算法三: Diffusion - 扩散模型
什么是Diffusion?
Diffusion Model (DDPM, Ho et al., 2020) 通过逐步去噪生成数据,是Stable Diffusion、DALL-E 2的核心。
核心思想:
- 前向过程(固定): 逐步加噪声 x → x₁ → … → xₜ ~ N(0,I)
- 反向过程(学习): 逐步去噪 xₜ → … → x₁ → x₀ ≈ 数据分布
数学:
前向: q(x_t | x_0) = N(x_t; √ᾱ_t·x_0, (1-ᾱ_t)·I)
ᾱ_t = ∏(1-β_i), β_t是噪声schedule
反向: p_θ(x_{t-1} | x_t) ≈ N(x_{t-1}; μ_θ(x_t,t), Σ_θ(x_t,t))
训练ε_θ预测噪声: L = ||ε - ε_θ(x_t, t)||²
对话5: Diffusion为什么生成质量高?
🤔 学生: Diffusion慢这么多,为什么还比GAN好?
💡 导师: 因为分解了难问题!
GAN的难点: 一步到位
噪声z → 图像x (巨大跳跃,判别器容易崩溃)
Diffusion的策略: 分T步
纯噪声 → 稍清晰 → 更清晰 → ... → 完美图像
每步只需: "去除一点噪声" (简单!)
类比:
- GAN: 一笔画完整幅画 (难,容易出错)
- Diffusion: 素描→上色→细节→完成 (每步简单,累积精细)
训练稳定性:
GAN:
Loss = min_G max_D ... (博弈,振荡)
Diffusion:
Loss = E[||ε - ε_θ(x_t,t)||²] (标准回归,凸优化!)
模式覆盖:
GAN: 容易mode collapse,丢失多样性
Diffusion: T步渐进,每步只调整小幅 → 自然覆盖所有模式
可控性:
Classifier Guidance: p(x_t | y) ∝ p(x_t) · p(y | x_t)
用分类器p(y|x_t)引导去噪方向 → 可控生成!
GAN: 潜在空间难解释,可控性差
缺点: 慢!
- GAN/VAE: 1次前向 → 图像
- Diffusion: 100-1000次前向 → 图像 (每步去一点噪)
加速方案:
- DDIM: 确定性采样,跳步(1000步→50步)
- Latent Diffusion (Stable Diffusion): 在VAE的潜在空间做扩散 → 8x加速
- 一致性模型: 单步生成,保持质量
✨ 关键洞察: Diffusion是”时间换质量”——用更多步数换更高质量和稳定性。本质是把”生成”分解为”逐步精炼”。
对话6: Diffusion的噪声schedule怎么设计?
🤔 学生: 为什么要设计β_t的schedule,直接均匀加噪不行吗?
💡 导师: 因为前期和后期需要不同噪声强度!
线性Schedule(最简单):
β_t = β_start + (β_end - β_start) · t/T
例如: β_1=0.0001, β_T=0.02
问题: 中间步噪声过大,信息丢失太快
Cosine Schedule (更好):
ᾱ_t = cos²((t/T + s) / (1+s) · π/2)
s是小偏移(防止t=0时ᾱ=1)
特点:
- 前期(t小): ᾱ接近1,慢慢加噪
- 中期: 平滑过渡
- 后期(t→T): ᾱ→0,完全噪声
直觉:
图像信息 vs 噪声:
t=0: 100%信号, 0%噪声 (原图)
t=T/4: 80%信号, 20%噪声 (稍模糊)
t=T/2: 40%信号, 60%噪声 (很模糊)
t=T: 0%信号, 100%噪声 (纯噪声)
Cosine schedule: 前期保护信息,后期快速破坏
Linear schedule: 匀速破坏,中期信息丢失过快
实验结果:
| Schedule | FID (越低越好) |
|---|---|
| Linear | 12.5 |
| Cosine | 8.2 |
| Quadratic | 10.1 |
✨ 关键洞察: 噪声schedule是个”信息破坏策略”——前期温和(保留结构),后期激进(彻底随机化)。
代码片段: DDPM核心
def noise_schedule(T, schedule_type='linear'):
"""计算噪声schedule"""
if schedule_type == 'linear':
betas = torch.linspace(0.0001, 0.02, T)
elif schedule_type == 'cosine':
s = 0.008
steps = torch.arange(T + 1, dtype=torch.float64) / T
alphas_cumprod = torch.cos((steps + s) / (1 + s) * torch.pi * 0.5) ** 2
alphas_cumprod = alphas_cumprod / alphas_cumprod[0]
betas = 1 - (alphas_cumprod[1:] / alphas_cumprod[:-1])
betas = torch.clip(betas, 0.0001, 0.9999)
alphas = 1 - betas
alphas_cumprod = torch.cumprod(alphas, dim=0)
return betas, alphas_cumprod
class DiffusionModel:
def __init__(self, noise_model, T=1000):
self.model = noise_model # ε_θ(x_t, t)
self.T = T
self.betas, self.alphas_cumprod = noise_schedule(T)
def q_sample(self, x_0, t, noise=None):
"""前向加噪: x_0 → x_t"""
if noise is None:
noise = torch.randn_like(x_0)
sqrt_alphas_cumprod_t = torch.sqrt(self.alphas_cumprod[t])
sqrt_one_minus_alphas_cumprod_t = torch.sqrt(1 - self.alphas_cumprod[t])
# x_t = √ᾱ_t · x_0 + √(1-ᾱ_t) · ε
return sqrt_alphas_cumprod_t * x_0 + sqrt_one_minus_alphas_cumprod_t * noise
def train_step(self, x_0):
"""训练: 预测噪声"""
# 随机采样时间步
t = torch.randint(0, self.T, (x_0.shape[0],))
# 加噪
noise = torch.randn_like(x_0)
x_t = self.q_sample(x_0, t, noise)
# 预测噪声
pred_noise = self.model(x_t, t)
# 损失
loss = F.mse_loss(pred_noise, noise)
return loss
@torch.no_grad()
def p_sample(self, x_t, t):
"""单步去噪: x_t → x_{t-1}"""
# 预测噪声
pred_noise = self.model(x_t, t)
# 计算均值
alpha_t = 1 - self.betas[t]
alpha_cumprod_t = self.alphas_cumprod[t]
# μ_θ = (1/√α_t) · (x_t - (β_t/√(1-ᾱ_t))·ε_θ)
mean = (x_t - (self.betas[t] / torch.sqrt(1 - alpha_cumprod_t)) * pred_noise) / torch.sqrt(alpha_t)
if t > 0:
noise = torch.randn_like(x_t)
variance = self.betas[t]
x_t_minus_1 = mean + torch.sqrt(variance) * noise
else:
x_t_minus_1 = mean
return x_t_minus_1
@torch.no_grad()
def sample(self, shape):
"""完整采样: x_T ~ N(0,I) → x_0"""
x_t = torch.randn(shape)
for t in reversed(range(self.T)):
x_t = self.p_sample(x_t, t)
return x_t
# 训练
diffusion = DiffusionModel(UNet(), T=1000)
for epoch in range(epochs):
for x_0 in dataloader:
loss = diffusion.train_step(x_0)
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 生成
generated = diffusion.sample(shape=(64, 3, 32, 32))关键点:
- 训练: 随机t,加噪,预测噪声
- 采样: 从T倒推到0,逐步去噪
- 重参数化: x_{t-1} = μ + √β · ε
📊 三大生成模型对比
| 维度 | VAE | GAN | Diffusion |
|---|---|---|---|
| 生成质量 | ⭐⭐ (模糊) | ⭐⭐⭐⭐ (清晰) | ⭐⭐⭐⭐⭐ (最高) |
| 训练稳定性 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| 多样性 | ⭐⭐⭐⭐ | ⭐⭐ (mode collapse) | ⭐⭐⭐⭐⭐ |
| 生成速度 | ⭐⭐⭐⭐⭐ (1步) | ⭐⭐⭐⭐⭐ (1步) | ⭐ (100-1000步) |
| 潜在空间 | ✅ 有Encoder | ❌ 无 | ❌ 无 |
| 可控性 | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ (Guidance) |
| 数学基础 | 变分推断 | 博弈论 | 非平衡热力学 |
应用场景:
- VAE: 异常检测、数据压缩、插值
- GAN: 图像超分辨率、风格迁移、人脸生成
- Diffusion: Text-to-Image(Stable Diffusion)、视频生成、音频合成
🎯 本章总结
生成模型的核心权衡:质量 vs 速度 vs 稳定性
演化路径:
VAE (2013) → 平滑潜在空间,但模糊
↓
GAN (2014) → 清晰生成,但不稳定
↓
Diffusion (2020) → 质量+稳定,但慢
↓
Latent Diffusion (2022) → 在VAE潜在空间做Diffusion → Stable Diffusion!
混合趋势:
- VAE-GAN: 结合潜在空间+清晰度
- Latent Diffusion: VAE压缩+Diffusion生成
- 一致性模型: Diffusion的蒸馏→单步生成
核心原则:
- No Free Lunch: 质量、速度、稳定性不可兼得
- 任务适配: 实时应用(GAN)vs 离线渲染(Diffusion)
- 持续演化: 新架构不断出现(Transformer+Diffusion→DiT)
下一站: 06-智能体轨道 - 探索如何让AI自主规划和决策!