summaryrefslogtreecommitdiff
path: root/sdil
diff options
context:
space:
mode:
Diffstat (limited to 'sdil')
-rw-r--r--sdil/core.py35
-rw-r--r--sdil/probes.py21
2 files changed, 46 insertions, 10 deletions
diff --git a/sdil/core.py b/sdil/core.py
index 3cd8a97..d7be8c3 100644
--- a/sdil/core.py
+++ b/sdil/core.py
@@ -219,6 +219,31 @@ class SDILNet:
return a, a
+def teaching_signal(net, l, c, h_l, cfg):
+ """Select the signal that drives a hidden layer's local plasticity.
+
+ ``match_innovation_norm`` is a deliberately strong raw-apical control: it
+ preserves each sample's raw apical direction exactly while rescaling its
+ Euclidean norm to the norm of the corresponding innovation. Therefore a
+ raw-vs-residual difference under this control cannot be attributed merely
+ to the nuisance making the teaching vector larger.
+
+ Returns ``(teaching, raw_apical, innovation)`` so training and diagnostic
+ probes use one definition of every signal.
+ """
+ a = net.apical(l, c, h_l)
+ innovation = a - net.baseline(l, h_l)
+ if cfg.use_residual:
+ teaching = innovation
+ elif cfg.raw_scale_control == "match_innovation_norm":
+ raw_norm = a.norm(dim=1, keepdim=True)
+ innovation_norm = innovation.norm(dim=1, keepdim=True)
+ teaching = a * (innovation_norm / raw_norm.clamp_min(1e-12))
+ else:
+ teaching = a
+ return teaching, a, innovation
+
+
# --------------------------------------------------------------------------
# node perturbation: causal estimate of -grad_{h_l} L used to TRAIN A_l
# --------------------------------------------------------------------------
@@ -341,11 +366,15 @@ class SDILConfig:
use_residual=True, learn_A=True, learn_P=True,
pert_sigma=1e-2, pert_every=5, pert_ndirs=1, momentum=0.0, wd=0.0,
settle_steps=0, kappa=0.0, feedback="error", p_update_on_neutral=True,
- normalize_delta=False, pert_mode="layerwise"):
+ normalize_delta=False, pert_mode="layerwise",
+ raw_scale_control="none"):
self.eta = eta
self.eta_A = eta_A
self.eta_P = eta_P
self.use_residual = use_residual # ablation: residual vs raw apical
+ if raw_scale_control not in ("none", "match_innovation_norm"):
+ raise ValueError(f"unknown raw_scale_control: {raw_scale_control}")
+ self.raw_scale_control = raw_scale_control
self.learn_A = learn_A # ablation: perturbation-trained vs fixed-random A
self.learn_P = learn_P # ablation: predictor on/off
self.pert_sigma = pert_sigma
@@ -390,7 +419,7 @@ def sdil_step(net, x, y, y_onehot, cfg, step, prev_error=None):
# ---- compute innovations for hidden layers ----
r_list, a_list = [], []
for l in range(net.L - 1):
- r, a = net.innovation(l, c, h[l + 1], use_residual=cfg.use_residual)
+ r, a, _ = teaching_signal(net, l, c, h[l + 1], cfg)
r_list.append(r)
a_list.append(a)
@@ -496,7 +525,7 @@ def _settle(net, h, u, c, cfg):
dt_over_tau = 1.0 / max(1, cfg.settle_steps)
for _ in range(cfg.settle_steps):
for l in range(net.L - 1):
- r, _ = net.innovation(l, c, h[l + 1], use_residual=cfg.use_residual)
+ r, _, _ = teaching_signal(net, l, c, h[l + 1], cfg)
target = net.act(u[l]) + cfg.kappa * r
h[l + 1] = h[l + 1] + dt_over_tau * (-h[l + 1] + target)
# recompute downstream pre-activations from settled hidden states
diff --git a/sdil/probes.py b/sdil/probes.py
index 16aca21..fad82ca 100644
--- a/sdil/probes.py
+++ b/sdil/probes.py
@@ -36,11 +36,20 @@ def true_hidden_grads(net, x, y):
return [g.detach() for g in grads], loss.item()
-def _row_cos(u, v, eps=1e-12):
- """Mean per-sample cosine between rows of u and v."""
+def _row_cos(u, v):
+ """Mean per-sample cosine, excluding rows with an exact zero vector.
+
+ Adding an absolute epsilon to the denominator makes cosine spuriously
+ depend on signal magnitude near convergence. In particular, a positive
+ per-sample rescaling can then appear to change direction. Exact zero rows
+ have no defined direction and are omitted instead.
+ """
num = (u * v).sum(1)
- den = u.norm(dim=1) * v.norm(dim=1) + eps
- return (num / den).mean().item()
+ den = u.norm(dim=1) * v.norm(dim=1)
+ valid = den > 0
+ if not valid.any():
+ return float("nan")
+ return (num[valid] / den[valid]).mean().item()
@torch.no_grad()
@@ -61,10 +70,8 @@ def alignment_report(net, x, y, y_onehot, cfg):
for l in range(net.L - 1):
g = grads[l]
negg = -g
- a = net.apical(l, c, h[l + 1]) # raw apical (with nuisance)
+ teaching, a, innovation = core.teaching_signal(net, l, c, h[l + 1], cfg)
Ac = c @ net.A[l].t() # pure vectorizer output
- innovation = a - net.baseline(l, h[l + 1])
- teaching = innovation if cfg.use_residual else a
# include the phi' gain that the plasticity rule actually applies,
# so the reported alignment is what the weight update "sees"
gain = net.act_prime(u[l])