Upload 23 files
Browse files- .gitattributes +1 -0
- README.md +72 -3
- dataset/blue.png +0 -0
- dataset/blue.txt +1 -0
- dataset/dark.png +0 -0
- dataset/dark.txt +1 -0
- dataset/green.png +0 -0
- dataset/green.txt +1 -0
- dataset/red.png +0 -0
- dataset/red.txt +1 -0
- dataset/white.png +0 -0
- dataset/white.txt +1 -0
- dataset/yellow.png +0 -0
- dataset/yellow.txt +1 -0
- main.py +53 -0
- model.png +3 -0
- model.py +124 -0
- out_blue.png +0 -0
- out_dark.png +0 -0
- out_green.png +0 -0
- out_red.png +0 -0
- out_white.png +0 -0
- out_yellow.png +0 -0
- train.py +110 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* 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
|
|
|
|
|
|
| 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
|
| 36 |
+
model.png filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,3 +1,72 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PixelModel 🖼️
|
| 2 |
+
|
| 3 |
+
> A neural network where the weights **are** the image.
|
| 4 |
+
|
| 5 |
+
## What is this?
|
| 6 |
+
|
| 7 |
+
`model.png` is not a picture of anything. It *is* the model.
|
| 8 |
+
Every pixel's RGB values store neural network weights:
|
| 9 |
+
- **R channel** → weight magnitude
|
| 10 |
+
- **B channel** → weight sign (≥128 = positive)
|
| 11 |
+
- **G channel** → bias values
|
| 12 |
+
|
| 13 |
+
At inference, pixels are parsed into 3 weight matrices (a tiny MLP),
|
| 14 |
+
your prompt is embedded into a small vector, and a forward pass
|
| 15 |
+
produces a 32×32 output image.
|
| 16 |
+
|
| 17 |
+
Training = gradient descent directly on pixel values. The PNG gets
|
| 18 |
+
updated until it can reproduce all images in the dataset.
|
| 19 |
+
|
| 20 |
+
## Files
|
| 21 |
+
|
| 22 |
+
```
|
| 23 |
+
model.png ← THE MODEL (64×3200 px, ~284 KB)
|
| 24 |
+
main.py ← inference
|
| 25 |
+
train.py ← training
|
| 26 |
+
model.py ← architecture (pixels → weights → forward pass)
|
| 27 |
+
dataset/ ← your training data
|
| 28 |
+
cat.png
|
| 29 |
+
cat.txt ← contains the prompt: "a cat"
|
| 30 |
+
...
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
## Usage
|
| 34 |
+
|
| 35 |
+
**Train:**
|
| 36 |
+
```bash
|
| 37 |
+
# Add image+prompt pairs to dataset/, then:
|
| 38 |
+
python train.py
|
| 39 |
+
python train.py --epochs 500 --lr 0.05
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
**Generate:**
|
| 43 |
+
```bash
|
| 44 |
+
python main.py "red"
|
| 45 |
+
python main.py "a cat" --out cat_out.png --scale 8
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
`--scale 8` upscales 32×32 → 256×256 with nearest-neighbour (keeps the pixel look).
|
| 49 |
+
|
| 50 |
+
## Architecture
|
| 51 |
+
|
| 52 |
+
```
|
| 53 |
+
prompt string
|
| 54 |
+
→ char-level embedding → 32-dim vector
|
| 55 |
+
→ W1 (64×32) → tanh
|
| 56 |
+
→ W2 (64×64) → tanh
|
| 57 |
+
→ W3 (3072×64) → sigmoid
|
| 58 |
+
→ reshape → 32×32×3 image
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
All weights live in `model.png`. The file you open in Preview **is** the neural net.
|
| 62 |
+
|
| 63 |
+
## Dataset tips
|
| 64 |
+
|
| 65 |
+
- 6–20 pairs works fine
|
| 66 |
+
- Simpler targets = faster convergence (solid colors, gradients, simple shapes)
|
| 67 |
+
- Run 200–500 epochs, loss should drop below 0.001 for simple datasets
|
| 68 |
+
- The model capacity is fixed (64×3200 pixels = ~600K "parameters")
|
| 69 |
+
|
| 70 |
+
## Why?
|
| 71 |
+
|
| 72 |
+
It's a toy. It's not useful. But it's cool that it works.
|
dataset/blue.png
ADDED
|
|
dataset/blue.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
blue
|
dataset/dark.png
ADDED
|
|
dataset/dark.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
dark
|
dataset/green.png
ADDED
|
|
dataset/green.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
green
|
dataset/red.png
ADDED
|
|
dataset/red.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
red
|
dataset/white.png
ADDED
|
|
dataset/white.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
white
|
dataset/yellow.png
ADDED
|
|
dataset/yellow.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
yellow
|
main.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
main.py – run the pixel model.
|
| 3 |
+
|
| 4 |
+
Usage:
|
| 5 |
+
python main.py "a red circle"
|
| 6 |
+
python main.py "a red circle" --model model.png --out out.png --scale 8
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import argparse
|
| 10 |
+
import sys
|
| 11 |
+
import os
|
| 12 |
+
import numpy as np
|
| 13 |
+
from PIL import Image
|
| 14 |
+
import torch
|
| 15 |
+
|
| 16 |
+
from model import load_model, forward, OUT_SIZE
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def generate(prompt: str, model_path: str, out_path: str, scale: int = 8):
|
| 20 |
+
if not os.path.exists(model_path):
|
| 21 |
+
sys.exit(f"Model not found: {model_path}\n"
|
| 22 |
+
f"Run: python train.py to create one first.")
|
| 23 |
+
|
| 24 |
+
pixels = load_model(model_path) # (H, W, 3)
|
| 25 |
+
|
| 26 |
+
with torch.no_grad():
|
| 27 |
+
result = forward(pixels, prompt) # (32, 32, 3)
|
| 28 |
+
|
| 29 |
+
arr = (result.numpy() * 255).clip(0, 255).astype(np.uint8)
|
| 30 |
+
img = Image.fromarray(arr, mode="RGB")
|
| 31 |
+
|
| 32 |
+
# upscale for visibility (nearest-neighbour keeps pixel look)
|
| 33 |
+
if scale > 1:
|
| 34 |
+
img = img.resize(
|
| 35 |
+
(OUT_SIZE * scale, OUT_SIZE * scale),
|
| 36 |
+
Image.NEAREST
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
img.save(out_path)
|
| 40 |
+
print(f"prompt : '{prompt}'")
|
| 41 |
+
print(f"model : {model_path} ({os.path.getsize(model_path)//1024} KB)")
|
| 42 |
+
print(f"output : {out_path} ({OUT_SIZE*scale}×{OUT_SIZE*scale} px)")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
if __name__ == "__main__":
|
| 46 |
+
p = argparse.ArgumentParser(description="PixelModel inference")
|
| 47 |
+
p.add_argument("prompt", help="Text prompt")
|
| 48 |
+
p.add_argument("--model", default="model.png", help="Path to model PNG")
|
| 49 |
+
p.add_argument("--out", default="out.png", help="Output image path")
|
| 50 |
+
p.add_argument("--scale", type=int, default=8,
|
| 51 |
+
help="Upscale factor for output (default 8 → 256×256)")
|
| 52 |
+
args = p.parse_args()
|
| 53 |
+
generate(args.prompt, args.model, args.out, args.scale)
|
model.png
ADDED
|
|
Git LFS Details
|
model.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PixelModel - the weights ARE the image.
|
| 3 |
+
|
| 4 |
+
model.png stores all weights as pixel RGB values (0-255 → floats).
|
| 5 |
+
Architecture:
|
| 6 |
+
- Row 0..H//3-1 → Layer 1 weights (prompt_dim → hidden)
|
| 7 |
+
- Row H//3..2*H//3-1 → Layer 2 weights (hidden → hidden)
|
| 8 |
+
- Row 2*H//3..H-1 → Layer 3 weights (hidden → output_flat)
|
| 9 |
+
|
| 10 |
+
Width W = max(prompt_dim, hidden, output_flat), padded/sliced as needed.
|
| 11 |
+
RGB channels encode sign & magnitude:
|
| 12 |
+
R = weight magnitude (0..255 → 0..1)
|
| 13 |
+
G = reserved (bias values, layer 1)
|
| 14 |
+
B = sign bit: <128 = negative, >=128 = positive
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
import torch.nn.functional as F
|
| 19 |
+
import numpy as np
|
| 20 |
+
from PIL import Image
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# ── config ────────────────────────────────────────────────────────────────────
|
| 24 |
+
PROMPT_DIM = 32 # prompt embedding size
|
| 25 |
+
HIDDEN = 64 # hidden layer width
|
| 26 |
+
OUT_SIZE = 32 # output image side length
|
| 27 |
+
OUT_FLAT = OUT_SIZE * OUT_SIZE * 3 # 3072
|
| 28 |
+
|
| 29 |
+
# model.png dimensions that fit all weights
|
| 30 |
+
# Layer sizes: (PROMPT_DIM→HIDDEN), (HIDDEN→HIDDEN), (HIDDEN→OUT_FLAT)
|
| 31 |
+
# We stack weight matrices vertically, each row = one output neuron's weights
|
| 32 |
+
# pad width to W = max of all input dims
|
| 33 |
+
MODEL_W = max(PROMPT_DIM, HIDDEN) # 64
|
| 34 |
+
MODEL_H = HIDDEN + HIDDEN + OUT_FLAT # 64+64+3072 = 3200
|
| 35 |
+
# → model.png is 64×3200 px (~600 KB uncompressed, tiny compressed)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def prompt_to_embedding(prompt: str) -> torch.Tensor:
|
| 39 |
+
"""Deterministic char-level embedding → PROMPT_DIM vector."""
|
| 40 |
+
vec = torch.zeros(PROMPT_DIM)
|
| 41 |
+
for i, ch in enumerate(prompt.lower()):
|
| 42 |
+
idx = i % PROMPT_DIM
|
| 43 |
+
vec[idx] += (ord(ch) / 127.0)
|
| 44 |
+
# normalise
|
| 45 |
+
norm = vec.norm()
|
| 46 |
+
if norm > 0:
|
| 47 |
+
vec = vec / norm
|
| 48 |
+
return vec # shape (PROMPT_DIM,)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def pixels_to_weights(pixels: torch.Tensor) -> tuple:
|
| 52 |
+
"""
|
| 53 |
+
pixels: (H, W, 3) float32 tensor, values 0..1
|
| 54 |
+
|
| 55 |
+
Returns W1, W2, W3 weight matrices.
|
| 56 |
+
"""
|
| 57 |
+
H, W, _ = pixels.shape
|
| 58 |
+
|
| 59 |
+
r = pixels[:, :, 0] # magnitude 0..1
|
| 60 |
+
b = pixels[:, :, 2] # sign: <0.5 = neg, >=0.5 = pos
|
| 61 |
+
|
| 62 |
+
# sign-magnitude → signed float in roughly [-1, 1]
|
| 63 |
+
sign = torch.where(b >= 0.5, torch.ones_like(b), -torch.ones_like(b))
|
| 64 |
+
vals = sign * r # signed weights
|
| 65 |
+
|
| 66 |
+
row = 0
|
| 67 |
+
|
| 68 |
+
# W1: HIDDEN × PROMPT_DIM
|
| 69 |
+
W1_rows = HIDDEN
|
| 70 |
+
W1 = vals[row: row + W1_rows, :PROMPT_DIM] # (64, 32)
|
| 71 |
+
row += W1_rows
|
| 72 |
+
|
| 73 |
+
# W2: HIDDEN × HIDDEN
|
| 74 |
+
W2_rows = HIDDEN
|
| 75 |
+
W2 = vals[row: row + W2_rows, :HIDDEN] # (64, 64)
|
| 76 |
+
row += W2_rows
|
| 77 |
+
|
| 78 |
+
# W3: OUT_FLAT × HIDDEN
|
| 79 |
+
W3_rows = OUT_FLAT
|
| 80 |
+
W3 = vals[row: row + W3_rows, :HIDDEN] # (3072, 64)
|
| 81 |
+
row += W3_rows
|
| 82 |
+
|
| 83 |
+
return W1, W2, W3
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def forward(pixels: torch.Tensor, prompt: str) -> torch.Tensor:
|
| 87 |
+
"""
|
| 88 |
+
pixels : (H, W, 3) float32, values 0..1 ← the model
|
| 89 |
+
prompt : str
|
| 90 |
+
returns: (OUT_SIZE, OUT_SIZE, 3) float32, values 0..1
|
| 91 |
+
"""
|
| 92 |
+
emb = prompt_to_embedding(prompt) # (32,)
|
| 93 |
+
W1, W2, W3 = pixels_to_weights(pixels)
|
| 94 |
+
|
| 95 |
+
x = torch.tanh(W1 @ emb) # (64,)
|
| 96 |
+
x = torch.tanh(W2 @ x) # (64,)
|
| 97 |
+
x = torch.sigmoid(W3 @ x) # (3072,) values 0..1
|
| 98 |
+
|
| 99 |
+
img = x.reshape(OUT_SIZE, OUT_SIZE, 3)
|
| 100 |
+
return img
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def load_model(path: str) -> torch.Tensor:
|
| 104 |
+
"""Load model.png → float tensor (H, W, 3) in [0,1]."""
|
| 105 |
+
img = Image.open(path).convert("RGB")
|
| 106 |
+
arr = np.array(img, dtype=np.float32) / 255.0
|
| 107 |
+
return torch.tensor(arr)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def save_model(pixels: torch.Tensor, path: str):
|
| 111 |
+
"""Save float tensor (H, W, 3) in [0,1] → model.png."""
|
| 112 |
+
arr = (pixels.detach().clamp(0, 1).numpy() * 255).astype(np.uint8)
|
| 113 |
+
Image.fromarray(arr, mode="RGB").save(path)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def init_model(path: str):
|
| 117 |
+
"""Create a fresh random model.png."""
|
| 118 |
+
# small random weights around 0 → R~0.1, B random sign
|
| 119 |
+
r = torch.rand(MODEL_H, MODEL_W) * 0.2 # small magnitude
|
| 120 |
+
g = torch.zeros(MODEL_H, MODEL_W)
|
| 121 |
+
b = (torch.rand(MODEL_H, MODEL_W) > 0.5).float() # random sign
|
| 122 |
+
pixels = torch.stack([r, g, b], dim=2)
|
| 123 |
+
save_model(pixels, path)
|
| 124 |
+
print(f"Initialised model: {path} ({MODEL_W}×{MODEL_H} px)")
|
out_blue.png
ADDED
|
|
out_dark.png
ADDED
|
|
out_green.png
ADDED
|
|
out_red.png
ADDED
|
|
out_white.png
ADDED
|
|
out_yellow.png
ADDED
|
|
train.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
train.py – optimize pixel values via gradient descent.
|
| 3 |
+
|
| 4 |
+
Dataset: dataset/ folder containing pairs:
|
| 5 |
+
dataset/cat.png + dataset/cat.txt (image + its prompt)
|
| 6 |
+
dataset/sun.png + dataset/sun.txt
|
| 7 |
+
...
|
| 8 |
+
|
| 9 |
+
Each .txt holds one line: the prompt for that image.
|
| 10 |
+
Target images are resized to OUT_SIZE×OUT_SIZE automatically.
|
| 11 |
+
|
| 12 |
+
Usage:
|
| 13 |
+
python train.py # train with defaults
|
| 14 |
+
python train.py --epochs 500 --lr 0.05
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import argparse
|
| 18 |
+
import os
|
| 19 |
+
import sys
|
| 20 |
+
import torch
|
| 21 |
+
import torch.nn.functional as F
|
| 22 |
+
import numpy as np
|
| 23 |
+
from PIL import Image
|
| 24 |
+
|
| 25 |
+
from model import (
|
| 26 |
+
MODEL_H, MODEL_W, OUT_SIZE,
|
| 27 |
+
forward, load_model, save_model, init_model,
|
| 28 |
+
pixels_to_weights, prompt_to_embedding,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
DATASET_DIR = "dataset"
|
| 32 |
+
MODEL_PATH = "model.png"
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def load_dataset():
|
| 36 |
+
pairs = []
|
| 37 |
+
for fname in sorted(os.listdir(DATASET_DIR)):
|
| 38 |
+
if not fname.lower().endswith((".png", ".jpg", ".jpeg")):
|
| 39 |
+
continue
|
| 40 |
+
stem = os.path.splitext(fname)[0]
|
| 41 |
+
txt_path = os.path.join(DATASET_DIR, stem + ".txt")
|
| 42 |
+
img_path = os.path.join(DATASET_DIR, fname)
|
| 43 |
+
if not os.path.exists(txt_path):
|
| 44 |
+
print(f" [skip] no .txt for {fname}")
|
| 45 |
+
continue
|
| 46 |
+
with open(txt_path) as f:
|
| 47 |
+
prompt = f.read().strip()
|
| 48 |
+
img = Image.open(img_path).convert("RGB").resize(
|
| 49 |
+
(OUT_SIZE, OUT_SIZE), Image.BILINEAR
|
| 50 |
+
)
|
| 51 |
+
target = torch.tensor(
|
| 52 |
+
np.array(img, dtype=np.float32) / 255.0
|
| 53 |
+
) # (32,32,3)
|
| 54 |
+
pairs.append((prompt, target))
|
| 55 |
+
print(f" loaded: '{prompt}' ← {fname}")
|
| 56 |
+
return pairs
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def train(epochs=300, lr=0.03, save_every=50):
|
| 60 |
+
# init model if missing
|
| 61 |
+
if not os.path.exists(MODEL_PATH):
|
| 62 |
+
init_model(MODEL_PATH)
|
| 63 |
+
|
| 64 |
+
raw = load_model(MODEL_PATH) # (H, W, 3)
|
| 65 |
+
|
| 66 |
+
# pixels are the parameters — wrap in a leaf tensor
|
| 67 |
+
pixels = raw.clone().requires_grad_(True)
|
| 68 |
+
optimizer = torch.optim.Adam([pixels], lr=lr)
|
| 69 |
+
|
| 70 |
+
print(f"\nLoading dataset from '{DATASET_DIR}/'…")
|
| 71 |
+
dataset = load_dataset()
|
| 72 |
+
if not dataset:
|
| 73 |
+
sys.exit("No dataset pairs found. Add image+txt pairs to dataset/")
|
| 74 |
+
|
| 75 |
+
print(f"\nTraining epochs={epochs} lr={lr} pairs={len(dataset)}\n")
|
| 76 |
+
|
| 77 |
+
for epoch in range(1, epochs + 1):
|
| 78 |
+
optimizer.zero_grad()
|
| 79 |
+
total_loss = torch.tensor(0.0)
|
| 80 |
+
|
| 81 |
+
for prompt, target in dataset:
|
| 82 |
+
# pixels must stay in [0,1] for the sign/magnitude encoding
|
| 83 |
+
p_clamped = pixels.clamp(0, 1)
|
| 84 |
+
pred = forward(p_clamped, prompt) # (32,32,3)
|
| 85 |
+
loss = F.mse_loss(pred, target)
|
| 86 |
+
total_loss = total_loss + loss
|
| 87 |
+
|
| 88 |
+
total_loss.backward()
|
| 89 |
+
optimizer.step()
|
| 90 |
+
|
| 91 |
+
# clamp pixels back to valid range after update
|
| 92 |
+
with torch.no_grad():
|
| 93 |
+
pixels.clamp_(0, 1)
|
| 94 |
+
|
| 95 |
+
if epoch % save_every == 0 or epoch == 1:
|
| 96 |
+
save_model(pixels, MODEL_PATH)
|
| 97 |
+
avg = total_loss.item() / len(dataset)
|
| 98 |
+
print(f" epoch {epoch:>5}/{epochs} loss={avg:.5f} → saved model.png")
|
| 99 |
+
|
| 100 |
+
save_model(pixels, MODEL_PATH)
|
| 101 |
+
print(f"\nDone. Final model saved to {MODEL_PATH}")
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
if __name__ == "__main__":
|
| 105 |
+
p = argparse.ArgumentParser()
|
| 106 |
+
p.add_argument("--epochs", type=int, default=300)
|
| 107 |
+
p.add_argument("--lr", type=float, default=0.03)
|
| 108 |
+
p.add_argument("--save-every", type=int, default=50)
|
| 109 |
+
args = p.parse_args()
|
| 110 |
+
train(args.epochs, args.lr, args.save_every)
|