Buckets:
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| from __future__ import annotations | |
| import math | |
| import random | |
| import zlib | |
| from typing import Dict, List, Optional, Tuple | |
| import numpy as np | |
| import torch | |
| from tqdm.auto import tqdm | |
| from .config import EPS, POS_TOPK, POS_TOPK_BY_PART, EVAL_TOPK | |
| from .knn import knn_qvec_for_question_text | |
| from .scoring import score_candidates, ScoreMode | |
| from .models.base import RecommenderBase | |
| def _ideal_dcg(k: int, num_rel: int) -> float: | |
| ideal = min(k, num_rel) | |
| return sum(1.0 / math.log2(i + 2.0) for i in range(ideal)) if ideal else 0.0 | |
| def _metrics_at_k(pred: List[str], rel_set: set, k: int) -> Dict[str, float]: | |
| top = pred[:k] | |
| hits = [1 if a in rel_set else 0 for a in top] | |
| Hk = int(sum(hits)) | |
| num_rel = max(1, len(rel_set)) | |
| P = Hk / float(k) | |
| R = Hk / float(num_rel) | |
| F1 = (2 * P * R) / (P + R) if (P + R) else 0.0 | |
| Hit = 1.0 if Hk > 0 else 0.0 | |
| dcg = sum(1.0 / math.log2(i + 2.0) for i, h in enumerate(hits) if h) | |
| idcg = _ideal_dcg(k, len(rel_set)) | |
| nDCG = (dcg / idcg) if idcg > 0 else 0.0 | |
| rr = 0.0 | |
| for i, h in enumerate(hits): | |
| if h: | |
| rr = 1.0 / float(i + 1) | |
| break | |
| return {"P": P, "R": R, "F1": F1, "Hit": Hit, "nDCG": nDCG, "MRR": rr} | |
| def evaluate_sampled_knn_top10( | |
| model: RecommenderBase, | |
| aid2idx: Dict[str, int], | |
| all_rankings: Dict[str, List[str]], | |
| all_questions: Dict[str, dict], | |
| eval_qids: List[str], | |
| knn_cache: dict, | |
| cand_size: int = 100, | |
| knn_N: int = 3, | |
| qid_to_part: Optional[Dict[str, str]] = None, | |
| pos_topk_by_part: Dict[str, int] = POS_TOPK_BY_PART, | |
| pos_topk_default: int = POS_TOPK, | |
| topk: int = EVAL_TOPK, | |
| score_mode: ScoreMode = "dot", | |
| seed: int = 123, | |
| desc: str = "Evaluating", | |
| use_torch: bool = True, | |
| ) -> Dict[int, Dict[str, float]]: | |
| """ | |
| Sampled eval with fixed top10: | |
| - candidates = positives ∪ random negatives to cand_size | |
| - qv via TF-IDF KNN weighted avg of train Q latent vectors | |
| - score on candidate subset | |
| - optionally score via torch on the model device (GPU if available) | |
| Return format aligned with print_metrics_table: {10: {...}} | |
| """ | |
| A = model.export_agent_embeddings() # (Na,F) | |
| bias_a = model.export_agent_bias() # (Na,) or None | |
| device = next(model.parameters()).device | |
| use_torch_backend = bool(use_torch and torch.cuda.is_available() and device.type == "cuda") | |
| if use_torch_backend: | |
| A_t = torch.from_numpy(A).to(device=device, non_blocking=True) | |
| bias_t = torch.from_numpy(bias_a).to(device=device, non_blocking=True) if bias_a is not None else None | |
| else: | |
| bias_t = None | |
| all_agents = list(aid2idx.keys()) | |
| all_agent_set = set(all_agents) | |
| agg = {m: 0.0 for m in ["P", "R", "F1", "Hit", "nDCG", "MRR"]} | |
| cnt, skipped = 0, 0 | |
| pbar = tqdm(eval_qids, desc=desc, leave=True, dynamic_ncols=True) | |
| for qid in pbar: | |
| k = pos_topk_by_part.get(qid_to_part.get(qid, ""), pos_topk_default) if qid_to_part else pos_topk_default | |
| gt = [aid for aid in all_rankings.get(qid, [])[:k] if aid in aid2idx] | |
| if not gt: | |
| skipped += 1 | |
| pbar.set_postfix({"done": cnt, "skipped": skipped}) | |
| continue | |
| rel_set = set(gt) | |
| neg_pool = list(all_agent_set - rel_set) | |
| qid_seed = (zlib.crc32(str(qid).encode("utf-8")) ^ (seed * 2654435761)) & 0xFFFFFFFF | |
| rnd = random.Random(qid_seed) | |
| need_neg = max(0, cand_size - len(gt)) | |
| if need_neg > 0 and neg_pool: | |
| k = min(need_neg, len(neg_pool)) | |
| sampled_negs = rnd.sample(neg_pool, k) | |
| cand = gt + sampled_negs | |
| else: | |
| cand = gt | |
| qtext = all_questions.get(qid, {}).get("input", "") | |
| qv = knn_qvec_for_question_text(qtext, knn_cache, N=knn_N) # (F,) | |
| ai_idx = np.array([aid2idx[a] for a in cand], dtype=np.int64) | |
| if use_torch_backend: | |
| ai_idx_t = torch.from_numpy(ai_idx).to(device=device, non_blocking=True) | |
| cand_emb = A_t.index_select(0, ai_idx_t) | |
| qv_t = torch.from_numpy(qv).to(device=device, non_blocking=True) | |
| if score_mode == "dot": | |
| s_t = cand_emb @ qv_t | |
| elif score_mode == "cosine": | |
| cand_norm = torch.linalg.norm(cand_emb, dim=1, keepdim=True) + EPS | |
| q_norm = torch.linalg.norm(qv_t) + EPS | |
| s_t = (cand_emb / cand_norm) @ (qv_t / q_norm) | |
| else: | |
| raise ValueError(f"Unknown score mode: {score_mode}") | |
| if bias_t is not None: | |
| s_t = s_t + bias_t.index_select(0, ai_idx_t) | |
| k_top = min(topk, s_t.numel()) | |
| order = torch.topk(s_t, k_top).indices.cpu().tolist() | |
| else: | |
| s = score_candidates(A, qv, ai_idx, bias_a=bias_a, mode=score_mode) | |
| order = np.argsort(-s)[:topk].tolist() | |
| pred = [cand[i] for i in order] | |
| met = _metrics_at_k(pred, rel_set, topk) | |
| for m in agg: | |
| agg[m] += met[m] | |
| cnt += 1 | |
| pbar.set_postfix({ | |
| "done": cnt, | |
| "skipped": skipped, | |
| f"P@{topk}": f"{(agg['P']/cnt):.4f}", | |
| f"nDCG@{topk}": f"{(agg['nDCG']/cnt):.4f}", | |
| f"MRR@{topk}": f"{(agg['MRR']/cnt):.4f}", | |
| "Ncand": len(cand), | |
| }) | |
| if cnt == 0: | |
| return {topk: {m: 0.0 for m in agg}} | |
| for m in agg: | |
| agg[m] /= cnt | |
| return {topk: agg} | |
| def split_eval_qids_by_part(eval_qids: List[str], qid_to_part: Dict[str, str]) -> Dict[str, List[str]]: | |
| out: Dict[str, List[str]] = {"PartI": [], "PartII": [], "PartIII": [], "Unknown": []} | |
| for qid in eval_qids: | |
| out.setdefault(qid_to_part.get(qid, "Unknown"), []).append(qid) | |
| return out | |
| def evaluate_sampled_direct_top10( | |
| model, | |
| aid2idx: Dict[str, int], | |
| qid2idx: Dict[str, int], | |
| all_rankings: Dict[str, List[str]], | |
| all_questions: Dict[str, dict], | |
| eval_qids: List[str], | |
| q_vectorizer, | |
| A_text_full: np.ndarray, | |
| cand_size: int = 100, | |
| qid_to_part: Optional[Dict[str, str]] = None, | |
| pos_topk_by_part: Dict[str, int] = POS_TOPK_BY_PART, | |
| pos_topk_default: int = POS_TOPK, | |
| topk: int = EVAL_TOPK, | |
| seed: int = 123, | |
| desc: str = "Evaluating", | |
| ) -> Dict[int, Dict[str, float]]: | |
| """ | |
| Sampled eval with fixed top10: | |
| - candidates = positives ∪ random negatives to cand_size | |
| - qv via TF-IDF vectorizer on question text | |
| - model scores directly with query + agent text features (no KNN) | |
| Return format aligned with print_metrics_table: {10: {...}} | |
| """ | |
| device = next(model.parameters()).device | |
| A_t = torch.tensor(A_text_full, dtype=torch.float32, device=device) | |
| all_agents = list(aid2idx.keys()) | |
| all_agent_set = set(all_agents) | |
| agg = {m: 0.0 for m in ["P", "R", "F1", "Hit", "nDCG", "MRR"]} | |
| cnt, skipped = 0, 0 | |
| pbar = tqdm(eval_qids, desc=desc, leave=True, dynamic_ncols=True) | |
| for qid in pbar: | |
| k = pos_topk_by_part.get(qid_to_part.get(qid, ""), pos_topk_default) if qid_to_part else pos_topk_default | |
| gt = [aid for aid in all_rankings.get(qid, [])[:k] if aid in aid2idx] | |
| if not gt: | |
| skipped += 1 | |
| pbar.set_postfix({"done": cnt, "skipped": skipped}) | |
| continue | |
| rel_set = set(gt) | |
| neg_pool = list(all_agent_set - rel_set) | |
| qid_seed = (zlib.crc32(str(qid).encode("utf-8")) ^ (seed * 2654435761)) & 0xFFFFFFFF | |
| rnd = random.Random(qid_seed) | |
| need_neg = max(0, cand_size - len(gt)) | |
| if need_neg > 0 and neg_pool: | |
| k = min(need_neg, len(neg_pool)) | |
| sampled_negs = rnd.sample(neg_pool, k) | |
| cand = gt + sampled_negs | |
| else: | |
| cand = gt | |
| qtext = all_questions.get(qid, {}).get("input", "") | |
| qv_np = q_vectorizer.transform([qtext]).toarray().astype(np.float32)[0] | |
| qv = torch.tensor(qv_np, dtype=torch.float32, device=device).unsqueeze(0) | |
| ai_idx = torch.tensor([aid2idx[a] for a in cand], dtype=torch.long, device=device) | |
| q_idx = torch.full((len(cand),), qid2idx[qid], dtype=torch.long, device=device) | |
| qv_rep = qv.repeat(len(cand), 1) | |
| with torch.no_grad(): | |
| scores = model.forward_score(qv_rep, A_t[ai_idx], ai_idx, q_idx=q_idx).detach().cpu().numpy() | |
| order = np.argsort(-scores)[:topk] | |
| pred = [cand[i] for i in order] | |
| met = _metrics_at_k(pred, rel_set, topk) | |
| for m in agg: | |
| agg[m] += met[m] | |
| cnt += 1 | |
| pbar.set_postfix({ | |
| "done": cnt, | |
| "skipped": skipped, | |
| f"P@{topk}": f"{(agg['P']/cnt):.4f}", | |
| f"nDCG@{topk}": f"{(agg['nDCG']/cnt):.4f}", | |
| f"MRR@{topk}": f"{(agg['MRR']/cnt):.4f}", | |
| "Ncand": len(cand), | |
| }) | |
| if cnt == 0: | |
| return {topk: {m: 0.0 for m in agg}} | |
| for m in agg: | |
| agg[m] /= cnt | |
| return {topk: agg} | |
| def evaluate_sampled_embedding_topk( | |
| model, | |
| qid2idx: Dict[str, int], | |
| aid2idx: Dict[str, int], | |
| all_rankings: Dict[str, List[str]], | |
| eval_qids: List[str], | |
| Q_t: torch.Tensor, | |
| A_t: torch.Tensor, | |
| cand_size: int = 100, | |
| qid_to_part: Optional[Dict[str, str]] = None, | |
| pos_topk_by_part: Dict[str, int] = POS_TOPK_BY_PART, | |
| pos_topk_default: int = POS_TOPK, | |
| topk: int = EVAL_TOPK, | |
| seed: int = 123, | |
| desc: str = "Evaluating", | |
| ) -> Dict[int, Dict[str, float]]: | |
| """Sampled eval using precomputed query/agent embeddings.""" | |
| device = next(model.parameters()).device | |
| Q_t = Q_t.to(device) | |
| A_t = A_t.to(device) | |
| all_agents = list(aid2idx.keys()) | |
| all_agent_set = set(all_agents) | |
| agg = {m: 0.0 for m in ["P", "R", "F1", "Hit", "nDCG", "MRR"]} | |
| cnt, skipped = 0, 0 | |
| pbar = tqdm(eval_qids, desc=desc, leave=True, dynamic_ncols=True) | |
| for qid in pbar: | |
| k = pos_topk_by_part.get(qid_to_part.get(qid, ""), pos_topk_default) if qid_to_part else pos_topk_default | |
| gt = [aid for aid in all_rankings.get(qid, [])[:k] if aid in aid2idx] | |
| if not gt: | |
| skipped += 1 | |
| pbar.set_postfix({"done": cnt, "skipped": skipped}) | |
| continue | |
| rel_set = set(gt) | |
| neg_pool = list(all_agent_set - rel_set) | |
| qid_seed = (zlib.crc32(str(qid).encode("utf-8")) ^ (seed * 2654435761)) & 0xFFFFFFFF | |
| rnd = random.Random(qid_seed) | |
| need_neg = max(0, cand_size - len(gt)) | |
| if need_neg > 0 and neg_pool: | |
| k = min(need_neg, len(neg_pool)) | |
| sampled_negs = rnd.sample(neg_pool, k) | |
| cand = gt + sampled_negs | |
| else: | |
| cand = gt | |
| qi = qid2idx[qid] | |
| qv = Q_t[qi : qi + 1].repeat(len(cand), 1) | |
| ai_idx = torch.tensor([aid2idx[a] for a in cand], dtype=torch.long, device=device) | |
| q_idx = torch.full((len(cand),), qi, dtype=torch.long, device=device) | |
| with torch.no_grad(): | |
| scores = model.forward_score(qv, A_t[ai_idx], ai_idx, q_idx=q_idx).detach().cpu().numpy() | |
| order = np.argsort(-scores)[:topk] | |
| pred = [cand[i] for i in order] | |
| met = _metrics_at_k(pred, rel_set, topk) | |
| for m in agg: | |
| agg[m] += met[m] | |
| cnt += 1 | |
| pbar.set_postfix({ | |
| "done": cnt, | |
| "skipped": skipped, | |
| f"P@{topk}": f"{(agg['P']/cnt):.4f}", | |
| f"nDCG@{topk}": f"{(agg['nDCG']/cnt):.4f}", | |
| f"MRR@{topk}": f"{(agg['MRR']/cnt):.4f}", | |
| "Ncand": len(cand), | |
| }) | |
| if cnt == 0: | |
| return {topk: {m: 0.0 for m in agg}} | |
| for m in agg: | |
| agg[m] /= cnt | |
| return {topk: agg} | |
Xet Storage Details
- Size:
- 11.8 kB
- Xet hash:
- c587a29a10c0d9420097a2e97c953f4faada8ddfc65b16db9bb5456a3dd1f4fd
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.