Skip to main content

NeuralBayesianNetworks (NBN)

CI PyPI Python License Documentation

PyTorch-native Bayesian networks with neural conditional distributions and GPU-batched inference.

Documentation: https://nbn.readthedocs.io (user guide, example notebooks, API reference).

NBN represents a Bayesian network over a known DAG as an nn.Module. Each node holds a learnable conditional distribution P(X | parents(X)), called a mechanism: a probability table, a linear-Gaussian model, a mixture density network or a normalising flow. You fit the network from data, then answer conditional and interventional queries. Thousands of queries run as one batched tensor operation on CUDA, on Apple-Silicon GPUs (mps) or on the CPU.

  • One API for discrete, continuous and hybrid networks. Mixed discrete/continuous networks with non-Gaussian continuous nodes are a first-class case.
  • Swappable mechanisms. Every mechanism is an nn.Module behind the same interface, from closed-form tables to neural density estimators.
  • Batched inference. query_batch answers B queries with different evidence in one call: exact variable elimination for discrete networks, likelihood weighting elsewhere, picked automatically.
  • Causal queries. do= interventions work in every engine and can vary per batch row.
  • Autograd-friendly. Likelihoods and samples are differentiable, so NBN composes with your own PyTorch models.
  • Reproducible benchmarks. A suite (nbn-bench) compares NBN with pgmpy, pomegranate and Pyro on networks with known ground truth.

Contents

Installation

NBN needs Python ≥ 3.10 and PyTorch ≥ 2.2. The PyPI package is nbn, and so is the import name.

pip install nbn                  # core library
pip install "nbn[neural]"        # + normalising flows (zuko)
pip install "nbn[all]"           # + benchmark suite and every baseline library
Extra Adds
neural zuko, for NormalizingFlowMechanism / ConditionalFlowMechanism
bench the nbn-bench runner, pgmpy, pomegranate, pandas, plotting
mcmc the Pyro baseline for the benchmark suite
all everything a benchmark run needs
dev tests, linters and type checker (from a checkout)
docs Sphinx toolchain for the documentation site (from a checkout)

Using a virtual environment is recommended.

Linux

python3 -m venv .venv && source .venv/bin/activate
pip install -U pip
pip install "nbn[neural]"

For an NVIDIA GPU, install a CUDA build of PyTorch first by following the selector at pytorch.org. After that, pip install nbn keeps the build you have. Check it with:

python -c "import torch; print(torch.cuda.is_available())"   # True

macOS (Apple Silicon and Intel)

On Apple-Silicon Macs (M1 and later), NBN uses the GPU through PyTorch's Metal backend (mps). The standard PyPI PyTorch wheel includes it, so there is nothing extra to install and nothing to configure.

1. Get Python ≥ 3.10. macOS does not ship a usable one. Check with python3 --version, and if it is missing or older, install it with either:

brew install python@3.12        # Homebrew (https://brew.sh)

or the official installer from python.org. On macOS the command is python3 (Homebrew also provides python3.12), and there is no plain python until you activate a virtual environment.

2. Create a virtual environment and install NBN.

python3 -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install "nbn[neural]"        # or "nbn[all]" for the benchmark suite

3. Check that the GPU is visible.

python -c "import torch; print(torch.backends.mps.is_available())"   # True

With the bench extra, nbn-bench check-env checks the whole environment. It ends with a line like platform darwin; accelerator mps (Apple Metal; float64 unavailable, accumulations run in float32).

Intel Macs run NBN on the CPU. PyTorch stopped publishing Intel-macOS wheels after 2.2, so pip installs torch 2.2.x there. If you then see a NumPy ABI error, run pip install "numpy<2".

From source

git clone https://github.com/Giovannibriglia/NeuralBayesianNetworks.git
cd NeuralBayesianNetworks
python3 -m venv .venv && source .venv/bin/activate
pip install -U pip
pip install -e ".[dev,all]"
pytest -n auto                   # fast test suite (the default selection)

Quick start

import torch
from nbn import NeuralBayesianNetwork

# Rain → WetGrass ← Sprinkler, and WetGrass → SoilMoisture (continuous)
model = NeuralBayesianNetwork(
    [("Rain", "WetGrass"), ("Sprinkler", "WetGrass"), ("WetGrass", "SoilMoisture")],
    variables={
        "Rain": ("discrete", 2),
        "Sprinkler": ("discrete", 2),
        "WetGrass": ("discrete", 2),
        "SoilMoisture": ("continuous", 1),
    },
    device="auto",  # cuda > mps (Apple Silicon) > cpu
)

# Toy data from a known process
n = 10_000
rain = torch.bernoulli(torch.full((n,), 0.3)).long()
sprinkler = torch.bernoulli(torch.full((n,), 0.4)).long()
wet = ((rain | sprinkler).bool() & (torch.rand(n) < 0.9)).long()
soil = (2.0 * wet + 0.5 * torch.randn(n)).unsqueeze(-1)
data = {"Rain": rain, "Sprinkler": sprinkler, "WetGrass": wet, "SoilMoisture": soil}

