Geometrically Attracting Random Recurrent Layer / experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json, math, os
  2import numpy as np
  3
  4SEED = 2803
  5rng = np.random.default_rng(SEED)
  6
  7
  8def linear_path(gains, choices):
  9    d = 1.0
 10    out = [d]
 11    for a, i in zip(gains, choices):
 12        d *= a[i]
 13        out.append(d)
 14    return np.asarray(out)
 15
 16
 17def mixed_boundary_sweep():
 18    # Two scalar candidate maps: one contractive and one expansive.
 19    lo, hi = 0.60, 1.40
 20    ps = np.linspace(0.0, 1.0, 21)
 21    n = 20000
 22    horizon = 30
 23    rows = []
 24    for p in ps:
 25        # candidate 1 (hi) is selected with probability p
 26        choices = rng.random((n, horizon)) < p
 27        # Vectorized path distances; this is also a direct Monte Carlo estimate.
 28        logs = np.where(choices, math.log(hi), math.log(lo))
 29        logd = np.cumsum(logs, axis=1)
 30        # log of E[d_t], stabilized, equals t*log((1-p)lo+p*hi) theoretically.
 31        t = np.arange(1, horizon + 1)
 32        log_mean_d = np.array([np.logaddexp.reduce(logd[:, j]) - math.log(n) for j in range(horizon)])
 33        slope = np.polyfit(t[-15:], log_mean_d[-15:], 1)[0]
 34        predicted = math.log((1-p)*lo + p*hi)
 35        rows.append({"p_hi": float(p), "predicted_log_exponent": predicted,
 36                     "observed_log_exponent": float(slope),
 37                     "predicted_mean_gain": (1-p)*lo+p*hi})
 38    # The expected-product boundary is mean gain = 1.
 39    predicted_p = (1-lo)/(hi-lo)
 40    observed_p = min(rows, key=lambda r: abs(r["observed_log_exponent"]))["p_hi"]
 41    return rows, predicted_p, observed_p
 42
 43
 44def expected_product_check():
 45    # Time-inhomogeneous independent routing: E product is product of expected gains.
 46    probs = np.array([.15, .75, .35, .60, .20, .80, .45, .55])
 47    gains = np.array([[.55, 1.35], [.70, 1.20], [.50, 1.50], [.80, 1.10],
 48                      [.60, 1.30], [.75, 1.25], [.55, 1.40], [.65, 1.15]])
 49    n = 300000
 50    choices = rng.random((n, len(probs))) < probs[None, :]
 51    paths = np.where(choices, gains[:, 1], gains[:, 0])
 52    empirical = np.mean(np.prod(paths, axis=1))
 53    predicted = float(np.prod((1-probs)*gains[:, 0] + probs*gains[:, 1]))
 54    relerr = abs(empirical-predicted)/predicted
 55    # Also verify the finite-horizon geometric rate for a stationary contractive mix.
 56    m = .94
 57    horizon = 12
 58    stationary_choices = rng.random((n, horizon)) < .4
 59    stationary_paths = np.where(stationary_choices, 1.3, .7)
 60    empirical_h = np.mean(np.prod(stationary_paths, axis=1))
 61    predicted_h = .94**horizon
 62    return {"predicted_product": predicted, "empirical_product": float(empirical),
 63            "relative_error": float(relerr), "stationary_horizon": horizon,
 64            "predicted_geometric_product": predicted_h,
 65            "empirical_geometric_product": float(empirical_h)}
 66
 67
 68def varying_schedule_check():
 69    # A deterministic time-varying schedule tests the product of p_t-weighted gains.
 70    p = np.array([.1,.7,.2,.8,.3,.6,.4,.5,.25,.75])
 71    lo = np.array([.55,.65,.60,.70,.58,.62,.56,.68,.61,.59])
 72    hi = np.array([1.20,1.10,1.30,1.05,1.25,1.15,1.18,1.08,1.22,1.12])
 73    means = (1-p)*lo+p*hi
 74    predicted = np.cumprod(means)
 75    n = 250000
 76    choices = rng.random((n, len(p))) < p
 77    products = np.cumprod(np.where(choices, hi, lo), axis=1)
 78    observed = np.mean(products, axis=0)
 79    relerr = np.max(np.abs(observed-predicted)/(predicted+1e-12))
 80    return {"predicted_final_product": float(predicted[-1]),
 81            "observed_final_product": float(observed[-1]),
 82            "max_relative_error_all_times": float(relerr),
 83            "mean_log_gain": float(np.mean(np.log(means)))}
 84
 85
 86def recurrent_stability_comparison():
 87    # Same nonlinear tanh setup: unconstrained baseline versus random candidate maps.
 88    # Distances are measured on paired trajectories with identical inputs and routing.
 89    local = np.random.default_rng(SEED + 1)
 90    dim, T, trials = 24, 200, 300
 91    x = local.normal(size=(T, dim)).astype(np.float32)
 92    def orthogonal(scale):
 93        q, _ = np.linalg.qr(local.normal(size=(dim, dim)))
 94        return (scale*q).astype(np.float32)
 95    W_base = orthogonal(1.18)
 96    W_lo, W_hi = orthogonal(.68), orthogonal(1.28)
 97    U = (local.normal(size=(dim, dim))*0.18).astype(np.float32)
 98    p = .35
 99    base_slopes, idea_slopes = [], []
