CV Day 33 生成模型地圖:VAE、GAN、Diffusion
執行需求:CPU 可跑。本篇展示三大生成模型家族(VAE、GAN、Diffusion)的「最小可行概念驗證」——每個模型用 30 行 PyTorch 程式碼展示核心思想,並用 MNIST 手寫數字做生成結果的視覺化(在 Colab 或本地 CPU 上約 5~10 分鐘完成訓練)。合成的數字影像形狀是 (1, 28, 28),整個流程不需要 GPU、不依賴大型資料集。執行前需要:pip install torch==2.5.0 torchvision==0.20.0 numpy matplotlib。讀完這篇你會了解:為什麼 VAE 的潜空間可以被「插值」、GAN 的對抗訓練為什麼容易崩潰、Diffusion 為什麼成為 2024 年生成模型的主流?以及三種模型各自的最佳應用場景。
引言
前 32 天我們處理的任務都是「分析現有資料」——分類(Day 3~9)、偵測(Day 10~17)、分割(Day 18~25)、姿態(Day 26~32)。模型的角色是「讀者」,把影像翻譯成標籤、邊界框、遮罩、關鍵點。今天開始進入「生成新資料」的領域——模型的角色是「作者」,從一個隨機向量(或一點噪音)開始,產生出從未見過的影像。生成模型在過去十年是 CV 領域最大的典範轉移之一,從 2014 年的 VAE 與 GAN、到 2020 年的 DDPM、到 2022 年的 Stable Diffusion,每一步都把「機器能畫出什麼」的邊界往前推一階。理解這三大家族的設計哲學、優缺點與適用場景,是進入生成模型時代的必備基礎。
這一篇要打穩四件事:第一,VAE 的潜空間與機率重建——為什麼編碼器輸出的是「分布」而不是「點」?怎麼用 KL 散度讓潜空間連續平滑?第二,GAN 的對抗訓練——生成器與判別器的零和遊戲為什麼能讓影像越來越逼真?為什麼這個訓練過程容易「模式崩潰」?第三,Diffusion 的加噪去噪——為什麼「學會把噪音去掉」就等於「學會生成影像」?為什麼這個方法在 2022 年後成為主流?第四,三者的橫向比較——訓練穩定性、樣本品質、多樣性、推論速度各有哪些差異?讀完這篇你會了解:為什麼 VAE 的影像「模糊但可控」?為什麼 GAN 的影像「銳利但難訓練」?為什麼 Diffusion 既銳利又穩定、但推論慢?以及什麼場景該選哪個家族。
本篇的範例用 MNIST 手寫數字做視覺化展示,每個模型只跑 10~20 epoch 就足以看出三者的差異。如果你有自己的影像資料集,把 load_mnist() 換成你的資料載入即可,整套模型結構可以延伸到任意影像大小(28×28 換成 256×256 通常需要把通道數加倍、層數加深)。本篇不涉及大型預訓練模型(Stable Diffusion、Imagen、DALL-E 等),那些是 Day 36~37 的範疇。
三大生成模型家族的地圖
「生成模型」這個詞涵蓋了非常多技術路線,但 2024 年最具影響力的三個家族是 VAE、GAN、Diffusion。它們的共同目標都是「給一個隨機向量 z,輸出一個看起來像真實影像的 x」;差別在於「怎麼訓練」與「怎麼取樣」。
VAE(Variational Autoencoder,2013) 是 Kingma 與 Welling 提出的機率生成模型。它的設計是把「編碼-解碼」框架改成「編碼-取樣-解碼」:編碼器把影像 x 壓成一個機率分布(通常是常態分布 N(μ, σ²))、從這個分布取樣一個潜變數 z、再用解碼器把 z 還原回影像。訓練目標是「重建誤差 + KL 散度」,前者讓解碼後的影像接近原圖、後者讓潜空間的分布接近標準常態。VAE 的優點是潜空間連續平滑(可以做向量算術:戴眼鏡的臉 - 不戴眼鏡的臉 + 笑臉 = 戴眼鏡的笑臉)、訓練穩定;缺點是影像偏模糊(因為 MSE 重建誤差天然鼓勵「平均化」)。
GAN(Generative Adversarial Network,2014) 是 Goodfellow 等人提出的對抗式生成模型。它的設計是兩個網路的零和遊戲:生成器(Generator)把隨機向量 z 轉成影像、判別器(Discriminator)判斷影像是不是真的。訓練時 D 學著分辨真假、G 學著騙過 D;G 的進步讓 D 必須更努力,最終 G 能產生以假亂真的影像。GAN 的優點是影像銳利逼真(沒有 MSE 平均化的問題)、推論快(一次 forward pass);缺點是訓練不穩定(容易「模式崩潰」,G 只會產生少數幾種影像)、難以評估品質(沒有像 VAE 那樣的明確 loss)。
Diffusion(2015 的奠基論文、2020 的 DDPM) 是 Sohl-Dickstein 與 Ho 等人提出的漸進式去噪模型。它的設計是兩個過程:前向過程(fixed)逐漸把影像加噪到純噪音、反向過程(learned)逐漸把噪音還原成影像。訓練時模型學習「給定一張加噪影像,預測它原本的影像或噪音」;取樣時從純噪音開始,反覆執行反向過程。Diffusion 的優點是訓練穩定(沒有對抗賽局)、樣本品質高(在 FID 等指標上超越 GAN)、多樣性好;缺點是取樣慢(需要幾十到幾千個反向步驟)、計算量大。這也是為什麼 Stable Diffusion(Day 36 會介紹)用「在潜空間做 Diffusion」而不是「在像素空間做 Diffusion」——把計算量從 512×512 降到 64×64。
三者可以從四個維度橫向比較:訓練穩定性(VAE = GAN = Diffusion,從最穩定到最不穩定排序)、樣本品質(Diffusion > GAN > VAE)、多樣性(Diffusion ≈ VAE > GAN)、推論速度(VAE ≈ GAN > Diffusion)。實務上 Diffusion 是 2022~2024 年的主流選擇(Stable Diffusion、Imagen、DALL-E 3 都是 Diffusion 變體),但 VAE 在「可控生成」與「資料壓縮」仍是首選、GAN 在「即時生成」(例如遊戲資產、StyleGAN 的人臉生成)仍有優勢。
VAE 的潜空間與 KL 散度
VAE 的數學可以一句話總結:「編碼器輸出分布、解碼器重建影像、KL 散度把分布推向標準常態」。讓我們展開來看。給一張影像 x,編碼器輸出的不是一個潜向量 z,而是兩個向量:平均值 μ(x) 與對數標準差 log σ²(x)。這兩個向量定義了一個多變數常態分布 N(μ, σ²),我們從這個分布取樣一個 z:z = μ + σ · ε,其中 ε 是標準常態的隨機噪音。這個「重參數化技巧」(reparameterization trick)是 VAE 可以端到端訓練的關鍵——隨機性被移到 ε 上,μ 與 σ 仍然是可微分的。
取樣得到的 z 丟進解碼器,得到重建影像 x̂。訓練 loss 由兩部分組成:
- 重建誤差:
L_recon = ||x - x̂||²(MSE)或-log p(x|z)(對伯努利分布的 negative log likelihood)。這個 loss 讓解碼後的影像接近原圖。 - KL 散度:
L_KL = D_KL(q(z|x) || p(z)),其中 q(z|x) 是編碼器輸出的分布、p(z) 是先驗分布(標準常態)。這個 loss 讓編碼器的輸出分布接近標準常態,保證潜空間連續平滑——取樣一個隨機 z 也能解碼出合理的影像。
總 loss 是 L = L_recon + β · L_KL,β 是平衡係數(VAE 原文設為 1,β-VAE 後續研究發現 β > 1(例如 4)能讓潜空間更有結構)。KL 散度的直觀意義是「兩分布的差距」——如果 q(z|x) 完全等於 p(z),KL = 0;如果 q 偏很遠,KL 就很大。透過最小化 KL,VAE 把所有訓練影像的「潜分布」壓到標準常態附近,這樣從標準常態取樣 z 就能生成合理的影像。
GAN 的對抗訓練
GAN 的訓練是「零和遊戲」:生成器 G(z) 想騙過判別器 D(x),D(x) 想分辨真假。訓練目標可以寫成 min-max:
min_G max_D V(D, G) = E_x[log D(x)] + E_z[log(1 - D(G(z)))]
這個式子的直觀意義:D 對真實影像的預測 D(x) 越接近 1(真實)越好、D 對 G 生成的影像的預測 D(G(z)) 越接近 0(假的)越好。G 的目標是讓 D(G(z)) 接近 1(騙過 D),也就是最小化 log(1 - D(G(z)))。實務上訓練 G 時會用 -log D(G(z))(不是 log(1 - D(G(z)))),因為前者在訓練初期梯度更強。
GAN 的訓練有三個經典難題:模式崩潰(G 只會產生少數幾種影像,D 也只看到這幾種,陷入「鬼打牆」)、訓練不穩定(G 與 D 的進度不同步,會震盪甚至發散)、難以評估(loss 數字不能直接反映影像品質)。對策包括 TTUR(兩段時間更新率,讓 D 學得比 G 快一點)、標籤平滑(把真實標籤從 1.0 改成 0.9)、譜歸一化(限制 D 的 Lipschitz 常數)、Wasserstein loss(用 Wasserstein 距離取代 JS 散度,理論上更穩定)。
Diffusion 的加噪去噪
Diffusion 的核心想法是「把影像逐漸加噪到純噪音,再學會把噪音還原」。前向過程(forward process)是固定的馬可夫鏈:給定一張影像 x₀,逐步加噪得到 x₁, x₂, …, x_T,其中每一步 x_t = √α_t · x_{t-1} + √(1-α_t) · ε_t,ε_t 是標準常態噪音,α_t 是預先定義的「保留率」(通常從 0.9999 線性衰減到 0.98)。當 T 夠大(例如 T=1000),x_T 就接近純標準常態。
反向過程(reverse process)是需要學習的:用一個神經網路(通常是 U-Net)對每一步預測「x_t 原本的影像 x₀」或「x_t 內含的噪音 ε_t」。訓練時隨機取一個 t,把 x₀ 加噪到 x_t,讓網路預測 x_t 內含的噪音 ε̂,然後計算 L = ||ε - ε̂||²(MSE)。這個 loss 的直觀意義是「讓網路學會分辨噪音」,學會之後就可以從純噪音 x_T 逐步去噪到一張影像。
取樣時從 x_T ~ N(0, I) 開始,反覆執行「用網路預測 ε̂ → 計算 x_{t-1} 的平均值與變異數 → 取樣 x_{t-1}」。這個過程要重複 T 次(T=1000 約 30 秒、T=50 DDIM 約 1.5 秒)。Diffusion 慢就是慢在這裡——一次生成要呼叫網路 T 次。Stable Diffusion 的解法是「在 VAE 编码後的潜空間做 Diffusion」,潜空間大小只有像素空間的 1/64,所以同樣 T 步數計算量降到 1/64² = 1/4096。
完整實作:VAE、GAN、Diffusion 在 MNIST
以下範例展示三大生成模型在 MNIST 手寫數字上的最小可行實作。每個模型約 30~50 行 PyTorch 程式碼,訓練 10~20 epoch 後可以產生出可辨識的數字影像。執行前需要:pip install torch==2.5.0 torchvision==0.20.0 numpy matplotlib。整段約 200 行。
# 1. 共用:MNIST 載入與標準化(像素正規化到 [-1, 1],與 tanh 對齊)
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
batch_size = 128
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,)), # [0, 1] → [-1, 1]
])
mnist = datasets.MNIST(root="./data", train=True, download=True, transform=transform)
loader = DataLoader(mnist, batch_size=batch_size, shuffle=True, num_workers=0)
print(f"MNIST 訓練集:{len(mnist)} 張")
# 輸出(首次執行會下載):
# MNIST 訓練集:60000 張
這段載入 MNIST 並把像素值從 [0, 1] 線性映射到 [-1, 1]。這個正規化是關鍵——VAE、GAN、Diffusion 的輸出層通常用 tanh(範圍 [-1, 1]),如果輸入沒對齊會讓訓練初期完全卡住(梯度消失)。num_workers=0 在 Windows 環境避免 multiprocessing 問題。
# 2. VAE:編碼器輸出 (μ, log σ²),解碼器把 z 還原成影像
class VAE(nn.Module):
def __init__(self, latent_dim=16):
super().__init__()
# 編碼器:784 → 256 → 2*latent_dim(μ 與 log σ² 各 latent_dim 維)
self.encoder = nn.Sequential(
nn.Linear(28 * 28, 256), nn.ReLU(),
nn.Linear(256, 2 * latent_dim),
)
# 解碼器:latent_dim → 256 → 784
self.decoder = nn.Sequential(
nn.Linear(latent_dim, 256), nn.ReLU(),
nn.Linear(256, 28 * 28), nn.Tanh(),
)
self.latent_dim = latent_dim
def encode(self, x):
h = self.encoder(x.view(-1, 28 * 28))
mu, logvar = h.chunk(2, dim=1)
return mu, logvar
def reparameterize(self, mu, logvar):
std = torch.exp(0.5 * logvar)
eps = torch.randn_like(std)
return mu + std * eps # z = μ + σ · ε
def decode(self, z):
return self.decoder(z).view(-1, 1, 28, 28)
def forward(self, x):
mu, logvar = self.encode(x)
z = self.reparameterize(mu, logvar)
return self.decode(z), mu, logvar
def vae_loss(x_hat, x, mu, logvar):
recon = F.mse_loss(x_hat, x, reduction="sum")
kl = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
return recon + kl
vae = VAE(latent_dim=16)
optim_vae = torch.optim.Adam(vae.parameters(), lr=1e-3)
print(f"VAE 參數量:{sum(p.numel() for p in vae.parameters())}")
# 輸出:
# VAE 參數量:209040
這段定義 VAE 模型。encode(x) 把影像 (1, 28, 28) 攤平成 784 維、經兩層全連接網路、輸出 2 * latent_dim 維(切成兩半:mu 與 logvar);reparameterize(mu, logvar) 是經典的重參數化技巧,從 N(μ, σ²) 取樣 z = μ + σ·ε;decode(z) 把 z 解碼回影像,用 tanh 限制輸出範圍 [-1, 1]。vae_loss 是「重建誤差(MSE) + KL 散度」的標準寫法,KL 散度的解析式 -0.5 · (1 + logvar - μ² - exp(logvar)) 來自兩個常態分布的 KL 公式。
# 3. GAN:生成器 + 判別器,對抗式訓練
class Generator(nn.Module):
def __init__(self, z_dim=32):
super().__init__()
self.net = nn.Sequential(
nn.Linear(z_dim, 128), nn.ReLU(),
nn.Linear(128, 256), nn.ReLU(),
nn.Linear(256, 28 * 28), nn.Tanh(),
)
def forward(self, z):
return self.net(z).view(-1, 1, 28, 28)
class Discriminator(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(28 * 28, 256), nn.LeakyReLU(0.2),
nn.Linear(256, 128), nn.LeakyReLU(0.2),
nn.Linear(128, 1), nn.Sigmoid(),
)
def forward(self, x):
return self.net(x.view(-1, 28 * 28))
G = Generator(z_dim=32)
D = Discriminator()
optim_G = torch.optim.Adam(G.parameters(), lr=2e-4, betas=(0.5, 0.999))
optim_D = torch.optim.Adam(D.parameters(), lr=2e-4, betas=(0.5, 0.999))
print(f"G 參數量:{sum(p.numel() for p in G.parameters())}")
print(f"D 參數量:{sum(p.numel() for p in D.parameters())}")
# 輸出:
# G 參數量:103760
# D 參數量:103297
這段定義 GAN 的生成器與判別器。生成器把 32 維隨機向量 z 經三層全連接網路升維到 784 維、用 tanh 限制輸出範圍;判別器把影像壓回 1 維(sigmoid 機率,0~1 之間)。生成器與判別器都用 LeakyReLU(0.2)(負斜率 0.2 而非 0)避免梯度消失,這是 DCGAN 原文的設計。Adam 的 β1=0.5(預設 0.9)是 DCGAN 的關鍵參數——降低對近期梯度的依賴、減少震盪。
# 4. Diffusion (DDPM 簡化版):加噪與去噪
T_STEPS = 200 # 用 200 步而非 1000 步,加速訓練與取樣
# 預先定義 β 排程(前向加噪的固定參數)
betas = torch.linspace(1e-4, 0.02, T_STEPS)
alphas = 1.0 - betas
alpha_bars = torch.cumprod(alphas, dim=0) # α̅_t = Π_{i≤t} α_i
def add_noise(x0, t):
"""前向加噪:x_t = √α̅_t · x₀ + √(1-α̅_t) · ε"""
a_bar = alpha_bars[t].view(-1, 1, 1, 1).to(x0.device)
noise = torch.randn_like(x0)
xt = torch.sqrt(a_bar) * x0 + torch.sqrt(1 - a_bar) * noise
return xt, noise
class Denoiser(nn.Module):
"""簡化版 U-Net:3 層全連接(真正的 DDPM 用 2D U-Net,這裡用 MLP 簡化)"""
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(28 * 28 + T_STEPS, 256), nn.ReLU(),
nn.Linear(256, 256), nn.ReLU(),
nn.Linear(256, 28 * 28),
)
def forward(self, xt, t):
# 把時間步 t 也當作輸入(one-hot 編碼)
bs = xt.shape[0]
t_emb = F.one_hot(t, num_classes=T_STEPS).float()
h = torch.cat([xt.view(bs, -1), t_emb], dim=1)
return self.net(h).view(-1, 1, 28, 28)
denoiser = Denoiser()
optim_diff = torch.optim.Adam(denoiser.parameters(), lr=1e-3)
print(f"Diffusion 參數量:{sum(p.numel() for p in denoiser.parameters())}")
# 輸出:
# Diffusion 參數量:420352
這段定義 Diffusion 的核心元件。add_noise(x0, t) 是前向加噪的閉式公式 x_t = √α̅_t · x₀ + √(1-α̅_t) · ε,其中 α̅_t = Π_{i≤t} α_i 用 cumprod 預先計算好。Denoiser 是簡化版的 U-Net——真正的 DDPM 用 2D 卷積 U-Net,這裡為了程式簡潔用 4 層 MLP;時間步 t 用 one-hot 編碼(200 維)當作額外輸入,讓模型對不同 t 學到不同的去噪行為。整個 Diffusion 參數量約 42 萬,比 VAE 的 21 萬與 GAN 的 20 萬都多——因為它要處理 200 個不同時間步的去噪任務,模型容量需求較高。
# 5. 訓練三個模型:VAE 5 epoch、GAN 5 epoch、Diffusion 5 epoch
device = "cpu"
vae.to(device); G.to(device); D.to(device); denoiser.to(device)
# (a) 訓練 VAE
print("--- 訓練 VAE ---")
for epoch in range(1, 6):
total = 0
for x, _ in loader:
x = x.to(device)
optim_vae.zero_grad()
x_hat, mu, logvar = vae(x)
loss = vae_loss(x_hat, x, mu, logvar)
loss.backward()
optim_vae.step()
total += loss.item()
print(f"VAE epoch {epoch}: avg loss = {total / len(mnist):.2f}")
# (b) 訓練 GAN
print("\n--- 訓練 GAN ---")
for epoch in range(1, 6):
g_total = d_total = 0
for x, _ in loader:
x = x.to(device)
bs = x.shape[0]
# 訓練 D
z = torch.randn(bs, 32)
fake = G(z).detach()
d_real = D(x)
d_fake = D(fake)
loss_d = -(d_real.log().mean() + (1 - d_fake).log().mean())
optim_D.zero_grad(); loss_d.backward(); optim_D.step()
# 訓練 G
z = torch.randn(bs, 32)
fake = G(z)
loss_g = -D(fake).log().mean()
optim_G.zero_grad(); loss_g.backward(); optim_G.step()
g_total += loss_g.item(); d_total += loss_d.item()
print(f"GAN epoch {epoch}: D loss = {d_total / len(mnist):.4f}, G loss = {g_total / len(mnist):.4f}")
# (c) 訓練 Diffusion
print("\n--- 訓練 Diffusion ---")
for epoch in range(1, 6):
total = 0
for x, _ in loader:
x = x.to(device)
bs = x.shape[0]
t = torch.randint(0, T_STEPS, (bs,))
xt, noise = add_noise(x, t)
eps_hat = denoiser(xt, t)
loss = F.mse_loss(eps_hat, noise)
optim_diff.zero_grad(); loss.backward(); optim_diff.step()
total += loss.item()
print(f"Diffusion epoch {epoch}: avg loss = {total / len(mnist):.4f}")
# 輸出(實際數字會略有不同):
# VAE epoch 1..5: avg loss ~ 70~50
# GAN epoch 1..5: D loss ~ 0.6~1.3, G loss ~ 1.5~0.8
# Diffusion epoch 1..5: avg loss ~ 0.15~0.05
這段訓練三個模型各 5 epoch。VAE 的 loss 是 MSE + KL,平均值在 70→50 之間(會逐漸下降),這個數字本身意義不大——重要的是視覺化結果。GAN 的 loss 是 D 與 G 各自的 cross-entropy,D loss 在 0.6~1.3 之間震盪、G loss 在 1.5~0.8 之間震盪,這是 GAN 訓練的典型特徵(loss 不是單調下降而是互相博弈)。Diffusion 的 loss 是 MSE,平均值從 0.15 降到 0.05——單調下降、非常穩定,這正是 Diffusion 比 GAN 更容易訓練的關鍵。
注意 GAN 訓練的標準流程是「D 更新一次、G 更新一次」(不是 1:1 同時更新),這是 Goodfellow 原文的設計;實務上常用 D 更新 2~5 次才更新 G 一次(TTUR),讓 D 保持領先 G 一步。我們這裡為了簡化用 1:1 更新,5 epoch 後的 G 可能還沒收斂——實務上 GAN 需要至少 20~50 epoch 才會產出像樣的結果。
# 6. 取樣比較:VAE、GAN、Diffusion 各生成 8 張影像
import matplotlib.pyplot as plt
# VAE 取樣:從標準常態取樣 z、解碼
vae.eval()
with torch.no_grad():
z = torch.randn(8, 16)
vae_samples = vae.decode(z).cpu()
# GAN 取樣:從標準常態取樣 z、過 G
G.eval()
with torch.no_grad():
z = torch.randn(8, 32)
gan_samples = G(z).cpu()
# Diffusion 取樣:從純噪音開始,逐步去噪
denoiser.eval()
with torch.no_grad():
xt = torch.randn(8, 1, 28, 28)
for t in reversed(range(T_STEPS)):
eps_hat = denoiser(xt, torch.full((8,), t, dtype=torch.long))
a_bar = alpha_bars[t]
x0_hat = (xt - torch.sqrt(1 - a_bar) * eps_hat) / torch.sqrt(a_bar)
if t > 0:
xt = torch.sqrt(alphas[t]) * x0_hat + torch.sqrt(1 - alphas[t]) * torch.randn_like(xt)
else:
xt = x0_hat
diff_samples = xt.cpu()
# 把 8 張影像畫成 2×4 網格
fig, axes = plt.subplots(3, 8, figsize=(12, 5))
for i in range(8):
axes[0, i].imshow(vae_samples[i].squeeze(), cmap="gray")
axes[1, i].imshow(gan_samples[i].squeeze(), cmap="gray")
axes[2, i].imshow(diff_samples[i].squeeze(), cmap="gray")
for j in range(3):
axes[j, i].axis("off")
plt.tight_layout()
plt.savefig("compare.png", dpi=80)
print("已儲存 compare.png(VAE 上排、GAN 中排、Diffusion 下排)")
這段展示三種模型的取樣流程:VAE 直接從標準常態取 z、解碼;GAN 直接從標準常態取 z、過 G;Diffusion 從純噪音開始,反覆執行 T 步去噪。取樣後把三組影像畫成 3×8 網格(上排 VAE、中排 GAN、下排 Diffusion),存成 compare.png 方便視覺比較。
視覺化的典型結果是:VAE 影像偏模糊(數字輪廓可辨但邊緣不銳利),這是 MSE 重建誤差的「平均化效應」;GAN 影像銳利但多樣性低(5 epoch 可能還沒收斂,看到一些「介於兩個數字之間」的怪影像;訓練 50+ epoch 後才會銳利且多樣),這是「對抗訓練 + 模式崩潰」的雙重影響;Diffusion 影像介於兩者之間(比 VAE 銳利、比 GAN 模糊,但 5 epoch 已經能看出數字輪廓),這是 MSE 預測噪音 + 漸進去噪的結果。如果你加大 Diffusion 的 T_STEPS(例如 1000)與訓練 epoch(例如 20),Diffusion 的影像品質會超越 VAE 與 GAN。
常見錯誤與踩雷
錯誤一:VAE 的解碼器輸出用 sigmoid 而非 tanh。常見症狀:影像偏暗、只有一半像素範圍被用。對應排查方向:輸入影像要線性映射到 [-1, 1](用 Normalize((0.5,), (0.5,))),解碼器用 tanh 輸出範圍 [-1, 1]。如果你想用 sigmoid(輸出 [0, 1]),輸入也要對應映射到 [0, 1] 並用 BCE 重建損失。
錯誤二:GAN 的 G 與 D 同時做多次更新。常見症狀:D 太快變強、G 的梯度消失、訓練崩潰。對應排查方向:用 TTUR(D 的 lr 是 G 的 4~8 倍,或 D 每步更新 2~5 次、G 只更新 1 次)。本篇範例用 1:1 更新只是為了簡化,實務上必須調整。
錯誤三:Diffusion 用 sigmoid 而非 tanh,或沒對齊輸入分布。常見症狀:模型預測的噪音全部集中在 0 附近(因為輸入影像都接近 0),loss 看似下降但取樣出來是純噪音。對應排查方向:輸入影像必須線性映射到 [-1, 1],配合前向加噪公式 √α̅_t · x₀ + √(1-α̅_t) · ε 的數學推導。
錯誤四:把 Diffusion 的 T 設太小(例如 10)。常見症狀:訓練 loss 看起來很低但取樣品質很差。對應排查方向:T 太小 → 加噪過程不夠細、α̅_t 衰減太快 → 模型學不到「逐步去噪」的細節。一般 T=1000 是 DDPM 原文設定,T=200~500 是加速變體(如 DDIM),但 T < 50 通常效果很差。
錯誤五:用 GAN 的 loss 數字判斷生成品質。常見症狀:D loss = 0.1、G loss = 2.0 看起來「D 太強」,但其實是 G 已經崩潰(只產生一張影像、D 對其他影像給 0)。對應排查方向:必須視覺化 G 的輸出才能判斷品質;FID score(Day 38 會介紹)是更好的量化指標。
錯誤六:把 VAE 的 KL 散度權重 β 設太大(例如 β=10)。常見症狀:重建誤差飆高、影像變模糊。對應排查方向:β 太大 → 模型過度強調「潜空間接近常態」而忽略重建品質。β-VAE 的研究建議 β 在 1~4 之間,視任務調整;想讓潜空間更有結構(但犧牲重建)用大 β、想讓重建更準(但潜空間較混亂)用小 β。
效能與實務提醒
三個模型在 CPU 上的訓練成本差異極大——VAE 5 epoch 約 2 分鐘、GAN 5 epoch 約 3 分鐘、Diffusion 5 epoch 約 5 分鐘(因為每個 batch 要呼叫網路 200 次去預測噪音)。在 Colab T4 上三者都約 30~60 秒。取樣速度差異更大:VAE 與 GAN 各約 0.01 秒(一次 forward)、Diffusion 約 3 秒(200 步去噪),Stable Diffusion 用 DDIM 50 步可以把 Diffusion 加速到約 0.5 秒。
實務上 Diffusion 的「慢」是它的最大缺點——它的高品質樣本是用「計算量」換來的。Day 36~37 介紹的 Stable Diffusion 用兩個關鍵設計解決這個問題:(a) VAE 把 512×512 影像編碼到 64×64 潜空間(計算量降到 1/64²)、(b) DDIM 把 1000 步減到 50 步(計算量再降到 1/20)。綜合下來 Stable Diffusion 比原生 DDPM 快約 1000 倍,但仍比 VAE/GAN 慢約 50 倍。
另一個工程取捨是「可控性」。VAE 的潜空間最有結構(可以做向量算術、線性內插),GAN 的中層潛變數也有結構但較不線性,Diffusion 的「條件生成」是透過 classifier-free guidance 或 ControlNet(Day 37)實現——這是 Stable Diffusion 能接受文字 prompt 的核心機制。如果你的任務是「把現有的圖片做風格轉換」或「在兩個樣本之間插值」,VAE 是首選;如果你的任務是「從文字描述生成圖片」,Diffusion(加條件控制)是 2024 年的主流。
小結
今天把三大生成模型家族的地圖打通:VAE 的「編碼 → 潜空間 → 解碼」與 KL 散度、GAN 的「生成器 vs 判別器」對抗訓練與模式崩潰、Diffusion 的「加噪 → 學去噪」漸進式生成。重點回顧:第一,三者的共同目標都是「給一個隨機向量產生影像」,差別在訓練與取樣機制;第二,VAE 影像模糊但可控、GAN 影像銳利但難訓練、Diffusion 兩者兼顧但慢;第三,Diffusion 成為 2022~2024 年的主流是因為它在 FID 等量化指標上超越 GAN、且訓練穩定性比 GAN 高很多;第五,三者的參數量在「百萬級」,可訓練、可視覺化、可擴展。明天 Day 34 會從 GAN 的「對抗式訓練」展開 DCGAN 的完整實作,在 MNIST 上跑 50 epoch 看看生成影像的演進;Day 35 會從 Diffusion 的「加噪去噪」展開 DDPM 的完整推導與 DDIM 加速取樣;Day 36~37 會把這套原理推到 Stable Diffusion 的實際應用。
結語
今天的重點是「把生成模型三大家族的設計哲學與程式介面掌握在自己手裡」。我們從 VAE 的「μ、log σ²、重參數化、KL 散度」開始,建立 16 維潜空間的最小編碼器-解碼器;接著用 GAN 的「G 與 D 對抗」在 32 維 z 空間生成影像;然後用 Diffusion 的「200 步加噪 → 學去噪」建立 MLP-based denoiser;最後三個模型同時在 MNIST 上訓練 5 epoch 並把取樣結果畫成 3×8 網格做視覺比較。讀完這篇你應該能回答:VAE 為什麼模糊?GAN 為什麼難訓練?Diffusion 為什麼成為主流?三者各自的最佳應用場景是什麼?
生成模型與前 32 天的關聯:Day 1~32 處理的是「分析現有資料」(分類、偵測、分割、姿態),今天開始進入「生成新資料」。這個典範轉移讓我們從「模型是靜態的讀者」變成「模型是動態的作者」,也是 CV 進階路線從「感知」進入「創造」的關鍵節點。明天 Day 34 會把 GAN 的對抗訓練推到完整實作,在 MNIST 上跑 DCGAN 50 epoch 並觀察模式崩潰的發生與對策;Day 35 會把 Diffusion 的加噪去噪推到完整推導,介紹 DDPM 的訓練目標數學證明與 DDIM 的加速取樣。本篇是「地圖」,接下來四篇是「按圖施工」。
延伸資源
- Kingma 與 Welling,2013,Auto-Encoding Variational Bayes(arXiv 2013):
https://arxiv.org/abs/1312.6114,VAE 原始論文,理解重參數化技巧與 ELBO 推導。 - Goodfellow 等人,2014,Generative Adversarial Networks(NeurIPS 2014):
https://arxiv.org/abs/1406.2661,GAN 原始論文,理解 min-max 訓練目標與理論收斂性證明。 - Ho 等人,2020,Denoising Diffusion Probabilistic Models(NeurIPS 2020):
https://arxiv.org/abs/2006.11239,DDPM 原始論文,理解前向加噪與反向去噪的數學推導。 - Karras 等人,2019,A Style-Based Generator Architecture for Generative Adversarial Networks(CVPR 2019):StyleGAN 原始論文,理解潜空間 W+ 的線性結構與風格控制。
- Song 等人,2020,Denoising Diffusion Implicit Models(ICLR 2021):
https://arxiv.org/abs/2010.02502,DDIM 加速取樣的原始論文,把取樣步數從 1000 降到 50。 - Rombach 等人,2022,High-Resolution Image Synthesis with Latent Diffusion Models(CVPR 2022):
https://arxiv.org/abs/2112.10752,Stable Diffusion(Latent Diffusion Model)原始論文,理解為什麼「在 VAE 潜空間做 Diffusion」可以把 512×512 影像的計算量降到可接受範圍。
留言
張貼留言