| |
| """ |
| scale_experiment.py β Scale comparison: KAIZEN 42M vs GPT-2 117M. |
| |
| Research question: does semantic structure recoverable by a linear head |
| from frozen LM hidden states improve with model scale? |
| |
| Protocol (identical to Phase 7.0 / semantic_probe.py): |
| - Same 50 paraphrase pairs from probe_paraphrase.json |
| - Same 70/30 task-level split (35 train, 15 held-out) |
| - Same LinearHead architecture (d_modelβ128, InfoNCE) |
| - Same recall@1 metric on held-out tasks |
| |
| Output: |
| - Raw recall@1 (no head) at each scale = lower bound |
| - Recall@1 learning curve across epochs [0,10,25,50,100,150,200,300] |
| - Final recall@1 = publishable single-number comparison |
| |
| KAIZEN 42M: embed_task via canonical prompt [BOS,USER]+q+[ASST], mean-pool, 512-dim |
| GPT-2 117M: plain question text tokenized with GPT-2 BPE, mean-pool last hidden, 768-dim |
| """ |
| import json, random, sys, time |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| from tokenizers import Tokenizer |
| from transformers import GPT2Model, GPT2Tokenizer |
| from huggingface_hub import hf_hub_download |
|
|
| from lora import KaizenWithLoRA |
| from eval_benchmark import BENCHMARK_TASKS, build_prompt_ids, BLOCK_SIZE, MAX_GEN |
| from online_learner import HF_TOKEN, HF_TOK_REPO |
|
|
| |
| def _get_default_ckpt(): |
| from online_learner import HF_TOKEN |
| from huggingface_hub import hf_hub_download |
| return hf_hub_download('qoa/kaizen-42m', 'phase4_latest.pt', token=HF_TOKEN) |
|
|
| DEFAULT_CKPT = _get_default_ckpt() |
| PROBE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'probe_paraphrase.json') |
| SEED = 42 |
| TRAIN_FRAC = 0.70 |
| HEAD_DIM = 128 |
| HEAD_LR = 1e-3 |
| HEAD_TEMP = 0.07 |
| CHECKPOINTS = [0, 10, 25, 50, 100, 150, 200, 300] |
|
|
|
|
| |
| class LinearHead(nn.Module): |
| def __init__(self, d_in: int, d_out: int = HEAD_DIM): |
| super().__init__() |
| self.proj = nn.Linear(d_in, d_out, bias=True) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| return F.normalize(self.proj(x), dim=-1) |
|
|
|
|
| def infonce_loss(a: torch.Tensor, b: torch.Tensor, |
| temp: float = HEAD_TEMP) -> torch.Tensor: |
| """Bidirectional InfoNCE on (anchor, positive) pairs. a, b: [B, D].""" |
| sim = a @ b.T / temp |
| labels = torch.arange(sim.shape[0]) |
| return (F.cross_entropy(sim, labels) + F.cross_entropy(sim.T, labels)) / 2 |
|
|
|
|
| |
| def recall_at_1_head(head: LinearHead, |
| anc_embs: torch.Tensor, |
| pos_embs: torch.Tensor, |
| held_out_idx: list) -> float: |
| """Recall@1 in g-space. |
| |
| For each held-out anchor i: query = head(anc_embs[i]). |
| Gallery = head(pos_embs) over ALL 50 tasks (train + held-out). |
| Correct if nearest gallery entry == i (the true paraphrase). |
| """ |
| with torch.no_grad(): |
| g_pos = head(pos_embs) |
| g_anc = head(anc_embs[held_out_idx]) |
|
|
| correct = 0 |
| for k, i in enumerate(held_out_idx): |
| dists = ((g_pos - g_anc[k].unsqueeze(0)) ** 2).sum(dim=1) |
| nearest = int(dists.argmin().item()) |
| if nearest == i: |
| correct += 1 |
| return correct / len(held_out_idx) |
|
|
|
|
| def recall_at_1_raw(anc_embs: torch.Tensor, |
| pos_embs: torch.Tensor, |
| held_out_idx: list) -> float: |
| """Recall@1 in raw embedding space (L2-normalized, cosine-equivalent).""" |
| with torch.no_grad(): |
| a_n = F.normalize(anc_embs, dim=-1) |
| p_n = F.normalize(pos_embs, dim=-1) |
| correct = 0 |
| for i in held_out_idx: |
| dists = ((p_n - a_n[i].unsqueeze(0)) ** 2).sum(dim=1) |
| nearest = int(dists.argmin().item()) |
| if nearest == i: |
| correct += 1 |
| return correct / len(held_out_idx) |
|
|
|
|
| |
| def train_with_checkpoints(d_in: int, |
| anc_train: torch.Tensor, |
| pos_train: torch.Tensor, |
| anc_all: torch.Tensor, |
| pos_all: torch.Tensor, |
| held_out_idx: list) -> dict: |
| """Train LinearHead; return {epoch: recall@1} at each CHECKPOINT epoch.""" |
| head = LinearHead(d_in=d_in) |
| optimizer = torch.optim.Adam(head.parameters(), lr=HEAD_LR) |
| N = anc_train.shape[0] |
| results = {} |
|
|
| if 0 in CHECKPOINTS: |
| results[0] = recall_at_1_head(head, anc_all, pos_all, held_out_idx) |
|
|
| max_ep = max(CHECKPOINTS) |
| for ep in range(1, max_ep + 1): |
| perm = torch.randperm(N) |
| optimizer.zero_grad() |
| loss = infonce_loss(head(anc_train[perm]), head(pos_train[perm])) |
| loss.backward() |
| optimizer.step() |
| if ep in CHECKPOINTS: |
| results[ep] = recall_at_1_head(head, anc_all, pos_all, held_out_idx) |
|
|
| return results |
|
|
|
|
| |
| def embed_kaizen(model: KaizenWithLoRA, tokenizer, |
| questions: list) -> torch.Tensor: |
| """Canonical prompt embed_task for all questions. Returns [N, 512].""" |
| vecs = [] |
| with torch.no_grad(): |
| for q in questions: |
| pids = build_prompt_ids(tokenizer, q)[:BLOCK_SIZE - MAX_GEN] |
| x = torch.tensor([pids], dtype=torch.long) |
| vecs.append(model.embed_task(x, adapter=None)) |
| return torch.stack(vecs) |
|
|
|
|
| def embed_gpt2(model: GPT2Model, tokenizer: GPT2Tokenizer, |
| questions: list) -> torch.Tensor: |
| """Mean-pool GPT-2 last hidden state for all questions. Returns [N, 768].""" |
| vecs = [] |
| with torch.no_grad(): |
| for q in questions: |
| inputs = tokenizer(q, return_tensors='pt', |
| truncation=True, max_length=512) |
| out = model(**inputs) |
| |
| vecs.append(out.last_hidden_state[0].mean(dim=0)) |
| return torch.stack(vecs) |
|
|
|
|
| |
| def main(): |
| t0 = time.time() |
| rng = random.Random(SEED) |
|
|
| print('Scale experiment: KAIZEN 42M vs GPT-2 117M') |
| print('Metric: recall@1 on held-out paraphrase pairs') |
| print(f'Checkpoints: {CHECKPOINTS}') |
| print('=' * 64) |
|
|
| |
| with open(PROBE_PATH) as f: |
| paraphrases = json.load(f) |
|
|
| orig_qs = [t[0] for t in BENCHMARK_TASKS] |
| para_qs = [p[0] for p in paraphrases] |
|
|
| |
| all_idx = list(range(50)) |
| rng.shuffle(all_idx) |
| n_train = int(TRAIN_FRAC * 50) |
| train_idx = all_idx[:n_train] |
| held_out_idx = all_idx[n_train:] |
| print(f'Train tasks: {n_train} | Held-out tasks: {len(held_out_idx)}') |
|
|
| |
| print('\n[1/4] Loading KAIZEN 42M and embedding probe questions...') |
| tok_file = hf_hub_download(HF_TOK_REPO, 'tokenizer.json', |
| token=HF_TOKEN, cache_dir=None) |
| kz_tok = Tokenizer.from_file(tok_file) |
| kz_model = KaizenWithLoRA() |
| kz_model.load_base(DEFAULT_CKPT) |
| kz_model.eval() |
| kz_params = sum(p.numel() for p in kz_model.parameters()) |
| print(f' KAIZEN: {kz_params/1e6:.1f}M params') |
|
|
| kz_anc = embed_kaizen(kz_model, kz_tok, orig_qs) |
| kz_pos = embed_kaizen(kz_model, kz_tok, para_qs) |
| del kz_model |
| print(f' Embeddings: {list(kz_anc.shape)} (elapsed {time.time()-t0:.0f}s)') |
|
|
| |
| print('\n[2/4] Loading GPT-2 117M and embedding probe questions...') |
| gpt2_tok = GPT2Tokenizer.from_pretrained('gpt2', |
| cache_dir='/tmp/gpt2_cache') |
| gpt2_model = GPT2Model.from_pretrained('gpt2', |
| cache_dir='/tmp/gpt2_cache') |
| gpt2_model.eval() |
| gpt2_params = sum(p.numel() for p in gpt2_model.parameters()) |
| print(f' GPT-2: {gpt2_params/1e6:.1f}M params') |
|
|
| g2_anc = embed_gpt2(gpt2_model, gpt2_tok, orig_qs) |
| g2_pos = embed_gpt2(gpt2_model, gpt2_tok, para_qs) |
| del gpt2_model |
| print(f' Embeddings: {list(g2_anc.shape)} (elapsed {time.time()-t0:.0f}s)') |
|
|
| |
| kz_raw = recall_at_1_raw(kz_anc, kz_pos, held_out_idx) |
| g2_raw = recall_at_1_raw(g2_anc, g2_pos, held_out_idx) |
| print(f'\nRaw recall@1 (frozen embeddings, no head, cosine NN):') |
| print(f' KAIZEN 42M : {kz_raw:.4f}') |
| print(f' GPT-2 117M : {g2_raw:.4f}') |
| print(f' Ξ (117Mβ42M): {g2_raw - kz_raw:+.4f}') |
|
|
| |
| kz_anc_tr = kz_anc[train_idx] |
| kz_pos_tr = kz_pos[train_idx] |
| g2_anc_tr = g2_anc[train_idx] |
| g2_pos_tr = g2_pos[train_idx] |
|
|
| |
| print(f'\n[3/4] Training LinearHead for KAIZEN 42M (512β{HEAD_DIM}, InfoNCE)...') |
| kz_curve = train_with_checkpoints( |
| d_in=512, |
| anc_train=kz_anc_tr, pos_train=kz_pos_tr, |
| anc_all=kz_anc, pos_all=kz_pos, |
| held_out_idx=held_out_idx, |
| ) |
| print(f' Done (elapsed {time.time()-t0:.0f}s)') |
|
|
| |
| print(f'[4/4] Training LinearHead for GPT-2 117M (768β{HEAD_DIM}, InfoNCE)...') |
| g2_curve = train_with_checkpoints( |
| d_in=768, |
| anc_train=g2_anc_tr, pos_train=g2_pos_tr, |
| anc_all=g2_anc, pos_all=g2_pos, |
| held_out_idx=held_out_idx, |
| ) |
| print(f' Done (elapsed {time.time()-t0:.0f}s)') |
|
|
| |
| print() |
| print('=' * 64) |
| print('Recall@1 on held-out paraphrase pairs (linear head, InfoNCE, 35 train)') |
| print(f'{"Epoch":>8s} {"KAIZEN 42M":>12s} {"GPT-2 117M":>12s} {"Ξ(117M-42M)":>12s}') |
| print('-' * 52) |
| print(f'{"raw":>8s} {kz_raw:12.4f} {g2_raw:12.4f} {g2_raw-kz_raw:+12.4f}') |
| for ep in CHECKPOINTS: |
| kz_r = kz_curve.get(ep, float('nan')) |
| g2_r = g2_curve.get(ep, float('nan')) |
| delta = g2_r - kz_r if (kz_r == kz_r and g2_r == g2_r) else float('nan') |
| print(f'{ep:8d} {kz_r:12.4f} {g2_r:12.4f} {delta:+12.4f}') |
|
|
| kz_final = kz_curve.get(max(CHECKPOINTS), 0.0) |
| g2_final = g2_curve.get(max(CHECKPOINTS), 0.0) |
|
|
| print() |
| print(f'Summary (epoch {max(CHECKPOINTS)}):') |
| print(f' KAIZEN 42M raw={kz_raw:.4f} β head={kz_final:.4f} ' |
| f'(gain={kz_final-kz_raw:+.4f})') |
| print(f' GPT-2 117M raw={g2_raw:.4f} β head={g2_final:.4f} ' |
| f'(gain={g2_final-g2_raw:+.4f})') |
| print(f' Scale benefit at epoch 300: {g2_final - kz_final:+.4f} ' |
| f'({"117M > 42M" if g2_final > kz_final else "42M >= 117M"})') |
| print(f'\nRuntime: {time.time()-t0:.0f}s') |
|
|
|
|
| if __name__ == '__main__': |
| main() |
|
|