summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--RESULTS.md18
-rw-r--r--ROADMAP.md6
-rw-r--r--THEORY.md222
-rwxr-xr-xexperiments/finalize_claims.sh2
-rw-r--r--experiments/verify_theory.py157
5 files changed, 405 insertions, 0 deletions
diff --git a/RESULTS.md b/RESULTS.md
index 0650f04..f7873a1 100644
--- a/RESULTS.md
+++ b/RESULTS.md
@@ -429,6 +429,23 @@ the frozen 90% d4 threshold; **no prefix is eligible and task seeds 6--8 remain
teaching directions are often strongly aligned even in mediocre runs, pointing next to regression
gain/tracking over the moving trajectory rather than simply adding vectorizer capacity.
+## Theory package and executable predictions
+
+`THEORY.md` proves the exact linearized variance of simultaneous Rademacher perturbation,
+including its cross-layer term; bounds centered finite-sigma bias; gives a smooth-loss descent
+condition that separates angle, gain, and curvature; proves the neutral-period innovation/SNR
+result and task-fit absorption failure; and tabulates query/work/memory assumptions across the
+main comparison set.
+
+The deterministic numerical verifier covers every frozen C5 axis. Across depth 2/4/8, width 8/32,
+and K1/4/16, empirical perturbation MSE is within 1% of the exact prediction in the reported cases.
+The finite-difference bias has log-log slope `2.000000` in sigma. A quadratic example changes from
+descent to ascent on opposite sides of the predicted learning-rate threshold. Neutral predictor
+residual power matches `(1-eta_P)^(2t)` across three rates and four update counts. Finally,
+neutral residualization raises the teaching squared cosine from `0.7492` to `0.9656`, whereas
+task-period fitting lowers it to `0.5395` by removing soma-correlated teaching. **The frozen C5
+theory package and numerical checks pass.**
+
## Paper-faithful non-backprop baselines
`PETITE` in the initial request is treated as **PEPITA**. Implementations and hyperparameters were
@@ -496,6 +513,7 @@ canonical d2 comparison: `experiments/ep_depth2_sweep.sh`; near-parameter compar
`experiments/ep_matched_sweep.sh`.
C2 direct causal diagnosis: `experiments/c2_nodepert_validation.sh` followed by
`python experiments/analyze_c2_nodepert_validation.py`.
+Theory checks: `python experiments/verify_theory.py`.
Audited aggregate tables: `experiments/analyze_verified.py`. The claim-locked finalizer is
`experiments/finalize_claims.sh`; it rejects missing/dirty inputs, regenerates both main figures,
their captions, source-hash manifest, and `results/audited_tables.md`, then runs all smoke checks.
diff --git a/ROADMAP.md b/ROADMAP.md
index 70d1649..75efa60 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -250,6 +250,12 @@ The minimum theory package contains:
Numerical simulations must test the predicted scaling with depth, width, directions, perturbation
scale, and predictor timescale.
+**Status: passed on 2026-07-22.** `THEORY.md` gives the exact simultaneous-Rademacher MSE with its
+cross-layer term, the Gaussian layerwise comparison, an `O(sigma^2)` antithetic bias bound, the
+smooth-loss angle/gain/curvature descent condition, the neutral innovation/SNR theorem and
+task-fit absorption result, and a phase/query/work/memory table. `experiments/verify_theory.py`
+checks depth, width, K, sigma, and predictor timescale deterministically; all assertions pass.
+
## Protocol discipline
- Hyperparameters are selected on a validation split created only from the training set.
diff --git a/THEORY.md b/THEORY.md
new file mode 100644
index 0000000..25d27ef
--- /dev/null
+++ b/THEORY.md
@@ -0,0 +1,222 @@
+# SDIL theory and cost audit
+
+This note states the minimum claims that can be proved for the implemented
+estimators and innovation rule. It deliberately separates an unbiased neural
+teaching direction from a useful parameter update: the completed experiments
+show that a high hidden-state cosine can coexist with poor optimization.
+
+## 1. Simultaneous multi-layer perturbation
+
+Consider one example and collect the hidden-state gradients
+`g_l = dL/dh_l` for hidden blocks `l=1,...,H`, where `g_l` has dimension
+`d_l`. For one simultaneous Rademacher intervention, draw independent
+`xi_l in {-1,+1}^{d_l}` and linearize the centered loss difference as
+
+```text
+D = sum_j <g_j, xi_j>.
+```
+
+The gradient and descent estimators for layer `l` are
+
+```text
+g_hat_l = xi_l D, q_l = -g_hat_l.
+```
+
+Independence and `E[xi_l xi_l^T] = I` give
+
+```text
+E[g_hat_l] = g_l.
+```
+
+Because a Rademacher vector has deterministic squared norm `d_l`, the exact
+linearized mean-squared error is
+
+```text
+E ||g_hat_l - g_l||^2
+ = (d_l - 1) ||g_l||^2
+ + d_l sum_{j != l} ||g_j||^2.
+```
+
+Averaging `K` independent directions divides this expression by `K`. The first
+term is ordinary directional-estimator variance; the second is cross-layer
+interference. Thus simultaneous perturbation is unbiased but its relative
+variance grows with layer width and with the total gradient energy injected at
+other layers. This predicts why K1 can work in an easy/depth-flat regime yet
+fail to train a moving context vectorizer on a compositional task.
+
+For comparison, layerwise Rademacher perturbation removes the cross-layer term.
+The current layerwise implementation uses Gaussian directions, for which
+
+```text
+E ||g_hat_l - g_l||^2 = (d_l + 1) ||g_l||^2 / K.
+```
+
+Layerwise targets therefore buy variance reduction with depth-dependent replay
+cost; they are not a free scalable replacement for simultaneous calibration.
+
+### Finite perturbation bias
+
+Suppose the third derivative of the loss has operator norm at most `M` in the
+perturbation neighborhood, and let `D_total = sum_l d_l`. A centered antithetic
+difference has directional error at most
+
+```text
+M sigma^2 ||xi||^3 / 6.
+```
+
+For simultaneous Rademacher directions this gives the conservative layerwise
+bias bound
+
+```text
+||E[g_hat_l] - g_l||
+ <= (M sigma^2 / 6) sqrt(d_l) D_total^(3/2).
+```
+
+The important dependence is `O(sigma^2)`, with a dimension-dependent constant.
+Reducing K removes variance but not this bias; increasing width/depth without a
+sigma audit is therefore unsafe.
+
+## 2. What is sufficient for a descent step
+
+Flatten all forward parameters into `theta`, let `p = grad L(theta)`, and let
+`v` be the complete local update before multiplying by the learning rate:
+
+```text
+theta_next = theta + eta v.
+```
+
+If the loss is `beta`-smooth and
+
+```text
+c = <-p, v> / (||p|| ||v||) > 0,
+```
+
+then the descent lemma gives
+
+```text
+L(theta_next) - L(theta)
+ <= -eta c ||p|| ||v|| + (beta eta^2 / 2) ||v||^2.
+```
+
+Consequently the update is guaranteed to descend only when
+
+```text
+0 < eta < 2 c ||p|| / (beta ||v||).
+```
+
+This separates three quantities that a neural-state cosine conflates:
+
+1. direction (`c`);
+2. gain (`||v|| / ||p||`);
+3. curvature (`beta`).
+
+For a local layer, multiplying a hidden teaching vector by the activation gain
+and presynaptic activity maps it into parameter space. A positive
+`cos(r_l, -g_l)` does not by itself imply a positive cosine after aggregating
+examples, parameters, and layers, and even perfect parameter alignment can
+increase loss if its gain makes the step exceed the bound. This is why future
+diagnostics must include parameter-update cosine, norm ratio, and realized
+single-step loss change rather than reporting hidden cosine alone.
+
+## 3. Somato-dendritic innovation as conditional nuisance removal
+
+Write apical activity during a task period as
+
+```text
+a = s + M h + epsilon,
+```
+
+where `s` is the instructional component, `M h` is ordinary traffic predictable
+from soma, and `epsilon` is unpredictable noise. In neutral periods `s=0`.
+Assume `Sigma_h = E[h h^T]` is nonsingular and `E[epsilon h^T]=0`. Population
+least squares on neutral periods yields
+
+```text
+P_neutral = E[a h^T | neutral] Sigma_h^(-1) = M.
+```
+
+The task-period innovation is then
+
+```text
+r_neutral = a - P_neutral h = s + epsilon.
+```
+
+If `s`, `M h`, and `epsilon` are mutually uncorrelated, a squared-cosine/SNR
+proxy improves from
+
+```text
+raw: S / (S + T + E)
+innovation:S / (S + E),
+```
+
+where `S`, `T`, and `E` are their expected squared norms. The gain is strict
+whenever predictable traffic has nonzero power.
+
+Task-period fitting has a different estimand. Let
+
+```text
+C = E[s h^T] Sigma_h^(-1).
+```
+
+Then `P_task = M + C` and
+
+```text
+r_task = s - C h + epsilon.
+```
+
+It removes the soma-predictable projection of the teaching signal itself. This
+proves the identification role of neutral periods and also defines the failure
+boundary: residualization cannot remove contextual traffic that remains
+unpredictable from the chosen per-cell soma statistic, and it can erase credit
+when the predictor is fit on task periods.
+
+For the diagonal normalized-LMS predictor used in the code, an ideal noiseless
+cell with neutral relation `a=m h` follows
+
+```text
+p_(t+1) = p_t + eta_P (m - p_t),
+traffic residual power fraction = (1 - eta_P)^(2t).
+```
+
+The relevant timescale is therefore the product of learning rate and number of
+neutral updates. Too few neutral updates leave traffic; task-period updates
+converge to the wrong coefficient regardless of speed.
+
+## 4. Hardware-independent complexity
+
+Let `H` be the number of hidden layers, `K` the perturbation directions per
+event, `e` the number of ordinary minibatches between events, `B` the batch
+size, and `F_l` the affine work of layer `l`. Define `F=sum_l F_l` and suffix
+work `S_l=sum_{j>l} F_j`.
+
+| method | activity phases / passes | transport | scalar task-loss observations | extra affine work | main transient memory |
+|:--|:--|:--|:--|:--|:--|
+| BP | forward + reverse | exact transposed Jacobians | ordinary supervised loss | about one reverse pass | saved activations / graph |
+| FA | forward + reverse-like serial feedback | fixed random feedback | ordinary loss | `O(F)` feedback work | activations + feedback states |
+| DFA | forward + direct feedback | fixed random output maps | ordinary loss | `O(sum_l d_l d_out)` | activations + feedback states |
+| learned NP, simultaneous | ordinary forward; one duplicate clean plus `2K` noisy full forwards every `e` batches | learned direct maps; no weight transport | `2KB/e` per ordinary batch | `(1+2K)F/e` | up to `2KB` noisy examples in the vectorized implementation |
+| learned NP, layerwise | ordinary forward; clean baseline plus antithetic suffix replays | learned direct maps | `(1+2KH)B/e` | `(F + 2K sum_l S_l)/e`, quadratic in depth for balanced layers | serial suffix replay, no `2K` batch expansion |
+| direct NP | layerwise or simultaneous estimator every update | none beyond causal intervention | same formulas with `e=1` | same formulas with `e=1` | estimator dependent |
+| canonical EP | free relaxation then nudged relaxation | symmetric energy weights | label injected in nudged phase | `T_free + T_nudged` recurrent steps | persistent/free and nudged states |
+| Dual Prop `DP^T` VGG | one inference phase, 17 forward-only layer passes in the author CIFAR protocol | dyadic forward/top-down states | ordinary target loss | 17 layer passes before update | two states per dyadic unit |
+| BurstCCN | one burst/event-probability phase with plastic W/Y/Q pathways | separate plastic feedback pathways | ordinary supervised target | architecture-dependent local W/Y/Q updates | event and burst states plus feedback weights |
+
+The project ledger reports the exact realized loss observations,
+forward-equivalent examples, peak allocated memory, and wall time. Asymptotic
+labels never replace those measurements. In particular, the frozen direct-NP
+C2 diagnosis costs `68.4x` ordinary work, whereas the frozen CIFAR d20 K1/e4
+protocol uses 16x fewer logical queries, 11x less calibration work, and 5.3x
+less total work than K16/e4 while retaining its gain over DFA.
+
+## 5. Executable checks
+
+Run:
+
+```bash
+python experiments/verify_theory.py
+```
+
+The verifier checks the exact Rademacher MSE across depth, width, and K; the
+quadratic finite-sigma bias law; the smooth-descent step threshold; neutral
+predictor convergence across `eta_P` and update counts; and the task-fit
+teaching-signal absorption predicted above.
diff --git a/experiments/finalize_claims.sh b/experiments/finalize_claims.sh
index 104d87c..047e86c 100755
--- a/experiments/finalize_claims.sh
+++ b/experiments/finalize_claims.sh
@@ -40,7 +40,9 @@ echo "predeclared Pareto/scaling claim gates passed"
"$TRAIN_PY" experiments/baseline_smoke.py
"$TRAIN_PY" experiments/synthetic_smoke.py
+"$TRAIN_PY" experiments/protocol_smoke.py
"$TRAIN_PY" experiments/smoke.py
+"$TRAIN_PY" experiments/verify_theory.py
summary_tmp=$(mktemp /tmp/sdil_audited_tables.XXXXXX)
trap 'rm -f "$summary_tmp"' EXIT
diff --git a/experiments/verify_theory.py b/experiments/verify_theory.py
new file mode 100644
index 0000000..7ee87bc
--- /dev/null
+++ b/experiments/verify_theory.py
@@ -0,0 +1,157 @@
+"""Deterministic numerical checks for the claims in THEORY.md."""
+import itertools
+import math
+
+import numpy as np
+
+
+def simultaneous_mse(rng, depth, width, directions, trials=12000):
+ gradients = rng.normal(size=(depth, width))
+ gradients /= np.linalg.norm(gradients, axis=1, keepdims=True)
+ estimate = np.zeros((trials, width), dtype=np.float64)
+ for _ in range(directions):
+ xi = rng.integers(0, 2, size=(trials, depth, width), dtype=np.int8)
+ xi = 2.0 * xi - 1.0
+ directional = np.einsum("tdw,dw->t", xi, gradients)
+ estimate += xi[:, 0, :] * directional[:, None] / directions
+ empirical = np.square(estimate - gradients[0]).sum(axis=1).mean()
+ total_energy = np.square(gradients).sum()
+ theoretical = (width * total_energy - np.square(gradients[0]).sum()) / directions
+ return empirical, theoretical
+
+
+def check_simultaneous_variance():
+ print("SIMULTANEOUS RADEMACHER MSE")
+ print("depth width K empirical theory ratio")
+ cases = ((2, 8, 1), (4, 8, 1), (8, 8, 1),
+ (4, 32, 1), (4, 32, 4), (4, 32, 16))
+ for index, (depth, width, directions) in enumerate(cases):
+ empirical, theoretical = simultaneous_mse(
+ np.random.default_rng(100 + index), depth, width, directions)
+ ratio = empirical / theoretical
+ print(f"{depth:5d} {width:5d} {directions:2d} "
+ f"{empirical:9.4f} {theoretical:9.4f} {ratio:6.3f}")
+ assert abs(ratio - 1.0) < 0.06
+
+
+def cubic_loss(z, linear, cubic):
+ return linear @ z + cubic * np.power(z, 3).sum() / 6.0
+
+
+def check_sigma_bias():
+ # Exhaustive directions remove Monte Carlo error, exposing only the
+ # centered-difference bias. For this separable cubic it is exactly O(sigma^2).
+ dim = 8
+ z = np.linspace(-0.4, 0.5, dim)
+ linear = np.linspace(0.2, 0.9, dim)
+ cubic = 1.7
+ true_gradient = linear + 0.5 * cubic * np.square(z)
+ directions = np.asarray(list(itertools.product((-1.0, 1.0), repeat=dim)))
+ sigmas = np.asarray((0.005, 0.01, 0.02, 0.04))
+ biases = []
+ print("\nCENTERED FINITE-DIFFERENCE BIAS")
+ print("sigma bias_norm")
+ for sigma in sigmas:
+ estimates = []
+ for xi in directions:
+ coefficient = (cubic_loss(z + sigma * xi, linear, cubic)
+ - cubic_loss(z - sigma * xi, linear, cubic)) / (2.0 * sigma)
+ estimates.append(xi * coefficient)
+ bias = np.linalg.norm(np.mean(estimates, axis=0) - true_gradient)
+ biases.append(bias)
+ print(f"{sigma:5.3f} {bias:.9e}")
+ slope = np.polyfit(np.log(sigmas), np.log(biases), 1)[0]
+ print(f"log-log slope={slope:.6f}")
+ assert abs(slope - 2.0) < 1e-6
+
+
+def check_descent_threshold():
+ rng = np.random.default_rng(301)
+ dim = 20
+ gradient = rng.normal(size=dim)
+ orthogonal = rng.normal(size=dim)
+ orthogonal -= orthogonal.dot(gradient) * gradient / gradient.dot(gradient)
+ orthogonal /= np.linalg.norm(orthogonal)
+ neg_gradient = -gradient / np.linalg.norm(gradient)
+ cosine = 0.35
+ update = cosine * neg_gradient + math.sqrt(1.0 - cosine ** 2) * orthogonal
+ beta = 2.3
+ threshold = 2.0 * cosine * np.linalg.norm(gradient) / (beta * np.linalg.norm(update))
+
+ def quadratic_change(step):
+ return step * gradient.dot(update) + 0.5 * beta * step ** 2 * update.dot(update)
+
+ below = quadratic_change(0.9 * threshold)
+ above = quadratic_change(1.1 * threshold)
+ print("\nSMOOTH DESCENT THRESHOLD")
+ print(f"cos={cosine:.3f} eta_max={threshold:.6f} "
+ f"delta_below={below:+.6f} delta_above={above:+.6f}")
+ assert below < 0 < above
+
+
+def check_predictor_timescale():
+ rng = np.random.default_rng(401)
+ examples = 50000
+ cells = 12
+ h = rng.normal(size=(examples, cells))
+ slopes = np.exp(rng.normal(scale=0.25, size=cells))
+ traffic = h * slopes
+ print("\nNEUTRAL PREDICTOR TIMESCALE")
+ print("eta steps residual_power empirical theory")
+ for eta in (0.002, 0.01, 0.05):
+ for steps in (0, 20, 100, 500):
+ predictor = slopes * (1.0 - (1.0 - eta) ** steps)
+ residual = traffic - h * predictor
+ empirical = np.square(residual).mean() / np.square(traffic).mean()
+ theoretical = (1.0 - eta) ** (2 * steps)
+ print(f"{eta:5.3f} {steps:5d} {empirical:14.8f} {theoretical:14.8f}")
+ assert abs(empirical - theoretical) < 1e-12
+
+
+def squared_cosine(x, y):
+ return float((x.ravel() @ y.ravel()) ** 2
+ / ((x.ravel() @ x.ravel()) * (y.ravel() @ y.ravel())))
+
+
+def check_innovation_identification():
+ rng = np.random.default_rng(501)
+ examples = 100000
+ cells = 8
+ h = rng.normal(size=(examples, cells))
+ traffic_slopes = np.linspace(0.8, 1.5, cells)
+ teaching_slopes = np.linspace(0.3, 0.7, cells)
+ innovation = rng.normal(scale=0.6, size=(examples, cells))
+ noise = rng.normal(scale=0.15, size=(examples, cells))
+ teaching = h * teaching_slopes + innovation
+ traffic = h * traffic_slopes
+ apical = teaching + traffic + noise
+
+ neutral_residual = apical - traffic
+ task_coeff = (apical * h).mean(axis=0) / np.square(h).mean(axis=0)
+ task_residual = apical - h * task_coeff
+ raw_alignment = squared_cosine(apical, teaching)
+ neutral_alignment = squared_cosine(neutral_residual, teaching)
+ task_alignment = squared_cosine(task_residual, teaching)
+ predictable_fraction = np.square(h * teaching_slopes).mean() / np.square(teaching).mean()
+ retained_fraction = np.square(teaching - h * teaching_slopes).mean() / np.square(teaching).mean()
+ print("\nINNOVATION IDENTIFICATION")
+ print(f"squared cosine raw={raw_alignment:.4f} neutral={neutral_alignment:.4f} "
+ f"task_fit={task_alignment:.4f}")
+ print(f"teaching predictable={predictable_fraction:.4f} "
+ f"retained_after_task_fit={retained_fraction:.4f}")
+ assert neutral_alignment > raw_alignment + 0.15
+ assert task_alignment < neutral_alignment - 0.15
+ assert abs(retained_fraction - (1.0 - predictable_fraction)) < 0.01
+
+
+def main():
+ check_simultaneous_variance()
+ check_sigma_bias()
+ check_descent_threshold()
+ check_predictor_timescale()
+ check_innovation_identification()
+ print("\nALL THEORY CHECKS PASSED")
+
+
+if __name__ == "__main__":
+ main()