"""Neural-only evaluation for three user-uploaded RAG/evidence datasets. All ranking uses original MiniLM cosine / pretrained MS-MARCO cross encoder. Graph is a fixed previously specified directed-link/title selection policy; gold is never accessed by scoring/selection. QASC has TWO explicitly separate protocols. """ import os os.environ['OPENBLAS_NUM_THREADS']='1'; os.environ['MKL_NUM_THREADS']='1'; os.environ['OMP_NUM_THREADS']='4' import json, time, struct, collections, sys from pathlib import Path import numpy as np import torch from safetensors.torch import load_file from minilm_cpu_pipeline import MiniLM, WordPiece, embed from neural_crossencoder_only import load_crossencoder,PairTokenizer,score_pairs from neural_evidence_graph_core import (hover,graph_arrays,graph_select,seeded_sample,doc,ROOT) from parquet_lite import read_col,reconstruct from parquet_probe import parse K=(2,3,5,8) def get_dense(): m=MiniLM();sd=load_file(str(ROOT/'original_minilm/model.safetensors')) for key in ('embeddings.position_ids','pooler.dense.bias','pooler.dense.weight'):sd.pop(key) m.load_state_dict(sd,strict=True);m.eval();torch.set_num_threads(4) return m,WordPiece(str(ROOT/'vocab.txt'),length=128) def embed_texts(texts,m,tok,batch=32,label=''): t=time.perf_counter();out=embed(texts,m,tok,batch_size=batch) print('DENSE_EMBED',label,len(texts),'seconds',round(time.perf_counter()-t,3),flush=True) return out,time.perf_counter()-t def get_qasc(): p=ROOT/'train-00000-of-00001.parquet';f=open(p,'rb');f.seek(-8,2) size=struct.unpack('=2] questions=seeded_sample(eligible,n) texts=[r['title']+'. '+r['body'][:1800] for r in corpus] de,tidx=embed_texts(texts,m,tok,label='MultiHop corpus') qe,tq=embed_texts([r['query'] for r in questions],m,tok,label='MultiHop queries') sim=qe@de.T;inds=np.argsort(-sim,axis=1,kind='stable')[:,:20] rows=[];sc=[] for i,r in enumerate(questions): docs=[doc(corpus[j]['title'],corpus[j]['body'],corpus[j]['url']) for j in inds[i]] rows.append({'qid':f'multihop:{i}', 'query':r['query'],'docs':docs,'gold':{e['url'] for e in r['evidence_list']},'pool':'Top-20 neural MiniLM retrieval from full 609-article corpus (no gold insertion)'}) sc.append(sim[i,inds[i]]) return rows,sc,{'index_seconds':tidx,'query_embedding_seconds':tq,'total_prep_seconds':time.perf_counter()-t,'index_size':len(corpus),'candidate_pool_size':20} def prepare_hover(n,m,tok): t=time.perf_counter();rows=hover(n) # Exact 10 candidate passages; labels sourced from corresponding HoVer claims. alltexts=[r['query'] for r in rows]+[d['title']+'. '+d['body'] for r in rows for d in r['docs']] em,sec=embed_texts(alltexts,m,tok,label='HoVer queries+passages') Q=em[:len(rows)]; Ds=np.split(em[len(rows):],np.cumsum([len(r['docs']) for r in rows])[:-1]);sc=[Ds[i]@Q[i] for i in range(len(rows))] return rows,sc,{'embedding_seconds':sec,'prep_seconds':time.perf_counter()-t,'candidate_pool_size':10,'eligibility':'HoVer dev claims with HotpotQA dev passages available and all annotated support titles present; conditional subset'} def prepare_qasc(n,m,tok): t=time.perf_counter();allrows,facts=get_qasc();qs=seeded_sample(allrows,n) fe,tidx=embed_texts(facts,m,tok,label='QASC all distinct supporting fact sentences') qe,tq=embed_texts([r['question'] for r in qs],m,tok,label='QASC query') sim=qe@fe.T;inds=np.argsort(-sim,axis=1,kind='stable')[:,:60] rows_native=[];scores_native=[];rows_ctrl=[];scores_ctrl=[] for i,r in enumerate(qs): gold={str(x).strip() for x in r['facts']} docs=[doc('',facts[int(j)],facts[int(j)]) for j in inds[i,:20]] rows_native.append({'qid':r['id'],'query':r['question'],'docs':docs,'gold':gold,'pool':'Top-20 MiniLM from all unique QASC supporting fact sentences, without gold insertion'}) scores_native.append(sim[i,inds[i,:20]]) neg=[int(j) for j in inds[i] if facts[int(j)] not in gold][:18] if len(neg)!=18:continue chosen=list(gold)+[facts[j] for j in neg] perm=np.random.default_rng(i+301).permutation(len(chosen)) chosen=[chosen[int(j)] for j in perm] look={f:j for j,f in enumerate(facts)} rows_ctrl.append({'qid':r['id'],'query':r['question'],'docs':[doc('',z,z) for z in chosen],'gold':gold,'pool':'CONTROLLED: 2 gold facts deliberately inserted + 18 MiniLM hard negatives (oracle=100%)'}) scores_ctrl.append(np.array([sim[i,look[z]] for z in chosen],np.float32)) return (rows_native,scores_native),(rows_ctrl,scores_ctrl),{'index_seconds':tidx,'query_embedding_seconds':tq,'total_prep_seconds':time.perf_counter()-t,'fact_corpus_size':len(facts),'eligible_questions':len(allrows),'candidate_pool_size':20} def assess(name,rows,dense_scores,cross_scores,loadtime,extra): tgraph=time.perf_counter();details=[] counts={method:{str(k):[] for k in K} for method in ('dense_cosine','crossencoder','dense_graph','crossencoder_graph')} oracle={str(k):[] for k in K};av=[];links=[];sizecnt=collections.Counter() for i,r in enumerate(rows): gold=r['gold'];docs=r['docs'];ss=np.asarray(dense_scores[i],dtype=np.float32);cc=np.asarray(cross_scores[i],dtype=np.float32) assert len(ss)==len(cc)==len(docs) link,match=graph_arrays(r);links.append(bool(link.any())) avail=len(gold&{d['id'] for d in docs});av.append(avail==len(gold));sizecnt[len(gold)]+=1 orders={ 'dense_cosine':np.argsort(-ss,kind='stable').tolist(), 'crossencoder':np.argsort(-cc,kind='stable').tolist(), 'dense_graph':graph_select(ss,link,match,max(K)), 'crossencoder_graph':graph_select(cc,link,match,max(K)), } record={'id':r['qid'],'gold_count':len(gold),'pool_gold_count':avail,'orders':orders,'hits':{}} for k in K: oracle[str(k)].append(int(avail==len(gold) and len(gold)<=k)) for method,order in orders.items(): hit=int(gold.issubset({docs[j]['id'] for j in order[:k]})) counts[method][str(k)].append(hit) record['hits'].setdefault(method,{})[str(k)]=hit details.append(record) graphseconds=time.perf_counter()-tgraph delta=np.array(counts['crossencoder_graph']['3'],int)-np.array(counts['crossencoder']['3'],int) if len(delta): gen=np.random.default_rng(31);ci=np.percentile(delta[gen.integers(0,len(delta),(2000,len(delta)))].mean(axis=1)*100,[2.5,97.5]) else:ci=[float('nan')]*2 res={'dataset':name,'questions':len(rows),'candidate_count_total':sum(len(r['docs']) for r in rows),'candidate_pool':rows[0]['pool'] if rows else '', 'gold_support_count_distribution':dict(sizecnt),'gold_available_fraction':round(float(np.mean(av))*100,2),'gold_available_count':sum(av), 'graph_link_fraction':round(float(np.mean(links))*100,2), 'oracle_complete_coverage_pct':{k:round(float(np.mean(v))*100,2) for k,v in oracle.items()}, 'complete_coverage_pct':{met:{k:round(float(np.mean(val))*100,2) for k,val in d.items()} for met,d in counts.items()}, 'crossencoder_graph_minus_crossencoder_at3_pp':round(float(delta.mean()*100),2),'ci95_pp':[round(float(z),2) for z in ci], 'paired_wins_losses_ties_at3':{'wins':int((delta>0).sum()),'losses':int((delta<0).sum()),'ties':int((delta==0).sum())}, 'timing_seconds':{**extra,**loadtime,'graph_selection_total_s':round(graphseconds,3),'graph_selection_ms_per_question':round(graphseconds*1000/max(1,len(rows)),2)}, 'models':{'dense':'user uploaded all-MiniLM-L6-v2', 'crossencoder':'user uploaded cross-encoder/ms-marco-MiniLM-L6-v2'}, 'protocol':'MiniLM embeddings at max 128 WordPiece for dense. Cross-encoder truncates paired query+passage at 256 WordPiece. Both score exact same candidate pools. Fixed evidence graph .4 minmax relevance + .5 title in already-selected passage + .2 query-title score. No TF-IDF anywhere in candidate construction or scoring. Gold used solely for pool audit and evaluation.'} (ROOT/f'{name}_neural_only_results.json').write_text(json.dumps(res,indent=2)) (ROOT/f'{name}_neural_only_per_question.json').write_text(json.dumps(details)) print('RESULT',name,json.dumps(res),flush=True) return res def evaluate(name,rows,denses,embtimes,ce,tok): pairs=[(r['query'],(d['title']+'. ' if d['title'] else '')+d['body']) for r in rows for d in r['docs']] print('CROSS_START',name,'n',len(rows),'pairs',len(pairs),flush=True) t=time.perf_counter();cross,perf=score_pairs(ce,tok,pairs,batch=20) tsec=time.perf_counter()-t lengths=np.cumsum([len(r['docs']) for r in rows])[:-1] scores=np.split(cross,lengths) report=assess(name,rows,denses,scores,{'crossencoder_scoring_seconds':round(tsec,3),'crossencoder_scoring_ms_per_question':round(tsec*1000/len(rows),2),'crossencoder_forward_seconds':round(perf['total_forward_s'],3)},embtimes) return report if __name__=='__main__': n=int(sys.argv[1]) if len(sys.argv)>1 else 300 ds=sys.argv[2:] or ['hover','multihop','qasc'] dense,tok=get_dense();ce=load_crossencoder();ctok=PairTokenizer(length=256) for task in ds: if task=='hover': rows,scores,times=prepare_hover(n,dense,tok) evaluate('hover',rows,scores,times,ce,ctok) elif task=='multihop': rows,scores,times=prepare_multihop(n,dense,tok) evaluate('multihop',rows,scores,times,ce,ctok) elif task=='qasc': (rows,scores),(ctrl,cscores),times=prepare_qasc(n,dense,tok) evaluate('qasc_neural_retrieval',rows,scores,times,ce,ctok) evaluate('qasc_controlled_pool',ctrl,cscores,{'cached_fact_embeddings_used':True},ce,ctok) else:raise ValueError(task)