fluct_init — Fluctuation-Driven Weight Initialisation
fluct_init initialises hardware layers close to the fluctuation-driven regime instead
of relying on generic ANN schemes such as Xavier or Kaiming. The goal is to place the
sub-threshold membrane potential near threshold at initialisation, which increases the
number of neurons that contribute surrogate-gradient signal from the first optimisation step.
Reference: Rossbroich, Gygax & Zenke (2022), Fluctuation-driven initialization for spiking neural network training
DOI: 10.48550/arXiv.2206.10226
Supported hardware: H1v1 and H1v2. All layers in a model must belong to the same family.
Relation to Rossbroich et al.
NWAVE's implementation is derived from Rossbroich et al., not a verbatim port:
- The PSP kernel ε(t) is measured numerically from the actual HW layer dynamics, rather than assumed to be exponential (as in the LIF paper). For H1v1 the nonlinear synapse makes a small-probe measurement exact; for H1v2 the linear synapse is exact by design.
- NWAVE adds an adaptive binary search on the feed-forward mean weight µ_W to ensure no dead neurons at initialisation. This step is not in the original Rossbroich et al. method.
H1v1/H1v2 Differences
H1v2 weights are weaker than H1v1, causing a lower charge to be added to the membrane of the neuron per unit value. This means that initializations that worked on H1v1 might generate weights out of bounds for H1v2 models. To prevent this we advice to use higher xi_targets and a good number of high firing inputs for each layer!
Import path
from nwavesdk.init.fluct_init import fluct_init
Quick start
H1v1 network (FF)
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
from nwavesdk.layers import H1v1Synapse, H1v1Layer, prepare_net
from nwavesdk.surrogate import fast_sigmoid
from nwavesdk.init.fluct_init import fluct_init
class TinyH1Net(nn.Module):
def __init__(self):
super().__init__()
self.synapse = H1v1Synapse(nb_inputs=16, nb_outputs=32, device="cpu")
self.layer = H1v1Layer(
n_neurons=32, taus=10e-3, dt=1e-3,
layer_topology="FF",
spike_grad=fast_sigmoid(slope=5.0),
device="cpu",
)
def forward(self, x):
prepare_net(self, collect_metrics=False)
cur = self.synapse(x)
spk, mem = self.layer(cur)
return spk, mem
x = (torch.rand(64, 100, 16) < 0.3).float()
train_dl = DataLoader(TensorDataset(x), batch_size=16, shuffle=True)
model = TinyH1Net()
fluct_init(model, train_dl, xi_target=1.0, alpha=1.0, n_batches=4, verbose=True)
H1v2 network (RC)
from nwavesdk.layers import H1v2Synapse, H1v2Layer, prepare_net
from nwavesdk.init.fluct_init import fluct_init
class TinyH1v2Net(nn.Module):
def __init__(self):
super().__init__()
self.syn = H1v2Synapse(nb_inputs=8, nb_outputs=16, device="cpu")
self.lyr = H1v2Layer(
n_neurons=16, taus=10e-3, dt=1e-3,
layer_topology="RC",
spike_grad=fast_sigmoid(slope=5.0),
device="cpu",
)
self.layer_pairs = [(self.syn, self.lyr)] # explicit — recommended for RC nets
def forward(self, x):
prepare_net(self, collect_metrics=False)
cur = self.syn(x)
spk, mem = self.lyr(cur)
return spk, mem
fluct_init(model, train_dl, xi_target=1.0, alpha=0.85, n_batches=4, verbose=True)
fluct_init modifies the model in-place and returns None.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
model |
nn.Module |
— | Initialised in-place. Must contain at least one (H1v1/H1v2 Synapse, H1v1/H1v2 Layer) dense pair. All pairs must belong to the same hardware family. |
dataloader |
DataLoader or iterable |
— | Input batches (B, T, n_F) or tuples whose first element is that tensor. Used to estimate firing-rate moments and check for dead neurons. |
xi_target |
float |
2.0 |
Target ξ = (θ − µ_U) / σ_U. Recommended range: 1–3. Lower = more fluctuation-driven. |
alpha |
float |
1 |
Fraction of σ_U² budget for FF weights; remaining (1 − alpha) goes to RC weights. Set to 1.0 for FF-only networks. |
n_batches |
int |
1 |
Batches per firing-rate estimate. Raise to 4–8 for noisy datasets. |
verbose |
bool |
True |
Print per-layer init summary. |
Verbose output
Example for a single FF layer:
[fluct_init] ξ=1.0 α=0.85 dt=1.0ms (stacked, adaptive µ) [H1v1]
Input → ν_mean=392.0Hz ν_var=392.0Hz ratio=1.0x
Layer 1 | ν_in=392.0Hz µ_W=0.5158 σ_FF=0.0516 µ_U=0.104
[fluct_init] done.
For a frontend-first model:
[fluct_init] ξ=1.0 α=0.85 dt=1.0ms (stacked, adaptive µ) [H1v1]
Frontend → ν_out=145.2Hz (used as ν_in for layer 1)
Layer 1 | ν_in=145.2Hz µ_W=0.3841 σ_FF=0.0384 σ_RC=0.0572 µ_U=0.097
[fluct_init] done.
For multi-layer stacked init, the propagated rate is also printed:
→ ν_2 = 145.3 Hz
| Field | Meaning |
|---|---|
ν_mean |
Mean input firing rate estimated from data [Hz]. |
ν_var |
Second moment used for variance-budget calculation. Equals ν_mean for binary spikes. |
ν_in |
Input firing rate used for this layer (dataloader input or previous layer output). |
µ_W |
Mean FF weight after adaptive search + 10 % safety margin. |
σ_FF |
Std of FF weights. If the FF budget is exhausted the floor 0.1 · µ_W is applied. |
σ_RC |
Std of RC weights (zero-mean). Printed only for recurrent layers. |
µ_U |
Resulting mean membrane potential = n_F · µ_W · ν_in · ε̄. |
Warnings
xi_target below 1.0
xi_target < 1.0 triggers a UserWarning. Very small ξ places the mean membrane
extremely close to threshold, which can cause very high firing rates and unstable
training. Recommended: 1 ≤ ξ ≤ 3.
Single-input layers — exhausted FF variance budget (n_F = 1)
Low fan-in layers may need a large µ_W just to keep all neurons active, exhausting
the FF variance budget. fluct_init detects this and applies a symmetry-breaking
floor σ_FF = 0.1 · µ_W instead of leaving all FF weights identical (which would
stall learning). The warning message calls this init mean-driven and suggests
using a smaller ξ to widen the budget.
Hardware weight range
After init, weights are checked against the hardware limits for the detected family:
H1v1: [−0.9, 0.9] · H1v2: [−1.66, 1.66]. Out-of-range weights emit a UserWarning
and will also be flagged by is_net_deployable(). Add weight_magnitude_loss(model)
to your training loss to softly enforce the constraint during training.
Dead neurons after init
If dead neurons remain after the adaptive search and symmetry-breaking floor,
fluct_init emits a warning with the layer index and count. Mitigations: smaller
xi_target, lower alpha, or more input neurons (increase n_F).
Supported architectures
| Feature | Supported |
|---|---|
| H1v1 (H1v1Synapse + H1v1Layer) | ✓ |
| H1v2 (H1v2Synapse + H1v2Layer) | ✓ |
| Mixed H1v1/H1v2 in one model | ✗ — raises ValueError |
| FF topology | ✓ |
| RC topology | ✓ |
| Heterogeneous τ per layer | ✓ |
| Analog inputs | ✓ |
| Binary spike inputs | ✓ |
| Multi-layer stacked init | ✓ — output ν propagated layer-to-layer |
| Frontend-first (H1v1/H1v2Frontend → H1v1Layer/H1v2Layer + dense pairs) | ✓ — frontend stage skipped, its output ν used as input for first dense pair |
| Frontend-only (no dense pairs after frontend) | ✗ — raises ValueError |
| LIFSynapse / LIFLayer | ✗ — use manual init for LIF networks |
Layer pair discovery
fluct_init finds dense (Synapse, Layer) pairs in two ways:
- Explicit
layer_pairsattribute (recommended for RC nets and complex architectures):python self.layer_pairs = [(self.syn1, self.lyr1), (self.syn2, self.lyr2)] - Auto-discovery: walks
model.named_modules()in registration order and collects consecutive(H1v1Synapse|H1v2Synapse, H1v1Layer|H1v2Layer)pairs.
For frontend-first models, expose frontend_stage = (frontend, frontend_layer) for
explicit detection, or register the Frontend immediately before its H1v1Layer/H1v2Layer so
auto-detection works.