Buckets:
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """ | |
| Infer/Eval script for OneRec++ one-step model (Query->Agent). | |
| What it does: | |
| - Rebuilds TF-IDF features exactly like training (fit on all questions). | |
| - Rebuilds vocab (sorted agent ids) to match ckpt token table. | |
| - Uses the same split logic (stratified by part). | |
| - Samples 200 queries per part from train + valid and prints metric tables. | |
| - Adds OOD diagnostics: | |
| * How much of predicted TopK are "train-seen GT agents" | |
| * Unseen-valid-GT coverage and Hit@K on queries whose GT is unseen. | |
| """ | |
| import os, json, math, argparse, random, zlib | |
| from datetime import datetime | |
| from typing import Dict, List, Tuple, Optional | |
| from collections import defaultdict | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from tqdm.auto import tqdm | |
| from sklearn.feature_extraction.text import TfidfVectorizer | |
| from sklearn.preprocessing import normalize | |
| from scipy.sparse import issparse | |
| from agent_rec.config import TFIDF_MAX_FEATURES, EVAL_TOPK, pos_topk_for_qid | |
| from agent_rec.data import stratified_train_valid_split | |
| from agent_rec.run_common import set_global_seed, warn_if_topk_diff | |
| try: | |
| from utils import print_metrics_table | |
| except Exception: | |
| # fallback if your utils import path changes | |
| def print_metrics_table(title, metrics, ks=(10,), filename=""): | |
| print(f"\n==== {title} ====") | |
| for k in ks: | |
| m = metrics[k] | |
| print(f"K={k} | P={m['P']:.4f} R={m['R']:.4f} F1={m['F1']:.4f} " | |
| f"Hit={m['Hit']:.4f} nDCG={m['nDCG']:.4f} MRR={m['MRR']:.4f}") | |
| # ---------------------- json / data ---------------------- | |
| def load_json(p: str): | |
| with open(p, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| def collect_data(data_root: str): | |
| parts = ["PartI", "PartII", "PartIII"] | |
| all_agents: Dict[str, dict] = {} | |
| all_questions: Dict[str, dict] = {} | |
| all_rankings: Dict[str, List[str]] = {} | |
| qid_to_part: Dict[str, str] = {} | |
| for part in parts: | |
| agents_path = os.path.join(data_root, part, "agents", "merge.json") | |
| questions_path = os.path.join(data_root, part, "questions", "merge.json") | |
| rankings_path = os.path.join(data_root, part, "rankings", "merge.json") | |
| if os.path.exists(agents_path): | |
| all_agents.update(load_json(agents_path)) | |
| if os.path.exists(questions_path): | |
| qd = load_json(questions_path) | |
| all_questions.update(qd) | |
| for qid in qd.keys(): | |
| qid_to_part.setdefault(qid, part) | |
| if os.path.exists(rankings_path): | |
| r = load_json(rankings_path) | |
| rr = r.get("rankings", {}) | |
| all_rankings.update(rr) | |
| for qid in rr.keys(): | |
| qid_to_part.setdefault(qid, part) | |
| tools_path = os.path.join(data_root, "Tools", "merge.json") | |
| tools = load_json(tools_path) if os.path.exists(tools_path) else {} | |
| return all_agents, all_questions, all_rankings, tools, qid_to_part | |
| def build_text_corpora(all_agents, all_questions, tools): | |
| q_ids = sorted(all_questions.keys()) | |
| q_texts = [all_questions[qid].get("input", "") for qid in q_ids] | |
| def _tool_text(tn: str) -> str: | |
| t = tools.get(tn, {}) | |
| desc = t.get("description", "") | |
| return f"{tn} {desc}".strip() | |
| a_ids = sorted(all_agents.keys()) | |
| a_texts = [] | |
| for aid in a_ids: | |
| a = all_agents[aid] | |
| mname = a.get("M", {}).get("name", "") | |
| tool_list = a.get("T", {}).get("tools", []) or [] | |
| concat_tool_desc = " ".join([_tool_text(tn) for tn in tool_list]) | |
| text = f"{mname} {concat_tool_desc}".strip() | |
| a_texts.append(text) | |
| return q_ids, q_texts, a_ids, a_texts | |
| def dataset_signature(a_ids: List[str], all_rankings: Dict[str, List[str]]) -> str: | |
| payload = {"a_ids": a_ids, "rankings": {k: all_rankings[k] for k in sorted(all_rankings.keys())}} | |
| blob = json.dumps(payload, sort_keys=True, ensure_ascii=False).encode("utf-8") | |
| sig = zlib.crc32(blob) & 0xFFFFFFFF | |
| return f"{sig:08x}" | |
| # ---------------------- metrics ---------------------- | |
| def _dcg_at_k(binary_hits, k): | |
| dcg = 0.0 | |
| for i, h in enumerate(binary_hits[:k]): | |
| if h: | |
| dcg += 1.0 / math.log2(i + 2.0) | |
| return dcg | |
| def evaluate_sampled(pred_ids_topk: List[str], rel_set: set, ks=(10,)): | |
| bin_hits = [1 if aid in rel_set else 0 for aid in pred_ids_topk] | |
| out = {} | |
| for k in ks: | |
| Hk = sum(bin_hits[:k]) | |
| P = Hk / float(k) | |
| R = Hk / float(len(rel_set)) if len(rel_set) > 0 else 0.0 | |
| F1 = (2*P*R)/(P+R) if (P+R) > 0 else 0.0 | |
| Hit = 1.0 if Hk > 0 else 0.0 | |
| dcg = _dcg_at_k(bin_hits, k) | |
| ideal = min(len(rel_set), k) | |
| idcg = sum(1.0 / math.log2(i + 2.0) for i in range(ideal)) if ideal > 0 else 0.0 | |
| nDCG = (dcg / idcg) if idcg > 0 else 0.0 | |
| rr = 0.0 | |
| for i in range(k): | |
| if bin_hits[i]: | |
| rr = 1.0 / float(i+1) | |
| break | |
| out[k] = {"P":P, "R":R, "F1":F1, "Hit":Hit, "nDCG":nDCG, "MRR":rr} | |
| return out | |
| # ---------------------- vocab / model defs ---------------------- | |
| class AgentVocab: | |
| def __init__(self, a_ids: List[str]): | |
| self.PAD = 0 | |
| self.BOS = 1 | |
| self.offset = 2 | |
| self.a_ids = a_ids | |
| self.vocab_size = len(a_ids) + self.offset | |
| self.aid2tok = {aid: i + self.offset for i, aid in enumerate(a_ids)} | |
| self.tok2aid = {i + self.offset: aid for i, aid in enumerate(a_ids)} | |
| def aid_to_token(self, aid: str) -> int: | |
| return self.aid2tok[aid] | |
| def token_to_aid(self, tok: int) -> Optional[str]: | |
| if tok < self.offset: | |
| return None | |
| return self.tok2aid.get(tok, None) | |
| class QueryEncoder(nn.Module): | |
| def __init__(self, d_q: int, hidden: int = 512, dropout: float = 0.1): | |
| super().__init__() | |
| self.net = nn.Sequential( | |
| nn.Linear(d_q, hidden), | |
| nn.GELU(), | |
| nn.Dropout(dropout), | |
| nn.Linear(hidden, hidden), | |
| nn.GELU(), | |
| nn.LayerNorm(hidden), | |
| ) | |
| for m in self.net: | |
| if isinstance(m, nn.Linear): | |
| nn.init.xavier_uniform_(m.weight) | |
| nn.init.zeros_(m.bias) | |
| def forward(self, q_vec: torch.Tensor) -> torch.Tensor: | |
| return self.net(q_vec) | |
| class OneStepGenerator(nn.Module): | |
| def __init__(self, enc_dim: int, vocab_size: int, tok_dim: int = 256, hidden: int = 512, dropout: float = 0.1): | |
| super().__init__() | |
| self.enc_dim = enc_dim | |
| self.vocab_size = vocab_size | |
| self.tok_dim = tok_dim | |
| self.tok_emb = nn.Embedding(vocab_size, tok_dim) | |
| self.q_proj = nn.Sequential( | |
| nn.Linear(enc_dim, hidden), | |
| nn.GELU(), | |
| nn.Dropout(dropout), | |
| nn.Linear(hidden, tok_dim, bias=False), | |
| ) | |
| self.a_proj = nn.Linear(tok_dim, enc_dim, bias=False) | |
| nn.init.xavier_uniform_(self.tok_emb.weight) | |
| for m in self.q_proj: | |
| if isinstance(m, nn.Linear): | |
| nn.init.xavier_uniform_(m.weight) | |
| if m.bias is not None: | |
| nn.init.zeros_(m.bias) | |
| nn.init.xavier_uniform_(self.a_proj.weight) | |
| with torch.no_grad(): | |
| self.tok_emb.weight.data[0].zero_() | |
| self.tok_emb.weight.data[1].zero_() | |
| def score(self, enc_vec: torch.Tensor, cand_tok: torch.Tensor) -> torch.Tensor: | |
| q = self.q_proj(enc_vec) # (B, tok_dim) | |
| Wc = self.tok_emb(cand_tok) # (B, C, tok_dim) | |
| logits = torch.einsum("be,bce->bc", q, Wc) | |
| return logits | |
| def generate(self, enc_vec: torch.Tensor, topk: int, cand_tok: torch.Tensor, temperature: float = 0.0): | |
| was_training = self.training | |
| self.eval() | |
| try: | |
| B, C = cand_tok.shape | |
| K = min(topk, C) | |
| logits = self.score(enc_vec, cand_tok) | |
| invalid = (cand_tok < 2) | |
| logits2 = logits.masked_fill(invalid, float("-inf")) | |
| idx = torch.topk(logits2, k=K, dim=-1).indices | |
| out = cand_tok.gather(1, idx) | |
| if K < topk: | |
| pad = torch.full((B, topk - K), 0, dtype=torch.long, device=out.device) | |
| out = torch.cat([out, pad], dim=1) | |
| return out | |
| finally: | |
| self.train(was_training) | |
| # ---------------------- sampling helpers ---------------------- | |
| def sample_qids_by_part( | |
| qids: List[str], | |
| qid_to_part: Dict[str, str], | |
| per_part: int, | |
| seed: int, | |
| parts: Optional[List[str]] = None | |
| ) -> List[str]: | |
| if per_part <= 0: | |
| return list(qids) | |
| rng = random.Random(seed) | |
| buckets = defaultdict(list) | |
| for q in qids: | |
| buckets[qid_to_part.get(q, "Unknown")].append(q) | |
| if parts is None: | |
| parts = sorted(buckets.keys()) | |
| out = [] | |
| for p in parts: | |
| lst = buckets.get(p, []) | |
| rng.shuffle(lst) | |
| out.extend(lst[: min(per_part, len(lst))]) | |
| rng.shuffle(out) | |
| return out | |
| def build_train_seen_agent_set( | |
| qids: List[str], | |
| all_rankings: Dict[str, List[str]], | |
| qid_to_part: Dict[str, str], | |
| topk_eval: int | |
| ) -> set: | |
| s = set() | |
| for qid in qids: | |
| k_pos = min(topk_eval, pos_topk_for_qid(qid, qid_to_part)) | |
| gt = all_rankings.get(qid, [])[:k_pos] | |
| s.update(gt) | |
| return s | |
| # ---------------------- eval with diagnostics ---------------------- | |
| def eval_with_diag( | |
| gen: OneStepGenerator, | |
| q_enc: QueryEncoder, | |
| Q_csr, | |
| q_ids: List[str], | |
| qid2idx: Dict[str,int], | |
| a_ids: List[str], | |
| all_rankings: Dict[str, List[str]], | |
| eval_qids: List[str], | |
| qid_to_part: Dict[str, str], | |
| train_seen_agents: set, | |
| device: torch.device, | |
| topk: int, | |
| cand_size: int, | |
| rng_seed: int, | |
| ): | |
| vocab = AgentVocab(a_ids) | |
| all_agent_set = set(a_ids) | |
| rnd = random.Random(rng_seed) | |
| ks = (topk,) | |
| agg = {k: {"P":0.0,"R":0.0,"F1":0.0,"Hit":0.0,"nDCG":0.0,"MRR":0.0} for k in ks} | |
| cnt = 0 | |
| skipped = 0 | |
| # diagnostics | |
| frac_pred_in_train_seen_sum = 0.0 | |
| all_pred_in_train_seen_cnt = 0 | |
| unique_pred = set() | |
| # unseen-valid-GT diagnostics (query-level) | |
| unseen_gt_query_cnt = 0 | |
| unseen_gt_hit_cnt = 0 | |
| unseen_gt_pred_cover_sum = 0.0 # fraction of preds in unseen_gt_set, averaged over those queries | |
| # precompute query encodings on the fly (batch size 1, cheap enough) | |
| q_enc.eval() | |
| gen.eval() | |
| pbar = tqdm(eval_qids, desc="Eval", total=len(eval_qids)) | |
| for qid in pbar: | |
| part = qid_to_part.get(qid, "Unknown") | |
| k_pos = min(topk, pos_topk_for_qid(qid, qid_to_part)) | |
| gt_list = [aid for aid in all_rankings.get(qid, [])[:k_pos] if aid in all_agent_set] | |
| if not gt_list: | |
| skipped += 1 | |
| continue | |
| rel_set = set(gt_list) | |
| # build sampled candidate pool (ensure GT included) | |
| neg_pool = list(all_agent_set - rel_set) | |
| need_neg = max(0, cand_size - len(gt_list)) | |
| if need_neg > 0 and len(neg_pool) > 0: | |
| k = min(need_neg, len(neg_pool)) | |
| sampled_negs = rnd.sample(neg_pool, k) | |
| cand_ids = gt_list + sampled_negs | |
| else: | |
| cand_ids = gt_list | |
| # unique + pad to cand_size | |
| cand_tok_list = [vocab.aid_to_token(a) for a in cand_ids] | |
| seen = set() | |
| cand_tok_u = [] | |
| for t in cand_tok_list: | |
| if t in seen: | |
| continue | |
| seen.add(t) | |
| cand_tok_u.append(t) | |
| while len(cand_tok_u) < cand_size: | |
| cand_tok_u.append(vocab.PAD) | |
| cand_tok_u = cand_tok_u[:cand_size] | |
| cand_tok = torch.tensor([cand_tok_u], dtype=torch.long, device=device) | |
| # encode q (densify only this row) | |
| qi = qid2idx[qid] | |
| q_dense = Q_csr[qi:qi+1].toarray().astype(np.float32) | |
| q_x = torch.from_numpy(q_dense).to(device, non_blocking=True) | |
| enc = q_enc(q_x) | |
| pred_tok = gen.generate(enc, topk=topk, cand_tok=cand_tok, temperature=0.0) | |
| pred_ids = [] | |
| for t in pred_tok[0].tolist(): | |
| aid = vocab.token_to_aid(t) | |
| if aid is not None: | |
| pred_ids.append(aid) | |
| for a in pred_ids: | |
| unique_pred.add(a) | |
| # metrics | |
| md = evaluate_sampled(pred_ids, rel_set, ks) | |
| for k in ks: | |
| for m in md[k]: | |
| agg[k][m] += md[k][m] | |
| cnt += 1 | |
| # diag: train-seen coverage | |
| if len(pred_ids) > 0: | |
| in_train_seen = sum(1 for a in pred_ids if a in train_seen_agents) | |
| frac = in_train_seen / float(len(pred_ids)) | |
| frac_pred_in_train_seen_sum += frac | |
| if in_train_seen == len(pred_ids): | |
| all_pred_in_train_seen_cnt += 1 | |
| # diag: unseen-valid-GT | |
| unseen_gt_set = rel_set - train_seen_agents | |
| if len(unseen_gt_set) > 0: | |
| unseen_gt_query_cnt += 1 | |
| hit_unseen = 1 if any(a in unseen_gt_set for a in pred_ids) else 0 | |
| unseen_gt_hit_cnt += hit_unseen | |
| unseen_gt_pred_cover_sum += sum(1 for a in pred_ids if a in unseen_gt_set) / float(len(pred_ids) if pred_ids else 1.0) | |
| if cnt > 0 and (cnt % 50 == 0): | |
| pbar.set_postfix({ | |
| "done": cnt, | |
| "skip": skipped, | |
| "P@K": f"{agg[topk]['P']/cnt:.4f}", | |
| "nDCG@K": f"{agg[topk]['nDCG']/cnt:.4f}", | |
| "trainSeenFrac": f"{(frac_pred_in_train_seen_sum/cnt):.3f}", | |
| }) | |
| if cnt == 0: | |
| metrics = {topk:{m:0.0 for m in ["P","R","F1","Hit","nDCG","MRR"]}} | |
| diag = {} | |
| return metrics, diag | |
| for m in agg[topk]: | |
| agg[topk][m] /= cnt | |
| diag = { | |
| "n_eval": cnt, | |
| "n_skipped": skipped, | |
| "unique_pred_agents": len(unique_pred), | |
| "avg_frac_pred_in_train_seen": frac_pred_in_train_seen_sum / cnt, | |
| "pct_queries_all_pred_in_train_seen": all_pred_in_train_seen_cnt / cnt, | |
| "unseen_gt_query_cnt": unseen_gt_query_cnt, | |
| "unseen_gt_hit_rate": (unseen_gt_hit_cnt / unseen_gt_query_cnt) if unseen_gt_query_cnt > 0 else 0.0, | |
| "unseen_gt_avg_frac_pred": (unseen_gt_pred_cover_sum / unseen_gt_query_cnt) if unseen_gt_query_cnt > 0 else 0.0, | |
| } | |
| return agg, diag | |
| def pretty_print_diag(title: str, diag: dict): | |
| print(f"\n---- {title} (OOD diagnostics) ----") | |
| for k, v in diag.items(): | |
| if isinstance(v, float): | |
| print(f"{k:>32s}: {v:.6f}") | |
| else: | |
| print(f"{k:>32s}: {v}") | |
| # ---------------------- main ---------------------- | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--data_root", type=str, required=True) | |
| ap.add_argument("--device", type=str, default="cpu") | |
| # TF-IDF / shape must match ckpt | |
| ap.add_argument("--max_features", type=int, default=TFIDF_MAX_FEATURES) | |
| # split | |
| ap.add_argument("--valid_ratio", type=float, default=0.2) | |
| ap.add_argument("--split_seed", type=int, default=42) | |
| # eval | |
| ap.add_argument("--topk", type=int, default=EVAL_TOPK) | |
| ap.add_argument("--eval_candidate_size", type=int, default=1000) | |
| ap.add_argument("--eval_per_part", type=int, default=200) | |
| ap.add_argument("--eval_parts", type=str, default="PartI,PartII,PartIII") | |
| ap.add_argument("--seed", type=int, default=1234) | |
| # ckpt | |
| ap.add_argument("--ckpt", type=str, default=None, | |
| help="Path to ckpt .pt. If empty, auto-locate via data_sig under .cache/OneRec_seq/models/") | |
| ap.add_argument("--ckpt_prefix", type=str, default="OneRec_seq", | |
| help="Cache prefix used during training (default: OneRec_seq).") | |
| args = ap.parse_args() | |
| eval_parts = [x.strip() for x in args.eval_parts.split(",") if x.strip()] | |
| warn_if_topk_diff(args.topk, expected=EVAL_TOPK) | |
| set_global_seed(args.seed) | |
| device = torch.device(args.device) | |
| # ---------- load data ---------- | |
| all_agents, all_questions, all_rankings, tools, qid_to_part = collect_data(args.data_root) | |
| q_ids, q_texts, a_ids, a_texts = build_text_corpora(all_agents, all_questions, tools) | |
| qid2idx = {qid:i for i,qid in enumerate(q_ids)} | |
| vocab = AgentVocab(a_ids) | |
| # ---------- locate ckpt ---------- | |
| data_sig = dataset_signature(a_ids, all_rankings) | |
| if args.ckpt is None: | |
| ckpt = os.path.join( | |
| args.data_root, | |
| f".cache/{args.ckpt_prefix}", | |
| "models", | |
| f"{args.ckpt_prefix}_{data_sig}.pt", | |
| ) | |
| args.ckpt = ckpt | |
| assert os.path.exists(args.ckpt), f"ckpt not found: {args.ckpt}" | |
| print(f"[ckpt] load: {args.ckpt}") | |
| # ---------- rebuild TF-IDF ---------- | |
| q_vec = TfidfVectorizer(max_features=args.max_features, lowercase=True) | |
| Q_csr = q_vec.fit_transform(q_texts) | |
| A_in_Q_csr = q_vec.transform(a_texts) | |
| if not issparse(Q_csr) or not issparse(A_in_Q_csr): | |
| raise ValueError("Q_csr / A_in_Q_csr must be sparse.") | |
| print(f"[tfidf] Q={Q_csr.shape} A_in_Q={A_in_Q_csr.shape}") | |
| # ---------- split ---------- | |
| qids_in_rank = [qid for qid in q_ids if qid in all_rankings] | |
| train_qids, valid_qids = stratified_train_valid_split( | |
| qids_in_rank, | |
| qid_to_part=qid_to_part, | |
| valid_ratio=args.valid_ratio, | |
| seed=args.split_seed | |
| ) | |
| print(f"[split] train={len(train_qids)} valid={len(valid_qids)} (valid_ratio={args.valid_ratio}, seed={args.split_seed})") | |
| # ---------- build train-seen GT agent set ---------- | |
| train_seen_agents = build_train_seen_agent_set( | |
| train_qids, all_rankings, qid_to_part, topk_eval=args.topk | |
| ) | |
| valid_seen_agents = build_train_seen_agent_set( | |
| valid_qids, all_rankings, qid_to_part, topk_eval=args.topk | |
| ) | |
| unseen_valid_agents = valid_seen_agents - train_seen_agents | |
| print(f"[agents] train_seen_gt={len(train_seen_agents)} valid_gt={len(valid_seen_agents)} unseen_valid_gt={len(unseen_valid_agents)}") | |
| # ---------- build models and load weights ---------- | |
| ck = torch.load(args.ckpt, map_location=device) | |
| # ckpt may be "SFT or SFT+DPO; just read state dict keys | |
| q_sd = ck.get("q_enc", None) | |
| g_sd = ck.get("gen", None) | |
| assert q_sd is not None and g_sd is not None, f"bad ckpt keys: {list(ck.keys())}" | |
| # infer dims from ckpt tensors (DO NOT GUESS) | |
| d_q = Q_csr.shape[1] | |
| # QueryEncoder hidden (=enc_dim) from first Linear: net.0.weight is (enc_dim, d_q) | |
| enc_dim = int(q_sd["net.0.weight"].shape[0]) | |
| # Generator dims from q_proj.0.weight: (gen_hidden, enc_dim) | |
| gen_hidden = int(g_sd["q_proj.0.weight"].shape[0]) | |
| tok_dim = int(g_sd["tok_emb.weight"].shape[1]) | |
| q_enc = QueryEncoder(d_q=d_q, hidden=enc_dim, dropout=0.0).to(device) | |
| gen = OneStepGenerator( | |
| enc_dim=enc_dim, | |
| vocab_size=vocab.vocab_size, | |
| tok_dim=tok_dim, | |
| hidden=gen_hidden, | |
| dropout=0.0 | |
| ).to(device) | |
| q_enc.load_state_dict(q_sd, strict=True) | |
| gen.load_state_dict(g_sd, strict=True) | |
| q_enc.eval(); gen.eval() | |
| print(f"[model] enc_dim={enc_dim} tok_dim={tok_dim} vocab={vocab.vocab_size} ck_mode={ck.get('mode','?')}") | |
| # ---------- sample qids ---------- | |
| parts = eval_parts | |
| train_eval_qids = sample_qids_by_part(train_qids, qid_to_part, per_part=args.eval_per_part, seed=args.seed, parts=parts) | |
| valid_eval_qids = sample_qids_by_part(valid_qids, qid_to_part, per_part=args.eval_per_part, seed=args.seed + 7, parts=parts) | |
| print(f"[eval] train_sampled={len(train_eval_qids)} valid_sampled={len(valid_eval_qids)} per_part={args.eval_per_part} parts={parts}") | |
| # ---------- run eval: train ---------- | |
| m_train, d_train = eval_with_diag( | |
| gen=gen, q_enc=q_enc, Q_csr=Q_csr, q_ids=q_ids, qid2idx=qid2idx, | |
| a_ids=a_ids, all_rankings=all_rankings, eval_qids=train_eval_qids, | |
| qid_to_part=qid_to_part, train_seen_agents=train_seen_agents, | |
| device=device, topk=args.topk, cand_size=args.eval_candidate_size, | |
| rng_seed=args.seed + 11 | |
| ) | |
| print_metrics_table("TRAIN (sampled per-part)", m_train, ks=(args.topk,), filename="OneRec_infer") | |
| pretty_print_diag("TRAIN", d_train) | |
| # per-part train | |
| for p in parts: | |
| q_part = [q for q in train_eval_qids if qid_to_part.get(q, "Unknown") == p] | |
| if not q_part: | |
| continue | |
| mp, dp = eval_with_diag( | |
| gen=gen, q_enc=q_enc, Q_csr=Q_csr, q_ids=q_ids, qid2idx=qid2idx, | |
| a_ids=a_ids, all_rankings=all_rankings, eval_qids=q_part, | |
| qid_to_part=qid_to_part, train_seen_agents=train_seen_agents, | |
| device=device, topk=args.topk, cand_size=args.eval_candidate_size, | |
| rng_seed=args.seed + 100 + hash(p) % 1000 | |
| ) | |
| print_metrics_table(f"TRAIN {p} (n={len(q_part)})", mp, ks=(args.topk,), filename="OneRec_infer") | |
| pretty_print_diag(f"TRAIN {p}", dp) | |
| # ---------- run eval: valid ---------- | |
| m_valid, d_valid = eval_with_diag( | |
| gen=gen, q_enc=q_enc, Q_csr=Q_csr, q_ids=q_ids, qid2idx=qid2idx, | |
| a_ids=a_ids, all_rankings=all_rankings, eval_qids=valid_eval_qids, | |
| qid_to_part=qid_to_part, train_seen_agents=train_seen_agents, | |
| device=device, topk=args.topk, cand_size=args.eval_candidate_size, | |
| rng_seed=args.seed + 19 | |
| ) | |
| print_metrics_table("VALID (sampled per-part)", m_valid, ks=(args.topk,), filename="OneRec_infer") | |
| pretty_print_diag("VALID", d_valid) | |
| # per-part valid | |
| for p in parts: | |
| q_part = [q for q in valid_eval_qids if qid_to_part.get(q, "Unknown") == p] | |
| if not q_part: | |
| continue | |
| mp, dp = eval_with_diag( | |
| gen=gen, q_enc=q_enc, Q_csr=Q_csr, q_ids=q_ids, qid2idx=qid2idx, | |
| a_ids=a_ids, all_rankings=all_rankings, eval_qids=q_part, | |
| qid_to_part=qid_to_part, train_seen_agents=train_seen_agents, | |
| device=device, topk=args.topk, cand_size=args.eval_candidate_size, | |
| rng_seed=args.seed + 200 + hash(p) % 1000 | |
| ) | |
| print_metrics_table(f"VALID {p} (n={len(q_part)})", mp, ks=(args.topk,), filename="OneRec_infer") | |
| pretty_print_diag(f"VALID {p}", dp) | |
| # extra: global unseen-valid-GT summary | |
| print("\n==== Global unseen-valid-GT summary (set-level) ====") | |
| print(f"unseen_valid_gt_agents={len(unseen_valid_agents)} (valid_gt - train_gt)") | |
| # 这里不做更重的统计了(你上面 query-level unseen_hit 已经够判断“只背训练 agent 还是能命中 valid GT”) | |
| if __name__ == "__main__": | |
| main() | |
Xet Storage Details
- Size:
- 22.7 kB
- Xet hash:
- 7ebe58c0199421e78c10567ca828e2b0a1ec78348b9bd94c369a668665e8f0b5
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.