|
Download experiments/README.md from DJLougen/pixelgen2: direct link, hf CLI and curl.
- Browser
- Download file 6.63 kB
-
https://huggingface.co/DJLougen/pixelgen2/resolve/main/experiments/README.md
- Command line
-
hf download hf://DJLougen/pixelgen2/experiments/README.md
-
curl -L -o README.md https://huggingface.co/DJLougen/pixelgen2/resolve/main/experiments/README.md
6.63 kB
| # Experiments: how far does hand-written capacity go? | |
| Every number below was produced by a script in this folder. No training anywhere. | |
| | script | question | answer | | |
| |---|---|---| | |
| | `add_class.py` | what changes when you add one class? | +400 values, 0 semantic edits to old weights, 0 behaviour change on old prompts | | |
| | `scale_classes.py` | how many classes before it breaks? | it doesn't; 10,021 classes still bit-isolated — but the word→class matrix is O(C²) | | |
| | `coded_classes.py` | can the class layer be compressed? | 528× at 10k classes via binary codes, at the cost of 61% multi-word interference | | |
| | `count_outputs.py` | what does composition buy? | 21,866 distinct images from 28 templates; 115× output for 1.6× params | | |
| ## 1. Adding one class is a pure append (in function space) | |
| `before.pt` is the frozen 11-class model. After adding `mushroom`: | |
| ``` | |
| tensor shapes on_bank (11,16,8)->(12,16,8) word_to_class (11,59)->(12,63) +400 values | |
| raw index diff 84 old values "changed" | |
| semantic diff 0 old (class,word) entries changed; shade/ramp/outline/palette identical | |
| old prompts 17/17 bit-identical over 3 seeds; only mushroom-word prompts moved (potion -> mushroom) | |
| ``` | |
| The 84-value raw diff is entirely a **column permutation**: the vocab is `sorted()`, so inserting `fungus` at column 11 shifts 48 of 59 old words right. Diffed by meaning, nothing moved. Lesson: if you want weight diffs to be legible, make index assignment append-only. | |
| The same check still passes after +10 hand-drawn classes and the accessory vocabulary: v1 renders the original 11 classes bit-identically. | |
| ## 2. Capacity is unbounded; addressing is quadratic | |
| Synthetic random classes (3 nonsense words each), rebuilt and probed at each size: | |
| ``` | |
| classes vocab params buffers MB dense% gen ms isolated | |
| 21 95 2,937 6,171 0.04 21.9% 0.12 True | |
| 121 395 51,537 31,771 0.33 57.4% 0.13 True | |
| 1,021 3,095 3,188,937 262,171 13.80 91.6% 0.25 True | |
| 3,021 9,095 27,560,937 774,171 113.34 97.0% 1.45 True | |
| 10,021 30,095 301,862,937 2,566,171 1217.72 99.1% 11.95 True | |
| ``` | |
| `isolated` = the 21 real classes and probe prompts (`red potion`, `dragon`, `""`, `bone`) are bit-identical to the 21-class model, and the newest synthetic class resolves by all its synonyms. It never fails. | |
| But vocab grows with classes, so `word_to_class` is V×C ≈ 3C² — at 10k classes it's 99.1% of the model and 99.997% zeros. Under a 100k-parameter budget the dense design tops out around **170 classes**. | |
| ## 3. Compressing the class layer buys size, costs semantics | |
| Replace `Linear(V,C)` with a binary code: each word emits its class's d=⌈log₂C⌉-bit code as ±1, a `Linear(d,C)` of code rows scores `d − 2·hamming`. | |
| ``` | |
| classes d dense params coded params ratio isolated interference | |
| 21 5 2,016 601 3.4x True 61% | |
| 121 7 47,916 3,733 12.8x True 61% | |
| 1,021 10 3,161,016 42,181 74.9x True 61% | |
| 3,021 12 27,479,016 148,413 185.2x True 61% | |
| 10,021 14 301,592,016 571,645 527.6x True 61% | |
| ``` | |
| Isolation survives. But `interference` — the share of `"wordA wordB"` prompts (over the 21 real classes) that resolve to a **third** class — jumps from 0% (dense; ties go to lower index) to 61%, flat across scale. The sum of two codes is nearest to some unrelated code. The bag-of-words layer's additive semantics were the thing being compressed away. Open question: do error-correcting or sparse k-hot codes recover 0% interference, and at what parameter cost? | |
| ## 4. Composition: the lever that actually matters | |
| `PixelGen2` (`pixelgen/compose.py`) adds four fixed-weight layers to the same model: | |
| | layer | mechanism | params | | |
| |---|---|---| | |
| | accessory | `Linear(V,8)` WTA → 16×8 part overlaid with `max` | 1,112 + 1,162 buffer | | |
| | second colour | `Linear(V,9)` over words after `with`/`wearing`/…; body ramp wins ties via a 0.5 one-hot bonus | 1,251 | | |
| | structural shading | two 3×3 convs: light where up-left neighbour is empty, dark where down-right is | 18 | | |
| | size | `Linear(V,1)` → nearest 0.75× resample, bottom-centred | 139 | | |
| ``` | |
| PixelGen : 4,227 params + 6,171 buffers | 189 prompts -> 189 distinct images | |
| PixelGen2: 6,747 params + 7,333 buffers | 24,192 prompts -> 21,866 distinct images | |
| hand-drawn content: 21 bodies + 7 parts = 28 templates | |
| distinct images per template: 9 -> 781 | |
| param growth 1.60x output growth 115.7x | |
| ``` | |
| (The 2,326 duplicates are prompts whose accessory colour equals the body colour, or whose accessory is hidden behind the body.) | |
| ### Quality, honestly | |
|  | |
| Top: row-band shading (v1). Bottom: structural rim lighting (v2), 18 parameters. It helps blobby shapes — potion, heart, mushroom, tree, gem get a readable highlight and shadow — and **hurts thin ones**: invader, bat, robot turn to speckle because every pixel is an edge. A fixed 3×3 kernel has no notion of "this shape is too thin to light". That's the limit of rule-based shading without a learned prior, and it's exactly the kind of thing a few hundred trained parameters would fix. | |
| Accessories are placed at fixed rows, so a crown floats above short bodies and a helmet merges into tall ones. Fixing that properly needs per-body anchor points — more hand-written content, not more mechanism. | |
| ## What this says about weight-space programming | |
| - **Adding capacity is free and modular.** Hand-written weights are block-diagonal by construction; trained weights are not. Add-a-class is `dict.append`. | |
| - **The expensive part is the index, not the content.** Dense lookup is O(C²); the content (templates) is O(C). Compression trades additivity for size. | |
| - **Composition beats enumeration by two orders of magnitude**, and it's the only route to "many things" that doesn't scale a human's drawing time linearly. | |
| - **Quality rules written by hand are brittle at the edges** — the shading result is the clearest example. The honest endpoint for pure weight-space programming is a very capable procedural generator with legible, auditable failure modes, and zero creativity outside what was drawn. | |
| - The natural next step is the hybrid the blog post gestures at: hand-write the structure (parts, compositing, palette, transforms — ~95% of params), and train only the small word→part mapping, so new vocabulary becomes learnable while everything downstream stays readable. | |