TinyDiffusion: four 2D checkpoints
Browse files- README.md +148 -0
- config.json +56 -0
- dot.safetensors +3 -0
- line.safetensors +3 -0
- moons-cosine.safetensors +3 -0
- moons-linear.safetensors +3 -0
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
|