File size: 7,357 Bytes
0e4284f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
#!/usr/bin/env python3
"""Export per-query NDCG@10 β€” lean version. No TSV I/O during run.
Saves periodic checkpoints for resume support."""
import json, os, time, math, gc, csv, traceback, sys
import numpy as np

os.environ["OPENAI_API_KEY"] = "dummy"
INDEX_DIR = "/workspace/pyserini_index"
QUERIES_FILE = "/workspace/fever/queries.jsonl"
QRELS_FILE = "/workspace/fever/qrels/test.tsv"
POOL_FILE = "/workspace/beir_pool.json"
OUT_DIR = "/workspace/rankings"
CHECKPOINT = os.path.join(OUT_DIR, "_checkpoint.json")
DELTAS_CSV = os.path.join(OUT_DIR, "query_deltas.csv")

os.makedirs(OUT_DIR, exist_ok=True)

print("Loading queries and qrels...")
queries = {}
with open(QUERIES_FILE) as f:
    for line in f:
        d = json.loads(line)
        queries[d['_id']] = d['text']

qrels = {}
with open(QRELS_FILE) as f:
    reader = csv.reader(f, delimiter='\t')
    next(reader)
    for row in reader:
        if not row: continue
        qrels.setdefault(row[0], {})[row[1]] = int(row[2])

with open(POOL_FILE) as f:
    pool_data = json.load(f)
eval_qids = pool_data["qids"]
pool_dict = pool_data["pool"]
print(f"{len(eval_qids)} queries with pool data")

print("Loading Pyserini searcher...")
from pyserini.search import SimpleSearcher
searcher = SimpleSearcher(INDEX_DIR)
searcher.set_bm25(k1=1.2, b=0.75)

def ndcg10(ranked_list, gt):
    dcg = sum((2**gt.get(did,0)-1)/math.log2(k+2) for k,(did,_) in enumerate(ranked_list[:10]))
    ig = sorted(gt.values(), reverse=True)
    idcg = sum((2**ig[k]-1)/math.log2(k+2) for k in range(min(10,len(ig))))
    return dcg/idcg if idcg > 0 else 0.0

print("Loading MiniLM...")
import torch
from transformers import AutoTokenizer, AutoModel
torch.set_num_threads(4)
model_name = "sentence-transformers/all-MiniLM-L6-v2"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name).eval()

def encode(texts):
    inputs = tokenizer(texts, padding=True, truncation=True, max_length=128, return_tensors="pt")
    with torch.no_grad():
        emb = model(**inputs).last_hidden_state.mean(dim=1)
    return emb

encode(["warmup"])
print("MiniLM ready")

ABLATIONS = {
    "bm25": (0, 0, 0, "bm25"),
    "dense": (0, 0, 0, "dense"),
    "dense+T": (0.2, 0, 0, "dense"),
    "dense+S": (0, 0.1, 0, "dense"),
    "dense+M": (0, 0, 0.2, "dense"),
    "dense+TS": (0.2, 0.1, 0, "dense"),
    "dense+MT": (0.2, 0, 0.2, "dense"),
    "dense+MS": (0, 0.1, 0.2, "dense"),
    "dense+MTS": (0.2, 0.1, 0.2, "dense"),
}
CT_VALS = [0.3, 0.5, 0.7, 0.8, 0.85, 0.9]
TW_VALS = [0.1, 0.15, 0.2, 0.3]

# ── Resume logic ──
results = []
start_idx = 0
errors = 0
if os.path.exists(CHECKPOINT):
    with open(CHECKPOINT) as f:
        cp = json.load(f)
        results = cp["results"]
        start_idx = cp["next_idx"]
        errors = cp.get("errors", 0)
    print(f"Resuming from query index {start_idx} ({len(results)} completed)")

t0 = time.time()

