Upload 17 files
Browse files- .gitattributes +2 -35
- .gitignore +2 -0
- EVAL_REPRODUCTION.md +98 -0
- INFERENCE.py +104 -0
- README.md +210 -3
- config.json +35 -0
- convert_to_safetensors.py +48 -0
- fetch_coco_subset.py +124 -0
- main.py +43 -0
- model.png +0 -0
- model.py +210 -0
- model.safetensors +3 -0
- pixelmodel-v1-loss.png +0 -0
- pixelmodel-v1-probe.png +0 -0
- pixelmodel_v2_kaggle.ipynb +103 -0
- run_eval.py +136 -0
- train.py +121 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,2 @@
|
|
| 1 |
-
*.
|
| 2 |
-
*.
|
| 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()
|