Views
No views yet
| 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 |
moons-cosine is the one to use: better samples than
moons-linear despite the worse loss (see below).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.moons-cosine and sampling it on the linear
schedule fails silently, producing plausible-looking garbage. See config.json.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.diffusers, no transformers, no from_pretrained — there is no library that knows this
architecture, so the module definition below is the API.1import math, torch, torch.nn as nn
2from safetensors.torch import load_file
3
4T = 100
5
6class TinyDiffusion(nn.Module):
7 def __init__(self):
8 super().__init__()
9 self.embed = nn.Embedding(T, 32)
10 self.layer1 = nn.Linear(2 + 32, 128)
11 self.layer2 = nn.Linear(128, 128)
12 self.layer3 = nn.Linear(128, 2)
13 self.act = nn.SiLU()
14
15 def forward(self, x, t):
16 h = torch.cat([x, self.embed(t)], dim=1)
17 h = self.act(self.layer1(h))
18 h = self.act(self.layer2(h))
19 return self.layer3(h)
20
21def linear_betas():
22 return torch.linspace(1e-4, 0.10, T)
23
24def cosine_betas(s=0.008): # Nichol & Dhariwal
25 t = torch.linspace(0, T, T + 1) / T
26 ab = torch.cos((t + s) / (1 + s) * math.pi / 2) ** 2
27 ab = ab / ab[0]
28 return (1 - ab[1:] / ab[:-1]).clamp(max=0.999)
29
30@torch.no_grad()
31def sample(model, betas, n=512):
32 alphas, ab = 1 - betas, torch.cumprod(1 - betas, dim=0)
33 x = torch.randn(n, 2)
34 for s in reversed(range(T)):
35 t = torch.full((n,), s, dtype=torch.long)
36 eps = model(x, t)
37 mean = (1 / alphas[s].sqrt()) * (x - (betas[s] / (1 - ab[s]).sqrt()) * eps)
38 x = mean + betas[s].sqrt() * torch.randn_like(x) if s > 0 else mean
39 return x
40
41model = TinyDiffusion().eval()
42model.load_state_dict(load_file("moons-cosine.safetensors"))
43pts = sample(model, cosine_betas()) # (512, 2)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.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.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% |
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.