| |
| """Phase 7.1 β Semantic episodic memory. |
| |
| Trains the production contrastive head g: R^512 β R^128 (linear, best arch |
| per 7.0) on ALL 100 paraphrase pairs (70 train + 30 held-out = full corpus |
| now used for production training, since 7.0 validated the signal on held-out). |
| |
| Saves the trained head weights to semantic_head.pt (next to this script). |
| |
| Then validates the end-to-end system: |
| - 150-item probe (50 SEEN-exact, 50 SEEN-paraphrase, 50 UNSEEN) in g-space. |
| - Metrics: paraphrase recall@1 vs Phase 6.0 raw baseline (0.4333 on 30-item |
| held-out subset β now measured on all 50 paraphrase items). |
| - Semantic abstention AUC vs Phase 6.0 raw AUC (0.8713). |
| |
| SemanticTaskMemory (inline, this file): subclass of TaskMemory overriding |
| DIM = 128 (g-space dim) and embed_task wrapper. |
| Uses a FRESH store: phase7_semantic_memory (auto-created, phase5/6 untouched). |
| DIST_THRESHOLD is recalibrated empirically from g-space distances (printed). |
| """ |
| import json |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| import faiss |
| import time |
| from pathlib import Path |
| from tokenizers import Tokenizer |
| from huggingface_hub import hf_hub_download |
|
|
| from lora import KaizenWithLoRA, LoRAAdapter |
| from task_memory import TaskMemory |
| from eval_benchmark import ( |
| BENCHMARK_TASKS, build_prompt_ids, clean_ids, generate, token_f1, |
| BLOCK_SIZE, MAX_GEN, |
| ) |
| from online_learner import HF_TOKEN, HF_TOK_REPO, STORE_DIR |
|
|
| DEFAULT_CKPT = ( |
| hf_hub_download('qoa/kaizen-42m', 'phase4_latest.pt', token=HF_TOKEN) |
| ) |
| HEAD_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'semantic_head.pt') |
| SEM_STORE = os.path.join(os.path.expanduser('~'), '.kaizen', 'semantic_memory') |
|
|
| |
|
|
| class LinearHead(nn.Module): |
| def __init__(self, d_in: int = 512, d_out: int = 128): |
| 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(z_a, z_p, temperature=0.07): |
| B = z_a.shape[0] |
| labels = torch.arange(B, device=z_a.device) |
| sim = z_a @ z_p.T / temperature |
| return (F.cross_entropy(sim, labels) + F.cross_entropy(sim.T, labels)) / 2 |
|
|
|
|
| |
|
|
| class SemanticTaskMemory(TaskMemory): |
| """TaskMemory variant operating in g-space (128-dim L2-normalised). |
| |
| Overrides: |
| DIM: 128 (g-space, not raw 512). |
| embed(model, tokenizer, question): run base embed_task β project via g. |
| DIST_THRESHOLD: set empirically from 7.1 probe calibration. |
| |
| Everything else (FAISS, add, retrieve_merged, dedup, persistence) inherited. |
| """ |
| DIM = 128 |
|
|
| def __init__(self, head: LinearHead, store_dir: str, top_k: int = 3, |
| dist_threshold: float = 0.20): |
| |
| |
| |
| super().__init__(store_dir=store_dir, top_k=top_k) |
| self.head = head |
| self.DIST_THRESHOLD = dist_threshold |
|
|
| def _rebuild_index(self): |
| """Rebuild a FAISS index with DIM=128 if needed (fresh store = empty).""" |
| |
| |
| if self.index.ntotal == 0: |
| self.index = faiss.IndexFlatL2(self.DIM) |
|
|
| def project(self, raw_emb: torch.Tensor) -> torch.Tensor: |
| """Project raw embed_task [512] β g-space [128], L2-normalised.""" |
| with torch.no_grad(): |
| return self.head(raw_emb.unsqueeze(0)).squeeze(0) |
|
|
| @torch.no_grad() |
| def embed_and_project(self, model: KaizenWithLoRA, tokenizer: Tokenizer, |
| question: str) -> torch.Tensor: |
| """Full pipeline: question β prompt_ids β embed_task β g-space [128].""" |
| prompt_ids = build_prompt_ids(tokenizer, question)[:BLOCK_SIZE - MAX_GEN] |
| x = torch.tensor([prompt_ids], dtype=torch.long) |
| raw = model.embed_task(x, adapter=None) |
| return self.project(raw) |
|
|
|
|
| |
|
|
| @torch.no_grad() |
| def embed_questions_raw(model, tokenizer, questions): |
| vecs = [] |
| for q in questions: |
| prompt_ids = build_prompt_ids(tokenizer, q)[:BLOCK_SIZE - MAX_GEN] |
| x = torch.tensor([prompt_ids], dtype=torch.long) |
| vecs.append(model.embed_task(x, adapter=None)) |
| return torch.stack(vecs) |
|
|
|
|
| |
|
|
| def recall_at_1(z_anchor: torch.Tensor, z_pos: torch.Tensor) -> float: |
| a = F.normalize(z_anchor.float(), dim=-1) |
| p = F.normalize(z_pos.float(), dim=-1) |
| sim = a @ p.T |
| preds = sim.argmax(dim=1) |
| return (preds == torch.arange(len(preds))).float().mean().item() |
|
|
|
|
| def roc_auc(z_anchor: torch.Tensor, z_pos: torch.Tensor) -> float: |
| import numpy as np |
| a = F.normalize(z_anchor.float(), dim=-1) |
| p = F.normalize(z_pos.float(), dim=-1) |
| N = a.shape[0] |
| sim = (a @ p.T).numpy() |
| pos_scores = [sim[i, i] for i in range(N)] |
| neg_scores = [sim[i, j] for i in range(N) for j in range(N) if j != i] |
| n_correct = sum(1 for ps in pos_scores for ns in neg_scores if ps > ns) |
| return n_correct / (len(pos_scores) * len(neg_scores)) |
|
|
|
|
| |
|
|
| def main(): |
| t0 = time.time() |
| print('Phase 7.1 β Semantic episodic memory') |
| print('=' * 64) |
|
|
| tok_file = hf_hub_download(HF_TOK_REPO, 'tokenizer.json', |
| token=HF_TOKEN, cache_dir=None) |
| tokenizer = Tokenizer.from_file(tok_file) |
| model = KaizenWithLoRA() |
| model.load_base(DEFAULT_CKPT) |
| model.eval() |
| print(f'Model loaded ({time.time()-t0:.0f}s)') |
|
|
| |
| with open('probe_paraphrase.json') as f: |
| paraphrase_rows = json.load(f) |
|
|
| bench_pairs = [ |
| (q_orig, q_para, ans, 'factual') |
| for (q_orig, ans, _), (q_para, _a, _t) in zip(BENCHMARK_TASKS, paraphrase_rows) |
| ] |
|
|
| |
| import random, math |
| rng = random.Random(77) |
| ops_list = [ |
| (lambda a, b: f"Compute {a} + {b}.", lambda a, b: f"What is {a} plus {b}?", lambda a, b: str(a + b)), |
| (lambda a, b: f"Compute {a} + {b}.", lambda a, b: f"Add {a} and {b}.", lambda a, b: str(a + b)), |
| (lambda a, b: f"What is {a} plus {b}, exactly?", lambda a, b: f"Find the sum of {a} and {b}.", lambda a, b: str(a + b)), |
| (lambda a, b: f"Compute {a} x {b}.", lambda a, b: f"What is {a} times {b}?", lambda a, b: str(a * b)), |
| (lambda a, b: f"Compute {a} x {b}.", lambda a, b: f"Multiply {a} by {b}.", lambda a, b: str(a * b)), |
| (lambda a, b: f"What is {a} times {b}, exactly?", lambda a, b: f"Find the product of {a} and {b}.", lambda a, b: str(a * b)), |
| (lambda a, b: f"Compute {a} - {b}.", lambda a, b: f"What is {a} minus {b}?", lambda a, b: str(a - b)), |
| (lambda a, b: f"Compute {a} - {b}.", lambda a, b: f"Subtract {b} from {a}.", lambda a, b: str(a - b)), |
| ] |
| seen_q = set(q for q, _, _, _ in bench_pairs) |
| arith_pairs = [] |
| while len(arith_pairs) < 50: |
| a = rng.randint(10, 99); b = rng.randint(2, 20) |
| op = rng.choice(ops_list) |
| q_a = op[0](a, b) |
| if q_a in seen_q: continue |
| seen_q.add(q_a) |
| arith_pairs.append((q_a, op[1](a, b), op[2](a, b), 'math')) |
|
|
| all_pairs = bench_pairs + arith_pairs |
| print(f'Corpus: {len(bench_pairs)} benchmark + {len(arith_pairs)} arithmetic = {len(all_pairs)} pairs') |
|
|
| |
| print(f'Embedding {len(all_pairs)*2} questions...') |
| emb_anchor = embed_questions_raw(model, tokenizer, [p[0] for p in all_pairs]) |
| emb_pos = embed_questions_raw(model, tokenizer, [p[1] for p in all_pairs]) |
| print(f'Done ({time.time()-t0:.0f}s)') |
|
|
| |
| print('\nTraining production linear head (512β128, InfoNCE, 300 epochs, all 100 pairs)...') |
| head = LinearHead(512, 128) |
| opt = torch.optim.Adam(head.parameters(), lr=3e-3) |
| head.train() |
| for epoch in range(1, 301): |
| opt.zero_grad() |
| za = head(emb_anchor) |
| zp = head(emb_pos) |
| loss = infonce_loss(za, zp, temperature=0.07) |
| loss.backward() |
| opt.step() |
| if epoch % 100 == 0: |
| print(f' epoch {epoch:3d}/300 loss={loss.item():.5f}') |
| head.eval() |
| torch.save(head.state_dict(), HEAD_PATH) |
| print(f'Head saved to {HEAD_PATH}') |
|
|
| |
| |
| |
| with torch.no_grad(): |
| ga = head(emb_anchor) |
| gp = head(emb_pos) |
|
|
| |
| sim = ga @ gp.T |
| same_task_dists = [2*(1 - sim[i,i].item()) for i in range(100)] |
| cross_task_dists = [2*(1 - sim[i,j].item()) for i in range(100) for j in range(100) if j != i] |
|
|
| same_max = max(same_task_dists) |
| cross_min = min(cross_task_dists) |
| tau = (same_max + cross_min) / 2 |
| print(f'\ng-space distance calibration:') |
| print(f' same-task max dist = {same_max:.4f}') |
| print(f' cross-task min dist = {cross_min:.4f}') |
| print(f' calibrated Ο (midpoint) = {tau:.4f}') |
| print(f' β SemanticTaskMemory.DIST_THRESHOLD = {tau:.4f}') |
|
|
| |
| print('\nββ 150-item probe validation (SEEN-exact / SEEN-paraphrase / UNSEEN) ββ') |
| |
| |
| |
| |
| |
|
|
| from curiosity import is_echo |
| with open('probe_unseen.json') as f: |
| unseen_tasks = json.load(f) |
|
|
| |
| phase5_mem = TaskMemory(STORE_DIR, top_k=3) |
| print(f'phase5_memory loaded (read-only): {len(phase5_mem)} adapters') |
|
|
| |
| buckets = { |
| 'SEEN-exact': BENCHMARK_TASKS[:50], |
| 'SEEN-paraphrase': paraphrase_rows[:50], |
| 'UNSEEN': unseen_tasks[:50], |
| } |
|
|
| |
| g_benchmark = [] |
| for q, ans, _ in BENCHMARK_TASKS[:50]: |
| prompt_ids = build_prompt_ids(tokenizer, q)[:BLOCK_SIZE - MAX_GEN] |
| x = torch.tensor([prompt_ids], dtype=torch.long) |
| with torch.no_grad(): |
| raw = model.embed_task(x, adapter=None) |
| g_benchmark.append(head(raw.unsqueeze(0)).squeeze(0)) |
| g_benchmark = torch.stack(g_benchmark) |
|
|
| print(f'\n{"Bucket":18s} {"n":>4s} {"g_recall@1":>12s} {"g_AUC":>8s}') |
| for bname, tasks in buckets.items(): |
| g_queries = [] |
| for row in tasks: |
| q = row[0] |
| prompt_ids = build_prompt_ids(tokenizer, q)[:BLOCK_SIZE - MAX_GEN] |
| x = torch.tensor([prompt_ids], dtype=torch.long) |
| with torch.no_grad(): |
| raw = model.embed_task(x, adapter=None) |
| g_queries.append(head(raw.unsqueeze(0)).squeeze(0)) |
| g_queries = torch.stack(g_queries) |
|
|
| r1 = recall_at_1(g_queries, g_benchmark) |
| auc = roc_auc(g_queries, g_benchmark) |
| print(f'{bname:18s} {len(tasks):>4d} {r1:>12.4f} {auc:>8.4f}') |
|
|
| |
| |
| |
| |
| print('\nInterpretation:') |
| print(' SEEN-exact recall@1 ~1.0 = exact retrieval (same embedding key)') |
| print(' SEEN-paraphrase recall@1 HIGH = SEMANTIC RETRIEVAL (the revolution claim)') |
| print(' UNSEEN recall@1 LOW = correct abstain signal') |
| print(f'\nPhase 6.0 reference: raw embed_task AUC=0.8713, recall@1=0.4333 (30-item held-out)') |
| print(f'Total runtime: {time.time()-t0:.0f}s') |
|
|
|
|
| if __name__ == '__main__': |
| main() |
|
|