DERBACH/latent-rubric / scripts /evaluate_heads.py
DERBACH's picture
download
raw
14.4 kB
import argparse,json,itertools,shutil,datetime
from pathlib import Path
import numpy as np,pandas as pd,torch
from eval.metrics import corr,matching,axis_metrics,cca,cka,selectivity,composition
from data.helpsteer import CRITERIA
from intervention.natural_pairs import natural_pairs
from analysis.tracking import rebuild,save_evaluation_provenance
from representations.cache import validate_feature_table,file_hash,atomic_json
def savez_atomic(path,**values):
path=Path(path);tmp=path.with_suffix('.tmp.npz');np.savez_compressed(tmp,**values);tmp.replace(path)
def validate_predictions(pred,ex,pref):
assert np.array_equal(ex.response_id.to_numpy(),pred['response_ids']),'Prediction response identity mismatch'
assert len(pred['reward'])==len(ex) and len(pred['z'])==len(ex)
assert np.isfinite(pred['reward']).all() and np.isfinite(pred['z']).all(),'Nonfinite predictions'
te=pref[(pref.split=='test')&~pref.tie]
assert np.array_equal(te.pair_id.to_numpy(),pred['test_pair_ids']),'Prediction preference pair identity mismatch'
assert np.array_equal(te.label.to_numpy(),pred['test_labels']),'Prediction preference labels changed'
logits=pred['reward'][te.a_index]-pred['reward'][te.b_index]
assert np.array_equal(logits,pred['test_logits']),'Saved preference logits inconsistent with rewards'
def compose_axes(z,a,pairs,axes):
if axes==[0,1,2,3]:return composition(z,a,pairs)
from eval.metrics import rubric_weights
from scipy.stats import kendalltau,spearmanr
w=rubric_weights()[:,axes];z=(z-z.mean(0))/(z.std(0)+1e-10);a=(a-a.mean(0))/(a.std(0)+1e-10)
sm=z@w.T;sh=a@w.T;i,j=np.asarray(pairs).T;dh=sh[i]-sh[j];dm=sm[i]-sm[j];mask=abs(dh)>1e-8
acc=np.array([np.mean((dm[:,r]*dh[:,r]>0)[mask[:,r]]) if mask[:,r].any() else np.nan for r in range(len(w))])
tau=np.array([kendalltau(sm[:,r],sh[:,r]).statistic for r in range(len(w))]);rho=np.array([spearmanr(sm[:,r],sh[:,r]).statistic for r in range(len(w))])
return {'composition_accuracy':float(np.nanmean(acc)),'composition_kendall_tau':float(np.nanmean(tau)),'composition_spearman':float(np.nanmean(rho)),'composition_valid_rubrics':int(np.isfinite(acc).sum()),'composition_reduced_projection':True},{'weights':w,'accuracy':acc,'tau':tau,'rho':rho,'pair_correct':(dm*dh>0).astype('float32'),'pair_valid':mask,'pairs':pairs}
def evaluate(path):
save_evaluation_provenance(path)
cfg=json.loads((path/'config.json').read_text()); metrics=json.loads((path/'metrics.json').read_text())
spec=json.loads((path/'fit_spec.json').read_text())
identity=validate_feature_table(f'outputs/features/{cfg["backbone_tag"]}','data/processed/responses.parquet',f'data/processed/tokenized_{cfg["backbone_tag"]}.parquet')
assert identity==spec['features'],'Original features changed since fit'
assert file_hash('data/processed/preferences.parquet')==spec['preferences_sha256'],'Preference table changed since fit'
for name,digest in json.loads((path/'TRAINED.json').read_text()).get('fit_artifact_hashes',{}).items():
assert file_hash(path/name)==digest,f'Fitted artifact changed: {name}'
ex=pd.read_parquet('data/processed/responses.parquet').sort_values('feature_index').reset_index(drop=True)
validate_predictions(np.load(path/'predictions.npz'),ex,pd.read_parquet('data/processed/preferences.parquet'))
if (path/'EVALUATED.json').exists():(path/'EVALUATED.json').replace(path/'EVALUATED.previous.json')
population={'human_test':'pending','natural':'pending','heldout_generated':'unavailable','stability':'pending'}
heldout_keys=['target_effect','criterion_leakage','leakage_median','leakage_ratio_of_means','residual_movement']
for key in heldout_keys:metrics[key]=None;metrics['natural_'+key]=None
metrics['natural_n']=0;metrics['heldout_intervention_n']=0
if (path/'heldout_interventions.npz').exists():(path/'heldout_interventions.npz').replace(path/'heldout_interventions.previous.npz')
source={}
for name in ['scripts/evaluate_heads.py','eval/metrics.py','intervention/natural_pairs.py','representations/cache.py']:
source[name]=file_hash(name);dest=path/'evaluation_source'/name;dest.parent.mkdir(parents=True,exist_ok=True);shutil.copyfile(name,dest)
(path/'evaluation_source_manifest.json').write_text(json.dumps(source,indent=2))
pref_pred=np.load(path/'predictions.npz')
metrics['preference_accuracy']=float(np.mean(pref_pred['test_logits']*(2*pref_pred['test_labels']-1)>0))
if cfg['method']=='scalar':
metrics['evaluation_status']='preference_evaluated_no_latent_axes'
atomic_json(path/'metrics.json',metrics)
atomic_json(path/'EVALUATION_POPULATIONS.json',{'human_test':'not_applicable','natural':'not_applicable','heldout_generated':'not_applicable','stability':'not_applicable'})
return
pred=np.load(path/'predictions.npz');z=pred['z'];ex=pd.read_parquet('data/processed/responses.parquet').sort_values('feature_index').reset_index(drop=True)
a=ex[['response_id','split']].merge(pd.read_parquet('data/processed/attributes.parquet'),on='response_id',how='left')[CRITERIA].to_numpy()
assert np.array_equal(ex.response_id.to_numpy(),pred['response_ids'])
axes=cfg.get('criteria_indices',[0,1,2,3]);q=len(axes)
tr=ex.split=='train';dev=ex.split=='dev';test=ex.split=='test';zt=z[test];at=a[test][:,axes]
singular=np.linalg.svd(z[dev]-z[dev].mean(0),compute_uv=False)
variance=singular**2/(np.sum(singular**2)+1e-12)
metrics['dev_coordinate_variance_fractions']=variance.tolist()
metrics['dev_coordinate_effective_rank']=float(np.exp(-np.sum(variance*np.log(variance+1e-12))))
named=cfg['method'] in ['ilr','attribute'] and (cfg['method']=='attribute' or cfg['n_interventions']>0)
stats,mat=axis_metrics(zt,at,named=named,rank=True);metrics.update(stats)
# Test Hungarian assignment is descriptive only; all deployed matching uses DEV.
if named:order=np.arange(q);sign=np.ones(q)
else:order,sign=matching(z[dev],a[dev][:,axes],rank=True)
remaining=[i for i in range(z.shape[1]) if i not in order];all_order=np.r_[order,remaining].astype(int);all_sign=np.r_[sign,np.ones(len(remaining))]
aligned=z[:,all_order]*all_sign; norm=aligned[tr].std(0)+1e-10
indices=np.flatnonzero(test);tex=ex[test].reset_index(drop=True);pairs=[];pids=[]
for pid,ids in tex.groupby('prompt_id').indices.items():
for i,j in itertools.combinations(ids,2):pairs.append((i,j));pids.append(pid)
pairs=np.array(pairs)
cm,cd=compose_axes(aligned[test,:q],at,pairs,axes);metrics.update(cm)
if not named:
metrics['composition_mapping']='Oracle Dev Axis Matching (Hungarian + signs)'
rawcm,_=compose_axes(zt[:,:q],at,pairs,axes);metrics.update({f'unmatched_{k}':v for k,v in rawcm.items()})
else:metrics['composition_mapping']='intervention-defined' if cfg['method']=='ilr' else 'dense-supervised'
natural=natural_pairs(tex,a[test],min_target=1,max_off=.5)
natural=natural[np.isin(natural[:,2],axes)]
if len(natural):natural[:,2]=np.array([{k:i for i,k in enumerate(axes)}[int(k)] for k in natural[:,2]])
ns={};nd={}
if len(natural):
ai,bi,k,s=natural.T;dz=(aligned[indices[bi.astype(int)]]-aligned[indices[ai.astype(int)]])/norm
ns,nd=selectivity(dz,k.astype(int),s,criteria=q);metrics.update({f'natural_{key}':v for key,v in ns.items()});metrics['natural_n']=len(natural)
population.update(human_test='complete',natural='complete' if len(natural) else 'empty')
savez_atomic(path/'evaluation.npz',correlation=corr(zt,at,rank=True),matched_correlation=mat,oracle_dev_order=order,oracle_dev_sign=sign,z_aligned=aligned[test],attributes=at,test_prompt_ids=tex.prompt_id.to_numpy(str),natural_pairs=natural,criteria_indices=np.array(axes),**{f'natural_{k}':v for k,v in nd.items()},**{f'composition_{k}':v for k,v in cd.items()},composition_prompt_ids=np.array(pids,dtype=str))
# Held-out generated interventions use TRAIN-origin prompts excluded from intervention training.
ipath=Path(f'outputs/features/{cfg["backbone_tag"]}_interventions')
if (ipath/'COMPLETE.json').exists():
identity=validate_feature_table(ipath,'data/processed/interventions.parquet',f'data/processed/tokenized_{cfg["backbone_tag"]}_interventions.parquet')
if cfg.get('intervention_identity') is not None:assert identity==cfg['intervention_identity'],'Intervention cache differs from fitted run'
for key in ['model','model_revision','tokenizer_revision','layers','d','pooling_sha256']:
assert cfg['feature_manifest'][key]==identity['manifest'][key],f'Original/modified feature-space mismatch: {key}'
ir=pd.read_parquet('data/processed/interventions.parquet');ir=ir[(ir.int_split=='test')&ir.strict&ir.criterion_index.isin(axes)]
population['heldout_generated']='empty' if not len(ir) else 'complete'
if len(ir):
from models.latent_linear import LatentLinear
from models.latent_mlp import LatentMLP
from models.multi_attribute import MultiAttribute
m=MultiAttribute(len(pred['mean'])) if cfg['method']=='attribute' else (LatentMLP if cfg['latent_architecture']=='mlp' else LatentLinear)(len(pred['mean']),cfg['latent_dim'])
m.load_state_dict(torch.load(path/'model.pt',map_location='cpu',weights_only=True));m.eval()
ih=np.asarray(np.load(ipath/f'layer{cfg["representation_layer"]}_{cfg["pooling_method"]}.npy',mmap_mode='r')[ir.feature_index],dtype='float32');ih=(ih-pred['mean'])/pred['scale']
assert np.isfinite(ih).all()
with torch.inference_mode():iz=np.concatenate([m.encode(x).numpy() for x in torch.tensor(ih).split(256)])
delta=(iz[:,all_order]*all_sign-aligned[ir.base_feature_index])/norm
mapped=ir.criterion_index.map({k:i for i,k in enumerate(axes)}).to_numpy()
im,idd=selectivity(delta,mapped,ir.direction.to_numpy(),criteria=q);metrics.update(im)
metrics['heldout_intervention_n']=len(ir)
savez_atomic(path/'heldout_interventions.npz',intervention_ids=ir.intervention_id.to_numpy(str),prompt_ids=ir.prompt_id.to_numpy(str),criterion=ir.criterion_index.to_numpy(),coordinate_criterion=mapped,direction=ir.direction.to_numpy(),delta_z=delta,**idd)
metrics['evaluation_status']='semantic_evaluated';atomic_json(path/'metrics.json',metrics)
atomic_json(path/'EVALUATION_POPULATIONS.json',population)
print(path.name,{k:metrics.get(k) for k in ['preference_accuracy','axis_purity','composition_accuracy','natural_n','target_effect']},flush=True)
def stability(paths):
groups={}
for p in paths:
r=json.loads((p/'metrics.json').read_text())
if r['method']=='scalar':continue
key=tuple(r[k] for k in ['backbone','representation_layer','pooling_method','method','latent_architecture','latent_dim','n_interventions','intervention_filter'])+(r.get('control','standard'),tuple(r.get('criteria_indices',[0,1,2,3])))
groups.setdefault(key,[]).append((p,r))
ex=pd.read_parquet('data/processed/responses.parquet').sort_values('feature_index');test=ex.split.to_numpy()=='test'
for items in groups.values():
for p,r in items:
x=np.load(p/'predictions.npz')['z'][test];ax=[];sub=[];cks=[];semantic=[];semantic_sub=[];residual=[];pair_records=[]
xa=np.load(p/'evaluation.npz')['z_aligned'];dim=r['criteria_count']
for q,s in items:
if q==p:continue
y=np.load(q/'predictions.npz')['z'][test];c=corr(x,y)
if r['method'] in ['ilr','attribute'] and (r['method']=='attribute' or r['n_interventions']):ax.append(np.diag(c)[:r['criteria_count']].mean())
else:
order,_=matching(x,y);ax.append(abs(c[order,np.arange(y.shape[1])]).mean())
sub.append(cca(x,y));cks.append(cka(x,y))
ya=np.load(q/'evaluation.npz')['z_aligned'];semantic.append(float(np.diag(corr(xa[:,:dim],ya[:,:dim])).mean()));semantic_sub.append(cca(xa[:,:dim],ya[:,:dim]))
if xa.shape[1]>dim:residual.append(float(abs(corr(xa[:,dim:],ya[:,dim:])).mean()))
pair_records.append({'other_seed':s['seed'],'full_axis_stability':float(ax[-1]),'full_subspace_overlap':float(sub[-1]),'dev_named_semantic_axis_stability':semantic[-1],'dev_named_semantic_subspace_overlap':semantic_sub[-1]})
r.update(axis_stability=float(np.mean(ax)) if ax else None,subspace_stability=float(np.mean(sub)) if sub else None,subspace_linear_cka=float(np.mean(cks)) if cks else None)
r.update(dev_named_semantic_axis_stability=float(np.mean(semantic)) if semantic else None,dev_named_semantic_subspace_stability=float(np.mean(semantic_sub)) if semantic_sub else None,residual_axis_stability=float(np.mean(residual)) if residual else None)
atomic_json(p/'seed_pair_stability.json',pair_records)
atomic_json(p/'metrics.json',r)
if __name__=='__main__':
p=argparse.ArgumentParser();p.add_argument('--pattern',default='*');args=p.parse_args()
torch.set_num_threads(2);paths=[p for p in Path('outputs/real').glob(args.pattern) if (p/'TRAINED.json').exists()]
for path in paths:evaluate(path)
stability(paths)
for path in paths:
save_evaluation_provenance(path)
populations=json.loads((path/'EVALUATION_POPULATIONS.json').read_text());populations['stability']='complete' if populations['stability']!='not_applicable' else 'not_applicable'
atomic_json(path/'EVALUATION_POPULATIONS.json',populations)
inputs={str(p):file_hash(p) for p in [Path('data/processed/responses.parquet'),Path('data/processed/attributes.parquet'),Path('data/processed/preferences.parquet'),path/'predictions.npz',path/'model.pt']}
if populations['heldout_generated'] in ['complete','empty']:inputs['data/processed/interventions.parquet']=file_hash('data/processed/interventions.parquet')
atomic_json(path/'EVALUATED.json',{'completed_at':datetime.datetime.now(datetime.timezone.utc).isoformat(),'populations':populations,'input_hashes':inputs,'evaluation_source':json.loads((path/'evaluation_source_manifest.json').read_text()),'artifact_hashes':{p.name:file_hash(p) for p in [path/'metrics.json',path/'evaluation.npz',path/'heldout_interventions.npz'] if p.exists()}})
rebuild()

Xet Storage Details

Size:
14.4 kB
·
Xet hash:
3ef116d024a53ff85fa23ee2ebaff3c87be08bb86f9521481425a201ca9b02fc

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.