File size: 4,917 Bytes
ab7c6e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
"""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))                       # ZBALANSUJ: równo znaków z obu
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:]])                   # val = 50% walc + 50% reel (uczciwy test fuzji)
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)        # front: walc
    x = g(x)                                        # mapper na styku
    for i in range(SEAM, len(B.blocks)): x = B.blocks[i](x)  # back: reel
    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")