braintrace.train_synthetic_gradient#
- braintrace.train_synthetic_gradient(learner, inputs, *, chunk_size=1, loss_fn=None, optimizer=None, lr=0.01, epochs=1, reset=True, batch_size=None)#
Fit the synthesiser against the learner’s own returned hidden cotangent.
The regression target for
M(h^{a_k})isdL_{>= a_k}/dh^{a_k}, and the learner already produces exactly that: with the hidden states in the differentiation set,brainstate.transform.gradreturns the window’s hidden cotangent, which (by the second pass of_update_fn_bwd) already carries the future term. So no new side channel is needed – the target comes out of the public API.Two properties make the fit honest, and both are enforced here:
the model parameters do not move – only the synthesiser’s are handed to the optimiser;
the target is detached, so the regression cannot reshape the model’s gradients to make itself easy to predict.
The auxiliary optimiser is left to the caller, as it is in the paper.
- Parameters:
learner (DNI) – A compiled learner with a synthesiser attached.
inputs (array) – A
(T, ...)sequence, consumed one window at a time.Tmust be a multiple of chunk_size; a ragged final window is refused rather than truncated (see Raises).chunk_size (int, optional) – Steps per window. This must match the window size the learner will be driven with: the synthesiser predicts the future at a window boundary, and boundaries move when the window size does. Training on one-step windows and then deploying on longer ones fits the wrong target – a much shorter future – and the result can easily be worse than no synthesiser at all. Default
1.loss_fn (callable, optional) –
loss_fn(output) -> scalar. This must be the objective the learner will actually be trained on, and it is the second half of the same trapchunk_sizedocuments. The synthesiser predictsdL_{>= b}/dh^b– a derivative of this loss. Fit it against the default sum-of-squares and then descend on, say,((out - target) ** 2).mean(), and the injected cotangent is the gradient of a different function at a different scale: not an approximation of the future credit but noise with the shape of one. Measured on the delayed-reward fixture, a mismatchedloss_fnleft the run worse than leaving DNI off entirely. Default: sum of squares.optimizer (braintools.optim.Optimizer, optional) – Already registered against
learner.synthesizer.states_dict(). IfNone, a plain SGD step with learning ratelris applied.lr (float, optional) – Learning rate for the built-in SGD step. Ignored when
optimizeris given. Default1e-2.epochs (int, optional) – Passes over
inputs.reset (bool, optional) – Whether to re-initialise the model states and the trace before each epoch. Default
True.batch_size (int, optional) – Batch size for that re-initialisation. Inferred from the learner’s own hidden states when omitted, which is almost always what you want; pass it only to override. It is not assumed to be 1 – doing so either raises a shape error on a wider learner or, worse, succeeds and fits the synthesiser against the wrong initial states.
- Returns:
list of float – The mean squared prediction error per epoch, averaged over every window boundary and the terminal boundary.
- Raises:
RuntimeError – If no synthesiser is attached to learner.
ValueError – If chunk_size is below 1, or if it does not divide
len(inputs).
Notes
The window loop is a
brainstate.transform.for_loop, so the body – which drives the learner through itscustom_vjpand then takes a regression step – is traced once per epoch rather than once per window (AGENTS.md rule 10). Everything that changes across windows isStateand threads through automatically: the model’s hidden states, the learner’s trace, the synthesiser’s parameters, and the optimiser’s moments. Epochs remain a Python loop, becauseresetcallsinit_all_states, which reallocates state and cannot run under a trace.Each epoch fits one extra pair beyond the window loop: the terminal state
h^Tagainst a target of exactly zero. Deployment injects at window exit states while the loop iterates entry states, and those sets differ at the ends –h^Tis injected at but never otherwise trained, and its true future gradient is zero because nothing follows it.