Spaces:
Runtime error
Runtime error
Upload opi_mathqa_full.py
Browse files- opi_mathqa_full.py +268 -0
opi_mathqa_full.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Full OPi-vs-GELU Math-QA experiment on math-ai/StackMathQA.
|
| 3 |
+
|
| 4 |
+
Design:
|
| 5 |
+
* StackMathQA 100K/200K/400K/800K/1600K
|
| 6 |
+
* question-level leakage-safe split
|
| 7 |
+
* SentencePiece BPE tokenizer trained on TRAIN ONLY
|
| 8 |
+
* identical encoder-decoder Transformer baseline vs OPi FFN
|
| 9 |
+
* OPi activation derived from k(x)=log|cos(pi*x/log(x))|
|
| 10 |
+
with a smooth squared-cosine form and a sign-preserving multiplicative gate
|
| 11 |
+
* teacher-forced validation loss/perplexity
|
| 12 |
+
* greedy generation on held-out questions
|
| 13 |
+
* token accuracy, normalized exact match, character F1, ROUGE-L
|
| 14 |
+
* numerical-answer extraction/equivalence when possible
|
| 15 |
+
* checkpointing, JSON report, CSV predictions, SHA-256/SHA-512
|
| 16 |
+
|
| 17 |
+
The full 100K file is 181 MB. StackMathQA also provides 200K, 400K, 800K and
|
| 18 |
+
1.6M configurations. The official dataset describes Q and A as LaTeX-encoded
|
| 19 |
+
math text and CC-BY-4.0.
|
| 20 |
+
"""
|
| 21 |
+
import argparse, csv, hashlib, json, math, os, random, re, time
|
| 22 |
+
from collections import Counter
|
| 23 |
+
from pathlib import Path
|
| 24 |
+
from urllib.request import Request, urlopen
|
| 25 |
+
|
| 26 |
+
import torch
|
| 27 |
+
import torch.nn as nn
|
| 28 |
+
import torch.nn.functional as F
|
| 29 |
+
import sentencepiece as spm
|
| 30 |
+
|
| 31 |
+
SEED=42
|
| 32 |
+
random.seed(SEED); torch.manual_seed(SEED)
|
| 33 |
+
|
| 34 |
+
CONFIGS={
|
| 35 |
+
'100k':'stackmathqa100k', '200k':'stackmathqa200k', '400k':'stackmathqa400k',
|
| 36 |
+
'800k':'stackmathqa800k', '1600k':'stackmathqa1600k'}
|
| 37 |
+
BASE='https://huggingface.co/datasets/math-ai/StackMathQA/resolve/main/data/{cfg}/all.jsonl'
|
| 38 |
+
EXPECTED={
|
| 39 |
+
'100k':'ddf7526cb21600dcf93991e400393eb9adc3549a6077d0cd0b0d37c45e1b037a'}
|
| 40 |
+
|
| 41 |
+
SPECIAL=['<pad>','<bos>','<eos>','<unk>']
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def set_seed(seed):
|
| 45 |
+
random.seed(seed); torch.manual_seed(seed); torch.cuda.manual_seed_all(seed)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def sha(path, alg='sha256'):
|
| 49 |
+
h=hashlib.new(alg)
|
| 50 |
+
with open(path,'rb') as f:
|
| 51 |
+
for b in iter(lambda:f.read(1024*1024),b''): h.update(b)
|
| 52 |
+
return h.hexdigest()
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def download(url,path):
|
| 56 |
+
path=Path(path); path.parent.mkdir(parents=True,exist_ok=True)
|
| 57 |
+
if path.exists(): return
|
| 58 |
+
req=Request(url,headers={'User-Agent':'OPi-MathQA/2.0'})
|
| 59 |
+
with urlopen(req,timeout=60) as r, open(path,'wb') as f:
|
| 60 |
+
total=int(r.headers.get('Content-Length') or 0); done=0; t=time.time()
|
| 61 |
+
while True:
|
| 62 |
+
b=r.read(4*1024*1024)
|
| 63 |
+
if not b: break
|
| 64 |
+
f.write(b); done+=len(b)
|
| 65 |
+
if total and time.time()-t>2:
|
| 66 |
+
print(f'\rdownload {done/1e6:.1f}/{total/1e6:.1f} MB',end='',flush=True); t=time.time()
|
| 67 |
+
print()
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def norm(s):
|
| 71 |
+
s=s.replace('\r',' ').replace('\n',' ')
|
| 72 |
+
s=re.sub(r'\s+',' ',s).strip().lower()
|
| 73 |
+
return s
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def canonical_question(q):
|
| 77 |
+
# Remove harmless whitespace differences while preserving LaTeX symbols.
|
| 78 |
+
return norm(q)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def load_rows(path,limit=0):
|
| 82 |
+
out=[]; seen=set(); questions=set()
|
| 83 |
+
with open(path,encoding='utf-8') as f:
|
| 84 |
+
for line in f:
|
| 85 |
+
if not line.strip(): continue
|
| 86 |
+
x=json.loads(line); q=(x.get('Q') or '').strip(); a=(x.get('A') or '').strip()
|
| 87 |
+
if not q or not a: continue
|
| 88 |
+
qk=canonical_question(q); k=(qk,norm(a))
|
| 89 |
+
if k in seen: continue
|
| 90 |
+
seen.add(k); out.append((q,a,x.get('meta',{})))
|
| 91 |
+
questions.add(qk)
|
| 92 |
+
if limit and len(out)>=limit: break
|
| 93 |
+
return out
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def split(rows, seed=SEED, test=.10, val=.10):
|
| 97 |
+
# All answers for the same question stay in one split.
|
| 98 |
+
out={'train':[],'val':[],'test':[]}
|
| 99 |
+
for row in rows:
|
| 100 |
+
qk=canonical_question(row[0])
|
| 101 |
+
h=int(hashlib.sha256((str(seed)+'|'+qk).encode()).hexdigest()[:12],16)/float(16**12-1)
|
| 102 |
+
out['test' if h<test else 'val' if h<test+val else 'train'].append(row)
|
| 103 |
+
return out
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def train_spm(train_rows, path, vocab_size=16000):
|
| 107 |
+
path=Path(path); path.parent.mkdir(parents=True,exist_ok=True)
|
| 108 |
+
if path.exists(): return
|
| 109 |
+
corpus=path.with_suffix('.txt')
|
| 110 |
+
with open(corpus,'w',encoding='utf-8') as f:
|
| 111 |
+
for q,a,_ in train_rows:
|
| 112 |
+
f.write(q.replace('\n',' ')+'\n'); f.write(a.replace('\n',' ')+'\n')
|
| 113 |
+
spm.SentencePieceTrainer.Train(
|
| 114 |
+
input=str(corpus), model_prefix=str(path.with_suffix('')),
|
| 115 |
+
vocab_size=vocab_size, model_type='bpe', character_coverage=1.0,
|
| 116 |
+
pad_id=0, bos_id=1, eos_id=2, unk_id=3,
|
| 117 |
+
user_defined_symbols=[], hard_vocab_limit=False)
|
| 118 |
+
corpus.unlink(missing_ok=True)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
class Tok:
|
| 122 |
+
def __init__(self, model):
|
| 123 |
+
self.sp=spm.SentencePieceProcessor(model_file=str(model))
|
| 124 |
+
self.pad=self.sp.pad_id(); self.bos=self.sp.bos_id(); self.eos=self.sp.eos_id(); self.unk=self.sp.unk_id()
|
| 125 |
+
def enc(self,s,maxlen):
|
| 126 |
+
ids=[self.bos]+self.sp.encode(s,out_type=int)[:maxlen-2]+[self.eos]
|
| 127 |
+
return ids+[self.pad]*(maxlen-len(ids))
|
| 128 |
+
def dec(self,ids):
|
| 129 |
+
ids=[i for i in ids if i not in (self.pad,self.bos)]
|
| 130 |
+
if self.eos in ids: ids=ids[:ids.index(self.eos)]
|
| 131 |
+
return self.sp.decode(ids)
|
| 132 |
+
def __len__(self): return self.sp.get_piece_size()
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def batch(rows,tok,maxq,maxa,device):
|
| 136 |
+
q=torch.tensor([tok.enc(r[0],maxq) for r in rows],dtype=torch.long,device=device)
|
| 137 |
+
a=torch.tensor([tok.enc(r[1],maxa) for r in rows],dtype=torch.long,device=device)
|
| 138 |
+
return q,a
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
class OPiActivation(nn.Module):
|
| 142 |
+
"""Sign-preserving activation derived from OPi's phase/kernel.
|
| 143 |
+
|
| 144 |
+
k(x)=log|cos(u(x))|, u(x)=pi*x/log(x).
|
| 145 |
+
We use 0.5*log(cos(u)^2+eps), a smooth numerical version, then use the
|
| 146 |
+
kernel as a multiplicative gate on z so the activation retains sign/information.
|
| 147 |
+
"""
|
| 148 |
+
def __init__(self, dim, alpha=.65, beta=.75, offset=1.5, eps=1e-5):
|
| 149 |
+
super().__init__(); self.register_buffer('alpha',torch.tensor(float(alpha))); self.register_buffer('beta',torch.tensor(float(beta))); self.offset=offset; self.eps=eps
|
| 150 |
+
def forward(self,z):
|
| 151 |
+
x=self.offset+F.softplus(z)
|
| 152 |
+
u=math.pi*x/torch.log(x)
|
| 153 |
+
k=.5*torch.log(torch.cos(u).square()+self.eps)
|
| 154 |
+
gate=1.0 + torch.clamp(self.beta, -0.95, 0.95)*torch.tanh(self.alpha*k)
|
| 155 |
+
return z*gate
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
class FFN(nn.Module):
|
| 159 |
+
def __init__(self,d,ff,opi):
|
| 160 |
+
super().__init__(); self.fc1=nn.Linear(d,ff); self.act=OPiActivation(ff) if opi else nn.GELU(); self.fc2=nn.Linear(ff,d)
|
| 161 |
+
def forward(self,x): return self.fc2(self.act(self.fc1(x)))
|
| 162 |
+
|
| 163 |
+
class EncBlock(nn.Module):
|
| 164 |
+
def __init__(self,d,heads,ff,drop,opi):
|
| 165 |
+
super().__init__(); self.att=nn.MultiheadAttention(d,heads,batch_first=True,dropout=drop); self.n1=nn.LayerNorm(d); self.ff=FFN(d,ff,opi); self.n2=nn.LayerNorm(d); self.drop=nn.Dropout(drop)
|
| 166 |
+
def forward(self,x,padmask):
|
| 167 |
+
y,_=self.att(x,x,x,key_padding_mask=padmask,need_weights=False); x=self.n1(x+self.drop(y)); return self.n2(x+self.drop(self.ff(x)))
|
| 168 |
+
|
| 169 |
+
class DecBlock(nn.Module):
|
| 170 |
+
def __init__(self,d,heads,ff,drop,opi):
|
| 171 |
+
super().__init__(); self.selfatt=nn.MultiheadAttention(d,heads,batch_first=True,dropout=drop); self.cross=nn.MultiheadAttention(d,heads,batch_first=True,dropout=drop); self.n1=nn.LayerNorm(d); self.n2=nn.LayerNorm(d); self.n3=nn.LayerNorm(d); self.ff=FFN(d,ff,opi); self.drop=nn.Dropout(drop)
|
| 172 |
+
def forward(self,x,mem,srcmask,tgtmask):
|
| 173 |
+
y,_=self.selfatt(x,x,x,attn_mask=tgtmask,need_weights=False); x=self.n1(x+self.drop(y))
|
| 174 |
+
y,_=self.cross(x,mem,mem,key_padding_mask=srcmask,need_weights=False); x=self.n2(x+self.drop(y)); return self.n3(x+self.drop(self.ff(x)))
|
| 175 |
+
|
| 176 |
+
class Model(nn.Module):
|
| 177 |
+
def __init__(self,vocab,d=256,heads=8,ff=512,layers=4,drop=.1,opi=False,maxlen=512):
|
| 178 |
+
super().__init__(); self.d=d; self.maxlen=maxlen; self.emb=nn.Embedding(vocab,d,padding_idx=0); self.pos=nn.Embedding(maxlen,d); self.enc=nn.ModuleList([EncBlock(d,heads,ff,drop,opi) for _ in range(layers)]); self.dec=nn.ModuleList([DecBlock(d,heads,ff,drop,opi) for _ in range(layers)]); self.out=nn.Linear(d,vocab)
|
| 179 |
+
def forward(self,src,tgt):
|
| 180 |
+
S=src.size(1); T=tgt.size(1); ps=torch.arange(S,device=src.device)[None,:]; pt=torch.arange(T,device=tgt.device)[None,:]
|
| 181 |
+
mem=self.emb(src)+self.pos(ps); srcmask=src.eq(0)
|
| 182 |
+
for b in self.enc: mem=b(mem,srcmask)
|
| 183 |
+
y=self.emb(tgt)+self.pos(pt); mask=torch.triu(torch.ones(T,T,device=tgt.device,dtype=torch.bool),1)
|
| 184 |
+
for b in self.dec: y=b(y,mem,srcmask,mask)
|
| 185 |
+
return self.out(y)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def lcs(a,b):
|
| 189 |
+
prev=[0]*(len(b)+1)
|
| 190 |
+
for x in a:
|
| 191 |
+
cur=[0]
|
| 192 |
+
for j,y in enumerate(b,1): cur.append(prev[j-1]+1 if x==y else max(prev[j],cur[-1]))
|
| 193 |
+
prev=cur
|
| 194 |
+
return prev[-1]
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def charf(ref,hyp):
|
| 198 |
+
rc=Counter(ref); hc=Counter(hyp); ov=sum((rc&hc).values())
|
| 199 |
+
if not ref or not hyp: return 0.0
|
| 200 |
+
p=ov/len(hyp); r=ov/len(ref); return 2*p*r/(p+r) if p+r else 0.0
|
| 201 |
+
|
| 202 |
+
NUM_RE=re.compile(r'(?<![A-Za-z])[-+]?\d+(?:\.\d+)?(?:e[-+]?\d+)?',re.I)
|
| 203 |
+
|
| 204 |
+
def nums(s):
|
| 205 |
+
return [round(float(x),10) for x in NUM_RE.findall(s)]
|
| 206 |
+
|
| 207 |
+
def metric_rows(refs,hyps):
|
| 208 |
+
tok_ok=tok_total=0; exact=0; cf=[]; rl=[]; numeq=[]
|
| 209 |
+
for r,h in zip(refs,hyps):
|
| 210 |
+
rt=norm(r).split(); ht=norm(h).split(); n=min(len(rt),len(ht)); tok_ok+=sum(rt[i]==ht[i] for i in range(n)); tok_total+=len(rt)
|
| 211 |
+
exact+=norm(r)==norm(h); cf.append(charf(norm(r),norm(h))); L=lcs(rt,ht); rl.append(L/max(1,len(rt)))
|
| 212 |
+
rn,hn=nums(r),nums(h); numeq.append(bool(rn) and rn==hn)
|
| 213 |
+
return {'token_accuracy':tok_ok/max(1,tok_total),'exact_match':exact/max(1,len(refs)),'char_f1':sum(cf)/max(1,len(cf)),'rougeL_recall':sum(rl)/max(1,len(rl)),'numeric_answer_match_when_both_numeric':sum(numeq)/max(1,len(numeq))}
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def generate(model,rows,tok,device,maxq,maxa,bs=4):
|
| 217 |
+
model.eval(); refs=[]; hyps=[]
|
| 218 |
+
with torch.no_grad():
|
| 219 |
+
for i in range(0,len(rows),bs):
|
| 220 |
+
chunk=rows[i:i+bs]; src=torch.tensor([tok.enc(r[0],maxq) for r in chunk],dtype=torch.long,device=device); B=src.size(0); out=torch.full((B,1),tok.bos,dtype=torch.long,device=device)
|
| 221 |
+
for _ in range(maxa-1):
|
| 222 |
+
logits=model(src,out); nxt=logits[:,-1].argmax(-1); out=torch.cat([out,nxt[:,None]],1)
|
| 223 |
+
if bool((nxt==tok.eos).all()): break
|
| 224 |
+
for j,r in enumerate(chunk): refs.append(r[1]); hyps.append(tok.dec(out[j].tolist()))
|
| 225 |
+
return refs,hyps
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def eval_loss(model,rows,tok,device,maxq,maxa,bs):
|
| 229 |
+
model.eval(); lossf=nn.CrossEntropyLoss(ignore_index=tok.pad); total=n=0
|
| 230 |
+
with torch.no_grad():
|
| 231 |
+
for i in range(0,len(rows),bs):
|
| 232 |
+
q,a=batch(rows[i:i+bs],tok,maxq,maxa,device); z=model(q,a[:,:-1]); loss=lossf(z.reshape(-1,z.size(-1)),a[:,1:].reshape(-1)); total+=float(loss)*q.size(0); n+=q.size(0)
|
| 233 |
+
return total/max(1,n)
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def train(model,train_rows,val_rows,tok,args,device):
|
| 237 |
+
opt=torch.optim.AdamW(model.parameters(),lr=args.lr,weight_decay=.01); lossf=nn.CrossEntropyLoss(ignore_index=tok.pad); best=float('inf'); best_state=None; history=[]
|
| 238 |
+
for ep in range(1,args.epochs+1):
|
| 239 |
+
model.train(); random.shuffle(train_rows); total=n=0; t=time.time()
|
| 240 |
+
for i in range(0,len(train_rows),args.batch):
|
| 241 |
+
q,a=batch(train_rows[i:i+args.batch],tok,args.max_q,args.max_a,device); z=model(q,a[:,:-1]); loss=lossf(z.reshape(-1,z.size(-1)),a[:,1:].reshape(-1)); opt.zero_grad(set_to_none=True); loss.backward(); nn.utils.clip_grad_norm_(model.parameters(),1.0); opt.step(); total+=float(loss)*q.size(0); n+=q.size(0)
|
| 242 |
+
vl=eval_loss(model,val_rows[:args.val_eval],tok,device,args.max_q,args.max_a,args.eval_batch); rec={'epoch':ep,'train_loss':total/max(1,n),'val_loss':vl,'val_ppl':math.exp(min(vl,20)),'seconds':time.time()-t}; history.append(rec); print(json.dumps(rec),flush=True)
|
| 243 |
+
if vl<best: best=vl; best_state={k:v.detach().cpu().clone() for k,v in model.state_dict().items()};
|
| 244 |
+
if args.patience and ep-history.index(min(history,key=lambda x:x['val_loss']))+1>=args.patience and ep>args.patience: break
|
| 245 |
+
if best_state: model.load_state_dict(best_state)
|
| 246 |
+
return history
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def main():
|
| 250 |
+
ap=argparse.ArgumentParser(); ap.add_argument('--config',choices=CONFIGS,default='100k'); ap.add_argument('--data',default=''); ap.add_argument('--download',action='store_true'); ap.add_argument('--rows',type=int,default=0); ap.add_argument('--epochs',type=int,default=3); ap.add_argument('--batch',type=int,default=16); ap.add_argument('--eval-batch',type=int,default=8); ap.add_argument('--val-eval',type=int,default=2000); ap.add_argument('--test-eval',type=int,default=2000); ap.add_argument('--max-q',type=int,default=384); ap.add_argument('--max-a',type=int,default=512); ap.add_argument('--vocab',type=int,default=16000); ap.add_argument('--d',type=int,default=256); ap.add_argument('--heads',type=int,default=8); ap.add_argument('--ff',type=int,default=512); ap.add_argument('--layers',type=int,default=4); ap.add_argument('--lr',type=float,default=2e-4); ap.add_argument('--patience',type=int,default=2); ap.add_argument('--device',default='cpu'); ap.add_argument('--out',default='opi_mathqa_report.json'); ap.add_argument('--predictions',default='opi_mathqa_predictions.csv'); ap.add_argument('--workdir',default='opi_mathqa_work'); ap.add_argument('--seed',type=int,default=42)
|
| 251 |
+
args=ap.parse_args(); set_seed(args.seed)
|
| 252 |
+
work=Path(args.workdir); work.mkdir(parents=True,exist_ok=True); path=Path(args.data or work/f'{CONFIGS[args.config]}.jsonl')
|
| 253 |
+
if args.download: download(BASE.format(cfg=CONFIGS[args.config]),path)
|
| 254 |
+
if not path.exists(): raise SystemExit(f'Dataset missing: {path}. Use --download on a network-enabled machine.')
|
| 255 |
+
got=sha(path); print('dataset_sha256',got);
|
| 256 |
+
if args.config in EXPECTED and not args.data: print('expected_sha256',EXPECTED[args.config],'match=',got==EXPECTED[args.config])
|
| 257 |
+
rows=load_rows(path,args.rows); splits=split(rows,args.seed); print('rows',len(rows),'splits',{k:len(v) for k,v in splits.items()})
|
| 258 |
+
spm_path=work/'tokenizer.model'; train_spm(splits['train'],spm_path,args.vocab); tok=Tok(spm_path); print('vocab',len(tok))
|
| 259 |
+
device=torch.device(args.device); report={'dataset':{'name':'math-ai/StackMathQA','config':args.config,'rows':len(rows),'splits':{k:len(v) for k,v in splits.items()},'sha256':got,'sha512':sha(path,'sha512')},'model':vars(args),'results':{}}
|
| 260 |
+
for name,opi in [('baseline_gelu',False),('opi',True)]:
|
| 261 |
+
set_seed(args.seed); model=Model(len(tok),args.d,args.heads,args.ff,args.layers,.1,opi,max(args.max_q,args.max_a)).to(device); params=sum(p.numel() for p in model.parameters()); print(name,'params',params)
|
| 262 |
+
hist=train(model,splits['train'],splits['val'],tok,args,device); refs,hyps=generate(model,splits['test'][:args.test_eval],tok,device,args.max_q,args.max_a,args.eval_batch); m=metric_rows(refs,hyps); m['test_loss']=eval_loss(model,splits['test'][:args.test_eval],tok,device,args.max_q,args.max_a,args.eval_batch); m['ppl']=math.exp(min(m['test_loss'],20)); m['params']=params; m['history']=hist; report['results'][name]=m
|
| 263 |
+
with open(args.predictions.replace('.csv',f'_{name}.csv'),'w',newline='',encoding='utf-8') as f:
|
| 264 |
+
w=csv.writer(f); w.writerow(['question','reference','prediction']); w.writerows(zip([r[0] for r in splits['test'][:args.test_eval]],refs,hyps))
|
| 265 |
+
torch.save({'state_dict':model.state_dict(),'tokenizer':str(spm_path),'args':vars(args),'metrics':m},work/f'{name}.pt')
|
| 266 |
+
Path(args.out).write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf-8'); print(json.dumps(report['results'],indent=2))
|
| 267 |
+
|
| 268 |
+
if __name__=='__main__': main()
|