| """E1 — stitch DWÓCH różnych ekspertów (wspólny słownik): front walca × g × back reela. |
| Trenuje mapper g (reszta zamrożona) na połączonym korpusie walc+reel; porównuje perplexity: |
| walc-alone / reel-alone / ensemble / stitch — na wspólnym held-out. Generuje próbkę stitchu. |
| Pytanie (KMS1): czy stitch reprezentacji bije ensemble (spójne złączenie), czy mush. |
| """ |
| import sys, math, os |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
| import torch |
| import torch.nn as nn |
| from torch.nn import functional as F |
| from core.gpt import GPT |
| from core.abc_to_midi import to_midi |
|
|
| sys.stdout.reconfigure(encoding="utf-8") |
| A_CKPT, B_CKPT = "data/models/waltz_ckpt.pt", "data/models/reel_sv_ckpt.pt" |
| SEAM, TRAIN_STEPS = 2, 800 |
|
|
| def load(p): |
| ck = torch.load(p, map_location="cpu", weights_only=False) |
| m = GPT(ck["config"]); m.load_state_dict(ck["model"]); m.eval() |
| for q in m.parameters(): q.requires_grad_(False) |
| return m, ck |
|
|
| A, ckA = load(A_CKPT); B, ckB = load(B_CKPT) |
| assert ckA["stoi"] == ckB["stoi"], "różny słownik — najpierw wspólny!" |
| stoi, itos = ckA["stoi"], ckA["itos"] |
| block, n_embd = ckA["config"].block_size, ckA["config"].n_embd |
| print(f"front=walc, back=reel | wspólny słownik {len(stoi)} | styk po bloku {SEAM}") |
|
|
| wtext = open("data/corpus/waltz.abc", encoding="utf-8").read() |
| rtext = open("data/corpus/reel.abc", encoding="utf-8").read() |
| L = min(len(wtext), len(rtext)) |
| wd = torch.tensor([stoi[c] for c in wtext[:L] if c in stoi], dtype=torch.long) |
| rd = torch.tensor([stoi[c] for c in rtext[:L] if c in stoi], dtype=torch.long) |
| nw, nr = int(0.9 * len(wd)), int(0.9 * len(rd)) |
| train = torch.cat([wd[:nw], rd[:nr]]) |
| val = torch.cat([wd[nw:], rd[nr:]]) |
| print(f"zbalansowane: po {L:,} znaków z każdego | val walc+reel po równo") |
| bs = 32 |
|
|
| def get_batch(split): |
| d = train if split == "train" else val |
| ix = torch.randint(len(d) - block, (bs,)) |
| return (torch.stack([d[i:i+block] for i in ix]), |
| torch.stack([d[i+1:i+1+block] for i in ix])) |
|
|
| g = nn.Linear(n_embd, n_embd) |
| nn.init.normal_(g.weight, std=0.02); nn.init.zeros_(g.bias) |
|
|
| def emb(m, idx): |
| return m.drop(m.tok_emb(idx) + m.pos_emb(torch.arange(idx.shape[1]))) |
|
|
| def run_single(m, idx): |
| x = emb(m, idx) |
| for blk in m.blocks: x = blk(x) |
| return m.head(m.ln_f(x)) |
|
|
| def run_ensemble(idx, alpha=0.5): |
| return alpha * run_single(A, idx) + (1 - alpha) * run_single(B, idx) |
|
|
| def run_stitch(idx): |
| x = emb(A, idx) |
| for i in range(SEAM): x = A.blocks[i](x) |
| x = g(x) |
| for i in range(SEAM, len(B.blocks)): x = B.blocks[i](x) |
| return B.head(B.ln_f(x)) |
|
|
| def ce(logits, y): return F.cross_entropy(logits.view(-1, logits.size(-1)), y.view(-1)) |
|
|
| @torch.no_grad() |
| def ev(run, iters=60, seed=1234): |
| torch.manual_seed(seed) |
| return sum(ce(run(x), y).item() for x, y in (get_batch("val") for _ in range(iters))) / iters |
|
|
| pA = ev(lambda i: run_single(A, i)); pB = ev(lambda i: run_single(B, i)); pE = ev(run_ensemble) |
| print(f"walc-alone: ppl {math.exp(pA):.2f}") |
| print(f"reel-alone: ppl {math.exp(pB):.2f}") |
| print(f"ensemble: ppl {math.exp(pE):.2f} (baseline do pobicia)") |
|
|
| opt = torch.optim.AdamW(g.parameters(), lr=1e-3) |
| for _ in range(TRAIN_STEPS): |
| x, y = get_batch("train") |
| loss = ce(run_stitch(x), y) |
| opt.zero_grad(); loss.backward(); opt.step() |
| pS = ev(run_stitch) |
| print(f"STITCH: ppl {math.exp(pS):.2f} (po {TRAIN_STEPS} krokach treningu g)") |
| print(f"\nWERDYKT: stitch {math.exp(pS):.2f} vs ensemble {math.exp(pE):.2f} -> " |
| + ("STITCH BIJE ENSEMBLE ✅" if pS < pE else "ensemble nie gorszy ❌")) |
|
|
| @torch.no_grad() |
| def gen_stitch(seed_str, n_new=400, temp=0.85, topk=18): |
| idx = torch.tensor([[stoi[c] for c in seed_str]]) |
| for _ in range(n_new): |
| logits = run_stitch(idx[:, -block:])[:, -1, :] / temp |
| v, _ = torch.topk(logits, topk); logits[logits < v[:, [-1]]] = float("-inf") |
| idx = torch.cat([idx, torch.multinomial(F.softmax(logits, -1), 1)], 1) |
| return "".join(itos[t] for t in idx[0].tolist()) |
|
|
| def first_tune(raw): |
| out = [] |
| for ln in raw.split("\n"): |
| if ln.startswith("X:") and out: break |
| out.append(ln) |
| return "\n".join(out).strip() |
|
|
| os.makedirs("data/recordings/stitch", exist_ok=True) |
| for i, key in enumerate(["D", "G", "Emin"], 1): |
| tune = first_tune(gen_stitch(f"X:1\nM:4/4\nK:{key}\n")) |
| base = f"data/recordings/stitch/stitch_{i}_{key}" |
| open(base + ".abc", "w", encoding="utf-8").write(tune + "\n") |
| to_midi(tune, base + ".mid") |
| print("\npróbki stitchu -> data/recordings/stitch/ (render fortepian)") |
| torch.save({"A": A_CKPT, "B": B_CKPT, "g": g.state_dict(), "seam": SEAM}, "data/models/stitch_g.pt") |
|
|