diff options
| -rw-r--r-- | BASELINES.md | 5 | ||||
| -rw-r--r-- | NOVELTY.md | 5 | ||||
| -rw-r--r-- | README.md | 7 | ||||
| -rw-r--r-- | THEORY.md | 22 | ||||
| -rw-r--r-- | experiments/conv_local_smoke.py | 39 | ||||
| -rw-r--r-- | experiments/conv_run.py | 39 | ||||
| -rw-r--r-- | sdil/conv.py | 73 |
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 @@ -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 @@ -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 @@ -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() |
