| --- |
| license: mit |
| library_name: pytorch |
| language: |
| - en |
| tags: |
| - pytorch |
| - safetensors |
| - diffusion |
| - ddpm |
| - tiny-model |
| - educational |
| - toy-dataset |
| --- |
| |
| # TinyDiffusion |
|
|
| DDPM on **2D points**. Not images. The denoiser is a 24,450-parameter MLP, trained on CPU in seconds. |
|
|
| Sibling of [TinyNet](https://huggingface.co/shahfazal/tinynet-relu-v1), and built for the same reason: |
| strip everything that isn't the mechanism. The forward noising, the epsilon objective, the ancestral |
| sampler and the time conditioning are unchanged from the ones in Stable Diffusion. Only the denoiser |
| changes with the data type, and a UNet would be the largest thing on screen while having nothing to |
| do with diffusion. So: an MLP, and 2D points, and the entire data distribution fits on one scatter plot. |
|
|
| - **Code:** https://github.com/shahfazal/tiny-diffusion |
| - **Write-up:** https://shahfazal.com/posts/tiny-diffusion/ |
|
|
| ## Checkpoints |
|
|
| | file | distribution | schedule | steps | train loss | |
| | -------------------------- | ------------------------------- | ---------- | ------ | ---------- | |
| | `dot.safetensors` | tight Gaussian at (2, 2), Ο=0.1 | linear | 3,000 | 0.066 | |
| | `line.safetensors` | `y = x` over [-2, 2], Ο=0.1 | linear | 3,000 | 0.258 | |
| | `moons-linear.safetensors` | two crescents (v0.1) | linear | 15,000 | 0.306 | |
| | `moons-cosine.safetensors` | two crescents (v0.1.1) | **cosine** | 15,000 | 0.413 | |
|
|
| Unconditional, one distribution per checkpoint. `moons-cosine` is the one to use: better samples than |
| `moons-linear` despite the worse loss (see below). |
|
|
| ## Three things that will bite you |
|
|
| Everything else is standard DDPM. These are not. |
|
|
| **1. `beta_T = 0.10`, not `0.02`.** The canonical `1e-4 β 0.02` range is tuned for T=1000. At T=100 it |
| injects a tenth of the total noise and stalls at `alpha_bar_T = 0.36`, so the forward process never |
| reaches `N(0, I)` while sampling still starts there. With `beta_T = 0.10`, `alpha_bar_T = 0.0056`. |
| |
| **2. Match the schedule to the checkpoint.** Loading `moons-cosine` and sampling it on the linear |
| schedule fails silently, producing plausible-looking garbage. See `config.json`. |
|
|
| **3. `moons` lives in normalised space.** It was trained on `make_moons(noise=0.05)` standardised to |
| zero mean / unit std per axis, so generated points come out in *that* space, not raw `make_moons` |
| coordinates. `dot` and `line` are unnormalised. |
|
|
| ## Usage |
|
|
| No `diffusers`, no `transformers`, no `from_pretrained` β there is no library that knows this |
| architecture, so the module definition below *is* the API. |
|
|
| ```python |
| import math, torch, torch.nn as nn |
| from safetensors.torch import load_file |
| |
| T = 100 |
| |
| class TinyDiffusion(nn.Module): |
| def __init__(self): |
| super().__init__() |
| self.embed = nn.Embedding(T, 32) |
| self.layer1 = nn.Linear(2 + 32, 128) |
| self.layer2 = nn.Linear(128, 128) |
| self.layer3 = nn.Linear(128, 2) |
| self.act = nn.SiLU() |
| |
| def forward(self, x, t): |
| h = torch.cat([x, self.embed(t)], dim=1) |
| h = self.act(self.layer1(h)) |
| h = self.act(self.layer2(h)) |
| return self.layer3(h) |
| |
| def linear_betas(): |
| return torch.linspace(1e-4, 0.10, T) |
| |
| def cosine_betas(s=0.008): # Nichol & Dhariwal |
| t = torch.linspace(0, T, T + 1) / T |
| ab = torch.cos((t + s) / (1 + s) * math.pi / 2) ** 2 |
| ab = ab / ab[0] |
| return (1 - ab[1:] / ab[:-1]).clamp(max=0.999) |
| |
| @torch.no_grad() |
| def sample(model, betas, n=512): |
| alphas, ab = 1 - betas, torch.cumprod(1 - betas, dim=0) |
| x = torch.randn(n, 2) |
| for s in reversed(range(T)): |
| t = torch.full((n,), s, dtype=torch.long) |
| eps = model(x, t) |
| mean = (1 / alphas[s].sqrt()) * (x - (betas[s] / (1 - ab[s]).sqrt()) * eps) |
| x = mean + betas[s].sqrt() * torch.randn_like(x) if s > 0 else mean |
| return x |
| |
| model = TinyDiffusion().eval() |
| model.load_state_dict(load_file("moons-cosine.safetensors")) |
| pts = sample(model, cosine_betas()) # (512, 2) |
| ``` |
|
|
| ## Architecture |
|
|
| `Embedding(100, 32)` β concat with the 2D point β `Linear(34, 128)` β SiLU β `Linear(128, 128)` β |
| SiLU β `Linear(128, 2)`. Output is epsilon, raw. 24,450 params, a third of which were the timestep |
| embedding before the width went to 128. |
|
|
| Trained with Adam (lr 1e-3, batch 256) on freshly sampled data every step β the dataset is a |
| generator, not an array, so memorisation is off the table and extra capacity is free. |
|
|
| `make_moons` returns which crescent each point came from. It's discarded: the model has no idea there |
| are two modes, which is what makes mode-dropping possible at all. Using it would be conditioning. |
|
|
| ## The loss ranks these backwards |
|
|
| Why `moons-cosine` ships despite the worse loss. Three seeds each: |
|
|
| | schedule | train loss | off-manifold | minority moon | |
| | -------- | ---------- | ------------ | ------------- | |
| | *real* | β | *0.014* | *50%* | |
| | linear | **0.330** | 0.046 | 47.8% | |
| | cosine | **0.404** | **0.038** | 48.8% | |
|
|
| *(off-manifold = mean distance from each generated point to the nearest real one.)* |
|
|
| **Cosine: 18% better samples, 22% worse loss. Every seed, no overlap.** Select on the loss curve and |
| you select linear, which is worse. |
|
|
| The floor is set by how much of `x0` is still recoverable from `x_t`. Cosine keeps signal alive |
| longer (SNR crosses 1 at t=49 vs t=37 for linear), so mid-trajectory the model is asked a *harder* |
| question and scores worse on it. The problem moved, not the quality. The same effect makes the loss |
| incomparable across the ladder: dot 0.06, line 0.27, moons 0.35 β all three succeeded. |
|
|
| ## Limitations |
|
|
| - 2D points. This will not generate images, by design. |
| - Unconditional. You can sample the moons distribution; you cannot ask for the *left* crescent. |
| - Samples are mildly over-dispersed (dot: Ο 0.12 generated vs 0.10 real) β reverse-step error |
| accumulating over 100 steps. |
|
|
| ## License |
|
|
| MIT. |
|
|