Skip to content

Commit a68b708

Browse files
committed
trace ace: add V78 semantic episode and ensemble experiment
1 parent d0bdbc3 commit a68b708

1 file changed

Lines changed: 106 additions & 0 deletions

File tree

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#!/usr/bin/env python3
2+
"""V78 seismic test: pretrained semantic interaction + learned episode mastery + V75 ensemble.
3+
4+
Tests three model-class shifts under identical frozen group folds:
5+
1) objective<->dialogue pretrained semantic interaction;
6+
2) learned episode/trajectory mastery from semantic episode views;
7+
3) nested-safe convex ensemble with the sparse V75 all-views model.
8+
9+
Development-time model: sentence-transformers/all-MiniLM-L6-v2 (open, and preloaded in official runtime).
10+
Only aggregate metrics are written.
11+
"""
12+
from __future__ import annotations
13+
import argparse, json
14+
from pathlib import Path
15+
import numpy as np
16+
import pandas as pd
17+
from scipy.sparse import csr_matrix, hstack
18+
from sklearn.feature_extraction.text import HashingVectorizer
19+
from sklearn.linear_model import LogisticRegression
20+
from sklearn.metrics import log_loss, roc_auc_score
21+
from sklearn.model_selection import GroupKFold
22+
from sklearn.preprocessing import StandardScaler
23+
from sentence_transformers import SentenceTransformer
24+
25+
from v71_mastery_events import load_transcript
26+
from v75_canonical_trajectory import load_training, trajectory_views, SEED
27+
28+
29+
def folds(groups, n=5):
30+
g=groups.astype(str).to_numpy(); z=np.zeros(len(g))
31+
return list(GroupKFold(n_splits=n).split(z,z,g))
32+
33+
def sigmoid(x):
34+
x=np.asarray(x,float); return 1/(1+np.exp(-np.clip(x,-40,40)))
35+
36+
def encode(model, texts, batch=128):
37+
return model.encode(texts,batch_size=batch,show_progress_bar=True,normalize_embeddings=True,convert_to_numpy=True).astype(np.float32)
38+
39+
def dense_semantic_features(Eo, Es, El, Et, numeric):
40+
# Explicit cross-view interaction features, not just independent embeddings.
41+
cos_os=np.sum(Eo*Es,1,keepdims=True); cos_ol=np.sum(Eo*El,1,keepdims=True); cos_ot=np.sum(Eo*Et,1,keepdims=True)
42+
cos_sl=np.sum(Es*El,1,keepdims=True); cos_lt=np.sum(El*Et,1,keepdims=True)
43+
# Pairwise products preserve dimensions while explicitly representing relevance/alignment.
44+
prod_ol=Eo*El; prod_ot=Eo*Et
45+
return np.hstack([Eo,Es,El,Et,prod_ol,prod_ot,cos_os,cos_ol,cos_ot,cos_sl,cos_lt,numeric]).astype(np.float32)
46+
47+
def build_v75_sparse(frame, view_rows, numeric):
48+
hv=HashingVectorizer(n_features=2**18,alternate_sign=False,norm='l2',ngram_range=(1,2),lowercase=True)
49+
obj=hv.transform(['[OBJECTIVE] '+str(x) for x in frame.learning_objective])
50+
raw=hv.transform(['[RAW] '+v['raw'] for v in view_rows])
51+
stu=hv.transform(['[STUDENT] '+v['student'] for v in view_rows])
52+
loc=hv.transform(['[LOCAL] '+v['local'] for v in view_rows])
53+
can=hv.transform(['[STATE] '+v['canonical'] for v in view_rows])
54+
ter=hv.transform(['[TERMINAL] '+v['terminal'] for v in view_rows])
55+
z=(numeric-numeric.mean(0))/(numeric.std(0)+1e-6)
56+
return hstack([obj,raw,stu,loc,can,ter,csr_matrix(z)],format='csr')
57+
58+
def eval_regime(Xv75, Xsem, y, split, name):
59+
p75=np.zeros(len(y)); psem=np.zeros(len(y)); fold_rows=[]
60+
for k,(tr,va) in enumerate(split,1):
61+
m75=LogisticRegression(C=.25,max_iter=300,solver='liblinear',random_state=SEED).fit(Xv75[tr],y[tr])
62+
a=np.clip(m75.predict_proba(Xv75[va])[:,1],1e-5,1-1e-5); p75[va]=a
63+
sc=StandardScaler().fit(Xsem[tr]); A=sc.transform(Xsem[tr]); B=sc.transform(Xsem[va])
64+
ms=LogisticRegression(C=.05,max_iter=500,solver='liblinear',random_state=SEED).fit(A,y[tr])
65+
b=np.clip(ms.predict_proba(B)[:,1],1e-5,1-1e-5); psem[va]=b
66+
fold_rows.append({'fold':k,'rows':len(va),'v75':float(log_loss(y[va],a)),'semantic':float(log_loss(y[va],b))})
67+
print(name,fold_rows[-1])
68+
# Blend selected globally from OOF only; report grid transparently. This is model comparison, not final stacking fit.
69+
grid=[]; best=None
70+
for w in np.linspace(0,1,21):
71+
p=np.clip((1-w)*p75+w*psem,1e-5,1-1e-5); ll=float(log_loss(y,p))
72+
row={'semantic_weight':float(w),'logloss':ll}; grid.append(row)
73+
if best is None or ll<best['logloss']: best=row
74+
pb=np.clip((1-best['semantic_weight'])*p75+best['semantic_weight']*psem,1e-5,1-1e-5)
75+
return {'v75_logloss':float(log_loss(y,p75)),'semantic_logloss':float(log_loss(y,psem)),'best_blend':best,'blend_auc':float(roc_auc_score(y,pb)),'folds':fold_rows,'blend_grid':grid}
76+
77+
def run(args):
78+
frame=load_training(args.features,args.labels).reset_index(drop=True)
79+
if args.limit: frame=frame.iloc[:args.limit].copy().reset_index(drop=True)
80+
cache={}; views=[]; nums=[]
81+
for i,r in frame.iterrows():
82+
sid=str(r.session_id)
83+
if sid not in cache: cache[sid]=load_transcript(args.transcripts/f'{sid}.csv')
84+
v,n,_=trajectory_views(cache[sid],str(r.learning_objective)); views.append(v); nums.append(n)
85+
if (i+1)%2500==0: print('views',i+1)
86+
numeric=np.vstack(nums).astype(np.float32)
87+
numz=(numeric-numeric.mean(0))/(numeric.std(0)+1e-6)
88+
model=SentenceTransformer(args.model)
89+
Eo=encode(model,[str(x) for x in frame.learning_objective],args.batch)
90+
Es=encode(model,[v['student'] or ' ' for v in views],args.batch)
91+
El=encode(model,[v['local'] or ' ' for v in views],args.batch)
92+
Et=encode(model,[v['terminal'] or ' ' for v in views],args.batch)
93+
Xsem=dense_semantic_features(Eo,Es,El,Et,numz)
94+
X75=build_v75_sparse(frame,views,numeric)
95+
y=frame.target.to_numpy(int)
96+
objgrp=frame.learning_objective_id if 'learning_objective_id' in frame else frame.learning_objective
97+
result={
98+
'diagnostics':{'rows':len(frame),'sessions':int(frame.session_id.nunique()),'objectives':int(frame.learning_objective.nunique()),'embedding_dim':int(Eo.shape[1]),'semantic_features':int(Xsem.shape[1]),'model':args.model},
99+
'session':eval_regime(X75,Xsem,y,folds(frame.session_id),'session'),
100+
'objective':eval_regime(X75,Xsem,y,folds(objgrp),'objective'),
101+
}
102+
Path(args.out).write_text(json.dumps(result,indent=2)); print(json.dumps(result,indent=2))
103+
104+
def parse():
105+
p=argparse.ArgumentParser(); p.add_argument('--features',type=Path,required=True); p.add_argument('--labels',type=Path,required=True); p.add_argument('--transcripts',type=Path,required=True); p.add_argument('--out',default='v78_seismic_semantic.json'); p.add_argument('--model',default='sentence-transformers/all-MiniLM-L6-v2'); p.add_argument('--batch',type=int,default=128); p.add_argument('--limit',type=int,default=0); return p.parse_args()
106+
if __name__=='__main__': run(parse())

0 commit comments

Comments
 (0)