summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--BASELINES.md5
-rw-r--r--NOVELTY.md5
-rw-r--r--README.md7
-rw-r--r--THEORY.md22
-rw-r--r--experiments/conv_local_smoke.py39
-rw-r--r--experiments/conv_run.py39
-rw-r--r--sdil/conv.py73
7 files changed, 181 insertions, 9 deletions
diff --git a/BASELINES.md b/BASELINES.md
index 7cc6c10..52fd12c 100644
--- a/BASELINES.md
+++ b/BASELINES.md
@@ -27,6 +27,11 @@ The audited implementations and completed results are in `RESULTS.md`:
forward synapses, and a separate update consumes only parent/child activities
to estimate each feedback kernel. It is deliberately treated as a strong
learned-FA baseline, not as SDIL novelty;
+- residual response mirroring (`rrm`) replaces WM's noisy estimate-then-average
+ step with local normalized LMS on the child-response prediction error. Its
+ stochastic update vanishes sample-by-sample at Q=W; the extra feedback
+ prediction convolution is included in MAC accounting. It is also an
+ inherited learned-alignment control;
- direct node perturbation uses a causal target on every hidden update and no
learned vectorizer;
- the no-traffic, `P=0` SDIL backbone is learned direct feedback by node
diff --git a/NOVELTY.md b/NOVELTY.md
index ebb10d2..1dae12b 100644
--- a/NOVELTY.md
+++ b/NOVELTY.md
@@ -71,6 +71,11 @@ that performance belongs to the baseline. A paper-level SDIL gain requires the
somato-dendritic innovation subtraction to remain necessary on top of the
mirrored hierarchical path under a frozen mixed-traffic intervention.
+Residual response mirroring changes the inherited baseline's estimator but not
+this attribution. Its zero-noise fixed point and any resulting scale belong to
+local predictive weight estimation; they become relevant to SDIL only as the
+feedback substrate on which innovation is separately ablated.
+
## Candidate SDIL contribution
The candidate contribution is the combination
diff --git a/README.md b/README.md
index bf3ce6a..b49774c 100644
--- a/README.md
+++ b/README.md
@@ -141,6 +141,13 @@ load-bearing on top of it.
`MIRROR_BASELINE.md` freezes that comparator's CIFAR-prefix capture, short
accuracy, and conditional full-validation gates.
+After estimate-then-average WM narrowly failed its short accuracy gate, the
+next substantive inherited control uses residual response mirroring. Q predicts
+the observed child response and learns only from the local prediction error;
+unlike WM, its update is zero for every probe at exact symmetry. The extra
+feedback prediction convolution is charged explicitly, and this rule receives
+no SDIL novelty credit.
+
The subsequent V3 mechanism estimates the required A/G matrix statistics
directly by perturbing the vectorizer parameter subspace. It remains
forward-only and uses two causal loss queries per direction. Its exact and
diff --git a/THEORY.md b/THEORY.md
index 7038969..c64eb46 100644
--- a/THEORY.md
+++ b/THEORY.md
@@ -291,6 +291,28 @@ mean/minimum feedback-forward cosine `0.9887/0.9661`, norm ratios
`[0.9537,1.0444]`, and zero forward-parameter access error in the update.
These are mechanics, not task evidence. A frozen comparator gate is required.
+The frozen WM short task gate later exposed a finite-sample defect of the
+estimate-then-average rule: each new `What` remains noisy even when `Q=W`, so
+an exponential tracker has a nonzero stationary error. Residual response
+mirroring (RRM) makes a substantive rule change. Given the same local probe
+and observed child response, it predicts the child response through Q and uses
+only the residual:
+
+```text
+u = conv(z, W),
+uhat = conv(z, Q),
+Delta Q = eta_M corr(z, u-uhat) / (count sigma^2).
+```
+
+In expectation this is the same contraction toward W, but when Q exactly
+matches W the update is zero for every individual probe rather than only in
+expectation. The readout uses the identical signed response residual. An
+executable audit measures fixed-point update RMS `2.75e-18`, response-residual
+fraction `3.08e-17`, and zero update dependence on forward parameters after
+the observations are fixed. RRM needs one extra local Q prediction convolution
+per mirror event, which is charged separately. It remains inherited local
+weight estimation, not SDIL novelty.
+
## 2. What is sufficient for a descent step
Flatten all forward parameters into `theta`, let `p = grad L(theta)`, and let
diff --git a/experiments/conv_local_smoke.py b/experiments/conv_local_smoke.py
index 7d24154..4e388a0 100644
--- a/experiments/conv_local_smoke.py
+++ b/experiments/conv_local_smoke.py
@@ -14,6 +14,7 @@ from sdil.conv import (CIFARHierarchicalFAResNet, CIFARLocalResNet,
conv_hierarchical_step, conv_local_step,
hierarchical_mirror_observations,
hierarchical_parameter_subspace_calibration,
+ normalized_residual_mirror_update,
normalized_response_mirror_update,
simultaneous_conv_node_perturbation,
vectorizer_subspace_apical_calibration)
@@ -794,6 +795,38 @@ def normalized_response_mirror_checks():
independence_error = max(float((a - b).abs().max()) for a, b in zip(
left.Q[1:] + [left.R_out], right.Q[1:] + [right.R_out]))
assert independence_error == 0.0
+
+ # Residual-response LMS has a per-observation exact fixed point: its update
+ # is zero, not merely zero in expectation, when Q/R match the forward maps.
+ fixed = CIFARHierarchicalFAResNet(
+ depth=8, base_width=2, seed=125, dtype=torch.float64)
+ fixed.Q = [value.clone() for value in fixed.W]
+ fixed.R_out.copy_(-fixed.W_out.t())
+ fixed_observations = hierarchical_mirror_observations(
+ fixed, batch_size=2, generator=torch.Generator().manual_seed(126))
+ fixed_metrics = normalized_residual_mirror_update(
+ fixed, fixed_observations, eta=1.0)
+ assert fixed_metrics["mirror_update_rms"] < 1e-14
+ assert fixed_metrics["mirror_response_residual_fraction"] < 1e-14
+
+ residual_left = CIFARHierarchicalFAResNet(
+ depth=8, base_width=2, seed=127, dtype=torch.float64)
+ residual_right = CIFARHierarchicalFAResNet(
+ depth=8, base_width=2, seed=127, dtype=torch.float64)
+ residual_observations = hierarchical_mirror_observations(
+ residual_left, batch_size=2,
+ generator=torch.Generator().manual_seed(128))
+ for value in residual_right.W + [residual_right.W_out]:
+ value.normal_(generator=torch.Generator().manual_seed(value.numel() + 1))
+ normalized_residual_mirror_update(
+ residual_left, residual_observations, eta=0.2)
+ normalized_residual_mirror_update(
+ residual_right, residual_observations, eta=0.2)
+ residual_independence_error = max(float((a - b).abs().max())
+ for a, b in zip(
+ residual_left.Q[1:] + [residual_left.R_out],
+ residual_right.Q[1:] + [residual_right.R_out]))
+ assert residual_independence_error == 0.0
return {
"mirror_estimate_mean_forward_cosine": sum(cosines) / len(cosines),
"mirror_estimate_min_forward_cosine": min(cosines),
@@ -801,6 +834,12 @@ def normalized_response_mirror_checks():
"mirror_estimate_max_norm_ratio": max(norm_ratios),
"mirror_update_forward_parameter_independence_error": independence_error,
"mirror_update_rms": metrics["mirror_update_rms"],
+ "residual_mirror_exact_fixed_point_update_rms": fixed_metrics[
+ "mirror_update_rms"],
+ "residual_mirror_exact_fixed_point_fraction": fixed_metrics[
+ "mirror_response_residual_fraction"],
+ "residual_mirror_forward_parameter_independence_error": (
+ residual_independence_error),
}
diff --git a/experiments/conv_run.py b/experiments/conv_run.py
index fcb6690..3b91da9 100644
--- a/experiments/conv_run.py
+++ b/experiments/conv_run.py
@@ -19,7 +19,8 @@ from sdil.conv import (CIFARHierarchicalFAResNet, CIFARLocalResNet,
conv_hierarchical_step,
conv_learned_hierarchical_step, conv_local_step,
evaluate_conv, hierarchical_parameter_subspace_calibration)
-from sdil.conv import normalized_response_mirror_step
+from sdil.conv import (normalized_residual_mirror_step,
+ normalized_response_mirror_step)
from sdil.data import DATA_DIR, get_cifar_image_splits
@@ -106,7 +107,7 @@ def build(args):
bn_momentum=args.bn_momentum, bn_eps=args.bn_eps)
if args.mode == "bp":
return CIFARLocalResNet(**common), None
- if args.mode in ("hfa", "lhfa", "wm"):
+ if args.mode in ("hfa", "lhfa", "wm", "rrm"):
net = CIFARHierarchicalFAResNet(
**common, feedback_seed=args.apical_seed,
feedback_scale=args.a_scale)
@@ -161,6 +162,7 @@ def work_report(net, mode, counters):
mirror_readout_macs = getattr(net, "R_out", torch.empty(0)).numel()
mirror_forward = (counters["mirror_conv_examples"] * mirror_conv_macs
+ counters["mirror_readout_examples"] * mirror_readout_macs)
+ mirror_prediction = mirror_forward if mode == "rrm" else 0
mirror_correlation = mirror_forward
components = {
"ordinary_forward_macs": normal_forward,
@@ -171,6 +173,7 @@ def work_report(net, mode, counters):
"apical_projection_macs": apical_inference,
"apical_regression_macs": apical_regression,
"mirror_response_macs": mirror_forward,
+ "mirror_feedback_prediction_macs": mirror_prediction,
"mirror_local_correlation_macs": mirror_correlation,
}
return {
@@ -206,7 +209,7 @@ def run(args):
raise ValueError("apical/predictor warmup is restricted to SDIL")
if args.mode == "lhfa" and args.learn_P:
raise ValueError("predictor learning is not defined for learned HFA")
- if args.mode != "wm" and args.mirror_warmup_steps:
+ if args.mode not in ("wm", "rrm") and args.mirror_warmup_steps:
raise ValueError("mirror warmup is restricted to weight mirror mode")
if args.mirror_every < 1 or args.mirror_batch_size < 1:
raise ValueError("invalid mirror cadence or batch size")
@@ -261,10 +264,11 @@ def run(args):
None if config is None or args.mode == "hfa" else
"hierarchical_feedback_parameters" if args.mode == "lhfa" else {
"wm": "local_parent_child_response",
+ "rrm": "local_parent_child_response_residual",
"unit_targets": "full_hidden_field",
"channel_subspace": "channel_basis_moments",
"vectorizer_subspace": "vectorizer_parameter_gradients",
- }[args.mode if args.mode == "wm"
+ }[args.mode if args.mode in ("wm", "rrm")
else config.apical_calibration_mode]),
"args": vars(args),
"provenance": provenance(),
@@ -291,7 +295,7 @@ def run(args):
if args.mode == "hfa" else 0),
"adaptive_feedback_parameters": (
getattr(net, "n_fixed_feedback_parameters", 0)
- if args.mode in ("lhfa", "wm") else 0),
+ if args.mode in ("lhfa", "wm", "rrm") else 0),
},
"epochs": [],
}
@@ -302,12 +306,15 @@ def run(args):
apical_warmup_wall = 0.0
mirror_warmup_wall = 0.0
loader_state = train.g.get_state().clone()
- if args.mode == "wm" and args.mirror_warmup_steps:
+ if args.mode in ("wm", "rrm") and args.mirror_warmup_steps:
sync(args.device)
mirror_start = time.time()
mirror_metrics = []
for _ in range(args.mirror_warmup_steps):
- metric = normalized_response_mirror_step(
+ mirror_function = (normalized_residual_mirror_step
+ if args.mode == "rrm"
+ else normalized_response_mirror_step)
+ metric = mirror_function(
net, batch_size=args.mirror_batch_size,
noise_std=args.mirror_noise_std, eta=args.mirror_eta,
generator=mirror_generator)
@@ -444,6 +451,20 @@ def run(args):
result = conv_hierarchical_step(net, x, y, config)
loss = result["loss"]
did_perturb = False
+ elif args.mode == "rrm":
+ if step % args.mirror_every == 0:
+ mirror_metric = normalized_residual_mirror_step(
+ net, batch_size=args.mirror_batch_size,
+ noise_std=args.mirror_noise_std, eta=args.mirror_eta,
+ generator=mirror_generator)
+ mirror_metrics.append(mirror_metric)
+ counters["mirror_conv_examples"] += args.mirror_batch_size
+ counters["mirror_readout_examples"] += mirror_metric[
+ "readout_batch_size"]
+ counters["mirror_events"] += 1
+ result = conv_hierarchical_step(net, x, y, config)
+ loss = result["loss"]
+ did_perturb = False
else:
result = conv_local_step(
net, x, y, config, step, generator=perturb_generator)
@@ -527,7 +548,7 @@ def run(args):
diagnostic_start = time.time()
diagnostics = (conv_hierarchical_alignment_report(
net, train.x[:probe], train.y[:probe])
- if args.mode in ("hfa", "lhfa", "wm") else
+ if args.mode in ("hfa", "lhfa", "wm", "rrm") else
conv_alignment_report(
net, train.x[:probe], train.y[:probe], config))
sync(args.device)
@@ -576,7 +597,7 @@ def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument(
"--mode", choices=(
- "bp", "dfa", "hfa", "lhfa", "wm", "sdil", "nodepert"),
+ "bp", "dfa", "hfa", "lhfa", "wm", "rrm", "sdil", "nodepert"),
required=True)
parser.add_argument("--out", required=True)
parser.add_argument("--device", default="cpu")
diff --git a/sdil/conv.py b/sdil/conv.py
index cc373e8..b3d9235 100644
--- a/sdil/conv.py
+++ b/sdil/conv.py
@@ -771,6 +771,70 @@ def normalized_response_mirror_update(net, observations, eta=0.1):
@torch.no_grad()
+def normalized_residual_mirror_update(net, observations, eta=0.1):
+ """Local normalized LMS on child-response prediction residuals.
+
+ Unlike estimate-then-average mirroring, the stochastic update vanishes for
+ every probe when Q exactly matches W. It therefore removes the stationary
+ estimator noise that compounds through a deep feedback chain. The update
+ still consumes only fixed probe/response observations and Q/R.
+ """
+ if not isinstance(net, CIFARHierarchicalFAResNet):
+ raise TypeError("residual mirror update requires a hierarchical net")
+ if not 0.0 < eta <= 1.0:
+ raise ValueError("residual mirror eta must be in (0, 1]")
+ variance = float(observations.get("noise_variance", 0.0))
+ conv = observations.get("conv")
+ if variance <= 0 or conv is None or len(conv) != len(net.Q):
+ raise ValueError("invalid residual mirror observations")
+ update_power = 0.0
+ residual_power = 0.0
+ response_power = 0.0
+ parameters = 0
+ response_units = 0
+ for index in range(1, len(net.Q)):
+ probe, response = conv[index]
+ spec = net.layer_specs[index]
+ prediction = F.conv2d(
+ probe, net.Q[index], stride=spec.stride, padding=spec.padding)
+ residual = response - prediction
+ correlation = torch.nn.grad.conv2d_weight(
+ probe, net.Q[index].shape, residual,
+ stride=spec.stride, padding=spec.padding)
+ counts = torch.nn.grad.conv2d_weight(
+ torch.ones_like(probe), net.Q[index].shape,
+ torch.ones_like(response), stride=spec.stride,
+ padding=spec.padding)
+ update = correlation / (variance * counts.clamp_min(1.0))
+ net.Q[index].add_(update, alpha=eta)
+ update_power += float(update.square().sum())
+ residual_power += float(residual.square().sum())
+ response_power += float(response.square().sum())
+ parameters += update.numel()
+ response_units += response.numel()
+ readout_probe, readout_response = observations["readout"]
+ readout_prediction = -(readout_probe @ net.R_out)
+ readout_residual = readout_response - readout_prediction
+ readout_update = -(readout_probe.t() @ readout_residual) / (
+ variance * readout_probe.shape[0])
+ net.R_out.add_(readout_update, alpha=eta)
+ update_power += float(readout_update.square().sum())
+ residual_power += float(readout_residual.square().sum())
+ response_power += float(readout_response.square().sum())
+ parameters += readout_update.numel()
+ response_units += readout_response.numel()
+ return {
+ "mirror_update_rms": math.sqrt(update_power / parameters),
+ "mirror_response_residual_rms": math.sqrt(
+ residual_power / response_units),
+ "mirror_response_residual_fraction": math.sqrt(
+ residual_power / max(response_power, 1e-300)),
+ "conv_batch_size": int(observations["conv_batch_size"]),
+ "readout_batch_size": int(observations["readout_batch_size"]),
+ }
+
+
+@torch.no_grad()
def normalized_response_mirror_step(
net, batch_size=1, noise_std=1.0, eta=0.1, generator=None):
observations = hierarchical_mirror_observations(
@@ -781,6 +845,15 @@ def normalized_response_mirror_step(
return metrics
+@torch.no_grad()
+def normalized_residual_mirror_step(
+ net, batch_size=1, noise_std=1.0, eta=0.1, generator=None):
+ observations = hierarchical_mirror_observations(
+ net, batch_size=batch_size, noise_std=noise_std,
+ generator=generator)
+ return normalized_residual_mirror_update(net, observations, eta=eta)
+
+
def conv_hierarchical_step(net, x, y, config):
"""One hierarchical-FA update using no reverse-mode graph or weight transport."""
config.validate()