Costi Claude Sonnet 5 commited on
Commit Β·
b451f49
1
Parent(s): d9a3537
Add safetensors weights and standalone INFERENCE.py
Browse filesExports the same 3 weight matrices already encoded in model.png to
model.safetensors via convert_to_safetensors.py, and adds INFERENCE.py
as a standalone entry point that loads from the safetensors file
instead of the PNG. Verified byte-identical output between main.py
and INFERENCE.py for the same prompt. README documents both paths.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- INFERENCE.py +77 -0
- README.md +26 -4
- convert_to_safetensors.py +47 -0
- model.safetensors +3 -0
INFERENCE.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
INFERENCE.py β run PixelModel from its model.safetensors weights.
|
| 3 |
+
|
| 4 |
+
This is the standalone entry point for anyone who just wants to load the
|
| 5 |
+
safetensors weights and generate an image, without needing model.png or
|
| 6 |
+
the rest of this repo's training code.
|
| 7 |
+
|
| 8 |
+
Usage:
|
| 9 |
+
python INFERENCE.py "a red circle"
|
| 10 |
+
python INFERENCE.py "a red circle" --model model.safetensors --out out.png --scale 8
|
| 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 |
+
PROMPT_DIM = 32
|
| 23 |
+
OUT_SIZE = 32
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def prompt_to_embedding(prompt: str) -> torch.Tensor:
|
| 27 |
+
"""Deterministic char-level embedding -> PROMPT_DIM vector."""
|
| 28 |
+
vec = torch.zeros(PROMPT_DIM)
|
| 29 |
+
for i, ch in enumerate(prompt.lower()):
|
| 30 |
+
idx = i % PROMPT_DIM
|
| 31 |
+
vec[idx] += ord(ch) / 127.0
|
| 32 |
+
norm = vec.norm()
|
| 33 |
+
if norm > 0:
|
| 34 |
+
vec = vec / norm
|
| 35 |
+
return vec
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def forward(weights: dict, prompt: str) -> torch.Tensor:
|
| 39 |
+
emb = prompt_to_embedding(prompt)
|
| 40 |
+
x = torch.tanh(weights["W1"] @ emb)
|
| 41 |
+
x = torch.tanh(weights["W2"] @ x)
|
| 42 |
+
x = torch.sigmoid(weights["W3"] @ x)
|
| 43 |
+
return x.reshape(OUT_SIZE, OUT_SIZE, 3)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def generate(prompt: str, model_path: str, out_path: str, scale: int = 8):
|
| 47 |
+
if not os.path.exists(model_path):
|
| 48 |
+
sys.exit(
|
| 49 |
+
f"Model not found: {model_path}\n"
|
| 50 |
+
f"Run: python convert_to_safetensors.py to create one from model.png first."
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
weights = load_file(model_path)
|
| 54 |
+
|
| 55 |
+
with torch.no_grad():
|
| 56 |
+
result = forward(weights, prompt)
|
| 57 |
+
|
| 58 |
+
arr = (result.numpy() * 255).clip(0, 255).astype(np.uint8)
|
| 59 |
+
img = Image.fromarray(arr, mode="RGB")
|
| 60 |
+
|
| 61 |
+
if scale > 1:
|
| 62 |
+
img = img.resize((OUT_SIZE * scale, OUT_SIZE * scale), Image.NEAREST)
|
| 63 |
+
|
| 64 |
+
img.save(out_path)
|
| 65 |
+
print(f"prompt : '{prompt}'")
|
| 66 |
+
print(f"model : {model_path} ({os.path.getsize(model_path)} bytes, safetensors)")
|
| 67 |
+
print(f"output : {out_path} ({OUT_SIZE * scale}x{OUT_SIZE * scale} px)")
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
if __name__ == "__main__":
|
| 71 |
+
p = argparse.ArgumentParser(description="PixelModel inference (safetensors)")
|
| 72 |
+
p.add_argument("prompt", help="Text prompt")
|
| 73 |
+
p.add_argument("--model", default="model.safetensors", help="Path to safetensors weights")
|
| 74 |
+
p.add_argument("--out", default="out.png", help="Output image path")
|
| 75 |
+
p.add_argument("--scale", type=int, default=8, help="Upscale factor for output (default 8 -> 256x256)")
|
| 76 |
+
args = p.parse_args()
|
| 77 |
+
generate(args.prompt, args.model, args.out, args.scale)
|
README.md
CHANGED
|
@@ -40,6 +40,19 @@ All weights live inside `model.png`.
|
|
| 40 |
|
| 41 |
---
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
## π§ͺ Dataset vs Outputs
|
| 44 |
|
| 45 |
| Target | Output |
|
|
@@ -56,10 +69,13 @@ All weights live inside `model.png`.
|
|
| 56 |
## π Files
|
| 57 |
|
| 58 |
```text
|
| 59 |
-
model.png
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
| 63 |
dataset/
|
| 64 |
red.png
|
| 65 |
red.txt β prompt: "red"
|
|
@@ -76,8 +92,14 @@ python train.py --epochs 500 --lr 0.05
|
|
| 76 |
|
| 77 |
python main.py "red"
|
| 78 |
python main.py "a cat" --out cat.png --scale 8
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
```
|
| 80 |
|
|
|
|
|
|
|
| 81 |
---
|
| 82 |
|
| 83 |
## π Tips
|
|
|
|
| 40 |
|
| 41 |
---
|
| 42 |
|
| 43 |
+
## π¦ Standard weights (safetensors)
|
| 44 |
+
|
| 45 |
+
`model.png` is the canonical model β training writes to it directly, and it's what makes PixelModel PixelModel. For tooling that expects standard weight files, the same 3 matrices are also exported as `model.safetensors` (202,752 parameters total, no bias terms):
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
python convert_to_safetensors.py # model.png -> model.safetensors
|
| 49 |
+
python convert_to_safetensors.py --model model.png --out model.safetensors
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
Re-run this after training if you retrain into a new `model.png` β `model.safetensors` doesn't update itself.
|
| 53 |
+
|
| 54 |
+
---
|
| 55 |
+
|
| 56 |
## π§ͺ Dataset vs Outputs
|
| 57 |
|
| 58 |
| Target | Output |
|
|
|
|
| 69 |
## π Files
|
| 70 |
|
| 71 |
```text
|
| 72 |
+
model.png β THE MODEL (64Γ3200 px)
|
| 73 |
+
model.safetensors β same weights, standard format (generated, see below)
|
| 74 |
+
main.py β inference, loads model.png
|
| 75 |
+
INFERENCE.py β inference, loads model.safetensors
|
| 76 |
+
convert_to_safetensors.py β model.png -> model.safetensors
|
| 77 |
+
train.py β training
|
| 78 |
+
model.py β architecture
|
| 79 |
dataset/
|
| 80 |
red.png
|
| 81 |
red.txt β prompt: "red"
|
|
|
|
| 92 |
|
| 93 |
python main.py "red"
|
| 94 |
python main.py "a cat" --out cat.png --scale 8
|
| 95 |
+
|
| 96 |
+
# equivalent, but loads model.safetensors instead of model.png
|
| 97 |
+
python convert_to_safetensors.py
|
| 98 |
+
python INFERENCE.py "a cat" --out cat.png --scale 8
|
| 99 |
```
|
| 100 |
|
| 101 |
+
`main.py` and `INFERENCE.py` produce byte-identical output for the same prompt β they're the same architecture and weights, just loaded from different files.
|
| 102 |
+
|
| 103 |
---
|
| 104 |
|
| 105 |
## π Tips
|
convert_to_safetensors.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
convert_to_safetensors.py β extract PixelModel's weights from model.png
|
| 3 |
+
and save them as model.safetensors.
|
| 4 |
+
|
| 5 |
+
model.png remains the canonical "weights ARE the image" artifact (see
|
| 6 |
+
README.md); this just re-exports its contents in a standard, directly
|
| 7 |
+
loadable format for tooling that expects .safetensors weights.
|
| 8 |
+
|
| 9 |
+
Usage:
|
| 10 |
+
python convert_to_safetensors.py
|
| 11 |
+
python convert_to_safetensors.py --model model.png --out model.safetensors
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import argparse
|
| 15 |
+
from safetensors.torch import save_file
|
| 16 |
+
|
| 17 |
+
from model import load_model, pixels_to_weights
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def convert(model_path: str, out_path: str):
|
| 21 |
+
pixels = load_model(model_path)
|
| 22 |
+
W1, W2, W3 = pixels_to_weights(pixels)
|
| 23 |
+
|
| 24 |
+
save_file(
|
| 25 |
+
{
|
| 26 |
+
"W1": W1.contiguous(),
|
| 27 |
+
"W2": W2.contiguous(),
|
| 28 |
+
"W3": W3.contiguous(),
|
| 29 |
+
},
|
| 30 |
+
out_path,
|
| 31 |
+
metadata={
|
| 32 |
+
"format": "pt",
|
| 33 |
+
"source": model_path,
|
| 34 |
+
"architecture": "PixelModel 3-layer MLP (char-embed -> tanh -> tanh -> sigmoid)",
|
| 35 |
+
},
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
total = W1.numel() + W2.numel() + W3.numel()
|
| 39 |
+
print(f"Wrote {out_path} ({total:,} parameters: W1={tuple(W1.shape)}, W2={tuple(W2.shape)}, W3={tuple(W3.shape)})")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
if __name__ == "__main__":
|
| 43 |
+
p = argparse.ArgumentParser(description="Export PixelModel weights (from model.png) to safetensors")
|
| 44 |
+
p.add_argument("--model", default="model.png", help="Path to source model PNG")
|
| 45 |
+
p.add_argument("--out", default="model.safetensors", help="Output safetensors path")
|
| 46 |
+
args = p.parse_args()
|
| 47 |
+
convert(args.model, args.out)
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8c365a7f71955241d343da88796e7c221f88b040e93006e9f1a46ecf5b36f10a
|
| 3 |
+
size 811344
|