| """ |
| TELEN + Cross-encoder re-rank top-5 for MRR boost. |
| TELEN retrieves top-50, CE re-ranks top-5 to push relevant doc to rank 1. |
| """ |
| import sys; sys.path.insert(0, ".") |
| sys.stdout.reconfigure(encoding='utf-8') |
| import warnings; warnings.filterwarnings("ignore") |
| import random, numpy as np, torch, torch.nn as nn, torch.nn.functional as F |
| from tqdm import tqdm; from collections import defaultdict |
| from transformers import AutoModel, AutoTokenizer |
|
|
| from src.telern.config import TELENConfig |
| from src.telern.model import create_model |
| from src.telern.evaluate import prepare_test_data, build_test_queries, build_test_corpus, compute_metrics |
|
|
| SEED = 42; random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED) |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| config = TELENConfig() |
|
|
| |
| test_df = prepare_test_data(config) |
| queries = build_test_queries(test_df, max_queries=300) |
| corpus = build_test_corpus(test_df) |
| corpus_ids = [d["article_id"] for d in corpus] |
| corpus_law_ids = [d["law_id"] for d in corpus] |
| train_df = test_df[test_df["year"] <= config.meta.train_split_year] |
| print(f"Test: {len(queries)} queries, {len(corpus)} docs") |
|
|
| |
| print("Loading TELEN...") |
| telen = create_model(config).to(device) |
| ckpt = torch.load(config.output_dir + "/telen_best.pt", map_location=device, weights_only=False) |
| telen.hypernetwork.load_state_dict(ckpt["hypernetwork"]) |
| telen.state_encoder.load_state_dict(ckpt["state_encoder"]) |
| telen.base_projection.load_state_dict(ckpt["base_projection"]) |
| telen.attn_query.data.copy_(ckpt["attn_query"]) |
| if len(train_df)>0: telen.build_graph(train_df) |
|
|
| print("Encoding corpus...") |
| c_embs = [] |
| for i in range(0,len(corpus),64): |
| with torch.no_grad(): r=telen([d["text"] for d in corpus[i:i+64]],use_stochastic=False); c_embs.append(r["embeddings"].cpu()) |
| c_embs = torch.cat(c_embs,dim=0) |
|
|
| |
| print("Loading cross-encoder...") |
| ce_encoder = AutoModel.from_pretrained("vinai/phobert-base-v2").to(device) |
| ce_head = nn.Sequential( |
| nn.Linear(ce_encoder.config.hidden_size, 512), nn.ReLU(), nn.Dropout(0.1), |
| nn.Linear(512, 256), nn.ReLU(), nn.Dropout(0.1), |
| nn.Linear(256, 1), |
| ).to(device) |
| ce_ckpt = torch.load("data/checkpoints/telen/cross_encoder_best.pt", map_location=device, weights_only=False) |
| ce_encoder.load_state_dict(ce_ckpt["encoder"]) |
| ce_head.load_state_dict(ce_ckpt["head"]) |
| ce_tokenizer = AutoTokenizer.from_pretrained("vinai/phobert-base-v2") |
| ce_encoder.eval(); ce_head.eval() |
|
|
| def ce_score(query, docs): |
| scores = [] |
| for i in range(0, len(docs), 32): |
| b_docs = docs[i:i+32] |
| enc = ce_tokenizer([query]*len(b_docs), b_docs, padding=True, truncation=True, max_length=256, return_tensors="pt") |
| with torch.no_grad(): |
| out = ce_encoder(input_ids=enc["input_ids"].to(device), attention_mask=enc["attention_mask"].to(device)) |
| s = torch.sigmoid(ce_head(out.last_hidden_state[:,0,:])).squeeze(-1).cpu().numpy() |
| if s.ndim==0: s=np.array([s]) |
| scores.append(s) |
| return np.concatenate(scores) |
|
|
| |
| rerank_k_vals = [5, 10, 20] |
| results = {} |
|
|
| |
| print("\n--- TELEN standalone ---") |
| v3_m = defaultdict(list) |
| for q in tqdm(queries, desc="TELEN"): |
| with torch.no_grad(): qe=telen([q["query_text"]],use_stochastic=False)["embeddings"].cpu() |
| sim=F.cosine_similarity(qe,c_embs).numpy() |
| si=sim.argsort()[::-1] |
| rel=np.array([1.0 if corpus_law_ids[j]==q["law_id"] else 0.0 for j in range(len(corpus))])[si] |
| for j,cid in enumerate(corpus_ids): |
| if cid==q["query_id"]: p=np.where(si==j)[0]; rel=np.delete(rel,p[0]) if len(p)>0 else None; break |
| for k in [3,5,10]: |
| for mn,mv in compute_metrics(rel[:k],[k]).items(): v3_m[mn].append(mv) |
| results["TELEN"] = {n:np.mean(v) for n,v in v3_m.items()} |
|
|
| |
| for rerank_k in rerank_k_vals: |
| print(f"\n--- TELEN + CE rerank top-{rerank_k} ---") |
| all_m = defaultdict(list) |
| for q in tqdm(queries, desc=f"CE-{rerank_k}"): |
| with torch.no_grad(): qe=telen([q["query_text"]],use_stochastic=False)["embeddings"].cpu() |
| sim=F.cosine_similarity(qe,c_embs).numpy() |
| top_k=sim.argsort()[::-1][:rerank_k] |
| top_docs=[corpus[idx]["text"] for idx in top_k] |
| ce_s=ce_score(q["query_text"],top_docs) |
| reranked=top_k[ce_s.argsort()[::-1]] |
| remaining=sim.argsort()[::-1][~np.isin(sim.argsort()[::-1],top_k)] |
| final=np.concatenate([reranked,remaining]) |
| rel=np.array([1.0 if corpus_law_ids[j]==q["law_id"] else 0.0 for j in final]) |
| for j,cid in enumerate(corpus_ids): |
| if cid==q["query_id"]: p=np.where(final==j)[0]; rel=np.delete(rel,p[0]) if len(p)>0 else None; break |
| for k in [3,5,10]: |
| for mn,mv in compute_metrics(rel[:k],[k]).items(): all_m[mn].append(mv) |
| results[f"TELEN+CE@{rerank_k}"] = {n:np.mean(v) for n,v in all_m.items()} |
|
|
| |
| print("\n"+"="*75) |
| print("MRR BOOST RESULTS") |
| print("="*75) |
| h=f"{'Method':<20}" |
| for m in [3,5,10]: h+=f" {'NDCG@'+str(m):>10} {'MRR@'+str(m):>10}" |
| print(h); print("-"*len(h)) |
| for name, r in results.items(): |
| row=f"{name:<20}" |
| for m in [3,5,10]: row+=f" {r[f'ndcg@{m}']:>10.4f} {r[f'mrr@{m}']:>10.4f}" |
| print(row) |
|
|
| print("\n--- MRR Gain vs TELEN standalone ---") |
| base=results["TELEN"] |
| for name in [k for k in results if k!="TELEN"]: |
| r=results[name] |
| gains=[(m,(r[f"mrr@{m}"]/max(base[f"mrr@{m}"],1e-6)-1)*100) for m in [3,5,10]] |
| print(f" {name}: " + " | ".join(f"MRR@{m}: {g:+.1f}%" for m,g in gains)) |
| print("Done!") |
|
|