Spaces:
Sleeping
Sleeping
File size: 8,482 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 | """CPU-only evidence-completeness benchmark for HotpotQA distractor setting.
Uses frozen local MiniLM, no gold evidence in selection. Dataset's supporting facts evaluate chosen titles.
"""
import os,time,re,json,sys,hashlib
os.environ['OPENBLAS_NUM_THREADS']='1';os.environ['OMP_NUM_THREADS']='4'
from pathlib import Path
import numpy as np
from minilm_cpu_pipeline import load_model,WordPiece,embed
from parquet_lite import load_2wiki
DATA=Path('/mnt/data/validation-00000-of-00001.parquet');OUT=Path('/mnt/data/wiki_subset_out');OUT.mkdir(exist_ok=True)
N_TUNE=200;N_TEST=600;RNG=20261030
def facets(q):
q=q.strip().rstrip('?')
# Query-provided pieces only; no gold evidence or topic annotations.
p=re.split(r'(?i)\b(?:and|or|whose|which|who|that|where|while|whereas)\b|[,;:]\s*',q)
p=[x.strip() for x in p if len(x.split())>=2]
if len(p)<2:
w=q.split();m=max(2,len(w)//2)
p=[' '.join(w[:m]),' '.join(w[m:])] if len(w)>=5 else [q,q]
return (p[:3]+[q]*3)[:3]
def safe_key(x):return re.sub(r'\s+',' ',x.strip().lower())
def load_data():
data=[r for r in load_2wiki(str(DATA),12576) if len(r['context'])==10]; rng=np.random.default_rng(RNG)
print('total 2Wiki valid candidates',len(data),flush=True)
# Sample from all subtypes without using labels for feature engineering.
ids=rng.permutation(len(data))[:N_TUNE+N_TEST]
items=[data[int(i)] for i in ids]
counts={}
for r in items:counts[r['type']]=counts.get(r['type'],0)+1
print('sample',len(items),'types',counts,flush=True)
return items,ids
def encode_data(items):
path=OUT/'wiki_minilm_800.npz'
identity=hashlib.sha256('|'.join(r['_id'] for r in items).encode()).hexdigest()[:16]
if path.exists():
dat=np.load(path)
if str(dat['identity'])==identity:
print('loaded cached embeddings',flush=True)
return dat['q'],dat['docs'],dat['facets']
model=load_model();tok=WordPiece(length=128)
queries=[r['question'] for r in items]
docs=[title+'. '+ ' '.join(sentences) for r in items for title,sentences in r['context']]
ftexts=[t for r in items for t in facets(r['question'])]
print('encoding',len(queries),'queries',len(docs),'docs',len(ftexts),'aspects',flush=True)
st=time.perf_counter();q=embed(queries,model,tok,batch_size=24,callback=lambda i,n,t:print('q',i,n,round(t,1),flush=True) if i>0 else None)
print('query done',round(time.perf_counter()-st,1),flush=True)
d=embed(docs,model,tok,batch_size=24,callback=lambda i,n,t:print('docs',i,n,round(t,1),flush=True) if i>0 else None).reshape(len(items),10,384)
print('documents done',round(time.perf_counter()-st,1),flush=True)
f=embed(ftexts,model,tok,batch_size=24,callback=lambda i,n,t:print('facets',i,n,round(t,1),flush=True) if i>0 else None).reshape(len(items),3,384)
print('facets done',round(time.perf_counter()-st,1),flush=True)
np.savez_compressed(path,identity=identity,q=q,docs=d,facets=f)
return q,d,f
def greedy(rel,dc,fac,method,k=5,alpha=.6,beta=.2,threshold=.35):
# Single query score vector, 10x10 document similarity, facet-doc sim 3x10.
K=len(rel);sel=[];remain=set(range(K));
cfac=np.clip(fac-threshold,0,None)
mx=np.zeros(len(cfac));mxcluster=np.zeros(K)
for t in range(k):
best=(-1e9,None)
for j in sorted(remain):
if method=='cosine':v=rel[j]
elif method=='mmr':v=rel[j] if not sel else alpha*rel[j]-(1-alpha)*max(0,max(dc[j,x] for x in sel))
elif method=='coverage':v=alpha*rel[j]+(1-alpha)*np.maximum(cfac[:,j]-mx,0).mean()
elif method=='facility':
weights=np.clip(rel,0,None)**2
v=alpha*rel[j]+(1-alpha)*np.dot(weights,np.maximum(np.clip(dc[:,j],0,None)-mxcluster,0))/max(weights.sum(),1e-8)
elif method=='bridge':
bridge=max((dc[j,x] for x in sel),default=0)
v=alpha*rel[j]+(1-alpha)*bridge-beta*max(0,bridge-.8) if sel else rel[j]
elif method=='combined':
gain=np.maximum(cfac[:,j]-mx,0).mean()
# Complementary candidate information, plus query-conditioned facet coverage.
weights=np.clip(rel,0,None)**2
facility=np.dot(weights,np.maximum(np.clip(dc[:,j],0,None)-mxcluster,0))/max(weights.sum(),1e-8)
v=alpha*rel[j]+(1-alpha)*(beta*gain+(1-beta)*facility)
else:raise ValueError(method)
if v>best[0]:best=(v,j)
j=best[1];sel.append(j);remain.remove(j)
mx=np.maximum(mx,cfac[:,j]);mxcluster=np.maximum(mxcluster,np.clip(dc[:,j],0,None))
return sel
def eval_one(items,scores,docsim,facsim,method,params,inds,ks=(2,3,4,5)):
out=np.zeros((len(inds),len(ks)),float); selected=[];goldcounts=[];omiss=[]
for ii,i in enumerate(inds):
r=items[i]; gold=set(safe_key(t) for t,_ in r['supporting_facts']);titles=[safe_key(x[0]) for x in r['context']]
goldcounts.append(len(gold));possible=gold.issubset(set(titles));omiss.append(possible)
order=greedy(scores[i],docsim[i],facsim[i],method,k=max(ks),**params)
selected.append(order)
for jj,k in enumerate(ks):out[ii,jj]=int(possible and gold.issubset(set(titles[x] for x in order[:k])))
return out,np.asarray(selected),np.asarray(goldcounts),np.asarray(omiss)
def main():
items,orig_idx=load_data();q,d,f=encode_data(items)
sims=np.einsum('nd,nkd->nk',q,d).astype('float32');dcs=np.einsum('nkd,njd->nkj',d,d).astype('float32');fs=np.einsum('nmd,nkd->nmk',f,d).astype('float32')
tunes=np.arange(N_TUNE);tests=np.arange(N_TUNE,N_TUNE+N_TEST)
param_grid={
'cosine':[{}],
'mmr':[{'alpha':a} for a in [.3,.5,.7,.85,.95]],
'coverage':[{'alpha':a,'threshold':t} for a in [.1,.3,.5,.7,.9] for t in [.2,.35,.5,.65]],
'facility':[{'alpha':a} for a in [.2,.4,.6,.8,.95]],
'bridge':[{'alpha':a,'beta':b} for a in [.4,.6,.8] for b in [0,.2,.4]],
'combined':[{'alpha':a,'beta':b,'threshold':t} for a in [.2,.4,.6,.8] for b in [.25,.5,.75] for t in [.3,.5]],
}
best={};val={}
for method,grid in param_grid.items():
st=time.perf_counter();records=[]
for params in grid:
arr,_,_,_=eval_one(items,sims,dcs,fs,method,params,tunes)
records.append((float(arr[:,1].mean()),float(arr[:,2].mean()),params))
records.sort(key=lambda x:(x[0],x[1]),reverse=True)
best[method]=records[0][2];val[method]=records[0][0]
print('TUNED',method,'@3',records[0][0],best[method],'seconds',round(time.perf_counter()-st,1),flush=True)
results={};test_arrays={};selections={}
for method,params in best.items():
st=time.perf_counter();arr,idx,ngold,oracle=eval_one(items,sims,dcs,fs,method,params,tests)
ms=(time.perf_counter()-st)/len(tests)*1000
results[method]={'parameters':params,'validation_complete_evidence_at3':val[method],'heldout_complete_evidence_at_k':{str(k):float(arr[:,j].mean()) for j,k in enumerate((2,3,4,5))},'selection_cpu_ms_per_question':ms,'gold_title_count_distribution':{str(x):int((ngold==x).sum()) for x in np.unique(ngold)},'oracle_coverage_rate':float(oracle.mean())}
test_arrays[method]=arr;selections[method]=idx
print('TEST',method,results[method]['heldout_complete_evidence_at_k'],'ms',round(ms,3),flush=True)
base=test_arrays['cosine'];rng=np.random.default_rng(RNG)
bs=rng.integers(len(tests),size=(2000,len(tests)))
for method in best:
delta=test_arrays[method][:,1]-base[:,1]
ci=np.quantile(delta[bs].mean(axis=1),[.025,.975]).tolist()
results[method]['delta_at3_vs_cosine']=float(delta.mean());results[method]['delta_at3_ci95']=ci
results[method]['paired_win_loss_at3']={'wins':int((delta>0).sum()),'losses':int((delta<0).sum()),'ties':int((delta==0).sum())}
ids=[items[i]['_id'] for i in tests];details=[]
for j,i in enumerate(tests):
r=items[i]; details.append({'id':r['_id'],'question':r['question'],'answer':r['answer'],'type':r['type'],'gold_support_titles':sorted(set(t for t,_ in r['supporting_facts'])),'cosine': [r['context'][v][0] for v in selections['cosine'][j]],'coverage':[r['context'][v][0] for v in selections['coverage'][j]],'combined':[r['context'][v][0] for v in selections['combined'][j]]})
result={'dataset':'2WikiMultiHopQA validation','source_file':str(DATA),'total_dev':12576,'sample_count':len(items),'tune_count':N_TUNE,'heldout_count':N_TEST,'random_seed':RNG,'protocol':'Each question has own ten provided paragraphs; gold supporting passage titles are evaluation only; frozen MiniLM encoded whole query, each candidate passage, and three heuristic query facets; hyperparameters chosen on 250 dev cases; tested on separate 750 dev cases. No generative LLM was invoked.','results':results}
(OUT/'wiki_results.json').write_text(json.dumps(result,indent=2));(OUT/'wiki_case_studies.json').write_text(json.dumps(details,indent=2));print('DONE',flush=True)
if __name__=='__main__':main()
|