This page describes the legacy --executor legacy training path in tools/snnsim/train.py. It connects the training controls to their implementation. For declarative graph training, use exp088 — Training recipes and graph-native learning graph checkpoints and input bindings have a different contract.
From the repository root, choose a fresh scratch directory and run a short MNIST plumbing check:
uv run python tools/snnsim/tool.py train --executor legacy \
--model ping --dataset mnist --n-hidden 32 --epochs 1 \
--max-samples 128 --batch-size 16 --t-ms 20 --dt 0.25 \
--readout mem-mean --lr 0.0001 --v-grad-dampen 1000 \
--seed 42 --out-dir temp/docs-trainingThis example exercises the training path; its short duration and small dataset are not a scientific baseline. The first run may download MNIST. Inspect finite loss, gradient diagnostics, skipped updates, and output files before scaling up. --epochs 0 is an initialization probe, not a trained checkpoint. Use the committed experiment recipe for a production run.
For MNIST, training uses a held-out validation partition from the official training data. Checkpoint selection minimises validation cross-entropy averaged over encoder draws; validation accuracy breaks exact loss ties. The official test partition is reserved for separate inference.
| Output | Meaning |
weights.pth | Validation-selected parameters, when a selected epoch exists. |
weights_final.pth | Parameters at the end of training. Use when measuring final-epoch dynamics. |
config.json | Resolved training configuration, split and encoder settings. |
metrics.json / metrics.jsonl | Summary and training history; checkpoint metadata records roles and hashes. |
test_predictions.json | Legacy filename for validation predictions; the filename does not make these held-out test results. |
A weight file is not a complete optimizer-resume checkpoint. Keep it with its configuration and recorded role. To evaluate the selected parameters separately:
uv run python tools/snnsim/tool.py sim --infer --executor legacy \
--load-config temp/docs-training/config.json \
--load-weights temp/docs-training/weights.pth \
--max-samples 128 --outputs per_cell_rates \
--out-dir temp/docs-evaluationThis capped evaluation is a plumbing check. Neither command creates a completed Pingstore run; experiment stages retain scientific evidence separately.
Every model here is a recurrent system run forward in time, so gradients come from Backpropagation Through Time (BPTT): unroll the recurrence into a deep feedforward graph — one layer per timestep, all sharing the same weights — and backpropagate through it.
Take a hidden state that evolves as
with input , score output , and parameters shared across time. Running steps gives a chain , which for gradients is treated as a depth- feedforward network with tied weights.
For a scalar loss depending on the final state, define as the total sensitivity to state . Then the contribution through state updates is
Here is one shared parameter and the derivative of holds its state and input arguments fixed. Direct parameter use in the readout contributes an additional term. Losses or readouts accumulated over time also inject sensitivities at intermediate steps.
The backward pass contains products of per-step Jacobians , the matrices of state derivatives. Repeated contraction can suppress gradients and amplification can enlarge them; individual norms above 1 do not by themselves prove that the product grows.
One simulation step is one step of the recurrence: the state includes membrane voltages, synaptic conductances, and refractory counters. A 200 ms trial at ms unrolls to steps. Gradient behaviour depends on the trajectory, surrogate, weights, and reset gates; recurrent coupling alone does not prove divergence. See exp015 — Gradient Stabilisation for the implemented intervention.
The spike function has zero gradient almost everywhere, so the backward pass substitutes a smooth surrogate. The legacy spike helper uses a fast-sigmoid surrogate. Forward is the hard step; backward is
Here is the pre-reset membrane value, the spike threshold, and the surrogate slope in inverse voltage units; denotes the backward surrogate, not the forward spike. This is Pinglab’s normalization. snnTorch’s FastSigmoid uses numerator 1 instead of : equal slopes do not generally give equal gradients.
It takes its slope from SURROGATE_SLOPE = 5.0, overridable per-run with --surrogate-slope.
--v-grad-dampen scales the backward derivative through the biophysical membrane increment. It does not rescale every gradient or guarantee stable training. Start from the chosen recipe and inspect the diagnostics before changing it; exp015 — Gradient Stabilisation explains the local derivatives and trade-offs.
Logits from the readout go into cross-entropy loss:
Here is cross-entropy loss, is minibatch size, indexes presentations, is the score vector, the true class, and indexes classes. Uniform predictions on ten classes give loss . The implementation uses AdamW with --weight-decay 0 by default. Gradients are clipped to unit norm (GRAD_CLIP = 1.0); an update with non-finite gradient norm is skipped. Checkpoint selection uses validation loss, not this training loss.
--readout selects the legacy class-score calculation. Set it explicitly when comparing recipes; the CLI default is rate, not mem-mean.
| Mode | Implemented reduction |
rate | Sum last-hidden spikes, then multiply by the readout matrix. Despite its name, this path does not divide by duration or apply softmax. |
mem-mean | Average the output LIF’s pre-reset membrane over time. Its subtractive reset changes later voltages. |
spike-count | Count spikes of each output LIF neuron, not the hidden population. |
spike-rate | Divide output-LIF counts by presentation duration in seconds. |
cumulative-potential | Accumulate per-step softmax values from a non-spiking leaky decoder. |
li is not an accepted legacy CLI mode. Changing a readout changes the score scale and gradient path; it is not merely a display choice. Output membrane parameters are separate from the hidden biophysical constants.
Hidden activity can be limited with --fr-reg-upper-target-hz and --fr-reg-upper-strength:
Here is the spike count of hidden excitatory neuron in presentation , is the number of those neurons, is presentation duration in seconds, is minibatch size, and are rates in Hz, and is the configured rate-penalty coefficient.
The ceiling is applied separately to each presentation’s population-mean hidden-E rate before averaging across the minibatch. The loss is normalised over neurons, presentation duration, samples, and hidden layers. This is the mechanism behind exp025 — Accuracy and Firing Rate With and Without Inhibition exp024 — Accuracy Plateaus While Firing Rate Rises tests the associated rate-plateau interpretation.
Dale-constrained magnitudes use a lower-clamped Gaussian, not a half-normal or truncated normal:
Here is a Gaussian draw for input index and output index , and is its non-negative clamp. The configured and are parent-Gaussian parameters on the summed-coupling scale, not moments of one stored edge. With initial-zero fraction and Bernoulli indicator , the stored initialization is
Here is fan-in and is 1 with probability , otherwise 0. Direct readout initialization (--readout-w-init-mean and --readout-w-init-std) bypasses this fan-in scaling; do not interpret its parameters as summed coupling.
The compensation keeps the expected column sum independent of ; lower clamping means that expected sum is , which is recorded alongside the configured parent parameters. Both lower-clamp zeros and explicitly zeroed entries remain trainable and may become positive. This is sparse initialization of a dense trainable matrix, not structural sparsity.
When Dale’s law is on, the feedforward matrices are clamped to when they are read by the forward pass and every trainable constrained matrix is projected back into the non-negative cone by project_dales() after each optimiser step. The recurrent conductance matrices , , , and are not forward-clamped: they are initialised non-negative and, when trainable, kept non-negative by the post-step projection. Their entries are conductance magnitudes; pathway-specific reversal potentials, rather than a negative stored , determine whether a synapse is excitatory or inhibitory.
--epochs, the skipped-update count, and whether weights.pth exists. An initialization snapshot is not a successful training run.Implementation reference: tools/snnsim/train.py, tools/snnsim/models.py, and tools/snnsim/tool.py.