wop commited on
Commit
e465a2f
·
verified ·
1 Parent(s): c4701da

Upload 17 files

Browse files
.gitattributes CHANGED
@@ -1,35 +1,2 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
2
+ *.npz filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ __pycache__/
2
+ *.pyc
EVAL_REPRODUCTION.md ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Reproducing the eval.md numbers
2
+
3
+ Step-by-step guide to reproduce the FID / CLIP Score numbers in `eval.md` from
4
+ scratch. Unlike v0 (whose eval scripts lived in a throwaway env), everything
5
+ needed is **in this repo**: `fetch_coco_subset.py` builds the data,
6
+ `eval/run_eval.py` computes the metrics with torchmetrics.
7
+
8
+ CPU-only is fine (that's how the published numbers were produced). Budget
9
+ ~7 GB disk: ~5 GB dataset shards + ~2 GB python deps and model weights
10
+ (InceptionV3, CLIP ViT-B/32).
11
+
12
+ ## 1. Environment
13
+
14
+ ```bash
15
+ python3.13 -m venv eval-venv
16
+ # Windows: eval-venv\Scripts\activate macOS/Linux: source eval-venv/bin/activate
17
+
18
+ pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
19
+ pip install torchmetrics torch-fidelity transformers "datasets<3" safetensors pillow numpy
20
+ ```
21
+
22
+ `torch-fidelity` is the backend torchmetrics uses for the FID InceptionV3
23
+ features; `transformers` backs the CLIPScore metric.
24
+
25
+ ## 2. Build the data (train + eval split)
26
+
27
+ ```bash
28
+ python fetch_coco_subset.py --out ../pm-work
29
+ ```
30
+
31
+ This streams [`sayakpaul/coco-30-val-2014`](https://huggingface.co/datasets/sayakpaul/coco-30-val-2014)
32
+ (a public 30K-pair subset of MS-COCO val2014) and produces:
33
+
34
+ - `eval_real/` — stream rows 0–4999: real images, 256×256 center-crop JPEG (FID real set)
35
+ - `eval_captions.json` — the 5,000 captions for those rows (generation + CLIP Score)
36
+ - `coco_train.npz` + `coco_train.captions.json` — training pairs from stream
37
+ rows 10,000+, at 64×64
38
+
39
+ **Leakage control:** the same COCO image appears in multiple rows with
40
+ different captions, so training rows are dropped if their image content hash
41
+ (md5 of a 64×64 bilinear thumbnail) appears in the eval set, and deduped
42
+ against each other. The counts printed at the end of the script report both.
43
+
44
+ Streaming row-by-row from the Hub is slow (~hours on a normal connection). The
45
+ much faster path is to download the 10 parquet shards first (~4.9 GB), then:
46
+
47
+ ```bash
48
+ python fetch_coco_subset.py --out ../pm-work --parquet-dir ../pm-work/shards
49
+ ```
50
+
51
+ Sorted shard filename order equals hub streaming order, so both paths produce
52
+ identical splits.
53
+
54
+ ## 3. (Optional) retrain
55
+
56
+ ```bash
57
+ python train.py --data ../pm-work/coco_train.npz --epochs 50
58
+ ```
59
+
60
+ Deterministic given `--seed` (default 0): same data + same seed reproduces the
61
+ published `model.png` up to floating-point ordering. Skip this step to
62
+ evaluate the shipped `model.png` as-is.
63
+
64
+ ## 4. Run the eval
65
+
66
+ ```bash
67
+ python eval/run_eval.py --work ../pm-work --model model.png --n 5000
68
+ ```
69
+
70
+ This:
71
+ 1. Generates one 64×64 image per eval caption (native resolution — no
72
+ upscaling before metrics).
73
+ 2. **FID** via `torchmetrics.image.fid.FrechetInceptionDistance`
74
+ (`feature=2048`, InceptionV3 pool3): real = 5,000 COCO val2014 images at
75
+ 256×256 center-crop, fake = the 5,000 generated 64×64 images. The metric's
76
+ Inception backbone resizes both sets to 299×299 internally.
77
+ 3. **CLIP Score** via `torchmetrics.multimodal.CLIPScore` with
78
+ `openai/clip-vit-base-patch32` (the leaderboard default): each generated
79
+ image against the caption that produced it, averaged.
80
+
81
+ Results are printed and written to `eval_results.json` in the `--work` dir.
82
+ First run downloads InceptionV3 (~95 MB) and CLIP ViT-B/32 (~600 MB).
83
+
84
+ ## Notes
85
+
86
+ - n=5000 vs the standard n=30000: FID's 2048-dim covariance estimate benefits
87
+ from more samples; n=5000 was chosen as a compromise that is far past the
88
+ singular-covariance regime that made v0's n=40 number directional-only,
89
+ while staying CPU-friendly. Raising `--n` (and `EVAL_N` in
90
+ `fetch_coco_subset.py`) toward 30K tightens the estimate at proportional
91
+ cost — but note this dataset has 30K *rows*, not 30K unique images, and
92
+ rows past index 10,000 are used for training, so a larger eval set requires
93
+ re-splitting.
94
+ - The generated set's resolution (64×64) is reported as the native generation
95
+ resolution per the leaderboard rules; FID/CLIP preprocessing upsamples
96
+ internally as part of the metric, not as part of generation.
97
+ - `INFERENCE.py` (safetensors) and `main.py` (model.png) are verified
98
+ byte-identical, so either weights file reproduces the same metrics.
INFERENCE.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ INFERENCE.py - run PixelModel v2 from model.safetensors, self-contained.
3
+
4
+ Only needs torch + safetensors + pillow + numpy; no other file from this
5
+ repo. Produces byte-identical output to main.py (which loads model.png)
6
+ for the same prompt and resolution.
7
+
8
+ Usage:
9
+ python INFERENCE.py "a red double decker bus"
10
+ python INFERENCE.py "a cat on a couch" --model model.safetensors --out cat.png --res 64 --scale 4
11
+ """
12
+
13
+ import argparse
14
+ import os
15
+ import sys
16
+
17
+ import numpy as np
18
+ import torch
19
+ from PIL import Image
20
+ from safetensors.torch import load_file
21
+
22
+ EMB_DIM = 64
23
+ NATIVE_RES = 64
24
+ FREQS = (1.0, 2.0, 4.0, 8.0)
25
+
26
+
27
+ def _fnv1a(data: bytes) -> int:
28
+ h = 0x811C9DC5
29
+ for byte in data:
30
+ h ^= byte
31
+ h = (h * 0x01000193) & 0xFFFFFFFF
32
+ return h
33
+
34
+
35
+ def prompt_to_embedding(prompt: str) -> torch.Tensor:
36
+ text = "".join(c if c.isalnum() or c == " " else " " for c in prompt.lower())
37
+ text = " ".join(text.split())
38
+ vec = np.zeros(EMB_DIM, dtype=np.float32)
39
+ padded = f" {text} "
40
+ for i in range(len(padded) - 2):
41
+ h = _fnv1a(padded[i:i + 3].encode("utf-8"))
42
+ vec[h % EMB_DIM] += 1.0 if (h >> 16) & 1 else -1.0
43
+ for word in text.split():
44
+ h = _fnv1a(b"w:" + word.encode("utf-8"))
45
+ vec[h % EMB_DIM] += 2.0 if (h >> 16) & 1 else -2.0
46
+ norm = np.linalg.norm(vec)
47
+ if norm > 0:
48
+ vec /= norm
49
+ return torch.from_numpy(vec)
50
+
51
+
52
+ def coord_features(res: int) -> torch.Tensor:
53
+ axis = torch.linspace(-1.0, 1.0, res)
54
+ yy, xx = torch.meshgrid(axis, axis, indexing="ij")
55
+ x, y = xx.reshape(-1), yy.reshape(-1)
56
+ feats = [x, y]
57
+ for f in FREQS:
58
+ feats += [torch.sin(f * torch.pi * x), torch.cos(f * torch.pi * x),
59
+ torch.sin(f * torch.pi * y), torch.cos(f * torch.pi * y)]
60
+ return torch.stack(feats, dim=1)
61
+
62
+
63
+ def forward(w: dict, prompt: str, res: int) -> torch.Tensor:
64
+ emb = prompt_to_embedding(prompt).unsqueeze(0)
65
+ z = torch.tanh(emb @ w["T1"].T + w["b1"])
66
+ z = torch.tanh(z @ w["T2"].T + w["b2"])
67
+ feats = coord_features(res)
68
+ P = feats.shape[0]
69
+ inp = torch.cat([z.expand(P, -1), feats], dim=1)
70
+ h = torch.tanh(inp @ w["D1"].T + w["bd1"])
71
+ h = torch.tanh(h @ w["D2"].T + w["bd2"])
72
+ rgb = torch.sigmoid(h @ w["D3"].T + w["bd3"])
73
+ return rgb.reshape(res, res, 3)
74
+
75
+
76
+ def main():
77
+ p = argparse.ArgumentParser(description="PixelModel v2 inference (safetensors)")
78
+ p.add_argument("prompt")
79
+ p.add_argument("--model", default="model.safetensors")
80
+ p.add_argument("--out", default="out.png")
81
+ p.add_argument("--res", type=int, default=NATIVE_RES)
82
+ p.add_argument("--scale", type=int, default=4)
83
+ args = p.parse_args()
84
+
85
+ if not os.path.exists(args.model):
86
+ sys.exit(f"Model not found: {args.model}\n"
87
+ f"Run: python convert_to_safetensors.py to create it from model.png.")
88
+
89
+ weights = load_file(args.model)
90
+ with torch.no_grad():
91
+ result = forward(weights, args.prompt, args.res)
92
+
93
+ arr = (result.numpy() * 255).clip(0, 255).astype(np.uint8)
94
+ img = Image.fromarray(arr, mode="RGB")
95
+ if args.scale > 1:
96
+ img = img.resize((args.res * args.scale,) * 2, Image.NEAREST)
97
+ img.save(args.out)
98
+ print(f"prompt : '{args.prompt}'")
99
+ print(f"model : {args.model} ({os.path.getsize(args.model)} bytes, safetensors)")
100
+ print(f"output : {args.out} ({args.res}x{args.res} native, x{args.scale} view)")
101
+
102
+
103
+ if __name__ == "__main__":
104
+ main()
README.md CHANGED
@@ -1,3 +1,210 @@
1
- ---
2
- license: mit
3
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ pipeline_tag: text-to-image
4
+ language:
5
+ - en
6
+ tags:
7
+ - image
8
+ - t2i
9
+ - text-to-image
10
+ - custom-code
11
+ - tiny
12
+ model-index:
13
+ - name: PixelModel-v1
14
+ results:
15
+ - task:
16
+ type: text-to-image
17
+ dataset:
18
+ name: fid
19
+ type: fid
20
+ metrics:
21
+ - name: fid
22
+ type: fid
23
+ value: 439.46
24
+ - task:
25
+ type: text-to-image
26
+ dataset:
27
+ name: clip
28
+ type: clip
29
+ metrics:
30
+ - name: clip
31
+ type: clip
32
+ value: 0.2002
33
+ ---
34
+
35
+ <table>
36
+ <tr>
37
+ <td>
38
+ <br>
39
+
40
+ # See us on the [Leaderboard ⇗](https://huggingface.co/spaces/FlameF0X/Tiny-T2I-Leaderboard)
41
+
42
+ </td>
43
+ </tr>
44
+ </table>
45
+
46
+ <table>
47
+ <tr>
48
+ <td>
49
+ <br>
50
+
51
+ # Blog post [read it here ⇗](https://huggingface.co/spaces/bench-labs/blog?post=pixelmodel-v1.html)
52
+
53
+ </td>
54
+ </tr>
55
+ </table>
56
+
57
+ # PixelModel v1 🖼️
58
+
59
+ <img src="model.png" alt="The entire PixelModel v1: a 160x149 PNG containing all 23,747 weights" style="image-rendering: pixelated; width: 320px; max-width: 100%;">
60
+
61
+
62
+ A neural network where the weights **are** the image. Now the model is a thumbnail.
63
+
64
+ ## 📌 What is this?
65
+
66
+ `model.png` is not a picture — it *is* the model.
67
+
68
+ Every pixel encodes neural network weights. At inference, the PNG is decoded
69
+ into weight matrices, the prompt is hashed into an embedding, and a
70
+ coordinate-conditioned decoder paints an image — at **any resolution**.
71
+
72
+ [v0](https://huggingface.co/bench-labs/pixelmodel) was a 202,752-parameter MLP
73
+ welded to 32×32 output, trained on 6 color swatches. v1 is **8.5× smaller
74
+ (23,747 parameters)**, renders at any resolution (native 64×64), and is trained
75
+ on ~20K real MS-COCO caption/image pairs.
76
+
77
+ # cherry on top 🍒
78
+ The model generates 600 images (cpu) in 5 (five) seconds.
79
+ Thats 5000 images in 24 seconds on cpu.
80
+ The model trained on cpu for just 30 minutes.
81
+
82
+ ## 🆕 v0 → v1
83
+
84
+ | | v0 | v1 |
85
+ |---|---|---|
86
+ | Parameters | 202,752 | **23,747** |
87
+ | `model.png` | 64×3200 px | **160×149 px** (a thumbnail) |
88
+ | Output head | 1 weight row per pixel (196K params, 97% of model) | CPPN decoder on (x, y) — resolution-free |
89
+ | Resolution | fixed 32×32 | any; native 64×64 |
90
+ | Prompt embedding | char-sum (order-blind, collision-heavy) | hashed trigrams + words (FNV-1a), still 0 params |
91
+ | Biases | none | yes |
92
+ | Weight precision | 8-bit (G channel wasted) | **16-bit** (R=high byte, G=low byte) |
93
+ | Training data | 6 solid-color swatches | ~20K MS-COCO caption/image pairs |
94
+
95
+ <img src="pixelmodel-v1-loss.png" alt="Training loss curve crossing below both naive-predictor baselines" style="width: 100%; max-width: 720px;">
96
+
97
+ ## 🎨 Weight Encoding
98
+
99
+ Each pixel stores one weight at 16-bit precision, mapped from [-2, 2]:
100
+
101
+ - **R channel** → high byte
102
+ - **G channel** → low byte
103
+ - **B channel** → reserved
104
+
105
+ Round-trip quantization error ≈ 3×10⁻⁵ per weight.
106
+
107
+ ## 🧠 Architecture
108
+
109
+ ```text
110
+ prompt string
111
+ → hashed char-trigram + word embedding (64-dim, deterministic, 0 params)
112
+ → T1 (80×64)+b → tanh
113
+ → T2 (64×80)+b → tanh = latent z (64)
114
+
115
+ for every pixel (x, y):
116
+ concat(z, fourier features of (x, y)) ← 18 coord dims, freqs 1/2/4/8
117
+ → D1 (80×82)+b → tanh
118
+ → D2 (80×80)+b → tanh
119
+ → D3 (3×80)+b → sigmoid = RGB
120
+ ```
121
+
122
+ Because pixels are decoded from coordinates, parameter count is independent of
123
+ resolution — `--res 256` works with the same 23,747 weights.
124
+
125
+ All weights live inside `model.png` (160×149 px).
126
+
127
+ ## 📦 Standard weights (safetensors)
128
+
129
+ `model.png` is the canonical model — training writes to it directly. For
130
+ tooling that expects standard weight files, the same 10 tensors are exported as
131
+ `model.safetensors` (23,747 parameters total):
132
+
133
+ ```bash
134
+ python convert_to_safetensors.py # model.png -> model.safetensors
135
+ ```
136
+
137
+ Parameter count is verifiable without running code: `config.json`
138
+ (`total_parameters: 23747`, full per-layer breakdown) and the safetensors
139
+ header metadata (`total_parameters`, `param_breakdown`, `has_bias`,
140
+ `text_encoder_parameters: 0`, `vae_parameters: 0`).
141
+
142
+ ## 📊 Benchmark results
143
+
144
+ Measured per the [Tiny-T2I-Leaderboard](https://huggingface.co/spaces/FlameF0X/Tiny-T2I-Leaderboard) protocol — see `eval.md` for full method and `EVAL_REPRODUCTION.md` to re-run:
145
+
146
+ | Metric | v0 | v1 | Tooling |
147
+ |---|---|---|---|
148
+ | FID ↓ | 566.84 (n=40) | **439.46** (n=5000) | `torchmetrics.image.fid.FrechetInceptionDistance` |
149
+ | CLIP Score ↑ | 18.60 (n=40) | **20.02** (n=5000) | `torchmetrics.multimodal.CLIPScore`, `openai/clip-vit-base-patch32` |
150
+ | Native resolution | 32×32 | 64×64 | |
151
+
152
+ Real set: MS-COCO val2014 pairs from `sayakpaul/coco-30-val-2014` (256×256
153
+ center-crop). Training images are hash-checked to be disjoint from the eval
154
+ images.
155
+
156
+ Expectation management: a 23K-parameter model does not draw recognizable
157
+ objects. It learns caption-conditioned color, layout, and texture statistics of
158
+ COCO photos — the outputs are blurry impressions, not pictures. The point is
159
+ the size:quality ratio and the PNG-as-model gimmick, executed honestly.
160
+
161
+ <img src="pixelmodel-v1-probe.png" alt="Target photos vs model outputs: the model produces caption-conditioned color washes" style="width: 100%; max-width: 640px;">
162
+
163
+ ## ⚙️ Usage
164
+
165
+ ```bash
166
+ # training data (COCO subset)
167
+ python fetch_coco_subset.py --out ../pm-work
168
+
169
+ # train (writes model.png every epoch)
170
+ python train.py --data ../pm-work/coco_train.npz --epochs 50
171
+
172
+ # inference from model.png (canonical)
173
+ python main.py "a red double decker bus" --out bus.png
174
+
175
+ # inference from model.safetensors (standalone, needs only this one file + torch)
176
+ python convert_to_safetensors.py
177
+ python INFERENCE.py "a red double decker bus" --out bus.png
178
+
179
+ # any resolution from the same 23,747 weights
180
+ python main.py "a beach with palm trees" --res 256 --scale 1
181
+
182
+ # benchmark eval
183
+ python eval/run_eval.py --work ../pm-work --model model.png --n 5000
184
+ ```
185
+
186
+ `main.py` and `INFERENCE.py` produce identical output for the same prompt and
187
+ resolution.
188
+
189
+ ## 📁 Files
190
+
191
+ ```text
192
+ model.png ← THE MODEL (160×149 px)
193
+ model.safetensors ← same weights, standard format + param metadata
194
+ config.json ← architecture + parameter-count metadata
195
+ main.py ← inference, loads model.png
196
+ INFERENCE.py ← inference, loads model.safetensors (standalone)
197
+ convert_to_safetensors.py ← model.png -> model.safetensors
198
+ train.py ← training
199
+ model.py ← architecture + PNG weight codec
200
+ fetch_coco_subset.py ← builds train/eval data from COCO
201
+ eval/run_eval.py ← FID + CLIP Score via torchmetrics
202
+ eval.md ← benchmark results + method
203
+ EVAL_REPRODUCTION.md ← step-by-step reproduction guide
204
+ ```
205
+
206
+ ---
207
+
208
+ *Still a toy. Slightly less useless. The model is literally a thumbnail.*
209
+
210
+ Bench Labs · Simple, Reliable, Open sourced
config.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "pixelmodel-v2",
3
+ "architecture": "hashed trigram/word embedding -> MLP trunk -> CPPN decoder (coordinate-conditioned, resolution-free)",
4
+ "weights_file": "model.safetensors",
5
+ "total_parameters": 200259,
6
+ "parameter_breakdown": {
7
+ "text_encoder_parameters": 0,
8
+ "generative_backbone_parameters": 200259,
9
+ "vae_parameters": 0
10
+ },
11
+ "layers": {
12
+ "T1": { "shape": [256, 64], "role": "prompt embedding -> trunk hidden", "parameters": 16384 },
13
+ "b1": { "shape": [256], "role": "trunk hidden bias", "parameters": 256 },
14
+ "T2": { "shape": [128, 256], "role": "trunk hidden -> latent", "parameters": 32768 },
15
+ "b2": { "shape": [128], "role": "latent bias", "parameters": 128 },
16
+ "D1": { "shape": [320, 146], "role": "latent + fourier(x,y) -> decoder hidden", "parameters": 46720 },
17
+ "bd1": { "shape": [320], "role": "decoder hidden bias", "parameters": 320 },
18
+ "D2": { "shape": [320, 320], "role": "decoder hidden -> decoder hidden", "parameters": 102400 },
19
+ "bd2": { "shape": [320], "role": "decoder hidden bias", "parameters": 320 },
20
+ "D3": { "shape": [3, 320], "role": "decoder hidden -> RGB", "parameters": 960 },
21
+ "bd3": { "shape": [3], "role": "RGB bias", "parameters": 3 }
22
+ },
23
+ "has_bias": true,
24
+ "prompt_embedding": "hashed character trigrams + words (FNV-1a), 64-dim, deterministic, 0 parameters",
25
+ "embedding_dim": 64,
26
+ "trunk_hidden_dim": 256,
27
+ "latent_dim": 128,
28
+ "decoder_hidden_dim": 320,
29
+ "coordinate_features": "x, y + sin/cos at frequencies (1, 2, 4, 8) per axis (18 dims)",
30
+ "native_resolution": "64x64",
31
+ "resolution_free": true,
32
+ "output_channels": 3,
33
+ "model_png": "256x783 px, 16-bit weight codec (R = high byte, G = low byte, B = reserved)",
34
+ "weight_range": [-2.0, 2.0]
35
+ }
convert_to_safetensors.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ convert_to_safetensors.py - model.png -> model.safetensors.
3
+
4
+ model.png stays the canonical model; this exports the same weights in the
5
+ standard format, with parameter-count metadata embedded in the safetensors
6
+ header so the count is verifiable without running any code.
7
+
8
+ Usage:
9
+ python convert_to_safetensors.py
10
+ python convert_to_safetensors.py --model model.png --out model.safetensors
11
+ """
12
+
13
+ import argparse
14
+ import json
15
+ import os
16
+
17
+ from safetensors.torch import save_file
18
+
19
+ from model import MODEL_H, MODEL_W, N_PARAMS, NATIVE_RES, PARAM_SPECS, load_model
20
+
21
+
22
+ def main():
23
+ p = argparse.ArgumentParser()
24
+ p.add_argument("--model", default="model.png")
25
+ p.add_argument("--out", default="model.safetensors")
26
+ args = p.parse_args()
27
+
28
+ weights = load_model(args.model)
29
+ counts = {name: int(weights[name].numel()) for name, _ in PARAM_SPECS}
30
+ assert sum(counts.values()) == N_PARAMS
31
+
32
+ metadata = {
33
+ "model_type": "pixelmodel-v2",
34
+ "total_parameters": str(N_PARAMS),
35
+ "param_breakdown": json.dumps(counts),
36
+ "text_encoder_parameters": "0",
37
+ "vae_parameters": "0",
38
+ "has_bias": "true",
39
+ "native_resolution": f"{NATIVE_RES}x{NATIVE_RES}",
40
+ "source_png": f"{MODEL_W}x{MODEL_H} px, 16-bit codec (R=high byte, G=low byte)",
41
+ }
42
+ save_file(weights, args.out, metadata=metadata)
43
+ print(f"{args.model} -> {args.out} ({os.path.getsize(args.out)} bytes, "
44
+ f"{N_PARAMS} parameters)")
45
+
46
+
47
+ if __name__ == "__main__":
48
+ main()
fetch_coco_subset.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ fetch_coco_subset.py - build train and eval sets from sayakpaul/coco-30-val-2014.
3
+
4
+ The stream of 30K (image, caption) rows is split by row index:
5
+ rows 0..EVAL_N-1 -> eval set (real images for FID + captions for generation)
6
+ rows TRAIN_START..end -> training candidates
7
+
8
+ Training images whose content hash also appears in the eval set are dropped,
9
+ so the model is never trained on an image it is evaluated against (the same
10
+ image can appear in multiple rows with different captions). Training rows are
11
+ also deduped against each other by image hash.
12
+
13
+ Outputs (in --out dir):
14
+ coco_train.npz images uint8 (N, 64, 64, 3), center-cropped
15
+ coco_train.captions.json N caption strings
16
+ eval_real/real_00000.jpg... EVAL_N real images, 256x256 center-crop, q95
17
+ eval_captions.json EVAL_N caption strings (for generation + CLIP)
18
+
19
+ Usage:
20
+ python fetch_coco_subset.py --out ../pm-work
21
+ # much faster: pre-download the 10 parquet shards, then
22
+ python fetch_coco_subset.py --out ../pm-work --parquet-dir ../pm-work/shards
23
+ """
24
+
25
+ import argparse
26
+ import glob
27
+ import hashlib
28
+ import json
29
+ import os
30
+
31
+ import numpy as np
32
+ from datasets import load_dataset
33
+ from PIL import Image
34
+
35
+ EVAL_N = 5000
36
+ TRAIN_START = 10000 # comfortable gap after the eval rows
37
+ TRAIN_RES = 64
38
+ EVAL_RES = 256
39
+
40
+
41
+ def center_crop_resize(img: Image.Image, size: int) -> Image.Image:
42
+ w, h = img.size
43
+ side = min(w, h)
44
+ left, top = (w - side) // 2, (h - side) // 2
45
+ return img.crop((left, top, left + side, top + side)).resize(
46
+ (size, size), Image.BICUBIC
47
+ )
48
+
49
+
50
+ def img_hash(img: Image.Image) -> str:
51
+ """Content hash of the image, robust to caption-duplicated rows."""
52
+ return hashlib.md5(
53
+ img.convert("RGB").resize((64, 64), Image.BILINEAR).tobytes()
54
+ ).hexdigest()
55
+
56
+
57
+ def main():
58
+ p = argparse.ArgumentParser()
59
+ p.add_argument("--out", required=True)
60
+ p.add_argument("--parquet-dir", default=None,
61
+ help="dir with the dataset's parquet shards already downloaded "
62
+ "(sorted filename order == hub streaming row order)")
63
+ args = p.parse_args()
64
+
65
+ real_dir = os.path.join(args.out, "eval_real")
66
+ os.makedirs(real_dir, exist_ok=True)
67
+
68
+ if args.parquet_dir:
69
+ files = sorted(glob.glob(os.path.join(args.parquet_dir, "*.parquet")))
70
+ assert files, f"no parquet files in {args.parquet_dir}"
71
+ ds = load_dataset("parquet", data_files=files, split="train", streaming=True)
72
+ else:
73
+ ds = load_dataset("sayakpaul/coco-30-val-2014", split="train", streaming=True)
74
+
75
+ eval_hashes = set()
76
+ eval_captions = []
77
+ train_images, train_captions = [], []
78
+ train_hashes = set()
79
+ skipped_eval_overlap = skipped_dupe = 0
80
+
81
+ for i, row in enumerate(ds):
82
+ img, caption = row["image"], row["caption"]
83
+
84
+ if i < EVAL_N:
85
+ eval_hashes.add(img_hash(img))
86
+ eval_captions.append(caption)
87
+ center_crop_resize(img.convert("RGB"), EVAL_RES).save(
88
+ os.path.join(real_dir, f"real_{i:05d}.jpg"), quality=95
89
+ )
90
+ if (i + 1) % 1000 == 0:
91
+ print(f" eval: {i + 1}/{EVAL_N}", flush=True)
92
+ continue
93
+
94
+ if i < TRAIN_START:
95
+ continue
96
+
97
+ h = img_hash(img)
98
+ if h in eval_hashes:
99
+ skipped_eval_overlap += 1
100
+ continue
101
+ if h in train_hashes:
102
+ skipped_dupe += 1
103
+ continue
104
+ train_hashes.add(h)
105
+ thumb = center_crop_resize(img.convert("RGB"), TRAIN_RES)
106
+ train_images.append(np.array(thumb, dtype=np.uint8))
107
+ train_captions.append(caption)
108
+ if len(train_images) % 2000 == 0:
109
+ print(f" train: {len(train_images)} (row {i})", flush=True)
110
+
111
+ images = np.stack(train_images)
112
+ np.savez_compressed(os.path.join(args.out, "coco_train.npz"), images=images)
113
+ with open(os.path.join(args.out, "coco_train.captions.json"), "w", encoding="utf-8") as f:
114
+ json.dump(train_captions, f)
115
+ with open(os.path.join(args.out, "eval_captions.json"), "w", encoding="utf-8") as f:
116
+ json.dump(eval_captions, f)
117
+
118
+ print(f"\neval: {len(eval_captions)} pairs @ {EVAL_RES}x{EVAL_RES} -> {real_dir}")
119
+ print(f"train: {len(train_captions)} pairs @ {TRAIN_RES}x{TRAIN_RES} "
120
+ f"(skipped: {skipped_eval_overlap} eval-overlap, {skipped_dupe} dupes)")
121
+
122
+
123
+ if __name__ == "__main__":
124
+ main()
main.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ main.py - PixelModel v1 inference from model.png (the canonical model).
3
+
4
+ Usage:
5
+ python main.py "a red double decker bus"
6
+ python main.py "a cat on a couch" --out cat.png --res 64 --scale 4
7
+ """
8
+
9
+ import argparse
10
+
11
+ import numpy as np
12
+ import torch
13
+ from PIL import Image
14
+
15
+ from model import NATIVE_RES, forward, load_model
16
+
17
+
18
+ def main():
19
+ p = argparse.ArgumentParser()
20
+ p.add_argument("prompt")
21
+ p.add_argument("--model", default="model.png")
22
+ p.add_argument("--out", default="out.png")
23
+ p.add_argument("--res", type=int, default=NATIVE_RES,
24
+ help="generation resolution (decoder is resolution-free)")
25
+ p.add_argument("--scale", type=int, default=4, help="nearest-neighbor upscale for viewing")
26
+ args = p.parse_args()
27
+
28
+ weights = load_model(args.model)
29
+ with torch.no_grad():
30
+ result = forward(weights, args.prompt, res=args.res)[0]
31
+
32
+ arr = (result.numpy() * 255).clip(0, 255).astype(np.uint8)
33
+ img = Image.fromarray(arr, mode="RGB")
34
+ if args.scale > 1:
35
+ img = img.resize((args.res * args.scale,) * 2, Image.NEAREST)
36
+ img.save(args.out)
37
+ print(f"prompt : '{args.prompt}'")
38
+ print(f"model : {args.model}")
39
+ print(f"output : {args.out} ({args.res}x{args.res} native, x{args.scale} view)")
40
+
41
+
42
+ if __name__ == "__main__":
43
+ main()
model.png ADDED
model.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PixelModel v2 - the weights ARE the image.
3
+
4
+ model.png stores every parameter of the network as pixel values.
5
+ v2 changes vs v1:
6
+ - Wider trunk and decoder: 200,259 parameters rather than 23,747, while
7
+ keeping the same prompt encoder, coordinate features, 64x64 native
8
+ resolution, and 16-bit PNG codec.
9
+ - The larger capacity is intended for the same ~20K COCO training rows.
10
+
11
+ v1 retained these changes from v0:
12
+ - Coordinate-conditioned decoder (CPPN-style): parameters no longer scale
13
+ with output resolution. v0 spent 196,608 of its 202,752 params on the
14
+ output head; v1 has 23,747 params total and renders at ANY resolution
15
+ (native 64x64 for eval).
16
+ - Hashed character-trigram + word prompt embedding (deterministic, 0 params)
17
+ instead of v0's char-sum, so different prompts actually get different,
18
+ partially compositional embeddings.
19
+ - Biases everywhere (v0 had none).
20
+ - 16-bit weight codec: R = high byte, G = low byte, B = reserved.
21
+ v0 wasted G and stored weights at 8 bits; v1's quantization error is
22
+ ~6e-5 per weight. model.png is 160x149 px - the model is a thumbnail.
23
+
24
+ Architecture:
25
+ prompt -> hashed trigram/word embedding (64)
26
+ -> T1 (256x64)+b tanh -> T2 (128x256)+b tanh -> latent z (128)
27
+ per pixel: concat(z, fourier(x,y) (18))
28
+ -> D1 (320x146)+b tanh -> D2 (320x320)+b tanh -> D3 (3x320)+b sigmoid -> RGB
29
+ """
30
+
31
+ import numpy as np
32
+ import torch
33
+ from PIL import Image
34
+
35
+ # -- config -------------------------------------------------------------------
36
+ EMB_DIM = 64 # prompt embedding size (hashed, no learned params)
37
+ TRUNK_HIDDEN = 256
38
+ LATENT = 128
39
+ COORD_FEATS = 18 # x, y + sin/cos at freqs (1,2,4,8) for each axis
40
+ DEC_HIDDEN = 320
41
+ NATIVE_RES = 64 # native eval resolution (decoder works at any res)
42
+
43
+ FREQS = (1.0, 2.0, 4.0, 8.0)
44
+
45
+ # every parameter tensor, in canonical flattening order
46
+ PARAM_SPECS = [
47
+ ("T1", (TRUNK_HIDDEN, EMB_DIM)),
48
+ ("b1", (TRUNK_HIDDEN,)),
49
+ ("T2", (LATENT, TRUNK_HIDDEN)),
50
+ ("b2", (LATENT,)),
51
+ ("D1", (DEC_HIDDEN, LATENT + COORD_FEATS)),
52
+ ("bd1", (DEC_HIDDEN,)),
53
+ ("D2", (DEC_HIDDEN, DEC_HIDDEN)),
54
+ ("bd2", (DEC_HIDDEN,)),
55
+ ("D3", (3, DEC_HIDDEN)),
56
+ ("bd3", (3,)),
57
+ ]
58
+ N_PARAMS = sum(int(np.prod(s)) for _, s in PARAM_SPECS) # 200,259
59
+
60
+ # model.png geometry: one weight per pixel, 16-bit (R=high, G=low, B=reserved)
61
+ MODEL_W = 256
62
+ MODEL_H = -(-N_PARAMS // MODEL_W) # 783
63
+ WMAX = 2.0 # weights live in [-2, 2]
64
+
65
+
66
+ # -- prompt embedding (deterministic, zero parameters) ------------------------
67
+
68
+ def _fnv1a(data: bytes) -> int:
69
+ h = 0x811C9DC5
70
+ for byte in data:
71
+ h ^= byte
72
+ h = (h * 0x01000193) & 0xFFFFFFFF
73
+ return h
74
+
75
+
76
+ def prompt_to_embedding(prompt: str) -> torch.Tensor:
77
+ """Hashed bag of character trigrams + words -> EMB_DIM vector, L2-normed."""
78
+ text = "".join(c if c.isalnum() or c == " " else " " for c in prompt.lower())
79
+ text = " ".join(text.split())
80
+ vec = np.zeros(EMB_DIM, dtype=np.float32)
81
+
82
+ padded = f" {text} "
83
+ for i in range(len(padded) - 2):
84
+ h = _fnv1a(padded[i:i + 3].encode("utf-8"))
85
+ sign = 1.0 if (h >> 16) & 1 else -1.0
86
+ vec[h % EMB_DIM] += sign
87
+
88
+ for word in text.split():
89
+ h = _fnv1a(b"w:" + word.encode("utf-8"))
90
+ sign = 1.0 if (h >> 16) & 1 else -1.0
91
+ vec[h % EMB_DIM] += 2.0 * sign # words weigh more than trigrams
92
+
93
+ norm = np.linalg.norm(vec)
94
+ if norm > 0:
95
+ vec /= norm
96
+ return torch.from_numpy(vec)
97
+
98
+
99
+ def prompts_to_embeddings(prompts) -> torch.Tensor:
100
+ return torch.stack([prompt_to_embedding(p) for p in prompts])
101
+
102
+
103
+ # -- coordinate features ------------------------------------------------------
104
+
105
+ _coord_cache = {}
106
+
107
+
108
+ def coord_features(res: int) -> torch.Tensor:
109
+ """(res*res, COORD_FEATS) fourier features of the pixel grid, cached."""
110
+ if res not in _coord_cache:
111
+ axis = torch.linspace(-1.0, 1.0, res)
112
+ yy, xx = torch.meshgrid(axis, axis, indexing="ij")
113
+ x = xx.reshape(-1)
114
+ y = yy.reshape(-1)
115
+ feats = [x, y]
116
+ for f in FREQS:
117
+ feats += [torch.sin(f * torch.pi * x), torch.cos(f * torch.pi * x),
118
+ torch.sin(f * torch.pi * y), torch.cos(f * torch.pi * y)]
119
+ _coord_cache[res] = torch.stack(feats, dim=1) # (res*res, 18)
120
+ return _coord_cache[res]
121
+
122
+
123
+ # -- forward ------------------------------------------------------------------
124
+
125
+ def encode_prompt(weights: dict, emb: torch.Tensor) -> torch.Tensor:
126
+ """emb (B, EMB_DIM) -> latent z (B, LATENT)."""
127
+ z = torch.tanh(emb @ weights["T1"].T + weights["b1"])
128
+ z = torch.tanh(z @ weights["T2"].T + weights["b2"])
129
+ return z
130
+
131
+
132
+ def decode_pixels(weights: dict, z: torch.Tensor, feats: torch.Tensor) -> torch.Tensor:
133
+ """z (B, LATENT), feats (P, COORD_FEATS) -> RGB (B, P, 3) in [0, 1]."""
134
+ B, P = z.shape[0], feats.shape[0]
135
+ inp = torch.cat([z.unsqueeze(1).expand(B, P, LATENT),
136
+ feats.unsqueeze(0).expand(B, P, COORD_FEATS)], dim=2)
137
+ h = torch.tanh(inp @ weights["D1"].T + weights["bd1"])
138
+ h = torch.tanh(h @ weights["D2"].T + weights["bd2"])
139
+ return torch.sigmoid(h @ weights["D3"].T + weights["bd3"])
140
+
141
+
142
+ def forward(weights: dict, prompts, res: int = NATIVE_RES) -> torch.Tensor:
143
+ """prompts: str or list of str -> (B, res, res, 3) float in [0, 1]."""
144
+ if isinstance(prompts, str):
145
+ prompts = [prompts]
146
+ emb = prompts_to_embeddings(prompts)
147
+ z = encode_prompt(weights, emb)
148
+ rgb = decode_pixels(weights, z, coord_features(res))
149
+ return rgb.reshape(len(prompts), res, res, 3)
150
+
151
+
152
+ # -- 16-bit PNG weight codec --------------------------------------------------
153
+
154
+ def weights_to_pixels(weights: dict) -> np.ndarray:
155
+ """weight dict -> (MODEL_H, MODEL_W, 3) uint8. R=high byte, G=low, B=0."""
156
+ flat = torch.cat([weights[n].detach().reshape(-1) for n, _ in PARAM_SPECS])
157
+ q = ((flat.clamp(-WMAX, WMAX) / WMAX + 1.0) / 2.0 * 65535.0).round()
158
+ q = q.to(torch.int64).cpu().numpy()
159
+ q = np.pad(q, (0, MODEL_W * MODEL_H - N_PARAMS))
160
+ img = np.zeros((MODEL_H * MODEL_W, 3), dtype=np.uint8)
161
+ img[:, 0] = q >> 8
162
+ img[:, 1] = q & 0xFF
163
+ return img.reshape(MODEL_H, MODEL_W, 3)
164
+
165
+
166
+ def pixels_to_weights(arr: np.ndarray) -> dict:
167
+ """(MODEL_H, MODEL_W, 3) uint8 -> weight dict."""
168
+ flat = arr.reshape(-1, 3).astype(np.int64)
169
+ q = (flat[:, 0] << 8) | flat[:, 1]
170
+ vals = (q.astype(np.float32) / 65535.0 * 2.0 - 1.0) * WMAX
171
+ vals = torch.from_numpy(vals[:N_PARAMS])
172
+ weights, i = {}, 0
173
+ for name, shape in PARAM_SPECS:
174
+ n = int(np.prod(shape))
175
+ weights[name] = vals[i:i + n].reshape(shape).clone()
176
+ i += n
177
+ return weights
178
+
179
+
180
+ def save_model(weights: dict, path: str):
181
+ Image.fromarray(weights_to_pixels(weights), mode="RGB").save(path)
182
+
183
+
184
+ def load_model(path: str) -> dict:
185
+ img = Image.open(path).convert("RGB")
186
+ arr = np.array(img, dtype=np.uint8)
187
+ assert arr.shape == (MODEL_H, MODEL_W, 3), \
188
+ f"expected {MODEL_H}x{MODEL_W} model.png, got {arr.shape[0]}x{arr.shape[1]}"
189
+ return pixels_to_weights(arr)
190
+
191
+
192
+ def init_weights(seed: int = 0) -> dict:
193
+ g = torch.Generator().manual_seed(seed)
194
+ weights = {}
195
+ for name, shape in PARAM_SPECS:
196
+ if len(shape) == 1:
197
+ weights[name] = torch.zeros(shape)
198
+ else:
199
+ fan_in = shape[1]
200
+ scale = 1.0 / fan_in ** 0.5
201
+ if name == "D3":
202
+ scale *= 0.1 # start near grey, not saturated
203
+ weights[name] = torch.randn(shape, generator=g) * scale
204
+ return weights
205
+
206
+
207
+ if __name__ == "__main__":
208
+ print(f"params: {N_PARAMS} model.png: {MODEL_W}x{MODEL_H} px")
209
+ for name, shape in PARAM_SPECS:
210
+ print(f" {name:>4} {str(tuple(shape)):>12} {int(np.prod(shape)):>6}")
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:26b6f43f85a826fcf0c7f1c013bb391e9a73169ab2fe18cb0a96bd62664a0a43
3
+ size 96028
pixelmodel-v1-loss.png ADDED
pixelmodel-v1-probe.png ADDED
pixelmodel_v2_kaggle.ipynb ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# PixelModel v2 — Kaggle training and evaluation\n",
8
+ "\n",
9
+ "This notebook trains the 200,259-parameter v2 model on the same approximately 20K disjoint COCO caption/image pairs as v1, then evaluates it on the held-out 5K COCO rows using FID and CLIP Score.\n",
10
+ "\n",
11
+ "Before running: enable **Internet** and **GPU** in Kaggle. Add this repository as a Kaggle Dataset named `pixelmodel-v2`, so it is mounted at `/kaggle/input/pixelmodel-v2`. The notebook copies it to the writable working directory; the trained `model.png`, `model.safetensors`, and evaluation results will be in `/kaggle/working/pixelmodel-v2-output`."
12
+ ]
13
+ },
14
+ {
15
+ "cell_type": "code",
16
+ "execution_count": null,
17
+ "metadata": {},
18
+ "outputs": [],
19
+ "source": [
20
+ "!pip -q install datasets safetensors 'torchmetrics[image]'\n",
21
+ "!pip -q install --upgrade 'torch-fidelity'\n",
22
+ "!nvidia-smi"
23
+ ]
24
+ },
25
+ {
26
+ "cell_type": "code",
27
+ "execution_count": null,
28
+ "metadata": {},
29
+ "outputs": [],
30
+ "source": [
31
+ "import os\n",
32
+ "import shutil\n",
33
+ "from pathlib import Path\n",
34
+ "\n",
35
+ "SOURCE_DIR = Path('/kaggle/input/pixelmodel-v2')\n",
36
+ "PROJECT_DIR = Path('/kaggle/working/pixelmodel-v2')\n",
37
+ "WORK_DIR = Path('/kaggle/working/pm-work')\n",
38
+ "OUTPUT_DIR = Path('/kaggle/working/pixelmodel-v2-output')\n",
39
+ "\n",
40
+ "assert SOURCE_DIR.exists(), f'Attach the v2 repository as a Kaggle Dataset: {SOURCE_DIR}'\n",
41
+ "shutil.rmtree(PROJECT_DIR, ignore_errors=True)\n",
42
+ "shutil.copytree(SOURCE_DIR, PROJECT_DIR)\n",
43
+ "WORK_DIR.mkdir(parents=True, exist_ok=True)\n",
44
+ "OUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n",
45
+ "os.chdir(PROJECT_DIR)\n",
46
+ "!python model.py\n",
47
+ "!cat config.json"
48
+ ]
49
+ },
50
+ {
51
+ "cell_type": "code",
52
+ "execution_count": null,
53
+ "metadata": {},
54
+ "outputs": [],
55
+ "source": [
56
+ "# Creates the v1-compatible split: ~20K 64x64 training pairs and 5K held-out eval pairs.\n",
57
+ "# This downloads COCO through Hugging Face, so Kaggle Internet must stay enabled.\n",
58
+ "!python fetch_coco_subset.py --out {WORK_DIR}"
59
+ ]
60
+ },
61
+ {
62
+ "cell_type": "code",
63
+ "execution_count": null,
64
+ "metadata": {},
65
+ "outputs": [],
66
+ "source": [
67
+ "# Start fresh: do not pass --resume for the first run.\n",
68
+ "# The v2 model is larger than v1, so use a GPU and give it a longer run.\n",
69
+ "!python train.py --data {WORK_DIR / 'coco_train.npz'} --epochs 50 --batch 128 --pixels 1024 --lr 0.002\n",
70
+ "!python convert_to_safetensors.py --model model.png --out model.safetensors\n",
71
+ "# Persist the trained model immediately; Kaggle exposes /kaggle/working as notebook output.\n",
72
+ "!cp model.png model.safetensors config.json {OUTPUT_DIR}/\n",
73
+ "!ls -lh {OUTPUT_DIR}"
74
+ ]
75
+ },
76
+ {
77
+ "cell_type": "code",
78
+ "execution_count": null,
79
+ "metadata": {},
80
+ "outputs": [],
81
+ "source": [
82
+ "# Held-out evaluation: generate 5K images, then report FID (lower is better) and CLIP Score (higher is better).\n",
83
+ "!python eval/run_eval.py --work {WORK_DIR} --model model.png --n 5000\n",
84
+ "!cp {WORK_DIR / 'eval_results.json'} {OUTPUT_DIR}/\n",
85
+ "!ls -lh {OUTPUT_DIR}\n",
86
+ "!cat {WORK_DIR / 'eval_results.json'}"
87
+ ]
88
+ }
89
+ ],
90
+ "metadata": {
91
+ "kernelspec": {
92
+ "display_name": "Python 3",
93
+ "language": "python",
94
+ "name": "python3"
95
+ },
96
+ "language_info": {
97
+ "name": "python",
98
+ "version": "3.x"
99
+ }
100
+ },
101
+ "nbformat": 4,
102
+ "nbformat_minor": 5
103
+ }
run_eval.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ run_eval.py - benchmark eval: COCO FID + CLIP Score via torchmetrics.
3
+
4
+ Protocol (matches the Tiny-T2I leaderboard requirements):
5
+ - FID: torchmetrics.image.fid.FrechetInceptionDistance (InceptionV3,
6
+ 2048-dim pool3 features). Real set: n COCO val2014 images (256x256
7
+ center-crop) from sayakpaul/coco-30-val-2014, rows 0..n-1 of the
8
+ stream — disjoint by image hash from the training set (see
9
+ fetch_coco_subset.py). Generated set: model output at native 64x64
10
+ for those same n captions.
11
+ - CLIP Score: torchmetrics.multimodal.CLIPScore with
12
+ openai/clip-vit-base-patch32 (the default), generated image vs the
13
+ caption that produced it.
14
+
15
+ Usage (after fetch_coco_subset.py has populated --work):
16
+ python eval/run_eval.py --work ../pm-work --model model.png --n 5000
17
+ """
18
+
19
+ import argparse
20
+ import json
21
+ import os
22
+ import sys
23
+ import time
24
+
25
+ import numpy as np
26
+ import torch
27
+ from PIL import Image
28
+
29
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
30
+ from model import NATIVE_RES, coord_features, decode_pixels, encode_prompt, load_model, prompts_to_embeddings # noqa: E402
31
+
32
+
33
+ def generate(model_path: str, captions, out_dir: str, device: torch.device, batch: int = 64):
34
+ os.makedirs(out_dir, exist_ok=True)
35
+ weights = load_model(model_path)
36
+ weights = {name: value.to(device) for name, value in weights.items()}
37
+ feats = coord_features(NATIVE_RES).to(device)
38
+ t0 = time.time()
39
+ for start in range(0, len(captions), batch):
40
+ chunk = captions[start:start + batch]
41
+ with torch.no_grad():
42
+ emb = prompts_to_embeddings(chunk).to(device)
43
+ z = encode_prompt(weights, emb)
44
+ rgb = decode_pixels(weights, z, feats)
45
+ arr = (rgb.reshape(len(chunk), NATIVE_RES, NATIVE_RES, 3).cpu().numpy()
46
+ * 255).clip(0, 255).astype(np.uint8)
47
+ for j in range(len(chunk)):
48
+ Image.fromarray(arr[j], mode="RGB").save(
49
+ os.path.join(out_dir, f"gen_{start + j:05d}.png"))
50
+ if (start // batch) % 20 == 0:
51
+ print(f" gen {start + len(chunk)}/{len(captions)} "
52
+ f"({time.time() - t0:.0f}s)", flush=True)
53
+ print(f" generated {len(captions)} images @ {NATIVE_RES}x{NATIVE_RES} "
54
+ f"in {time.time() - t0:.0f}s", flush=True)
55
+
56
+
57
+ def load_batch(paths):
58
+ imgs = [np.array(Image.open(p).convert("RGB"), dtype=np.uint8) for p in paths]
59
+ return torch.from_numpy(np.stack(imgs)).permute(0, 3, 1, 2) # (B,3,H,W) uint8
60
+
61
+
62
+ def compute_fid(real_dir: str, gen_dir: str, n: int, device: torch.device, batch: int = 32) -> float:
63
+ from torchmetrics.image.fid import FrechetInceptionDistance
64
+ fid = FrechetInceptionDistance(feature=2048, normalize=False).to(device)
65
+ t0 = time.time()
66
+ for label, dir_, real in (("real", real_dir, True), ("gen", gen_dir, False)):
67
+ files = sorted(os.listdir(dir_))[:n]
68
+ for start in range(0, len(files), batch):
69
+ imgs = load_batch([os.path.join(dir_, f) for f in files[start:start + batch]])
70
+ fid.update(imgs.to(device), real=real)
71
+ if (start // batch) % 25 == 0:
72
+ print(f" fid/{label}: {start + imgs.shape[0]}/{len(files)} "
73
+ f"({time.time() - t0:.0f}s)", flush=True)
74
+ return float(fid.compute())
75
+
76
+
77
+ def compute_clip_score(gen_dir: str, captions, device: torch.device, batch: int = 32):
78
+ from torchmetrics.multimodal import CLIPScore
79
+ metric = CLIPScore(model_name_or_path="openai/clip-vit-base-patch32").to(device)
80
+ files = sorted(os.listdir(gen_dir))[:len(captions)]
81
+ t0 = time.time()
82
+ for start in range(0, len(files), batch):
83
+ imgs = load_batch([os.path.join(gen_dir, f) for f in files[start:start + batch]])
84
+ metric.update(imgs.to(device), captions[start:start + imgs.shape[0]])
85
+ if (start // batch) % 25 == 0:
86
+ print(f" clip: {start + imgs.shape[0]}/{len(files)} "
87
+ f"({time.time() - t0:.0f}s)", flush=True)
88
+ return float(metric.compute())
89
+
90
+
91
+ def main():
92
+ p = argparse.ArgumentParser()
93
+ p.add_argument("--work", required=True, help="dir from fetch_coco_subset.py")
94
+ p.add_argument("--model", default="model.png")
95
+ p.add_argument("--n", type=int, default=5000)
96
+ p.add_argument("--device", default="auto", help="auto, cpu, cuda, or a PyTorch device string")
97
+ p.add_argument("--skip-gen", action="store_true")
98
+ p.add_argument("--skip-fid", action="store_true")
99
+ args = p.parse_args()
100
+ device = torch.device("cuda" if args.device == "auto" and torch.cuda.is_available()
101
+ else "cpu" if args.device == "auto" else args.device)
102
+ print(f"device: {device}")
103
+
104
+ with open(os.path.join(args.work, "eval_captions.json"), encoding="utf-8") as f:
105
+ captions = json.load(f)[:args.n]
106
+ real_dir = os.path.join(args.work, "eval_real")
107
+ gen_dir = os.path.join(args.work, "eval_gen")
108
+
109
+ if not args.skip_gen:
110
+ print(f"[1/3] generating {len(captions)} images from '{args.model}'...")
111
+ generate(args.model, captions, gen_dir, device)
112
+ fid = None
113
+ if not args.skip_fid:
114
+ print("[2/3] FID (torchmetrics.image.fid, InceptionV3 2048)...")
115
+ fid = compute_fid(real_dir, gen_dir, args.n, device)
116
+ print(f"FID = {fid:.4f}", flush=True)
117
+ print("[3/3] CLIP Score (torchmetrics, openai/clip-vit-base-patch32)...")
118
+ clip = compute_clip_score(gen_dir, captions, device)
119
+ print(f"CLIP Score = {clip:.4f} (cosine {clip / 100:.4f})")
120
+
121
+ print(f"\nRESULTS n={args.n} native_res={NATIVE_RES}x{NATIVE_RES}")
122
+ if fid is not None:
123
+ print(f" FID = {fid:.2f}")
124
+ print(f" CLIP Score = {clip:.2f}")
125
+ out_path = os.path.join(args.work, "eval_results.json")
126
+ results = {"n": args.n, "native_resolution": f"{NATIVE_RES}x{NATIVE_RES}",
127
+ "fid": fid, "clip_score": clip}
128
+ if fid is None and os.path.exists(out_path):
129
+ old = json.load(open(out_path))
130
+ results["fid"] = old.get("fid")
131
+ with open(out_path, "w") as f:
132
+ json.dump(results, f, indent=2)
133
+
134
+
135
+ if __name__ == "__main__":
136
+ main()
train.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ train.py - train PixelModel v1 on caption/image pairs, save into model.png.
3
+
4
+ Expects the npz produced by fetch_coco_subset.py:
5
+ images uint8 (N, 64, 64, 3)
6
+ captions json list of N strings (stored alongside as captions.json)
7
+
8
+ Each step samples a batch of captions and a random subset of pixel
9
+ coordinates (the CPPN decoder makes per-pixel training natural), computes
10
+ MSE against the target pixels, and Adam-steps the weights. Weights are
11
+ clamped to [-WMAX, WMAX] so they always round-trip through the 16-bit
12
+ PNG codec. model.png is written every epoch.
13
+
14
+ Usage:
15
+ python train.py --data ../pm-work/coco_train.npz
16
+ python train.py --data ../pm-work/coco_train.npz --epochs 40 --lr 2e-3
17
+ """
18
+
19
+ import argparse
20
+ import json
21
+ import os
22
+ import time
23
+
24
+ import numpy as np
25
+ import torch
26
+
27
+ from model import (
28
+ NATIVE_RES, N_PARAMS, PARAM_SPECS, WMAX,
29
+ coord_features, decode_pixels, encode_prompt,
30
+ init_weights, load_model, prompts_to_embeddings, save_model,
31
+ )
32
+
33
+ MODEL_PATH = "model.png"
34
+
35
+
36
+ def main():
37
+ p = argparse.ArgumentParser()
38
+ p.add_argument("--data", required=True, help="npz with images (N,64,64,3) uint8")
39
+ p.add_argument("--captions", default=None, help="json list of captions (default: <data>.captions.json)")
40
+ p.add_argument("--epochs", type=int, default=30)
41
+ p.add_argument("--batch", type=int, default=128)
42
+ p.add_argument("--pixels", type=int, default=768, help="pixel coords sampled per step")
43
+ p.add_argument("--lr", type=float, default=2e-3)
44
+ p.add_argument("--seed", type=int, default=0)
45
+ p.add_argument("--device", default="auto", help="auto, cpu, cuda, or a PyTorch device string")
46
+ p.add_argument("--resume", action="store_true", help="continue from existing model.png")
47
+ args = p.parse_args()
48
+
49
+ torch.manual_seed(args.seed)
50
+ rng = np.random.default_rng(args.seed)
51
+ device = torch.device("cuda" if args.device == "auto" and torch.cuda.is_available()
52
+ else "cpu" if args.device == "auto" else args.device)
53
+ print(f"device: {device}")
54
+
55
+ data = np.load(args.data)
56
+ images = data["images"] # (N, 64, 64, 3) uint8
57
+ cap_path = args.captions or args.data.replace(".npz", ".captions.json")
58
+ with open(cap_path, encoding="utf-8") as f:
59
+ captions = json.load(f)
60
+ N = len(captions)
61
+ assert images.shape[0] == N
62
+ res = images.shape[1]
63
+ print(f"dataset: {N} pairs @ {res}x{res}")
64
+
65
+ print("precomputing prompt embeddings...")
66
+ embs = prompts_to_embeddings(captions).to(device) # (N, 64)
67
+
68
+ targets = torch.from_numpy(images.reshape(N, res * res, 3).astype(np.float32) / 255.0).to(device)
69
+ feats_all = coord_features(res).to(device) # (res*res, 18)
70
+
71
+ if args.resume and os.path.exists(MODEL_PATH):
72
+ weights = load_model(MODEL_PATH)
73
+ print(f"resumed from {MODEL_PATH}")
74
+ else:
75
+ weights = init_weights(args.seed)
76
+ for w in weights.values():
77
+ w.data = w.data.to(device)
78
+ w.requires_grad_(True)
79
+ params = [weights[n] for n, _ in PARAM_SPECS]
80
+ opt = torch.optim.Adam(params, lr=args.lr)
81
+
82
+ steps_per_epoch = N // args.batch
83
+ print(f"training: {args.epochs} epochs x {steps_per_epoch} steps "
84
+ f"(batch={args.batch}, pixels/step={args.pixels}, lr={args.lr}, "
85
+ f"params={N_PARAMS})\n")
86
+
87
+ t0 = time.time()
88
+ for epoch in range(1, args.epochs + 1):
89
+ order = rng.permutation(N)
90
+ ep_loss, ep_steps = 0.0, 0
91
+ for s in range(steps_per_epoch):
92
+ idx = order[s * args.batch:(s + 1) * args.batch]
93
+ pix = torch.from_numpy(rng.choice(res * res, size=args.pixels, replace=False)).to(device)
94
+
95
+ emb = embs[idx]
96
+ tgt = targets[idx][:, pix, :] # (B, P, 3)
97
+
98
+ z = encode_prompt(weights, emb)
99
+ pred = decode_pixels(weights, z, feats_all[pix])
100
+ loss = torch.nn.functional.mse_loss(pred, tgt)
101
+
102
+ opt.zero_grad()
103
+ loss.backward()
104
+ opt.step()
105
+ with torch.no_grad():
106
+ for w in params:
107
+ w.clamp_(-WMAX, WMAX)
108
+
109
+ ep_loss += loss.item()
110
+ ep_steps += 1
111
+
112
+ save_model(weights, MODEL_PATH)
113
+ elapsed = time.time() - t0
114
+ print(f"epoch {epoch:>3}/{args.epochs} loss={ep_loss / ep_steps:.5f} "
115
+ f"elapsed={elapsed:.0f}s -> saved {MODEL_PATH}", flush=True)
116
+
117
+ print(f"\nDone in {time.time() - t0:.0f}s. Final model saved to {MODEL_PATH}")
118
+
119
+
120
+ if __name__ == "__main__":
121
+ main()