Implicitly padded FFT convolution / stage2_bench.py

Failed on benchmark

Raw ⬇ ZIP
  1import os, sys, json, time
  2import numpy as np
  3import torch
  4import torch.nn as nn
  5import torch.nn.functional as F
  6sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  7from bench import train_model, evaluate, sweep_baseline, make_report
  8
  9# Local custom track: convolution is the relevant mathematical structure.
 10META = {'name':'long_1d_convolution', 'domain':'cnn',
 11        'description':'Synthetic long 1D signal classification requiring learned local convolution.'}
 12
 13def get_dataset(seed, n_train=400, n_test=200):
 14    rng=np.random.RandomState(seed)
 15    def make(n):
 16        x=rng.normal(0,0.35,(n,1,64)).astype('float32')
 17        y=rng.randint(0,2,n).astype('int64')
 18        # Class-dependent local motifs at random positions, with nuisance low frequency signal.
 19        for i in range(n):
 20            pos=rng.randint(4,59)
 21            motif=np.array([1.,1.,-1.,-1.,1.,1.],dtype='float32') if y[i] else np.array([-1.,1.,1.,-1.,-1.,1.],dtype='float32')
 22            x[i,0,pos:pos+6] += 0.9*motif
 23            x[i,0] += (0.15*np.sin(np.arange(64)*rng.uniform(.08,.22)+rng.uniform(0,6.28))).astype('float32')
 24        return x,y
 25    xtr,ytr=make(n_train); xte,yte=make(n_test)
 26    return {'xtr':torch.tensor(xtr), 'ytr':torch.tensor(ytr),
 27            'xte':torch.tensor(xte), 'yte':torch.tensor(yte),
 28            'task':'classification','metric':'err','input_shape':(1,64),'out_dim':2,
 29            'track':'long_1d_convolution','meta':META,'custom':True}
 30
 31def implicit_dft(x, M, m):
 32    # Positive-sign DFT, with zero injection only in bounded tiles (no length-M input).
 33    L=x.shape[-1]; p=(L+m-1)//m; q=M//m
 34    tile=F.pad(x, (0,p*m-L)).reshape(*x.shape[:-1],p,m).to(torch.float32).to(torch.complex64)
 35    r=torch.arange(q,device=x.device)[:,None]; t=torch.arange(p,device=x.device)[None,:]
 36    s=torch.arange(m,device=x.device)[None,:]
 37    zq=torch.exp(2j*torch.pi*(r*t)/q); zqm=torch.exp(2j*torch.pi*(r*s)/(q*m))
 38    U=torch.einsum('rt,...ts->...rs',zq,tile)*zqm
 39    ell=torch.arange(m,device=x.device)[:,None]
 40    zm=torch.exp(2j*torch.pi*(ell*s)/m)
 41    out=torch.einsum('ls,...rs->...rl',zm,U)
 42    return out.transpose(-2,-1).reshape(*x.shape[:-1],M)
 43
 44def fft_conv(x, w, implicit=False, m=8):
 45    # Cross-correlation, matching Conv1d(no padding): convolution with reversed kernel.
 46    L=x.shape[-1]; K=w.shape[-1]; M=1
 47    while M < L+K-1: M*=2
 48    wr=w.flip(-1)
 49    if implicit:
 50        X=implicit_dft(x,M,m); W=implicit_dft(wr,M,m)
 51    else:
 52        X=torch.fft.fft(F.pad(x,(0,M-L)).to(torch.complex64))
 53        W=torch.fft.fft(F.pad(wr,(0,M-K)).to(torch.complex64))
 54    full=torch.fft.ifft(X.unsqueeze(1)*W.unsqueeze(0), n=M).real
 55    return full[...,K-1:K-1+L-K+1]
 56
 57class FFTConv1d(nn.Module):
 58    def __init__(self, cin, cout, k, implicit=False, m=8):
 59        super().__init__(); self.weight=nn.Parameter(torch.empty(cout,cin,k)); self.bias=nn.Parameter(torch.zeros(cout))
 60        nn.init.kaiming_uniform_(self.weight,a=np.sqrt(5)); self.implicit=implicit; self.m=m
 61    def forward(self,x):
 62        # x B,C,L; einsum gives B,O,Lout and sums input channels.
 63        ys=[]
 64        for o in range(self.weight.shape[0]):
 65            yo=0
 66            for c in range(self.weight.shape[1]): yo=yo+fft_conv(x[:,c,:],self.weight[o:o+1,c,:],self.implicit,self.m)
 67            ys.append(yo)
 68        return torch.cat(ys,dim=1)+self.bias[None,:,None]
 69
 70class LongCNN(nn.Module):
 71    def __init__(self, implicit=False, m=8):
 72        super().__init__(); self.conv=FFTConv1d(1,8,15,implicit,m); self.fc=nn.Linear(8,2)
 73    def forward(self,x): return self.fc(F.relu(self.conv(x)).mean(-1))
 74
 75def run_one(seed, lr, implicit, m=8, epochs=12, return_model=False):
 76    torch.manual_seed(seed); np.random.seed(seed)
 77    d=get_dataset(seed)
 78    net=LongCNN(implicit,m)
 79    net,metric,_=train_model(net,d,epochs=epochs,lr=lr,batch=128)
 80    return (metric,net,d) if return_model else metric
 81
 82def main():
 83    # Shared union: every idea learning rate is also evaluated by baseline.
 84    lrs=[0.001,0.003,0.01]
 85    grid=[{'lr':v,'m':8} for v in lrs]
 86    base=sweep_baseline(lambda cfg: (lambda seed: run_one(seed,cfg['lr'],False,8)), grid)
 87    idea=evaluate(lambda seed: run_one(seed,base['best_cfg']['lr'],True,8))
 88    # Two nearby settings, same shared LR grid; select best idea configuration on 4 sweep seeds.
 89    idea_sweep=[]
 90    for cfg in grid:
 91        r=evaluate(lambda seed,cfg=cfg: run_one(seed,cfg['lr'],True,cfg['m']), seeds=(0,1,2,3))
 92        idea_sweep.append({'cfg':cfg,'mean':r['mean']})
 93    best=min(idea_sweep,key=lambda z:z['mean'])['cfg']
 94    idea=evaluate(lambda seed: run_one(seed,best['lr'],True,best['m']))
 95    # Trained-model behavioral signature: compare each operator's prediction to direct torch convolution.
 96    _,ib,d=run_one(0,best['lr'],True,best['m'],return_model=True)
 97    _,bb,_=run_one(0,base['best_cfg']['lr'],False,8,return_model=True)
 98    ib.eval(); bb.eval(); x=d['xte'][:16].to(next(ib.parameters()).device)
 99    with torch.no_grad():