100    for _ in range(trials):
101        h = local.normal(size=dim).astype(np.float32); hp = h.copy(); hp[0] += 1
102        hi = local.normal(size=dim).astype(np.float32); hip = hi.copy(); hip[0] += 1
103        db, di = [], []
104        for t in range(T):
105            h, hp = np.tanh(W_base@h + U@x[t]), np.tanh(W_base@hp + U@x[t])
106            choose = local.random() < p
107            W = W_hi if choose else W_lo
108            hi, hip = np.tanh(W@hi + U@x[t]), np.tanh(W@hip + U@x[t])
109            db.append(np.linalg.norm(h-hp)+1e-12); di.append(np.linalg.norm(hi-hip)+1e-12)
110        tt = np.arange(T)
111        base_slopes.append(np.polyfit(tt[-80:], np.log(db[-80:]), 1)[0])
112        idea_slopes.append(np.polyfit(tt[-80:], np.log(di[-80:]), 1)[0])
113    expected_gain = (1-p)*.68+p*1.28
114    return {"baseline_predicted_upper_log_rate": math.log(1.18),
115            "baseline_observed_mean_log_distance_slope": float(np.mean(base_slopes)),
116            "idea_predicted_log_mean_gain": math.log(expected_gain),
117            "idea_observed_mean_log_distance_slope": float(np.mean(idea_slopes)),
118            "idea_mean_gain": expected_gain,
119            "idea_fraction_decaying": float(np.mean(np.asarray(idea_slopes)<0)),
120            "baseline_fraction_decaying": float(np.mean(np.asarray(base_slopes)<0))}
121
122
123def main():
124    product = expected_product_check()
125    schedule = varying_schedule_check()
126    boundary_rows, predicted_p, observed_p = mixed_boundary_sweep()
127    stability = recurrent_stability_comparison()
128    # Boundary tolerance is one grid step; product checks use a 2% MC tolerance.
129    product_ok = product["relative_error"] < .02 and schedule["max_relative_error_all_times"] < .02
130    boundary_ok = abs(predicted_p-observed_p) <= .06
131    # Nonlinear hidden states should show the predicted ordering and contraction signal.
132    stability_ok = (stability["idea_observed_mean_log_distance_slope"] < 0 and
133                    stability["baseline_observed_mean_log_distance_slope"] > stability["idea_observed_mean_log_distance_slope"])
134    result = {"seed": SEED, "expected_product_check": product,
135              "time_varying_schedule_check": schedule,
136              "boundary": {"predicted_p_hi": predicted_p, "observed_grid_p_hi": observed_p,
137                           "rows": boundary_rows},
138              "nonlinear_stability": stability,
139              "predictions_confirmed": {"expected_product": product_ok,
140                                        "mixed_gain_boundary": boundary_ok,
141                                        "nonlinear_attraction": stability_ok},
142              "worked": bool(product_ok and boundary_ok and stability_ok)}
143    print(json.dumps(result, indent=2))
144
145if __name__ == "__main__":
146    main()