@xdev-asia/xdev-knowledge-mcp 1.0.58 → 1.0.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (15) hide show
  1. package/content/blog/ai/nvidia-dli-generative-ai-chung-chi-va-lo-trinh-hoc.md +894 -0
  2. package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/chapters/01-deep-learning-foundations/lessons/01-bai-1-pytorch-neural-network-fundamentals.md +790 -0
  3. package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/chapters/01-deep-learning-foundations/lessons/02-bai-2-transformer-architecture-attention.md +984 -0
  4. package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/chapters/02-diffusion-models/lessons/01-bai-3-unet-architecture-denoising.md +1111 -0
  5. package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/chapters/02-diffusion-models/lessons/02-bai-4-ddpm-forward-reverse-diffusion.md +1007 -0
  6. package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/chapters/02-diffusion-models/lessons/03-bai-5-clip-text-to-image-pipeline.md +1037 -0
  7. package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/chapters/03-llm-applications-rag/lessons/01-bai-6-llm-inference-pipeline-design.md +929 -0
  8. package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/chapters/03-llm-applications-rag/lessons/02-bai-7-rag-retrieval-augmented-generation.md +1099 -0
  9. package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/chapters/03-llm-applications-rag/lessons/03-bai-8-rag-agent-build-evaluate.md +1249 -0
  10. package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/chapters/04-agentic-ai-customization/lessons/01-bai-9-agentic-ai-multi-agent-systems.md +1357 -0
  11. package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/chapters/04-agentic-ai-customization/lessons/02-bai-10-llm-evaluation-lora-fine-tuning.md +1867 -0
  12. package/content/series/luyen-thi/luyen-thi-nvidia-dli-generative-ai/index.md +237 -0
  13. package/data/quizzes/nvidia-dli-generative-ai.json +350 -0
  14. package/data/quizzes.json +14 -0
  15. package/package.json +1 -1
