Spaces:
Running
Running
File size: 5,800 Bytes
4dfafb0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | """M5 sub-experiment B — early directional read from a PARTIAL golds-first index.
The full UniXcoder index runs golds-first (index_corpus.py GOLDS_FIRST=1), so the
first ~22k embedded docs are every eval query's gold target; docs after that are
train-split distractors whose pool grows as indexing proceeds. This script does a
fair, exact brute-force cosine A/B over the SAME candidate pool for both models,
reading vectors straight from the .cache/*.npy files — no Qdrant, no HNSW recall
loss, no CPU contention with the running indexer.
Pool at run time = the first P = unixcoder embed-progress docs (golds-first order).
UniXcoder doc vectors come from its cache[0:P]; MiniLM doc vectors are pulled from
the full MiniLM cache for the SAME doc set (mapped by doc_id). Queries evaluated =
those whose gold is within the pool. Metrics: MRR@10 / nDCG@10 / Recall@100.
Usage:
.venv/bin/python scripts/sample_eval_partial.py # eval at current P
.venv/bin/python scripts/sample_eval_partial.py --min-docs 44000 # require P first
"""
from __future__ import annotations
import argparse
import os
import sys
import numpy as np
sys.path.insert(0, "src")
from codesearch.data import load_codesearch # noqa: E402
from codesearch.eval.metrics import ( # noqa: E402
mean_ndcg,
mean_recall,
mean_reciprocal_rank,
)
from codesearch.embedding import load_encoder # noqa: E402
CACHE = ".cache"
UNIX_MODEL = "microsoft/unixcoder-base"
MINILM_MODEL = "all-MiniLM-L6-v2"
UNIX_NPY = f"{CACHE}/corpus_vectors_microsoft_unixcoder-base.npy"
UNIX_PROG = f"{CACHE}/corpus_vectors_microsoft_unixcoder-base.progress"
MINILM_NPY = f"{CACHE}/corpus_vectors_all-MiniLM-L6-v2.npy"
UNIX_DIM, MINILM_DIM = 768, 384
def _read_progress(path: str) -> int:
with open(path) as f:
return int(f.read().strip() or "0")
def _golds_first_corpus(corpus, queries):
"""Reproduce index_corpus.py's GOLDS_FIRST reorder exactly."""
gold_ids = {q["relevant_id"] for q in queries}
golds = [d for d in corpus if d["id"] in gold_ids]
rest = [d for d in corpus if d["id"] not in gold_ids]
return golds + rest
def _topk_ids(query_vecs, doc_vecs, doc_ids, k=100, chunk=512):
"""Brute-force cosine (vectors are pre-normalized): top-k doc_ids per query."""
out = []
for i in range(0, len(query_vecs), chunk):
sims = query_vecs[i : i + chunk] @ doc_vecs.T # (b, P)
kk = min(k, sims.shape[1])
part = np.argpartition(-sims, kk - 1, axis=1)[:, :kk]
for r in range(sims.shape[0]):
order = part[r][np.argsort(-sims[r, part[r]])]
out.append([doc_ids[j] for j in order])
return out
def _eval_model(name, npy_path, dim, doc_rows, query_texts, relevant_ids, encoder):
"""doc_rows: np.ndarray of cache-row indices for the pool docs (in pool order)."""
mm = np.memmap(npy_path, dtype=np.float32, mode="r").reshape(-1, dim)
doc_vecs = np.ascontiguousarray(mm[doc_rows]) # (P, dim)
print(f"[{name}] encoding {len(query_texts):,} queries...", flush=True)
qv = encoder.encode(query_texts, normalize_embeddings=True, batch_size=128)
qv = np.asarray(qv, dtype=np.float32)
retrieved = _topk_ids(qv, doc_vecs, POOL_IDS, k=100)
qr = list(zip(retrieved, relevant_ids))
return (
mean_reciprocal_rank(qr, k=10),
mean_ndcg(qr, k=10),
mean_recall(qr, k=100),
)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--min-docs", type=int, default=0,
help="Require at least this many embedded docs before evaluating.")
args = ap.parse_args()
P = _read_progress(UNIX_PROG)
print(f"UniXcoder embed-progress P = {P:,} docs")
if P < args.min_docs:
print(f"Not enough yet (need {args.min_docs:,}); exiting.")
return 0
corpus, queries = load_codesearch(n=-1)
orig_row = {d["id"]: i for i, d in enumerate(corpus)} # doc_id -> MiniLM cache row
gf = _golds_first_corpus(corpus, queries) # golds-first (== unixcoder cache order)
pool = gf[:P]
global POOL_IDS
POOL_IDS = [d["id"] for d in pool]
pool_id_set = set(POOL_IDS)
# rows into each cache for the pool docs, in pool order
unix_rows = np.arange(P) # unixcoder cache is already golds-first
minilm_rows = np.array([orig_row[i] for i in POOL_IDS]) # map to original-order MiniLM cache
subset = [q for q in queries if q["relevant_id"] in pool_id_set]
texts = [q["query"] for q in subset]
rel = [q["relevant_id"] for q in subset]
print(f"Pool: {P:,} docs | eval queries (gold in pool): {len(subset):,}")
if not subset:
print("No queries have their gold in the pool yet — wait for more golds.")
return 0
unix_enc = load_encoder(UNIX_MODEL)
minilm_enc = load_encoder(MINILM_MODEL)
u_mrr, u_ndcg, u_rec = _eval_model("unixcoder", UNIX_NPY, UNIX_DIM, unix_rows, texts, rel, unix_enc)
m_mrr, m_ndcg, m_rec = _eval_model("minilm", MINILM_NPY, MINILM_DIM, minilm_rows, texts, rel, minilm_enc)
print("\n" + "=" * 62)
print(f"PARTIAL-INDEX A/B (pool={P:,} docs, n={len(subset):,} queries, brute-force cosine)")
print("NOTE: reduced pool inflates absolute scores; the MODEL DELTA is the signal.")
print("=" * 62)
print(f"{'Model':<16}{'MRR@10':>10}{'nDCG@10':>10}{'Recall@100':>12}")
print("-" * 62)
print(f"{'MiniLM (base)':<16}{m_mrr:>10.4f}{m_ndcg:>10.4f}{m_rec:>12.4f}")
print(f"{'UniXcoder':<16}{u_mrr:>10.4f}{u_ndcg:>10.4f}{u_rec:>12.4f}")
print(f"{'Δ (Unix-Mini)':<16}{u_mrr-m_mrr:>+10.4f}{u_ndcg-m_ndcg:>+10.4f}{u_rec-m_rec:>+12.4f}")
print("=" * 62)
return 0
if __name__ == "__main__":
sys.exit(main())
|