-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtopic_diffusion.html
More file actions
941 lines (887 loc) · 61.3 KB
/
Copy pathtopic_diffusion.html
File metadata and controls
941 lines (887 loc) · 61.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Diffusion Models — Interactive Lesson</title>
<!-- ── Shared site chrome (global top bar, search, theme, glossary) ── -->
<link rel="stylesheet" href="site-polish.css">
<!-- ── The Lens-Switcher design system ── -->
<link rel="stylesheet" href="lens-template.css">
<script src="catalog.js"></script>
<script src="theme.js" defer></script>
<script src="search.js" defer></script>
<script src="nav.js" defer></script>
<script src="glossary.js" defer></script>
<!-- Landmark-papers rail (rendered by papers.js into the global chrome) -->
<script>
window.SUITE_PAPERS_TOPIC = 'Diffusion Models';
window.SUITE_PAPERS = [
{
title: 'Denoising Diffusion Probabilistic Models (DDPM)',
authors: 'Ho, Jain, Abbeel · NeurIPS 2020',
summary: 'The paper that established the modern diffusion model framework for image generation. It showed that training a neural network to predict the noise added to an image at each diffusion step, then running the reverse process iteratively from pure Gaussian noise, produces high-quality samples that rival GANs — without adversarial training instability. DDPM is the direct ancestor of Stable Diffusion and DALL·E.',
url: 'https://arxiv.org/abs/2006.11239'
},
{
title: 'High-Resolution Image Synthesis with Latent Diffusion Models',
authors: 'Rombach, Blattmann, Lorenz, Esser, Ommer · CVPR 2022',
summary: 'Proposed running the diffusion process in a compressed latent space (via a pretrained autoencoder) rather than pixel space, making high-resolution image synthesis computationally practical. This is the direct basis of Stable Diffusion. Also introduced cross-attention conditioning that lets the model accept text prompts via CLIP or T5 embeddings at each denoising step.',
url: 'https://arxiv.org/abs/2112.10752'
}
];
</script>
<script src="papers.js" defer></script>
<!-- The page config is set inline below (window.LESSON_DATA), then the engine renders it. -->
<script src="lens-template.js" defer></script>
</head>
<body>
<!--
This page is a THIN lesson: a config + content object (window.LESSON_DATA)
consumed by lens-template.js, which renders the entire Lens-Switcher UI.
The only bespoke code is the Visual lens forward/reverse demo (mount) — a
seeded, mathematically-correct 2D point-cloud diffusion with t/T sliders,
schedule + dataset selectors, a per-step β schedule bar, and a signal (ᾱₜ)
meter. Everything else is declarative. The canvas self-manages HiDPI scaling
(lens pages don't load canvas-hidpi.js).
-->
<script>
window.LESSON_DATA = {
meta: {
docTitle: "Diffusion Models",
title: "Diffusion Models",
titleHtml: "Diffusion Models, <span class=\"grad\">through any lens</span>",
kicker: "One topic · six ways to see it",
subtitle: "How do DALL·E and Stable Diffusion turn <em>noise</em> into a picture? Pick the lens that fits how you want to learn it right now — switch any time. The same idea, re-framed for your brain.",
prev: { href: "topic_gan.html", title: "GANs" },
next: { href: "topic_rl.html", title: "Reinforcement Learning" }
},
lenses: [
/* ═══════════════ INTUITION ═══════════════ */
{
id: "intuition", icon: "🧠", name: "Intuition", sub: "what & why",
qword: "intuition", tag: "Intuition lens",
heading: "The plain-English idea",
lead: "Forget the equations for a second. Take a real image, slowly bury it in random noise over many tiny steps, then train a network to undo one step of noise at a time. To generate something new, start from pure noise and let the network denoise its way to a picture. That's basically the whole trick.",
html: `
<div class="prose">
<div class="callout note">
<span class="lbl">🎯 The one-sentence version</span>
Gradually destroy a real image into pure noise over many small steps, train a neural network to reverse each step, then generate new images by starting from random noise and letting the network denoise its way to a picture.
</div>
<div class="card full">
<h4><span class="n">1</span>The marble-in-sand analogy</h4>
<p>Imagine you have a sculpture (your clean image). You take a handful of sand and sprinkle a little on it — step 1. Sprinkle more — step 2. After 1000 such steps the sculpture is buried and indistinguishable from a random pile of sand.</p>
<p>Now imagine you could learn to reverse that process: look at a partly-buried sculpture and predict exactly which sand grains to remove. Do that 1000 times starting from a fresh random pile of sand and you sculpt something new.</p>
<div class="callout analogy">
<span class="lbl">🔖 Mapping the analogy</span>
That is diffusion. The "sand" is Gaussian noise. The "sculptor" is a neural network (a U-Net). The "1000 steps" is the noise schedule. <b>Generation = run the sculptor 1000 times on fresh sand.</b>
</div>
</div>
<div class="card full">
<h4><span class="n">2</span>Two directions are the whole architecture</h4>
<p>The <b>forward</b> pass is a fixed math recipe: repeatedly add a little Gaussian noise until the image is unrecognisable. The <b>reverse</b> pass is what the model learns: undo one noise step at a time. These two directions are the entire architecture of diffusion.</p>
<p>The training target is wonderfully simple: given a noisy image <code>xₜ</code> and the timestep <code>t</code>, <b>predict the noise ε that was added</b>. Subtract your prediction and you have a slightly-cleaner estimate. The model is trained by mean-squared-error on predicted-vs-actual noise — no discriminator, just regression.</p>
<div class="callout note">
<span class="lbl">🎲 Why Gaussian noise</span>
Both the noise we add step-by-step and the random tensor we start generation from are <b>Gaussian</b> — values from a bell curve, most landing near zero. Pure Gaussian noise looks like TV static, and that's exactly where generation begins.
</div>
</div>
<div class="card full">
<h4><span class="n">3</span>Contrast with a GAN</h4>
<p>GANs generate in a <b>single</b> forward pass through a generator and use a discriminator as an adversarial signal. Diffusion models generate <b>iteratively</b> (many steps) and are trained with a simple regression loss — no adversarial training, hence more stable but slower.</p>
</div>
<h3 class="block-h">Prerequisites — the five ideas underneath</h3>
<div class="layer-stack">
<div class="layer-row" style="border-color:rgba(88,214,255,.4);background:rgba(88,214,255,.05)">
<div class="nm" style="color:var(--accent-2)">🎲 Gaussian noise</div>
<div class="dt">Values from a bell curve — each pixel offset random but most near zero. The noise we add and the tensor we start generation from are both Gaussian; pure noise looks like static.</div>
</div>
<div class="layer-row" style="border-color:rgba(240,136,62,.4);background:rgba(240,136,62,.05)">
<div class="nm" style="color:var(--orange)">➡️ Forward vs reverse</div>
<div class="dt">Forward = a fixed recipe that repeatedly adds noise. Reverse = the learned undoing, one step at a time. These two directions are the whole architecture.</div>
</div>
<div class="layer-row" style="border-color:rgba(124,131,255,.4);background:rgba(124,131,255,.05)">
<div class="nm" style="color:var(--accent)">⏱️ Schedule & timesteps</div>
<div class="dt">The forward process is split into T steps (often T = 1000). A noise schedule (linear or cosine) controls how much noise β₁…βₜ each step adds; t is fed to the model so it knows how noisy its input is.</div>
</div>
<div class="layer-row" style="border-color:rgba(57,211,83,.4);background:rgba(57,211,83,.05)">
<div class="nm" style="color:var(--green)">🧹 Predict-and-remove</div>
<div class="dt">Given noisy xₜ and t, predict the noise ε added; subtract it for a cleaner estimate. The model — a U-Net — is trained by MSE on predicted vs actual noise. No discriminator: just regression.</div>
</div>
<div class="layer-row" style="border-color:rgba(188,140,255,.45);background:rgba(188,140,255,.05)">
<div class="nm" style="color:var(--violet)">✍️ Conditioning</div>
<div class="dt">At each denoising step the model also receives an embedding of your text prompt (CLIP or T5). Cross-attention layers in the U-Net attend to the text tokens to steer denoising toward prompt-consistent images.</div>
</div>
</div>
<h3 class="block-h">The whole pipeline, one glance</h3>
<div class="pipeline">
<div class="pipe-block conv">x₀<span class="sub">clean image</span></div>
<div class="pipe-arrow">→</div>
<div class="pipe-block pool">forward<span class="sub">add ε, step by step</span></div>
<div class="pipe-arrow">→</div>
<div class="pipe-block pool">x_T<span class="sub">pure noise</span></div>
<div class="pipe-arrow">→</div>
<div class="pipe-block conv">reverse<span class="sub">U-Net predicts ε</span></div>
<div class="pipe-arrow">→</div>
<div class="pipe-block fc">x̂₀<span class="sub">generated image</span></div>
</div>
<p style="margin-top:22px;color:var(--muted);font-size:14px;">Got the gist? Tap <b style="color:var(--accent-2)">👁️ Visual</b> to scrub a real forward/reverse process on a 2D point cloud, or <b style="color:var(--accent-2)">➗ Math</b> to see the exact closed-form noising and the MSE objective.</p>
</div>`
},
/* ═══════════════ VISUAL ═══════════════ */
{
id: "visual", icon: "👁️", name: "Visual", sub: "watch it",
qword: "visual walk-through", tag: "Visual lens",
heading: "Watch noise destroy and re-form a shape",
lead: "A seeded 2D point cloud is noised forward (add Gaussian noise via the closed-form at any t) and then denoised in reverse by analytically subtracting the known noise step-by-step. Scrub the timestep, change the noise schedule or the dataset, and watch the β schedule and remaining-signal bars track along.",
html: `
<div class="warn-box-lens callout warn" style="max-width:var(--measure)">
<span class="lbl">⚠️ What this is</span>
A 2D point-cloud demo with a seeded, mathematically-correct forward process and a scripted reverse process that analytically removes noise (using the true ε that was added, not a trained network). It faithfully illustrates the geometry and the schedule. It is <em>not</em> a trained neural-network denoiser — training a real DDPM in-browser would need GPU acceleration.
</div>
<div id="demo-wrap">
<div id="demo-buttons">
<button class="dbtn dbtn-primary" id="btn-play-fwd">▶ Forward (add noise)</button>
<button class="dbtn dbtn-accent2" id="btn-play-rev" disabled>◀ Reverse (denoise)</button>
<button class="dbtn" id="btn-reset">↻ Reset</button>
</div>
<div id="demo-canvases">
<div id="demo-main-canvas">
<canvas id="canvas-main" width="480" height="340" aria-label="Forward and reverse diffusion on a 2D point cloud"></canvas>
<div id="demo-legend">
<span><span class="legend-dot" style="background:#7c83ff"></span> Clean data points</span>
<span><span class="legend-dot" style="background:#58d6ff"></span> Current state</span>
<span><span class="legend-dot" style="background:#ff5c5c"></span> Noise contribution</span>
</div>
</div>
<div id="demo-controls">
<div class="demo-ctrl-row">
<label class="demo-ctrl-label" for="slider-t">Timestep t</label>
<input type="range" id="slider-t" min="0" max="50" value="0">
<span class="demo-ctrl-val" id="lbl-t">0</span>
</div>
<div class="demo-ctrl-row">
<label class="demo-ctrl-label" for="slider-T">T (steps)</label>
<input type="range" id="slider-T" min="10" max="50" value="50" step="5">
<span class="demo-ctrl-val" id="lbl-T">50</span>
</div>
<div class="demo-ctrl-row">
<span class="demo-ctrl-label">Schedule</span>
<select id="sel-schedule" class="demo-sel">
<option value="linear">Linear</option>
<option value="cosine" selected>Cosine</option>
</select>
</div>
<div class="demo-ctrl-row">
<span class="demo-ctrl-label">Data</span>
<select id="sel-data" class="demo-sel">
<option value="circles" selected>Concentric circles</option>
<option value="moons">Two moons</option>
<option value="grid">Grid</option>
<option value="text">Letter "D"</option>
</select>
</div>
<div id="demo-status"><b>t = 0</b> — Clean data. Scrub the slider or press Forward to add noise step-by-step.</div>
<div style="font-size:11px;color:var(--muted);margin-top:2px;padding-left:2px;">Noise schedule (β per step):</div>
<div id="schedule-vis"></div>
<div style="font-size:11px;color:var(--muted);padding-left:2px;margin-top:2px;">Current step highlighted in cyan.</div>
</div>
</div>
<!-- Signal remaining bar -->
<div class="signal-row">
<span>Signal remaining ᾱₜ:</span>
<div class="signal-track"><div id="signal-bar"></div></div>
<span id="signal-val">1.000</span>
</div>
</div>
<h3 class="block-h">What to observe</h3>
<div class="card full">
<p><b>Forward (scrub slider right or press Forward):</b> Each step the points drift away from their clean positions. Early steps barely move the points (small βₜ); later steps scatter them fast. By t = T the cloud is pure Gaussian noise — the original shape is gone. Notice the cosine schedule keeps structure longer than linear.</p>
<p><b>Reverse (press Reverse after Forward completes):</b> The animation retraces backward, analytically subtracting the known noise contribution at each step. The cloud re-forms into the original shape. This is what a trained U-Net would approximate — it just wouldn't have access to the true ε and would need to infer it.</p>
<p style="margin-bottom:0;"><b>Try:</b> switch between Linear and Cosine schedules and watch how quickly structure dissolves. Switch datasets to see noise is data-agnostic.</p>
</div>
<div class="callout good" style="max-width:var(--measure)">
<span class="lbl">✓ Why the signal bar matters</span>
The bar shows <b>ᾱₜ</b> (alpha-bar) — the fraction of the original signal still present. It starts at 1.0 (all signal) and decays toward 0 (all noise) as t → T. The cosine schedule's gentler decay is exactly why it preserves structure longer. See the closed-form that produces ᾱₜ in the <b style="color:var(--accent-2)">➗ Math</b> lens.
</div>`,
/* ── bespoke interactive: seeded forward/reverse 2D diffusion ──
Ported from the original sidebar lesson. All DOM lookups are scoped
to `root` (the lens body) so multiple lenses never collide, and the
canvas self-manages HiDPI scaling (lens pages don't load canvas-hidpi.js). */
mount: function (root, ctx) {
var canvas = root.querySelector('#canvas-main');
var c2d = canvas.getContext('2d');
var dpr = 1;
var STATE = {
T: 50, t: 0, schedule: 'cosine', dataset: 'circles', n: 160,
playing: false, direction: 'fwd', animId: null
};
/* ── Seeded PRNG (Mulberry32) ── */
function mulberry32(seed) {
return function () {
seed |= 0; seed = seed + 0x6D2B79F5 | 0;
var t = Math.imul(seed ^ seed >>> 15, 1 | seed);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
function randn(rng) {
var u1 = rng(), u2 = rng();
return Math.sqrt(-2 * Math.log(u1 + 1e-10)) * Math.cos(2 * Math.PI * u2);
}
/* ── Dataset generators ── */
function makeDataset(type, n, rng) {
var pts = [];
if (type === 'circles') {
var perRing = [n * 0.35 | 0, n * 0.4 | 0, n - (n * 0.35 | 0) - (n * 0.4 | 0)];
var radii = [0.20, 0.45, 0.70];
for (var r = 0; r < 3; r++) {
for (var i = 0; i < perRing[r]; i++) {
var angle = rng() * 2 * Math.PI;
var rad = radii[r] + randn(rng) * 0.025;
pts.push([rad * Math.cos(angle), rad * Math.sin(angle)]);
}
}
} else if (type === 'moons') {
for (var i2 = 0; i2 < n; i2++) {
var half = i2 < n / 2;
var a = (i2 % (n / 2)) / (n / 2) * Math.PI;
if (half) pts.push([Math.cos(a) * 0.6, Math.sin(a) * 0.6 - 0.1 + randn(rng) * 0.04]);
else pts.push([Math.cos(a + Math.PI) * 0.6 + 0.15, Math.sin(a + Math.PI) * 0.6 + 0.25 + randn(rng) * 0.04]);
}
} else if (type === 'grid') {
var side = Math.ceil(Math.sqrt(n));
var count = 0;
for (var gy = 0; gy < side && count < n; gy++) {
for (var gx = 0; gx < side && count < n; gx++, count++) {
pts.push([
(gx / (side - 1) - 0.5) * 1.3 + randn(rng) * 0.02,
(gy / (side - 1) - 0.5) * 1.3 + randn(rng) * 0.02
]);
}
}
} else { // text 'D'
var letterPts = [];
for (var i3 = 0; i3 < 40; i3++) letterPts.push([-0.35, (i3 / 39) * 1.2 - 0.6]);
for (var i4 = 0; i4 < 80; i4++) {
var ac = (i4 / 79 - 0.5) * Math.PI;
letterPts.push([Math.cos(ac) * 0.55, Math.sin(ac) * 0.6]);
}
for (var i5 = 0; i5 < 20; i5++) {
letterPts.push([-0.35 + i5 / 19 * 0.3, 0.60]);
letterPts.push([-0.35 + i5 / 19 * 0.3, -0.60]);
}
for (var i6 = 0; i6 < letterPts.length && pts.length < n; i6++) {
pts.push([letterPts[i6][0] + randn(rng) * 0.02, letterPts[i6][1] + randn(rng) * 0.02]);
}
while (pts.length < n) pts.push([randn(rng) * 0.01, randn(rng) * 0.01]);
}
return pts;
}
/* ── Noise schedule ── */
function computeAlphaBar(T, schedule) {
var alphaBar = new Float32Array(T + 1);
alphaBar[0] = 1.0;
if (schedule === 'linear') {
var b0 = 0.0001, b1 = 0.02;
for (var t = 1; t <= T; t++) {
var beta = b0 + (b1 - b0) * (t - 1) / (T - 1);
alphaBar[t] = alphaBar[t - 1] * (1 - beta);
}
} else { // cosine
for (var tc = 0; tc <= T; tc++) {
var s = 0.008;
var f = function (tInner) { return Math.pow(Math.cos(((tInner / T + s) / (1 + s)) * (Math.PI / 2)), 2); };
alphaBar[tc] = f(tc) / f(0);
if (alphaBar[tc] < 0) alphaBar[tc] = 0;
}
}
return alphaBar;
}
function computeBetas(alphaBar, T) {
var betas = new Float32Array(T);
for (var t = 1; t <= T; t++) {
var prev = alphaBar[t - 1];
betas[t - 1] = prev > 0 ? 1 - alphaBar[t] / prev : 0;
}
return betas;
}
/* ── Noise cache (seeded, per timestep per point) ── */
function buildNoiseCache(T, n, seed) {
var rng = mulberry32(seed);
var cache = [];
for (var t = 0; t < T; t++) {
var step = [];
for (var i = 0; i < n; i++) step.push([randn(rng), randn(rng)]);
cache.push(step);
}
return cache;
}
/* ── Closed-form x_t for all points ── */
function computeXt(x0, t, alphaBar, noiseCache) {
if (t === 0) return x0.map(function (p) { return [p[0], p[1]]; });
var sqAB = Math.sqrt(alphaBar[t]);
var sqOneMinusAB = Math.sqrt(1 - alphaBar[t]);
return x0.map(function (p, i) {
var ex = noiseCache[0][i][0];
var ey = noiseCache[0][i][1];
return [sqAB * p[0] + sqOneMinusAB * ex, sqAB * p[1] + sqOneMinusAB * ey];
});
}
function addOneStep(pts, t, alphaBar, noiseCache) {
var prevAB = alphaBar[t - 1], curAB = alphaBar[t];
var beta = prevAB > 0 ? 1 - curAB / prevAB : 0;
var sqAlpha = Math.sqrt(1 - beta), sqBeta = Math.sqrt(beta);
return pts.map(function (p, i) {
return [sqAlpha * p[0] + sqBeta * noiseCache[t][i][0],
sqAlpha * p[1] + sqBeta * noiseCache[t][i][1]];
});
}
function removeOneStep(pts, t, alphaBar, noiseCache) {
var prevAB = alphaBar[t - 1], curAB = alphaBar[t];
var beta = prevAB > 0 ? 1 - curAB / prevAB : 0;
var sqAlpha = Math.sqrt(1 - beta), sqBeta = Math.sqrt(beta);
return pts.map(function (p, i) {
return [sqAlpha > 0 ? (p[0] - sqBeta * noiseCache[t][i][0]) / sqAlpha : noiseCache[0][i][0],
sqAlpha > 0 ? (p[1] - sqBeta * noiseCache[t][i][1]) / sqAlpha : noiseCache[0][i][1]];
});
}
/* ── Canvas helpers (CSS-pixel coords; HiDPI handled via transform) ── */
var W = 480, H = 340, CX = W / 2, CY = H / 2, SCALE = Math.min(W, H) / 2.4;
function ptToCanvas(p) { return [CX + p[0] * SCALE, CY - p[1] * SCALE]; }
function drawScene(x0, xt, t, T, alphaBar) {
c2d.clearRect(0, 0, W, H);
c2d.fillStyle = '#0d1117'; c2d.fillRect(0, 0, W, H);
c2d.strokeStyle = '#1c2128'; c2d.lineWidth = 1;
for (var gx = -1.5; gx <= 1.5; gx += 0.5) {
var sx = ptToCanvas([gx, -1.5])[0];
c2d.beginPath(); c2d.moveTo(sx, 0); c2d.lineTo(sx, H); c2d.stroke();
}
for (var gy = -1.5; gy <= 1.5; gy += 0.5) {
var sy = ptToCanvas([0, gy])[1];
c2d.beginPath(); c2d.moveTo(0, sy); c2d.lineTo(W, sy); c2d.stroke();
}
var noiseAlpha = Math.min(1, t / T);
var cleanOpacity = Math.max(0.08, 1 - noiseAlpha * 0.92);
for (var ci = 0; ci < x0.length; ci++) {
var cp = ptToCanvas(x0[ci]);
c2d.beginPath(); c2d.arc(cp[0], cp[1], 3.5, 0, 2 * Math.PI);
c2d.fillStyle = 'rgba(124,131,255,' + cleanOpacity + ')'; c2d.fill();
}
for (var i = 0; i < xt.length; i++) {
var cp2 = ptToCanvas(xt[i]);
var size = 2.5 + noiseAlpha * 1.0;
c2d.beginPath(); c2d.arc(cp2[0], cp2[1], size, 0, 2 * Math.PI);
c2d.fillStyle = t === 0 ? '#7c83ff'
: 'rgba(' + (88 + Math.floor(noiseAlpha * 167)) + ',' + (214 - Math.floor(noiseAlpha * 130)) + ',' + (255 - Math.floor(noiseAlpha * 200)) + ',0.85)';
c2d.fill();
}
if (t > 0 && t < T && noiseAlpha > 0.05) {
var stride = Math.ceil(xt.length / 12);
c2d.strokeStyle = 'rgba(255,92,92,0.4)'; c2d.lineWidth = 1;
for (var ai = 0; ai < xt.length; ai += stride) {
var aa = ptToCanvas(x0[ai]), bb = ptToCanvas(xt[ai]);
c2d.beginPath(); c2d.moveTo(aa[0], aa[1]); c2d.lineTo(bb[0], bb[1]); c2d.stroke();
}
}
c2d.fillStyle = '#8b949e'; c2d.font = '11px monospace';
c2d.fillText('t = ' + t + ' / T = ' + T + ' αbar_t = ' + alphaBar[t].toFixed(3), 10, 20);
var pct = ((t / T) * 100).toFixed(0);
c2d.fillText('Noise: ' + pct + '% Signal: ' + (100 - pct) + '%', 10, 36);
}
function drawSchedule(alphaBar, T, currentT) {
var el = root.querySelector('#schedule-vis');
el.innerHTML = '';
var betas = computeBetas(alphaBar, T);
var maxB = Math.max.apply(null, Array.prototype.slice.call(betas));
for (var i = 0; i < T; i++) {
var bar = document.createElement('div');
bar.className = 'sched-bar' + (i === currentT - 1 ? ' current' : '');
bar.style.height = Math.max(4, (betas[i] / maxB * 36)) + 'px';
if (i > currentT) bar.style.opacity = '0.25';
el.appendChild(bar);
}
}
function updateStatus(t, T, alphaBar) {
var el = root.querySelector('#demo-status');
var pct = (t / T * 100).toFixed(0);
if (t === 0) {
el.innerHTML = '<b>t = 0</b> — Clean data. Scrub the slider or press <b>Forward</b> to add noise step-by-step.';
} else if (t === T) {
el.innerHTML = '<b>t = T = ' + T + '</b> — Pure Gaussian noise. αbar_T = ' + alphaBar[T].toFixed(4) + ' ≈ 0. Original structure is gone. Press <b>Reverse</b> to denoise analytically.';
} else {
el.innerHTML = '<b>t = ' + t + '</b> / T = ' + T + ' | αbar_t = <b>' + alphaBar[t].toFixed(3) + '</b> | ' + pct + '% noise added. Structure is ' + (pct < 40 ? 'mostly intact' : pct < 70 ? 'partially visible' : 'mostly lost') + '.';
}
root.querySelector('#signal-bar').style.width = (alphaBar[t] * 100) + '%';
root.querySelector('#signal-val').textContent = alphaBar[t].toFixed(3);
}
var X0 = [], Xt = [], alphaBar = [], noiseCache = [];
var rngSeed = 0xABCD1234;
function init() {
var rng = mulberry32(rngSeed + STATE.dataset.length);
X0 = makeDataset(STATE.dataset, STATE.n, rng);
alphaBar = computeAlphaBar(STATE.T, STATE.schedule);
noiseCache = buildNoiseCache(STATE.T + 1, STATE.n, rngSeed + 7);
var rng2 = mulberry32(rngSeed + 42);
for (var i = 0; i < STATE.n; i++) noiseCache[0][i] = [randn(rng2), randn(rng2)];
Xt = X0.map(function (p) { return [p[0], p[1]]; });
STATE.t = 0;
drawScene(X0, Xt, 0, STATE.T, alphaBar);
drawSchedule(alphaBar, STATE.T, 0);
updateStatus(0, STATE.T, alphaBar);
root.querySelector('#lbl-t').textContent = '0';
var st = root.querySelector('#slider-t');
st.max = STATE.T; st.value = '0';
root.querySelector('#btn-play-rev').disabled = true;
root.querySelector('#btn-play-fwd').disabled = false;
}
function stopAnim() {
STATE.playing = false;
if (STATE.animId) { clearTimeout(STATE.animId); STATE.animId = null; }
}
function animForward() {
if (!STATE.playing || STATE.t >= STATE.T) {
STATE.playing = false;
root.querySelector('#btn-play-fwd').textContent = '▶ Forward (add noise)';
root.querySelector('#btn-play-rev').disabled = (STATE.t < STATE.T);
return;
}
STATE.t++;
Xt = addOneStep(Xt, STATE.t, alphaBar, noiseCache);
root.querySelector('#slider-t').value = STATE.t;
root.querySelector('#lbl-t').textContent = STATE.t;
drawScene(X0, Xt, STATE.t, STATE.T, alphaBar);
drawSchedule(alphaBar, STATE.T, STATE.t);
updateStatus(STATE.t, STATE.T, alphaBar);
var delay = ctx.isReducedMotion ? 200 : Math.max(20, 100 - STATE.T * 0.5);
STATE.animId = setTimeout(function () { requestAnimationFrame(animForward); }, delay);
}
function animReverse() {
if (!STATE.playing || STATE.t <= 0) {
STATE.playing = false;
root.querySelector('#btn-play-rev').textContent = '◀ Reverse (denoise)';
root.querySelector('#btn-play-fwd').disabled = false;
root.querySelector('#btn-play-rev').disabled = (STATE.t < STATE.T);
return;
}
Xt = removeOneStep(Xt, STATE.t, alphaBar, noiseCache);
STATE.t--;
root.querySelector('#slider-t').value = STATE.t;
root.querySelector('#lbl-t').textContent = STATE.t;
drawScene(X0, Xt, STATE.t, STATE.T, alphaBar);
drawSchedule(alphaBar, STATE.T, STATE.t);
updateStatus(STATE.t, STATE.T, alphaBar);
var delay = ctx.isReducedMotion ? 200 : Math.max(20, 100 - STATE.T * 0.5);
STATE.animId = setTimeout(function () { requestAnimationFrame(animReverse); }, delay);
}
/* ── wiring ── */
root.querySelector('#slider-t').addEventListener('input', function () {
if (STATE.playing) stopAnim();
var tNew = parseInt(this.value, 10);
root.querySelector('#lbl-t').textContent = tNew;
Xt = computeXt(X0, tNew, alphaBar, noiseCache);
STATE.t = tNew;
drawScene(X0, Xt, tNew, STATE.T, alphaBar);
drawSchedule(alphaBar, STATE.T, tNew);
updateStatus(tNew, STATE.T, alphaBar);
root.querySelector('#btn-play-rev').disabled = tNew < STATE.T;
root.querySelector('#btn-play-fwd').disabled = tNew >= STATE.T;
});
root.querySelector('#slider-T').addEventListener('input', function () {
STATE.T = parseInt(this.value, 10);
root.querySelector('#lbl-T').textContent = STATE.T;
init();
});
root.querySelector('#sel-schedule').addEventListener('change', function () {
STATE.schedule = this.value; init();
});
root.querySelector('#sel-data').addEventListener('change', function () {
STATE.dataset = this.value; init();
});
root.querySelector('#btn-play-fwd').addEventListener('click', function () {
if (STATE.playing) { stopAnim(); this.textContent = '▶ Forward (add noise)'; return; }
if (STATE.t >= STATE.T) return;
STATE.direction = 'fwd'; STATE.playing = true;
this.textContent = '⏸ Pause';
root.querySelector('#btn-play-rev').disabled = true;
animForward();
});
root.querySelector('#btn-play-rev').addEventListener('click', function () {
if (STATE.playing) { stopAnim(); this.textContent = '◀ Reverse (denoise)'; return; }
if (STATE.t <= 0) return;
STATE.direction = 'rev'; STATE.playing = true;
this.textContent = '⏸ Pause';
root.querySelector('#btn-play-fwd').disabled = true;
animReverse();
});
root.querySelector('#btn-reset').addEventListener('click', function () {
stopAnim();
root.querySelector('#btn-play-fwd').textContent = '▶ Forward (add noise)';
root.querySelector('#btn-play-rev').textContent = '◀ Reverse (denoise)';
init();
});
/* ── HiDPI sizing (self-managed; lens pages don't include canvas-hidpi.js) ── */
function resize() {
var shell = canvas.parentElement;
var cssW = Math.min(480, Math.max(260, shell.clientWidth));
var cssH = Math.round(cssW * (340 / 480));
W = cssW; H = cssH; CX = W / 2; CY = H / 2; SCALE = Math.min(W, H) / 2.4;
dpr = window.devicePixelRatio || 1;
canvas.width = Math.round(cssW * dpr);
canvas.height = Math.round(cssH * dpr);
canvas.style.width = cssW + 'px';
canvas.style.height = cssH + 'px';
c2d.setTransform(dpr, 0, 0, dpr, 0, 0);
drawScene(X0, Xt, STATE.t, STATE.T, alphaBar);
}
window.addEventListener('resize', function () {
var p = document.getElementById('panel-visual');
if (p && p.classList.contains('active')) resize();
});
init();
resize();
}
},
/* ═══════════════ MATH ═══════════════ */
{
id: "math", icon: "➗", name: "Math", sub: "the formula",
qword: "math", tag: "Math lens",
heading: "The arithmetic, exactly",
lead: "Diffusion rests on a fixed forward noising process (with a one-line closed-form shortcut), a single MSE training objective, and a reverse sampler. Everything the demo does is those equations — plus the two engineering wins (latent space, classifier-free guidance) that make DALL·E and Stable Diffusion practical.",
html: `
<div class="prose">
<h3 class="block-h">Forward diffusion — a Markov chain</h3>
<p>Each step adds a small amount of Gaussian noise controlled by βₜ:</p>
<div class="formula">
<span class="hl">q(x_t | x_{t-1})</span> = N(x_t; √(1−βₜ)·x_{t-1}, βₜ·I)
<span class="cm"># closed-form shortcut — no iteration needed:</span>
<span class="big">x_t = √ᾱₜ · x₀ + √(1−ᾱₜ) · ε</span>
<span class="cm"> ᾱₜ = product of (1−β) from step 1…t</span>
<span class="cm"> ε ~ N(0, I)</span>
</div>
<p>The closed-form lets training jump to <em>any</em> timestep t in one go — no need to simulate the full chain for each training example. That single line is exactly what the Visual lens uses to place every point at timestep t.</p>
<h3 class="block-h">The training objective</h3>
<p>Sample x₀ from data, sample t uniformly in [1, T], sample ε ~ N(0, I). Compute xₜ. Ask the U-Net to predict ε. Minimise:</p>
<div class="formula">
<span class="big">L = E[ ‖ ε − ε_θ(x_t, t) ‖² ]</span>
<span class="cm"># just mean-squared-error on the noise!</span>
</div>
<p>That is the entire training loss. No adversarial component, no perceptual loss by default. The network <code>ε_θ</code> is a <b>U-Net</b> with the timestep embedding fed in as a sinusoidal positional encoding.</p>
<h3 class="block-h">The noise schedule</h3>
<p><code>βₜ</code> controls the noise rate at step t. Two common schedules:</p>
<div class="derive">
<div class="step"><span class="n">1</span><span class="txt"><b>Linear:</b> β increases uniformly from <span class="mono">β₁ ≈ 10⁻⁴</span> to <span class="mono">βₜ ≈ 0.02</span>. Simple, but over-corrupts early pixels.</span></div>
<div class="step"><span class="n">2</span><span class="txt"><b>Cosine (improved DDPM):</b> <span class="mono">ᾱₜ</span> follows a cosine curve — noise is added more slowly at the beginning and end, which better preserves structure for more of the trajectory.</span></div>
<div class="step"><span class="n">3</span><span class="txt"><b>Step count:</b> T = 1000 in the original DDPM. Modern samplers (DDIM, DPM-Solver) cut effective steps to 20–50 at inference by taking larger steps.</span></div>
</div>
<h3 class="block-h">Latent diffusion (Stable Diffusion)</h3>
<p>Running 1000 U-Net passes on a full 512×512 image is expensive. <b>Latent Diffusion Models (LDM)</b> — the basis of Stable Diffusion — solve this in two stages:</p>
<div class="derive">
<div class="step"><span class="n">1</span><span class="txt"><b>Autoencoder:</b> a pre-trained VAE compresses the image to a small latent (e.g. <span class="mono">64×64×4</span>). Train the VAE once, freeze it.</span></div>
<div class="step"><span class="n">2</span><span class="txt"><b>Diffusion in latent space:</b> all 1000 noising/denoising steps happen in the <span class="mono">64×64×4</span> space — ~64× fewer pixels, much faster.</span></div>
</div>
<p>At generation time: denoise in the latent, then decode with the VAE decoder to get the final image.</p>
<h3 class="block-h">Classifier-free guidance (CFG) — text conditioning</h3>
<p>To steer generation toward a text prompt without a separate classifier, <b>CFG</b> trains the U-Net with the text embedding sometimes dropped (replaced by an empty token). At inference:</p>
<div class="formula">
<span class="big">ε_guided = ε_uncond + <span class="hl">w</span> · (ε_cond − ε_uncond)</span>
<span class="cm"> w = guidance scale (typical: 7–12)</span>
<span class="cm"> ε_cond = prediction given the prompt</span>
<span class="cm"> ε_uncond = prediction with empty prompt</span>
</div>
<p>This amplifies the direction in which the noise prediction changes most due to the prompt. Higher <code>w</code> = more prompt-adherent but less diverse / can over-saturate. It requires two U-Net forward passes per step — one conditioned, one not — so roughly doubles inference cost. DALL·E 3 and Stable Diffusion XL use this throughout.</p>
<div class="callout note">
<span class="lbl">📐 Where the text embedding comes from</span>
The text embedding comes from a frozen <b>CLIP</b> or <b>T5</b> encoder; cross-attention in the U-Net attends to its token sequence at every denoising step, so the prompt can influence the image at every scale.
</div>
</div>`
},
/* ═══════════════ CODE ═══════════════ */
{
id: "code", icon: "💻", name: "Code", sub: "in numpy",
qword: "code", tag: "Code lens",
heading: "DDPM, from scratch",
lead: "The whole thing is short. Here is the closed-form forward process, the one-line MSE training loop, and the reverse sampler — exactly the equations from the Math lens, made literal. The neural network is abstracted as `unet(x, t)`; everything else is just arithmetic on the schedule.",
code: {
filename: "ddpm.py",
html:
`<span class="ln"><span class="tk-kw">import</span> numpy <span class="tk-kw">as</span> np</span>
<span class="ln"></span>
<span class="ln">T <span class="tk-op">=</span> <span class="tk-num">1000</span></span>
<span class="ln">betas <span class="tk-op">=</span> np.<span class="tk-bi">linspace</span>(<span class="tk-num">1e-4</span>, <span class="tk-num">0.02</span>, T) <span class="tk-cm"># linear noise schedule</span></span>
<span class="ln">alphas <span class="tk-op">=</span> <span class="tk-num">1.0</span> <span class="tk-op">-</span> betas</span>
<span class="ln">alpha_bar <span class="tk-op">=</span> np.<span class="tk-bi">cumprod</span>(alphas) <span class="tk-cm"># ᾱ_t = Π(1-β) up to t</span></span>
<span class="ln"></span>
<span class="ln"><span class="tk-kw">def</span> <span class="tk-fn">q_sample</span>(x0, t, eps):</span>
<span class="ln"> <span class="tk-str">"""Forward: jump straight to x_t via the closed form."""</span></span>
<span class="ln"> <span class="tk-kw">return</span> np.<span class="tk-bi">sqrt</span>(alpha_bar[t]) <span class="tk-op">*</span> x0 <span class="tk-op">+</span> np.<span class="tk-bi">sqrt</span>(<span class="tk-num">1</span> <span class="tk-op">-</span> alpha_bar[t]) <span class="tk-op">*</span> eps</span>
<span class="ln"></span>
<span class="ln"><span class="tk-kw">def</span> <span class="tk-fn">train_step</span>(x0, unet):</span>
<span class="ln"> <span class="tk-str">"""One DDPM training step — MSE on the predicted noise."""</span></span>
<span class="ln"> t <span class="tk-op">=</span> np.random.<span class="tk-bi">randint</span>(<span class="tk-num">0</span>, T) <span class="tk-cm"># random timestep</span></span>
<span class="ln"> eps <span class="tk-op">=</span> np.random.<span class="tk-bi">randn</span>(<span class="tk-op">*</span>x0.shape) <span class="tk-cm"># the true noise ε ~ N(0,I)</span></span>
<span class="ln"> x_t <span class="tk-op">=</span> <span class="tk-fn">q_sample</span>(x0, t, eps)</span>
<span class="ln"> eps_pred <span class="tk-op">=</span> unet(x_t, t) <span class="tk-cm"># network predicts the noise</span></span>
<span class="ln"> <span class="tk-kw">return</span> np.<span class="tk-bi">mean</span>((eps <span class="tk-op">-</span> eps_pred) <span class="tk-op">**</span> <span class="tk-num">2</span>) <span class="tk-cm"># ← the entire loss</span></span>
<span class="ln"></span>
<span class="ln"><span class="tk-kw">def</span> <span class="tk-fn">sample</span>(unet, shape):</span>
<span class="ln"> <span class="tk-str">"""Reverse: start from pure noise, denoise step by step."""</span></span>
<span class="ln"> x <span class="tk-op">=</span> np.random.<span class="tk-bi">randn</span>(<span class="tk-op">*</span>shape) <span class="tk-cm"># x_T = pure Gaussian noise</span></span>
<span class="ln"> <span class="tk-kw">for</span> t <span class="tk-kw">in</span> <span class="tk-bi">reversed</span>(<span class="tk-bi">range</span>(T)):</span>
<span class="ln"> eps_pred <span class="tk-op">=</span> unet(x, t)</span>
<span class="ln"> <span class="tk-cm"># point estimate of x_0, then take one step back toward x_{t-1}</span></span>
<span class="ln"> mean <span class="tk-op">=</span> (x <span class="tk-op">-</span> betas[t] <span class="tk-op">/</span> np.<span class="tk-bi">sqrt</span>(<span class="tk-num">1</span> <span class="tk-op">-</span> alpha_bar[t]) <span class="tk-op">*</span> eps_pred) <span class="tk-op">/</span> np.<span class="tk-bi">sqrt</span>(alphas[t])</span>
<span class="ln"> noise <span class="tk-op">=</span> np.random.<span class="tk-bi">randn</span>(<span class="tk-op">*</span>shape) <span class="tk-kw">if</span> t <span class="tk-op">></span> <span class="tk-num">0</span> <span class="tk-kw">else</span> <span class="tk-num">0</span></span>
<span class="ln"> x <span class="tk-op">=</span> mean <span class="tk-op">+</span> np.<span class="tk-bi">sqrt</span>(betas[t]) <span class="tk-op">*</span> noise</span>
<span class="ln"> <span class="tk-kw">return</span> x <span class="tk-cm"># → a generated sample x̂_0</span></span>`,
after: `
<div class="prose">
<div class="callout note">
<span class="lbl">💡 Reading it</span>
<b>q_sample</b> is the closed-form forward process — one multiply-add to reach any timestep. <b>train_step</b> is the whole objective: corrupt with known noise, predict it, take the MSE. <b>sample</b> is the reverse loop: from pure noise, repeatedly subtract the predicted noise and step back one timestep until t = 0.
</div>
<div class="callout good">
<span class="lbl">✓ In practice</span>
<b>unet(x, t)</b> is a full U-Net with timestep + (optionally) text-embedding conditioning, trained on a GPU. Stable Diffusion runs this exact loop in a VAE <b>latent</b> space and adds classifier-free guidance — but the learning signal is still MSE on the noise, exactly as here.
</div>
<p style="color:var(--muted);font-size:14px;">Want to see this loop's geometry instead of reading it? The <b style="color:var(--accent-2)">👁️ Visual</b> lens scrubs the forward and reverse processes on a 2D cloud.</p>
</div>`
}
},
/* ═══════════════ PRACTICE ═══════════════ */
{
id: "practice", icon: "🎯", name: "Practice", sub: "test me",
qword: "practice", tag: "Practice lens",
heading: "Quick self-check",
lead: "Three questions. Pick an answer to reveal whether it's right and why. No score is kept until you've tried them — this is for you, not a grade.",
quiz: [
{
q: "What does the neural network (U-Net) actually learn to predict during diffusion training?",
opts: [
"The clean image directly, in one pass",
"The noise ε that was added to produce the noisy image xₜ",
"Whether an image is real or fake (a discriminator score)",
"The text prompt that best describes the image"
],
correct: 1,
why: "The training target is the <b>noise ε</b>. Given a noisy image xₜ and the timestep t, the U-Net predicts the noise that was added, and the loss is simply <b>MSE between predicted and actual noise</b>. Subtracting the prediction gives a cleaner estimate — no discriminator, just regression. (Predicting the real/fake score is a GAN; predicting the image directly is an equivalent but less-used parameterisation.)"
},
{
q: "Why does Stable Diffusion run the diffusion process in a compressed latent space instead of on raw pixels?",
opts: [
"Latent space makes the images sharper",
"It removes the need for a noise schedule",
"It is far cheaper — ~64× fewer values per denoising step, so 1000 U-Net passes become practical",
"Pixels can't be noised, only latents can"
],
correct: 2,
why: "Running 1000 U-Net passes on a full 512×512 image is expensive. <b>Latent Diffusion Models</b> compress the image with a pretrained VAE to e.g. 64×64×4 (~64× fewer values), do all the noising/denoising there, then decode once. This is what makes high-resolution synthesis computationally practical — the schedule and the MSE objective are unchanged."
},
{
q: "Compared with a GAN, what is the main trade-off of a diffusion model?",
opts: [
"Diffusion is less stable to train but faster to sample",
"Diffusion trains stably (simple MSE) but sampling is slow (many iterative steps)",
"Diffusion can't be conditioned on text",
"Diffusion always produces blurrier images"
],
correct: 1,
why: "Diffusion's MSE objective converges <b>stably</b> with no adversarial minimax game or mode collapse — but generation needs <b>many iterative steps</b> (T = 1000 in DDPM, ~20–50 with modern samplers), so sampling is slow versus a GAN's single forward pass. Diffusion also conditions naturally on text via CFG + cross-attention and produces state-of-the-art (not blurry) images."
}
]
},
/* ═══════════════ GO DEEPER ═══════════════ */
{
id: "deeper", icon: "📺", name: "Go Deeper", sub: "course + uses",
qword: "full course", tag: "Go-deeper lens",
heading: "Where it's used, and the authoritative course",
lead: "Ready for the full treatment? Below: where diffusion ships today (text-to-image, video, audio), how it compares to GANs and VAEs, the active research racing to make it fast, and the definitive free course with the lectures that map onto this lesson.",
html: `
<div class="gd-hero">
<div class="course">MIT 6.S184 · Flow Matching & Diffusion Models</div>
<div class="by">MIT · Peter Holderrieth, Ezra Erives · <a href="https://www.youtube.com/playlist?list=PL_1TbuIu65A9rac2dMLkvXvPS5YHtADRF" target="_blank" rel="noopener">watch the full course →</a></div>
<div class="gd-list">
<div class="gd-item">
<div class="ply">▶</div>
<div class="body">
<div class="t"><a href="https://youtu.be/KxWkA3KBdRI" target="_blank" rel="noopener">L1 · Generative AI with SDEs</a></div>
<div class="d">The stochastic-differential-equation view of generation — the continuous-time formulation behind the forward/reverse processes in this lesson.</div>
</div>
</div>
<div class="gd-item">
<div class="ply">▶</div>
<div class="body">
<div class="t"><a href="https://youtu.be/VDnM5D6wXio" target="_blank" rel="noopener">L2 · Constructing a Training Target</a></div>
<div class="d">How the denoising / flow-matching objective is built — the formal version of the MSE-on-noise loss in the Math and Code lenses.</div>
</div>
</div>
<div class="gd-item">
<div class="ply">▶</div>
<div class="body">
<div class="t"><a href="https://youtu.be/iv-5mZ_9CPY" target="_blank" rel="noopener">3Blue1Brown · How AI images work (gentle pre-read)</a></div>
<div class="d">An intuitive primer before the formal lectures — good if you want the picture first.</div>
</div>
</div>
</div>
</div>
<h3 class="block-h">Where diffusion is used in the real world</h3>
<div class="card full">
<h4>🖼 Text-to-image</h4>
<p><b>Stable Diffusion (Stability AI):</b> open-weights LDM (SD 1.5, SDXL, SD3). CLIP text encoder + cross-attention U-Net + VAE latent space; public and local-runnable. <b>DALL·E 2 (OpenAI):</b> CLIP image embedding as condition + diffusion decoder (two-stage); DALL·E 3 upgraded to a stronger text encoder and richer captions. <b>Imagen (Google):</b> cascaded diffusion — low-res model upsampled by super-resolution diffusion models, with a T5 text encoder. <b>Midjourney:</b> proprietary, diffusion-based, cloud-only.</p>
</div>
<div class="card full">
<h4>🎬 Video, audio & other domains</h4>
<p><b>Sora (OpenAI, 2024):</b> video diffusion in latent space on spatiotemporal patches using a DiT (Diffusion Transformer) backbone instead of a U-Net — same forward/reverse principle, extended to the time dimension. <b>Audio:</b> WaveGrad, AudioLM, Stable Audio, MusicGen variants — forward process over mel-spectrograms or raw waveforms. <b>3D / molecular:</b> DiffDock (protein-ligand docking), diffusion over 3D point clouds, SMILES molecule generation. <b>Image editing:</b> SDEdit, InstructPix2Pix — add noise to a real image up to a mid-timestep, then denoise conditioned on a new prompt.</p>
</div>
<div class="card full">
<h4>🌳 Position on the AI family tree</h4>
<p>On <a href="ai_family_tree.html">the Map of the Field</a>, diffusion models live under <b>Deep Learning → Generative Models</b>, alongside GANs and VAEs. All three learn a distribution over data and sample from it. The branching:</p>
<div class="layer-stack" style="margin-top:8px;">
<div class="layer-row" style="border-color:rgba(255,107,107,.4);background:rgba(255,107,107,.05)">
<div class="nm" style="color:var(--red)">GANs</div>
<div class="dt">One-shot generation, adversarial training, mode-collapse risk.</div>
</div>
<div class="layer-row" style="border-color:rgba(255,219,92,.45);background:rgba(255,219,92,.05)">
<div class="nm" style="color:var(--yellow)">VAEs</div>
<div class="dt">Encoder + decoder, learns a compressed latent, blurry outputs.</div>
</div>
<div class="layer-row" style="border-color:rgba(124,131,255,.4);background:rgba(124,131,255,.05)">
<div class="nm" style="color:var(--accent)">Diffusion</div>
<div class="dt">Iterative denoising, stable training, high quality. LDMs borrow the VAE latent space as a computational substrate.</div>
</div>
</div>
<p style="margin-top:12px;">Diffusion and autoregressive models (the GPT family) are currently the two dominant paradigms for high-quality generation.</p>
</div>
<h3 class="block-h">Diffusion vs GAN vs VAE</h3>
<div class="cmp-table-wrap">
<table class="cmp-table">
<thead>
<tr><th>Aspect</th><th>Diffusion</th><th>GAN</th><th>VAE</th></tr>
</thead>
<tbody>
<tr><td>Training stability</td><td class="good">Stable (simple MSE loss)</td><td class="bad">Unstable (mode collapse, vanishing grads)</td><td class="good">Stable</td></tr>
<tr><td>Sample quality</td><td class="good">State-of-the-art</td><td class="mid">High, mode-limited</td><td class="bad">Blurry</td></tr>
<tr><td>Sampling speed</td><td class="bad">Slow (100–1000 steps)</td><td class="good">Single forward pass</td><td class="good">Single forward pass</td></tr>
<tr><td>Mode diversity</td><td class="good">High</td><td class="bad">Often mode-drops</td><td class="mid">Moderate</td></tr>
<tr><td>Conditioning</td><td class="good">Natural (CFG, cross-attn)</td><td class="mid">Possible (cGAN)</td><td class="mid">Possible</td></tr>
</tbody>
</table>
</div>
<h3 class="block-h">Strengths & limits — the honest picture</h3>
<div class="card full">
<h4 style="color:var(--green)">Strengths</h4>
<ul style="font-size:15px; color:var(--text); line-height:1.85; padding-left:18px; margin:8px 0 0;">
<li><b>Image quality:</b> a leading generative approach — as of 2024, diffusion dominated text-to-image benchmarks on FID and human-preference studies (the frontier keeps evolving; hybrid/autoregressive approaches are active research).</li>
<li><b>Training stability:</b> a simple MSE regression on noise — no minimax game, no discriminator to balance; models converge reliably without mode collapse.</li>
<li><b>Flexible conditioning:</b> CFG + cross-attention make it easy to condition on text, class labels, edges, depth maps, or reference images (ControlNet) — swappable without retraining the core model.</li>
<li><b>Tractable likelihood:</b> a well-defined objective derived from a variational lower bound (ELBO), so training loss is interpretable and improvements are measurable.</li>
</ul>
</div>
<div class="card full">
<h4 style="color:var(--red)">Limits</h4>
<ul style="font-size:15px; color:var(--text); line-height:1.85; padding-left:18px; margin:8px 0 0;">
<li><b>Slow sampling:</b> DDPM needs T = 1000 U-Net passes per image — seconds on a GPU, minutes on CPU. A GAN generates in one pass; this is the primary real-time bottleneck.</li>
<li><b>High memory & compute:</b> each step is a full U-Net pass; with CFG (two passes/step) and 50 DDIM steps that's 100 passes. LDMs help by working in the latent, but inference is still much heavier than a GAN.</li>
<li><b>Text-to-image accuracy:</b> still struggles with counting (3 red apples), spatial relations (left of, behind), and abstract concepts — prompt engineering became a discipline because the text→image mapping is imperfect.</li>
</ul>
</div>
<div class="card full">
<h4 style="color:var(--accent)">Active research — distillation / few-step methods</h4>
<ul style="font-size:15px; color:var(--text); line-height:1.85; padding-left:18px; margin:8px 0 0;">
<li><b>DDIM (2021):</b> deterministic sampling with fewer non-Markovian steps (50 instead of 1000) — same model, different solver.</li>
<li><b>DPM-Solver, DEIS:</b> ODE solvers tuned for diffusion; 20 steps often sufficient.</li>
<li><b>Consistency Models (2023):</b> train a model to map any xₜ directly to x₀ in one step; can be distilled from a diffusion model.</li>
<li><b>Latent Consistency Models (LCM):</b> consistency distillation in latent space; 4-step generation (used in SDXL-Turbo).</li>
<li><b>SDXL-Turbo / FLUX:</b> adversarial + distillation for 1–4 step generation at near-DDPM quality.</li>
</ul>
</div>
<h3 class="block-h">The landmark papers</h3>
<div class="paper-card">
<div class="pt">Denoising Diffusion Probabilistic Models (DDPM)</div>
<div class="pa">Ho, Jain, Abbeel · NeurIPS 2020</div>
<div class="ps">Established the modern diffusion framework: train a network to predict the noise added at each step, then run the reverse process from pure Gaussian noise. Produced samples rivalling GANs without adversarial instability — the direct ancestor of Stable Diffusion and DALL·E.</div>
</div>
<div class="paper-card">
<div class="pt">High-Resolution Image Synthesis with Latent Diffusion Models</div>
<div class="pa">Rombach, Blattmann, Lorenz, Esser, Ommer · CVPR 2022</div>
<div class="ps">Ran diffusion in a compressed latent space (via a pretrained autoencoder) instead of pixel space, making high-resolution synthesis practical — the direct basis of Stable Diffusion. Also introduced cross-attention conditioning for text prompts via CLIP/T5 embeddings.</div>
</div>
<div class="callout good" style="max-width:var(--measure)">
<span class="lbl">✓ Bottom line</span>
The conceptual core is small: a fixed forward noising process, one MSE-on-noise objective, and an iterative reverse sampler. Every production system — Stable Diffusion, DALL·E, Sora — is that same recipe scaled up, moved into a latent space, and steered with classifier-free guidance.
</div>`
}
]
};
</script>
<!-- ── Diffusion demo styling (scoped to the Visual lens markup) ── -->
<style>
#demo-wrap {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
}
#demo-buttons { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
.dbtn {
padding: 8px 14px; border-radius: 7px; border: 1px solid var(--border);
cursor: pointer; font-size: 12px; font-weight: 600; transition: all 0.2s;
background: var(--surface); color: var(--text); font-family: inherit; min-height: 38px;
}
.dbtn:hover { background: #232b3a; border-color: var(--accent); color: #fff; }
.dbtn-primary { background: var(--accent); color: #fff; border-color: var(--accent); }
.dbtn-primary:hover { filter: brightness(1.08); }
.dbtn-accent2 { background: var(--accent-2); color: #04222b; border-color: var(--accent-2); }
.dbtn-accent2:hover { filter: brightness(1.08); }
.dbtn:disabled { opacity: 0.4; cursor: not-allowed; }
#demo-canvases { display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; margin-bottom: 6px; }
#demo-main-canvas { flex: 1; min-width: 260px; }
#canvas-main { display: block; border-radius: 6px; border: 1px solid var(--border); max-width: 100%; height: auto; }
#demo-controls { display: flex; flex-direction: column; gap: 10px; min-width: 220px; flex: 1; }
.demo-ctrl-row {
display: flex; align-items: center; gap: 8px; padding: 8px 12px;
background: var(--surface); border: 1px solid var(--border); border-radius: 7px;
}
.demo-ctrl-label { font-size: 10px; font-weight: 700; color: var(--accent); text-transform: uppercase; letter-spacing: 0.7px; flex-shrink: 0; min-width: 70px; }
.demo-ctrl-val { font-size: 13px; font-weight: 700; color: var(--text); min-width: 32px; font-family: var(--font-mono); text-align: right; }
.demo-ctrl-row input[type=range] { accent-color: var(--accent); flex: 1; cursor: pointer; min-width: 60px; }
.demo-sel {
flex: 1; background: var(--bg); border: 1px solid var(--border); color: var(--text);
border-radius: 5px; padding: 6px 8px; font-size: 12px; font-family: inherit; cursor: pointer; min-height: 34px;
}
#demo-status {
background: var(--surface); border: 1px solid var(--border); border-radius: 7px;
padding: 10px 14px; font-size: 12px; color: var(--text); line-height: 1.7; min-height: 52px;
}
#demo-status b { color: var(--accent-2); }
#schedule-vis {
display: flex; align-items: flex-end; gap: 2px; height: 50px;
background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
padding: 6px 10px; overflow: hidden;
}
.sched-bar { flex: 1; background: var(--accent); border-radius: 2px 2px 0 0; transition: opacity 0.2s; min-width: 2px; opacity: 0.7; }
.sched-bar.current { background: var(--accent-2); opacity: 1; box-shadow: 0 0 6px rgba(88,214,255,0.6); }
#demo-legend { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 10px; font-size: 11px; color: var(--muted); }
.legend-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 4px; vertical-align: middle; }
.signal-row { margin-top: 12px; font-size: 11px; color: var(--muted); display: flex; align-items: center; gap: 8px; }
.signal-track { flex: 1; height: 8px; background: var(--surface); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
#signal-bar { height: 100%; background: var(--accent); transition: width 0.2s; width: 100%; }
#signal-val { font-family: var(--font-mono); color: var(--accent); }
/* compare table (Go Deeper) */
.cmp-table-wrap { overflow-x: auto; margin-top: 10px; }
.cmp-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.cmp-table th { background: var(--surface-2); border: 1px solid var(--border); padding: 9px 12px; text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--muted); }
.cmp-table td { border: 1px solid var(--border); padding: 9px 12px; vertical-align: top; line-height: 1.6; color: var(--text); }
.cmp-table td:first-child { font-weight: 700; color: var(--accent-2); background: rgba(88,214,255,0.03); }
.cmp-table td.good { color: var(--green); }
.cmp-table td.bad { color: var(--red); }
.cmp-table td.mid { color: var(--yellow); }
.cmp-table tr:hover td { background: var(--surface-2); }
@media (max-width: 600px) {
#demo-wrap { padding: 14px; }
#demo-controls { width: 100%; }
}
</style>
</body>
</html>