Diffusion Models
Mô hình khuếch tán
Bạn xem video mực nhỏ vào nước — mực lan ra dần. Nếu quay ngược video, mực sẽ...
Ẩn dụ: Người điêu khắc đá cẩm thạch
Michelangelo từng nói: "Pho tượng đã có sẵn trong khối đá, tôi chỉ đục bỏ phần thừa." Diffusion model hoạt động đúng như vậy. Khối đá thô = nhiễu Gaussian thuần túy. Mỗi nhát đục nhỏ = một bước khử nhiễu. Kết quả sau hàng trăm nhát = pho tượng (ảnh) hiện ra dần từ hỗn loạn.
Điểm thú vị: model không ghi nhớ pho tượng cụ thể nào. Nó học một quy luật tổng quát "cạnh này nên sắc hơn, vùng kia nên mịn hơn" — gọi là score function. Prompt chỉ đơn giản là nói với người điêu khắc: "hôm nay hãy đục ra một con rồng đang bay trong mưa". Cùng một người, cùng bộ kỹ năng, nhưng prompt khác → kết quả khác hoàn toàn.
Hình minh họa
Bước 1: Vẽ hình của bạn
Click vào ô để tô đen/trắng. Vẽ chữ, hình, hoặc bất kỳ pattern nào bạn muốn.
So sánh Forward vs Reverse Process
Forward (thêm nhiễu)
Reverse (khử nhiễu)
Khám phá cùng lúc
Forward bắt đầu từ ảnh sạch, thêm nhiễu dần. Reverse bắt đầu từ nhiễu, khử dần thành ảnh mới. Hai quá trình ngược chiều nhưng cùng số bước.
So sánh sampler: DDPM vs DDIM
Cùng một diffusion model (cùng U-Net đã train), nhưng chọn sampler khác nhau cho kết quả rất khác. Kéo thanh trượt dưới để thấy ảnh hưởng của số step tới thời gian sinh ảnh.
Cần đủ step để khử nhiễu mượt. Với 50 step, chất lượng ≈ kém (ảnh nhoè). Thời gian ~ 3.00s/ảnh.
Với 50 step, chất lượng ≈ rất tốt. Cùng seed → cùng output. 50 step là đủ cho hầu hết prompt.
Lưu ý: đây là mô phỏng trực quan, không phải benchmark thực tế.
DDPM — Markovian, stochastic
- Số bước tiêu biểu1000 (có thể 250–4000)
- Tính ngẫu nhiên (η)Stochastic: η = 1, mỗi lần sample khác nhau
- Quá trình ngượcMarkovian: x_{t-1} chỉ phụ thuộc x_t
- Tốc độChậm, ~30–60 giây/ảnh trên A100
- Ứng dụngResearch baseline, nghiên cứu lý thuyết
- Tái hiện (reproducibility)Khó — do stochasticity giữa các bước
DDIM — Non-Markovian, deterministic
- Số bước tiêu biểu20–50 step
- Tính ngẫu nhiên (η)Deterministic khi η = 0 — cùng noise → cùng ảnh
- Quá trình ngượcNon-Markovian: x_{t-1} phụ thuộc cả x_t và x_0 dự đoán
- Tốc độNhanh ~10× với 50 step, chất lượng vẫn ổn
- Ứng dụngProduction — Stable Diffusion mặc định
- Tái hiện (reproducibility)Dễ — seed cố định → ảnh cố định
Classifier-Free Guidance — kéo slider để khám phá
Guidance scale w điều khiển mức độ "bám prompt". Công thức: ε̂ = ε_uncond + w · (ε_cond − ε_uncond). Kéo slider để xem trạng thái đầu ra — thấp thì lờ mờ không bám prompt, quá cao thì bão hoà, sweet spot ở w ≈ 7.5.
Kiến trúc Stable Diffusion: VAE + U-Net + Text Encoder
Stable Diffusion là latent diffusion model: thay vì khuếch tán trên pixel 512×512×3 = 786k chiều, ta nén ảnh xuống latent 64×64×4 = 16k chiều bằng VAE, rồi diffusion chạy trong không gian ít hơn ~48 lần. Text encoder (CLIP-L hoặc T5) cung cấp điều kiện qua cross-attention.
Quy trình Thêm nhiễu → Học khử nhiễu → Sinh ảnh mới chính là Diffusion Model — nền tảng của Stable Diffusion, DALL·E và Midjourney! Bí mật không phải ở việc "nhớ" ảnh huấn luyện, mà là học một trường vector score function chỉ đường về phân phối dữ liệu thật: mỗi bước chỉ đi một đoạn ngắn theo hướng giảm nhiễu, và sau hàng chục bước, điểm bắt đầu ngẫu nhiên tự rơi vào manifold của ảnh có nghĩa.
So với GAN chỉ cần 1 forward pass, diffusion phải lặp lại nhiều bước — đây là trade-off giữa chất lượng và tốc độ.
Diffusion model cần bao nhiêu bước để sinh 1 ảnh? (Thường là 20-1000 bước khử nhiễu). Đây là ưu điểm hay nhược điểm so với GAN?
Bạn nhận deliverable một mô hình SD fine-tune cho prompt 'Vietnamese food photography'. Team cần tái hiện chính xác một ảnh cho poster quảng cáo. Nên chọn sampler nào và vì sao?
Giải thích
Diffusion Model là lớp mô hình sinh (generative model) học phân phối dữ liệu p(x) bằng cách định nghĩa một quá trình forward dần phá huỷ thông tin bằng nhiễu Gaussian, và học một quá trình reverse để tái tạo dữ liệu từ nhiễu thuần tuý. Diffusion vượt trội GAN về độ ổn định huấn luyện, độ đa dạng (không mode collapse) và chất lượng sinh — đồng thời dễ điều kiện hoá (conditional generation) cho text-to-image, text-to-video, inpainting, super-resolution, v.v.
Forward Process — Thêm nhiễu có hệ thống
Tại mỗi bước t, ta thêm nhiễu Gaussian theo schedule β_t ∈ (0, 1):
Bằng tham số hoá ᾱ_t = ∏_{s=1}^t(1-β_s), ta có closed-form cho bước bất kỳ:
Khi T đủ lớn (thường 1000), ᾱ_T → 0, nên x_T ≈ 𝒩(0, I) — nhiễu thuần Gaussian. Đây là điểm khởi đầu của reverse process.
Reverse Process — U-Net học khử nhiễu
Ta muốn học p_θ(x_{t-1} | x_t). Với β nhỏ, phân phối này xấp xỉ Gaussian, tham số hoá:
Ho et al. (2020) chứng minh reparametrisation sau đưa loss về dạng rất gọn — chỉ cần U-Net dự đoán nhiễu ε đã thêm:
Với x_t = √ᾱ_t x_0 + √(1-ᾱ_t) ε, ε ∼ 𝒩(0, I). Ý nghĩa: training chỉ đơn giản là cho U-Net xem ảnh đã thêm nhiễu và đoán lại nhiễu — rồi tối thiểu hoá MSE. Không cần discriminator, không cần adversarial, không mode collapse.
Classifier-Free Guidance (CFG)
Để tăng độ bám prompt mà không cần classifier riêng, ta huấn luyện U-Net với cả prompt thật và prompt rỗng (drop prompt 10% thời gian). Khi inference:
Với w là guidance scale. w = 1 nghĩa là chỉ dùng conditional (không boost), w > 1 khuếch đại chênh lệch → bám prompt chặt hơn. SD 1.5 khuyến nghị w ≈ 7.5; quá cao gây oversaturation như đã thấy trong viz ở trên.
import torch
import torch.nn.functional as F
# 1. Noise schedule (β_t) — thường dùng linear hoặc cosine
T = 1000
betas = torch.linspace(1e-4, 0.02, T) # linear schedule
alphas = 1.0 - betas
alphas_cumprod = torch.cumprod(alphas, dim=0) # ᾱ_t
def sample_noisy(x0, t, noise):
"""Thêm nhiễu tới bước t bằng closed-form (không cần loop)."""
a_bar = alphas_cumprod[t].view(-1, 1, 1, 1)
return a_bar.sqrt() * x0 + (1 - a_bar).sqrt() * noise
# 2. Training step
def train_step(unet, x0, optimizer):
batch = x0.size(0)
# Lấy t ngẫu nhiên cho mỗi ảnh trong batch
t = torch.randint(0, T, (batch,), device=x0.device)
# ε ~ N(0, I)
noise = torch.randn_like(x0)
# Tạo x_t
x_t = sample_noisy(x0, t, noise)
# U-Net dự đoán nhiễu
noise_pred = unet(x_t, t)
# MSE loss giữa ε thực và ε dự đoán
loss = F.mse_loss(noise_pred, noise)
optimizer.zero_grad()
loss.backward()
optimizer.step()
return loss.item()
# 3. Vòng lặp training
for epoch in range(n_epochs):
for x0 in dataloader:
loss = train_step(unet, x0.to(device), optimizer)@torch.no_grad()
def ddim_sample(unet, shape, timesteps=50, eta=0.0, device="cuda"):
"""DDIM sampling: non-Markovian, deterministic khi eta=0."""
x = torch.randn(shape, device=device)
# Chọn 50 bước rải đều trong [0, T)
step_ratio = T // timesteps
ddim_steps = torch.arange(0, T, step_ratio).flip(0)
for i, t in enumerate(ddim_steps):
t_batch = torch.full((shape[0],), t, device=device)
t_prev = ddim_steps[i + 1] if i + 1 < len(ddim_steps) else -1
# U-Net predict noise
eps = unet(x, t_batch)
a_t = alphas_cumprod[t]
a_prev = alphas_cumprod[t_prev] if t_prev >= 0 else torch.tensor(1.0)
# Dự đoán x_0
x0_pred = (x - (1 - a_t).sqrt() * eps) / a_t.sqrt()
# Hệ số stochastic (eta=0 → fully deterministic)
sigma = eta * ((1 - a_prev) / (1 - a_t)).sqrt() * (1 - a_t / a_prev).sqrt()
direction = (1 - a_prev - sigma ** 2).sqrt() * eps
noise = sigma * torch.randn_like(x) if eta > 0 else 0.0
# Cập nhật x theo DDIM formula
x = a_prev.sqrt() * x0_pred + direction + noise
return x # ảnh sinh raimport torch
from diffusers import StableDiffusionPipeline, DDIMScheduler
# Load pipeline Stable Diffusion 1.5
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype=torch.float16,
).to("cuda")
# Thay scheduler mặc định bằng DDIM để deterministic
pipe.scheduler = DDIMScheduler.from_config(pipe.scheduler.config)
# Inference với CFG scale + seed cố định
generator = torch.Generator("cuda").manual_seed(42)
image = pipe(
prompt="a Vietnamese pho bowl, studio lighting, 8k photography",
negative_prompt="blurry, low quality, deformed",
num_inference_steps=30, # DDIM chỉ cần 20-50 steps
guidance_scale=7.5, # CFG sweet spot
generator=generator,
).images[0]
image.save("pho.png")
# --- CFG bên trong thực chất làm gì? (pseudo-code) ---
# for t in timesteps:
# # 1. Duplicate latent: [cond, uncond]
# latent_in = torch.cat([latent] * 2)
# # 2. Text embedding: [cond_emb, uncond_emb]
# text_emb = torch.cat([cond_emb, uncond_emb])
# # 3. U-Net forward một lần cho cả 2
# eps_cond, eps_uncond = unet(latent_in, t, text_emb).chunk(2)
# # 4. Guided noise
# eps_hat = eps_uncond + guidance_scale * (eps_cond - eps_uncond)
# # 5. Scheduler step
# latent = scheduler.step(eps_hat, t, latent).prev_sampleclass StableDiffusion(nn.Module):
def __init__(self):
super().__init__()
# 1. VAE: nén và giải nén ảnh
self.vae = AutoencoderKL(
in_channels=3, out_channels=3,
latent_channels=4, # 4 kênh latent
downsample_factor=8, # 512 → 64
)
# 2. Text encoder: prompt → embedding
self.text_encoder = CLIPTextModel.from_pretrained("clip-vit-large")
# 3. U-Net: denoise latent có điều kiện text
self.unet = UNet2DConditionModel(
sample_size=64, # latent resolution
in_channels=4, # latent channels
cross_attention_dim=768, # khớp CLIP-L
)
def encode_image(self, image):
"""512×512×3 → 64×64×4 latent."""
latent = self.vae.encode(image).latent_dist.sample()
return latent * 0.18215 # scale factor của SD 1.5
def decode_latent(self, latent):
"""64×64×4 → 512×512×3 ảnh."""
return self.vae.decode(latent / 0.18215).sample
def forward(self, latent, t, prompt_emb):
"""Predict noise từ latent có điều kiện prompt embedding."""
return self.unet(latent, t, encoder_hidden_states=prompt_emb).sample
@torch.no_grad()
def generate(self, prompt, steps=50, cfg=7.5):
# Encode prompt + empty prompt
cond = self.text_encoder(tokenize(prompt))
uncond = self.text_encoder(tokenize(""))
# Random latent noise
z = torch.randn(1, 4, 64, 64, device="cuda")
# Denoise loop (DDIM)
for t in reversed(range(0, 1000, 1000 // steps)):
eps_c = self.forward(z, t, cond)
eps_u = self.forward(z, t, uncond)
eps = eps_u + cfg * (eps_c - eps_u)
z = ddim_step(z, eps, t)
# Decode latent → image
return self.decode_latent(z)Ứng dụng thực tế của diffusion model đã bùng nổ từ 2022:
- Text-to-image: Stable Diffusion, Midjourney, DALL·E 3, Imagen, Firefly, Ideogram.
- Image editing: inpainting, outpainting (mở rộng ảnh), instruct-pix2pix, ControlNet (điều khiển theo pose/canny/depth).
- Text-to-video: Sora (OpenAI), Veo (Google), Runway Gen-3, Pika, Kling, Luma Dream Machine — dùng diffusion 3D (thêm chiều thời gian).
- Text-to-3D: DreamFusion, Magic3D — dùng SDS loss để guide NeRF.
- Audio: Riffusion (nhạc), AudioLDM, Stable Audio — diffusion trên spectrogram hoặc latent audio.
- Protein & khoa học: RFdiffusion (thiết kế protein), AlphaFold 3 (cấu trúc protein-ligand), DiffDock (docking phân tử).
- Robotics: Diffusion Policy — sinh action sequence cho robot, hơn hẳn MLP policy truyền thống.
- Super-resolution & restoration: SUPIR, StableSR, Real-ESRGAN hybrid với diffusion.
Bẫy thường gặp (pitfalls):
- Schedule mismatch giữa training & inference: train với linear nhưng sample với cosine → ảnh bị "xám", thiếu contrast. Luôn dùng cùng schedule.
- CFG scale quá cao: w = 20 không phải "siêu bám prompt" — là artifact nặng. Giữ w ∈ [5, 9] cho SD.
- Bỏ qua negative prompt: "low quality, blurry, deformed, extra fingers" — nâng chất lượng đáng kể miễn phí.
- VAE decode bị 'banding': dùng fp16 decoder gây artifact dải màu. Giải: decode ở fp32 hoặc dùng VAE fine-tune (madebyollin/sdxl-vae-fp16-fix).
- Overfit khi fine-tune: LoRA/DreamBooth dễ học thuộc vài ảnh → bị "memorise" (vi phạm bản quyền). Dùng augmentation, prior preservation loss, rank LoRA nhỏ (8–16).
- Ignoring prompt weighting syntax: mỗi pipeline có cú pháp khác nhau —
(word:1.3)trong Automatic1111,word++trong ComfyUI. Học cú pháp đúng cho tool đang dùng. - Latent leak giữa concept: khi training custom model trên dataset nhỏ, embedding bị "đa nghĩa" (token gốc + concept mới). Dùng token mới (rare-token) thay vì override token có sẵn.
- Forward Process thêm nhiễu Gaussian theo β_t schedule; sau T bước, x_T ≈ 𝒩(0, I) — nhiễu thuần.
- Reverse Process dùng U-Net dự đoán ε (MSE loss đơn giản); sample lặp lại từ nhiễu về ảnh có nghĩa.
- DDPM stochastic, 1000 step; DDIM non-Markovian, deterministic khi η=0, chỉ cần 20–50 step — production chuẩn.
- Classifier-Free Guidance: ε̂ = ε_uncond + w·(ε_cond − ε_uncond). Sweet spot w ≈ 5–9; quá cao gây oversaturation.
- Stable Diffusion = VAE (nén 512→64) + U-Net (diffusion trên latent) + Text Encoder (CLIP/T5 + cross-attention).
- Chất lượng cao, không mode collapse, dễ điều kiện hoá — nhưng chậm hơn GAN. DPM-Solver, LCM, SDXL Turbo đang đóng gap tốc độ.
Kiểm tra hiểu biết
Forward process trong Diffusion Model thực hiện điều gì?