Publish 55K parameter class-conditional diffusion model
Browse files- README.md +41 -0
- evaluation.json +43 -0
- model.safetensors +3 -0
- samples.png +0 -0
- source/app.py +64 -0
- source/model.py +60 -0
- source/requirements.txt +5 -0
- source/train.py +208 -0
README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
task_categories:
|
| 4 |
+
- unconditional-image-generation
|
| 5 |
+
tags:
|
| 6 |
+
- diffusion
|
| 7 |
+
- classifier-free-guidance
|
| 8 |
+
- tiny-model
|
| 9 |
+
- pytorch
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# PocketDiffusion
|
| 13 |
+
|
| 14 |
+
PocketDiffusion is a compact class-conditional denoising diffusion model for 8x8
|
| 15 |
+
handwritten digits. It learns to predict Gaussian noise over 50 diffusion steps and
|
| 16 |
+
uses classifier-free guidance during sampling.
|
| 17 |
+
|
| 18 |
+
The same frozen Tiny Vision classifier used for GlyphForge evaluates conditional
|
| 19 |
+
recognizability, making the VAE and diffusion results directly comparable under one
|
| 20 |
+
judge.
|
| 21 |
+
|
| 22 |
+
## Reproduce
|
| 23 |
+
|
| 24 |
+
```powershell
|
| 25 |
+
uv run python projects/tiny-vision-foundry/prepare_data.py
|
| 26 |
+
uv run python projects/pocket-diffusion/train.py
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
## Verified results
|
| 30 |
+
|
| 31 |
+
- Parameters: **55,608**
|
| 32 |
+
- Diffusion steps: **50**
|
| 33 |
+
- Training epochs: **300**
|
| 34 |
+
- Generated samples: **1,000**
|
| 35 |
+
- Selected classifier-free guidance: **3.0**
|
| 36 |
+
- Frozen-judge class fidelity: **96.20%**
|
| 37 |
+
|
| 38 |
+
Guidance search improved fidelity monotonically from 46.10% at `1.0` to 96.20% at
|
| 39 |
+
`3.0`. Per-class fidelity ranged from 83% for digit `8` to 100% for digits `0` and
|
| 40 |
+
`6`. Mean within-class pixel variance ranged from 0.0209 to 0.0456, noticeably higher
|
| 41 |
+
than the CVAE's 0.0066 to 0.0168 range under the same 100-samples-per-class protocol.
|
evaluation.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model": "PocketDiffusion",
|
| 3 |
+
"parameters": 55608,
|
| 4 |
+
"diffusion_steps": 50,
|
| 5 |
+
"epochs": 300,
|
| 6 |
+
"guidance_search": {
|
| 7 |
+
"1.0": 0.460999995470047,
|
| 8 |
+
"1.5": 0.7149999737739563,
|
| 9 |
+
"2.0": 0.8560000061988831,
|
| 10 |
+
"2.5": 0.9359999895095825,
|
| 11 |
+
"3.0": 0.9620000123977661
|
| 12 |
+
},
|
| 13 |
+
"generation": {
|
| 14 |
+
"judge_accuracy": 0.9620000123977661,
|
| 15 |
+
"judge_accuracy_by_class": {
|
| 16 |
+
"0": 1.0,
|
| 17 |
+
"1": 0.9200000166893005,
|
| 18 |
+
"2": 0.9900000095367432,
|
| 19 |
+
"3": 0.9800000190734863,
|
| 20 |
+
"4": 0.9599999785423279,
|
| 21 |
+
"5": 0.9800000190734863,
|
| 22 |
+
"6": 1.0,
|
| 23 |
+
"7": 0.9900000095367432,
|
| 24 |
+
"8": 0.8299999833106995,
|
| 25 |
+
"9": 0.9700000286102295
|
| 26 |
+
},
|
| 27 |
+
"mean_pixel_variance_by_class": {
|
| 28 |
+
"0": 0.020940322428941727,
|
| 29 |
+
"1": 0.03187673166394234,
|
| 30 |
+
"2": 0.03836727514863014,
|
| 31 |
+
"3": 0.03142565116286278,
|
| 32 |
+
"4": 0.04158321022987366,
|
| 33 |
+
"5": 0.034002821892499924,
|
| 34 |
+
"6": 0.022990796715021133,
|
| 35 |
+
"7": 0.036991652101278305,
|
| 36 |
+
"8": 0.04558330774307251,
|
| 37 |
+
"9": 0.03457583114504814
|
| 38 |
+
},
|
| 39 |
+
"samples": 1000,
|
| 40 |
+
"guidance": 3.0
|
| 41 |
+
},
|
| 42 |
+
"judge": "Tiny Vision labels-only student, 98.52% real-image test accuracy"
|
| 43 |
+
}
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f4f48f2fbc3b5dd6af4c631c3d86cff984ffbd50e624064c5cf929054bd7d813
|
| 3 |
+
size 223088
|
samples.png
ADDED
|
source/app.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
import gradio as gr
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
from model import PocketDenoiser
|
| 9 |
+
from PIL import Image
|
| 10 |
+
from safetensors.torch import load_file
|
| 11 |
+
|
| 12 |
+
STEPS = 50
|
| 13 |
+
ARTIFACT = Path(__file__).resolve().parent / "artifacts" / "pocket-diffusion"
|
| 14 |
+
MODEL = PocketDenoiser(diffusion_steps=STEPS)
|
| 15 |
+
MODEL.load_state_dict(load_file(ARTIFACT / "model.safetensors"))
|
| 16 |
+
MODEL.eval()
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def generate_digit(label: int, seed: int, guidance: float) -> Image.Image:
|
| 20 |
+
generator = torch.Generator().manual_seed(seed)
|
| 21 |
+
betas = torch.linspace(1e-4, 0.025, STEPS)
|
| 22 |
+
alphas = 1.0 - betas
|
| 23 |
+
cumulative = torch.cumprod(alphas, dim=0)
|
| 24 |
+
pixels = torch.randn(1, 64, generator=generator)
|
| 25 |
+
labels = torch.tensor([label])
|
| 26 |
+
null_labels = torch.tensor([10])
|
| 27 |
+
with torch.no_grad():
|
| 28 |
+
for step in reversed(range(STEPS)):
|
| 29 |
+
timesteps = torch.tensor([step])
|
| 30 |
+
conditional = MODEL(pixels, timesteps, labels)
|
| 31 |
+
unconditional = MODEL(pixels, timesteps, null_labels)
|
| 32 |
+
noise_prediction = unconditional + guidance * (conditional - unconditional)
|
| 33 |
+
alpha = alphas[step]
|
| 34 |
+
mean = (
|
| 35 |
+
pixels - (1 - alpha) / torch.sqrt(1 - cumulative[step]) * noise_prediction
|
| 36 |
+
) / torch.sqrt(alpha)
|
| 37 |
+
if step:
|
| 38 |
+
pixels = mean + torch.sqrt(betas[step]) * torch.randn(
|
| 39 |
+
pixels.shape,
|
| 40 |
+
generator=generator,
|
| 41 |
+
)
|
| 42 |
+
else:
|
| 43 |
+
pixels = mean
|
| 44 |
+
image = torch.clamp((pixels[0] + 1) / 2, 0, 1).reshape(8, 8).numpy()
|
| 45 |
+
array = np.clip(image * 255, 0, 255).astype(np.uint8)
|
| 46 |
+
return Image.fromarray(array, mode="L").resize(
|
| 47 |
+
(512, 512),
|
| 48 |
+
Image.Resampling.NEAREST,
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
with gr.Blocks(title="PocketDiffusion") as demo:
|
| 53 |
+
gr.Markdown("# PocketDiffusion\nGenerate a digit through 50 reverse-denoising steps.")
|
| 54 |
+
with gr.Row():
|
| 55 |
+
label = gr.Slider(0, 9, value=8, step=1, label="Digit")
|
| 56 |
+
seed = gr.Slider(0, 100_000, value=2032, step=1, label="Noise seed")
|
| 57 |
+
guidance = gr.Slider(1.0, 4.0, value=3.0, step=0.1, label="Guidance")
|
| 58 |
+
output = gr.Image(value=generate_digit(8, 2032, 3.0), label="Generated glyph")
|
| 59 |
+
button = gr.Button("Denoise", variant="primary")
|
| 60 |
+
button.click(generate_digit, inputs=[label, seed, guidance], outputs=output)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
if __name__ == "__main__":
|
| 64 |
+
demo.launch()
|
source/model.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
from torch import nn
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class PocketDenoiser(nn.Module):
|
| 8 |
+
def __init__(self, diffusion_steps: int = 50) -> None:
|
| 9 |
+
super().__init__()
|
| 10 |
+
self.diffusion_steps = diffusion_steps
|
| 11 |
+
self.time_embedding = nn.Embedding(diffusion_steps, 24)
|
| 12 |
+
self.label_embedding = nn.Embedding(11, 24)
|
| 13 |
+
self.network = nn.Sequential(
|
| 14 |
+
nn.Linear(64 + 24 + 24, 160),
|
| 15 |
+
nn.GELU(),
|
| 16 |
+
nn.Linear(160, 160),
|
| 17 |
+
nn.GELU(),
|
| 18 |
+
nn.Linear(160, 64),
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
def forward(
|
| 22 |
+
self,
|
| 23 |
+
noisy_pixels: torch.Tensor,
|
| 24 |
+
timesteps: torch.Tensor,
|
| 25 |
+
labels: torch.Tensor,
|
| 26 |
+
) -> torch.Tensor:
|
| 27 |
+
features = torch.cat(
|
| 28 |
+
[
|
| 29 |
+
noisy_pixels,
|
| 30 |
+
self.time_embedding(timesteps),
|
| 31 |
+
self.label_embedding(labels),
|
| 32 |
+
],
|
| 33 |
+
dim=1,
|
| 34 |
+
)
|
| 35 |
+
return self.network(features)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class TinyVisionJudge(nn.Module):
|
| 39 |
+
def __init__(self) -> None:
|
| 40 |
+
super().__init__()
|
| 41 |
+
self.features = nn.Sequential(
|
| 42 |
+
nn.Conv2d(1, 8, kernel_size=3, padding=1),
|
| 43 |
+
nn.GELU(),
|
| 44 |
+
nn.Conv2d(8, 8, kernel_size=3, padding=1, groups=8),
|
| 45 |
+
nn.GELU(),
|
| 46 |
+
nn.Conv2d(8, 12, kernel_size=1),
|
| 47 |
+
nn.GELU(),
|
| 48 |
+
nn.MaxPool2d(2),
|
| 49 |
+
)
|
| 50 |
+
self.classifier = nn.Sequential(
|
| 51 |
+
nn.Flatten(),
|
| 52 |
+
nn.Linear(12 * 4 * 4, 10),
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
def forward(self, pixels: torch.Tensor) -> torch.Tensor:
|
| 56 |
+
return self.classifier(self.features(pixels))
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def parameter_count(model: nn.Module) -> int:
|
| 60 |
+
return sum(parameter.numel() for parameter in model.parameters())
|
source/requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=5,<7
|
| 2 |
+
numpy>=2,<3
|
| 3 |
+
pillow>=11,<13
|
| 4 |
+
safetensors>=0.6,<1
|
| 5 |
+
torch>=2.7,<3
|
source/train.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import random
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import pandas as pd
|
| 9 |
+
import torch
|
| 10 |
+
import trackio
|
| 11 |
+
from model import PocketDenoiser, TinyVisionJudge, parameter_count
|
| 12 |
+
from PIL import Image
|
| 13 |
+
from safetensors.torch import load_file, save_file
|
| 14 |
+
from torch.nn import functional as F
|
| 15 |
+
from torch.utils.data import DataLoader, TensorDataset
|
| 16 |
+
|
| 17 |
+
PROJECT_DIR = Path(__file__).resolve().parent
|
| 18 |
+
ROOT_DIR = PROJECT_DIR.parents[1]
|
| 19 |
+
VISION_DIR = ROOT_DIR / "projects" / "tiny-vision-foundry"
|
| 20 |
+
DATA_DIR = VISION_DIR / "data"
|
| 21 |
+
JUDGE_WEIGHTS = VISION_DIR / "artifacts" / "tiny-student-scratch" / "model.safetensors"
|
| 22 |
+
ARTIFACT_DIR = PROJECT_DIR / "artifacts" / "pocket-diffusion"
|
| 23 |
+
STEPS = 50
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def seed_everything(seed: int) -> None:
|
| 27 |
+
random.seed(seed)
|
| 28 |
+
np.random.seed(seed)
|
| 29 |
+
torch.manual_seed(seed)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def load_training_data() -> DataLoader:
|
| 33 |
+
frame = pd.read_parquet(DATA_DIR / "train.parquet")
|
| 34 |
+
pixels = np.stack(frame["image"].to_numpy()).astype(np.float32) / 8.0 - 1.0
|
| 35 |
+
labels = frame["label"].to_numpy(dtype=np.int64, copy=True)
|
| 36 |
+
return DataLoader(
|
| 37 |
+
TensorDataset(torch.from_numpy(pixels), torch.from_numpy(labels)),
|
| 38 |
+
batch_size=128,
|
| 39 |
+
shuffle=True,
|
| 40 |
+
generator=torch.Generator().manual_seed(2032),
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def schedule() -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 45 |
+
betas = torch.linspace(1e-4, 0.025, STEPS)
|
| 46 |
+
alphas = 1.0 - betas
|
| 47 |
+
cumulative = torch.cumprod(alphas, dim=0)
|
| 48 |
+
return betas, alphas, cumulative
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@torch.inference_mode()
|
| 52 |
+
def sample(
|
| 53 |
+
model: PocketDenoiser,
|
| 54 |
+
labels: torch.Tensor,
|
| 55 |
+
guidance: float,
|
| 56 |
+
seed: int,
|
| 57 |
+
) -> torch.Tensor:
|
| 58 |
+
generator = torch.Generator().manual_seed(seed)
|
| 59 |
+
betas, alphas, cumulative = schedule()
|
| 60 |
+
pixels = torch.randn(len(labels), 64, generator=generator)
|
| 61 |
+
null_labels = torch.full_like(labels, 10)
|
| 62 |
+
model.eval()
|
| 63 |
+
for step in reversed(range(STEPS)):
|
| 64 |
+
timesteps = torch.full((len(labels),), step, dtype=torch.long)
|
| 65 |
+
conditional = model(pixels, timesteps, labels)
|
| 66 |
+
unconditional = model(pixels, timesteps, null_labels)
|
| 67 |
+
predicted_noise = unconditional + guidance * (conditional - unconditional)
|
| 68 |
+
alpha = alphas[step]
|
| 69 |
+
cumulative_alpha = cumulative[step]
|
| 70 |
+
mean = (
|
| 71 |
+
pixels - (1 - alpha) / torch.sqrt(1 - cumulative_alpha) * predicted_noise
|
| 72 |
+
) / torch.sqrt(alpha)
|
| 73 |
+
if step:
|
| 74 |
+
noise = torch.randn(pixels.shape, generator=generator)
|
| 75 |
+
pixels = mean + torch.sqrt(betas[step]) * noise
|
| 76 |
+
else:
|
| 77 |
+
pixels = mean
|
| 78 |
+
return torch.clamp((pixels + 1) / 2, 0, 1)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
@torch.inference_mode()
|
| 82 |
+
def generation_metrics(
|
| 83 |
+
model: PocketDenoiser,
|
| 84 |
+
judge: TinyVisionJudge,
|
| 85 |
+
guidance: float,
|
| 86 |
+
) -> tuple[dict, torch.Tensor, torch.Tensor]:
|
| 87 |
+
labels = torch.arange(10).repeat_interleave(100)
|
| 88 |
+
generated = sample(model, labels, guidance=guidance, seed=2032)
|
| 89 |
+
predictions = judge(generated.reshape(-1, 1, 8, 8)).argmax(dim=1)
|
| 90 |
+
accuracy_by_class = {
|
| 91 |
+
str(label): float(
|
| 92 |
+
(predictions[labels == label] == labels[labels == label]).float().mean()
|
| 93 |
+
)
|
| 94 |
+
for label in range(10)
|
| 95 |
+
}
|
| 96 |
+
diversity = {
|
| 97 |
+
str(label): float(generated[labels == label].var(dim=0).mean())
|
| 98 |
+
for label in range(10)
|
| 99 |
+
}
|
| 100 |
+
return (
|
| 101 |
+
{
|
| 102 |
+
"judge_accuracy": float((predictions == labels).float().mean()),
|
| 103 |
+
"judge_accuracy_by_class": accuracy_by_class,
|
| 104 |
+
"mean_pixel_variance_by_class": diversity,
|
| 105 |
+
"samples": len(labels),
|
| 106 |
+
"guidance": guidance,
|
| 107 |
+
},
|
| 108 |
+
generated,
|
| 109 |
+
labels,
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def save_grid(generated: torch.Tensor, labels: torch.Tensor, path: Path) -> None:
|
| 114 |
+
selected = [generated[labels == label][:10] for label in range(10)]
|
| 115 |
+
images = torch.cat(selected).reshape(10, 10, 8, 8).cpu().numpy()
|
| 116 |
+
canvas = np.zeros((80, 80), dtype=np.uint8)
|
| 117 |
+
for row in range(10):
|
| 118 |
+
for column in range(10):
|
| 119 |
+
canvas[
|
| 120 |
+
row * 8 : (row + 1) * 8,
|
| 121 |
+
column * 8 : (column + 1) * 8,
|
| 122 |
+
] = np.clip(images[row, column] * 255, 0, 255).astype(np.uint8)
|
| 123 |
+
Image.fromarray(canvas, mode="L").resize((800, 800), Image.Resampling.NEAREST).save(
|
| 124 |
+
path
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def main() -> None:
|
| 129 |
+
seed_everything(2032)
|
| 130 |
+
loader = load_training_data()
|
| 131 |
+
model = PocketDenoiser()
|
| 132 |
+
judge = TinyVisionJudge()
|
| 133 |
+
judge.load_state_dict(load_file(JUDGE_WEIGHTS))
|
| 134 |
+
judge.eval()
|
| 135 |
+
betas, _, cumulative = schedule()
|
| 136 |
+
optimizer = torch.optim.AdamW(model.parameters(), lr=0.0015, weight_decay=0.001)
|
| 137 |
+
epochs = 300
|
| 138 |
+
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
|
| 139 |
+
trackio.init(
|
| 140 |
+
project="pocket-diffusion",
|
| 141 |
+
name="conditional-ddpm-cfg-v1",
|
| 142 |
+
config={
|
| 143 |
+
"parameters": parameter_count(model),
|
| 144 |
+
"diffusion_steps": STEPS,
|
| 145 |
+
"epochs": epochs,
|
| 146 |
+
"label_dropout": 0.12,
|
| 147 |
+
},
|
| 148 |
+
)
|
| 149 |
+
for epoch in range(1, epochs + 1):
|
| 150 |
+
model.train()
|
| 151 |
+
running_loss = 0.0
|
| 152 |
+
examples = 0
|
| 153 |
+
for pixels, labels in loader:
|
| 154 |
+
timesteps = torch.randint(0, STEPS, (len(labels),))
|
| 155 |
+
noise = torch.randn_like(pixels)
|
| 156 |
+
cumulative_alpha = cumulative[timesteps].unsqueeze(1)
|
| 157 |
+
noisy = (
|
| 158 |
+
torch.sqrt(cumulative_alpha) * pixels
|
| 159 |
+
+ torch.sqrt(1 - cumulative_alpha) * noise
|
| 160 |
+
)
|
| 161 |
+
conditioned_labels = labels.clone()
|
| 162 |
+
drop = torch.rand(len(labels)) < 0.12
|
| 163 |
+
conditioned_labels[drop] = 10
|
| 164 |
+
prediction = model(noisy, timesteps, conditioned_labels)
|
| 165 |
+
loss = F.mse_loss(prediction, noise)
|
| 166 |
+
optimizer.zero_grad(set_to_none=True)
|
| 167 |
+
loss.backward()
|
| 168 |
+
optimizer.step()
|
| 169 |
+
running_loss += loss.item() * len(labels)
|
| 170 |
+
examples += len(labels)
|
| 171 |
+
scheduler.step()
|
| 172 |
+
if epoch == 1 or epoch % 10 == 0:
|
| 173 |
+
trackio.log(
|
| 174 |
+
{
|
| 175 |
+
"epoch": epoch,
|
| 176 |
+
"noise_prediction_mse": running_loss / examples,
|
| 177 |
+
"learning_rate": scheduler.get_last_lr()[0],
|
| 178 |
+
}
|
| 179 |
+
)
|
| 180 |
+
trackio.finish()
|
| 181 |
+
|
| 182 |
+
guidance_candidates = {}
|
| 183 |
+
for guidance in [1.0, 1.5, 2.0, 2.5, 3.0]:
|
| 184 |
+
metrics, _, _ = generation_metrics(model, judge, guidance)
|
| 185 |
+
guidance_candidates[str(guidance)] = metrics["judge_accuracy"]
|
| 186 |
+
best_guidance = float(max(guidance_candidates, key=guidance_candidates.get))
|
| 187 |
+
generation, generated, labels = generation_metrics(model, judge, best_guidance)
|
| 188 |
+
results = {
|
| 189 |
+
"model": "PocketDiffusion",
|
| 190 |
+
"parameters": parameter_count(model),
|
| 191 |
+
"diffusion_steps": STEPS,
|
| 192 |
+
"epochs": epochs,
|
| 193 |
+
"guidance_search": guidance_candidates,
|
| 194 |
+
"generation": generation,
|
| 195 |
+
"judge": "Tiny Vision labels-only student, 98.52% real-image test accuracy",
|
| 196 |
+
}
|
| 197 |
+
ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)
|
| 198 |
+
save_file(model.state_dict(), ARTIFACT_DIR / "model.safetensors")
|
| 199 |
+
save_grid(generated, labels, ARTIFACT_DIR / "samples.png")
|
| 200 |
+
(ARTIFACT_DIR / "evaluation.json").write_text(
|
| 201 |
+
json.dumps(results, indent=2),
|
| 202 |
+
encoding="utf-8",
|
| 203 |
+
)
|
| 204 |
+
print(json.dumps(results, indent=2))
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
if __name__ == "__main__":
|
| 208 |
+
main()
|