Residual-Redundancy Adapter Clustering / experiment.py
Mechanism confirmed, baseline not beaten
1import json
2import math
3import numpy as np
4
5
6def tc_gaussian(cov, eps=1e-10):
7 cov = np.asarray(cov, float)
8 d = np.diag(cov)
9 sign, ld = np.linalg.slogdet(cov + eps * np.eye(len(d)))
10 if sign <= 0:
11 return float("nan")
12 return float(0.5 * (np.log(d + eps).sum() - ld))
13
14
15def shrink_cov(x, alpha=0.0):
16 s = np.cov(x, rowvar=False, ddof=1)
17 return (1-alpha) * s + alpha * np.diag(np.diag(s))
18
19
20def merge_score(cov, a, b, eps=1e-10):
21 ab = list(a) + list(b)
22 return tc_gaussian(cov[np.ix_(ab, ab)], eps) - tc_gaussian(cov[np.ix_(list(a), list(a))], eps) - tc_gaussian(cov[np.ix_(list(b), list(b))], eps)
23
24
25def agglomerative(cov, k, eps=1e-10):
26 groups = [[i] for i in range(cov.shape[0])]
27 while len(groups) > k:
28 best = None
29 for i in range(len(groups)):
30 for j in range(i+1, len(groups)):
31 val = merge_score(cov, groups[i], groups[j], eps)
32 if best is None or val > best[0]:
33 best = (val, i, j)
34 _, i, j = best
35 groups[i] = groups[i] + groups[j]
36 del groups[j]
37 return sorted([sorted(g) for g in groups])
38
39
40def equicorr(d, rho):
41 return (1-rho)*np.eye(d) + rho*np.ones((d,d))
42
43
44def exact_equicorr_tc(d, rho):
45 # det(Sigma)=(1-rho)^(d-1)*(1+(d-1)rho), unit variances.
46 return -0.5*((d-1)*math.log(1-rho) + math.log(1+(d-1)*rho))
47
48
49
50def fit_shared_lowrank(x, y, q=2, ridge=1e-3, iters=80):
51 # Shared adapter basis U with task-specific heads V: Y ~= X U V.
52 n, p = x.shape; t = y.shape[1]
53 U = np.random.default_rng(777).normal(size=(p, q)) * .1
54 V = np.random.default_rng(778).normal(size=(q, t)) * .1
55 for _ in range(iters):
56 xu = x @ U
57 V = np.linalg.solve(xu.T @ xu + ridge*np.eye(q), xu.T @ y)
58 xv = y @ V.T
59 U = np.linalg.solve(x.T @ x + ridge*np.eye(p), x.T @ xv)
60 return U, V
61
62
63def predict_shared_lowrank(x, model):
64 U, V = model
65 return x @ U @ V
66
67def fit_cluster_models(xtr, ytr, groups, ridge=1e-3):
68 # Frozen base is zero; each routed adapter is a linear weight vector.
69 p = xtr.shape[1]
70 ws = []
71 for g in groups:
72 yy = ytr[:, g].mean(axis=1)
73 a = xtr.T @ xtr + ridge*np.eye(p)
74 w = np.linalg.solve(a, xtr.T @ yy)
75 ws.append(w)
76 return ws
77
78
79def predict_cluster(x, ws, groups):
80 out = np.zeros((len(x), sum(len(g) for g in groups)))
81 # groups are contiguous in this experiment after canonical sorting.
82 for w, g in zip(ws, groups):
83 out[:, g] = x @ w[:, None]
84 return out
85
86
87def regression_demo(rng):
88 t, p, ntr, nv = 6, 12, 700, 1000
89 true_groups = [[0,1], [2,3], [4,5]]
90 xtr, xv = rng.normal(size=(ntr,p)), rng.normal(size=(nv,p))
91 base_w = rng.normal(size=(3,p))
92 w = np.zeros((t,p))
93 for j,g in enumerate(true_groups):
94 for task in g: w[task] = base_w[j] + .10*rng.normal(size=p)
95 # Correlated task noise makes residual dependence operationally visible.
96 noise_cov = np.eye(t)*.35
97 for g in true_groups:
98 for i in g:
99 for j in g:
100 if i != j: noise_cov[i,j] = .28
101 etr = rng.multivariate_normal(np.zeros(t), noise_cov, ntr)
102 ev = rng.multivariate_normal(np.zeros(t), noise_cov, nv)
103 ytr, yv = xtr@w.T + etr, xv@w.T + ev
104 # Warm-up predictor used only to collect held-out residuals.
105 warm_model = fit_shared_lowrank(xtr, ytr, q=2)
106 warm = predict_shared_lowrank(xv, warm_model)
107 residual = yv - warm
108 cov = shrink_cov(residual, alpha=.10)
109 discovered = agglomerative(cov, 3)
110 random_groups = [[0,2], [1,5], [3,4]]
111 results = {}
112 # Equal nominal budget: rank-2 shared factor has p*q + q*t = 36 parameters;
113 # each of three rank-1 routed adapters has 3*p = 36 parameters.
114 shared_model = fit_shared_lowrank(xtr, ytr, q=2)
115 results['shared'] = {'mse': float(np.mean((predict_shared_lowrank(xv, shared_model)-yv)**2)),
116 'groups': [list(range(t))], 'params': p*2+2*t}
117 for name, groups in [('random', random_groups), ('residual_tc', discovered), ('oracle', true_groups)]:
118 ws = fit_cluster_models(xtr, ytr, groups)
119 pred = predict_cluster(xv, ws, groups)
120 results[name] = {'mse': float(np.mean((pred-yv)**2)), 'groups': groups, 'params': len(groups)*p}
121 return results
122
123
124def main():
125 rng = np.random.default_rng(1015)
126 # Prediction 1: exact TC rises according to closed form with rho.
127 d = 4
128 rhos = [0.0, .1, .2, .4, .6, .8]
129 tc_curve = []
130 for rho in rhos:
131 x = rng.multivariate_normal(np.zeros(d), equicorr(d,rho), 30000)
132 observed = tc_gaussian(np.cov(x, rowvar=False))
133 predicted = exact_equicorr_tc(d,rho)
134 tc_curve.append({'rho':rho, 'predicted':predicted, 'observed':observed, 'abs_error':abs(observed-predicted)})
135 # Prediction 2: near independence TC is quadratic in rho, not linear.
136 small = []
137 for rho in [.02,.04,.08,.12,.16]:
138 pred = exact_equicorr_tc(d,rho)
139 quadratic = d*(d-1)/4 * rho*rho
140 small.append({'rho':rho, 'observed_exact':pred, 'quadratic_prediction':quadratic, 'ratio':pred/quadratic})
141 # Prediction 3: pairwise Gaussian TC is -1/2 log(1-rho^2), and
142 # block clustering should recover the population pairs once signal exceeds sampling noise.
143 pair_tc = []
144 for rho in [.0,.2,.4,.6,.8]:
145 x = rng.multivariate_normal(np.zeros(2), equicorr(2,rho), 12000)
146 pair_tc.append({'rho': rho, 'predicted': -0.5*math.log(max(1-rho*rho, 1e-12)),
147 'observed': tc_gaussian(np.cov(x, rowvar=False))})
148 recovery = []
149 for rho in [.0,.2,.4,.6,.8]:
150 cov = np.eye(6)
151 for g in [[0,1],[2,3],[4,5]]:
152 i,j=g; cov[i,j]=cov[j,i]=rho
153 x = rng.multivariate_normal(np.zeros(6), cov, 5000)
154 est = shrink_cov(x, .05)
155 groups = agglomerative(est, 3)
156 recovery.append({'rho':rho, 'groups':groups, 'exact_recovery':groups==[[0,1],[2,3],[4,5]], 'within_tc':float(np.mean([tc_gaussian(est[np.ix_(g,g)]) for g in [[0,1],[2,3],[4,5]]]))})
157 regression = regression_demo(rng)
158 output = {'tc_rho_sweep':tc_curve, 'small_rho_quadratic_sweep':small, 'pair_tc_sweep': pair_tc, 'block_clustering_sweep':recovery, 'regression_demo':regression}
159 with open('results.json','w') as f: json.dump(output,f,indent=2)
160 print(json.dumps(output, indent=2))
161
162if __name__ == '__main__': main()