e-prop: local eligibility and learning signals#
e-prop separates a synapse-local eligibility trace from a learning signal broadcast by the readout. The separation is the scientific content of the method; a generic loss curve alone would not demonstrate it.
1. Principle#
For synapse \(i\to j\), e-prop forms
\(e_{ji}^t\) is local to the neuron/synapse dynamics. \(L_j^t\) carries task information from the readout. Symmetric feedback uses the actual reverse-AD learning-signal direction. Random feedback uses a fixed projection and removes weight transport only partially in the current hook contract; it must not be described as numerically equivalent to symmetric feedback.
2. Applicability and exclusions#
Use e-prop for recurrent LIF/ALIF-style networks when local traces and an explicit learning-signal approximation are the intended inductive bias.
Do not infer exact BPTT gradients for arbitrary recurrent coupling. The \(\kappa\) filter changes temporal credit assignment, while random feedback adds a separate approximation and requires an explicit key.
import brainstate
import braintools
import braintrace
import jax
import jax.numpy as jnp
brainstate.random.seed(21)
class SmallEPropSNN(brainstate.nn.Module):
def __init__(self, n_in=2, n_rec=6):
super().__init__()
self.n_rec = n_rec
self.w_in = brainstate.ParamState(
0.3 * brainstate.random.randn(n_in, n_rec)
)
self.w_rec = brainstate.ParamState(
0.1 * brainstate.random.randn(n_rec, n_rec)
)
self.w_out = brainstate.ParamState(
0.2 * brainstate.random.randn(n_rec, 1)
)
self.surrogate = braintools.surrogate.ReluGrad()
def init_state(self, **kwargs):
self.v = brainstate.HiddenState(jnp.zeros(self.n_rec))
self.z = brainstate.HiddenState(jnp.zeros(self.n_rec))
def reset_state(self, **kwargs):
self.v.value = jnp.zeros_like(self.v.value)
self.z.value = jnp.zeros_like(self.z.value)
def update(self, x):
drive = (
braintrace.matmul(x, self.w_in.value)
+ braintrace.matmul(self.z.value, self.w_rec.value)
)
voltage = 0.9 * self.v.value + drive - self.z.value
spike = self.surrogate(voltage - 0.45)
self.v.value = voltage
self.z.value = spike
return spike @ self.w_out.value
3. Compile and expose the filter coordinate#
The sequence driver advances the SNN once per call. This example uses symmetric feedback so the effect under inspection is the \(\kappa\) eligibility filter, not a simultaneous random-feedback approximation.
inputs = brainstate.random.bernoulli(
0.35, size=(16, 2)
).astype(jnp.float32)
targets = jnp.full((16, 1), 0.25)
mask = (jnp.arange(16) >= 4).astype(jnp.float32)
model = SmallEPropSNN()
brainstate.nn.init_all_states(model)
learner = braintrace.compile(
model,
braintrace.EProp,
inputs[0],
feedback="symmetric",
kappa_filter_decay=0.8,
)
def loss_step(x, target):
return jnp.mean((learner(x) - target) ** 2)
brainstate.nn.reset_all_states(model)
learner.reset_state()
grads, loss = learner.etrace_grad(
inputs,
targets,
step_fn=loss_step,
mask=mask,
loss_output="scalar",
return_value=True,
)
gradient_norm = jnp.sqrt(sum(jnp.sum(g * g) for g in grads.values()))
etraces = learner.get_etrace_of(model.w_rec)
trace_norm = jnp.sqrt(sum(
jnp.sum(leaf * leaf) for leaf in jax.tree.leaves(etraces)
))
print("trace filter:", learner.config.trace_filter)
print("kappa:", learner.config.kappa)
print("learning signal:", learner.config.learning_signal)
print(f"masked loss: {float(loss):.4f}")
print("finite nonzero trace:", bool(jnp.isfinite(trace_norm) & (trace_norm > 0)))
print("finite nonzero gradient:", bool(jnp.isfinite(gradient_norm) & (gradient_norm > 0)))
trace filter: kappa
kappa: 0.8
learning signal: symmetric
masked loss: 0.0586
finite nonzero trace: True
finite nonzero gradient: True
4. Interpretation and limits#
The printed configuration verifies which learning-rule coordinates were compiled. The nonzero recurrent-weight trace shows that the local eligibility state was advanced, while the finite gradient checks its contraction through the public sequence path. Neither check isolates the numerical effect of \(\kappa\). A causal comparison would hold model, sequence, feedback, and parameters fixed while varying only \(\kappa\), then use a finite-window oracle appropriate to the learning-rule claim.
Reference and API#
Bellec et al., “A solution to the learning dilemma for recurrent networks of spiking neurons,” Nature Communications 11, 3625 (2020), doi:10.1038/s41467-020-17236-y.
API:
braintrace.EProp,braintrace.compile(),braintrace.SequenceDriverMixin.etrace_grad().