wop commited on
Commit
38bfe91
·
verified ·
1 Parent(s): 754a252

Upload 23 files

Browse files
.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
- license: mit
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

  • SHA256: 65406c7e694bb06735f9dd106ea340bdea8a7fd662037dbed0165ca69ff35244
  • Pointer size: 131 Bytes
  • Size of remote file: 292 kB
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)