Event-driven shared-neuron graph / event_graph_experiment.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import heapq
  2import json
  3import time
  4import numpy as np
  5from sklearn.datasets import load_digits
  6from sklearn.model_selection import train_test_split
  7from sklearn.linear_model import LogisticRegression
  8from sklearn.metrics import accuracy_score
  9
 10SEED = 7
 11
 12class EventGraph:
 13    def __init__(self, n_source, n_hidden, n_out, source_edges, hidden_edges, vmax=100):
 14        self.S, self.H, self.O = n_source, n_hidden, n_out
 15        self.source_edges = source_edges
 16        self.hidden_edges = hidden_edges
 17        self.vmax = vmax
 18
 19    def run(self, x, return_trace=False):
 20        a = np.zeros(self.H, dtype=float)
 21        visits = np.zeros(self.H, dtype=int)
 22        z = np.zeros(self.O, dtype=float)
 23        q, serial = [], 0
 24        for i, value in enumerate(x):
 25            heapq.heappush(q, (0, serial, i, float(value)))
 26            serial += 1
 27        events = 0
 28        trace = []
 29        while q and events < 100000:
 30            t, _, node, value = heapq.heappop(q)
 31            events += 1
 32            if node < self.S:
 33                for si, h, w, d in self.source_edges:
 34                    if si == node:
 35                        heapq.heappush(q, (t + d, serial, self.S + h, w * value))
 36                        serial += 1
 37                continue
 38            h = node - self.S
 39            if h >= self.H:
 40                z[h - self.H] += value
 41                continue
 42            if visits[h] >= self.vmax:
 43                continue
 44            a[h] += value
 45            visits[h] += 1
 46            y = np.tanh(a[h])
 47            trace.append((t, h, a[h], y, visits[h]))
 48            for hh, out, w, d in self.hidden_edges:
 49                if hh == h:
 50                    heapq.heappush(q, (t + d, serial, self.S + self.H + out, w * y))
 51                    serial += 1
 52        result = dict(z=z, accum=a, visits=visits, events=events, trace=trace)
 53        return result if return_trace else z
 54
 55
 56def prediction_sweeps():
 57    rng = np.random.default_rng(SEED)
 58    rows = []
 59    # Prediction 1: with one shared node and linear response, final accumulator is sum of arrivals.
 60    # For k equal arrivals c, a = k*c exactly; measured slope should be 1.
 61    ks = np.arange(1, 9)
 62    measured = []
 63    for k in ks:
 64        edges = [(i, 0, 1.0, int(i % 2)) for i in range(k)]
 65        g = EventGraph(k, 1, 1, edges, [(0, 0, 1.0, 0)])
 66        measured.append(g.run(np.full(k, 0.1), True)['accum'][0])
 67    slope = float(np.polyfit(ks, measured, 1)[0] / 0.1)
 68    rows.append({'prediction': 'shared accumulator slope versus number of equal arrivals = 1/cancel-free sum',
 69                  'predicted': 1.0, 'observed': slope, 'relative_error': abs(slope-1.0)})
 70
 71    # Prediction 2: tanh response saturates; equal arrivals k*c approach 1, with
 72    # inverse-tanh(0.9)/c predicted for the 0.9 crossing.
 73    c = 0.2
 74    crossing = None
 75    ys = []
 76    for k in range(1, 31):
 77        edges = [(i, 0, 1.0, 0) for i in range(k)]
 78        g = EventGraph(k, 1, 1, edges, [(0, 0, 1.0, 0)])
 79        y = g.run(np.full(k, c), True)['trace'][-1][3]
 80        ys.append(y)
 81        if crossing is None and y >= .9:
 82            crossing = k
 83    predicted_crossing = int(np.ceil(np.arctanh(.9) / c))
 84    rows.append({'prediction': 'shared tanh response reaches 0.9 at k=ceil(atanh(0.9)/c)',
 85                  'predicted': predicted_crossing, 'observed': crossing,
 86                  'relative_error': abs(crossing-predicted_crossing)/predicted_crossing})
 87
 88    # Prediction 3: visit cap Vmax truncates exactly after Vmax hidden visits.
 89    cap_rows = []
 90    k = 8
 91    for cap in range(1, 6):
 92        edges = [(i, 0, 1.0, 0) for i in range(k)]
 93        g = EventGraph(k, 1, 1, edges, [(0, 0, 1.0, 0)], vmax=cap)
 94        r = g.run(np.ones(k), True)
 95        cap_rows.append((cap, int(r['visits'][0]), float(r['accum'][0])))
 96    rows.append({'prediction': 'visit cap produces exactly min(k,Vmax) accepted updates',
 97                 'predicted': [min(k, c) for c in range(1, 6)],
 98                 'observed': [x[1] for x in cap_rows],
 99                 'relative_error': 0.0 if all(x[1] == min(k, x[0]) for x in cap_rows) else 1.0})
100    return rows
101
102
103def make_features(X, shared=True, P=None, Q=None, seed=SEED):
104    rng = np.random.default_rng(seed)
105    n, d = X.shape
106    S, H, O = 16, 24, 10
107    if P is None:
108        P = rng.normal(0, 1/np.sqrt(d), (d, S))
109    if Q is None:
110        Q = rng.normal(0, 1/np.sqrt(H), (H, O))
111    out = []
112    event_counts = []
113    for x in X:
114        src = x @ P
115        if shared:
116            edges = [(i, int(i % 8), 0.55, i % 3) for i in range(S)]
117        else:
118            edges = [(i, i, 0.55, 0) for i in range(S)]
119        hidden_edges = [(h, o, Q[h, o], 0) for h in range(H) for o in range(O)]
120        g = EventGraph(S, H, O, edges, hidden_edges, vmax=20)
121        r = g.run(src, return_trace=True)
122        out.append(r['z'])
123        event_counts.append(r['events'])
124    return np.asarray(out), float(np.mean(event_counts))
125
126
127def classification_test():
128    digits = load_digits()
129    X = digits.data.astype(float) / 16.0
130    y = digits.target
131    Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=.25, random_state=SEED, stratify=y)
132    rng = np.random.default_rng(SEED)
133    P = rng.normal(0, 1/np.sqrt(X.shape[1]), (X.shape[1], 16))
134    Q = rng.normal(0, 1/np.sqrt(24), (24, 10))
135    result = {}
136    for shared in (False, True):
137        t0 = time.perf_counter()
138        Ftr, etr = make_features(Xtr, shared=shared, P=P, Q=Q)
139        Fte, ete = make_features(Xte, shared=shared, P=P, Q=Q)
140        clf = LogisticRegression(max_iter=300, C=1.0, random_state=SEED)
141        clf.fit(Ftr, ytr)
142        pred = clf.predict(Fte)
143        result['shared' if shared else 'no_sharing'] = {
144            'accuracy': float(accuracy_score(yte, pred)),
145            'seconds': time.perf_counter() - t0,
146            'train_mean_events': etr, 'test_mean_events': ete,
147            'feature_dim': int(Ftr.shape[1])
148        }
149    return result
150
151
152def main():
153    report = {'seed': SEED, 'mechanism_checks': prediction_sweeps(), 'classification': classification_test()}
154    with open('results.json', 'w') as f:
155        json.dump(report, f, indent=2)
156    print(json.dumps(report, indent=2))
157
158if __name__ == '__main__':
159    main()