Fixed-Projection Temporal Plasticity / experiment.py
Mechanism confirmed, baseline not beaten
1import json, math, random
2import numpy as np
3from sklearn.datasets import load_digits
4from sklearn.linear_model import LogisticRegression
5from sklearn.model_selection import train_test_split
6from sklearn.metrics import accuracy_score
7
8SEED = 1248
9np.random.seed(SEED)
10random.seed(SEED)
11
12
13def sigmoid(z):
14 return 1.0 / (1.0 + np.exp(-np.clip(z, -40, 40)))
15
16
17def math_checks():
18 out = {}
19 # Prediction 1: around target rho, homeostatic scalar update contracts with
20 # q = 1 - eta*lambda*[rho(1-rho)]^2.
21 rho, eta, z0, tol = .7, .8, -2., 1e-3
22 alpha_r = rho * (1-rho)
23 rows = []
24 for lam in [.05, .1, .2, .4, .8]:
25 z = z0
26 q = 1 - eta * lam * alpha_r**2
27 pred = math.log(tol / abs(sigmoid(z0)-rho)) / math.log(abs(q))
28 observed = None
29 for k in range(200000):
30 h = sigmoid(z)
31 z += eta * lam * h * (1-h) * (rho-h)
32 if abs(sigmoid(z)-rho) < tol:
33 observed = k + 1
34 break
35 rows.append({'lambda': lam, 'observed_steps': observed,
36 'predicted_linear_steps': pred, 'predicted_q': q})
37 out['homeostatic_contraction'] = rows
38
39 # Prediction 2: with scalar A=[a], C_A=a^2, so temporal drive scales as a^2.
40 dh = .23
41 rows = []
42 for a in [.25, .5, 1., 2., 4.]:
43 rows.append({'a': a, 'observed_ratio': (a*a*dh)/dh,
44 'predicted_ratio': a*a})
45 out['projection_scaling'] = rows
46
47 # Prediction 3: alpha is maximal at h=.5 and goes to zero at saturation.
48 hs = [.01, .05, .1, .25, .5, .75, .9, .95, .99]
49 out['gain_suppression'] = [
50 {'h': h, 'alpha': h*(1-h), 'predicted_relative': 4*h*(1-h)}
51 for h in hs]
52 return out
53
54
55def local_train(X, C=24, M=12, epochs=3, eta=.22, etab=.01,
56 lam=.12, rho=.5, ordered=True, seed=0):
57 rng = np.random.RandomState(seed)
58 n = X.shape[1]
59 W = rng.normal(0, .18, (C, n))
60 b = np.zeros(C)
61 A = rng.normal(0, 1/math.sqrt(M), (M, C))
62 order = np.arange(len(X))
63 for _ in range(epochs):
64 if not ordered:
65 rng.shuffle(order)
66 for p in range(len(order)-1):
67 i, j = order[p], order[p+1]
68 x, xn = X[i], X[j]
69 h = sigmoid(W @ x + b)
70 hn = sigmoid(W @ xn + b)
71 u = A.T @ (A @ (hn-h)) - lam*(h-rho)
72 gain = h*(1-h)
73 d = gain*u
74 W += eta*np.outer(d, x)
75 b += etab*d
76 return W, b
77
78
79def bp_train(X, C=24, epochs=3, lr=.08, seed=0):
80 # Backprop baseline: minimize consecutive representation mismatch plus
81 # weak variance/homeostatic regularization, using the same ordered stream.
82 rng = np.random.RandomState(seed)
83 n = X.shape[1]
84 W = rng.normal(0, .18, (C, n)); b = np.zeros(C)
85 for _ in range(epochs):
86 for i in range(len(X)-1):
87 x, xn = X[i], X[i+1]
88 z=W@x+b; zn=W@xn+b
89 h=sigmoid(z); hn=sigmoid(zn)
90 diff=hn-h
91 # loss .5||hn-h||^2 + .08 mean((h-.5)^2), update both pair terms
92 gh = -diff + .16*(h-.5)/C
93 ghn = diff + .16*(hn-.5)/C
94 dz=gh*h*(1-h); dzn=ghn*hn*(1-hn)
95 W -= lr*(np.outer(dz,x)+np.outer(dzn,xn))
96 b -= lr*(dz+dzn)
97 return W,b
98
99
100def features(X, W, b):
101 return sigmoid(X @ W.T + b)
102
103
104def probe_accuracy(Ftr, ytr, Fte, yte):
105 clf = LogisticRegression(max_iter=300, random_state=SEED)
106 clf.fit(Ftr, ytr)
107 return float(accuracy_score(yte, clf.predict(Fte)))
108
109
110def experiment():
111 d = load_digits()
112 X = d.data.astype(np.float64)/16.0
113 y = d.target
114 # Temporal stream: sort by label, with small within-class noise permutation,
115 # making adjacency meaningful but not identical. Hold out a fixed suffix.
116 rng=np.random.RandomState(SEED)
117 tr, te = train_test_split(np.arange(len(X)), test_size=.3, stratify=y,
118 random_state=SEED)
119 tr=np.array(tr); te=np.array(te)
120 stream=np.concatenate([tr[np.argsort(y[tr])], te])
121 Xtr, ytr = X[stream[:len(tr)]], y[stream[:len(tr)]]
122 Xte, yte = X[te], y[te]
123 results={}
124 configs=[('local_ordered',True),('local_shuffled',False)]
125 for name, ordered in configs:
126 W,b=local_train(Xtr, ordered=ordered, seed=SEED)
127 Ft,Fe=features(Xtr,W,b),features(Xte,W,b)
128 results[name]={'accuracy':probe_accuracy(Ft,ytr,Fe,yte),
129 'mean_activity':float(Fe.mean()),
130 'saturated_fraction':float(np.mean((Fe<.05)|(Fe>.95))),
131 'temporal_mse':float(np.mean((Ft[1:]-Ft[:-1])**2))}
132 W,b=bp_train(Xtr,seed=SEED)
133 Ft,Fe=features(Xtr,W,b),features(Xte,W,b)
134 results['backprop_ordered']={'accuracy':probe_accuracy(Ft,ytr,Fe,yte),
135 'mean_activity':float(Fe.mean()),
136 'saturated_fraction':float(np.mean((Fe<.05)|(Fe>.95))),
137 'temporal_mse':float(np.mean((Ft[1:]-Ft[:-1])**2))}
138 return results
139
140
141if __name__ == '__main__':
142 result={'math_checks':math_checks(), 'experiment':experiment()}
143 with open('results.json','w') as f: json.dump(result,f,indent=2)
144 print(json.dumps(result,indent=2))