@@ -0,0 +1,1007 @@
1
+ ---
2
+ id: 019c9619-nv01-p2-l04
3
+ title: 'Bài 4: DDPM — Forward & Reverse Diffusion'
4
+ slug: bai-4-ddpm-forward-reverse-diffusion
5
+ description: >-
6
+ Forward diffusion: Markov chain, variance schedule, reparameterization.
7
+ Reverse diffusion: predict noise, denoise step-by-step.
8
+ Noise scheduling: linear, cosine schedules.
9
+ Training objective: simplified ELBO loss.
10
+ Classifier-Free Diffusion Guidance (CFG).
11
+ duration_minutes: 90
12
+ is_free: true
13
+ video_url: null
14
+ sort_order: 4
15
+ section_title: "Part 2: Generative AI with Diffusion Models"
16
+ course:
17
+ id: 019c9619-nv01-7001-c001-nv0100000001
18
+ title: 'Luyện thi NVIDIA DLI — Generative AI with Diffusion Models & LLMs'
19
+ slug: luyen-thi-nvidia-dli-generative-ai
20
+ ---
21
+
22
+ <h2 id="gioi-thieu">1. Giới thiệu: Toán học đằng sau Diffusion Models</h2>
23
+
24
+ <p>Bài này là <strong>phần khó nhất</strong> trong toàn bộ khoá DLI. Bạn sẽ đi sâu vào nền tảng toán học của <strong>Denoising Diffusion Probabilistic Models (DDPM)</strong> — paper gốc từ Ho et al. (2020). Mọi diffusion model hiện đại (Stable Diffusion, DALL·E, Imagen) đều dựa trên framework này.</p>
25
+
26
+ <p>Bài trước bạn đã xây xong <strong>U-Net</strong> — kiến trúc backbone. Giờ bạn sẽ hiểu chính xác U-Net học cái gì, bằng cách nào, và tại sao nó hoạt động.</p>
27
+
28
+ <blockquote><p><strong>Exam tip:</strong> NVIDIA DLI assessment yêu cầu bạn implement cả <strong>forward diffusion</strong>, <strong>reverse sampling</strong>, và <strong>training loop</strong> từ đầu. Hiểu rõ từng công thức và cách chúng map sang code PyTorch là bắt buộc — không chỉ chạy code mẫu.</p></blockquote>
29
+
30
+ <pre><code class="language-text">
31
+ DDPM Overview — Two Processes
32
+ ═════════════════════════════
33
+
34
+ FORWARD DIFFUSION q(x_t | x_{t-1}) REVERSE DIFFUSION p_θ(x_{t-1} | x_t)
35
+ ────────────────────────────────── ───────────────────────────────────────
36
+
37
+ x_0 ──► x_1 ──► x_2 ──►...──► x_T x_T ──► x_{T-1} ──►...──► x_1 ──► x_0
38
+ (clean) +ε +ε (noise) (noise) U-Net U-Net (clean)
39
+
40
+ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
41
+ │ ████ │→ │ ▓▓▓▓ │→ │ ░░░░ │→ │ ···· │ Forward: add noise (fixed, no learning)
42
+ │ ████ │ │ ▓▓▓▓ │ │ ░░░░ │ │ ···· │
43
+ └──────┘ └──────┘ └──────┘ └──────┘
44
+ t = 0 t = 100 t = 500 t = 1000
45
+
46
+ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
47
+ │ ···· │→ │ ░░░░ │→ │ ▓▓▓▓ │→ │ ████ │ Reverse: remove noise (learned by U-Net)
48
+ │ ···· │ │ ░░░░ │ │ ▓▓▓▓ │ │ ████ │
49
+ └──────┘ └──────┘ └──────┘ └──────┘
50
+ t = 1000 t = 500 t = 100 t = 0
51
+ </code></pre>
52
+
53
+ <figure><img src="/storage/uploads/2026/04/nvidia-dli-bai4-ddpm-diffusion-process.png" alt="DDPM — Forward Diffusion (thêm noise) và Reverse Diffusion (khử noise)" loading="lazy" /><figcaption>DDPM — Forward Diffusion (thêm noise) và Reverse Diffusion (khử noise)</figcaption></figure>
54
+
55
+ <h2 id="forward-diffusion">2. Forward Diffusion Process</h2>
56
+
57
+ <h3 id="markov-chain">2.1 Markov Chain formulation</h3>
58
+
59
+ <p>Forward diffusion là quá trình dần dần thêm <strong>Gaussian noise</strong> vào ảnh gốc x_0 qua T timesteps. Đây là một <strong>Markov chain</strong> — mỗi bước chỉ phụ thuộc vào bước ngay trước đó:</p>
60
+
61
+ <pre><code class="language-text">
62
+ q(x_{1:T} | x_0) = ∏_{t=1}^{T} q(x_t | x_{t-1})
63
+ </code></pre>
64
+
65
+ <p>Tại mỗi timestep t, chúng ta thêm noise theo phân phối Gaussian:</p>
66
+
67
+ <pre><code class="language-text">
68
+ q(x_t | x_{t-1}) = N(x_t; √(1 - β_t) · x_{t-1}, β_t · I)
69
+ ▲ mean ▲ variance
70
+ </code></pre>
71
+
72
+ <p>Trong đó <strong>β_t</strong> (beta) là <strong>variance schedule</strong> — một giá trị nhỏ tăng dần từ β_1 ≈ 0.0001 đến β_T ≈ 0.02. Nó kiểm soát lượng noise được thêm tại mỗi step.</p>
73
+
74
+ <table>
75
+ <thead>
76
+ <tr><th>Ký hiệu</th><th>Ý nghĩa</th><th>Giá trị điển hình</th></tr>
77
+ </thead>
78
+ <tbody>
79
+ <tr><td>β_t</td><td>Variance tại timestep t</td><td>0.0001 → 0.02</td></tr>
80
+ <tr><td>α_t = 1 - β_t</td><td>Signal retention ratio</td><td>0.9999 → 0.98</td></tr>
81
+ <tr><td>ᾱ_t = ∏_{s=1}^{t} α_s</td><td>Cumulative signal retention</td><td>≈1.0 → ≈0.0</td></tr>
82
+ <tr><td>T</td><td>Tổng số timesteps</td><td>1000 (DDPM gốc)</td></tr>
83
+ <tr><td>ε</td><td>Standard Gaussian noise</td><td>ε ~ N(0, I)</td></tr>
84
+ </tbody>
85
+ </table>
86
+
87
+ <h3 id="closed-form">2.2 Closed-form: Nhảy thẳng đến timestep bất kỳ</h3>
88
+
89
+ <p>Điểm then chốt: ta <strong>không cần chạy tuần tự</strong> T bước forward. Nhờ tính chất cộng của Gaussian, ta có closed-form để tính x_t trực tiếp từ x_0:</p>
90
+
91
+ <pre><code class="language-text">
92
+ q(x_t | x_0) = N(x_t; √(ᾱ_t) · x_0, (1 - ᾱ_t) · I)
93
+
94
+ Trong đó:
95
+ ᾱ_t = α_1 · α_2 · ... · α_t = ∏_{s=1}^{t} (1 - β_s)
96
+ </code></pre>
97
+
98
+ <p>Điều này cực kỳ quan trọng cho training — ta có thể sample bất kỳ timestep t nào mà không cần simulate toàn bộ chain.</p>
99
+
100
+ <h3 id="reparameterization">2.3 Reparameterization Trick</h3>
101
+
102
+ <p>Để sample x_t từ phân phối trên và <strong>backpropagate gradient</strong>, ta dùng <strong>reparameterization trick</strong>:</p>
103
+
104
+ <pre><code class="language-text">
105
+ x_t = √(ᾱ_t) · x_0 + √(1 - ᾱ_t) · ε where ε ~ N(0, I)
106
+ ──────────────── ──────────────────
107
+ signal component noise component
108
+ </code></pre>
109
+
110
+ <p>Công thức này nói: tại timestep t, ảnh x_t là <strong>trộn tuyến tính</strong> giữa ảnh gốc (scaled bởi √ᾱ_t) và noise thuần (scaled bởi √(1−ᾱ_t)). Khi t nhỏ → ᾱ_t ≈ 1 → gần như toàn signal. Khi t lớn → ᾱ_t ≈ 0 → gần như toàn noise.</p>
111
+
112
+ <pre><code class="language-text">
113
+ Signal vs Noise qua timestep (T=1000, linear schedule)
114
+ ═══════════════════════════════════════════════════════
115
+
116
+ Signal: √(ᾱ_t) Noise: √(1-ᾱ_t)
117
+ 1.0 ┤████████░░░░░░ 0.0 ┤░░░░░░░░████████
118
+ │████████░░░░░░ │░░░░░░░░████████
119
+ │██████░░░░░░░░ │░░░░░░██████████
120
+ │████░░░░░░░░░░ │░░░░████████████
121
+ │██░░░░░░░░░░░░ │░░██████████████
122
+ 0.0 ┤░░░░░░░░░░░░░░ 1.0 ┤████████████████
123
+ └────────────── └────────────────
124
+ t=0 t=T t=0 t=T
125
+
126
+ Tại t ≈ T/2: signal ≈ noise (ảnh nửa sạch nửa noise)
127
+ Tại t = T: signal ≈ 0 (pure Gaussian noise)
128
+ </code></pre>
129
+
130
+ <h3 id="forward-code">2.4 Implementation: forward_diffusion()</h3>
131
+
132
+ <pre><code class="language-python">
133
+ import torch
134
+ import torch.nn as nn
135
+ import math
136
+
137
+ def forward_diffusion(x_0, t, sqrt_alpha_bar, sqrt_one_minus_alpha_bar):
138
+ """
139
+ Apply forward diffusion: x_t = sqrt(alpha_bar_t) * x_0 + sqrt(1 - alpha_bar_t) * eps
140
+
141
+ Args:
142
+ x_0: (B, C, H, W) — clean images
143
+ t: (B,) — timestep indices (0-indexed)
144
+ sqrt_alpha_bar: (T,) — precomputed sqrt(ᾱ_t)
145
+ sqrt_one_minus_alpha_bar: (T,) — precomputed sqrt(1 - ᾱ_t)
146
+ Returns:
147
+ x_t: (B, C, H, W) — noisy images at timestep t
148
+ noise: (B, C, H, W) — the noise that was added (needed for loss)
149
+ """
150
+ # Sample Gaussian noise
151
+ noise = torch.randn_like(x_0)
152
+
153
+ # Gather coefficients for each sample in batch
154
+ # (B,) -> (B, 1, 1, 1) for broadcasting with (B, C, H, W)
155
+ s_alpha = sqrt_alpha_bar[t].view(-1, 1, 1, 1)
156
+ s_one_minus = sqrt_one_minus_alpha_bar[t].view(-1, 1, 1, 1)
157
+
158
+ # Reparameterization: x_t = sqrt(ᾱ_t) * x_0 + sqrt(1-ᾱ_t) * ε
159
+ x_t = s_alpha * x_0 + s_one_minus * noise
160
+
161
+ return x_t, noise
162
+ </code></pre>
163
+
164
+ <blockquote><p><strong>Exam tip:</strong> Chú ý <code>.view(-1, 1, 1, 1)</code> — đây là pattern bắt buộc khi gather scalar coefficients rồi broadcast với 4D tensor. Quên reshape sẽ gây shape mismatch. Assessment thường test chính xác điểm này.</p></blockquote>
165
+
166
+ <h2 id="noise-scheduling">3. Noise Scheduling</h2>
167
+
168
+ <h3 id="linear-schedule">3.1 Linear Schedule (DDPM gốc)</h3>
169
+
170
+ <p>Paper DDPM gốc dùng <strong>linear schedule</strong>: β tăng tuyến tính từ β_1 = 0.0001 đến β_T = 0.02 qua T = 1000 steps.</p>
171
+
172
+ <pre><code class="language-python">
173
+ def linear_beta_schedule(T, beta_start=1e-4, beta_end=0.02):
174
+ """
175
+ Linear variance schedule: β_t increases linearly from beta_start to beta_end.
176
+ Original DDPM (Ho et al. 2020).
177
+ """
178
+ return torch.linspace(beta_start, beta_end, T)
179
+
180
+
181
+ def precompute_schedule(betas):
182
+ """Precompute all coefficients from beta schedule."""
183
+ alphas = 1.0 - betas # α_t = 1 - β_t
184
+ alpha_bar = torch.cumprod(alphas, dim=0) # ᾱ_t = ∏ α_s
185
+ sqrt_alpha_bar = torch.sqrt(alpha_bar) # √(ᾱ_t)
186
+ sqrt_one_minus_alpha_bar = torch.sqrt(1.0 - alpha_bar) # √(1 - ᾱ_t)
187
+ sqrt_alpha = torch.sqrt(alphas) # √(α_t) — for reverse step
188
+
189
+ return {
190
+ 'betas': betas,
191
+ 'alphas': alphas,
192
+ 'alpha_bar': alpha_bar,
193
+ 'sqrt_alpha_bar': sqrt_alpha_bar,
194
+ 'sqrt_one_minus_alpha_bar': sqrt_one_minus_alpha_bar,
195
+ 'sqrt_alpha': sqrt_alpha,
196
+ }
197
+
198
+ # Usage
199
+ T = 1000
200
+ schedule = precompute_schedule(linear_beta_schedule(T))
201
+ </code></pre>
202
+
203
+ <h3 id="cosine-schedule">3.2 Cosine Schedule (Improved DDPM)</h3>
204
+
205
+ <p><strong>Vấn đề</strong> với linear schedule: ᾱ_t giảm quá nhanh ở giữa → ảnh bị destroy quá sớm, gây mất thông tin. <strong>Cosine schedule</strong> (Nichol & Dhariwal 2021) khắc phục bằng cách thiết kế ᾱ_t theo hàm cosine — giảm mượt hơn, đặc biệt tốt cho ảnh high-resolution.</p>
206
+
207
+ <pre><code class="language-python">
208
+ def cosine_beta_schedule(T, s=0.008):
209
+ """
210
+ Cosine variance schedule (Nichol & Dhariwal 2021).
211
+ Designs alpha_bar directly via cosine function, then derives betas.
212
+ The 's' offset prevents beta from being too small near t=0.
213
+ """
214
+ steps = torch.arange(T + 1, dtype=torch.float32)
215
+ # f(t) = cos( (t/T + s) / (1+s) * π/2 )²
216
+ f_t = torch.cos(((steps / T) + s) / (1 + s) * (math.pi / 2)) ** 2
217
+ alpha_bar = f_t / f_t[0] # normalize so alpha_bar[0] = 1
218
+
219
+ # Derive betas from alpha_bar: β_t = 1 - ᾱ_t / ᾱ_{t-1}
220
+ betas = 1 - (alpha_bar[1:] / alpha_bar[:-1])
221
+ betas = torch.clamp(betas, min=1e-5, max=0.999) # numerical stability
222
+
223
+ return betas
224
+ </code></pre>
225
+
226
+ <table>
227
+ <thead>
228
+ <tr><th>Đặc điểm</th><th>Linear Schedule</th><th>Cosine Schedule</th></tr>
229
+ </thead>
230
+ <tbody>
231
+ <tr><td>ᾱ_t tại t=T/2</td><td>≈ 0.05 (gần 0)</td><td>≈ 0.50 (vẫn còn signal)</td></tr>
232
+ <tr><td>Signal destruction</td><td>Nhanh, aggressive</td><td>Mượt, gradual</td></tr>
233
+ <tr><td>High-resolution images</td><td>Kém (mất detail sớm)</td><td>Tốt hơn nhiều</td></tr>
234
+ <tr><td>Original paper</td><td>DDPM (Ho 2020)</td><td>Improved DDPM (Nichol 2021)</td></tr>
235
+ <tr><td>Dùng trong Stable Diffusion</td><td>Không</td><td>Có (biến thể)</td></tr>
236
+ <tr><td>NVIDIA DLI focus</td><td>Implement trong lab</td><td>Hiểu concept, so sánh</td></tr>
237
+ </tbody>
238
+ </table>
239
+
240
+ <pre><code class="language-text">
241
+ ᾱ_t Comparison: Linear vs Cosine (T=1000)
242
+ ══════════════════════════════════════════
243
+
244
+ ᾱ_t
245
+ 1.0 ┤C C C C L
246
+ │C C C L
247
+ 0.8 ┤ C C L
248
+ │ C L
249
+ 0.6 ┤ C L
250
+ │ C L
251
+ 0.4 ┤ C L
252
+ │ C L
253
+ 0.2 ┤ C L
254
+ │ C C L L
255
+ 0.0 ┤ C C C L L L L
256
+ └─────────────────────────
257
+ t=0 t=250 t=500 t=750 t=1000
258
+
259
+ L = Linear schedule (drops fast mid-range)
260
+ C = Cosine schedule (smooth decay, retains signal longer)
261
+
262
+ Key: Cosine giữ signal lâu hơn → better generation quality
263
+ </code></pre>
264
+
265
+ <blockquote><p><strong>Exam tip:</strong> Câu hỏi sẽ hỏi "tại sao cosine schedule tốt hơn linear?" — Đáp: vì cosine giữ signal lâu hơn ở timestep trung bình, tránh <strong>information destruction</strong> quá sớm. Với linear, ᾱ_{T/2} ≈ 0.05 nghĩa là 95% signal đã mất ở giữa quá trình.</p></blockquote>
266
+
267
+ <h2 id="reverse-diffusion">4. Reverse Diffusion Process</h2>
268
+
269
+ <h3 id="reverse-goal">4.1 Mục tiêu: học phân phối ngược</h3>
270
+
271
+ <p><strong>Reverse diffusion</strong> là quá trình ngược lại — bắt đầu từ pure noise x_T ~ N(0, I) và dần dần denoise về ảnh sạch x_0. Đây là phần <strong>learned</strong> — U-Net sẽ học:</p>
272
+
273
+ <pre><code class="language-text">
274
+ p_θ(x_{t-1} | x_t) = N(x_{t-1}; μ_θ(x_t, t), σ²_t · I)
275
+ ▲ predicted mean ▲ fixed variance
276
+ </code></pre>
277
+
278
+ <p>Thay vì dự đoán mean μ trực tiếp, DDPM chọn cách elegant hơn: <strong>model dự đoán noise ε</strong> mà đã được thêm vào ảnh. Từ ε̂ predicted, ta suy ra mean:</p>
279
+
280
+ <pre><code class="language-text">
281
+ μ_θ(x_t, t) = ────────── · ( x_t − ──────────── · ε_θ(x_t, t) )
282
+ 1 1 - α_t
283
+ ─────── ──────────────
284
+ √(α_t) √(1 - ᾱ_t)
285
+
286
+ Viết gọn:
287
+ 1 (1 - α_t)
288
+ μ_θ(x_t, t) = ───────── · (x_t − ─────────── · ε_θ(x_t, t))
289
+ √(α_t) √(1 - ᾱ_t)
290
+ </code></pre>
291
+
292
+ <h3 id="sampling-algorithm">4.2 Sampling Algorithm</h3>
293
+
294
+ <p>Thuật toán sampling đi từ x_T về x_0:</p>
295
+
296
+ <pre><code class="language-text">
297
+ Algorithm: DDPM Sampling
298
+ ════════════════════════
299
+ Input: trained model ε_θ, noise schedule {β_t, α_t, ᾱ_t}
300
+
301
+ 1. Sample x_T ~ N(0, I) ← start from pure noise
302
+ 2. For t = T, T-1, ..., 1:
303
+ a. If t > 1: sample z ~ N(0, I)
304
+ Else: z = 0 ← no noise at final step
305
+ b. ε̂ = ε_θ(x_t, t) ← U-Net predicts noise
306
+ c. μ = (1/√α_t) · (x_t − ((1-α_t)/√(1-ᾱ_t)) · ε̂)
307
+ d. x_{t-1} = μ + σ_t · z ← denoise one step
308
+ where σ_t = √(β_t) ← simplified variance
309
+
310
+ 3. Return x_0
311
+ </code></pre>
312
+
313
+ <pre><code class="language-text">
314
+ Reverse Process Visualization
315
+ ═════════════════════════════
316
+
317
+ x_T (pure noise) x_0 (clean image)
318
+ ┌──────────┐ ┌──────────┐
319
+ │ ·:·:·:·: │ U-Net × T │ ████████ │
320
+ │ :·:·:·:· │ ──────────► │ ██ ██ │
321
+ │ ·:·:·:·: │ denoise │ ██ ██ │
322
+ │ :·:·:·:· │ iteratively │ ████████ │
323
+ └──────────┘ └──────────┘
324
+
325
+ Step-by-step (T=1000):
326
+ t=1000 t=750 t=500 t=250 t=0
327
+ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
328
+ │ ···· │ → │ ░··░ │ → │ ░▓▓░ │ → │ ▓██▓ │ → │ ████ │
329
+ │ ···· │ │ ·░░· │ │ ▓░░▓ │ │ █▓▓█ │ │ █ █ │
330
+ │ ···· │ │ ·░░· │ │ ▓░░▓ │ │ █▓▓█ │ │ █ █ │
331
+ │ ···· │ │ ░··░ │ │ ░▓▓░ │ │ ▓██▓ │ │ ████ │
332
+ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘
333
+ noise only structure shape details clean!
334
+ emerges forms sharpen
335
+ </code></pre>
336
+
337
+ <h3 id="reverse-code">4.3 Implementation: reverse sampling loop</h3>
338
+
339
+ <pre><code class="language-python">
340
+ @torch.no_grad()
341
+ def sample_ddpm(model, shape, schedule, device='cuda'):
342
+ """
343
+ DDPM sampling: generate images from pure noise.
344
+
345
+ Args:
346
+ model: trained U-Net noise predictor ε_θ
347
+ shape: (B, C, H, W) — output shape
348
+ schedule: dict with 'betas', 'alphas', 'alpha_bar', etc.
349
+ device: torch device
350
+ Returns:
351
+ x_0: (B, C, H, W) — generated images
352
+ """
353
+ T = len(schedule['betas'])
354
+ betas = schedule['betas'].to(device)
355
+ alphas = schedule['alphas'].to(device)
356
+ alpha_bar = schedule['alpha_bar'].to(device)
357
+ sqrt_alpha = schedule['sqrt_alpha'].to(device)
358
+ sqrt_one_minus_alpha_bar = schedule['sqrt_one_minus_alpha_bar'].to(device)
359
+
360
+ # Step 1: Start from pure Gaussian noise
361
+ x_t = torch.randn(shape, device=device)
362
+
363
+ # Step 2: Iteratively denoise from t=T-1 down to t=0
364
+ for t in reversed(range(T)):
365
+ t_batch = torch.full((shape[0],), t, device=device, dtype=torch.long)
366
+
367
+ # (a) Predict noise using U-Net
368
+ eps_pred = model(x_t, t_batch)
369
+
370
+ # (b) Compute predicted mean μ_θ
371
+ # μ = (1/√α_t) * (x_t - (1-α_t)/√(1-ᾱ_t) * ε̂)
372
+ coeff_xt = 1.0 / sqrt_alpha[t]
373
+ coeff_eps = (1.0 - alphas[t]) / sqrt_one_minus_alpha_bar[t]
374
+ mu = coeff_xt * (x_t - coeff_eps * eps_pred)
375
+
376
+ # (c) Add noise (except at t=0)
377
+ if t > 0:
378
+ sigma = torch.sqrt(betas[t])
379
+ z = torch.randn_like(x_t)
380
+ x_t = mu + sigma * z
381
+ else:
382
+ x_t = mu # final step: no noise added
383
+
384
+ return x_t
385
+ </code></pre>
386
+
387
+ <blockquote><p><strong>Exam tip:</strong> Hai sai lầm phổ biến nhất trong sampling loop: (1) quên <code>@torch.no_grad()</code> → tốn VRAM gấp 3-4x, OOM crash. (2) Thêm noise ở bước t=0 → ảnh output bị noisy. Luôn check <code>if t > 0</code> trước khi thêm z.</p></blockquote>
388
+
389
+ <h2 id="training-objective">5. Training Objective: Simplified ELBO Loss</h2>
390
+
391
+ <h3 id="elbo-intuition">5.1 Từ ELBO đến Simplified Loss</h3>
392
+
393
+ <p>Về mặt lý thuyết, DDPM optimize <strong>variational lower bound (ELBO)</strong> của log-likelihood. Tuy nhiên, Ho et al. phát hiện rằng một <strong>simplified loss</strong> hoạt động tốt hơn trong thực tế:</p>
394
+
395
+ <pre><code class="language-text">
396
+ Full ELBO Loss (lý thuyết):
397
+ L_vlb = L_0 + L_1 + ... + L_{T-1} + L_T
398
+ = ∑_t KL(q(x_{t-1}|x_t,x_0) || p_θ(x_{t-1}|x_t))
399
+
400
+ Simplified Loss (thực tế — DDPM paper):
401
+ L_simple = E_{t ~ U{1,T}, x_0, ε} [ || ε − ε_θ(x_t, t) ||² ]
402
+
403
+ Ý nghĩa:
404
+ - Sample timestep t ngẫu nhiên
405
+ - Tạo x_t từ x_0 via forward diffusion
406
+ - U-Net dự đoán noise ε̂ = ε_θ(x_t, t)
407
+ - Loss = MSE giữa noise thật (ε) và noise dự đoán (ε̂)
408
+ </code></pre>
409
+
410
+ <p>Đây chính là lý do tại sao bạn cần trả về <code>noise</code> từ <code>forward_diffusion()</code> — nó là <strong>ground truth label</strong> cho training.</p>
411
+
412
+ <h3 id="training-algorithm">5.2 Training Algorithm</h3>
413
+
414
+ <pre><code class="language-text">
415
+ Algorithm: DDPM Training
416
+ ════════════════════════
417
+ Repeat until convergence:
418
+ 1. Sample x_0 ~ q(x_0) ← batch from dataset
419
+ 2. Sample t ~ Uniform({1, ..., T}) ← random timestep per sample
420
+ 3. Sample ε ~ N(0, I) ← target noise
421
+ 4. Compute x_t = √(ᾱ_t)·x_0 + √(1−ᾱ_t)·ε ← forward diffusion
422
+ 5. Compute ε̂ = ε_θ(x_t, t) ← U-Net predicts noise
423
+ 6. Loss = MSE(ε, ε̂) ← compare real vs predicted noise
424
+ 7. Backprop & update θ
425
+ </code></pre>
426
+
427
+ <h3 id="training-code">5.3 Implementation: Complete Training Loop</h3>
428
+
429
+ <pre><code class="language-python">
430
+ def train_ddpm(model, dataloader, schedule, epochs=100, lr=2e-4, device='cuda'):
431
+ """
432
+ Full DDPM training loop.
433
+
434
+ Args:
435
+ model: U-Net noise predictor
436
+ dataloader: yields (images, labels) batches
437
+ schedule: precomputed noise schedule dict
438
+ epochs: number of training epochs
439
+ lr: learning rate
440
+ device: torch device
441
+ """
442
+ optimizer = torch.optim.Adam(model.parameters(), lr=lr)
443
+ loss_fn = nn.MSELoss()
444
+ T = len(schedule['betas'])
445
+
446
+ sqrt_alpha_bar = schedule['sqrt_alpha_bar'].to(device)
447
+ sqrt_one_minus_alpha_bar = schedule['sqrt_one_minus_alpha_bar'].to(device)
448
+
449
+ model.train()
450
+ for epoch in range(epochs):
451
+ epoch_loss = 0.0
452
+ for batch_idx, (x_0, _) in enumerate(dataloader):
453
+ x_0 = x_0.to(device)
454
+ B = x_0.shape[0]
455
+
456
+ # Step 2: Sample random timesteps
457
+ t = torch.randint(0, T, (B,), device=device)
458
+
459
+ # Steps 3-4: Forward diffusion (sample noise + compute x_t)
460
+ x_t, noise = forward_diffusion(
461
+ x_0, t, sqrt_alpha_bar, sqrt_one_minus_alpha_bar
462
+ )
463
+
464
+ # Step 5: Predict noise
465
+ noise_pred = model(x_t, t)
466
+
467
+ # Step 6: Compute loss
468
+ loss = loss_fn(noise_pred, noise)
469
+
470
+ # Step 7: Backprop
471
+ optimizer.zero_grad()
472
+ loss.backward()
473
+ optimizer.step()
474
+
475
+ epoch_loss += loss.item()
476
+
477
+ avg_loss = epoch_loss / len(dataloader)
478
+ if (epoch + 1) % 10 == 0:
479
+ print(f"Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.4f}")
480
+ </code></pre>
481
+
482
+ <table>
483
+ <thead>
484
+ <tr><th>Training Component</th><th>Vai trò</th><th>Code tương ứng</th></tr>
485
+ </thead>
486
+ <tbody>
487
+ <tr><td>x_0 from dataset</td><td>Clean image input</td><td><code>x_0 = batch[0].to(device)</code></td></tr>
488
+ <tr><td>t ~ Uniform</td><td>Random timestep</td><td><code>torch.randint(0, T, (B,))</code></td></tr>
489
+ <tr><td>ε ~ N(0,I)</td><td>Target noise</td><td><code>torch.randn_like(x_0)</code></td></tr>
490
+ <tr><td>x_t via reparameterization</td><td>Noisy image</td><td><code>forward_diffusion(...)</code></td></tr>
491
+ <tr><td>ε_θ(x_t, t)</td><td>U-Net prediction</td><td><code>model(x_t, t)</code></td></tr>
492
+ <tr><td>MSE(ε, ε̂)</td><td>Simplified loss</td><td><code>nn.MSELoss()(pred, target)</code></td></tr>
493
+ </tbody>
494
+ </table>
495
+
496
+ <blockquote><p><strong>Exam tip:</strong> Trong assessment, bạn sẽ phải viết training loop từ đầu. Thứ tự các bước là critical: forward diffusion → predict → loss → backprop. Nếu bạn đặt <code>optimizer.zero_grad()</code> sai vị trí hoặc quên <code>loss.backward()</code>, model sẽ không học — và bạn mất điểm.</p></blockquote>
497
+
498
+ <h2 id="cfg">6. Classifier-Free Diffusion Guidance (CFG)</h2>
499
+
500
+ <h3 id="cfg-concept">6.1 Conditional Generation và CFG</h3>
501
+
502
+ <p>DDPM vanilla tạo ảnh không điều kiện (unconditional). Để tạo ảnh theo điều kiện (class label, text prompt), ta cần <strong>conditional generation</strong>. <strong>Classifier-Free Guidance (CFG)</strong> là phương pháp elegant nhất:</p>
503
+
504
+ <pre><code class="language-text">
505
+ Classifier-Free Guidance
506
+ ═════════════════════════
507
+
508
+ Training: Model nhận condition c, nhưng randomly drop c → ∅ với xác suất p_uncond
509
+ ┌──────────────────────────────────────┐
510
+ │ if random() < p_uncond (e.g. 0.1): │
511
+ │ c = ∅ (null / empty) │ ← 10% unconditional
512
+ │ ε̂ = ε_θ(x_t, t, c) │
513
+ └──────────────────────────────────────┘
514
+
515
+ Inference: Combine conditional & unconditional predictions
516
+ ┌──────────────────────────────────────────────────────────────┐
517
+ │ ε̂_uncond = ε_θ(x_t, t, ∅) ← unconditional pred │
518
+ │ ε̂_cond = ε_θ(x_t, t, c) ← conditional pred │
519
+ │ │
520
+ │ ε̂_guided = ε̂_uncond + w · (ε̂_cond − ε̂_uncond) │
521
+ │ ▲ ▲ guidance direction │
522
+ │ baseline amplified by scale w │
523
+ └──────────────────────────────────────────────────────────────┘
524
+
525
+ w = guidance scale:
526
+ w = 1.0 → standard conditional (no guidance)
527
+ w = 7.5 → typical value (Stable Diffusion default)
528
+ w = 20 → very strong guidance → faithful but less diverse
529
+ w = 0.0 → purely unconditional
530
+ </code></pre>
531
+
532
+ <h3 id="cfg-tradeoff">6.2 Guidance Scale Trade-off</h3>
533
+
534
+ <table>
535
+ <thead>
536
+ <tr><th>Guidance Scale w</th><th>Quality</th><th>Diversity</th><th>Condition Fidelity</th><th>Use case</th></tr>
537
+ </thead>
538
+ <tbody>
539
+ <tr><td>0.0</td><td>Thấp</td><td>Rất cao</td><td>Không (unconditional)</td><td>Exploration</td></tr>
540
+ <tr><td>1.0</td><td>Trung bình</td><td>Cao</td><td>Chuẩn</td><td>No guidance</td></tr>
541
+ <tr><td>3.0 – 5.0</td><td>Tốt</td><td>Trung bình</td><td>Tốt</td><td>Balanced generation</td></tr>
542
+ <tr><td>7.0 – 8.5</td><td>Rất tốt</td><td>Thấp hơn</td><td>Rất tốt</td><td>Default Stable Diffusion</td></tr>
543
+ <tr><td>15.0 – 20.0</td><td>Oversaturated</td><td>Rất thấp</td><td>Quá mức</td><td>Artistic, stylized</td></tr>
544
+ </tbody>
545
+ </table>
546
+
547
+ <h3 id="cfg-code">6.3 Implementation: CFG Training & Sampling</h3>
548
+
549
+ <pre><code class="language-python">
550
+ def train_ddpm_cfg(model, dataloader, schedule, epochs=100,
551
+ lr=2e-4, p_uncond=0.1, num_classes=10, device='cuda'):
552
+ """
553
+ DDPM training with Classifier-Free Guidance.
554
+ Model takes (x_t, t, class_label) as input.
555
+ During training, randomly replace class_label with null_class.
556
+ """
557
+ optimizer = torch.optim.Adam(model.parameters(), lr=lr)
558
+ loss_fn = nn.MSELoss()
559
+ T = len(schedule['betas'])
560
+ null_class = num_classes # use num_classes as "no class" token
561
+
562
+ sqrt_ab = schedule['sqrt_alpha_bar'].to(device)
563
+ sqrt_omab = schedule['sqrt_one_minus_alpha_bar'].to(device)
564
+
565
+ model.train()
566
+ for epoch in range(epochs):
567
+ for x_0, labels in dataloader:
568
+ x_0, labels = x_0.to(device), labels.to(device)
569
+ B = x_0.shape[0]
570
+
571
+ # Random timestep
572
+ t = torch.randint(0, T, (B,), device=device)
573
+
574
+ # CFG: randomly drop condition
575
+ mask = torch.rand(B, device=device) < p_uncond
576
+ labels_cfg = labels.clone()
577
+ labels_cfg[mask] = null_class # replace with null token
578
+
579
+ # Forward diffusion
580
+ x_t, noise = forward_diffusion(x_0, t, sqrt_ab, sqrt_omab)
581
+
582
+ # Predict noise (conditioned on possibly-null label)
583
+ noise_pred = model(x_t, t, labels_cfg)
584
+
585
+ # Loss
586
+ loss = loss_fn(noise_pred, noise)
587
+ optimizer.zero_grad()
588
+ loss.backward()
589
+ optimizer.step()
590
+
591
+
592
+ @torch.no_grad()
593
+ def sample_ddpm_cfg(model, shape, schedule, class_label, guidance_scale=7.5,
594
+ num_classes=10, device='cuda'):
595
+ """
596
+ DDPM sampling with Classifier-Free Guidance.
597
+
598
+ ε̂_guided = ε̂_uncond + w * (ε̂_cond - ε̂_uncond)
599
+
600
+ Args:
601
+ class_label: (B,) — target class for each sample
602
+ guidance_scale: w — higher = more faithful to condition
603
+ """
604
+ T = len(schedule['betas'])
605
+ betas = schedule['betas'].to(device)
606
+ alphas = schedule['alphas'].to(device)
607
+ alpha_bar = schedule['alpha_bar'].to(device)
608
+ sqrt_alpha = schedule['sqrt_alpha'].to(device)
609
+ sqrt_omab = schedule['sqrt_one_minus_alpha_bar'].to(device)
610
+ null_class = num_classes
611
+
612
+ x_t = torch.randn(shape, device=device)
613
+ class_label = class_label.to(device)
614
+ null_label = torch.full_like(class_label, null_class)
615
+
616
+ for t in reversed(range(T)):
617
+ t_batch = torch.full((shape[0],), t, device=device, dtype=torch.long)
618
+
619
+ # Two forward passes: conditional + unconditional
620
+ eps_cond = model(x_t, t_batch, class_label) # ε_θ(x_t, t, c)
621
+ eps_uncond = model(x_t, t_batch, null_label) # ε_θ(x_t, t, ∅)
622
+
623
+ # CFG formula
624
+ eps_guided = eps_uncond + guidance_scale * (eps_cond - eps_uncond)
625
+
626
+ # Compute mean
627
+ coeff_xt = 1.0 / sqrt_alpha[t]
628
+ coeff_eps = (1.0 - alphas[t]) / sqrt_omab[t]
629
+ mu = coeff_xt * (x_t - coeff_eps * eps_guided)
630
+
631
+ # Denoise step
632
+ if t > 0:
633
+ sigma = torch.sqrt(betas[t])
634
+ x_t = mu + sigma * torch.randn_like(x_t)
635
+ else:
636
+ x_t = mu
637
+
638
+ return x_t
639
+ </code></pre>
640
+
641
+ <blockquote><p><strong>Exam tip:</strong> CFG cần <strong>hai lần forward pass</strong> mỗi sampling step — một lần conditional, một lần unconditional. Đây là lý do sampling với CFG chậm gấp đôi. Trong DLI lab, nếu bạn chỉ chạy một forward pass thì guidance sẽ không có tác dụng — output giống unconditional.</p></blockquote>
642
+
643
+ <h2 id="cheat-sheet">7. Cheat Sheet: Tổng hợp công thức DDPM</h2>
644
+
645
+ <table>
646
+ <thead>
647
+ <tr><th>Công thức</th><th>Ý nghĩa</th><th>Dùng ở đâu</th></tr>
648
+ </thead>
649
+ <tbody>
650
+ <tr><td>x_t = √ᾱ_t · x_0 + √(1−ᾱ_t) · ε</td><td>Forward diffusion (closed-form)</td><td>Training: tạo x_t từ x_0</td></tr>
651
+ <tr><td>ε̂ = ε_θ(x_t, t)</td><td>U-Net dự đoán noise</td><td>Training: output | Sampling: denoise</td></tr>
652
+ <tr><td>L = MSE(ε, ε̂)</td><td>Simplified ELBO loss</td><td>Training: compute loss</td></tr>
653
+ <tr><td>μ = (1/√α_t)(x_t − (1−α_t)/√(1−ᾱ_t) · ε̂)</td><td>Predicted mean for reverse step</td><td>Sampling: compute μ_θ</td></tr>
654
+ <tr><td>x_{t−1} = μ + √β_t · z</td><td>Sampling step (z=0 khi t=0)</td><td>Sampling: denoise one step</td></tr>
655
+ <tr><td>ε̂ = ε̂_∅ + w(ε̂_c − ε̂_∅)</td><td>CFG guidance formula</td><td>Conditional sampling</td></tr>
656
+ </tbody>
657
+ </table>
658
+
659
+ <pre><code class="language-text">
660
+ DDPM Pipeline Summary
661
+ ═════════════════════
662
+
663
+ ┌─────────────────────────────────────────────────────────┐
664
+ │ TRAINING │
665
+ │ │
666
+ │ x_0 ──[forward_diffusion]──► x_t ──[U-Net]──► ε̂ │
667
+ │ │ ↑ │ │
668
+ │ └─── t,ε (random) ───────────────── MSE(ε, ε̂) │ │
669
+ │ │ │
670
+ │ backprop │
671
+ └─────────────────────────────────────────────────────────┘
672
+
673
+ ┌─────────────────────────────────────────────────────────┐
674
+ │ SAMPLING │
675
+ │ │
676
+ │ x_T ──► [U-Net] ──► ε̂ ──► μ_θ ──► x_{T-1} │
677
+ │ │ │
678
+ │ [U-Net] ──► ε̂ ──► μ_θ ──► x_{T-2} │
679
+ │ │ │
680
+ │ ...repeat T times... │ │
681
+ │ ▼ │
682
+ │ x_0 (generated!) │
683
+ └─────────────────────────────────────────────────────────┘
684
+
685
+ ┌─────────────────────────────────────────────────────────┐
686
+ │ CFG SAMPLING │
687
+ │ │
688
+ │ At each step t: │
689
+ │ ε̂_∅ = UNet(x_t, t, null) ← unconditional │
690
+ │ ε̂_c = UNet(x_t, t, class) ← conditional │
691
+ │ ε̂ = ε̂_∅ + w · (ε̂_c − ε̂_∅) ← guided prediction │
692
+ │ x_{t-1} = denoise(x_t, ε̂) │
693
+ └─────────────────────────────────────────────────────────┘
694
+ </code></pre>
695
+
696
+ <h2 id="practice">8. Practice Questions</h2>
697
+
698
+ <p>5 coding questions — hãy tự implement trước khi xem đáp án.</p>
699
+
700
+ <p><strong>Q1: Implement forward_diffusion(x_0, t, noise_schedule) → x_t, noise</strong></p>
701
+
702
+ <p>Viết hàm <code>forward_diffusion</code> nhận một batch ảnh x_0, tensor timesteps t, và dictionary noise_schedule chứa các precomputed coefficients. Trả về x_t và noise ε đã dùng.</p>
703
+
704
+ <pre><code class="language-python">
705
+ def forward_diffusion(x_0, t, noise_schedule):
706
+ """
707
+ Args:
708
+ x_0: (B, C, H, W) — clean images, normalized to [-1, 1]
709
+ t: (B,) — integer timestep indices
710
+ noise_schedule: dict with keys:
711
+ 'sqrt_alpha_bar': (T,) tensor
712
+ 'sqrt_one_minus_alpha_bar': (T,) tensor
713
+ Returns:
714
+ x_t: (B, C, H, W) — noisy images
715
+ noise: (B, C, H, W) — the Gaussian noise added
716
+ """
717
+ # TODO: Implement forward diffusion using reparameterization trick
718
+ pass
719
+ </code></pre>
720
+
721
+ <details>
722
+ <summary>Show Answer Q1</summary>
723
+
724
+ <pre><code class="language-python">
725
+ def forward_diffusion(x_0, t, noise_schedule):
726
+ sqrt_alpha_bar = noise_schedule['sqrt_alpha_bar']
727
+ sqrt_one_minus_alpha_bar = noise_schedule['sqrt_one_minus_alpha_bar']
728
+
729
+ # Sample noise ε ~ N(0, I)
730
+ noise = torch.randn_like(x_0)
731
+
732
+ # Gather coefficients for batch and reshape for broadcasting
733
+ # (B,) → (B, 1, 1, 1)
734
+ s_ab = sqrt_alpha_bar[t].view(-1, 1, 1, 1)
735
+ s_omab = sqrt_one_minus_alpha_bar[t].view(-1, 1, 1, 1)
736
+
737
+ # Reparameterization trick:
738
+ # x_t = √(ᾱ_t) * x_0 + √(1 - ᾱ_t) * ε
739
+ x_t = s_ab * x_0 + s_omab * noise
740
+
741
+ return x_t, noise
742
+ </code></pre>
743
+
744
+ <p><em>Explanation: Ba bước key: (1) sample noise cùng shape với x_0 bằng <code>torch.randn_like</code>, (2) gather coefficients theo index t rồi reshape <code>.view(-1, 1, 1, 1)</code> để broadcast 4D, (3) áp dụng reparameterization trick. Lưu ý phải trả về cả noise vì training loop cần nó làm target cho MSE loss.</em></p>
745
+ </details>
746
+
747
+ <p><strong>Q2: Implement reverse diffusion sampling loop</strong></p>
748
+
749
+ <p>Viết hàm <code>sample_ddpm</code> tạo ảnh mới từ pure noise bằng cách lặp reverse diffusion steps. Model đã được train xong.</p>
750
+
751
+ <pre><code class="language-python">
752
+ @torch.no_grad()
753
+ def sample_ddpm(model, n_samples, img_channels, img_size, schedule, device):
754
+ """
755
+ Args:
756
+ model: trained U-Net, expects (x_t, t_batch) → predicted noise
757
+ n_samples: int — number of images to generate
758
+ img_channels: int — e.g., 1 for MNIST
759
+ img_size: int — e.g., 28
760
+ schedule: dict with 'betas', 'alphas', 'alpha_bar',
761
+ 'sqrt_alpha', 'sqrt_one_minus_alpha_bar'
762
+ Returns:
763
+ images: (n_samples, C, H, W) — generated images
764
+ """
765
+ # TODO: Implement the full DDPM sampling algorithm
766
+ pass
767
+ </code></pre>
768
+
769
+ <details>
770
+ <summary>Show Answer Q2</summary>
771
+
772
+ <pre><code class="language-python">
773
+ @torch.no_grad()
774
+ def sample_ddpm(model, n_samples, img_channels, img_size, schedule, device):
775
+ T = len(schedule['betas'])
776
+ betas = schedule['betas'].to(device)
777
+ alphas = schedule['alphas'].to(device)
778
+ sqrt_alpha = schedule['sqrt_alpha'].to(device)
779
+ sqrt_omab = schedule['sqrt_one_minus_alpha_bar'].to(device)
780
+
781
+ shape = (n_samples, img_channels, img_size, img_size)
782
+
783
+ # Start from pure noise x_T ~ N(0, I)
784
+ x_t = torch.randn(shape, device=device)
785
+
786
+ for t in reversed(range(T)):
787
+ t_batch = torch.full((n_samples,), t, device=device, dtype=torch.long)
788
+
789
+ # U-Net predicts noise
790
+ eps_pred = model(x_t, t_batch)
791
+
792
+ # Compute predicted mean:
793
+ # μ_θ = (1/√α_t) * (x_t − ((1−α_t) / √(1−ᾱ_t)) * ε̂)
794
+ coeff_xt = 1.0 / sqrt_alpha[t]
795
+ coeff_eps = (1.0 - alphas[t]) / sqrt_omab[t]
796
+ mu = coeff_xt * (x_t - coeff_eps * eps_pred)
797
+
798
+ # Sample x_{t-1}: add noise for t > 0, otherwise return mean
799
+ if t > 0:
800
+ sigma = torch.sqrt(betas[t])
801
+ z = torch.randn_like(x_t)
802
+ x_t = mu + sigma * z
803
+ else:
804
+ x_t = mu
805
+
806
+ return x_t
807
+ </code></pre>
808
+
809
+ <p><em>Explanation: Sampling loop chạy ngược từ t=T-1 về t=0. Tại mỗi step: (1) U-Net dự đoán noise ε̂, (2) tính mean μ_θ bằng công thức DDPM, (3) thêm noise z nếu t > 0 (stochastic sampling). Critical: dùng <code>@torch.no_grad()</code> để tránh tích luỹ gradient qua 1000 steps — sẽ gây OOM. Bước t=0 không thêm noise vì đó là output cuối cùng.</em></p>
810
+ </details>
811
+
812
+ <p><strong>Q3: Debug — model outputs black images</strong></p>
813
+
814
+ <p>Một sinh viên implement reverse sampling nhưng kết quả luôn ra ảnh đen (gần 0). Tìm bug trong code dưới đây:</p>
815
+
816
+ <pre><code class="language-python">
817
+ @torch.no_grad()
818
+ def buggy_sample(model, shape, schedule, device):
819
+ T = len(schedule['betas'])
820
+ betas = schedule['betas'].to(device)
821
+ alphas = schedule['alphas'].to(device)
822
+ sqrt_alpha = schedule['sqrt_alpha'].to(device)
823
+ sqrt_omab = schedule['sqrt_one_minus_alpha_bar'].to(device)
824
+
825
+ x_t = torch.randn(shape, device=device)
826
+
827
+ for t in reversed(range(T)):
828
+ t_batch = torch.full((shape[0],), t, device=device, dtype=torch.long)
829
+ eps_pred = model(x_t, t_batch)
830
+
831
+ # BUG IS HERE — find it!
832
+ coeff_xt = 1.0 / sqrt_alpha[t]
833
+ coeff_eps = (1.0 - alphas[t]) / sqrt_omab[t]
834
+ mu = coeff_xt * (x_t + coeff_eps * eps_pred) # line A
835
+
836
+ if t > 0:
837
+ sigma = torch.sqrt(betas[t])
838
+ x_t = mu + sigma * torch.randn_like(x_t)
839
+ else:
840
+ x_t = mu
841
+
842
+ return x_t
843
+ </code></pre>
844
+
845
+ <details>
846
+ <summary>Show Answer Q3</summary>
847
+
848
+ <p><strong>Bug:</strong> Dòng tính μ dùng dấu <code>+</code> thay vì <code>−</code> trước <code>coeff_eps * eps_pred</code>.</p>
849
+
850
+ <pre><code class="language-python">
851
+ # BUG (line A):
852
+ mu = coeff_xt * (x_t + coeff_eps * eps_pred) # ← WRONG: + instead of -
853
+
854
+ # FIX:
855
+ mu = coeff_xt * (x_t - coeff_eps * eps_pred) # ← CORRECT: subtract noise
856
+ </code></pre>
857
+
858
+ <p><em>Explanation: Công thức DDPM reverse mean là μ = (1/√α_t)(x_t <strong>−</strong> ((1−α_t)/√(1−ᾱ_t)) · ε̂). Dấu trừ là bản chất của "denoise" — ta trừ đi phần noise predicted. Khi dùng dấu cộng, ta thực chất <strong>thêm noise</strong> thay vì bỏ noise → qua 1000 steps, ảnh bị trung hoà (oscillate quanh 0) → output ra ảnh gần 0 (đen). Đây là bug tinh vi vì code vẫn chạy không lỗi, output vẫn đúng shape — chỉ giá trị sai.</em></p>
859
+ </details>
860
+
861
+ <p><strong>Q4: Implement CFG sampling with guidance scale</strong></p>
862
+
863
+ <p>Model đã được train với condition dropout. Viết hàm sampling có Classifier-Free Guidance.</p>
864
+
865
+ <pre><code class="language-python">
866
+ @torch.no_grad()
867
+ def sample_cfg(model, shape, schedule, class_labels, guidance_scale,
868
+ num_classes, device):
869
+ """
870
+ Args:
871
+ model: U-Net with signature model(x_t, t, class_label) → noise
872
+ shape: (B, C, H, W)
873
+ class_labels: (B,) — target class indices
874
+ guidance_scale: float w — e.g. 7.5
875
+ num_classes: int — total classes (null_class = num_classes)
876
+ Returns:
877
+ images: (B, C, H, W)
878
+ """
879
+ # TODO: Implement CFG sampling
880
+ # Hint: two forward passes per step — conditional & unconditional
881
+ pass
882
+ </code></pre>
883
+
884
+ <details>
885
+ <summary>Show Answer Q4</summary>
886
+
887
+ <pre><code class="language-python">
888
+ @torch.no_grad()
889
+ def sample_cfg(model, shape, schedule, class_labels, guidance_scale,
890
+ num_classes, device):
891
+ T = len(schedule['betas'])
892
+ betas = schedule['betas'].to(device)
893
+ alphas = schedule['alphas'].to(device)
894
+ sqrt_alpha = schedule['sqrt_alpha'].to(device)
895
+ sqrt_omab = schedule['sqrt_one_minus_alpha_bar'].to(device)
896
+ null_class = num_classes
897
+
898
+ x_t = torch.randn(shape, device=device)
899
+ class_labels = class_labels.to(device)
900
+ null_labels = torch.full_like(class_labels, null_class)
901
+
902
+ for t in reversed(range(T)):
903
+ t_batch = torch.full((shape[0],), t, device=device, dtype=torch.long)
904
+
905
+ # Two forward passes
906
+ eps_uncond = model(x_t, t_batch, null_labels) # ε_θ(x_t, t, ∅)
907
+ eps_cond = model(x_t, t_batch, class_labels) # ε_θ(x_t, t, c)
908
+
909
+ # CFG: ε̂ = ε_uncond + w * (ε_cond - ε_uncond)
910
+ eps_guided = eps_uncond + guidance_scale * (eps_cond - eps_uncond)
911
+
912
+ # Reverse step with guided noise prediction
913
+ coeff_xt = 1.0 / sqrt_alpha[t]
914
+ coeff_eps = (1.0 - alphas[t]) / sqrt_omab[t]
915
+ mu = coeff_xt * (x_t - coeff_eps * eps_guided)
916
+
917
+ if t > 0:
918
+ sigma = torch.sqrt(betas[t])
919
+ x_t = mu + sigma * torch.randn_like(x_t)
920
+ else:
921
+ x_t = mu
922
+
923
+ return x_t
924
+ </code></pre>
925
+
926
+ <p><em>Explanation: CFG sampling khác standard sampling ở chỗ tại mỗi step ta chạy <strong>hai lần U-Net</strong>: (1) unconditional với null_class, (2) conditional với class thật. Sau đó combine: ε̂ = ε̂_∅ + w·(ε̂_c − ε̂_∅). Khi w=1.0 → standard conditional (không có guidance). Khi w>1.0 → amplify sự khác biệt giữa conditional và unconditional → ảnh rõ nét hơn nhưng ít diverse. null_class thường = num_classes (index nằm ngoài class thật).</em></p>
927
+ </details>
928
+
929
+ <p><strong>Q5: Compare linear vs cosine schedule — when does ᾱ_t drop below 0.01?</strong></p>
930
+
931
+ <p>Viết code tính và so sánh: với T=1000, ᾱ_t giảm xuống dưới 0.01 tại timestep nào cho mỗi schedule? Điều này có ý nghĩa gì cho chất lượng generation?</p>
932
+
933
+ <pre><code class="language-python">
934
+ def compare_schedules(T=1000):
935
+ """
936
+ Compute alpha_bar for both linear and cosine schedules.
937
+ Find the timestep where alpha_bar drops below 0.01 for each.
938
+ Print comparison results.
939
+ """
940
+ # TODO: implement using linear_beta_schedule() and cosine_beta_schedule()
941
+ pass
942
+ </code></pre>
943
+
944
+ <details>
945
+ <summary>Show Answer Q5</summary>
946
+
947
+ <pre><code class="language-python">
948
+ import torch
949
+ import math
950
+
951
+ def linear_beta_schedule(T, beta_start=1e-4, beta_end=0.02):
952
+ return torch.linspace(beta_start, beta_end, T)
953
+
954
+ def cosine_beta_schedule(T, s=0.008):
955
+ steps = torch.arange(T + 1, dtype=torch.float32)
956
+ f_t = torch.cos(((steps / T) + s) / (1 + s) * (math.pi / 2)) ** 2
957
+ alpha_bar = f_t / f_t[0]
958
+ betas = 1 - (alpha_bar[1:] / alpha_bar[:-1])
959
+ return torch.clamp(betas, min=1e-5, max=0.999)
960
+
961
+ def compare_schedules(T=1000):
962
+ # Linear schedule
963
+ betas_lin = linear_beta_schedule(T)
964
+ alphas_lin = 1.0 - betas_lin
965
+ alpha_bar_lin = torch.cumprod(alphas_lin, dim=0)
966
+
967
+ # Cosine schedule
968
+ betas_cos = cosine_beta_schedule(T)
969
+ alphas_cos = 1.0 - betas_cos
970
+ alpha_bar_cos = torch.cumprod(alphas_cos, dim=0)
971
+
972
+ # Find where alpha_bar < 0.01
973
+ threshold = 0.01
974
+ t_lin = (alpha_bar_lin < threshold).nonzero(as_tuple=True)[0][0].item()
975
+ t_cos = (alpha_bar_cos < threshold).nonzero(as_tuple=True)[0][0].item()
976
+
977
+ print(f"Linear schedule: ᾱ_t < {threshold} at t = {t_lin}")
978
+ print(f" ᾱ at t=250: {alpha_bar_lin[250]:.4f}")
979
+ print(f" ᾱ at t=500: {alpha_bar_lin[500]:.4f}")
980
+ print(f" ᾱ at t=750: {alpha_bar_lin[750]:.6f}")
981
+ print()
982
+ print(f"Cosine schedule: ᾱ_t < {threshold} at t = {t_cos}")
983
+ print(f" ᾱ at t=250: {alpha_bar_cos[250]:.4f}")
984
+ print(f" ᾱ at t=500: {alpha_bar_cos[500]:.4f}")
985
+ print(f" ᾱ at t=750: {alpha_bar_cos[750]:.4f}")
986
+ print()
987
+ print(f"Difference: cosine giữ signal thêm {t_cos - t_lin} timesteps")
988
+
989
+ compare_schedules()
990
+ # Output (approximate):
991
+ # Linear schedule: ᾱ_t < 0.01 at t ≈ 650
992
+ # ᾱ at t=250: 0.6766
993
+ # ᾱ at t=500: 0.0473
994
+ # ᾱ at t=750: 0.000014
995
+ #
996
+ # Cosine schedule: ᾱ_t < 0.01 at t ≈ 940
997
+ # ᾱ at t=250: 0.8536
998
+ # ᾱ at t=500: 0.5000
999
+ # ᾱ at t=750: 0.1464
1000
+ #
1001
+ # Difference: cosine giữ signal thêm ~290 timesteps
1002
+ </code></pre>
1003
+
1004
+ <p><em>Explanation: Linear schedule destroy signal sớm — ᾱ_t < 0.01 quanh t≈650, nghĩa là 35% cuối của chain gần như vô ích (noise gần như pure). Cosine giữ ᾱ_t > 0.01 đến t≈940, sử dụng hiệu quả hơn toàn bộ T steps. Đặc biệt chú ý: tại t=500 (giữa chain), linear chỉ còn ᾱ≈0.05 (5% signal) trong khi cosine còn ᾱ≈0.50 (50% signal). Điều này giải thích vì sao cosine cho chất lượng generation tốt hơn — model có gradient hữu ích từ nhiều timesteps hơn, không bị wasted computation ở vùng noise thuần.</em></p>
1005
+ </details>
1006
+
1007
+ <blockquote><p><strong>Exam tip:</strong> Trong DLI assessment, câu hỏi về noise schedule thường yêu cầu bạn <strong>giải thích tại sao</strong> một schedule tốt hơn. Key insight: schedule tốt phải phân bố signal destruction <strong>đều qua tất cả timesteps</strong> — không quá nhanh (linear), không quá chậm. Cosine đạt điều này bằng cách thiết kế ᾱ_t trực tiếp thay vì β_t.</p></blockquote>