shahfazal commited on
Commit
f1a3155
·
verified ·
1 Parent(s): 34d2d8c

TinyDiffusion: four 2D checkpoints

Browse files
README.md ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ library_name: pytorch
4
+ language:
5
+ - en
6
+ tags:
7
+ - pytorch
8
+ - safetensors
9
+ - diffusion
10
+ - ddpm
11
+ - tiny-model
12
+ - educational
13
+ - toy-dataset
14
+ ---
15
+
16
+ # TinyDiffusion
17
+
18
+ DDPM on **2D points**. Not images. The denoiser is a 24,450-parameter MLP, trained on CPU in seconds.
19
+
20
+ Sibling of [TinyNet](https://huggingface.co/shahfazal/tinynet-relu-v1), and built for the same reason:
21
+ strip everything that isn't the mechanism. The forward noising, the epsilon objective, the ancestral
22
+ sampler and the time conditioning are unchanged from the ones in Stable Diffusion. Only the denoiser
23
+ changes with the data type, and a UNet would be the largest thing on screen while having nothing to
24
+ do with diffusion. So: an MLP, and 2D points, and the entire data distribution fits on one scatter plot.
25
+
26
+ - **Code:** https://github.com/shahfazal/tiny-diffusion
27
+ - **Write-up:** https://shahfazal.com/posts/tiny-diffusion/
28
+
29
+ ## Checkpoints
30
+
31
+ | file | distribution | schedule | steps | train loss |
32
+ | -------------------------- | ------------------------------- | ---------- | ------ | ---------- |
33
+ | `dot.safetensors` | tight Gaussian at (2, 2), σ=0.1 | linear | 3,000 | 0.066 |
34
+ | `line.safetensors` | `y = x` over [-2, 2], σ=0.1 | linear | 3,000 | 0.258 |
35
+ | `moons-linear.safetensors` | two crescents (v0.1) | linear | 15,000 | 0.306 |
36
+ | `moons-cosine.safetensors` | two crescents (v0.1.1) | **cosine** | 15,000 | 0.413 |
37
+
38
+ Unconditional, one distribution per checkpoint. `moons-cosine` is the one to use: better samples than
39
+ `moons-linear` despite the worse loss (see below).
40
+
41
+ ## Three things that will bite you
42
+
43
+ Everything else is standard DDPM. These are not.
44
+
45
+ **1. `beta_T = 0.10`, not `0.02`.** The canonical `1e-4 → 0.02` range is tuned for T=1000. At T=100 it
46
+ injects a tenth of the total noise and stalls at `alpha_bar_T = 0.36`, so the forward process never
47
+ reaches `N(0, I)` while sampling still starts there. With `beta_T = 0.10`, `alpha_bar_T = 0.0056`.
48
+
49
+ **2. Match the schedule to the checkpoint.** Loading `moons-cosine` and sampling it on the linear
50
+ schedule fails silently, producing plausible-looking garbage. See `config.json`.
51
+
52
+ **3. `moons` lives in normalised space.** It was trained on `make_moons(noise=0.05)` standardised to
53
+ zero mean / unit std per axis, so generated points come out in *that* space, not raw `make_moons`
54
+ coordinates. `dot` and `line` are unnormalised.
55
+
56
+ ## Usage
57
+
58
+ No `diffusers`, no `transformers`, no `from_pretrained` — there is no library that knows this
59
+ architecture, so the module definition below *is* the API.
60
+
61
+ ```python
62
+ import math, torch, torch.nn as nn
63
+ from safetensors.torch import load_file
64
+
65
+ T = 100
66
+
67
+ class TinyDiffusion(nn.Module):
68
+ def __init__(self):
69
+ super().__init__()
70
+ self.embed = nn.Embedding(T, 32)
71
+ self.layer1 = nn.Linear(2 + 32, 128)
72
+ self.layer2 = nn.Linear(128, 128)
73
+ self.layer3 = nn.Linear(128, 2)
74
+ self.act = nn.SiLU()
75
+
76
+ def forward(self, x, t):
77
+ h = torch.cat([x, self.embed(t)], dim=1)
78
+ h = self.act(self.layer1(h))
79
+ h = self.act(self.layer2(h))
80
+ return self.layer3(h)
81
+
82
+ def linear_betas():
83
+ return torch.linspace(1e-4, 0.10, T)
84
+
85
+ def cosine_betas(s=0.008): # Nichol & Dhariwal
86
+ t = torch.linspace(0, T, T + 1) / T
87
+ ab = torch.cos((t + s) / (1 + s) * math.pi / 2) ** 2
88
+ ab = ab / ab[0]
89
+ return (1 - ab[1:] / ab[:-1]).clamp(max=0.999)
90
+
91
+ @torch.no_grad()
92
+ def sample(model, betas, n=512):
93
+ alphas, ab = 1 - betas, torch.cumprod(1 - betas, dim=0)
94
+ x = torch.randn(n, 2)
95
+ for s in reversed(range(T)):
96
+ t = torch.full((n,), s, dtype=torch.long)
97
+ eps = model(x, t)
98
+ mean = (1 / alphas[s].sqrt()) * (x - (betas[s] / (1 - ab[s]).sqrt()) * eps)
99
+ x = mean + betas[s].sqrt() * torch.randn_like(x) if s > 0 else mean
100
+ return x
101
+
102
+ model = TinyDiffusion().eval()
103
+ model.load_state_dict(load_file("moons-cosine.safetensors"))
104
+ pts = sample(model, cosine_betas()) # (512, 2)
105
+ ```
106
+
107
+ ## Architecture
108
+
109
+ `Embedding(100, 32)` → concat with the 2D point → `Linear(34, 128)` → SiLU → `Linear(128, 128)` →
110
+ SiLU → `Linear(128, 2)`. Output is epsilon, raw. 24,450 params, a third of which were the timestep
111
+ embedding before the width went to 128.
112
+
113
+ Trained with Adam (lr 1e-3, batch 256) on freshly sampled data every step — the dataset is a
114
+ generator, not an array, so memorisation is off the table and extra capacity is free.
115
+
116
+ `make_moons` returns which crescent each point came from. It's discarded: the model has no idea there
117
+ are two modes, which is what makes mode-dropping possible at all. Using it would be conditioning.
118
+
119
+ ## The loss ranks these backwards
120
+
121
+ Why `moons-cosine` ships despite the worse loss. Three seeds each:
122
+
123
+ | schedule | train loss | off-manifold | minority moon |
124
+ | -------- | ---------- | ------------ | ------------- |
125
+ | *real* | – | *0.014* | *50%* |
126
+ | linear | **0.330** | 0.046 | 47.8% |
127
+ | cosine | **0.404** | **0.038** | 48.8% |
128
+
129
+ *(off-manifold = mean distance from each generated point to the nearest real one.)*
130
+
131
+ **Cosine: 18% better samples, 22% worse loss. Every seed, no overlap.** Select on the loss curve and
132
+ you select linear, which is worse.
133
+
134
+ The floor is set by how much of `x0` is still recoverable from `x_t`. Cosine keeps signal alive
135
+ longer (SNR crosses 1 at t=49 vs t=37 for linear), so mid-trajectory the model is asked a *harder*
136
+ question and scores worse on it. The problem moved, not the quality. The same effect makes the loss
137
+ incomparable across the ladder: dot 0.06, line 0.27, moons 0.35 — all three succeeded.
138
+
139
+ ## Limitations
140
+
141
+ - 2D points. This will not generate images, by design.
142
+ - Unconditional. You can sample the moons distribution; you cannot ask for the *left* crescent.
143
+ - Samples are mildly over-dispersed (dot: σ 0.12 generated vs 0.10 real) — reverse-step error
144
+ accumulating over 100 steps.
145
+
146
+ ## License
147
+
148
+ MIT.
config.json ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "tinydiffusion",
3
+ "description": "DDPM on 2D points. Not images. The denoiser is an MLP, never a UNet.",
4
+ "architecture": {
5
+ "timestep_embedding": {
6
+ "type": "nn.Embedding",
7
+ "num_embeddings": 100,
8
+ "dim": 32
9
+ },
10
+ "layers": [
11
+ "Linear(2+32, 128)",
12
+ "SiLU",
13
+ "Linear(128, 128)",
14
+ "SiLU",
15
+ "Linear(128, 2)"
16
+ ],
17
+ "note": "Output is the predicted 2D noise, raw. No activation on the final layer.",
18
+ "params": 24450
19
+ },
20
+ "diffusion": {
21
+ "T": 100,
22
+ "objective": "epsilon (predict the noise that was added)",
23
+ "sampler": "ancestral DDPM, no noise added on the final step",
24
+ "schedules": {
25
+ "linear": {
26
+ "betas": "torch.linspace(1e-4, 0.10, 100)",
27
+ "alpha_bar_T": 0.0056,
28
+ "warning": "beta_T is 0.10, NOT the usual 0.02. The 0.02 default is tuned for T=1000; at T=100 it stalls at alpha_bar_T=0.36 and never reaches pure noise."
29
+ },
30
+ "cosine": {
31
+ "betas": "Nichol & Dhariwal: alpha_bar from a cosine curve, betas derived",
32
+ "alpha_bar_T": 0.0
33
+ }
34
+ }
35
+ },
36
+ "checkpoints": {
37
+ "dot": {
38
+ "schedule": "linear"
39
+ },
40
+ "line": {
41
+ "schedule": "linear"
42
+ },
43
+ "moons-linear": {
44
+ "schedule": "linear"
45
+ },
46
+ "moons-cosine": {
47
+ "schedule": "cosine"
48
+ }
49
+ },
50
+ "data_space": {
51
+ "dot": "Gaussian, centre (2.0, 2.0), std 0.1. NOT normalised.",
52
+ "line": "y = x, u ~ Uniform[-2, 2], perpendicular Gaussian fuzz std 0.1. NOT normalised.",
53
+ "moons": "sklearn make_moons(noise=0.05), normalised to zero mean / unit std per axis. Generated points come out in THAT space, not raw make_moons space."
54
+ },
55
+ "license": "mit"
56
+ }
dot.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:88ea4e7688b2068ae686b4124acd28ab7b9a0a54e5959ca9eab5c1674dd0deff
3
+ size 98336
line.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bc49fb34be6d674bc71f5a1f250fe48c18ca21e03756757616d190e7f55f42e8
3
+ size 98336
moons-cosine.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:407c996a9de30284fa8f531ce05b3a4546bfcf15347d1966abd844205bf8e6d6
3
+ size 98336
moons-linear.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:866b9e18703f2691546d5a2b9565ee6cef46aaf6591b794bcb75e06f98bc13bf
3
+ size 98336