model.auto_mechanisms()  # tables for discrete nodes, mixture density networks for continuous
model.fit(data)

# P(Rain | WetGrass = 1)                     ≈ [0.48, 0.52]
print(model.query(["Rain"], evidence={"WetGrass": 1}))

# 1,024 conditional queries in one batched call → shape [1024, 2]
evidence = {"WetGrass": torch.randint(0, 2, (1024,))}
print(model.query_batch(["Rain"], evidence).shape)

# Intervention: P(WetGrass | do(Sprinkler = 1)) ≈ [0.10, 0.90]
print(model.query(["WetGrass"], do={"Sprinkler": 1}))

# Ancestral samples and the (differentiable) log-likelihood of data
samples = model.sample(5)
print(model.log_prob(data).mean())

To choose a mechanism per node instead of auto_mechanisms(), call model.set_mechanism("SoilMoisture", LinearGaussianMechanism()) (all mechanisms import from nbn) before fit.

Examples. examples/ has one executed notebook with plots per feature, each checked against a known ground truth: parameter learning, inference engines, interventions, sampling, incremental updates, a gallery of the continuous mechanisms, batching / devices / persistence, and autograd (01–08), then structural causal models and probability sets (nbn.scm, nbn.uncertainty; 09–13). More end-to-end examples are in tests/integration/.

Core concepts

Network. NeuralBayesianNetwork(edges, variables, device=...) holds the DAG, the variable types (("discrete", cardinality) or ("continuous", dim)) and one mechanism per node. The main methods are fit (all mechanisms, node by node), update (fold in new data without retraining), query / query_batch, sample, log_prob and intervene.

Mechanisms (nbn.mechanisms) are the conditional distributions:

Node type Mechanisms
Discrete CategoricalTableMechanism (closed-form counts), NeuralCategoricalMechanism (MLP), SmoothedEmpiricalCategoricalMechanism, BinningCategoricalTable (continuous parents)
Continuous, parametric LinearGaussianMechanism (closed form), MDNMechanism (mixture density network), NormalizingFlowMechanism / ConditionalFlowMechanism ([neural] extra)
Continuous, non-parametric ConditionalKDEMechanism, KNNConditionalMechanism, FlexCodeMechanism
Fixed DeterministicMechanism, DiracGaussianMechanism

Inference engines (nbn.inference):

Engine Use
TensorVariableElimination Exact, log-domain, einsum-based VE for discrete networks; batched over evidence rows
LikelihoodWeightingEngine Batched importance sampling; works with any mechanism
HybridRouter (default) VE when every node is discrete and the treewidth is ≤ 25, otherwise likelihood weighting

Pass engine= to query / query_batch to override the default.

Gradients. log_prob and sample (including sample(n, do=...)) are differentiable with respect to the parameters, and parent values or do= values computed by your own nn.Module carry gradients back into it. query / query_batch are not differentiable (VE detaches at factor build; likelihood weighting runs under inference_mode). intervene() returns a deep copy for querying. To get gradients through an intervention, use sample(n, do=...).

Snapshotting. To revert an optimisation step, copy the state dict: snap = copy.deepcopy(mech.state_dict()). A plain state_dict() shares storage with the live parameters, so an optimiser step silently changes the snapshot too.

Hardware acceleration

device="auto" (the default for models, nbn-bench --device and YAML configs) picks CUDA, then MPS (Apple Silicon), then CPU. The results are the same on every backend, up to floating-point precision and Monte-Carlo noise.

On MPS, NBN works around the Metal backend's gaps for you:

  • No float64 on Metal. Sufficient statistics (counts, normal equations, weighted moments) accumulate in float32 on mps and in float64 elsewhere.
  • No linalg.lstsq kernel. The small closed-form Gaussian solves run on the CPU. They are [D_pa+1, D_pa+1] matrices, so this costs nothing.
  • Other missing kernels. import nbn sets PYTORCH_ENABLE_MPS_FALLBACK=1 on macOS unless you have set it. Any op a dependency needs that Metal lacks then runs on the CPU with a one-time warning instead of failing. Set it to 0 before importing to opt out.

When the GPU helps. GPUs pay off on large batches: many queries per query_batch call, large training sets, wide networks. A toy network like the quick start is often faster on device="cpu", because kernel-launch overhead dominates. CI runs the full test suite on Linux and on an Apple-Silicon macOS runner, where the device-parametrised tests use a real mps device.

Benchmark highlights

These numbers come from the paper-data run at tag v0.6c-d (RTX 4070 Laptop GPU, 5 seeds, networks of 10–1000 nodes). To reproduce them, see docs/BENCHMARKS.md.

