前面几天的模型大多在做判别(discriminative):给一张图判断猫狗,给一句话预测下一个 token。今天换一个根本不同的问题——生成(generative):不是判断现有数据,而是凭空造出从未存在、却像真的的新样本,这是 Stable Diffusion 画图、Sora 生成视频背后的核心。我们走一遍 2014–2022 的进化线:GAN → VAE → Diffusion → Flow Matching,每一步都是对前一步某个具体缺陷的精确回应。理解它,你会看清「图像生成」不是魔法,而是四种「如何把随机噪声搬运成真实数据」的工程答案。
GAN 是造假者 vs 验钞机的军备竞赛。更贴你的后端世界:像一个 fuzzer(生成器) 不断造出畸形输入,去骗过一套 断言检查器(判别器);检查器每被骗一次就升级规则,fuzzer 又被迫造得更逼真。两者互相施压、协同进化,最后 fuzzer 造出的「假数据」逼真到检查器只能瞎猜——此时生成器就学会了真实数据的分布。
生成的根本难题:真实数据(如所有「人脸」)的概率分布 p(x) 极其复杂,显式写不出公式。GAN(Goodfellow et al. 2014)的天才之处是绕过它——不直接建模 p(x),而是训练两个网络对抗:
关键洞察:判别器本身就是一个「可学习的 loss 函数」。手工设计的 loss(如像素级 MSE)会逼模型生成模糊的平均脸;而 D 会随 G 进步不断变聪明,提供一个永远「恰到好处」的训练信号。两者玩一个 minimax 博弈(极小极大):
那个 minimax 目标函数写出来是:
minG maxD E[log D(x)] + E[log(1 − D(G(z)))]
拆开看:第一项 D 想让 D(x) 趋近 1(真图判真);第二项 D 想让 D(G(z)) 趋近 0(假图判假),而 G 想反过来让它趋近 1(骗成功)。两者抢同一个式子的最大/最小——这就是「对抗」二字的数学含义。
import torch, torch.nn as nn G = nn.Sequential(nn.Linear(100, 256), nn.ReLU(), nn.Linear(256, 784), nn.Tanh()) D = nn.Sequential(nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 1), nn.Sigmoid()) bce = nn.BCELoss() # 二分类交叉熵:真=1, 假=0 for real in dataloader: # real: 一批真图 (B, 784) z = torch.randn(real.size(0), 100) fake = G(z) # ① 训判别器:真图判真、假图判假(fake.detach 切断 G 的梯度) loss_D = bce(D(real), ones) + bce(D(fake.detach()), zeros) opt_D.zero_grad(); loss_D.backward(); opt_D.step() # ② 训生成器:让 D 把假图判成真(标签故意填 1) loss_G = bce(D(fake), ones) opt_G.zero_grad(); loss_G.backward(); opt_G.step()
VAE 是一个带不确定性的有损 codec。普通 autoencoder 像 JPEG:把图压成一个固定的压缩码(latent code),再解压还原。但 VAE 不把图编码成一个点,而是编码成一小团概率云(一个高斯分布)——类似数据库里把记录存成「带误差棒的索引」而非精确坐标。这一改让整个隐空间连续、无空洞,随手在里面采样一个点解码,就能得到一张全新的合理图像。
普通 autoencoder 能压缩还原,但不能生成:它的隐空间「布满洞」,你在两个已知点之间随便采一个,解码出来往往是噪声垃圾——因为它从没被要求让隐空间「填满」。VAE(Kingma & Welling 2013)的解法是强制隐变量服从标准正态分布,靠两个力量拉扯达成:
两者之和就是著名的 ELBO(证据下界)——最大化它等价于最大化数据似然。但这里有个工程障碍:「采样」这个操作不可微,梯度没法穿过随机节点回传到编码器。VAE 的杀手锏是 重参数化技巧(reparameterization trick):
直觉:随机性是「生成多样性」的来源,却挡住了梯度。重参数化把随机部分(ε)抽离成外部输入,剩下 μ、σ 全在可微主干上——既保留随机性,又让梯度畅通。这个技巧后来在强化学习、扩散模型里反复出现。
import torch, torch.nn.functional as F def vae_step(x, encoder, decoder): mu, logvar = encoder(x).chunk(2, dim=-1) # 编码器同时吐 μ 和 log(σ²) # 重参数化:z = μ + σ·ε,σ = exp(½·logvar) std = torch.exp(0.5 * logvar) eps = torch.randn_like(std) # ε ~ N(0,1),随机性外置 z = mu + std * eps x_hat = decoder(z) # ELBO = 重构损失 + KL 散度(让隐分布贴近 N(0,1)) recon = F.mse_loss(x_hat, x, reduction="sum") kl = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp()) return recon + kl # 最小化它 = 最大化 ELBO # 生成新样本:z = torch.randn(...); decoder(z) —— 无需编码器
扩散模型把「一步生成」这个难问题,拆成几百步微小的去噪修正——和分布式系统里「把一次大事务拆成幂等的小步骤、逐步收敛到目标状态」是同一种智慧。具体地:前向过程像让一张清晰照片在噪声里慢慢"腐烂",每步加一点高斯噪声,几百步后变成纯雪花点;反向过程训练一个网络把这个腐烂过程一步步倒放,从纯噪声里逐渐"显影"出一张图。
GAN 不稳定、会坍缩;VAE 模糊。扩散模型(Ho, Jain & Abbeel 2020,即 DDPM)用一个朴素到惊艳的思路同时解决两者:把生成拆成大量极简单的子任务。它有两个过程:
最妙的是 loss 被简化到极致——就是个噪声回归:
L = ‖ ε − εθ(x_t, t) ‖²
其中 ε 是前向时真实加进去的噪声(你自己加的,是已知答案),εθ 是网络预测。这就是普通的均方误差监督学习——没有对抗、没有 KL 配平,训练稳如磐石。这是扩散打败 GAN 的根本原因。
import torch, torch.nn.functional as F def diffusion_loss(x0, model, alpha_bars): B = x0.size(0) t = torch.randint(0, T, (B,)) # 每张图随机选一个时间步 t ab = alpha_bars[t].view(B, 1, 1, 1) # ᾱ_t:累积保留比例 noise = torch.randn_like(x0) # ε:真实加进去的噪声(已知答案) # 一步直接生成 x_t(闭式解,不用真跑 t 次) x_t = ab.sqrt() * x0 + (1 - ab).sqrt() * noise pred = model(x_t, t) # 网络预测噪声 return F.mse_loss(pred, noise) # 就是噪声回归,简单稳定 # 采样:从 x_T~N(0,1) 出发,循环 T 步,每步减去 model 预测的噪声
如果说扩散是让噪声点做布朗运动式的随机游走慢慢挪到数据,流匹配就是把这条曲折路径拉成一条直线传送带。它学一个「速度场(velocity field)」——空间里每个点都标注「此刻该往哪个方向、以多快的速度流动」,就像流体力学里的流场,或者地图导航里每个路口的箭头。沿着这个场走一条确定的直线(ODE),就能把噪声点精准搬运到数据点——路更直、步数更少。
扩散虽稳但慢且数学绕(涉及随机微分方程 SDE、score 函数)。流匹配(Lipman et al. 2022)把它极度简化:直接学一个向量场 vθ(x, t),把噪声分布「流」成数据分布。训练目标简单到不可思议——给定噪声点 x₀ 和数据点 x₁,连一条直线,那么任意时刻 t 的位置和速度都是闭式的:
又是一个朴素的回归!直觉:网络在学「站在路径任意一点,该朝哪个方向走才能到数据」。生成时从噪声 x₀ 出发,用普通 ODE 求解器沿速度场积分到 t=1 就得一张图——因为路径接近直线,很少几步就能走完。
一个深刻的统一:扩散其实是流匹配的特例(对应一种弯曲高斯路径)。流匹配把生成建模重新表述为「学一个把噪声搬到数据的速度场」,扩散只是其中一种走法——这种视角统一,正是它成为 2024–2025 前沿主流的原因。
import torch, torch.nn.functional as F def flow_matching_loss(x1, model): x0 = torch.randn_like(x1) # 起点:纯噪声 t = torch.rand(x1.size(0), 1) # 路径上随机取一个时刻 t∈[0,1] # 直线插值:x_t 在噪声 x0 与数据 x1 之间 x_t = (1 - t) * x0 + t * x1 target = x1 - x0 # 直线的速度(恒定方向) pred = model(x_t, t) # 网络预测此处的速度场 return F.mse_loss(pred, target) # 又是一个朴素回归 # 采样:x = randn(...); 用 ODE 求解器沿 model 的速度场从 t=0 积分到 t=1