# ── Main loop ──
for qi in range(start_idx, len(eval_qids)):
    qid = eval_qids[qi]
    try:
        pool_100 = pool_dict.get(qid, [])
        if not pool_100:
            continue
        
        pool = [p[0] for p in pool_100]
        bm25_s = np.array([p[1] for p in pool_100], dtype=float)
        
        pool_texts = []
        for docid in pool[:100]:
            doc = searcher.doc(docid)
            if doc:
                raw = doc.raw()
                d = json.loads(raw)
                pool_texts.append(d.get('contents', ''))
            else:
                pool_texts.append("")
        
        if not any(pool_texts):
            continue
        
        bm25_n = (bm25_s - bm25_s.min()) / (bm25_s.max() - bm25_s.min() + 1e-10)
        q_emb = encode([queries[qid]])
        pool_embs = encode(pool_texts)
        
        n_pool = len(pool)
        q_normed = q_emb / (torch.norm(q_emb, dim=1, keepdim=True) + 1e-10)
        pool_normed = pool_embs / (torch.norm(pool_embs, dim=1, keepdim=True) + 1e-10)
        dense_scores = (q_normed @ pool_normed.T).cpu().numpy().flatten()
        dense_n = (dense_scores - dense_scores.min()) / (dense_scores.max() - dense_scores.min() + 1e-10)
        
        sim = (pool_normed @ pool_normed.T).cpu().numpy()
        cs = np.sum(sim > 0.7, axis=1).astype(float)
        csn = (cs - cs.min()) / (cs.max() - cs.min() + 1e-10)
        iso = cs <= 1
        sim_top2 = sim[0][1] if n_pool > 1 else 0
        
        gt = qrels.get(qid, {})
        
        row = {"qid": qid}
        for a_name, (tw, sh, mu, base_type) in ABLATIONS.items():
            base = bm25_n if base_type == "bm25" else dense_n
            if a_name == "bm25" or a_name == "dense":
                ranked = sorted([(pool[i], float(bm25_n[i] if base_type == 'bm25' else dense_n[i])) for i in range(n_pool)], key=lambda x: -x[1])
            else:
                fs = base.copy()
                if tw > 0: fs += tw * csn
                if sh > 0: fs -= sh * iso.astype(float)
                if mu > 0 and n_pool > 2 and sim_top2 > 0.7:
                    fs[(sim[0] > 0.7) | (sim[1] > 0.7)] += mu
                ranked = sorted([(pool[i], float(fs[i])) for i in range(n_pool)], key=lambda x: -x[1])
            row[a_name] = ndcg10(ranked, gt)
        
        # Sweep
        for ct in CT_VALS:
            cs2 = np.sum(sim > ct, axis=1).astype(float)
            csn2 = (cs2 - cs2.min()) / (cs2.max() - cs2.min() + 1e-10)
            for tw2 in TW_VALS:
                fs2 = dense_n + tw2 * csn2
                ranked = sorted([(pool[i], float(fs2[i])) for i in range(n_pool)], key=lambda x: -x[1])
                k = f"tawatur_ct={ct}_tw={tw2}"
                row[k] = ndcg10(ranked, gt)
        
        results.append(row)
        
        del q_emb, pool_embs, pool_normed, sim
        gc.collect()
        
        if qi > 0 and qi % 200 == 0:
            rate = (qi + 1 - start_idx) / max(time.time() - t0, 1)
            remaining = (len(eval_qids) - qi - 1) / rate if rate > 0 else 0
            print(f"  {qi+1}/{len(eval_qids)} @ {rate:.1f}q/s, ~{remaining:.0f}s left")
            
            # Save checkpoint every 200 queries
            with open(CHECKPOINT, 'w') as f:
                json.dump({"results": results, "next_idx": qi + 1, "errors": errors}, f)
            
    except Exception as e:
        errors += 1
        print(f"  ERROR qid={qid}: {e}")
        if errors > 20:
            print("TOO MANY ERRORS")
            break
        continue

# ── Write CSV ──
if results:
    fields = list(results[0].keys())
    with open(DELTAS_CSV, 'w') as f:
        f.write(",".join(fields) + "\n")
        for row in results:
            f.write(",".join(str(row.get(f, "")) for f in fields) + "\n")
    print(f"\nExported {len(results)} queries to {DELTAS_CSV}")

# Cleanup
if os.path.exists(CHECKPOINT):
    os.remove(CHECKPOINT)

# Print summary
dense_vals = np.array([r["dense"] for r in results])
print(f"\n{'System':<25} {'NDCG@10':>10} {'vs Dense':>12}")
print("-" * 50)
for k in ["bm25", "dense", "dense+T", "dense+S", "dense+M", "dense+TS", "dense+MT", "dense+MS", "dense+MTS"]:
    vals = np.array([r[k] for r in results])
    mean_v = np.mean(vals)
    d = mean_v - np.mean(dense_vals)
    print(f"{k:<25} {mean_v:>10.4f} {d:>+12.4f}")

elapsed = time.time() - t0
print(f"\nTotal: {len(results)} queries, {errors} errors, {elapsed:.0f}s")