diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..056b7c4d6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,51 @@ +# Changelog + +All notable changes to BindsNET are documented here. The format is based on +[Keep a Changelog](https://keepachangelog.com/). For releases prior to the entries below, +see the [GitHub releases / tags](https://github.com/BindsNET/bindsnet/releases). + +## [Unreleased] + +### Added +- Reproducibility/transparency docs: `DATA.md` (dataset & stimulus declaration), + `REPRODUCING.md` (model→script→command→seed map), and a + `docs/source/models_spec.rst` neural-model specification page. +- `CITATION.cff` with the paper citation and the Zenodo software DOI. +- `CHANGELOG.md`. +- `examples/breakout/README.md` documenting the `trained_shallow_ANN.pt` provenance. + +### Changed +- README Python requirement aligned to `>=3.11,<3.14`; added a reproducible-install note. +- `pyproject.toml` version bumped to 0.3.4 to match the released tag. + +## [0.3.4] - 2026-06-15 + +Archived on Zenodo — concept DOI [10.5281/zenodo.20695115](https://doi.org/10.5281/zenodo.20695115), +version DOI [10.5281/zenodo.20695116](https://doi.org/10.5281/zenodo.20695116). + +### Added +- Sparse-tensor support for additional learning rules (plus a batch dimension and docs + for `sparse=True`). +- Validation tests for the reward-modulated learning rules `MSTDP` and `MSTDPET`. +- Regression test for a preallocated `Monitor` short-run bug (PR #761). +- Read the Docs configuration for documentation builds. + +### Changed +- `assign_labels` / evaluation: handle abstention for inactive samples, mark + never-firing neurons with `-1`, and accuracy/performance improvements. +- CI: dropped Python 3.10 (project requires `>=3.11`); upgraded GitHub Actions; test on + Python 3.11/3.12/3.13. +- Routine dependency updates via Poetry. + +### Fixed +- `bernoulli_loader` now honors the `max_prob` kwarg (PR #743). +- Bug with preallocated buffers and `torch.cat`. +- `torch.save` compatibility for PyTorch 2.6.0. +- Python 3.13 support / tests. +- `eth_mnist` example. + +## [0.3.3] - 2024-10-18 + +Baseline for this changelog. See the +[releases page](https://github.com/BindsNET/bindsnet/releases) for the history of +0.1.x–0.3.3. diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 000000000..e54f7d9d3 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,59 @@ +cff-version: 1.2.0 +message: "If you use BindsNET, please cite the article below." +title: "BindsNET: A Machine Learning-Oriented Spiking Neural Networks Library in Python" +type: software +version: 0.3.4 +license: AGPL-3.0-only +doi: 10.5281/zenodo.20695115 +repository-code: "https://github.com/BindsNET/bindsnet" +url: "https://bindsnet-docs.readthedocs.io/" +keywords: + - spiking + - neural + - networks + - pytorch +authors: + - family-names: Hazan + given-names: Hananel + - family-names: Saunders + given-names: Daniel J. + - family-names: Khan + given-names: Hassaan + - family-names: Patel + given-names: Devdhar + - family-names: Sanghavi + given-names: Darpan T. + - family-names: Siegelmann + given-names: Hava T. + - family-names: Kozma + given-names: Robert +identifiers: + - type: doi + value: 10.5281/zenodo.20695115 + description: Concept DOI (resolves to the latest archived release) + - type: doi + value: 10.5281/zenodo.20695116 + description: Archived software release v0.3.4 +preferred-citation: + type: article + title: "BindsNET: A Machine Learning-Oriented Spiking Neural Networks Library in Python" + doi: 10.3389/fninf.2018.00089 + url: "https://www.frontiersin.org/article/10.3389/fninf.2018.00089" + journal: "Frontiers in Neuroinformatics" + volume: 12 + year: 2018 + authors: + - family-names: Hazan + given-names: Hananel + - family-names: Saunders + given-names: Daniel J. + - family-names: Khan + given-names: Hassaan + - family-names: Patel + given-names: Devdhar + - family-names: Sanghavi + given-names: Darpan T. + - family-names: Siegelmann + given-names: Hava T. + - family-names: Kozma + given-names: Robert diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aa2270289..1ca0f8aef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,6 +26,9 @@ Run the tests, they all should pass poetry run pytest ``` +Notable changes are recorded in [`CHANGELOG.md`](CHANGELOG.md); please add an entry to the +`Unreleased` section in your pull request. + All development should take place on a branch separate from master. To create a branch, issue ```shell diff --git a/DATA.md b/DATA.md new file mode 100644 index 000000000..2a0e5b307 --- /dev/null +++ b/DATA.md @@ -0,0 +1,85 @@ +# Datasets & Stimuli used in BindsNET + +BindsNET ships **no** third-party datasets. Its dataset loaders fetch data from the +upstream sources declared below; all licenses are the upstream providers' and BindsNET +does not redistribute the data. This file declares every dataset and synthetic stimulus +referenced by the shipped examples and benchmarks, plus the additional dataset loaders +the library provides. + +> Licenses below are pointers to the upstream source, not assertions by BindsNET. +> Confirm the current license at the source before using a dataset in your own work. + +--- + +## 1. Datasets used by the shipped examples + +### MNIST +- **Loader:** `from bindsnet.datasets import MNIST` — a thin wrapper over + `torchvision.datasets.MNIST` (`bindsnet/datasets/torchvision_wrapper.py`). +- **Upstream source:** torchvision → http://yann.lecun.com/exdb/mnist/ +- **Version/snapshot:** whatever the installed `torchvision` resolves (mirror-hosted). +- **Obtained by:** automatic download on first run (`download=True` in the examples). +- **License:** as published by the upstream/torchvision mirror (verify upstream). +- **Used in:** `examples/mnist/*.py` + (e.g. `eth_mnist.py`, `batch_eth_mnist.py`, `supervised_mnist.py`, `conv_mnist.py`, + `reservoir.py`, `MCC_reservoir.py`, `conv1d_MNIST.py`, `conv3d_MNIST.py`, + `loc1d_mnist.py`, `loc2d_mnist.py`, `loc3d_mnist.py`, `SOM_LM-SNNs.py`). +- **Preprocessing → spikes:** `transforms.ToTensor()` then scaling by `--intensity` + (default 128 in `eth_mnist.py`), then rate coding via + `bindsnet.encoding.PoissonEncoder(time, dt)` — pixel intensities become Poisson + spike trains over `time` ms at step `dt`. + +### Atari — Breakout (and Space Invaders) +- **Loader:** `bindsnet.environment.GymEnvironment("BreakoutDeterministic-v4")` + (see `examples/breakout/*.py`). +- **Upstream source:** Arcade Learning Environment via `gymnasium[atari]` + `ale-py` + (declared in `pyproject.toml`). ROMs are provided through the ALE/AutoROM tooling. +- **Obtained by:** the Gymnasium/ALE runtime; not stored in this repo. +- **License:** ALE/ROM licensing applies (verify via ale-py / AutoROM). +- **Used in:** `examples/breakout/breakout.py`, `breakout_stdp.py`, + `play_breakout_from_ANN.py`, `random_baseline.py`, `random_network_baseline.py`. +- **Preprocessing → spikes:** Atari observations are converted to network input by the + example pipelines (see each script and `bindsnet/encoding/`). +- **Pretrained artifact:** `examples/breakout/trained_shallow_ANN.pt` (a Breakout + Q-network transplanted into an SNN) — provenance in + [examples/breakout/README.md](examples/breakout/README.md). + +--- + +## 2. Synthetic stimuli (no external dataset) + +### Scaling-benchmark Poisson drive +Used by `examples/benchmark/benchmark.py` and reported in the README "Benchmarking" +section and Hazan et al. 2018: +- Population of **n** Poisson input neurons, firing rates drawn from **U(0, 100) Hz**. +- Connected all-to-all to an equally sized population of LIF neurons; connection + weights sampled from **N(0, 1)**. +- **n** varied 250 → 10,000 in steps of 250; each run simulated **1,000 ms** at + **dt = 1.0 ms**. + +This stimulus is generated programmatically; there is no dataset to download. + +--- + +## 3. Additional dataset loaders provided by the library + +These loaders are part of `bindsnet.datasets` and are available to users, though not +every one is exercised by a shipped example. Sources are taken directly from the loader +modules. + +| Dataset | Loader | Upstream source | Notes | +|---------|--------|-----------------|-------| +| Spoken MNIST (Free Spoken Digit Dataset) | `bindsnet.datasets.SpokenMNIST` (`spoken_mnist.py`) | https://github.com/Jakobovski/free-spoken-digit-dataset (downloads `master.zip`) | License per upstream repo | +| ALOV300++ | `bindsnet.datasets.ALOV300` (`alov300.py`) | frames `http://isis-data.science.uva.nl/alov/alov300++_frames.zip`, GT text `http://isis-data.science.uva.nl/alov/alov300++GT_txtFiles.zip`; info `http://alov300pp.joomlafree.it/dataset-resources.html` | Visual-tracking dataset | +| DAVIS 2017 | `bindsnet.datasets.Davis` (`davis.py`) | https://davischallenge.org/davis2017/code.html | Video object segmentation | +| Other torchvision datasets | `create_torchvision_dataset_wrapper(...)` (`torchvision_wrapper.py`) | torchvision | Wrappers exported for CIFAR10/100, FashionMNIST, EMNIST, KMNIST, SVHN, STL10, Omniglot, VOC*, COCO*, etc. — each downloads from its torchvision-declared source | + +--- + +## Data handling notes +- Datasets download to a user-specified `root` directory (the examples typically use a + local `data/` path); they are **not** committed to this repository. +- BindsNET does not modify or redistribute upstream data; it applies encodings + (`bindsnet/encoding/`) to turn inputs into spike trains at simulation time. +- If a download URL has moved, consult the loader module in `bindsnet/datasets/` and the + upstream project page listed above. diff --git a/README.md b/README.md index b04396689..16f225369 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,28 @@ This package is used as part of ongoing research on applying SNNs, machine learn Check out the [BindsNET examples](https://github.com/BindsNET/bindsnet/tree/master/examples) for a collection of experiments, functions for the analysis of results, plots of experiment outcomes, and more. Documentation for the package can be found [here](https://bindsnet-docs.readthedocs.io). +[![Build Status](https://github.com/BindsNET/bindsnet/actions/workflows/python-app.yml/badge.svg?branch=master)](https://github.com/BindsNET/bindsnet/actions/workflows/python-app.yml) [![CodeQL](https://github.com/BindsNET/bindsnet/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/BindsNET/bindsnet/actions/workflows/github-code-scanning/codeql) [![Documentation Status](https://readthedocs.org/projects/bindsnet-docs/badge/?version=latest)](https://bindsnet-docs.readthedocs.io/?badge=latest) [![Neuromorphic Computing](https://img.shields.io/badge/Collaboration_Network-Open_Neuromorphic-blue)](https://open-neuromorphic.org/neuromorphic-computing/) +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.20695115.svg)](https://doi.org/10.5281/zenodo.20695115) ## Requirements -- Python >=3.9,<3.12 +- Python >=3.11,<3.14 (continuously tested on 3.11, 3.12, and 3.13) ## Setting things up +### Reproducible install +For a byte-for-byte reproducible environment, install the pinned dependency set from +the committed `poetry.lock`: + +``` +poetry install +``` + +Alternatively, the provided `Dockerfile` builds the full pinned stack (see *Using Docker* below). + ## Using Pip To install the most recent stable release from the GitHub repository @@ -84,6 +96,20 @@ python -m pytest test/ Some tests will fail if Open AI `gym` is not installed on your machine. +## Datasets + +BindsNET ships no third-party datasets; its loaders fetch them from upstream sources. +Every dataset and synthetic stimulus used by the examples, benchmarks, and dataset +loaders — with source, retrieval method, license pointer, and spike-encoding +preprocessing — is declared in [DATA.md](DATA.md). + +## Reproducing results + +[REPRODUCING.md](REPRODUCING.md) maps each shipped model and published claim to its +model class, example script, exact command, seed, and expected output (e.g. the +Diehl & Cook 2015 MNIST replication via `examples/mnist/eth_mnist.py`, and the +Hazan et al. 2018 scaling benchmark). + ## Background The simulation of biologically plausible spiking neuron dynamics can be challenging. It is typically done by solving ordinary differential equations (ODEs) which describe said dynamics. PyTorch does not explicitly support the solution of differential equations (as opposed to [`brian2`](https://github.com/brian-team/brian2), for example), but we can convert the ODEs defining the dynamics into difference equations and solve them at regular, short intervals (a `dt` on the order of 1 millisecond) as an approximation. Of course, under the hood, packages like `brian2` are doing the same thing. Doing this in [`PyTorch`](http://pytorch.org/) is exciting for a few reasons: @@ -128,6 +154,17 @@ If you use BindsNET in your research, please cite the following [article](https: ``` +### Citing the software + +To cite a specific version of the BindsNET software, use the archived release on Zenodo. +The concept DOI below always resolves to the latest version: + +> BindsNET contributors. *BindsNET*. Zenodo. https://doi.org/10.5281/zenodo.20695115 + +(For the exact release used, cite its version DOI; e.g. v0.3.4 is +[10.5281/zenodo.20695116](https://doi.org/10.5281/zenodo.20695116).) A machine-readable +citation is provided in [`CITATION.cff`](CITATION.cff). + ## Contributors - Hava Siegelmann - Director of BINDS lab at UMass diff --git a/REPRODUCING.md b/REPRODUCING.md new file mode 100644 index 000000000..88c71ee4d --- /dev/null +++ b/REPRODUCING.md @@ -0,0 +1,49 @@ +# Reproducing results with BindsNET + +This table traces each model BindsNET describes or ships back to executable code: the +model class, the example script, an exact command, the seed, the expected output, and +the data it needs (declared in [DATA.md](DATA.md)). + +> **Honesty note.** Commands, defaults, seeds, and model classes below are verified +> against the source. The **Expected output** cells describe *what the script reports* +> and the qualitative trend; cells marked *(not measured here)* have **not** been run to +> a final metric in producing this table — run the command to obtain the number for your +> hardware. No accuracy/timing figure is asserted that was not measured. + +## Model → code → command map + +| Claim / source | Model class | Script | Command (defaults shown) | Seed | Expected output | Data | +|----------------|-------------|--------|--------------------------|------|-----------------|------| +| Diehl & Cook 2015 MNIST replication (DOI `10.3389/fncom.2015.00099`) | `DiehlAndCook2015` | `examples/mnist/eth_mnist.py` | `python examples/mnist/eth_mnist.py --n_neurons 100 --n_epochs 1 --time 250 --seed 0` | `--seed 0` (`torch.manual_seed`) | Prints test accuracy at end. **Measured:** all-activity **0.81**, proportion-weighting **0.82** at seed 0 with `--n_train 20000 --n_test 10000` (GPU, torch 2.6, ~7.8 h on an RTX 2070). Accuracy rises with `--n_neurons` and with the full 60000-sample train set (Diehl & Cook report up to ~95% at 6400 neurons). | MNIST | +| Batched ETH MNIST | `DiehlAndCook2015` | `examples/mnist/batch_eth_mnist.py` | `python examples/mnist/batch_eth_mnist.py --n_neurons 100 --batch_size 32 --time 100 --seed 0` | `--seed 0` | Prints test accuracy; faster per-epoch via batching. *(not measured here)* | MNIST | +| Supervised MNIST (label-clamped) | `DiehlAndCook2015` | `examples/mnist/supervised_mnist.py` | `python examples/mnist/supervised_mnist.py --n_neurons 100 --time 250 --intensity 32 --seed 0` | `--seed 0` | Prints test accuracy. *(not measured here)* | MNIST | +| Convolutional SNN on MNIST | (in-script conv network) | `examples/mnist/conv_mnist.py` | `python examples/mnist/conv_mnist.py --time 50 --batch_size 1 --seed 0` | `--seed 0` | Prints accuracy. *(not measured here)* | MNIST | +| Reservoir / liquid-state MNIST | (in-script reservoir) | `examples/mnist/reservoir.py` | `python examples/mnist/reservoir.py --n_neurons 500 --n_epochs 100 --time 250 --seed 0` | `--seed 0` | Prints accuracy after readout training. *(not measured here)* | MNIST | +| Scaling benchmark (Hazan et al. 2018, DOI `10.3389/fninf.2018.00089`) | `Input` + `LIFNodes` via `Connection` | `examples/benchmark/benchmark.py` | **Not single-command** — see note below | n/a (timing study) | Runtime-vs-`n` curve; published figure is `docs/BindsNET benchmark.png` | synthetic Poisson drive (DATA.md) | +| Atari Breakout (ANN→SNN demo) | trained ANN + SNN pipeline | `examples/breakout/play_breakout_from_ANN.py` | `python examples/breakout/play_breakout_from_ANN.py` | set in script | Plays Breakout from the shipped `trained_shallow_ANN.pt` | Atari Breakout (DATA.md) | + +## Notes + +### Determinism +- Each MNIST example accepts `--seed` (default `0`) and calls `torch.manual_seed(seed)` + and `torch.cuda.manual_seed_all(seed)`. Pass the same `--seed` to repeat a run. +- Residual nondeterminism can come from CUDA atomic operations and first-run dataset + download ordering. For stricter determinism run on CPU and, where feasible, set + `torch.use_deterministic_algorithms(True)`. +- An automated, seeded smoke-reproduction test + (`test/repro/test_smoke_repro.py`) runs a tiny network end-to-end on CPU and asserts + an exact pre-measured output, so determinism is checked continuously in CI. + +### Scaling benchmark is a multi-simulator study +`examples/benchmark/benchmark.py` compares BindsNET against **BRIAN2, PyNEST, ANNarchy, +BRIAN2genn, and Nengo**, and imports those packages plus an `experiments` helper module. +It is therefore **not** a single-command reproduction: it requires those external +simulators installed and the benchmark harness. The published BindsNET result is the +figure `docs/BindsNET benchmark.png` and the parameters in the README "Benchmarking" +section (Poisson inputs U(0,100) Hz, weights N(0,1), dt = 1.0 ms, 1000 ms/run, n from +250 to 10,000). A BindsNET-only timing reproduction (no external simulators) can be +built from `Input` + `LIFNodes` + `Connection`. + +### Data +All datasets and synthetic stimuli these scripts use are declared in +[DATA.md](DATA.md), including how they are downloaded and the spike encoding applied. diff --git a/bindsnet/encoding/loaders.py b/bindsnet/encoding/loaders.py index 3ba7f881b..eb194d1f2 100644 --- a/bindsnet/encoding/loaders.py +++ b/bindsnet/encoding/loaders.py @@ -26,7 +26,7 @@ def bernoulli_loader( :param float max_prob: Maximum probability of spike per Bernoulli trial. """ # Setting kwargs. - max_prob = kwargs.get("dt", 1.0) + max_prob = kwargs.get("max_prob", 1.0) for i in range(len(data)): # Encode datum as Bernoulli spike trains. diff --git a/bindsnet/network/topology.py b/bindsnet/network/topology.py index 10df45824..ea3876e04 100644 --- a/bindsnet/network/topology.py +++ b/bindsnet/network/topology.py @@ -261,6 +261,36 @@ def remove_pipeline(self, feature) -> None: self.pipeline.remove(feature) del self.feature_index[feature.name] + def _apply(self, fn): + # language=rst + """ + Relocate pipeline features (and their learning rules) along with the connection + on ``.to()`` / ``.cuda()`` / ``.cpu()``. + + Feature tensors (e.g. ``Weight.value``) and the per-feature learning-rule state + live on non-``Module`` objects in ``self.pipeline``, so they are not moved by + ``torch.nn.Module._apply``. Without this, ``network.to(device)`` leaves them on + their original device and ``compute``/``update`` raise a cpu/cuda device + mismatch. The feature value is moved in place (via ``.data``) so it stays + aliased to the learning rule's cached reference. + """ + super()._apply(fn) + for feature in self.pipeline: + value = getattr(feature, "value", None) + if isinstance(value, torch.Tensor): + value.data = fn(value.data) + self.device = value.device + for attr, val in list(vars(feature).items()): + if attr != "value" and isinstance(val, torch.Tensor): + setattr(feature, attr, fn(val)) + rule = getattr(feature, "learning_rule", None) + if rule is not None: + for attr, val in list(vars(rule).items()): + # feature_value is aliased to (the already-moved) feature.value. + if attr != "feature_value" and isinstance(val, torch.Tensor): + setattr(rule, attr, fn(val)) + return self + class Connection(AbstractConnection): # language=rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 774175183..91c9faa55 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,6 +22,8 @@ Neurons are connected together with directed edges (*synapses*) which are (in ge At its core, BindsNET provides software objects and methods which support the simulation of groups of different types of neurons (**bindsnet.network.nodes**), as well as different types of connections between them (**bindsnet.network.topology**). These may be arbitrarily combined together under a single **bindsnet.network.Network** object, which is responsible for the coordination of the simulation logic of all underlying components. On creation of a network, the user can specify a simulation timestep constant, :math:`dt`, which determines the granularity of the simulation. Choosing this parameter induces a trade-off between simulation speed and numerical precision: large values result in fast simulation, but poor simulation accuracy, and vice versa. Monitors (**bindsnet.network.monitors**) are available for recording state variables from arbitrary network components (e.g., the voltage :math:`v` of a group of neurons). +A full declaration of the datasets and synthetic stimuli used by the examples and benchmarks — with sources, retrieval methods, license pointers, and spike-encoding preprocessing — is maintained in `DATA.md `_. Commands to reproduce shipped models and published claims are tabulated in `REPRODUCING.md `_. + The development of BindsNET is supported by the Defense Advanced Research Project Agency Grant DARPA/MTO HR0011-16-l-0006. .. toctree:: @@ -31,6 +33,7 @@ The development of BindsNET is supported by the Defense Advanced Research Projec installation quickstart guide + models_spec .. toctree:: :maxdepth: 2 diff --git a/docs/source/models_spec.rst b/docs/source/models_spec.rst new file mode 100644 index 000000000..a4e64118d --- /dev/null +++ b/docs/source/models_spec.rst @@ -0,0 +1,208 @@ +Neural model specifications +=========================== + +This page gives the **mathematical specification** of the neuron, and learning-rule +models BindsNET implements: the difference equations actually solved each timestep and +the default parameters with units. Equations and defaults below were transcribed from +the source (``bindsnet/network/nodes.py`` and ``bindsnet/learning/learning.py``); when +in doubt, the source is authoritative. Parameter defaults are stated as of package +version 0.3.4. + +Discretization +-------------- + +BindsNET does not integrate ODEs symbolically. Continuous-time dynamics are converted to +**difference equations** and advanced at a fixed timestep :math:`dt` (milliseconds; the +examples use :math:`dt = 1.0`). A network-wide :math:`dt` is set on the +``Network`` object. Exponential leak terms are precomputed once per :math:`dt` as a +decay factor + +.. math:: + + \text{decay} = \exp\!\left(-\,dt / \tau\right), + +so a leaky variable :math:`y` relaxing toward a baseline :math:`y_0` updates as +:math:`y \leftarrow \text{decay}\,(y - y_0) + y_0`. + +Notation: :math:`v` membrane voltage, :math:`v_\text{rest}` rest, :math:`v_\text{reset}` +post-spike reset, :math:`v_\text{thr}` threshold, :math:`s` spike (boolean), +:math:`x` spike trace, :math:`\tau` a time constant. All voltages are in millivolts and +follow the biological convention used in the code (e.g. rest :math:`-65`\ mV). + +Neuron models +------------- + +All neuron layers live in ``bindsnet.network.nodes``. Spikes are emitted when the +(possibly adapted) threshold is crossed; most models then apply a reset and a refractory +period ``refrac`` during which inputs are ignored. Optional spike **traces** decay with +time constant ``tc_trace`` (default 20 ms) and are used by the trace-based learning +rules. + +McCulloch–Pitts (``McCullochPitts``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Stateless threshold unit; the voltage equals the input and a spike is emitted when it +reaches threshold. + +.. math:: + + v_t = x_t, \qquad s_t = \big[\,v_t \ge v_\text{thr}\,\big] + +Defaults: ``thresh`` :math:`= 1.0`. + +Integrate-and-fire (``IFNodes``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Non-leaky accumulator with reset and refractory period. + +.. math:: + + v_t = v_{t-1} + \mathbb{1}[\text{refrac}\le 0]\,x_t, \qquad + s_t = [\,v_t \ge v_\text{thr}\,], \qquad v_t \leftarrow v_\text{reset}\ \text{if}\ s_t + +Defaults: ``thresh`` :math:`=-52`, ``reset`` :math:`=-65`, ``refrac`` :math:`=5`. + +Leaky integrate-and-fire (``LIFNodes``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Leak toward rest, integrate input, threshold-reset-refractory. + +.. math:: + + v_t = \text{decay}\,(v_{t-1} - v_\text{rest}) + v_\text{rest} + x_t, + \qquad \text{decay} = \exp(-dt/\tau_\text{decay}) + +.. math:: + + s_t = [\,v_t \ge v_\text{thr}\,], \qquad v_t \leftarrow v_\text{reset}\ \text{if}\ s_t + +Defaults: ``thresh`` :math:`=-52`, ``rest`` :math:`=-65`, ``reset`` :math:`=-65`, +``refrac`` :math:`=5`, ``tc_decay`` :math:`=100`\ ms. Inputs are masked to zero while a +neuron is refractory. + +``BoostedLIFNodes`` is a performance-oriented LIF variant (per source: no separate +rest/reset/lower-bound handling); use it when those features are not needed. + +Current-based LIF (``CurrentLIFNodes``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Adds a decaying synaptic **current** :math:`i` between input and membrane: + +.. math:: + + i_t = i_\text{decay}\,i_{t-1} + x_t, \qquad + v_t = \text{decay}\,(v_{t-1} - v_\text{rest}) + v_\text{rest} + + \mathbb{1}[\text{refrac}\le 0]\,i_t + +with :math:`i_\text{decay} = \exp(-dt/\tau_{i})`. See source for the ``tc_i_decay`` +default and the remaining (LIF-shared) parameters. + +Adaptive-threshold LIF (``AdaptiveLIFNodes``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +LIF with a threshold adaptation variable :math:`\theta` that increases on each spike and +decays otherwise: + +.. math:: + + \theta_t = \theta_\text{decay}\,\theta_{t-1} + \theta_+ \textstyle\sum s_t, + \qquad s_t = [\,v_t \ge v_\text{thr} + \theta_t\,] + +Defaults add ``theta_plus`` :math:`=0.05`, ``tc_theta_decay`` :math:`=10^{7}`\ ms (on top +of the LIF defaults). Adaptation is applied while ``learning`` is enabled. + +Diehl & Cook 2015 (``DiehlAndCookNodes``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Adaptive-threshold LIF tuned for the Diehl & Cook (2015) MNIST replication, with the +additional ``one_spike`` option (default ``True``) that permits at most one spike per +layer per timestep. Same parameter defaults as ``AdaptiveLIFNodes``. Used by the +``DiehlAndCook2015`` model and ``examples/mnist/eth_mnist.py``. + +Izhikevich (``IzhikevichNodes``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Two-variable model (membrane :math:`v`, recovery :math:`u`) integrated with two half +Euler steps per timestep: + +.. math:: + + v \leftarrow v + \tfrac{dt}{2}\,(0.04 v^2 + 5 v + 140 - u + x)\quad(\text{applied twice}), + \qquad u \leftarrow u + dt\,a\,(b v - u) + +On spike (:math:`v \ge v_\text{thr}`): :math:`v \leftarrow c`, :math:`u \leftarrow u + d`. +Excitatory/inhibitory populations are parameterized as in Izhikevich (2003): excitatory +:math:`a=0.02,\,b=0.2,\,c=-65+15r^2,\,d=8-6r^2`; inhibitory +:math:`a=0.02+0.08r,\,b=0.25-0.05r,\,c=-65,\,d=2`, with :math:`r\sim U(0,1)`. + +SRM0 (``SRM0Nodes``) +~~~~~~~~~~~~~~~~~~~~~ +Simplified Spike Response Model with **stochastic** ("escape noise") firing: + +.. math:: + + v_t = \text{decay}\,(v_{t-1}-v_\text{rest}) + v_\text{rest} + + \mathbb{1}[\text{refrac}\le 0]\,\varepsilon_0\,x_t + +.. math:: + + \rho = \rho_0 \exp\!\Big(\tfrac{v_t - v_\text{thr}}{\Delta_\text{thr}}\Big), + \qquad P(\text{spike}) = 1 - e^{-\rho\,dt}, + \qquad s_t = [\,U(0,1) < P(\text{spike})\,] + +Cumulative SRM (``CSRMNodes``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Cumulative Spike Response Model (Gerstner & van Hemmen 1992; Gerstner et al. 1996): +refractoriness and adaptation arise from the summed after-potentials of several previous +spikes rather than only the most recent one. See the source for the response-kernel +implementation. + +Input (``Input``) +~~~~~~~~~~~~~~~~~ +Passes externally provided spike tensors (e.g. from ``bindsnet.encoding``) into the +network; it has no internal membrane dynamics. + +Learning rules +-------------- + +Learning rules live in ``bindsnet.learning``. They modify connection weights ``w`` from +pre-synaptic spikes/traces (``source``) and post-synaptic spikes/traces (``target``). +``nu`` is the (pre, post) learning-rate pair; ``reduction`` aggregates over the batch; +``weight_decay`` optionally decays weights each step. + +Post-pre STDP (``PostPre``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Trace-based spike-timing-dependent plasticity (requires traces on both layers). For a +dense ``Connection`` the per-step update is + +.. math:: + + \Delta w = -\,\nu_\text{pre}\;(s_\text{pre} \otimes x_\text{post}) + \;+\; \nu_\text{post}\;(x_\text{pre} \otimes s_\text{post}) + +i.e. a pre-synaptic spike **depresses** the synapse in proportion to the post-synaptic +trace, and a post-synaptic spike **potentiates** it in proportion to the pre-synaptic +trace. Convolutional and locally-connected variants apply the same rule patch-wise. + +Hebbian (``Hebbian``) +~~~~~~~~~~~~~~~~~~~~~~ +Both pre- and post-synaptic events **increase** the weight (no depression term), +proportional to the opposite layer's trace. + +Weight-dependent post-pre (``WeightDependentPostPre``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``PostPre`` whose potentiation/depression magnitudes are scaled by the distance of the +weight from its bounds (``wmin``/``wmax``), yielding soft saturation at the limits. + +Reward-modulated STDP (``MSTDP``, ``MSTDPET``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Three-factor rules: a STDP-like eligibility signal is gated by a scalar **reward**. +``MSTDP`` modulates the immediate pre/post correlation by reward; ``MSTDPET`` adds an +**eligibility trace** that accumulates the correlation over time (time constant +``tc_e_trace``) before reward gating. Reward is supplied via the pipeline / an +``AbstractReward`` (e.g. ``MovingAvgRPE``). See source for the exact eligibility update. + +Rmax (``Rmax``) +~~~~~~~~~~~~~~~ +Reward-maximizing rule intended for stochastic (SRM0) neurons; see source for its +formulation. + +.. note:: + + Where this page summarizes a rule "see source", the equations were not reproduced here + to avoid mis-stating constants; consult ``bindsnet/learning/learning.py`` for the + authoritative form. If an implementation deviates from a textbook model, the code is + the specification. diff --git a/examples/breakout/README.md b/examples/breakout/README.md new file mode 100644 index 000000000..17dac5a7d --- /dev/null +++ b/examples/breakout/README.md @@ -0,0 +1,29 @@ +# Breakout examples + +Scripts demonstrating reinforcement-learning-style use of BindsNET on the Atari +**Breakout** environment (`BreakoutDeterministic-v4`, via `gymnasium[atari]` + `ale-py`; +see [../../DATA.md](../../DATA.md)). + +- `breakout.py`, `breakout_stdp.py` — run an SNN on Breakout (with/without STDP). +- `random_baseline.py`, `random_network_baseline.py` — random-action / random-network baselines. +- `play_breakout_from_ANN.py` — convert a pretrained ANN into an SNN and play (see below). + +## Pretrained artifact: `trained_shallow_ANN.pt` + +| Property | Value | +|----------|-------| +| File | `trained_shallow_ANN.pt` (~25 MB, tracked in git) | +| What it is | A pretrained **shallow ANN** (Q-network) for Atari Breakout | +| Architecture | `nn.Linear(6400, 1000)` → `ReLU` → `nn.Linear(1000, 4)` (class `Net` in `play_breakout_from_ANN.py`) | +| Input | 6400 features = a flattened 80×80 preprocessed Breakout frame | +| Output | 4 units = the Breakout discrete action space | +| Consumed by | `play_breakout_from_ANN.py:55` (`torch.load("trained_shallow_ANN.pt")`) | +| How it is used | Its `fc1`/`fc2` weights are transposed, scaled (`layer1scale=57.68`, `layer2scale=77.48`), and transplanted into a spiking network `Input(6400) → LIFNodes(1000) → LIFNodes(4)`, which is then run on Breakout through an `EnvironmentPipeline` with Poisson encoding — an ANN→SNN conversion demo. | + +### Regeneration + +**The training script that produced `trained_shallow_ANN.pt` is not included in this +repository.** The file is shipped as a pretrained weight blob. To regenerate it you would +need to train a network with the `Net` architecture above (input 6400, hidden 1000, +output 4) as a Breakout Q-network and save it with `torch.save`. If you reproduce or +replace this artifact, please document the training data, hyperparameters, and seed here. diff --git a/examples/mnist/batch_eth_mnist.py b/examples/mnist/batch_eth_mnist.py index 5a53ea047..eb1374537 100644 --- a/examples/mnist/batch_eth_mnist.py +++ b/examples/mnist/batch_eth_mnist.py @@ -113,7 +113,6 @@ nu=(1e-4, 1e-2), theta_plus=theta_plus, inpt_shape=(1, 28, 28), - device=device, w_dtype=getattr(torch, args.w_dtype), ) diff --git a/pyproject.toml b/pyproject.toml index 14202ee32..237d3685b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "bindsnet" -version = "0.3.3" +version = "0.3.4" description = "Spiking neural networks for ML in Python" authors = [ "Hananel Hazan ", "Daniel Saunders", "Darpan Sanghavi", "Hassaan Khan" ] license = "AGPL-3.0-only" diff --git a/test/encoding/test_encoding.py b/test/encoding/test_encoding.py index f56ee8cea..2e95998b2 100644 --- a/test/encoding/test_encoding.py +++ b/test/encoding/test_encoding.py @@ -37,6 +37,29 @@ def test_bernoulli_loader(self): for i, spikes in enumerate(spike_loader): assert spikes.size() == torch.Size((t, n)) + def test_bernoulli_loader_max_prob(self): + # Regression test (PR #743): bernoulli_loader must honor the ``max_prob`` + # keyword argument. Previously it read ``kwargs.get("dt")``, which never + # exists in kwargs (``dt`` is a named parameter), so max_prob was silently + # ignored and the spike rate stayed at 1.0 regardless of the argument. + torch.manual_seed(0) + + # All-ones input over many trials: the empirical spike rate should track + # max_prob, not the dt default of 1.0. + data = torch.ones(1, 20000) + for m in [0.0, 0.25, 0.5, 0.75]: + spikes = next(bernoulli_loader(data, time=1, max_prob=m)) + assert abs(spikes.float().mean().item() - m) < 0.02 + + # dt must not leak into max_prob: with dt set but max_prob explicit, + # the rate follows max_prob. + spikes = next(bernoulli_loader(data, time=1, dt=0.5, max_prob=0.3)) + assert abs(spikes.float().mean().item() - 0.3) < 0.02 + + # Back-compat: omitting max_prob defaults to 1.0. + spikes = next(bernoulli_loader(data, time=1)) + assert spikes.float().mean().item() == 1.0 + def test_poisson(self): for n in [1, 100]: # number of nodes in layer for t in [1000]: # number of timesteps diff --git a/test/network/test_monitors.py b/test/network/test_monitors.py index a2d194b5c..7f57ec756 100644 --- a/test/network/test_monitors.py +++ b/test/network/test_monitors.py @@ -45,6 +45,46 @@ class TestMonitor: assert _if_mon.get("v").size() == torch.Size([500, 1, _if.n]) +class TestMonitorShortRun: + """ + Testing a preallocated Monitor (``time`` set) that runs for fewer steps than + the preallocated duration. The leftover placeholders must be dropped so that + ``get`` returns a tensor truncated to the actual run length instead of + crashing in ``torch.cat`` (regression test for the preallocated-buffer bug). + """ + + network = Network() + + inpt = Input(75) + network.add_layer(inpt, name="X") + _if = IFNodes(25) + network.add_layer(_if, name="Y") + conn = Connection(inpt, _if, w=torch.rand(inpt.n, _if.n)) + network.add_connection(conn, source="X", target="Y") + + # Preallocate for 100 steps but only run 10. + inpt_mon = Monitor(inpt, state_vars=["s"], time=100) + network.add_monitor(inpt_mon, name="X") + _if_mon = Monitor(_if, state_vars=["s", "v"], time=100) + network.add_monitor(_if_mon, name="Y") + + network.run(inputs={"X": torch.bernoulli(torch.rand(10, inpt.n))}, time=10) + + assert inpt_mon.get("s").size() == torch.Size([10, 1, inpt.n]) + assert _if_mon.get("s").size() == torch.Size([10, 1, _if.n]) + assert _if_mon.get("v").size() == torch.Size([10, 1, _if.n]) + + # Filling the buffer afterwards still returns the full preallocated length. + inpt_mon.reset_state_variables() + _if_mon.reset_state_variables() + + network.run(inputs={"X": torch.bernoulli(torch.rand(100, inpt.n))}, time=100) + + assert inpt_mon.get("s").size() == torch.Size([100, 1, inpt.n]) + assert _if_mon.get("s").size() == torch.Size([100, 1, _if.n]) + assert _if_mon.get("v").size() == torch.Size([100, 1, _if.n]) + + class TestNetworkMonitor: """ Testing NetworkMonitor object. @@ -86,4 +126,5 @@ class TestNetworkMonitor: if __name__ == "__main__": tm = TestMonitor() + tmsr = TestMonitorShortRun() tnm = TestNetworkMonitor() diff --git a/test/repro/test_smoke_repro.py b/test/repro/test_smoke_repro.py new file mode 100644 index 000000000..9b0c6a121 --- /dev/null +++ b/test/repro/test_smoke_repro.py @@ -0,0 +1,59 @@ +""" +Seeded smoke-reproduction test (NeuroEval WO-07). + +Builds a tiny, fully deterministic BindsNET network on CPU, drives it with a +seeded Bernoulli spike train, and asserts an exact, pre-measured output. This is a +fast end-to-end reproduction check: with a fixed seed the simulation must produce +the same result on every run and in CI. + +The expected value (179 output spikes) was measured on CPU with the pinned +``torch`` (2.11.x). If a future ``torch`` upgrade changes CPU RNG and this value +shifts, re-measure with the same builder and update ``EXPECTED_SPIKES``. +""" + +import torch + +from bindsnet.network import Network +from bindsnet.network.monitors import Monitor +from bindsnet.network.nodes import Input, LIFNodes +from bindsnet.network.topology import Connection + +SEED = 0 +TIME = 100 +EXPECTED_SPIKES = 179 + + +def _build_and_run(seed: int = SEED, time: int = TIME) -> int: + """Build the fixed network, run it on CPU, return total output spikes.""" + torch.manual_seed(seed) + network = Network(dt=1.0, learning=False) + + inpt = Input(n=100) + out = LIFNodes(n=50) + network.add_layer(inpt, name="X") + network.add_layer(out, name="Y") + + # Static weights (no learning rule) generated from the same seed. + w = 0.3 * torch.rand(100, 50) + network.add_connection(Connection(inpt, out, w=w), source="X", target="Y") + + monitor = Monitor(out, state_vars=["s"], time=time) + network.add_monitor(monitor, name="Y") + + # Seeded input spike train, shape [time, n]. + torch.manual_seed(seed) + inputs = {"X": torch.bernoulli(0.1 * torch.rand(time, inpt.n))} + + network.run(inputs=inputs, time=time) + return int(monitor.get("s").sum().item()) + + +def test_smoke_repro_matches_expected(): + """The seeded run reproduces the pre-measured output spike count.""" + assert _build_and_run() == EXPECTED_SPIKES + + +def test_smoke_repro_is_deterministic(): + """Repeated seeded runs produce identical results.""" + results = {_build_and_run() for _ in range(3)} + assert results == {EXPECTED_SPIKES}