100        wi=ib.conv.weight; xb=x
101        direct=[]
102        for o in range(8):
103            yo=0
104            for c in range(wi.shape[1]): yo=yo+fft_conv(xb[:,c,:],wi[o:o+1,c,:],False,8)
105            direct.append(yo)
106        direct=torch.cat(direct,1)+ib.conv.bias[None,:,None]; observed=float((ib.conv(xb)-direct).norm()/direct.norm())
107        # same trained-system output comparison, not an analytic-only check
108        system_gap=float((ib(xb)-bb(xb)).norm()/bb(xb).norm())
109    sig={'prediction':'implicit convolution equals explicit convolution; tile storage ratio M/(ceil(L/m)*m)',
110         'predicted_relative_operator_error':0.0,'observed_relative_operator_error':observed,
111         'predicted_storage_ratio':128/( ((64+8-1)//8)*8 ), 'trained_system_output_gap':system_gap,
112         'confirmed': bool(observed < 1e-5)}
113    rep=make_report('long_1d_convolution','cnn_small',base,idea,{'mechanism_signature':sig,'custom_track':{'name':'long_1d_convolution','file':'stage2_bench.py','domain':'cnn'}})
114    rep['idea_sweep']=idea_sweep; rep['selection_note']='Baseline and idea share lr union [0.001, 0.003, 0.01]; idea m fixed a priori at 8.'
115    with open('bench_report.json','w') as f: json.dump(rep,f,indent=2)
116    print(json.dumps(rep,indent=2))
117if __name__=='__main__': main()