Setting NBN Best external baseline Result
Inference, continuous linear-Gaussian, 10–1000 nodes nbn-lg-lw pgmpy predict 8–22× faster (0.72 s vs 8.5 s at n=1000); W₁ within 0.02 of pgmpy at every size
Inference, discrete, n=10 nbn-cat-ve pgmpy VE 75× faster (1.4 ms vs 108 ms)
Parameter learning, discrete, n ≥ 50 nbn-cat pgmpy MLE ~2.3× lower TV error (0.146 vs 0.340 at n=1000)
Parameter learning, continuous linear-Gaussian nbn-lg pgmpy Same accuracy (W₁ ≈ 0.083), 2× faster at n=1000
Hybrid networks, 10–1000 nodes nbn-hybrid — All 25 cells finished (W₁ ≈ 0.08–0.10); no external library in that run had an applicable hybrid baseline

Pyro's importance sampler was added as a hybrid baseline after that run. Numbers vary within Monte-Carlo noise across hardware.

Running the benchmarks

pip install -U "nbn[all]" && nbn-bench check-env          # must print "environment OK"
nbn-bench inference --config nbn/bench/configs/synthetic/smoke_tests/inference_smoke.yaml
nbn-bench plot results/benchmark_synthetic_smoke_<timestamp> --output-dir results/figures

The configs, metrics, the paper-scale runs (scripts/run_all_benchmarks.sh), the figures and how to reproduce the published numbers are documented in docs/BENCHMARKS.md.

Development

CONTRIBUTING.md covers the development setup, the test lanes, the device rules and the pull-request flow.

pip install -e ".[dev,all]"
pytest -n auto                        # fast gate (default: not slow/subprocess/network/perf/cuda)
pytest -n auto -m "slow or not slow"  # everything runnable here (network tests download bnlearn networks)
pytest -m "slow or subprocess"        # benchmark-harness lane: real cell workers, the CLI
pytest -m mps                         # Metal tests (macOS); -m cuda / -m multi_gpu on NVIDIA hosts
pytest -p no:xdist -m perf tests/perf # wall-clock assertions, on a quiet machine
pytest --nbmake examples/             # execute the example notebooks
ruff check nbn/ tests/ examples/ scripts/ && mypy nbn/core nbn/mechanisms

Repository layout:

nbn/                 the library: core/, mechanisms/, inference/, learning/, sampling/, update/
nbn/bench/           benchmark suite: runner, baseline adapters, configs, bnlearn data
tests/unit/          library tests (core, mechanisms, inference, learning, update)
tests/integration/   library end-to-end examples
tests/bench/         nbn.bench tests (bench/integration/: runner, CLI, bnlearn)
tests/perf/          wall-clock assertions (-m perf)
tests/data/          fixtures, incl. vendored bnlearn .bif networks
examples/            executed Jupyter notebooks: one per feature, then SCMs and probability sets
scripts/             run_all_benchmarks.sh and other helpers
docs/                Sphinx documentation site (https://nbn.readthedocs.io), incl. the benchmark guides

Releases are cut by pushing a v* tag; .github/workflows/publish.yml builds and uploads to PyPI. Known limitations and the roadmap are tracked in the issues.

Citing NBN

If you use NBN in your research, please cite the software:

@software{briglia_nbn,
  author  = {Briglia, Giovanni},
  title   = {{NeuralBayesianNetworks}: PyTorch-native Bayesian networks with neural mechanisms and GPU-batched inference},
  url     = {https://github.com/Giovannibriglia/NeuralBayesianNetworks},
  license = {Apache-2.0}
}

License

Apache License 2.0. See LICENSE.

Metadata

Release files for nbn 0.25.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nbn 0.25.0
File Size Uploaded
nbn-0.25.0.tar.gz 4.1 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for nbn 0.25.0
File Interpreter ABI Platform
nbn-0.25.0-py3-none-any.whl Python 3 none any Details

Total release size: 6.7 MB

Release files / nbn-0.25.0.tar.gz

Download URL nbn-0.25.0.tar.gz
Size 4.1 MB
Tags Source
SHA-256 checksum
How to use checksums
43d00d37efcae291d49b09372b41a5ffb2bc6a456dc4ebff017224f341440e96
BLAKE2b-256 checksum
How to use checksums
997a71884f879209609bfa0f0ddfb6454de9a5d0dc75b6a61561b052627c7928
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release files / nbn-0.25.0-py3-none-any.whl

Download URL nbn-0.25.0-py3-none-any.whl
Size 2.7 MB
Tags Python 3
SHA-256 checksum
How to use checksums
cc235ca607d5d353e6fe0ee637b2ede5ffd956b4118f11220e95feebfa10a686
BLAKE2b-256 checksum
How to use checksums
ad8458e87f07e6f718a1b57bd9d9c2e4b1978c62c650eec1270360fad9ea2b68
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.25.0 This release

2 release files

0.24.0

2 release files

0.23.0

2 release files

0.22.0

2 release files

0.21.0

2 release files

0.19.0

2 release files

0.18.3

2 release files

0.18.2

2 release files

0.18.1

2 release files

0.18.0

2 release files

0.17.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page