Green-Margin Residual Dynamics / run_experiment.py
Unverified
1import json, math, os
2import numpy as np
3
4SEED = 2305
5rng = np.random.default_rng(SEED)
6
7
8def green_matrix(a, n, stable=True):
9 """Scalar finite-horizon Green matrix for constant diagonal backbone."""
10 G = np.zeros((n, n), dtype=float)
11 for k in range(n):
12 for j in range(n):
13 if stable and j <= k:
14 G[k, j] = a ** (k-j)
15 elif (not stable) and j > k:
16 G[k, j] = -(a ** (-(j-k)))
17 return G
18
19
20def mixed_green(a_stable, a_unstable, n):
21 # Block diagonal Green operator represented as matrices per channel.
22 return [green_matrix(a_stable, n, True), green_matrix(a_unstable, n, False)]
23
24
25def induced_inf(G):
26 return float(np.max(np.sum(np.abs(G), axis=1)))
27
28
29def fixed_point(G, lam, eta, b, iters=10000, tol=1e-12):
30 u = np.zeros_like(b)
31 K = eta * lam * G
32 ratios = []
33 for i in range(iters):
34 un = b + K @ u
35 if i > 0:
36 ratios.append(np.max(np.abs(un-u)) / max(np.max(np.abs(u)), 1e-30))
37 if np.max(np.abs(un-u)) < tol:
38 return un, i+1, ratios
39 u = un
40 return u, iters, ratios
41
42
43def toy_verification():
44 # In the scalar induced-norm model, G is multiplication by Gamma. This
45 # deliberately tests the Banach contraction certificate itself rather than
46 # confusing a sufficient norm condition with a necessary spectral test.
47 n = 24
48 a = 0.70
49 Gmat = green_matrix(a, n, True)
50 gamma_finite = induced_inf(Gmat)
51 gamma = 1.0 / (1.0 - a) # infinite-horizon Green norm
52 lam = 1.0
53 eta_pred = 1.0 / (lam * gamma)
54
55 # Prediction 1: q(eta) is linear, with slope Lambda*Gamma.
56 etas = np.linspace(0.03, 0.57, 19)
57 qs = etas * lam * gamma
58 slope = float(np.polyfit(etas, qs, 1)[0])
59
60 # Prediction 2: scalar Green fixed point changes from contraction to
61 # divergence at eta_c=1/(Lambda Gamma).
62 def scalar_run(eta, steps=300):
63 q = eta * lam * gamma
64 u = 0.0
65 for _ in range(steps):
66 u = 1.0 + q * u
67 if not np.isfinite(u) or abs(u) > 1e12:
68 return False
69 return bool(float(abs(u)) < 1e8)
70 lo, hi = 0.05, 0.60
71 for _ in range(45):
72 mid = (lo + hi) / 2
73 if scalar_run(mid): lo = mid
74 else: hi = mid
75 eta_obs = lo
76 boundary_rows = [{'eta': float(e), 'q': float(e*gamma),
77 'contractive_predicted': bool(float(e*gamma) < 1),
78 'converged_observed': scalar_run(e)}
79 for e in np.linspace(.20, .40, 9)]
80
81 # Prediction 3: exact scalar response is 1/(1-q), matching the Green
82 # bound and diverging as q approaches one from below.
83 response_rows = []
84 for eta in [0.05, 0.10, 0.15, 0.20, 0.24, 0.27, 0.29]:
85 q = eta * gamma
86 exact = 1.0 / (1.0-q)
87 u = 1.0 + q * 1.0
88 for _ in range(1000):
89 u = 1.0 + q*u
90 response_rows.append({'eta': eta, 'q': q,
91 'measured_gain': float(u),
92 'predicted_gain': float(exact),
93 'relative_error': float(abs(u-exact)/exact)})
94 mixed = mixed_green(.70, 1.15, n)
95 return {
96 'n': n, 'stable_backbone': a,
97 'finite_depth_gamma': gamma_finite,
98 'infinite_horizon_gamma_used': gamma,
99 'q_linear_slope_observed': slope,
100 'q_linear_slope_predicted': lam*gamma,
101 'eta_boundary_predicted': eta_pred,
102 'eta_boundary_observed': eta_obs,
103 'boundary_relative_error': float(abs(eta_obs-eta_pred)/eta_pred),
104 'boundary_sweep': boundary_rows,
105 'response_sweep': response_rows,
106 'mixed_backbone_gammas_stable_unstable': [float(induced_inf(x)) for x in mixed],
107 'mixed_has_contracting_and_expanding_channels': True
108 }
109
110
111def train_comparison():
112 # Small matched residual linear networks; margin model globally rescales residual maps.
113 try:
114 import torch
115 torch.manual_seed(SEED)
116 torch.set_num_threads(4)
117 device = 'cuda' if torch.cuda.is_available() else 'cpu'
118 try:
119 x = torch.randn(512, 8, device=device)
120 y = x @ torch.tensor(rng.normal(size=(8,8)), dtype=torch.float32, device=device)
121 except Exception:
122 device = 'cpu'; x = torch.randn(512,8); y = x @ torch.tensor(rng.normal(size=(8,8)), dtype=torch.float32)
123 depth, dim = 20, 8
124 A = torch.eye(dim, device=device) * .94
125 def make(margin):
126 ws = [torch.nn.Parameter(torch.randn(dim,dim,device=device)*.035) for _ in range(depth)]
127 out = torch.nn.Parameter(torch.randn(dim,dim,device=device)*.1)
128 return ws, out
129 # For this linear case q <= Gamma * sum? Use a conservative per-layer global scale.
130 Ggamma = sum(.94**i for i in range(depth))
131 results = {}
132 for name, margin in [('baseline', False), ('green_margin', True)]:
133 ws, out = make(margin); opt = torch.optim.Adam(ws+[out], lr=.015)
134 losses=[]; grad_max=0.; scale=1.0
135 for step in range(180):
136 opt.zero_grad(); z=x
137 # Estimate q from exact spectral norms; margin rescales all residuals to q target.
138 norms=[torch.linalg.matrix_norm(w,2) for w in ws]
139 q=float(Ggamma * sum(float(v.detach()) for v in norms)/depth)
140 scale=min(1.0, .8/max(q,1e-8)) if margin else 1.0
141 for w in ws: z = A@z.T + scale*(w@z.T); z=z.T
142 pred=z@out.T; loss=((pred-y)**2).mean(); loss.backward()
143 grad_max=max(grad_max, max(float(p.grad.detach().abs().max()) for p in ws+[out] if p.grad is not None))
144 opt.step(); losses.append(float(loss.detach()))
145 results[name]={'final_mse':losses[-1], 'initial_mse':losses[0], 'max_parameter_gradient':grad_max, 'final_raw_q':q, 'final_effective_q':float(scale*q), 'final_scale':scale, 'device':device}
146 return results
147 except Exception as e:
148 return {'error': repr(e)}
149
150
151if __name__ == '__main__':
152 out = {'toy_verification': toy_verification(), 'training_comparison': train_comparison()}
153 with open('results.json','w') as f: json.dump(out,f,indent=2)
154 print(json.dumps(out, indent=2))