pixelgen2 / experiments /add_class.py
DJLougen's picture
PixelGen2: compositional hand-weighted pixel-art generator + 4 scaling experiments
6fbb543 verified
Raw History Blame Contribute Delete
5.97 kB
"""Measure exactly how PixelGen changed after adding one class.
Compares the frozen before-state (experiments/before.pt, before_outputs.npz,
before_meta.json) against the live model built from current source.
Reports:
1. tensor shapes before -> after
2. raw parameter diff (how many values differ by index)
3. semantic diff (how many values differ once rows/cols are aligned by
class name / word, i.e. the change that actually matters)
4. vocab index remap (sorted vocab means new words shift old columns)
5. output equivalence on every pre-existing prompt (bit-exact or not)
6. what the new prompts do now vs before
"""
import json
from pathlib import Path
import numpy as np
import torch
from pixelgen.model import PixelGen
HERE = Path(__file__).resolve().parent
before_sd = torch.load(HERE / "before.pt", map_location="cpu", weights_only=True)
before_out = np.load(HERE / "before_outputs.npz")
meta = json.load(open(HERE / "before_meta.json"))
b_vocab, b_classes, prompts = meta["vocab"], meta["classes"], meta["prompts"]
m = PixelGen()
after_sd = m.state_dict()
a_vocab, a_classes = m.vocab, m.classes
new_classes = [c for c in a_classes if c not in b_classes]
new_words = [w for w in a_vocab if w not in b_vocab]
print("=" * 70)
print(f"new classes: {new_classes} new words: {new_words}")
print(f"vocab {len(b_vocab)} -> {len(a_vocab)} classes {len(b_classes)} -> {len(a_classes)}")
# 1. shapes -------------------------------------------------------------
print("\n[1] tensor shapes")
tot_b = tot_a = 0
for k in after_sd:
sb, sa = tuple(before_sd[k].shape), tuple(after_sd[k].shape)
tot_b += before_sd[k].numel(); tot_a += after_sd[k].numel()
flag = " <-- grew" if sb != sa else ""
print(f" {k:24} {str(sb):>16} -> {str(sa):<16}{flag}")
print(f" total values {tot_b} -> {tot_a} (+{tot_a - tot_b})")
# 2. raw diff ------------------------------------------------------------
print("\n[2] raw diff by index (naive view)")
for k in after_sd:
b, a = before_sd[k], after_sd[k]
if b.shape == a.shape:
n = int((b != a).sum())
print(f" {k:24} {n:5d} / {a.numel()} values differ")
else:
# compare overlapping prefix region
sl = tuple(slice(0, s) for s in b.shape)
n = int((b != a[sl]).sum())
print(f" {k:24} {n:5d} / {b.numel()} values differ in the old-shaped prefix (+{a.numel()-b.numel()} new)")
# 3. semantic diff (align by names) ---------------------------------------
print("\n[3] semantic diff (rows/cols aligned by class name / word)")
def aligned_linear(name):
b, a = before_sd[f"{name}.weight"], after_sd[f"{name}.weight"]
rows = b_classes if name == "word_to_class" else m.ramps
changed = []
for ri, r in enumerate(rows):
for w in b_vocab:
bv = b[ri, b_vocab[w]].item()
av = a[a_classes.index(r) if name == "word_to_class" else ri, a_vocab[w]].item()
if bv != av:
changed.append((r, w, bv, av))
return changed
for name in ("word_to_class", "word_to_ramp"):
ch = aligned_linear(name)
print(f" {name:16} old (row,word) entries changed: {len(ch)} {ch[:5]}")
bb, ab = before_sd["word_to_class.bias"], after_sd["word_to_class.bias"]
print(" word_to_class.bias old-class entries changed:",
sum(bb[i].item() != ab[a_classes.index(c)].item() for i, c in enumerate(b_classes)))
for bank in ("on_bank", "free_bank"):
b, a = before_sd[bank], after_sd[bank]
n = sum(int((b[i] != a[a_classes.index(c)]).sum()) for i, c in enumerate(b_classes))
print(f" {bank:16} old-class template cells changed: {n}")
for k in ("shade_bank", "ramp_table", "outline.weight", "palette_conv.weight"):
print(f" {k:16} identical: {torch.equal(before_sd[k], after_sd[k])}")
# 4. vocab remap ----------------------------------------------------------
print("\n[4] vocab index remap caused by sorted insertion")
moved = [(w, b_vocab[w], a_vocab[w]) for w in b_vocab if b_vocab[w] != a_vocab[w]]
print(f" {len(moved)} of {len(b_vocab)} old words changed column index")
print(" first few:", moved[:6])
print(" new word columns:", {w: a_vocab[w] for w in new_words})
# 5. output equivalence ---------------------------------------------------
print("\n[5] output equivalence on pre-existing prompts (3 seeds each)")
same = diff = 0
changed_prompts = []
for i, p in enumerate(prompts):
b = before_out[f"p{i}"]
a = m.generate([p] * 3, [0, 1, 2], scale=1)
if np.array_equal(a, b):
same += 1
else:
diff += 1
changed_prompts.append(p)
print(f" identical: {same} changed: {diff} -> {changed_prompts}")
# 6. new prompt behaviour ---------------------------------------------------
print("\n[6] what the new-class prompts resolve to now")
ref = {c: (m.generate([c], [0], scale=1)[0].sum(-1) > 0) for c in a_classes}
for p in ["mushroom", "red mushroom", "purple fungus", "toadstool", "dragon", ""]:
mask = m.generate([p], [0], scale=1)[0].sum(-1) > 0
cls = [c for c, r in ref.items() if np.array_equal(r, mask)]
i = prompts.index(p) if p in prompts else None
was = "n/a"
if i is not None:
bm = before_out[f"p{i}"][0].sum(-1) > 0
was = [c for c, r in ref.items() if np.array_equal(r, bm)]
print(f" {p!r:18} before -> {was} after -> {cls}")
# image ---------------------------------------------------------------------
from PIL import Image
S = 6; cell = 16 * S; pad = 4
rows = [("before", b_classes), ("after", a_classes)]
W = max(len(c) for _, c in rows) * (cell + pad) + pad
img = Image.new("RGB", (W, 2 * (cell + pad) + pad), (29, 43, 83))
for r, (_, classes) in enumerate(rows):
for c, cls in enumerate(classes):
img.paste(Image.fromarray(m.generate([cls], [0], scale=S)[0]), (pad + c * (cell + pad), pad + r * (cell + pad)))
img.save(HERE / "before_after.png")
print(f"\nwrote {HERE/'before_after.png'} (top: old classes rendered by new model; bottom: all classes)")