Partial-ReNoise Neural Architecture Mutation / bench_partial_renoise.py
Mechanism confirmed, baseline not beaten
1import sys, json, random
2from pathlib import Path
3import numpy as np
4import torch
5import torch.nn as nn
6import torch.nn.functional as F
7sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
8from bench import get_dataset, make_model, train_model, evaluate, sweep_baseline, make_report
9
10SEEDS = tuple(range(8))
11LRS = [1e-3, 3e-3, 6e-3]
12GAMMAS = [0.05, 0.15, 0.30]
13EPOCHS = 2
14NTRAIN, NTEST = 400, 100
15_DATA = {}
16
17
18def alpha_bar(gamma, T=20):
19 t = int(round(float(gamma) * T))
20 return float(np.prod(1.0 - np.linspace(.01, .30, T)[:t])) if t else 1.0
21
22
23class PartialReNoiseCNN(nn.Module):
24 """A trained CNN system with an anchored partial architecture kernel.
25
26 The valid parent uses 3x3 convolutions. Each mutation is a shape-compatible
27 1x1 convolution. Per forward pass, each layer retains its parent operation
28 with alpha_bar(gamma), otherwise uses the mutated operation.
29 """
30 def __init__(self, out_dim, gamma):
31 super().__init__()
32 self.gamma = float(gamma)
33 self.a = alpha_bar(gamma)
34 self.parent = nn.ModuleList([
35 nn.Conv2d(3, 32, 3, padding=1),
36 nn.Conv2d(32, 64, 3, padding=1),
37 nn.Conv2d(64, 96, 3, padding=1)])
38 self.mutant = nn.ModuleList([
39 nn.Conv2d(3, 32, 1), nn.Conv2d(32, 64, 1), nn.Conv2d(64, 96, 1)])
40 self.fc1 = nn.Linear(96 * 4 * 4, 128)
41 self.fc2 = nn.Linear(128, out_dim)
42 self.last_mask = None
43
44 def forward(self, x, force_parent=False):
45 masks = []
46 for i, (p, m) in enumerate(zip(self.parent, self.mutant)):
47 use_parent = force_parent or (not self.training) or (torch.rand((), device=x.device) < self.a)
48 masks.append(float(use_parent))
49 x = p(x) if use_parent else m(x)
50 x = F.relu(x)
51 x = F.max_pool2d(x, 2)
52 self.last_mask = masks
53 x = x.flatten(1)
54 return self.fc2(F.relu(self.fc1(x)))
55
56
57def seed_all(seed):
58 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
59 if torch.cuda.is_available():
60 try: torch.cuda.manual_seed_all(seed)
61 except Exception: pass
62
63
64def train_mutated(ds, gamma, lr, epochs=EPOCHS):
65 seed_all(int(ds['_seed']))
66 net = PartialReNoiseCNN(int(ds['out_dim']), gamma)
67 device = 'cuda' if torch.cuda.is_available() else 'cpu'
68 try:
69 net.to(device)
70 opt = torch.optim.Adam(net.parameters(), lr=lr)
71 x, y = ds['xtr'].to(device), ds['ytr'].to(device)
72 bs = min(128, len(x)); net.train()
73 for _ in range(epochs):
74 order = torch.randperm(len(x), device=device)
75 for j in range(0, len(x), bs):
76 ix = order[j:j+bs]; opt.zero_grad(set_to_none=True)
77 loss = F.cross_entropy(net(x[ix]), y[ix]); loss.backward(); opt.step()
78 net.eval()
79 with torch.no_grad():
80 pred = net(ds['xte'].to(device)).argmax(1)
81 metric = float((pred != ds['yte'].to(device)).float().mean().cpu())
82 return metric, net
83 except Exception:
84 net.to('cpu'); opt = torch.optim.Adam(net.parameters(), lr=lr)
85 x, y = ds['xtr'], ds['ytr']; bs=min(128,len(x)); net.train()
86 for _ in range(epochs):
87 order=torch.randperm(len(x))
88 for j in range(0,len(x),bs):
89 ix=order[j:j+bs]; opt.zero_grad(set_to_none=True)
90 loss=F.cross_entropy(net(x[ix]),y[ix]); loss.backward(); opt.step()
91 net.eval()
92 with torch.no_grad():
93 metric=float((net(ds['xte']).argmax(1)!=ds['yte']).float().mean())
94 return metric, net
95
96
97def baseline_one(seed, lr):
98 seed_all(seed); ds=_DATA.setdefault(int(seed), get_dataset('vision', seed, NTRAIN, NTEST)); ds=dict(ds); ds['_seed']=seed
99 _, metric, _ = train_model(make_model('cnn_small', ds['input_shape'], ds['out_dim']), ds,
100 epochs=EPOCHS, lr=lr, batch=128, log=lambda *_: None)
101 return float(metric)
102
103
104def idea_one(seed, gamma, lr):
105 ds=_DATA.setdefault(int(seed), get_dataset('vision', seed, NTRAIN, NTEST)); ds=dict(ds); ds['_seed']=seed
106 return train_mutated(ds, gamma, lr)[0]
107
108
109def main():
110 # Baseline sweep uses exactly the union of all idea learning rates.
111 base = sweep_baseline(lambda cfg: (lambda s: baseline_one(s, cfg['lr'])),
112 [{'lr': lr} for lr in LRS], seeds=SEEDS)
113 # Explicitly evaluate the idea at best baseline lr and two nearby settings.
114 best_lr = float(base['best_cfg']['lr'])
115 idea_lrs = LRS
116 candidates=[]
117 for gamma in GAMMAS:
118 for lr in idea_lrs:
119 vals=[idea_one(s,gamma,lr) for s in SEEDS]
120 candidates.append({'gamma':gamma,'lr':lr,'mean':float(np.mean(vals)),'std':float(np.std(vals)),
121 'per_seed':vals,'n':len(vals)})
122 best=min(candidates,key=lambda z:z['mean'])
123 idea_res={k:best[k] for k in ('mean','std','per_seed','n')}
124 idea_res['config']={'gamma':best['gamma'],'lr':best['lr'],'baseline_best_lr':best_lr}
125 # Re-test the trained behavior: operation retention is measured from masks.
126 sig=[]
127 for g in [0.05,0.15,0.30,1.0]:
128 ds=get_dataset('vision', 0, 128, 64); ds['_seed']=0
129 _, net=train_mutated(ds,g,best['lr'],epochs=1)
130 net.train(); _=net(ds['xtr'][:64])
131 observed=float(np.mean(net.last_mask))
132 sig.append({'gamma':g,'predicted_retention':alpha_bar(g),'observed_retention':observed,
133 'absolute_error':abs(observed-alpha_bar(g))})
134 monotonic=all(sig[i]['predicted_retention'] >= sig[i+1]['predicted_retention'] for i in range(len(sig)-1))
135 signature={'prediction':'parent-operation retention decreases monotonically with gamma according to alpha_bar',
136 'trained_model_measurements':sig,'confirmed':bool(monotonic and max(x['absolute_error'] for x in sig)<=0.20)}
137 report=make_report('vision','cnn_small',base,idea_res,extra=signature)
138 report['protocol_notes']={'structural_match':'vision CNN architecture mutation',
139 'paired_seeds':list(SEEDS),'baseline_grid':LRS,'idea_grid':LRS,'epochs':EPOCHS,
140 'metric':'classification error, lower is better'}
141 Path('bench_report.json').write_text(json.dumps(report,indent=2))
142 print(json.dumps(report,indent=2))
143
144if __name__=='__main__': main()