Spaces:
Sleeping
Sleeping
File size: 10,134 Bytes
dd990bf | 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 | """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('<I',f.read(4))[0];f.seek(-8-size,2);meta=parse(f.read(size));rows=[]
for rg in meta[4]:
cols={}
for c in rg[1]:
key=tuple(c[3][3])
if key not in [('id',),('question',),('fact1',),('fact2',)]:continue
cols[key]=reconstruct(*read_col(f,c,{key:(1,0)}),1,0)
rows.extend([{'id':cols[('id',)][i], 'question':cols[('question',)][i], 'facts':[cols[('fact1',)][i],cols[('fact2',)][i]]} for i in range(rg[3])])
rows=[r for r in rows if all(x and str(x).strip() for x in r['facts']) and r['facts'][0]!=r['facts'][1]]
facts=list(dict.fromkeys(z.strip() for r in rows for z in r['facts']))
return rows,facts
def prepare_multihop(n,m,tok):
t=time.perf_counter();corpus=json.load(open(ROOT/'corpus.json'));allq=json.load(open(ROOT/'MultiHopRAG.json'))
eligible=[r for r in allq if len({e['url'] for e in r['evidence_list']})>=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)
|